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:
Andrew Quenehen
2026-08-03 13:02:08 -03:00
87 changed files with 34575 additions and 3553 deletions
+76 -32
View File
@@ -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
View File
@@ -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
+42
View File
@@ -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
+9 -3
View File
@@ -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",
+8 -2
View File
@@ -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
+130 -66
View File
@@ -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
+65 -24
View File
@@ -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
+54 -20
View File
@@ -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
View File
@@ -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
+107 -42
View File
@@ -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
+193 -107
View File
@@ -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
+38 -9
View File
@@ -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 -4
View File
@@ -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)