mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
Merge branch 'dev' into feat/switch-nx
Bring FlexLove launcher UI ("big ui moment"), iOS picker dismiss, and Metal updates into the Switch NX branch. Keep pack_love (with libs/), NX inbox/rescan paths, SwitchDiagnostics, and Save Editor PadInput; port Scan again labels into LauncherView.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+76
-32
@@ -12,6 +12,7 @@
|
||||
-- Vertical rhythm (scaled by Kit's height/768 factor, everything else flexes):
|
||||
-- 0 6px tri-colour version rail, identical to the launcher's
|
||||
-- 6 64px title bar identity, file chip, Save / Reload / Open / Close
|
||||
-- (104px when the bar reflows to two rows, #715)
|
||||
-- 70 66px tab rail 6 tab tiles + right-aligned validation pill
|
||||
-- 136 flex content one panel per tab, 20px gutters
|
||||
-- -38 38px status bar the last Ops message + the keyboard map
|
||||
@@ -412,16 +413,49 @@ local function drawFileChip(x, y, w, h)
|
||||
Kit.text("mono", shown, cx, y + (h - Kit.textHeight("mono")) / 2, PAL.detail)
|
||||
end
|
||||
|
||||
local function drawTitleBar(x, y, w, h)
|
||||
-- Measure the title bar's right-aligned action cluster. Shared by App.draw
|
||||
-- (which must size the bar before drawing it) and drawTitleBar, so the
|
||||
-- two-row decision and the layout can never disagree (#715).
|
||||
local function titleButtons()
|
||||
local s = Kit.scale
|
||||
local gap = 8 * s
|
||||
local b = {
|
||||
gap = gap,
|
||||
closeW = 22 * s + Kit.textWidth("button", S._quitArmed and "Discard?" or "Close"),
|
||||
openW = 22 * s + Kit.textWidth("button", "Open..."),
|
||||
reloadW = 22 * s + Kit.textWidth("button", "Reload"),
|
||||
}
|
||||
b.saveLabel, b.saveKind, b.saveEnabled = "SAVED", "disabled", false
|
||||
if not S.allowSave then
|
||||
b.saveLabel = "SAVE LOCKED"
|
||||
elseif S.dirty then
|
||||
b.saveLabel, b.saveKind, b.saveEnabled = "SAVE", "primary", true
|
||||
end
|
||||
b.saveW = 30 * s + Kit.textWidth("button", b.saveLabel)
|
||||
b.total = b.saveW + b.reloadW + b.openW + b.closeW + 3 * gap
|
||||
return b
|
||||
end
|
||||
|
||||
-- Whether the identity block plus the action cluster fit on one 64px row.
|
||||
-- When they do not, the bar reflows to two rows (identity + file chip above,
|
||||
-- buttons below) instead of shrinking or overlapping (#715).
|
||||
local function titleNeedsTwoRows(w)
|
||||
local s = Kit.scale
|
||||
return titleButtons().total > w - 2 * (22 * s) - (34 * s) - 10 * s
|
||||
end
|
||||
|
||||
local function drawTitleBar(x, y, w, h, twoRow)
|
||||
local s = Kit.scale
|
||||
local pad = 22 * s
|
||||
Theme.col(PAL.cardBorder, 0.22)
|
||||
love.graphics.rectangle("fill", x, y + h - 1, w, 1)
|
||||
|
||||
-- the identity row is the whole bar in one-row mode, the top slice in two
|
||||
local rowH = twoRow and (h * 0.55) or h
|
||||
local cx = x + pad
|
||||
-- SE badge, the same rounded-square chip shape the launcher's tabs use
|
||||
local badge = 34 * s
|
||||
local by = y + (h - badge) / 2
|
||||
local by = y + (rowH - badge) / 2
|
||||
Theme.gradRounded(cx, by, badge, badge, 9 * s, PAL.chipTop, PAL.chipBot, 1, 1)
|
||||
Kit.textCenter("tab", "SE", cx, by + (badge - Kit.textHeight("tab")) / 2, badge,
|
||||
{ 159, 180, 221 })
|
||||
@@ -432,34 +466,30 @@ local function drawTitleBar(x, y, w, h)
|
||||
-- this bar that must always be reachable, so on a phone the identity block,
|
||||
-- the version chip and the file chip are what yield. Measuring them last
|
||||
-- is why they used to paint straight through the buttons (#497).
|
||||
local b = titleButtons()
|
||||
local btnH = 38 * s
|
||||
local btnY = y + (h - btnH) / 2
|
||||
local btnY = twoRow and (y + rowH + (h - rowH - btnH) / 2) or (y + (h - btnH) / 2)
|
||||
local rightEdge = x + w - pad
|
||||
local gap = 8 * s
|
||||
local closeW = 22 * s + Kit.textWidth("button", "Close")
|
||||
local openW = 22 * s + Kit.textWidth("button", "Open...")
|
||||
local reloadW = 22 * s + Kit.textWidth("button", "Reload")
|
||||
local gap = b.gap
|
||||
local saveLabel, saveKind, saveEnabled = b.saveLabel, b.saveKind, b.saveEnabled
|
||||
|
||||
local saveLabel, saveKind, saveEnabled = "SAVED", "disabled", false
|
||||
if not S.allowSave then
|
||||
saveLabel = "SAVE LOCKED"
|
||||
elseif S.dirty then
|
||||
saveLabel, saveKind, saveEnabled = "SAVE", "primary", true
|
||||
end
|
||||
local saveW = 30 * s + Kit.textWidth("button", saveLabel)
|
||||
|
||||
local closeX = rightEdge - closeW
|
||||
local openX = closeX - gap - openW
|
||||
local reloadX = openX - gap - reloadW
|
||||
local saveX = reloadX - gap - saveW
|
||||
-- clamped at the left pad so a window narrower than the cluster overflows
|
||||
-- to the right (clipped) instead of stacking buttons on each other
|
||||
local saveX = math.max(x + pad, rightEdge - b.total)
|
||||
local reloadX = saveX + b.saveW + gap
|
||||
local openX = reloadX + b.reloadW + gap
|
||||
local closeX = openX + b.openW + gap
|
||||
-- the identity row yields to the buttons in one-row mode; in two-row mode
|
||||
-- the buttons are on their own row and the identity keeps the full width
|
||||
local identityLimit = twoRow and (rightEdge + 14 * s) or saveX
|
||||
|
||||
local wordH = Kit.textHeight("wordmark")
|
||||
local brandH = Kit.textHeight("brand")
|
||||
local blockY = y + (h - (wordH + 2 * s + brandH)) / 2
|
||||
local blockY = y + (rowH - (wordH + 2 * s + brandH)) / 2
|
||||
local wordW = math.max(
|
||||
Theme.spacedWidth(Kit.fonts.wordmark, "SAVE EDITOR", 2 * s),
|
||||
Theme.spacedWidth(Kit.fonts.brand, "GEN1RECOMP", 1 * s))
|
||||
if cx + wordW + 12 * s < saveX then
|
||||
if cx + wordW + 12 * s < identityLimit then
|
||||
love.graphics.setFont(Kit.fonts.wordmark)
|
||||
Theme.col(PAL.heading, 1)
|
||||
Theme.spaced(Kit.fonts.wordmark, "SAVE EDITOR", cx, blockY, 2 * s)
|
||||
@@ -477,8 +507,8 @@ local function drawTitleBar(x, y, w, h)
|
||||
local c = (S.version == "blue") and PAL.blue or PAL.red
|
||||
local cw = Kit.textWidth("chip", name) + 16 * s
|
||||
local ch = 22 * s
|
||||
local cy = y + (h - ch) / 2
|
||||
if cx + cw + 12 * s < saveX then
|
||||
local cy = y + (rowH - ch) / 2
|
||||
if cx + cw + 12 * s < identityLimit then
|
||||
Theme.col(c, 0.1)
|
||||
love.graphics.rectangle("fill", cx, cy, cw, ch, 6 * s, 6 * s)
|
||||
Theme.stroke(cx, cy, cw, ch, 6 * s, c, 0.5, 1)
|
||||
@@ -491,22 +521,22 @@ local function drawTitleBar(x, y, w, h)
|
||||
-- Save is the only green-filled control in the chrome; a corrupt load
|
||||
-- renders it steel with the reason parked in the status bar rather than
|
||||
-- hiding it (rule 3 of the design spec).
|
||||
if Kit.button(saveX, btnY, saveW, btnH, saveLabel,
|
||||
if Kit.button(saveX, btnY, b.saveW, btnH, saveLabel,
|
||||
{ kind = saveKind, enabled = saveEnabled or not S.allowSave,
|
||||
glow = S.dirty and S.allowSave and 0.6 or nil }) then
|
||||
App.save()
|
||||
end
|
||||
if Kit.button(reloadX, btnY, reloadW, btnH, "Reload") then App.reload() end
|
||||
if Kit.button(openX, btnY, openW, btnH, "Open...") then App.chooseAndOpen() end
|
||||
if Kit.button(closeX, btnY, closeW, btnH,
|
||||
if Kit.button(reloadX, btnY, b.reloadW, btnH, "Reload") then App.reload() end
|
||||
if Kit.button(openX, btnY, b.openW, btnH, "Open...") then App.chooseAndOpen() end
|
||||
if Kit.button(closeX, btnY, b.closeW, btnH,
|
||||
S._quitArmed and "Discard?" or "Close",
|
||||
{ kind = S._quitArmed and "danger" or "ghost" }) then
|
||||
App.close()
|
||||
end
|
||||
|
||||
local chipW = (saveX - 14 * s) - cx
|
||||
local chipW = (identityLimit - 14 * s) - cx
|
||||
if chipW > 80 * s then
|
||||
drawFileChip(cx, y + (h - 38 * s) / 2, chipW, 38 * s)
|
||||
drawFileChip(cx, y + (rowH - 38 * s) / 2, chipW, 38 * s)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -680,9 +710,18 @@ local function drawStatusBar(x, y, w, h)
|
||||
"+R reload . Esc clear selection . Close returns to the launcher")
|
||||
or (ctrl .. "+S save . " .. ctrl ..
|
||||
"+R reload . Esc clear selection . arrows pan map . wheel scrolls lists")
|
||||
-- The status message is the load-bearing half of this bar (every Ops verb
|
||||
-- narrates through it); the keyboard map is decoration. On a phone the
|
||||
-- two used to overlap because the hint was drawn unconditionally and the
|
||||
-- status ellipsized against a negative budget (#715), so now the hint only
|
||||
-- draws when the status still keeps a readable share of the bar.
|
||||
local hintW = Kit.textWidth("tiny", hint)
|
||||
Kit.textRight("tiny", hint, x + w - pad, y + (h - Kit.textHeight("tiny")) / 2, PAL.faint)
|
||||
local avail = w - 2 * pad - hintW - 14 * s
|
||||
if avail >= 120 * s then
|
||||
Kit.textRight("tiny", hint, x + w - pad, y + (h - Kit.textHeight("tiny")) / 2, PAL.faint)
|
||||
else
|
||||
avail = w - 2 * pad
|
||||
end
|
||||
Kit.text("mono", Kit.ellipsize("mono", S.status or "", avail), x + pad,
|
||||
y + (h - Kit.textHeight("mono")) / 2, PAL.detail)
|
||||
end
|
||||
@@ -716,12 +755,17 @@ function App.draw()
|
||||
Theme.field(width, height)
|
||||
|
||||
local railH = 6 * s
|
||||
local titleH = 64 * s
|
||||
-- The title bar reflows to two rows (identity above, buttons below) when
|
||||
-- the window is too narrow for both on one, instead of the buttons and the
|
||||
-- identity painting through each other (#715). The taller bar simply
|
||||
-- costs the content column height, which scrolls.
|
||||
local titleTwoRow = titleNeedsTwoRows(width)
|
||||
local titleH = (titleTwoRow and 104 or 64) * s
|
||||
local tabH = 66 * s
|
||||
local statusH = 38 * s
|
||||
|
||||
Theme.versionRail(0, 0, width, railH)
|
||||
drawTitleBar(0, railH, width, titleH)
|
||||
drawTitleBar(0, railH, width, titleH, titleTwoRow)
|
||||
drawTabRail(0, railH + titleH, width, tabH)
|
||||
|
||||
local contentY = railH + titleH + tabH
|
||||
|
||||
+189
-24
@@ -65,6 +65,18 @@ function Kit.beginFrame(mx, my, clicked, wheel)
|
||||
Kit.mouseX, Kit.mouseY = mx, my
|
||||
Kit.mouseClicked = clicked
|
||||
Kit.wheelY = wheel or 0
|
||||
-- Held-button state is polled, not evented: the editor is hosted both
|
||||
-- standalone and inside the launcher, and neither routes mousereleased
|
||||
-- here. Touch drag scrolling (#715) rides this poll, so it works in both
|
||||
-- hosts without new plumbing. The stub has no love.mouse.isDown; a frame
|
||||
-- without it simply has no drags.
|
||||
local down = false
|
||||
if love and love.mouse and love.mouse.isDown then
|
||||
down = love.mouse.isDown(1) and true or false
|
||||
end
|
||||
Kit.mouseDown = down
|
||||
if not down then Kit._drag = nil end
|
||||
Kit.resetClip()
|
||||
if love and love.timer and love.timer.getTime then
|
||||
Kit.time = love.timer.getTime()
|
||||
end
|
||||
@@ -83,11 +95,23 @@ end
|
||||
-- (720x1560) is TALLER than the desktop reference and barely half as wide, so
|
||||
-- a height-only scale drew a 1.6x desktop layout into a 720px window and every
|
||||
-- right-aligned cluster in the chrome landed on top of the block to its left.
|
||||
-- The layout needs roughly 1000 logical px of width, so the window now pays
|
||||
-- for both axes. Every desktop and landscape size still lands on the height
|
||||
-- term, which is why they stay pixel-identical to before.
|
||||
--
|
||||
-- The #497 answer was to shrink the whole layout down to fit the width
|
||||
-- (floor 0.62), which #715 showed is its own failure: a small window got a
|
||||
-- complete but unreadably tiny desktop layout, and the panels still assumed
|
||||
-- their columns fit. Shrink-to-fit is gone. The scale now never dips below
|
||||
-- 0.9, so text and the 26px tap targets stay readable everywhere, and a
|
||||
-- narrow window is answered by REFLOW instead: every panel compares its real
|
||||
-- pixel width against what its columns need (Party/Boxes/Items stack their
|
||||
-- cards, Dex/Events drop grid columns, the chrome wraps its button row) and
|
||||
-- whatever no longer fits vertically scrolls through Kit.scroll /
|
||||
-- Kit.scrollPixels. The width term survives only to keep a portrait phone
|
||||
-- from inflating to the 1.6 cap its height alone would buy: 640 real px is
|
||||
-- the narrowest the single-row chrome fits at scale 1. Desktop sizes
|
||||
-- (width >= 640 * height / 768) still land on the height term, so they stay
|
||||
-- pixel-identical to before.
|
||||
function Kit.layout(width, height)
|
||||
local s = Theme.clamp(math.min(width / 1000, height / 768), 0.62, 1.6)
|
||||
local s = Theme.clamp(math.min(width / 640, height / 768), 0.9, 1.6)
|
||||
local key = ("%dx%d"):format(width, height)
|
||||
if Kit._fontKey ~= key then
|
||||
Kit._fontKey = key
|
||||
@@ -128,11 +152,36 @@ function Kit.blur()
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- hit testing
|
||||
-- A widget inside a scrolled clip region can sit at coordinates outside the
|
||||
-- visible rect (#715: stacked panels scroll in pixels), so the active clip
|
||||
-- bounds the hit: what the user cannot see cannot take the tap.
|
||||
function Kit.hit(x, y, w, h)
|
||||
local c = Kit._clipRect
|
||||
if c and not (Kit.mouseX >= c.x and Kit.mouseX <= c.x + c.w
|
||||
and Kit.mouseY >= c.y and Kit.mouseY <= c.y + c.h) then
|
||||
return false
|
||||
end
|
||||
return Kit.mouseX >= x and Kit.mouseX <= x + w
|
||||
and Kit.mouseY >= y and Kit.mouseY <= y + h
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------ layout audit
|
||||
-- #715 reflow tests: when a test sets Kit.audit to a table, every control
|
||||
-- that could take a click this frame appends its rect (plus the clip that
|
||||
-- bounds it), so a window-size sweep can assert that no two controls
|
||||
-- overlap and none escapes the window. Shielded widgets are skipped: under
|
||||
-- a modal they cannot take the tap, and the modal legitimately covers them.
|
||||
Kit.audit = nil
|
||||
|
||||
local function audit(class, x, y, w, h, label)
|
||||
local a = Kit.audit
|
||||
if not a or Kit.blockClicks then return end
|
||||
local c = Kit._clipRect
|
||||
a[#a + 1] = { class = class, x = x, y = y, w = w, h = h,
|
||||
label = tostring(label or ""),
|
||||
clip = c and { x = c.x, y = c.y, w = c.w, h = c.h } or nil }
|
||||
end
|
||||
|
||||
function Kit.hover(x, y, w, h)
|
||||
return Kit.hit(x, y, w, h)
|
||||
end
|
||||
@@ -224,6 +273,7 @@ end
|
||||
-- true when the row was clicked this frame.
|
||||
function Kit.row(x, y, w, h, selected, accent, r)
|
||||
r = r or 12 * Kit.scale
|
||||
audit("row", x, y, w, h, "row")
|
||||
if not G then return Kit.press(x, y, w, h) end
|
||||
accent = accent or PAL.green
|
||||
if selected then Theme.glow(x, y, w, h, r, accent, 0.45) end
|
||||
@@ -280,6 +330,9 @@ local KINDS = {
|
||||
function Kit.button(x, y, w, h, label, opts)
|
||||
opts = opts or {}
|
||||
local enabled = opts.enabled ~= false
|
||||
-- disabled buttons audit too: rule 3 keeps them visible, so they still
|
||||
-- must not paint over a neighbour (#715)
|
||||
audit("control", x, y, w, h, label)
|
||||
local kind = KINDS[enabled and (opts.kind or "ghost") or "disabled"]
|
||||
local r = opts.radius or 10 * Kit.scale
|
||||
local hot = enabled and Kit.hover(x, y, w, h)
|
||||
@@ -327,6 +380,7 @@ end
|
||||
-- A pill toggle (badges, dex SEEN/OWN, event sub-tabs). `on` colours it;
|
||||
-- returns true when clicked.
|
||||
function Kit.chip(x, y, w, h, label, on, onColor, offColor)
|
||||
audit("control", x, y, w, h, label)
|
||||
local c = on and (onColor or PAL.green) or (offColor or PAL.steel)
|
||||
if G then
|
||||
local r = 6 * Kit.scale
|
||||
@@ -367,6 +421,7 @@ end
|
||||
-- routes love.textinput / love.keypressed in through Kit.textinput /
|
||||
-- Kit.keypressed. Returns the (possibly edited) value; the caller stores it.
|
||||
function Kit.textfield(id, x, y, w, h, value, placeholder)
|
||||
audit("control", x, y, w, h, id)
|
||||
value = tostring(value or "")
|
||||
if Kit.press(x, y, w, h) then Kit.focus = id end
|
||||
local focused = (Kit.focus == id)
|
||||
@@ -428,8 +483,12 @@ function Kit.pager(x, y, w, offset, total, perPage)
|
||||
local shown = math.min(perPage, math.max(0, total - offset))
|
||||
local label = ("%d-%d of %d"):format(total > 0 and offset + 1 or 0,
|
||||
offset + shown, total)
|
||||
Kit.text("mono", label, x + 2 * bw + 20 * Kit.scale,
|
||||
y + (h - Kit.textHeight("mono")) / 2, PAL.caption)
|
||||
-- the counter clips to the width the caller granted: a panel parking a
|
||||
-- button on the pager line passes a reduced w and the text yields instead
|
||||
-- of running underneath it (#715)
|
||||
local labelX = x + 2 * bw + 20 * Kit.scale
|
||||
Kit.text("mono", Kit.ellipsize("mono", label, math.max(0, x + w - labelX)),
|
||||
labelX, y + (h - Kit.textHeight("mono")) / 2, PAL.caption)
|
||||
return offset, h
|
||||
end
|
||||
|
||||
@@ -443,37 +502,143 @@ end
|
||||
-- panel under an open species picker would scroll through the modal.
|
||||
local SCROLL_ROWS = 3
|
||||
|
||||
function Kit.scroll(x, y, w, h, offset, total, perPage)
|
||||
-- `step` is optional and exists for grids: a 4-column dex page must move in
|
||||
-- multiples of 4 or the columns shear. Lists leave it nil and keep the old
|
||||
-- behaviour bit for bit (wheel notch = 3 rows, drag = 1 row per row height).
|
||||
function Kit.scroll(x, y, w, h, offset, total, perPage, step)
|
||||
local maxOffset = math.max(0, (total or 0) - (perPage or 0))
|
||||
offset = Theme.clamp(offset or 0, 0, maxOffset)
|
||||
if Kit.blockClicks or (Kit.wheelY or 0) == 0 then return offset end
|
||||
if Kit.blockClicks then return offset end
|
||||
|
||||
-- Touch drag (#715): a phone has no wheel and the pagers are small
|
||||
-- targets, so a held pointer dragging vertically over the list body
|
||||
-- scrolls it. The drag is keyed to the rect it started in and follows the
|
||||
-- pointer even once it leaves, like every native scroll view; the press
|
||||
-- frame itself still dispatches as a click, which is the pre-existing
|
||||
-- press-on-down contract, so a tap keeps selecting rows.
|
||||
local dragStep = math.max(1, step or 1)
|
||||
if Kit.mouseDown and maxOffset > 0 and h > 0 and (perPage or 0) > 0 then
|
||||
local key = math.floor(x) .. ":" .. math.floor(y)
|
||||
local d = Kit._drag
|
||||
if not d and Kit.hit(x, y, w, h) then
|
||||
Kit._drag = { key = key, startY = Kit.mouseY, base = offset }
|
||||
elseif d and d.key == key then
|
||||
local visRows = math.max(1, math.floor(perPage / dragStep))
|
||||
local rowPx = math.max(1, h / visRows)
|
||||
local moved = math.floor((d.startY - Kit.mouseY) / rowPx + 0.5) * dragStep
|
||||
offset = Theme.clamp(d.base + moved, 0, maxOffset)
|
||||
end
|
||||
end
|
||||
|
||||
if (Kit.wheelY or 0) == 0 then return offset end
|
||||
if not Kit.hit(x, y, w, h) then return offset end
|
||||
-- LOVE reports wheel-up as positive y; up moves the window toward the top
|
||||
-- of the list, which is a smaller offset.
|
||||
local rows = math.max(1, math.min(SCROLL_ROWS, perPage or SCROLL_ROWS))
|
||||
local step = (Kit.wheelY > 0) and -rows or rows
|
||||
local rows = step or math.max(1, math.min(SCROLL_ROWS, perPage or SCROLL_ROWS))
|
||||
local notch = (Kit.wheelY > 0) and -rows or rows
|
||||
Kit.wheelY = 0
|
||||
return Theme.clamp(offset + step, 0, maxOffset)
|
||||
return Theme.clamp(offset + notch, 0, maxOffset)
|
||||
end
|
||||
|
||||
-- Clip drawing to a rect (list bodies). No-ops under the headless stub.
|
||||
function Kit.pushClip(x, y, w, h)
|
||||
-- A compact mobile viewport can leave a panel with no room for a list.
|
||||
-- LÖVE rejects negative scissor dimensions, so treat an exhausted clip
|
||||
-- region as empty instead of passing invalid geometry through to it.
|
||||
Kit._clipActive = G and G.setScissor ~= nil
|
||||
if Kit._clipActive then
|
||||
if w <= 0 or h <= 0 then
|
||||
G.setScissor(0, 0, 0, 0)
|
||||
else
|
||||
G.setScissor(math.floor(x), math.floor(y), math.ceil(w), math.ceil(h))
|
||||
-- Pixel-unit sibling of Kit.scroll for a whole stacked card column (#715
|
||||
-- reflow): `offset` is a pixel offset into `contentH` pixels of laid-out
|
||||
-- content shown through an `h`-pixel viewport. Same three rules as
|
||||
-- Kit.scroll (pointer-inside only, notch consumed, shielded by
|
||||
-- Kit.blockClicks), same drag contract (a tap still dispatches as a click).
|
||||
-- Call it AFTER the content so any inner Kit.scroll list gets first claim on
|
||||
-- a wheel notch or drag that lands over it.
|
||||
function Kit.scrollPixels(x, y, w, h, offset, contentH)
|
||||
local maxOffset = math.max(0, (contentH or 0) - math.max(0, h))
|
||||
offset = Theme.clamp(offset or 0, 0, maxOffset)
|
||||
if Kit.blockClicks then return offset end
|
||||
|
||||
if Kit.mouseDown and maxOffset > 0 and h > 0 then
|
||||
local key = "px:" .. math.floor(x) .. ":" .. math.floor(y)
|
||||
local d = Kit._drag
|
||||
if not d and Kit.hit(x, y, w, h) then
|
||||
Kit._drag = { key = key, startY = Kit.mouseY, base = offset }
|
||||
elseif d and d.key == key then
|
||||
offset = Theme.clamp(d.base + (d.startY - Kit.mouseY), 0, maxOffset)
|
||||
end
|
||||
end
|
||||
|
||||
if (Kit.wheelY or 0) == 0 then return offset end
|
||||
if not Kit.hit(x, y, w, h) then return offset end
|
||||
local notch = 48 * Kit.scale
|
||||
local delta = (Kit.wheelY > 0) and -notch or notch
|
||||
Kit.wheelY = 0
|
||||
return Theme.clamp(offset + delta, 0, maxOffset)
|
||||
end
|
||||
|
||||
-- Thin overlay scrollbar along the right edge of a list body, drawn after
|
||||
-- the rows so it stays visible. Pure indicator (the drag above and the
|
||||
-- pager are the controls): on a phone the old layout looked "stuck" because
|
||||
-- nothing said the list continued past the fold (#715).
|
||||
function Kit.scrollbar(x, y, w, h, offset, total, perPage)
|
||||
if not G then return end
|
||||
total, perPage = total or 0, perPage or 0
|
||||
if total <= perPage or h <= 0 or perPage <= 0 then return end
|
||||
local bw = 3 * Kit.scale
|
||||
local bx = x + w - bw
|
||||
Theme.col(PAL.cardBorder, 0.22)
|
||||
G.rectangle("fill", bx, y, bw, h, bw / 2, bw / 2)
|
||||
local maxOffset = total - perPage
|
||||
local th = math.max(18 * Kit.scale, h * perPage / total)
|
||||
local ty = y + (h - th) * (Theme.clamp(offset or 0, 0, maxOffset) / maxOffset)
|
||||
Theme.col(PAL.blue, 0.55)
|
||||
G.rectangle("fill", bx, ty, bw, th, bw / 2, bw / 2)
|
||||
end
|
||||
|
||||
-- Clip drawing to a rect (list bodies, scrolled cards). A stack since #715:
|
||||
-- a stacked panel scrolls its whole column inside one clip and the lists
|
||||
-- inside it push their own, so pushes nest by intersecting with the rect
|
||||
-- above and a pop restores that rect rather than clearing the scissor. The
|
||||
-- tracked rect also bounds Kit.hit, so a widget scrolled out of view is
|
||||
-- inert instead of taking taps aimed at whatever is drawn where it left.
|
||||
-- Under the headless stub the scissor is a no-op but the rect tracking (and
|
||||
-- so the hit fencing) still runs.
|
||||
local clipStack = {}
|
||||
|
||||
local function applyClip(rect)
|
||||
Kit._clipRect = rect
|
||||
if not (G and G.setScissor) then return end
|
||||
if not rect then
|
||||
G.setScissor()
|
||||
elseif rect.w <= 0 or rect.h <= 0 then
|
||||
-- A compact mobile viewport can leave a panel with no room for a list.
|
||||
-- LÖVE rejects negative scissor dimensions, so treat an exhausted clip
|
||||
-- region as empty instead of passing invalid geometry through to it.
|
||||
G.setScissor(0, 0, 0, 0)
|
||||
else
|
||||
G.setScissor(math.floor(rect.x), math.floor(rect.y),
|
||||
math.ceil(rect.w), math.ceil(rect.h))
|
||||
end
|
||||
end
|
||||
|
||||
function Kit.pushClip(x, y, w, h)
|
||||
local prev = clipStack[#clipStack]
|
||||
local x2, y2 = x + math.max(0, w), y + math.max(0, h)
|
||||
if prev then
|
||||
x, y = math.max(x, prev.x), math.max(y, prev.y)
|
||||
x2 = math.min(x2, prev.x + prev.w)
|
||||
y2 = math.min(y2, prev.y + prev.h)
|
||||
end
|
||||
local rect = { x = x, y = y, w = math.max(0, x2 - x), h = math.max(0, y2 - y) }
|
||||
clipStack[#clipStack + 1] = rect
|
||||
applyClip(rect)
|
||||
end
|
||||
|
||||
function Kit.popClip()
|
||||
if Kit._clipActive and G and G.setScissor then G.setScissor() end
|
||||
Kit._clipActive = false
|
||||
clipStack[#clipStack] = nil
|
||||
applyClip(clipStack[#clipStack])
|
||||
end
|
||||
|
||||
-- A pcall-ed draw that raised mid-clip must not leak the stack into later
|
||||
-- frames (every hit test would stay fenced to the dead rect), so the frame
|
||||
-- boundary clears it.
|
||||
function Kit.resetClip()
|
||||
for i = #clipStack, 1, -1 do clipStack[i] = nil end
|
||||
applyClip(nil)
|
||||
end
|
||||
|
||||
return Kit
|
||||
|
||||
@@ -271,6 +271,45 @@ function Ops.closeSpeciesPicker(S, Kit)
|
||||
if Kit and Kit.blur then Kit.blur() end
|
||||
end
|
||||
|
||||
-- The Boxes panel's add flow rides the same picker (#715): instead of
|
||||
-- silently dropping catalog entry #1 into the box, "+ Add mon here" and the
|
||||
-- dashed empty cells open the picker in box-add mode, and the committed
|
||||
-- species goes through Ops.boxAddSpecies below. No selection is required:
|
||||
-- the target is the box, not a mon.
|
||||
function Ops.openBoxAddPicker(S, Kit)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= BoxesMod.CAPACITY then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
||||
end
|
||||
S.speciesPicker = { query = "", offset = 0, opened = true, mode = "box-add" }
|
||||
if Kit then Kit.focus = "species-picker" end -- soft keyboard rises (#529)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Commit half of the box-add picker. Builds the mon exactly the way
|
||||
-- Ops.partyAdd does (MonOps.create at Lv5, owned by the save's player), so a
|
||||
-- box mon and a party mon born in the editor are indistinguishable.
|
||||
function Ops.boxAddSpecies(S, id)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= BoxesMod.CAPACITY then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
||||
end
|
||||
if not Ops.speciesUsable(S, id) then
|
||||
return Ops.say(S, ("%s has no usable base stats, cannot add it")
|
||||
:format(tostring(id)))
|
||||
end
|
||||
local mon = MonOps.create(S.data, id, 5)
|
||||
mon.ot = S.save.player.name
|
||||
mon.otId = S.save.player.id
|
||||
table.insert(box, mon)
|
||||
S.selectedBoxSlot = #box
|
||||
S.editingMon = mon
|
||||
return Ops.mark(S, ("Added %s Lv5 to box %d slot %d")
|
||||
:format(id, S.selectedBox, #box))
|
||||
end
|
||||
|
||||
function Ops.setDv(S, mon, key, value)
|
||||
if not mon then return false end
|
||||
local want = clamp(math.floor(value), 0, 15)
|
||||
@@ -362,6 +401,9 @@ function Ops.selectBoxSlot(S, index)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Kept for the keyboard/test path; the Boxes panel itself goes through the
|
||||
-- species picker (Ops.openBoxAddPicker -> Ops.boxAddSpecies) so the user
|
||||
-- chooses what lands in the box instead of always getting catalog entry #1.
|
||||
function Ops.boxAdd(S)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= BoxesMod.CAPACITY then
|
||||
|
||||
@@ -40,15 +40,20 @@ function State.new()
|
||||
|
||||
-- party / inspector
|
||||
selectedParty = 1,
|
||||
partyOffset = 0, -- roster scroll position (#715)
|
||||
inspectorScroll = 0, -- MonEditor body pixel scroll (#715)
|
||||
editingMon = nil, -- reference into party or a box
|
||||
-- species picker overlay: nil when closed, otherwise { query, offset }.
|
||||
-- Modal in the literal sense -- App shields every widget under it for the
|
||||
-- frame -- because Kit hit-tests without a z-order (#541).
|
||||
-- species picker overlay: nil when closed, otherwise { query, offset }
|
||||
-- plus mode = "box-add" when it is adding to a box instead of changing a
|
||||
-- species (Ops.openBoxAddPicker). Modal in the literal sense -- App
|
||||
-- shields every widget under it for the frame -- because Kit hit-tests
|
||||
-- without a z-order (#541).
|
||||
speciesPicker = nil,
|
||||
|
||||
-- boxes
|
||||
selectedBox = 1,
|
||||
selectedBoxSlot = 1,
|
||||
dockOffset = 0, -- party dock scroll position (#715)
|
||||
|
||||
-- items
|
||||
itemQuery = "",
|
||||
@@ -58,6 +63,7 @@ function State.new()
|
||||
itemPickOffset = 0, -- scroll position in the ADD ITEM list (#595)
|
||||
bagOffset = 0,
|
||||
pcOffset = 0,
|
||||
itemsScroll = 0, -- stacked-layout pixel scroll (#715)
|
||||
|
||||
-- events
|
||||
eventsTab = "flags",
|
||||
|
||||
@@ -227,7 +227,12 @@ end
|
||||
function Theme.ellipsize(font, text, maxW)
|
||||
text = tostring(text or "")
|
||||
if not font then return text end
|
||||
if maxW <= 0 or font:getWidth(text) <= maxW then return text end
|
||||
-- A non-positive budget means "nothing fits", not "everything fits": the
|
||||
-- old early-out returned the whole string, which is how a phone-width
|
||||
-- status bar ended up with two lines of text stacked on top of each other
|
||||
-- (#715).
|
||||
if maxW <= 0 then return "" end
|
||||
if font:getWidth(text) <= maxW then return text end
|
||||
local ell = "..."
|
||||
local ew = font:getWidth(ell)
|
||||
while #text > 0 and font:getWidth(text) + ew > maxW do
|
||||
@@ -239,7 +244,8 @@ end
|
||||
function Theme.ellipsizeLeft(font, text, maxW)
|
||||
text = tostring(text or "")
|
||||
if not font then return text end
|
||||
if maxW <= 0 or font:getWidth(text) <= maxW then return text end
|
||||
if maxW <= 0 then return "" end -- same rule as Theme.ellipsize (#715)
|
||||
if font:getWidth(text) <= maxW then return text end
|
||||
local ell = "..."
|
||||
local ew = font:getWidth(ell)
|
||||
while #text > 0 and font:getWidth(text) + ew > maxW do
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
-- Boxes panel: the 12 PC boxes as a real grid rather than the old 20-row
|
||||
-- text list. Three columns:
|
||||
-- text list. Three columns at full width:
|
||||
-- box strip which boxes have room, so you can see where a deposit lands
|
||||
-- the grid 5 x 4 = Boxes.CAPACITY, empty cells are dashed and clickable
|
||||
-- the grid empty cells are dashed and clickable
|
||||
-- party dock the deposit source and withdraw target, both in one place
|
||||
--
|
||||
-- Selecting a slot points S.editingMon at it, so switching to the Party tab
|
||||
-- keeps inspecting the same mon.
|
||||
--
|
||||
-- #715 reflow: the three columns need about 900 real px. Below that the
|
||||
-- panel stacks the grid over the party dock at full width and drops the box
|
||||
-- strip (the grid header's < > steppers and Box N counter cover its job).
|
||||
-- The grid's column count adapts to the width it actually gets, the dock's
|
||||
-- roster scrolls, and the action labels shorten when the row is tight, so
|
||||
-- no button ever paints over its neighbour.
|
||||
--
|
||||
-- Adding a mon opens the same searchable species picker the inspector uses
|
||||
-- (see SpeciesPicker.lua / Ops.openBoxAddPicker): the picked species lands
|
||||
-- in the selected box as a Lv5 mon built by the same MonOps path partyAdd
|
||||
-- uses, so its stats, exp and moves are consistent.
|
||||
|
||||
local BoxesMod = require("src.pokemon.Boxes")
|
||||
local PartyMod = require("src.pokemon.Party")
|
||||
@@ -16,24 +28,10 @@ local PAL = Theme.PAL
|
||||
local M = {}
|
||||
|
||||
local COLS = 5
|
||||
local ROWS = math.ceil(BoxesMod.CAPACITY / COLS)
|
||||
|
||||
function M.draw(S, Kit, x, y, w, h)
|
||||
local function drawStrip(S, Kit, boxes, x, y, stripW, h)
|
||||
local s = Kit.scale
|
||||
local gap = 20 * s
|
||||
local pad = 16 * s
|
||||
|
||||
S.selectedBox = Ops.clamp(S.selectedBox or 1, 1, BoxesMod.COUNT)
|
||||
S.save.currentBox = S.selectedBox
|
||||
local boxes = Ops.boxes(S)
|
||||
local box = boxes[S.selectedBox]
|
||||
|
||||
local stripW = math.max(150 * s, math.min(200 * s, w * 0.16))
|
||||
local dockW = math.max(220 * s, math.min(300 * s, w * 0.22))
|
||||
local gridX = x + stripW + gap
|
||||
local gridW = w - stripW - dockW - 2 * gap
|
||||
|
||||
-- ------------------------------------------------------------ box strip
|
||||
Kit.card(x, y, stripW, h)
|
||||
Kit.caption(x + pad, y + pad, ("BOXES . %d"):format(BoxesMod.COUNT))
|
||||
local stripTop = y + pad + Kit.textHeight("caption") + 10 * s
|
||||
@@ -56,8 +54,10 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
Kit.meter(mx, ry + (bRowH - 5 * s) / 2, 44 * s, 5 * s,
|
||||
fill / BoxesMod.CAPACITY * 100, fill >= BoxesMod.CAPACITY and PAL.yellow or PAL.blue)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- the grid
|
||||
local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
local s = Kit.scale
|
||||
Kit.card(gridX, y, gridW, h)
|
||||
local gpad = 18 * s
|
||||
local gx = gridX + gpad
|
||||
@@ -78,17 +78,59 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
Ops.stepBox(S, 1)
|
||||
end
|
||||
|
||||
-- Bottom action row, measured before it is drawn (#715): full labels when
|
||||
-- they fit side by side, short verbs when they do not, so Withdraw / Add /
|
||||
-- Release can never stack on each other the way the fixed offsets did.
|
||||
local actH = 34 * s
|
||||
local actY = y + h - gpad - actH
|
||||
local wdLabel, addLabel = "Withdraw to party", "+ Add mon here"
|
||||
local relLabel = Ops.armLabel(S, "box-release", "Release")
|
||||
local function widths()
|
||||
return Kit.textWidth("small", wdLabel) + 22 * s,
|
||||
Kit.textWidth("small", addLabel) + 22 * s,
|
||||
Kit.textWidth("small", relLabel) + 22 * s
|
||||
end
|
||||
local wdW, addW, relW = widths()
|
||||
if wdW + addW + relW + 20 * s > ginner then
|
||||
wdLabel, addLabel = "Withdraw", "+ Add"
|
||||
wdW, addW, relW = widths()
|
||||
end
|
||||
if Kit.button(gx, actY, wdW, actH, wdLabel,
|
||||
{ font = "small", radius = 9 * s,
|
||||
enabled = #S.save.party < PartyMod.MAX }) then
|
||||
Ops.withdraw(S)
|
||||
end
|
||||
if Kit.button(gx + wdW + 10 * s, actY, addW, actH, addLabel,
|
||||
{ font = "small", radius = 9 * s,
|
||||
enabled = #box < BoxesMod.CAPACITY }) then
|
||||
Ops.openBoxAddPicker(S, Kit)
|
||||
end
|
||||
if Kit.button(gx + ginner - relW, actY, relW, actH, relLabel,
|
||||
{ kind = "danger", font = "small", radius = 9 * s }) then
|
||||
Ops.release(S)
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- the grid
|
||||
local gridTop = y + gpad + headH + 14 * s
|
||||
local gridH = actY - 14 * s - gridTop
|
||||
local cellGap = 10 * s
|
||||
local cellW = (ginner - cellGap * (COLS - 1)) / COLS
|
||||
local cellH = math.min((gridH - cellGap * (ROWS - 1)) / ROWS, 110 * s)
|
||||
-- Columns adapt to the real width (#715): a cell needs ~86px before its
|
||||
-- name reads, so a narrow card gets fewer, taller-stacked columns instead
|
||||
-- of five slivers.
|
||||
local cols = math.max(2, math.min(COLS,
|
||||
math.floor((ginner + cellGap) / (86 * s + cellGap))))
|
||||
local rows = math.ceil(BoxesMod.CAPACITY / cols)
|
||||
local cellW = math.max(0, (ginner - cellGap * (cols - 1)) / cols)
|
||||
-- floor at Kit's 26px tap target so a short window shrinks the cells but
|
||||
-- never inverts them (#715); overflow clips inside the grid body rather
|
||||
-- than running over the action row, and the clip fences the hit tests
|
||||
local cellH = math.max(26 * s,
|
||||
math.min((gridH - cellGap * (rows - 1)) / rows, 110 * s))
|
||||
|
||||
Kit.pushClip(gx, gridTop, ginner, gridH)
|
||||
for i = 1, BoxesMod.CAPACITY do
|
||||
local cc = (i - 1) % COLS
|
||||
local cr = math.floor((i - 1) / COLS)
|
||||
local cc = (i - 1) % cols
|
||||
local cr = math.floor((i - 1) / cols)
|
||||
local bx = gx + cc * (cellW + cellGap)
|
||||
local by = gridTop + cr * (cellH + cellGap)
|
||||
local mon = box[i]
|
||||
@@ -104,7 +146,8 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
Kit.ellipsize("mono", mon.species, cellW - 12 * s), bx,
|
||||
by + cellH / 2 - Kit.textHeight("mono") / 2, cellW, PAL.text)
|
||||
else
|
||||
-- empty slots are dashed and clickable: clicking one adds a mon there
|
||||
-- empty slots are dashed and clickable: clicking one opens the species
|
||||
-- picker to add a mon there
|
||||
Theme.col(PAL.cardBorder, Kit.hover(bx, by, cellW, cellH) and 0.6 or 0.32)
|
||||
Theme.dashed(bx, by, cellW, cellH, 11 * s, 6 * s, 5 * s)
|
||||
Kit.text("micro", tostring(i), bx + 10 * s, by + 8 * s, PAL.faint)
|
||||
@@ -112,31 +155,16 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
cellW, PAL.faint)
|
||||
if Kit.press(bx, by, cellW, cellH) then
|
||||
S.selectedBoxSlot = math.min(i, #box + 1)
|
||||
Ops.boxAdd(S)
|
||||
Ops.openBoxAddPicker(S, Kit)
|
||||
end
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
end
|
||||
|
||||
local wdW = 170 * s
|
||||
if Kit.button(gx, actY, wdW, actH, "Withdraw to party",
|
||||
{ font = "small", radius = 9 * s,
|
||||
enabled = #S.save.party < PartyMod.MAX }) then
|
||||
Ops.withdraw(S)
|
||||
end
|
||||
if Kit.button(gx + wdW + 10 * s, actY, 140 * s, actH, "+ Add mon here",
|
||||
{ font = "small", radius = 9 * s,
|
||||
enabled = #box < BoxesMod.CAPACITY }) then
|
||||
Ops.boxAdd(S)
|
||||
end
|
||||
local relW = 110 * s
|
||||
if Kit.button(gx + ginner - relW, actY, relW, actH,
|
||||
Ops.armLabel(S, "box-release", "Release"),
|
||||
{ kind = "danger", font = "small", radius = 9 * s }) then
|
||||
Ops.release(S)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------- party dock
|
||||
local dx = gridX + gridW + gap
|
||||
local function drawDock(S, Kit, dx, y, dockW, h)
|
||||
local s = Kit.scale
|
||||
local pad = 16 * s
|
||||
Kit.card(dx, y, dockW, h)
|
||||
Kit.caption(dx + pad, y + pad, "PARTY DOCK")
|
||||
Kit.textRight("mono", ("%d/%d"):format(#S.save.party, PartyMod.MAX),
|
||||
@@ -144,35 +172,71 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local dTop = y + pad + Kit.textHeight("caption") + 10 * s
|
||||
local dInner = dockW - 2 * pad
|
||||
local dRowH = 34 * s
|
||||
for i, mon in ipairs(S.save.party) do
|
||||
local ry = dTop + (i - 1) * (dRowH + 7 * s)
|
||||
if Kit.row(dx + pad, ry, dInner, dRowH, S.editingMon == mon, PAL.green, 9 * s) then
|
||||
Ops.selectParty(S, i)
|
||||
end
|
||||
local lv = ("Lv%d"):format(mon.level)
|
||||
local lvW = Kit.textWidth("tiny", lv)
|
||||
Kit.textRight("tiny", lv, dx + pad + dInner - 10 * s,
|
||||
ry + (dRowH - Kit.textHeight("tiny")) / 2, PAL.caption)
|
||||
Kit.text("mono", Kit.ellipsize("mono", mon.species, dInner - 30 * s - lvW),
|
||||
dx + pad + 10 * s, ry + (dRowH - Kit.textHeight("mono")) / 2, PAL.text)
|
||||
end
|
||||
local dGap = 7 * s
|
||||
|
||||
-- Deposit is pinned to the card bottom and the roster scrolls above it
|
||||
-- (#715): six party rows used to be laid out unconditionally and the
|
||||
-- button drawn below them, which on a short card walked both straight out
|
||||
-- of the card.
|
||||
local depH = 36 * s
|
||||
local depY = y + h - pad - depH
|
||||
local listH = math.max(0, depY - 10 * s - dTop)
|
||||
|
||||
if #S.save.party == 0 then
|
||||
Kit.emptyBox(dx + pad, dTop, dInner, 70 * s, "Party is empty.")
|
||||
Kit.emptyBox(dx + pad, dTop, dInner, math.min(listH, 70 * s), "Party is empty.")
|
||||
else
|
||||
local visible = math.max(1, math.floor((listH + dGap) / (dRowH + dGap)))
|
||||
S.dockOffset = Kit.scroll(dx + pad, dTop, dInner, listH,
|
||||
S.dockOffset or 0, #S.save.party, visible)
|
||||
Kit.pushClip(dx + pad, dTop, dInner, listH)
|
||||
for i = 1, visible do
|
||||
local slot = S.dockOffset + i
|
||||
local mon = S.save.party[slot]
|
||||
if not mon then break end
|
||||
local ry = dTop + (i - 1) * (dRowH + dGap)
|
||||
if Kit.row(dx + pad, ry, dInner, dRowH, S.editingMon == mon, PAL.green, 9 * s) then
|
||||
Ops.selectParty(S, slot)
|
||||
end
|
||||
local lv = ("Lv%d"):format(mon.level)
|
||||
local lvW = Kit.textWidth("tiny", lv)
|
||||
Kit.textRight("tiny", lv, dx + pad + dInner - 10 * s,
|
||||
ry + (dRowH - Kit.textHeight("tiny")) / 2, PAL.caption)
|
||||
Kit.text("mono", Kit.ellipsize("mono", mon.species, dInner - 30 * s - lvW),
|
||||
dx + pad + 10 * s, ry + (dRowH - Kit.textHeight("mono")) / 2, PAL.text)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(dx + pad, dTop, dInner, listH,
|
||||
S.dockOffset, #S.save.party, math.max(1, math.floor((listH + dGap) / (dRowH + dGap))))
|
||||
end
|
||||
|
||||
local depY = dTop + math.max(#S.save.party, 2) * (dRowH + 7 * s) + 6 * s
|
||||
if Kit.button(dx + pad, depY, dInner, 36 * s, "Deposit selected slot",
|
||||
if Kit.button(dx + pad, depY, dInner, depH, "Deposit selected slot",
|
||||
{ kind = "accent", font = "small", radius = 9 * s,
|
||||
enabled = #S.save.party > 0 }) then
|
||||
Ops.deposit(S)
|
||||
end
|
||||
local noteY = depY + 36 * s + 10 * s
|
||||
local noteH = y + h - pad - noteY
|
||||
if noteH > Kit.textHeight("tiny") * 2 then
|
||||
Kit.textCenter("tiny",
|
||||
"Deposit fills the current box first, then the next box with room, and " ..
|
||||
"the status bar says where the mon landed.",
|
||||
dx + pad, noteY, dInner, PAL.caption)
|
||||
end
|
||||
|
||||
function M.draw(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
local gap = 20 * s
|
||||
|
||||
S.selectedBox = Ops.clamp(S.selectedBox or 1, 1, BoxesMod.COUNT)
|
||||
S.save.currentBox = S.selectedBox
|
||||
local boxes = Ops.boxes(S)
|
||||
local box = boxes[S.selectedBox]
|
||||
|
||||
if w < 900 * s then
|
||||
-- stacked (#715): grid over dock, strip dropped (see the header comment)
|
||||
local dockH = Theme.clamp(h * 0.38, 140 * s, 320 * s)
|
||||
drawGrid(S, Kit, box, x, y, w, h - dockH - gap)
|
||||
drawDock(S, Kit, x, y + h - dockH, w, dockH)
|
||||
else
|
||||
local stripW = math.max(150 * s, math.min(200 * s, w * 0.16))
|
||||
local dockW = math.max(220 * s, math.min(300 * s, w * 0.22))
|
||||
local gridW = w - stripW - dockW - 2 * gap
|
||||
drawStrip(S, Kit, boxes, x, y, stripW, h)
|
||||
drawGrid(S, Kit, box, x + stripW + gap, y, gridW, h)
|
||||
drawDock(S, Kit, x + w - dockW, y, dockW, h)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ local PAL = Theme.PAL
|
||||
|
||||
local M = {}
|
||||
|
||||
local COLS = 4
|
||||
-- Grid columns adapt to the card width: four at the design size, fewer on a
|
||||
-- phone so the name and the two chips stay readable instead of shearing into
|
||||
-- each other (#715). 180 logical px is the narrowest a row reads at.
|
||||
local MAX_COLS = 4
|
||||
local function colsFor(inner, s)
|
||||
return math.max(1, math.min(MAX_COLS, math.floor(inner / (180 * s))))
|
||||
end
|
||||
|
||||
function M.draw(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
@@ -33,36 +39,60 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local headW = math.max(Kit.captionWidth("POKEDEX"),
|
||||
Kit.textWidth("headline", ("%d / %d owned"):format(owned, total)))
|
||||
|
||||
-- bulk actions, laid out from the right edge inward
|
||||
-- bulk actions, measured first (#715): at full width they sit right-aligned
|
||||
-- on the headline; on a narrower card they take rows of their own below it
|
||||
-- and FLOW, wrapping to further rows when even one is too narrow, so the
|
||||
-- cluster can never paint over the headline or over itself.
|
||||
local actH = 34 * s
|
||||
local actY = y + pad + (headH - actH) / 2
|
||||
local buttons = {
|
||||
{ label = "Own party + boxes", kind = "ghost", fn = Ops.dexStamp },
|
||||
{ label = "See all", kind = "accent", fn = Ops.dexSeeAll },
|
||||
{ label = "Own all", kind = "good", fn = Ops.dexOwnAll },
|
||||
{ label = Ops.armLabel(S, "dex-clear", "Wipe dex"), kind = "danger",
|
||||
fn = Ops.dexClear },
|
||||
}
|
||||
local rightEdge = cx + inner
|
||||
local clearLabel = Ops.armLabel(S, "dex-clear", "Wipe dex")
|
||||
local clearW = Kit.textWidth("small", clearLabel) + 32 * s
|
||||
rightEdge = rightEdge - clearW
|
||||
if Kit.button(rightEdge, actY, clearW, actH, clearLabel,
|
||||
{ kind = "danger", font = "small", radius = 9 * s }) then
|
||||
Ops.dexClear(S)
|
||||
local clusterW = -10 * s
|
||||
for _, b in ipairs(buttons) do
|
||||
clusterW = clusterW + 10 * s + Kit.textWidth("small", b.label) + 32 * s
|
||||
end
|
||||
for i = #buttons, 1, -1 do
|
||||
local b = buttons[i]
|
||||
local bw = Kit.textWidth("small", b.label) + 32 * s
|
||||
rightEdge = rightEdge - 10 * s - bw
|
||||
if Kit.button(rightEdge, actY, bw, actH, b.label,
|
||||
{ kind = b.kind, font = "small", radius = 9 * s }) then
|
||||
b.fn(S)
|
||||
local ownRow = clusterW > inner - headW - 24 * s
|
||||
local actRows = 1
|
||||
local rightEdge = cx + inner
|
||||
if not ownRow then
|
||||
local actY = y + pad + (headH - actH) / 2
|
||||
for i = #buttons, 1, -1 do
|
||||
local b = buttons[i]
|
||||
local bw = Kit.textWidth("small", b.label) + 32 * s
|
||||
rightEdge = rightEdge - bw
|
||||
if Kit.button(rightEdge, actY, bw, actH, b.label,
|
||||
{ kind = b.kind, font = "small", radius = 9 * s }) then
|
||||
b.fn(S)
|
||||
end
|
||||
rightEdge = rightEdge - 10 * s
|
||||
end
|
||||
rightEdge = rightEdge + 10 * s
|
||||
else
|
||||
local bx = cx
|
||||
local by = y + pad + headH + 10 * s
|
||||
for _, b in ipairs(buttons) do
|
||||
local bw = Kit.textWidth("small", b.label) + 32 * s
|
||||
if bx > cx and bx + bw > cx + inner then
|
||||
bx = cx
|
||||
by = by + actH + 8 * s
|
||||
actRows = actRows + 1
|
||||
end
|
||||
if Kit.button(bx, by, bw, actH, b.label,
|
||||
{ kind = b.kind, font = "small", radius = 9 * s }) then
|
||||
b.fn(S)
|
||||
end
|
||||
bx = bx + bw + 10 * s
|
||||
end
|
||||
end
|
||||
|
||||
-- the two completion meters fill whatever the header leaves between the
|
||||
-- headline and the button cluster
|
||||
-- headline and the button cluster (the full line, when the cluster wrapped)
|
||||
local meterX = cx + headW + 24 * s
|
||||
local meterW = rightEdge - 24 * s - meterX
|
||||
local meterW = (ownRow and cx + inner or rightEdge) - 24 * s - meterX
|
||||
if meterW > 120 * s then
|
||||
local my = y + pad
|
||||
Kit.text("tiny", "SEEN", meterX, my, PAL.caption)
|
||||
@@ -77,23 +107,32 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
|
||||
-- --------------------------------------------------------- species grid
|
||||
local cols = colsFor(inner, s)
|
||||
local pagerH = 30 * s
|
||||
local pagerY = y + h - pad - pagerH
|
||||
local gridTop = y + pad + headH + 18 * s
|
||||
+ (ownRow and actRows * (actH + 8 * s) + 2 * s or 0)
|
||||
local rowH = 38 * s
|
||||
local rowGap = 8 * s
|
||||
local colGap = 16 * s
|
||||
local colW = (inner - colGap * (COLS - 1)) / COLS
|
||||
local perCol = math.max(1, math.floor((pagerY - 12 * s - gridTop) / (rowH + rowGap)))
|
||||
local perPage = perCol * COLS
|
||||
local colW = (inner - colGap * (cols - 1)) / cols
|
||||
local gridH = pagerY - 12 * s - gridTop
|
||||
local perCol = math.max(1, math.floor(gridH / (rowH + rowGap)))
|
||||
local perPage = perCol * cols
|
||||
S.dexOffset = Ops.clamp(S.dexOffset or 0, 0, math.max(0, #species - perPage))
|
||||
-- wheel / touch drag move whole grid rows so the columns never shear (#715)
|
||||
S.dexOffset = Kit.scroll(cx, gridTop, inner, gridH, S.dexOffset,
|
||||
#species, perPage, cols)
|
||||
|
||||
local chipW = 46 * s
|
||||
local chipH = 22 * s
|
||||
-- clip the grid body so a too-short window clips the last partial row
|
||||
-- (and fences its hits) instead of drawing it over the pager (#715)
|
||||
Kit.pushClip(cx, gridTop, inner, gridH)
|
||||
for i = 1, math.min(perPage, #species - S.dexOffset) do
|
||||
local id = species[S.dexOffset + i]
|
||||
local ci = (i - 1) % COLS
|
||||
local ri = math.floor((i - 1) / COLS)
|
||||
local ci = (i - 1) % cols
|
||||
local ri = math.floor((i - 1) / cols)
|
||||
local rx = cx + ci * (colW + colGap)
|
||||
local ry = gridTop + ri * (rowH + rowGap)
|
||||
local isSeen = dex.seen[id] == true
|
||||
@@ -119,7 +158,9 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
Ops.dexOwned(S, id, not isOwned)
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
|
||||
Kit.scrollbar(cx, gridTop, inner, gridH, S.dexOffset, #species, perPage)
|
||||
S.dexOffset = Kit.pager(cx, pagerY, inner, S.dexOffset, #species, perPage)
|
||||
end
|
||||
|
||||
|
||||
@@ -102,19 +102,26 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local inner = w - 2 * pad
|
||||
|
||||
-- ------------------------------------------------------------ sub-tabs
|
||||
-- The pills flow left to right and WRAP when the card is too narrow to
|
||||
-- hold all four on one line (#715): a fixed row used to run the last pill
|
||||
-- past the card edge.
|
||||
local pillH = 32 * s
|
||||
local px = cx
|
||||
local px, py = cx, y + pad
|
||||
for _, t in ipairs(SUB_TABS) do
|
||||
local pw = Kit.textWidth("small", t.label) + 32 * s
|
||||
if px > cx and px + pw > cx + inner then
|
||||
px = cx
|
||||
py = py + pillH + 8 * s
|
||||
end
|
||||
local active = (S.eventsTab == t.id)
|
||||
Theme.col(PAL.rowBg, 0.6)
|
||||
love.graphics.rectangle("fill", px, y + pad, pw, pillH, pillH / 2, pillH / 2)
|
||||
Theme.stroke(px, y + pad, pw, pillH, pillH / 2,
|
||||
love.graphics.rectangle("fill", px, py, pw, pillH, pillH / 2, pillH / 2)
|
||||
Theme.stroke(px, py, pw, pillH, pillH / 2,
|
||||
active and PAL.blue or PAL.cardBorder, active and 0.8 or 0.24,
|
||||
active and 1.5 * s or 1)
|
||||
Kit.textCenter("small", t.label, px, y + pad + (pillH - Kit.textHeight("small")) / 2,
|
||||
Kit.textCenter("small", t.label, px, py + (pillH - Kit.textHeight("small")) / 2,
|
||||
pw, active and PAL.heading or PAL.muted)
|
||||
if Kit.press(px, y + pad, pw, pillH) then
|
||||
if Kit.press(px, py, pw, pillH) then
|
||||
S.eventsTab = t.id
|
||||
S.eventsOffset = 0
|
||||
Ops.disarm(S)
|
||||
@@ -123,12 +130,21 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
px = px + pw + 10 * s
|
||||
end
|
||||
|
||||
-- The filter shares the last pill row when there is room for at least a
|
||||
-- usable field beside the pills; on a narrow window it wraps onto its own
|
||||
-- row instead of painting over the last pill (#715).
|
||||
local clearW = 74 * s
|
||||
local fieldW = math.min(280 * s, math.max(140 * s, cx + inner - clearW - 10 * s - px - 10 * s))
|
||||
local filterY = py
|
||||
local availF = cx + inner - clearW - 10 * s - px - 10 * s
|
||||
if availF < 120 * s then
|
||||
filterY = py + pillH + 8 * s
|
||||
availF = inner - clearW - 10 * s
|
||||
end
|
||||
local fieldW = math.min(280 * s, math.max(120 * s, availF))
|
||||
local fieldX = cx + inner - clearW - 10 * s - fieldW
|
||||
S.eventFilter = Kit.textfield("event-filter", fieldX, y + pad, fieldW, pillH,
|
||||
S.eventFilter = Kit.textfield("event-filter", fieldX, filterY, fieldW, pillH,
|
||||
S.eventFilter, "filter keys...")
|
||||
if Kit.button(cx + inner - clearW, y + pad, clearW, pillH, "Clear",
|
||||
if Kit.button(cx + inner - clearW, filterY, clearW, pillH, "Clear",
|
||||
{ kind = "accent", font = "small", radius = 8 * s,
|
||||
enabled = S.eventFilter ~= "" }) then
|
||||
S.eventFilter = ""
|
||||
@@ -136,8 +152,9 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
Ops.say(S, "Filter cleared")
|
||||
end
|
||||
|
||||
local hintY = y + pad + pillH + 10 * s
|
||||
Kit.text("small", HINTS[S.eventsTab] or "", cx, hintY, PAL.caption)
|
||||
local hintY = filterY + pillH + 10 * s
|
||||
Kit.text("small", Kit.ellipsize("small", HINTS[S.eventsTab] or "", inner),
|
||||
cx, hintY, PAL.caption)
|
||||
|
||||
-- ---------------------------------------------------------- row grid
|
||||
local rows = buildRows(S)
|
||||
@@ -147,21 +164,31 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local rowH = 34 * s
|
||||
local rowGap = 8 * s
|
||||
local colGap = 20 * s
|
||||
local colW = (inner - colGap) / 2
|
||||
local perCol = math.max(1, math.floor((pagerY - 12 * s - gridTop) / (rowH + rowGap)))
|
||||
local perPage = perCol * 2
|
||||
-- two columns need ~460 logical px before the checkbox labels read; a
|
||||
-- phone gets one full-width column instead of two crushed ones (#715)
|
||||
local cols = (inner >= 460 * s) and 2 or 1
|
||||
local colW = (inner - colGap * (cols - 1)) / cols
|
||||
local gridH = pagerY - 12 * s - gridTop
|
||||
local perCol = math.max(1, math.floor(gridH / (rowH + rowGap)))
|
||||
local perPage = perCol * cols
|
||||
|
||||
S.eventsOffset = Ops.clamp(S.eventsOffset or 0, 0, math.max(0, #rows - perPage))
|
||||
-- wheel / touch drag move whole grid rows, same contract as the pager (#715)
|
||||
S.eventsOffset = Kit.scroll(cx, gridTop, inner, gridH, S.eventsOffset,
|
||||
#rows, perPage, cols)
|
||||
|
||||
if #rows == 0 then
|
||||
Kit.emptyBox(cx, gridTop, inner, 80 * s,
|
||||
Kit.emptyBox(cx, gridTop, inner, math.min(gridH, 80 * s),
|
||||
S.eventFilter ~= "" and "No key matches that filter."
|
||||
or "Nothing recorded here yet.")
|
||||
end
|
||||
-- clip the grid body: on a window too short for even one row the partial
|
||||
-- row clips (and its hit test is fenced) instead of covering the pager (#715)
|
||||
Kit.pushClip(cx, gridTop, inner, gridH)
|
||||
for i = 1, math.min(perPage, #rows - S.eventsOffset) do
|
||||
local row = rows[S.eventsOffset + i]
|
||||
local ci = (i - 1) % 2
|
||||
local ri = math.floor((i - 1) / 2)
|
||||
local ci = (i - 1) % cols
|
||||
local ri = math.floor((i - 1) / cols)
|
||||
local rx = cx + ci * (colW + colGap)
|
||||
local ry = gridTop + ri * (rowH + rowGap)
|
||||
if row.header then
|
||||
@@ -175,23 +202,30 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
if changed then row.set(newChecked) end
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
|
||||
S.eventsOffset = Kit.pager(cx, pagerY, inner, S.eventsOffset, #rows, perPage)
|
||||
Kit.scrollbar(cx, gridTop, inner, gridH, S.eventsOffset, #rows, perPage)
|
||||
|
||||
-- "Clear all" only makes sense for the two key tables the editor owns
|
||||
-- wholesale; flags and object toggles are cleared one row at a time.
|
||||
-- wholesale; flags and object toggles are cleared one row at a time. Its
|
||||
-- width is reserved BEFORE the pager draws, so the pager's counter yields
|
||||
-- to the button instead of running underneath it (#715).
|
||||
local clearKey = (S.eventsTab == "trainers" and "defeatedTrainers")
|
||||
or (S.eventsTab == "items" and "itemsTaken") or nil
|
||||
local clearBw = 0
|
||||
if clearKey then
|
||||
local label = (S.eventsTab == "trainers") and "Clear all trainers"
|
||||
or "Clear all items taken"
|
||||
local bw = Kit.textWidth("small", label) + 32 * s
|
||||
if Kit.button(cx + inner - bw, pagerY, bw, pagerH,
|
||||
clearBw = Kit.textWidth("small", label) + 32 * s
|
||||
if Kit.button(cx + inner - clearBw, pagerY, clearBw, pagerH,
|
||||
Ops.armLabel(S, "clear-" .. clearKey, label),
|
||||
{ kind = "danger", font = "small", radius = 8 * s }) then
|
||||
Ops.clearTable(S, clearKey, label:gsub("^Clear all ", ""))
|
||||
end
|
||||
clearBw = clearBw + 10 * s
|
||||
end
|
||||
S.eventsOffset = Kit.pager(cx, pagerY, inner - clearBw, S.eventsOffset,
|
||||
#rows, perPage)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
+192
-114
@@ -8,6 +8,12 @@
|
||||
-- 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")
|
||||
@@ -50,35 +56,30 @@ local function quantityRow(S, Kit, x, y, w, h, id, qty, selected, onMinus, onPlu
|
||||
return clicked
|
||||
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)
|
||||
-- ---------------------------------------------------------------- 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 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
|
||||
-- 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 = pad * 2 + Kit.textHeight("caption") + 8 * s
|
||||
local function moneyHeight(Kit, s, pad)
|
||||
return pad * 2 + Kit.textHeight("caption") + 8 * s
|
||||
+ Kit.textHeight("headline") + 10 * s + 30 * s
|
||||
Kit.card(x, y, leftW, moneyH)
|
||||
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 + leftW - pad - maxW, y + pad - 4 * s, maxW, 26 * s, "Max out",
|
||||
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 + moneyH - pad - 30 * s
|
||||
local mbW = (leftW - 2 * pad - 3 * 8 * s) / 4
|
||||
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,
|
||||
@@ -86,20 +87,54 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
Ops.addMoney(S, delta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------ picker
|
||||
local badgeIds = Ops.badgeIds(S)
|
||||
local badgeCols = 4
|
||||
local badgeRows = math.ceil(#badgeIds / badgeCols)
|
||||
local badgeH = pad * 2 + Kit.textHeight("caption") + 10 * s
|
||||
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
|
||||
local pickY = y + moneyH + gap
|
||||
local pickH = h - moneyH - badgeH - 2 * gap
|
||||
Kit.card(x, pickY, leftW, pickH)
|
||||
Kit.caption(x + pad, pickY + pad, "ADD ITEM")
|
||||
local qy = pickY + pad + Kit.textHeight("caption") + 8 * 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
|
||||
|
||||
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 qy = y + pad + Kit.textHeight("caption") + 8 * s
|
||||
local prevQuery = S.itemQuery or ""
|
||||
S.itemQuery = Kit.textfield("item-query", x + pad, qy, leftW - 2 * pad, 32 * s,
|
||||
S.itemQuery = Kit.textfield("item-query", x + pad, qy, w - 2 * pad, 32 * s,
|
||||
S.itemQuery or "", "search item ids...")
|
||||
-- a new query is a new list: keep the first hit on screen rather than
|
||||
-- leaving the view parked wherever the old result set had scrolled to
|
||||
@@ -116,7 +151,7 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
|
||||
local addH = 32 * s
|
||||
local addY = pickY + pickH - pad - addH
|
||||
local addY = y + h - pad - addH
|
||||
local listTop = qy + 32 * s + 10 * s
|
||||
local listBottom = addY - 10 * s
|
||||
local cRowH = 28 * s
|
||||
@@ -125,32 +160,36 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
-- #595: the wheel drives the same offset a pager would, so the whole
|
||||
-- catalog is reachable with the mouse alone. Kit.scroll clamps, which is
|
||||
-- also what pulls the view back when a narrower query shortens the list.
|
||||
S.itemPickOffset = Kit.scroll(x + pad, listTop, leftW - 2 * pad,
|
||||
S.itemPickOffset = Kit.scroll(x + pad, listTop, w - 2 * pad,
|
||||
listBottom - listTop, S.itemPickOffset or 0, #choices, visible)
|
||||
Kit.pushClip(x + pad, listTop, leftW - 2 * pad, listBottom - listTop)
|
||||
Kit.pushClip(x + pad, listTop, w - 2 * pad, listBottom - listTop)
|
||||
for i = 1, math.min(visible, #choices - S.itemPickOffset) do
|
||||
local id = choices[S.itemPickOffset + i]
|
||||
local ry = listTop + (i - 1) * (cRowH + cGap)
|
||||
if Kit.row(x + pad, ry, leftW - 2 * pad, cRowH, id == S.selectedItemId,
|
||||
if Kit.row(x + pad, ry, w - 2 * pad, cRowH, id == S.selectedItemId,
|
||||
PAL.green, 8 * s) then
|
||||
S.selectedItemId = id
|
||||
Ops.say(S, "Picked " .. id)
|
||||
end
|
||||
Kit.text("mono", Kit.ellipsize("mono", id, leftW - 2 * pad - 20 * s),
|
||||
Kit.text("mono", Kit.ellipsize("mono", id, w - 2 * pad - 20 * s),
|
||||
x + pad + 10 * s, ry + (cRowH - Kit.textHeight("mono")) / 2, PAL.text)
|
||||
end
|
||||
Kit.popClip()
|
||||
-- the drag/wheel offset is also made visible: on a phone the list looked
|
||||
-- bottomless-yet-stuck without an indicator (#715)
|
||||
Kit.scrollbar(x + pad, listTop, w - 2 * pad, listBottom - listTop,
|
||||
S.itemPickOffset, #choices, visible)
|
||||
-- the position counter rides the caption line, where it can never collide
|
||||
-- with the list body or the two add buttons below it
|
||||
if #choices > visible then
|
||||
Kit.textRight("micro", ("%d-%d of %d"):format(S.itemPickOffset + 1,
|
||||
math.min(S.itemPickOffset + visible, #choices), #choices),
|
||||
x + leftW - pad, pickY + pad, PAL.faint)
|
||||
x + w - pad, y + pad, PAL.faint)
|
||||
elseif #choices == 0 then
|
||||
Kit.text("mono", "no item matches", x + pad + 10 * s, listTop + 8 * s, PAL.faint)
|
||||
end
|
||||
|
||||
local halfW = (leftW - 2 * pad - 8 * s) / 2
|
||||
local halfW = (w - 2 * pad - 8 * s) / 2
|
||||
if Kit.button(x + pad, addY, halfW, addH, "-> Bag",
|
||||
{ font = "small", radius = 8 * s, enabled = S.selectedItemId ~= nil }) then
|
||||
Ops.addToBag(S, S.selectedItemId)
|
||||
@@ -159,102 +198,141 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
{ font = "small", radius = 8 * s, enabled = S.selectedItemId ~= nil }) then
|
||||
Ops.addToPc(S, S.selectedItemId)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------ badges
|
||||
local badgeY = y + h - badgeH
|
||||
Kit.card(x, badgeY, leftW, badgeH)
|
||||
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
|
||||
-- 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
|
||||
Kit.caption(x + pad, badgeY + pad, "BADGES")
|
||||
Kit.textRight("mono", ("%d/%d"):format(earned, #badgeIds), x + leftW - pad,
|
||||
badgeY + pad, PAL.caption)
|
||||
local bTop = badgeY + pad + Kit.textHeight("caption") + 10 * s
|
||||
local bW = (leftW - 2 * pad - (badgeCols - 1) * 7 * s) / badgeCols
|
||||
for i, id in ipairs(badgeIds) do
|
||||
local bc = (i - 1) % badgeCols
|
||||
local br = math.floor((i - 1) / badgeCols)
|
||||
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
|
||||
|
||||
-- --------------------------------------------------------------- bag
|
||||
local order = Bag.order(S.save)
|
||||
local capacity = Bag.capacity(S.data)
|
||||
Kit.card(bagX, y, listW, h)
|
||||
Kit.caption(bagX + pad, y + pad, "BAG")
|
||||
Kit.textRight("mono", ("%d/%d slots"):format(Bag.slots(S.save), capacity),
|
||||
bagX + listW - pad, y + pad, PAL.caption)
|
||||
local barY = y + pad + Kit.textHeight("caption") + 8 * s
|
||||
local slotFrac = Bag.slots(S.save) / capacity
|
||||
Kit.meter(bagX + pad, barY, listW - 2 * pad, 5 * s, slotFrac * 100,
|
||||
slotFrac >= 1 and PAL.yellow or PAL.blue)
|
||||
|
||||
local pagerH = 30 * s
|
||||
local pagerY = y + h - pad - pagerH
|
||||
local rowsTop = barY + 5 * s + 12 * s
|
||||
local rowH = 36 * s
|
||||
local rowGap = 6 * s
|
||||
local perPage = math.max(1, math.floor((pagerY - 12 * s - rowsTop) / (rowH + rowGap)))
|
||||
S.bagOffset = Ops.clamp(S.bagOffset or 0, 0, math.max(0, #order - perPage))
|
||||
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)
|
||||
S.bagOffset = Kit.scroll(bagX + pad, rowsTop, listW - 2 * pad,
|
||||
pagerY - 12 * s - rowsTop, S.bagOffset, #order, perPage)
|
||||
offset = Kit.scroll(x + pad, rowsTop, w - 2 * pad, listH, offset, #order, perPage)
|
||||
|
||||
if #order == 0 then
|
||||
Kit.emptyBox(bagX + pad, rowsTop, listW - 2 * pad, 70 * s, "Bag is empty.")
|
||||
Kit.emptyBox(x + pad, rowsTop, w - 2 * pad, math.min(listH, 70 * s), cfg.empty)
|
||||
end
|
||||
for i = 1, math.min(perPage, #order - S.bagOffset) do
|
||||
local id = order[S.bagOffset + i]
|
||||
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, bagX + pad, ry, listW - 2 * pad, rowH, id,
|
||||
S.save.inventory[id] or 0, id == S.selectedBagId,
|
||||
function() Ops.bagAdjust(S, id, -1) end,
|
||||
function() Ops.bagAdjust(S, id, 1) end,
|
||||
function() Ops.bagDrop(S, id) end) then
|
||||
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
|
||||
end
|
||||
S.bagOffset = Kit.pager(bagX + pad, pagerY, listW - 2 * pad, S.bagOffset,
|
||||
#order, perPage)
|
||||
end,
|
||||
adjust = function(id, d) Ops.bagAdjust(S, id, d) end,
|
||||
drop = function(id) Ops.bagDrop(S, id) end,
|
||||
})
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------- pc storage
|
||||
local function drawPc(S, Kit, x, y, w, h)
|
||||
local pcOrder = Ops.pcOrder(S)
|
||||
Kit.card(pcX, y, listW, h)
|
||||
Kit.caption(pcX + pad, y + pad, "PC STORAGE")
|
||||
Kit.textRight("mono", ("%d kinds"):format(#pcOrder), pcX + listW - pad,
|
||||
y + pad, PAL.caption)
|
||||
S.pcOffset = Ops.clamp(S.pcOffset or 0, 0, math.max(0, #pcOrder - perPage))
|
||||
S.pcOffset = Kit.scroll(pcX + pad, rowsTop, listW - 2 * pad,
|
||||
pagerY - 12 * s - rowsTop, S.pcOffset, #pcOrder, perPage)
|
||||
if #pcOrder == 0 then
|
||||
Kit.emptyBox(pcX + pad, rowsTop, listW - 2 * pad, 70 * s,
|
||||
"PC storage is empty. Items sent here have no slot cap.")
|
||||
end
|
||||
for i = 1, math.min(perPage, #pcOrder - S.pcOffset) do
|
||||
local id = pcOrder[S.pcOffset + i]
|
||||
local ry = rowsTop + (i - 1) * (rowH + rowGap)
|
||||
if quantityRow(S, Kit, pcX + pad, ry, listW - 2 * pad, rowH, id,
|
||||
S.save.pcItems[id] or 0, id == S.selectedPcId,
|
||||
function() Ops.pcAdjust(S, id, -1) end,
|
||||
function() Ops.pcAdjust(S, id, 1) end,
|
||||
function() Ops.pcDrop(S, id) end) then
|
||||
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
|
||||
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
|
||||
S.pcOffset = Kit.pager(pcX + pad, pagerY, listW - 2 * pad, S.pcOffset,
|
||||
#pcOrder, perPage)
|
||||
|
||||
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
|
||||
|
||||
@@ -156,16 +156,44 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
S.mapQuery = S.mapQuery or ""
|
||||
S.mapZoom = clampZoom(S.mapZoom or 2)
|
||||
|
||||
-- Column plan (#715). Side by side, the list and spawn cards claim ~470
|
||||
-- logical px before the viewport gets anything, and a portrait phone does
|
||||
-- not have it: the old layout answered by laying the viewport out at a
|
||||
-- negative width, which the scissor below rejected ("Can't set scissor
|
||||
-- with negative width and/or height") and took the whole editor down.
|
||||
-- Portrait now stacks the three cards vertically -- list, viewport, spawn
|
||||
-- inspector, each full width -- and every viewport dimension is clamped at
|
||||
-- zero so no window shape can reach the scissor with a negative rect.
|
||||
local listW = math.max(200 * s, math.min(260 * s, w * 0.2))
|
||||
local sideW = math.max(230 * s, math.min(300 * s, w * 0.22))
|
||||
local viewX = x + listW + gap
|
||||
local viewW = w - listW - sideW - 2 * gap
|
||||
local stacked = h > w or viewW < 260 * s
|
||||
local lr, vr, sr -- list / viewport / spawn card rects
|
||||
if stacked then
|
||||
local capH = Kit.textHeight("caption")
|
||||
-- list: caption, search field, three rows, pager, the goto button
|
||||
local listH = math.min(math.max(0, h * 0.32),
|
||||
2 * pad + capH + 8 * s + 32 * s + 10 * s + 3 * 30 * s + 10 * s
|
||||
+ 30 * s + 10 * s + 34 * s)
|
||||
-- spawns: caption, three 62px rows, the hint line
|
||||
local sideH = math.min(math.max(0, h * 0.34),
|
||||
2 * pad + capH + 12 * s + 3 * (62 * s + 8 * s) - 8 * s + 6 * s + 30 * s)
|
||||
lr = { x = x, y = y, w = w, h = listH }
|
||||
vr = { x = x, y = y + listH + gap, w = w,
|
||||
h = math.max(0, h - listH - sideH - 2 * gap) }
|
||||
sr = { x = x, y = y + h - sideH, w = w, h = sideH }
|
||||
else
|
||||
lr = { x = x, y = y, w = listW, h = h }
|
||||
vr = { x = x + listW + gap, y = y, w = math.max(0, viewW), h = h }
|
||||
sr = { x = x + w - sideW, y = y, w = sideW, h = h }
|
||||
end
|
||||
|
||||
-- --------------------------------------------------------- the map list
|
||||
Kit.card(x, y, listW, h)
|
||||
Kit.caption(x + pad, y + pad, "MAPS")
|
||||
local qy = y + pad + Kit.textHeight("caption") + 8 * s
|
||||
S.mapQuery = Kit.textfield("map-query", x + pad, qy, listW - 2 * pad, 32 * s,
|
||||
local listInner = lr.w - 2 * pad
|
||||
Kit.card(lr.x, lr.y, lr.w, lr.h)
|
||||
Kit.caption(lr.x + pad, lr.y + pad, "MAPS")
|
||||
local qy = lr.y + pad + Kit.textHeight("caption") + 8 * s
|
||||
S.mapQuery = Kit.textfield("map-query", lr.x + pad, qy, listInner, 32 * s,
|
||||
S.mapQuery, "search maps...")
|
||||
|
||||
local ids = {}
|
||||
@@ -176,31 +204,40 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
|
||||
local gotoH = 34 * s
|
||||
local gotoY = y + h - pad - gotoH
|
||||
local gotoY = lr.y + lr.h - pad - gotoH
|
||||
local pagerH = 30 * s
|
||||
local pagerY = gotoY - 10 * s - pagerH
|
||||
local listTop = qy + 32 * s + 10 * s
|
||||
local mRowH = 26 * s
|
||||
local mGap = 4 * s
|
||||
local perPage = math.max(1, math.floor((pagerY - 10 * s - listTop) / (mRowH + mGap)))
|
||||
local listBodyH = pagerY - 10 * s - listTop
|
||||
local perPage = math.max(1, math.floor(listBodyH / (mRowH + mGap)))
|
||||
S.mapListOffset = Ops.clamp(S.mapListOffset or 0, 0, math.max(0, #ids - perPage))
|
||||
-- wheel and touch drag reach the list too (#715): App routes the wheel to
|
||||
-- zoom on this tab, so the list rides Kit's drag path and the pager alone
|
||||
-- on desktop -- on a phone the drag is the difference between "stuck" and
|
||||
-- scrollable.
|
||||
S.mapListOffset = Kit.scroll(lr.x + pad, listTop, listInner, listBodyH,
|
||||
S.mapListOffset, #ids, perPage)
|
||||
|
||||
for i = 1, math.min(perPage, #ids - S.mapListOffset) do
|
||||
local id = ids[S.mapListOffset + i]
|
||||
local ry = listTop + (i - 1) * (mRowH + mGap)
|
||||
if Kit.row(x + pad, ry, listW - 2 * pad, mRowH, id == S.mapId, PAL.blue, 7 * s) then
|
||||
if Kit.row(lr.x + pad, ry, listInner, mRowH, id == S.mapId, PAL.blue, 7 * s) then
|
||||
MapBrowser.select(S, id)
|
||||
end
|
||||
Kit.text("tiny", Kit.ellipsize("tiny", id, listW - 2 * pad - 18 * s),
|
||||
x + pad + 9 * s, ry + (mRowH - Kit.textHeight("tiny")) / 2,
|
||||
Kit.text("tiny", Kit.ellipsize("tiny", id, listInner - 18 * s),
|
||||
lr.x + pad + 9 * s, ry + (mRowH - Kit.textHeight("tiny")) / 2,
|
||||
id == S.mapId and PAL.heading or PAL.muted)
|
||||
end
|
||||
if #ids == 0 then
|
||||
Kit.text("mono", "no map matches", x + pad + 9 * s, listTop + 8 * s, PAL.faint)
|
||||
Kit.text("mono", "no map matches", lr.x + pad + 9 * s, listTop + 8 * s, PAL.faint)
|
||||
end
|
||||
S.mapListOffset = Kit.pager(x + pad, pagerY, listW - 2 * pad, S.mapListOffset,
|
||||
Kit.scrollbar(lr.x + pad, listTop, listInner, listBodyH,
|
||||
S.mapListOffset, #ids, perPage)
|
||||
S.mapListOffset = Kit.pager(lr.x + pad, pagerY, listInner, S.mapListOffset,
|
||||
#ids, perPage)
|
||||
if Kit.button(x + pad, gotoY, listW - 2 * pad, gotoH, "Go to save location",
|
||||
if Kit.button(lr.x + pad, gotoY, listInner, gotoH, "Go to save location",
|
||||
{ font = "small", radius = 9 * s }) then
|
||||
MapBrowser.select(S, S.save.player.map)
|
||||
Ops.say(S, ("Jumped to %s (%d,%d)"):format(S.save.player.map,
|
||||
@@ -208,18 +245,18 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------- the viewport
|
||||
Kit.card(viewX, y, viewW, h)
|
||||
Kit.card(vr.x, vr.y, vr.w, vr.h)
|
||||
local vpad = 18 * s
|
||||
local vx0 = viewX + vpad
|
||||
local vinner = viewW - 2 * vpad
|
||||
local vx0 = vr.x + vpad
|
||||
local vinner = math.max(0, vr.w - 2 * vpad)
|
||||
local headH = 28 * s
|
||||
Kit.text("monoBig", tostring(S.mapId), vx0,
|
||||
y + vpad + (headH - Kit.textHeight("monoBig")) / 2, PAL.heading)
|
||||
vr.y + vpad + (headH - Kit.textHeight("monoBig")) / 2, PAL.heading)
|
||||
|
||||
local ok, map = pcall(MapLoader.load, S.data, S.mapId)
|
||||
if not ok then
|
||||
Kit.text("mono", "Failed to load map: " .. tostring(map), vx0,
|
||||
y + vpad + headH + 20 * s, PAL.red)
|
||||
vr.y + vpad + headH + 20 * s, PAL.red)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -227,38 +264,46 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
local oLabel = outdoor and "OUTDOOR" or "INDOOR"
|
||||
local oW = Kit.textWidth("tiny", oLabel) + 16 * s
|
||||
local oX = vx0 + Kit.textWidth("monoBig", tostring(S.mapId)) + 14 * s
|
||||
Theme.stroke(oX, y + vpad + (headH - 20 * s) / 2, oW, 20 * s, 6 * s,
|
||||
Theme.stroke(oX, vr.y + vpad + (headH - 20 * s) / 2, oW, 20 * s, 6 * s,
|
||||
PAL.cardBorder, 0.3, 1)
|
||||
Kit.textCenter("tiny", oLabel, oX,
|
||||
y + vpad + (headH - 20 * s) / 2 + (20 * s - Kit.textHeight("tiny")) / 2, oW,
|
||||
vr.y + vpad + (headH - 20 * s) / 2 + (20 * s - Kit.textHeight("tiny")) / 2, oW,
|
||||
outdoor and PAL.green or PAL.muted)
|
||||
|
||||
-- zoom cluster, right-aligned in the viewport header
|
||||
-- zoom cluster, right-aligned in the viewport header. The centre button
|
||||
-- is the one part with a long label; on a header too narrow to hold it
|
||||
-- beside the title it is dropped (its job is covered by the list's "Go to
|
||||
-- save location" plus the first-draw centering) rather than painted over
|
||||
-- the map name (#715).
|
||||
local centerW = 130 * s
|
||||
local zBtn = 32 * s
|
||||
local rightEdge = vx0 + vinner
|
||||
if Kit.button(rightEdge - centerW, y + vpad, centerW, headH, "Center on player",
|
||||
{ kind = "accent", font = "small", radius = 7 * s }) then
|
||||
if S.save.player.map == S.mapId then
|
||||
centerOn(S, S.save.player.x, S.save.player.y)
|
||||
Ops.say(S, "Centred on the player")
|
||||
else
|
||||
Ops.say(S, "Player isn't on this map")
|
||||
local zoomW = 2 * zBtn + 56 * s + 12 * s
|
||||
local showCenter = vinner >= zoomW + 10 * s + centerW + 160 * s
|
||||
if showCenter then
|
||||
if Kit.button(rightEdge - centerW, vr.y + vpad, centerW, headH, "Center on player",
|
||||
{ kind = "accent", font = "small", radius = 7 * s }) then
|
||||
if S.save.player.map == S.mapId then
|
||||
centerOn(S, S.save.player.x, S.save.player.y)
|
||||
Ops.say(S, "Centred on the player")
|
||||
else
|
||||
Ops.say(S, "Player isn't on this map")
|
||||
end
|
||||
end
|
||||
end
|
||||
local zx = rightEdge - centerW - 10 * s - (2 * zBtn + 56 * s + 12 * s)
|
||||
if Kit.stepper(zx, y + vpad, zBtn, headH, "-", { radius = 7 * s }) then
|
||||
local zx = rightEdge - (showCenter and (centerW + 10 * s) or 0) - zoomW
|
||||
if Kit.stepper(zx, vr.y + vpad, zBtn, headH, "-", { radius = 7 * s }) then
|
||||
S.mapZoom = clampZoom(S.mapZoom - 0.5)
|
||||
end
|
||||
Kit.textCenter("mono", ("%.2fx"):format(S.mapZoom), zx + zBtn + 6 * s,
|
||||
y + vpad + (headH - Kit.textHeight("mono")) / 2, 56 * s, PAL.muted)
|
||||
if Kit.stepper(zx + zBtn + 62 * s, y + vpad, zBtn, headH, "+", { radius = 7 * s }) then
|
||||
vr.y + vpad + (headH - Kit.textHeight("mono")) / 2, 56 * s, PAL.muted)
|
||||
if Kit.stepper(zx + zBtn + 62 * s, vr.y + vpad, zBtn, headH, "+", { radius = 7 * s }) then
|
||||
S.mapZoom = clampZoom(S.mapZoom + 0.5)
|
||||
end
|
||||
|
||||
local legendH = 22 * s
|
||||
local vy0 = y + vpad + headH + 12 * s
|
||||
local vh0 = (y + h - vpad - legendH - 10 * s) - vy0
|
||||
local vy0 = vr.y + vpad + headH + 12 * s
|
||||
local vh0 = math.max(0, (vr.y + vr.h - vpad - legendH - 10 * s) - vy0)
|
||||
S._mapViewW, S._mapViewH = vinner, vh0
|
||||
|
||||
-- First draw of a map: park the camera somewhere meaningful rather than at
|
||||
@@ -279,8 +324,11 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
Theme.stroke(vx0, vy0, vinner, vh0, 12 * s, PAL.cardBorder, 0.28, 1)
|
||||
|
||||
-- love_stub (headless tests) lacks push/pop/scale/scissor; skip the actual
|
||||
-- render there but keep all click/button logic below running.
|
||||
if love.graphics.push then
|
||||
-- render there but keep all click/button logic below running. The size
|
||||
-- guard is the #715 crash fix proper: an exhausted viewport (a window
|
||||
-- shorter or narrower than the chrome) renders nothing instead of handing
|
||||
-- LOVE a negative scissor rect.
|
||||
if love.graphics.push and vinner > 0 and vh0 > 0 then
|
||||
love.graphics.setScissor(math.floor(vx0), math.floor(vy0),
|
||||
math.ceil(vinner), math.ceil(vh0))
|
||||
love.graphics.push()
|
||||
@@ -292,6 +340,23 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
love.graphics.setScissor()
|
||||
end
|
||||
|
||||
-- Touch pan (#715): arrows/WASD and the wheel are desktop-only inputs, so
|
||||
-- a held pointer drags the camera directly. A plain tap still selects a
|
||||
-- cell via the click handling below; only movement while held pans.
|
||||
if Kit.mouseDown and not Kit.blockClicks
|
||||
and (S._mapDrag or Kit.hit(vx0, vy0, vinner, vh0)) then
|
||||
local d = S._mapDrag
|
||||
if not d then
|
||||
S._mapDrag = { mx = Kit.mouseX, my = Kit.mouseY,
|
||||
camX = S.mapCamX, camY = S.mapCamY }
|
||||
else
|
||||
S.mapCamX = d.camX - (Kit.mouseX - d.mx) / S.mapZoom
|
||||
S.mapCamY = d.camY - (Kit.mouseY - d.my) / S.mapZoom
|
||||
end
|
||||
elseif not Kit.mouseDown then
|
||||
S._mapDrag = nil
|
||||
end
|
||||
|
||||
-- click handling: warp cells jump the view, everything else selects
|
||||
if Kit.mouseClicked then
|
||||
local cx, cy = cellAtScreen(S, map, Kit, vx0, vy0, vinner, vh0)
|
||||
@@ -307,7 +372,7 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
|
||||
-- legend + the current selection readout
|
||||
local ly = y + h - vpad - legendH + 4 * s
|
||||
local ly = vr.y + vr.h - vpad - legendH + 4 * s
|
||||
local lx = vx0
|
||||
local legend = {
|
||||
{ PAL.blue, "warp", false },
|
||||
@@ -332,11 +397,11 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
vx0 + vinner, ly, PAL.caption)
|
||||
|
||||
-- ------------------------------------------------------ spawn inspector
|
||||
local sx0 = viewX + viewW + gap
|
||||
Kit.card(sx0, y, sideW, h)
|
||||
Kit.caption(sx0 + pad, y + pad, "SPAWN POINTS")
|
||||
local sTop = y + pad + Kit.textHeight("caption") + 12 * s
|
||||
local sInner = sideW - 2 * pad
|
||||
local sx0 = sr.x
|
||||
Kit.card(sx0, sr.y, sr.w, sr.h)
|
||||
Kit.caption(sx0 + pad, sr.y + pad, "SPAWN POINTS")
|
||||
local sTop = sr.y + pad + Kit.textHeight("caption") + 12 * s
|
||||
local sInner = sr.w - 2 * pad
|
||||
local player = S.save.player
|
||||
local out = S.save.lastOutdoor
|
||||
local heal = S.save.lastHeal
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
-- the Party and Boxes panels dock into (rule 1 of the design spec): the list
|
||||
-- stays visible while you edit, and Escape clears the selection rather than
|
||||
-- "closing a window".
|
||||
--
|
||||
-- #715 reflow: the inspector used to shrink its stat tiles and DV/move rows
|
||||
-- against a vertical budget, and past a point the rows still ran over the
|
||||
-- action buttons. Sizes are fixed at readable values now; when the card is
|
||||
-- too short for them the whole body scrolls (Kit.scrollPixels), and when it
|
||||
-- is too narrow for the DV | moves split the two columns stack. The clip
|
||||
-- over the card doubles as the hit fence, so a control scrolled out of view
|
||||
-- cannot take a stray tap.
|
||||
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
@@ -66,63 +74,12 @@ end
|
||||
-- and the differences between mons stay legible.
|
||||
local STAT_SCALE = 400
|
||||
|
||||
function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
-- The -5 -1 [Lv] +1 +5 stepper row plus the EXP readout, at (lx0, ly).
|
||||
local function drawLevelRow(S, Kit, mon, lx0, ly)
|
||||
local s = Kit.scale
|
||||
Kit.card(x, y, w, h)
|
||||
local mon = S.editingMon
|
||||
local pad = 18 * s
|
||||
if not mon then
|
||||
-- The inspector column is always drawn, so it explains itself rather
|
||||
-- than collapsing and reflowing the panel underneath it.
|
||||
local tw = math.min(w - 40 * s, 340 * s)
|
||||
Kit.textCenter("button",
|
||||
"Pick a slot on the left to inspect it. Every change here re-runs the " ..
|
||||
"Gen1 stat formulas, so HP and stats stay legal.",
|
||||
x + (w - tw) / 2, y + h / 2 - Kit.textHeight("button"), tw, PAL.muted)
|
||||
return
|
||||
end
|
||||
|
||||
local def = S.data.pokemon[mon.species]
|
||||
local cx, cy = x + pad, y + pad
|
||||
local inner = w - 2 * pad
|
||||
-- Backstop for a window too short for even the compacted rhythm below:
|
||||
-- nothing this panel draws may land outside its own card (#497). Party
|
||||
-- draws the inspector last, so no outer clip is lost by the pop at the end.
|
||||
Kit.pushClip(x, y, w, h)
|
||||
|
||||
-- ---------------------------------------------------------- header row
|
||||
local sprite = 96 * s
|
||||
MonEditor.drawSprite(S, Kit, mon.species, cx, cy, sprite)
|
||||
local hx = cx + sprite + 18 * s
|
||||
local hw = inner - sprite - 18 * s
|
||||
|
||||
Kit.text("title", mon.species, hx, cy, PAL.heading)
|
||||
local nameW = Kit.textWidth("title", mon.species)
|
||||
Kit.text("tiny", ("#%03d"):format(def and def.dex or 0), hx + nameW + 12 * s,
|
||||
cy + Kit.textHeight("title") - Kit.textHeight("tiny") - 2 * s, PAL.caption)
|
||||
|
||||
-- One control instead of a pair of arrows: cycling walked the catalog an
|
||||
-- entry at a time (151 taps to cross the dex) and ran a full MonOps
|
||||
-- recalculation on every step, including on records the Gen1 formulas
|
||||
-- cannot use, which is what crashed the editor (#541). This opens the
|
||||
-- searchable picker; the species name itself is a second, larger target.
|
||||
local pickH = 30 * s
|
||||
local pickW = math.min(150 * s, math.max(90 * s, hw * 0.6))
|
||||
local px = hx + hw - pickW
|
||||
local py = cy + (Kit.textHeight("title") - pickH) / 2
|
||||
local openPicker = Kit.button(px, py, pickW, pickH, "Change species",
|
||||
{ kind = "accent", font = "small", radius = 8 * s })
|
||||
if not openPicker then
|
||||
openPicker = Kit.press(hx, cy, math.max(0, px - hx - 10 * s),
|
||||
Kit.textHeight("title"))
|
||||
end
|
||||
if openPicker then Ops.openSpeciesPicker(S, Kit) end
|
||||
|
||||
-- level stepper: -5 -1 [Lv] +1 +5, matching MonOps.setLevel's 1..100 clamp
|
||||
local ly = cy + Kit.textHeight("title") + 14 * s
|
||||
local lh = 28 * s
|
||||
Kit.caption(hx, ly + (lh - Kit.textHeight("caption")) / 2, "LEVEL")
|
||||
local lx = hx + 52 * s
|
||||
Kit.caption(lx0, ly + (lh - Kit.textHeight("caption")) / 2, "LEVEL")
|
||||
local lx = lx0 + 52 * s
|
||||
local bw = 40 * s
|
||||
for _, d in ipairs({ { "-5", -5 }, { "-1", -1 } }) do
|
||||
if Kit.stepper(lx, ly, bw, lh, d[1], { font = "small", radius = 7 * s }) then
|
||||
@@ -141,52 +98,19 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
Kit.text("mono", ("EXP %d"):format(mon.exp or 0), lx + 6 * s,
|
||||
ly + (lh - Kit.textHeight("mono")) / 2, PAL.muted)
|
||||
return lh
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------- derived stats
|
||||
local statsY = cy + sprite + 18 * s
|
||||
Kit.caption(cx, statsY, "STATS . recalculated from level + DVs")
|
||||
statsY = statsY + Kit.textHeight("caption") + 10 * s
|
||||
local gap = 12 * s
|
||||
local cellW = (inner - gap * 4) / 5
|
||||
-- Everything below the header competes for one vertical budget. At the
|
||||
-- design size it is generous; in a 720px-tall window (a phone held
|
||||
-- sideways) it is not, and the DV / move rows used to run past the card and
|
||||
-- paint over the status bar (#497). Shrink the two flexible blocks -- the
|
||||
-- stat tiles and the DV / move rows -- instead of overflowing, with floors
|
||||
-- that keep every row the 26px target Kit's rule 6 promises. statsY is
|
||||
-- already past the STATS caption here, so only the DVs / MOVES caption is
|
||||
-- subtracted.
|
||||
local actH = 34 * s
|
||||
local rowGap = 8 * s
|
||||
local budget = (y + h - pad) - statsY - (Kit.textHeight("caption") + 10 * s)
|
||||
- 18 * s - actH - 4 * s
|
||||
local cellH = Theme.clamp(budget * 0.3, 46 * s, 68 * s)
|
||||
local rowH = Theme.clamp((budget - cellH) / 4 - rowGap, 26 * s, 34 * s)
|
||||
for i, st in ipairs(STAT_KEYS) do
|
||||
local bx = cx + (i - 1) * (cellW + gap)
|
||||
Theme.row(bx, statsY, cellW, cellH, 10 * s, 0.6)
|
||||
local value = (mon.stats and mon.stats[st.field]) or 0
|
||||
Kit.text("micro", st.key, bx + 12 * s, statsY + 10 * s, PAL.caption)
|
||||
Kit.text("stat", tostring(value), bx + 12 * s,
|
||||
statsY + 10 * s + Kit.textHeight("micro") + 4 * s, PAL.heading)
|
||||
Kit.meter(bx + 12 * s, statsY + cellH - 14 * s, cellW - 24 * s, 5 * s,
|
||||
value / STAT_SCALE * 100, PAL.blue)
|
||||
end
|
||||
|
||||
-- --------------------------------------------------- DVs | moves split
|
||||
local colY = statsY + cellH + 18 * s
|
||||
local colGap = 18 * s
|
||||
local colW = (inner - colGap) / 2
|
||||
local rightX = cx + colW + colGap
|
||||
|
||||
Kit.caption(cx, colY, "DVs")
|
||||
Kit.textRight("tiny", ("HP DV auto-derived . %d"):format(mon.dvs.hp or 0),
|
||||
cx + colW, colY, PAL.caption)
|
||||
Kit.caption(rightX, colY, "MOVES")
|
||||
Kit.textRight("tiny", "click a slot to cycle", rightX + colW, colY, PAL.caption)
|
||||
|
||||
local rowY = colY + Kit.textHeight("caption") + 10 * s
|
||||
-- Everything the level row needs in width, for the "does it fit beside the
|
||||
-- sprite" decision.
|
||||
local function levelRowWidth(Kit, mon)
|
||||
local s = Kit.scale
|
||||
return 52 * s + 2 * (40 * s + 8 * s) + 58 * s + 8 * s + 2 * (40 * s + 8 * s)
|
||||
+ 6 * s + Kit.textWidth("mono", ("EXP %d"):format(mon.exp or 0))
|
||||
end
|
||||
|
||||
local function drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
|
||||
local s = Kit.scale
|
||||
for i, key in ipairs(DV_KEYS) do
|
||||
local ry = rowY + (i - 1) * (rowH + rowGap)
|
||||
Theme.row(cx, ry, colW, rowH, 10 * s, 0.6)
|
||||
@@ -212,7 +136,10 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
Ops.setDv(S, mon, key, 15)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
|
||||
local s = Kit.scale
|
||||
for slot = 1, 4 do
|
||||
local ry = rowY + (slot - 1) * (rowH + rowGap)
|
||||
Theme.row(rightX, ry, colW, rowH, 10 * s, 0.6)
|
||||
@@ -239,16 +166,175 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
Ops.clearMove(S, mon, slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local actY = rowY + 4 * (rowH + rowGap) + 4 * s
|
||||
local actW = (colW - 10 * s) / 2
|
||||
if Kit.button(rightX, actY, actW, actH, "Reset to learnset",
|
||||
{ font = "small", radius = 9 * s }) then
|
||||
Ops.resetMoves(S, mon)
|
||||
function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
Kit.card(x, y, w, h)
|
||||
local mon = S.editingMon
|
||||
local pad = 18 * s
|
||||
if not mon then
|
||||
-- The inspector column is always drawn, so it explains itself rather
|
||||
-- than collapsing and reflowing the panel underneath it.
|
||||
local tw = math.min(w - 40 * s, 340 * s)
|
||||
Kit.textCenter("button",
|
||||
"Pick a slot on the left to inspect it. Every change here re-runs the " ..
|
||||
"Gen1 stat formulas, so HP and stats stay legal.",
|
||||
x + (w - tw) / 2, y + h / 2 - Kit.textHeight("button"), tw, PAL.muted)
|
||||
return
|
||||
end
|
||||
if Kit.button(rightX + actW + 10 * s, actY, actW, actH, "Full heal",
|
||||
{ kind = "good", font = "small", radius = 9 * s }) then
|
||||
Ops.healMon(S, mon)
|
||||
|
||||
local def = S.data.pokemon[mon.species]
|
||||
local inner = w - 2 * pad
|
||||
local capH = Kit.textHeight("caption")
|
||||
local titleH = Kit.textHeight("title")
|
||||
|
||||
-- Reflow decisions, all against real pixels (#715): the DV | moves split
|
||||
-- needs ~470px of card interior; below that the two stacks go one above
|
||||
-- the other. A narrow card also drops the sprite to 64px so the header
|
||||
-- text keeps room.
|
||||
local narrow = inner < 470 * s
|
||||
local sprite = (narrow and 64 or 96) * s
|
||||
local rowH = 30 * s
|
||||
local rowGap = 8 * s
|
||||
local cellH = 52 * s
|
||||
local actH = 34 * s
|
||||
|
||||
local hw = inner - sprite - 18 * s
|
||||
local levelInHeader = hw >= levelRowWidth(Kit, mon)
|
||||
local headerH
|
||||
if levelInHeader then
|
||||
headerH = math.max(sprite, titleH + 14 * s + 28 * s)
|
||||
else
|
||||
-- the level row does not fit beside the sprite: it drops below the
|
||||
-- header block at full card width instead of painting over the sprite
|
||||
headerH = math.max(sprite, titleH) + 12 * s + 28 * s
|
||||
end
|
||||
|
||||
local colRowsH = 4 * (rowH + rowGap) - rowGap
|
||||
local colsH
|
||||
if narrow then
|
||||
colsH = (capH + 10 * s + colRowsH) * 2 + 14 * s + 10 * s + actH
|
||||
else
|
||||
colsH = capH + 10 * s + colRowsH + 12 * s + actH
|
||||
end
|
||||
local contentH = pad + headerH + 18 * s
|
||||
+ capH + 10 * s + cellH + 18 * s
|
||||
+ colsH + pad
|
||||
|
||||
-- Called before the widgets so this frame already draws at the updated
|
||||
-- offset; any list-free card body is fair game for the drag (#715).
|
||||
S.inspectorScroll = Kit.scrollPixels(x, y, w, h, S.inspectorScroll, contentH)
|
||||
Kit.pushClip(x, y, w, h)
|
||||
local cx = x + pad
|
||||
local cy = y + pad - S.inspectorScroll
|
||||
|
||||
-- ---------------------------------------------------------- header row
|
||||
MonEditor.drawSprite(S, Kit, mon.species, cx, cy, sprite)
|
||||
local hx = cx + sprite + 18 * s
|
||||
|
||||
-- One control instead of a pair of arrows: cycling walked the catalog an
|
||||
-- entry at a time (151 taps to cross the dex) and ran a full MonOps
|
||||
-- recalculation on every step, including on records the Gen1 formulas
|
||||
-- cannot use, which is what crashed the editor (#541). This opens the
|
||||
-- searchable picker; the species name itself is a second, larger target.
|
||||
local pickH = 30 * s
|
||||
local pickW = math.min(150 * s, math.max(90 * s, hw * 0.6))
|
||||
local px = hx + hw - pickW
|
||||
local py = cy + (titleH - pickH) / 2
|
||||
|
||||
-- the species name yields to the button instead of running under it (#715)
|
||||
local name = Kit.ellipsize("title", mon.species, math.max(40 * s, px - hx - 12 * s))
|
||||
Kit.text("title", name, hx, cy, PAL.heading)
|
||||
local nameW = Kit.textWidth("title", name)
|
||||
if nameW + Kit.textWidth("tiny", "#000") + 12 * s < px - hx - 12 * s then
|
||||
Kit.text("tiny", ("#%03d"):format(def and def.dex or 0), hx + nameW + 12 * s,
|
||||
cy + titleH - Kit.textHeight("tiny") - 2 * s, PAL.caption)
|
||||
end
|
||||
|
||||
local openPicker = Kit.button(px, py, pickW, pickH, "Change species",
|
||||
{ kind = "accent", font = "small", radius = 8 * s })
|
||||
if not openPicker then
|
||||
openPicker = Kit.press(hx, cy, math.max(0, px - hx - 10 * s), titleH)
|
||||
end
|
||||
if openPicker then Ops.openSpeciesPicker(S, Kit) end
|
||||
|
||||
-- level stepper: -5 -1 [Lv] +1 +5, matching MonOps.setLevel's 1..100 clamp
|
||||
if levelInHeader then
|
||||
drawLevelRow(S, Kit, mon, hx, cy + titleH + 14 * s)
|
||||
else
|
||||
drawLevelRow(S, Kit, mon, cx, cy + math.max(sprite, titleH) + 12 * s)
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------- derived stats
|
||||
local statsY = cy + headerH + 18 * s
|
||||
Kit.caption(cx, statsY, "STATS . recalculated from level + DVs")
|
||||
statsY = statsY + capH + 10 * s
|
||||
local gap = 12 * s
|
||||
local cellW = (inner - gap * 4) / 5
|
||||
for i, st in ipairs(STAT_KEYS) do
|
||||
local bx = cx + (i - 1) * (cellW + gap)
|
||||
Theme.row(bx, statsY, cellW, cellH, 10 * s, 0.6)
|
||||
local value = (mon.stats and mon.stats[st.field]) or 0
|
||||
Kit.text("micro", st.key, bx + 12 * s, statsY + 8 * s, PAL.caption)
|
||||
Kit.text("stat", tostring(value), bx + 12 * s,
|
||||
statsY + 8 * s + Kit.textHeight("micro") + 2 * s, PAL.heading)
|
||||
Kit.meter(bx + 12 * s, statsY + cellH - 12 * s, cellW - 24 * s, 5 * s,
|
||||
value / STAT_SCALE * 100, PAL.blue)
|
||||
end
|
||||
|
||||
-- --------------------------------------------------- DVs | moves split
|
||||
local colY = statsY + cellH + 18 * s
|
||||
if narrow then
|
||||
-- stacked: DVs first, then moves, then the two actions side by side at
|
||||
-- full width (#715)
|
||||
Kit.caption(cx, colY, "DVs")
|
||||
Kit.textRight("tiny", ("HP DV auto-derived . %d"):format(mon.dvs.hp or 0),
|
||||
cx + inner, colY, PAL.caption)
|
||||
local rowY = colY + capH + 10 * s
|
||||
drawDvRows(S, Kit, mon, cx, rowY, inner, rowH, rowGap)
|
||||
|
||||
local movesY = rowY + colRowsH + 14 * s
|
||||
Kit.caption(cx, movesY, "MOVES")
|
||||
Kit.textRight("tiny", "click a slot to cycle", cx + inner, movesY, PAL.caption)
|
||||
local mRowY = movesY + capH + 10 * s
|
||||
drawMoveRows(S, Kit, mon, cx, mRowY, inner, rowH, rowGap)
|
||||
|
||||
local actY = mRowY + colRowsH + 10 * s
|
||||
local actW = (inner - 10 * s) / 2
|
||||
if Kit.button(cx, actY, actW, actH, "Reset to learnset",
|
||||
{ font = "small", radius = 9 * s }) then
|
||||
Ops.resetMoves(S, mon)
|
||||
end
|
||||
if Kit.button(cx + actW + 10 * s, actY, actW, actH, "Full heal",
|
||||
{ kind = "good", font = "small", radius = 9 * s }) then
|
||||
Ops.healMon(S, mon)
|
||||
end
|
||||
else
|
||||
local colGap = 18 * s
|
||||
local colW = (inner - colGap) / 2
|
||||
local rightX = cx + colW + colGap
|
||||
|
||||
Kit.caption(cx, colY, "DVs")
|
||||
Kit.textRight("tiny", ("HP DV auto-derived . %d"):format(mon.dvs.hp or 0),
|
||||
cx + colW, colY, PAL.caption)
|
||||
Kit.caption(rightX, colY, "MOVES")
|
||||
Kit.textRight("tiny", "click a slot to cycle", rightX + colW, colY, PAL.caption)
|
||||
|
||||
local rowY = colY + capH + 10 * s
|
||||
drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
|
||||
drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
|
||||
|
||||
local actY = rowY + colRowsH + 12 * s
|
||||
local actW = (colW - 10 * s) / 2
|
||||
if Kit.button(rightX, actY, actW, actH, "Reset to learnset",
|
||||
{ font = "small", radius = 9 * s }) then
|
||||
Ops.resetMoves(S, mon)
|
||||
end
|
||||
if Kit.button(rightX + actW + 10 * s, actY, actW, actH, "Full heal",
|
||||
{ kind = "good", font = "small", radius = 9 * s }) then
|
||||
Ops.healMon(S, mon)
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
end
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
-- Reorder lives on the row itself (the up/down pair appears on the selected
|
||||
-- row) rather than in a bottom button strip, which leaves Add / Remove as the
|
||||
-- only two panel-level verbs.
|
||||
--
|
||||
-- #715 reflow: side by side, the roster and the inspector need about 640
|
||||
-- real px between them. Anything narrower stacks the two cards (roster
|
||||
-- above, inspector below) at full width instead of shrinking both into
|
||||
-- unreadable slivers, and the roster body scrolls (wheel / touch drag /
|
||||
-- Kit.scrollbar) rather than silently truncating past the fold.
|
||||
|
||||
local PartyMod = require("src.pokemon.Party")
|
||||
local Theme = require("Theme")
|
||||
@@ -29,11 +35,8 @@ local function hpColor(frac)
|
||||
return PAL.green
|
||||
end
|
||||
|
||||
function Party.draw(S, Kit, x, y, w, h)
|
||||
local function drawRoster(S, Kit, x, y, listW, h)
|
||||
local s = Kit.scale
|
||||
local gap = 20 * s
|
||||
local listW = rosterWidth(w, s)
|
||||
|
||||
Kit.card(x, y, listW, h)
|
||||
local pad = 18 * s
|
||||
local cx = x + pad
|
||||
@@ -56,12 +59,21 @@ function Party.draw(S, Kit, x, y, w, h)
|
||||
local rowH = 64 * s
|
||||
local rowGap = 8 * s
|
||||
S.selectedParty = Ops.clamp(S.selectedParty or 1, 1, #S.save.party)
|
||||
for i, mon in ipairs(S.save.party) do
|
||||
-- The list used to `break` past the fold, silently hiding party slots on
|
||||
-- a short window; it scrolls instead now (#715), same offset contract as
|
||||
-- every other list in the editor.
|
||||
local visible = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
|
||||
S.partyOffset = Kit.scroll(cx, listTop, innerW, listH,
|
||||
S.partyOffset or 0, #S.save.party, visible)
|
||||
Kit.pushClip(cx, listTop, innerW, listH)
|
||||
for i = 1, visible do
|
||||
local slot = S.partyOffset + i
|
||||
local mon = S.save.party[slot]
|
||||
if not mon then break end
|
||||
local ry = listTop + (i - 1) * (rowH + rowGap)
|
||||
if ry + rowH > listTop + listH then break end
|
||||
local selected = (S.editingMon == mon)
|
||||
if Kit.row(cx, ry, innerW, rowH, selected, PAL.green) then
|
||||
Ops.selectParty(S, i)
|
||||
Ops.selectParty(S, slot)
|
||||
end
|
||||
|
||||
local rpad = 12 * s
|
||||
@@ -96,7 +108,7 @@ function Party.draw(S, Kit, x, y, w, h)
|
||||
local tw = math.max(40 * s, (cx + innerW - rightW - 10 * s) - tx)
|
||||
local name = Kit.ellipsize("monoRow", mon.species, tw - 34 * s)
|
||||
Kit.text("monoRow", name, tx, ry + 10 * s, PAL.heading)
|
||||
Kit.text("tiny", ("#%d"):format(i),
|
||||
Kit.text("tiny", ("#%d"):format(slot),
|
||||
tx + Kit.textWidth("monoRow", name) + 8 * s, ry + 12 * s, PAL.caption)
|
||||
|
||||
local maxHp = (mon.stats and mon.stats.hp) or 1
|
||||
@@ -105,6 +117,9 @@ function Party.draw(S, Kit, x, y, w, h)
|
||||
Kit.text("tiny", ("HP %d/%d"):format(mon.hp or 0, maxHp), tx,
|
||||
ry + rowH - 10 * s - Kit.textHeight("tiny"), PAL.muted)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, listTop, innerW, listH,
|
||||
S.partyOffset, #S.save.party, visible)
|
||||
end
|
||||
|
||||
local halfW = (innerW - 10 * s) / 2
|
||||
@@ -118,8 +133,22 @@ function Party.draw(S, Kit, x, y, w, h)
|
||||
{ kind = "danger", font = "small", radius = 9 * s }) then
|
||||
Ops.partyRemove(S)
|
||||
end
|
||||
end
|
||||
|
||||
MonEditor.draw(S, Kit, x + listW + gap, y, w - listW - gap, h)
|
||||
function Party.draw(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
local gap = 20 * s
|
||||
if w < 640 * s then
|
||||
-- stacked (#715): roster on top with enough height for a few rows, the
|
||||
-- inspector takes the rest and scrolls internally (see MonEditor)
|
||||
local rosterH = Theme.clamp(h * 0.42, 150 * s, 300 * s)
|
||||
drawRoster(S, Kit, x, y, w, rosterH)
|
||||
MonEditor.draw(S, Kit, x, y + rosterH + gap, w, h - rosterH - gap)
|
||||
else
|
||||
local listW = rosterWidth(w, s)
|
||||
drawRoster(S, Kit, x, y, listW, h)
|
||||
MonEditor.draw(S, Kit, x + listW + gap, y, w - listW - gap, h)
|
||||
end
|
||||
end
|
||||
|
||||
return Party
|
||||
|
||||
@@ -23,11 +23,23 @@ function Picker.results(S)
|
||||
return Ops.speciesSearch(S, p and p.query or "")
|
||||
end
|
||||
|
||||
-- One commit funnel for both of the picker's jobs: changing the inspected
|
||||
-- mon's species, and the Boxes panel's add flow (mode "box-add"), which
|
||||
-- creates a fresh Lv5 mon in the selected box instead (#715). Either way an
|
||||
-- unusable record refuses in the status bar rather than crashing (#541).
|
||||
local function commit(S, id)
|
||||
local p = S.speciesPicker
|
||||
if p and p.mode == "box-add" then
|
||||
return Ops.boxAddSpecies(S, id)
|
||||
end
|
||||
return Ops.setSpecies(S, S.editingMon, id)
|
||||
end
|
||||
|
||||
-- Enter commits the top match, which is the whole point of a search field.
|
||||
function Picker.commitFirst(S, Kit)
|
||||
local hits = Picker.results(S)
|
||||
if not hits[1] then return Ops.say(S, "No species matches that") end
|
||||
local ok = Ops.setSpecies(S, S.editingMon, hits[1])
|
||||
local ok = commit(S, hits[1])
|
||||
if ok then Ops.closeSpeciesPicker(S, Kit) end
|
||||
return ok
|
||||
end
|
||||
@@ -65,7 +77,8 @@ function Picker.draw(S, Kit, width, height)
|
||||
local cx, cy = x + pad, y + pad
|
||||
local inner = w - 2 * pad
|
||||
|
||||
Kit.caption(cx, cy, "CHOOSE A SPECIES")
|
||||
Kit.caption(cx, cy, p.mode == "box-add"
|
||||
and ("ADD TO BOX %d"):format(S.selectedBox or 1) or "CHOOSE A SPECIES")
|
||||
local closeW = 30 * s
|
||||
if Kit.button(x + w - pad - closeW, cy - 4 * s, closeW, 26 * s, "x",
|
||||
{ font = "small", radius = 7 * s }) then
|
||||
@@ -86,6 +99,9 @@ function Picker.draw(S, Kit, width, height)
|
||||
local listH = (y + h - pad - pagerH - 10 * s) - cy
|
||||
local perPage = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
|
||||
p.offset = Theme.clamp(p.offset or 0, 0, math.max(0, #hits - perPage))
|
||||
-- wheel / touch drag scroll the modal list too; the shield is already
|
||||
-- lowered for this layer, so Kit.scroll works here and only here (#715)
|
||||
p.offset = Kit.scroll(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
|
||||
if #hits == 0 then
|
||||
Kit.emptyBox(cx, cy, inner, listH, "Nothing matches that.")
|
||||
@@ -99,9 +115,11 @@ function Picker.draw(S, Kit, width, height)
|
||||
-- A record the formulas cannot use still lists, greyed: hiding it would
|
||||
-- make a modded species look like it never registered (#541).
|
||||
local usable = Ops.speciesUsable(S, id)
|
||||
local current = (S.editingMon and S.editingMon.species == id)
|
||||
-- box-add has no "current" species: nothing is being replaced
|
||||
local current = p.mode ~= "box-add"
|
||||
and (S.editingMon and S.editingMon.species == id) or false
|
||||
if Kit.row(cx, ry, inner, rowH, current, PAL.green, 9 * s) then
|
||||
if Ops.setSpecies(S, S.editingMon, id) then
|
||||
if commit(S, id) then
|
||||
Ops.closeSpeciesPicker(S, Kit)
|
||||
Kit.popClip()
|
||||
return
|
||||
@@ -121,6 +139,7 @@ function Picker.draw(S, Kit, width, height)
|
||||
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.caption)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
end
|
||||
|
||||
p.offset = Kit.pager(cx, y + h - pad - pagerH, inner, p.offset, #hits, perPage)
|
||||
|
||||
Reference in New Issue
Block a user