editor clean up

This commit is contained in:
bryanthaboi
2026-10-07 19:07:05 -04:00
parent 5742782ea5
commit 527957de52
17 changed files with 1673 additions and 812 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 29 KiB

+2
View File
@@ -248,6 +248,8 @@ run_tier "T2 save editor: mobile properties (fixtures)" \
run_tier "T2 save editor: Gen 3 IV / EV / PP (fixtures)" "$LUA" tests/save_editor_gen3_ev_iv_tests.lua
run_tier "T2 save editor: touch value controls" \
env POKEPORT_DATA_DIR=tests/fixture_data "$LUA" tests/save_editor_touch_controls_test.lua
run_tier "T2 save editor: inspector layout" \
env POKEPORT_DATA_DIR=tests/fixture_data "$LUA" tests/save_editor_inspector_layout_test.lua
run_tier "T2 save editor: wheel scrolling" "$LUA" tests/save_editor_wheel_bug595_test.lua
run_tier "T2 save editor: pad / NX input" "$LUA" tests/save_editor_pad_input_test.lua
run_tier "T2 save editor: gold / gen2 (fixtures)" "$LUA" tests/save_editor_gen2_tests.lua
+3 -3
View File
@@ -210,7 +210,7 @@ function Button.draw(Kit, x, y, w, h, label, opts, hot, focused)
ix,
y + (h - layout.iconSize) / 2,
layout.iconSize,
ink,
opts.iconInk or ink,
enabled and 1 or B.disabledA
)
lx = ix + layout.iconSize + layout.gap
@@ -221,7 +221,7 @@ function Button.draw(Kit, x, y, w, h, label, opts, hot, focused)
x + (w - layout.trailing - layout.iconSize) / 2,
top,
layout.iconSize,
ink,
opts.iconInk or ink,
enabled and 1 or B.disabledA
)
ly = top + layout.iconSize + layout.gap
@@ -261,7 +261,7 @@ function Button.draw(Kit, x, y, w, h, label, opts, hot, focused)
or ""
local groupW = size + (hasLabel and gap + Kit.textWidth(fname, shown) or 0)
local ix = opts.align == "left" and x + B.labelPad * Kit.scale or x + (w - groupW) / 2
Icons.draw(opts.icon, ix, y + (h - size) / 2, size, ink, enabled and 1 or B.disabledA)
Icons.draw(opts.icon, ix, y + (h - size) / 2, size, opts.iconInk or ink, enabled and 1 or B.disabledA)
if hasLabel then
if bold then
Kit.textBold(fname, shown, ix + size + gap, ty, ink)
+3
View File
@@ -54,6 +54,9 @@ local editorNames = {
"user-round",
"wallet",
"flag",
"sparkles",
"shuffle",
"folder-open",
}
local atlases = {
{ path = "assets/launcher/lucide/icons.png", names = names },
+1 -1
View File
@@ -108,7 +108,7 @@ frame(390, 844, "Android")
local low = S.toast.rect[2]
Kit.focus = "mon-nickname"
frame(390, 844, "Android")
T.check(S.toast.rect[2] < 844 / 3 and S.toast.rect[2] < low, "a focused field on a phone moves the toast under the tab rail")
T.check(S.toast.rect[2] < 844 / 2 and S.toast.rect[2] < low, "a focused field on a phone moves the toast above the keyboard")
Kit.blur()
local MapBrowser, ItemPicker = require("MapBrowser"), require("ItemPicker")
+3
View File
@@ -1049,6 +1049,8 @@ do
f:write(SaveData.encode(data))
f:close()
local stubDimensions = love.graphics.getDimensions
love.graphics.getDimensions = function() return 1280, 900 end
App.load(tmpPath, { version = "red" })
local S = App.getState()
S.tab = "party"
@@ -1107,6 +1109,7 @@ do
App.textinput("POKE@MON")
App.draw()
eq(S.nicknameDraft, "POKEMON", "the @ terminator is stripped at input")
love.graphics.getDimensions = stubDimensions
os.remove(tmpPath)
for _, bak in ipairs(FsIo.globPrefix(tmpPath .. ".bak-")) do os.remove(bak) end
+6 -7
View File
@@ -792,17 +792,15 @@ do
Kit.audit = {}
S.monSection="origin"
MonEditor.draw(S, Kit, 0, 0, 1280, 720)
local labels = {}
for _, r in ipairs(Kit.audit) do labels[r.label] = true end
Kit.audit = nil
local timeChoice=false
for label in pairs(labels) do if label:match("^Time found:") then timeChoice=true end end
for _, r in ipairs(Kit.audit) do if r.id == "choice-caughtTime" then timeChoice=true end end
Kit.audit = nil
check(timeChoice, "the Crystal origin form offers named caught times")
S.inspectorScroll=300
Kit.audit={}
MonEditor.draw(S,Kit,0,0,1280,720)
local caughtGender=false
for _,r in ipairs(Kit.audit) do if r.label:match("^Caught by:") then caughtGender=true end end
for _,r in ipairs(Kit.audit) do if r.id == "caught-by-boy" then caughtGender=true end end
Kit.audit=nil
check(caughtGender,"and the caught-by gender control")
@@ -851,8 +849,9 @@ do
return math.floor(lowest - H + 0.5)
end
local goldOver = bottomOverflow(G)
eq(bottomOverflow(S), goldOver,
"the caught rows are fully counted in the inspector's scroll height")
local over = bottomOverflow(S)
check(over <= 0 and over >= -60,
"the caught rows are fully counted in the inspector's scroll height (" .. over .. ")")
check(goldOver <= 4, ("the inspector reaches its last control (%d px short)")
:format(goldOver))
end
+137
View File
@@ -0,0 +1,137 @@
package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
_G.love = require("tests.love_stub")
local App = require("tools.save-editor.App")
local Ops, Gen, Kit = require("Ops"), require("Gen"), require("Kit")
local History = require("History")
local SD = require("src.core.SaveData")
local count = 0
local function check(ok, msg)
count = count + 1
assert(ok, msg)
end
local path = os.tmpname() .. "-inspector-layout.lua"
local f = assert(io.open(path, "wb"))
f:write(SD.encode(Gen.newGame("red")))
f:close()
App.load(path, { version = "red", embedded = true })
local S = App.getState()
assert(S.save and not S.loadError, S.status)
Ops.partyAdd(S)
Ops.partyAdd(S)
Ops.selectParty(S, 1)
local mon = S.editingMon
S.dirty = false
local function window(w, h, os)
love.graphics.getDimensions = function() return w, h end
love.window.getSafeArea = function() return 0, 0, w, h end
love.system.getOS = function() return os end
end
local function controls()
App.draw()
Kit.audit = {}
App.draw()
local list = Kit.audit
Kit.audit = nil
return list
end
local function find(list, id)
for _, c in ipairs(list) do
if c.id == id then return c end
end
end
local printed = {}
local realPrint = love.graphics.print
love.graphics.print = function(text, ...)
printed[#printed + 1] = tostring(text)
return realPrint(text, ...)
end
window(1280, 900, "OS X")
S.tab, S.monSection, S.inspectorScroll = "party", "main", 0
local list = controls()
local seen = {}
for _, text in ipairs(printed) do seen[text] = true end
love.graphics.print = realPrint
check(seen.Saved and seen["Save editor"], "title bar shows the save state as plain text")
for text in pairs(seen) do
check(not text:find("●", 1, true), "no status dot in the title bar")
end
check(find(list, "tab-monSection-moves") ~= nil, "wide inspectors use section tabs")
check(find(list, "change-species") ~= nil, "species has a Change button")
local box = assert(find(list, "value-level"), "level value box")
local undo = #S.undoStack
App.mousepressed(box.x + box.w / 2, box.y + box.h / 2, 1)
App.draw()
check(Kit.focus == "value-edit-level", "tapping the value box edits it inline")
for _ = 1, 4 do App.keypressed("backspace") end
App.textinput("4x2")
App.draw()
App.keypressed("return")
App.draw()
check(mon.level == 42, "typed level commits on Enter (got " .. tostring(mon.level) .. ")")
check(#S.undoStack == undo + 1, "one commit is one undo")
list = controls()
box = assert(find(list, "value-level"), "level value box after commit")
App.mousepressed(box.x + box.w / 2, box.y + box.h / 2, 1)
App.draw()
App.textinput("9")
App.draw()
App.keypressed("escape")
App.draw()
check(mon.level == 42 and Kit.focus == nil, "Escape discards an inline edit")
local slider = assert(find(list, "slider-level"), "level slider")
undo = #S.undoStack
local sy = slider.y + slider.h / 2
App.touchpressed("slide", slider.x + slider.w * 0.2, sy)
App.draw()
App.touchmoved("slide", slider.x + slider.w * 0.6, sy)
App.draw()
App.touchmoved("slide", slider.x + slider.w * 0.95, sy)
App.draw()
check(mon.level == 42, "dragging the slider only previews")
App.touchreleased("slide", slider.x + slider.w * 0.95, sy)
App.draw()
check(mon.level > 90, "releasing the slider commits (got " .. tostring(mon.level) .. ")")
check(#S.undoStack == undo + 1, "a whole drag is one undo")
local first = next(S.data.moves)
Ops.setMove(S, mon, 1, S.data.moves[first].id or first)
Ops.setPpUps(S, mon, 1, 2)
S.monSection, S.inspectorScroll = "moves", 0
list = controls()
local zero = assert(find(list, "ppup-1-0"), "PP Ups segment 0")
App.mousepressed(zero.x + zero.w / 2, zero.y + zero.h / 2, 1)
App.draw()
check(require("MonOps").getPpUps(mon, 1) == 0, "PP Ups segment sets the count")
check(find(list, "move-1") ~= nil and find(list, "slider-pp-1") ~= nil, "move card has Change and a PP slider")
window(390, 844, "Android")
S.monSection, S.inspectorScroll = "main", 0
list = controls()
local tiles = {}
for _, c in ipairs(list) do
if c.class == "row" then tiles[#tiles + 1] = c end
end
check(#tiles >= 2, "phone shows the party grid above the inspector")
local heal
for _, c in ipairs(list) do
if c.label == "Full heal" then heal = c end
end
check(heal and heal.y > tiles[#tiles].y, "phone shows the inspector below the party grid")
App.touchpressed("tile", tiles[2].x + tiles[2].w / 2, tiles[2].y + tiles[2].h / 2)
App.touchreleased("tile", tiles[2].x + tiles[2].w / 2, tiles[2].y + tiles[2].h / 2)
App.draw()
check(S.editingMon == S.save.party[2], "tapping a party tile selects it")
History.undo(S)
os.remove(path)
print(("save editor inspector layout: %d checks passed"):format(count))
+23 -4
View File
@@ -36,7 +36,7 @@ Paint.draw = function(owner, x, y, w, h, label, opts, hot, focused)
if opts.icon == "save" then
saveRect = { x = x, y = y, w = w, h = h }
end
if label == "Change species" then
if opts.id == "change-species" then
speciesY = y
end
local theme = require("Theme")
@@ -364,8 +364,18 @@ for _, size in ipairs({
case[1], "main", "bag", "storage", "view"
S.mobileInspector, S.inspectorScroll, S.pageScroll = true, 0, {}
navigationRects = {}
frame("dropdown page " .. case[2], W, H)
local pageControls = frame("dropdown page " .. case[2], W, H)
local rect = navigationRects[case[2]]
if case[2] == "monSection" and S._navOffset and (not rect or rect.y + rect.h > H) then
S.inspectorScroll = S._navOffset
navigationRects = {}
pageControls = frame("dropdown page scrolled " .. case[2], W, H)
rect = navigationRects[case[2]]
end
local sectionTabs = 0
for _, c in ipairs(pageControls) do
if c.id and c.id:match("^tab%-monSection%-") then sectionTabs = sectionTabs + 1 end
end
if rect then
local previous, selected, anchor = S[case[2]], S.editingMon, speciesY
Kit.focus = "property-level"
@@ -382,7 +392,8 @@ for _, size in ipairs({
check(not S.navPopup and S[case[2]] == previous, "Escape cancels " .. case[2])
check(S.editingMon == selected, "Escape preserves the Pokemon selection")
else
check(case[2] == "mapSection" and W > H, "only the simultaneous map columns omit a chooser")
check(case[2] == "mapSection" and W > H or case[2] == "monSection" and sectionTabs == case[3],
"only the simultaneous map columns and full section tabs omit a chooser")
end
end
end
@@ -485,7 +496,13 @@ local realSave, saved = App.save, 0
App.save = function()
saved = saved + 1
end
App.mousepressed(saveRect.x + saveRect.w / 2, saveRect.y + saveRect.h / 2, 1)
local tapX = saveRect.x + saveRect.w / 2
local popupRect = S.navPopup and S.navPopup.rect
if popupRect and tapX <= popupRect.x + popupRect.w then
tapX = popupRect.x + popupRect.w + 2
end
check(tapX < saveRect.x + saveRect.w, "Save has a target outside the popup")
App.mousepressed(tapX, saveRect.y + saveRect.h / 2, 1)
App.draw()
check(saved == 0 and S.navPopup == nil, "outside tap dismisses without activating Save underneath")
App.save = realSave
@@ -537,6 +554,8 @@ check(
-- Option taps change the intended nested screen. A drag inside a short
-- popup scrolls the options without selecting the row under its release.
App.draw()
S.inspectorScroll = S._navOffset or 0
App.draw()
rect = navigationRects.monSection
App.mousepressed(rect.x + rect.w / 2, rect.y + rect.h / 2, 1)
App.draw()
+2 -1
View File
@@ -10,7 +10,8 @@ NAMES = [
"save", "rotate-ccw", "ellipsis", "copy", "heart", "package", "backpack", "list-filter",
"arrow-up", "arrow-down", "arrow-left", "arrow-right", "arrow-up-down", "eye", "award",
"book-open", "shield-check", "map-pin", "grid-2x2", "sliders-horizontal", "expand",
"chevrons-up", "users", "user-round", "wallet", "flag",
"chevrons-up", "users", "user-round", "wallet", "flag", "sparkles", "shuffle",
"folder-open",
]
+137 -159
View File
@@ -633,164 +633,142 @@ function App.joystickhat(joystick, hat, direction)
end
-- ------------------------------------------------------------------ chrome
-- Compact action row, with file identity above it when height allows.
local function drawTitleBar(x, y, w, h)
local pad, gap, row = 12 * Kit.scale, 8 * Kit.scale, Kit.controlH()
local inner = w - 2 * pad
if not S.compactChrome and not Kit.desktop then
Kit.text(
"tab",
"SAVE EDITOR" .. (S.version and (" / " .. S.version:upper()) or ""),
x + pad,
y + 8 * Kit.scale,
PAL.heading
)
Kit.text(
"tiny",
Kit.ellipsize("tiny", (S.dirty and "UNSAVED " or "SAVED ") .. (S.path or "New save"), inner),
x + pad,
y + Kit.textHeight("tab") + 12 * Kit.scale,
S.dirty and PAL.yellow or PAL.caption
)
local function versionLabel()
local ok, info = pcall(require("src.core.GameVersion").info, S.version)
if ok and type(info) == "table" and info.label then return info.label end
return S.version and (S.version:sub(1, 1):upper() .. S.version:sub(2)) or nil
end
local function shortPath(path)
if not path then return "New save" end
local parts = {}
for part in tostring(path):gmatch("[^/\\]+") do parts[#parts + 1] = part end
if #parts <= 3 then return tostring(path) end
return ".../" .. table.concat(parts, "/", #parts - 2)
end
local function drawIdentity(x, y, w, h)
local s = Kit.scale
local titleH, pathH = Kit.textHeight("button"), Kit.textHeight("tiny")
local ty = y + (h - titleH - pathH - 4 * s) / 2
local lead, version, sep = "Save editor", versionLabel(), " / "
local leadW, sepW = Kit.textWidth("button", lead), Kit.textWidth("button", sep)
Kit.textBold("button", Kit.ellipsize("button", lead, w), x, ty, PAL.heading)
if version and leadW + sepW + 40 * s < w then
Kit.text("button", sep, x + leadW + 1, ty, PAL.muted)
Kit.textBold("button", Kit.ellipsize("button", version, w - leadW - sepW - 1), x + leadW + sepW + 1, ty, PAL.heading)
end
local narrow = w < 600 * Kit.scale and not S.compactChrome
local cols = narrow and 4 or 6
local bw = (inner - (cols - 1) * gap) / cols
local by = S.compactChrome and (y + 8 * Kit.scale)
or (y + Kit.textHeight("tab") + Kit.textHeight("tiny") + 20 * Kit.scale)
local saveInk = S.allowSave and S.dirty and PAL.green or PAL.muted
local actions = {
{
S.allowSave and (S.dirty and "Save" or "Saved") or "Save locked",
"ghost",
function()
App.save()
end,
S.dirty or not S.allowSave,
{
face = "invert",
ink = saveInk,
stroke = saveInk,
icon = S.allowSave and "save" or "lock",
},
},
{
"Undo",
"ghost",
function()
require("History").undo(S)
end,
S.undoStack and #S.undoStack > 0,
},
{
"Redo",
"ghost",
function()
require("History").redo(S)
end,
S.redoStack and #S.redoStack > 0,
},
{
"Reload",
"ghost",
function()
App.reload()
end,
true,
},
{
"Open",
"accent",
function()
App.chooseAndOpen()
end,
true,
},
{
S._quitArmed and "Discard?" or "Close",
S._quitArmed and "danger" or "ghost",
function()
App.close()
end,
true,
},
Kit.text("tiny", Kit.ellipsize("tiny", shortPath(S.path), w), x, ty + titleH + 4 * s, PAL.muted)
end
local function saveStatus()
if S.dirty then return "Unsaved changes", PAL.yellow end
return "Saved", PAL.muted
end
local function saveButton(x, y, h, measure)
local label = S.allowSave and "Save" or "Save locked"
local ink = S.allowSave and S.dirty and PAL.green or PAL.muted
local opts = {
font = "button",
face = "invert",
ink = ink,
stroke = ink,
icon = S.allowSave and "save" or "lock",
enabled = S.dirty or not S.allowSave,
}
local function actionOptions(action)
local opts = action[5] or {}
opts.kind, opts.enabled, opts.font = action[2], action[4], "small"
return opts
local w = Kit.buttonWidth(label, opts, h)
if not measure and Kit.button(x - w, y, w, h, label, opts) then
App.save()
end
if Kit.desktop then
local widths, total = {}, 5 * gap
for i, action in ipairs(actions) do
widths[i] = Kit.buttonWidth(action[1], actionOptions(action), row)
if i == 1 then
widths[i] = math.max(widths[i], Kit.buttonWidth("Save locked", { font = "small", icon = "lock" }, row))
elseif i == 6 then
widths[i] = math.max(widths[i], Kit.buttonWidth("Discard?", { font = "small" }, row))
end
total = total + widths[i]
end
local bx, by = x + w - pad - total, y + 8 * Kit.scale
local identityW = bx - gap - (x + pad)
local labelH, pathH = Kit.textHeight("tab"), Kit.textHeight("tiny")
local ty = by + (row - labelH - pathH - 4 * Kit.scale) / 2
Kit.text("tab", Kit.ellipsize("tab", "SAVE EDITOR" .. (S.version and (" / " .. S.version:upper()) or ""), identityW), x + pad, ty, PAL.heading)
Kit.text("tiny", Kit.ellipsize("tiny", (S.dirty and "UNSAVED " or "SAVED ") .. (S.path or "New save"), identityW), x + pad, ty + labelH + 4 * Kit.scale, S.dirty and PAL.yellow or PAL.caption)
for i, action in ipairs(actions) do
if Kit.button(bx, by, widths[i], row, action[1], actionOptions(action)) then action[3]() end
bx = bx + widths[i] + gap
end
return
return w
end
local function drawActionRow(x, y, w, h, withIdentity)
local s, row = Kit.scale, Kit.controlH()
local gap, pad = 8 * s, (Kit.desktop and 20 or 12) * s
local by = y + (h - row) / 2
local rx = x + w - pad
if S._quitArmed then
local cw = Kit.buttonWidth("Discard?", { font = "button" }, row)
rx = rx - cw
if Kit.button(rx, by, cw, row, "Discard?", { kind = "danger", font = "button" }) then App.close() end
else
rx = rx - row
if Kit.iconButton(rx, by, row, row, "x", "Close", { kind = "ghost" }) then App.close() end
end
if narrow then
local more = {
S.chromeMenu and "Less" or "More",
"ghost",
function()
S.chromeMenu = not S.chromeMenu
Kit.blur()
end,
true,
rx = rx - 2 * gap
rx = rx - saveButton(rx, by, row)
rx = rx - gap - row
if Kit.iconButton(rx, by, row, row, "folder-open", "Open", { kind = "ghost" }) then App.chooseAndOpen() end
rx = rx - gap - row
if Kit.iconButton(rx, by, row, row, "rotate-ccw", "Reload", { kind = "ghost" }) then App.reload() end
rx = rx - gap - row
if Kit.iconButton(rx, by, row, row, "redo-2", "Redo", { kind = "ghost", enabled = S.redoStack and #S.redoStack > 0 }) then
require("History").redo(S)
end
rx = rx - 4 * s - row
if Kit.iconButton(rx, by, row, row, "undo-2", "Undo", { kind = "ghost", enabled = S.undoStack and #S.undoStack > 0 }) then
require("History").undo(S)
end
rx = rx - 18 * s
local status, color = saveStatus()
local statusW = Kit.textWidth("small", status)
local left = x + pad + (withIdentity and 150 * s or 0)
if rx - statusW >= left then
Kit.textRight("small", status, rx, by + (row - Kit.textHeight("small")) / 2, color)
rx = rx - statusW - 24 * s
end
if withIdentity then
drawIdentity(x + pad, y, rx - x - pad, h)
end
end
local function chromeMenuCols(w)
local s, gap = Kit.scale, 8 * Kit.scale
local widest = 0
for _, label in ipairs({ "Redo", "Reload", "Open", "Close", "Discard?" }) do
widest = math.max(widest, Kit.buttonWidth(label, { font = "small" }, Kit.controlH()))
end
return (w - 24 * s) >= 4 * widest + 3 * gap and 4 or 2
end
local function drawTitleBar(x, y, w, h)
if Kit.desktop or S.compactChrome or w >= 760 * Kit.scale then
return drawActionRow(x, y, w, h, not S.compactChrome)
end
local s = Kit.scale
local pad, gap, row = 12 * s, 8 * s, Kit.controlH()
local inner = w - 2 * pad
local idH = Kit.textHeight("button") + Kit.textHeight("tiny") + 4 * s
drawIdentity(x + pad, y + 8 * s, inner, idH)
local by = y + idH + 20 * s
local rx = x + w - pad - row
if Kit.iconButton(rx, by, row, row, S.chromeMenu and "x" or "ellipsis", S.chromeMenu and "Less" or "More", { kind = "ghost" }) then
S.chromeMenu = not S.chromeMenu
Kit.blur()
end
rx = rx - gap
rx = rx - saveButton(rx, by, row) - gap - row
if Kit.iconButton(rx, by, row, row, "undo-2", "Undo", { kind = "ghost", enabled = S.undoStack and #S.undoStack > 0 }) then
require("History").undo(S)
end
local status, color = saveStatus()
Kit.text("small", Kit.ellipsize("small", status, rx - gap - x - pad), x + pad, by + (row - Kit.textHeight("small")) / 2, color)
if S.chromeMenu then
local menu = {
{ "Redo", "redo-2", function() require("History").redo(S) end, S.redoStack and #S.redoStack > 0 },
{ "Reload", "rotate-ccw", function() App.reload() end, true },
{ "Open", "folder-open", function() App.chooseAndOpen() end, true },
{ S._quitArmed and "Discard?" or "Close", S._quitArmed and "trash" or "x", function() App.close() end, true },
}
local primary = { actions[1], actions[2], actions[3], more }
for i, a in ipairs(primary) do
if Kit.button(x + pad + (i - 1) * (bw + gap), by, bw, row, a[1], actionOptions(a)) then
a[3]()
end
end
if S.chromeMenu then
local menuW = (inner - 2 * gap) / 3
for i = 4, 6 do
local a = actions[i]
if
Kit.button(
x + pad + (i - 4) * (menuW + gap),
by + row + gap,
menuW,
row,
a[1],
actionOptions(a)
)
then
a[3]()
end
end
end
return
end
for i, a in ipairs(actions) do
if
Kit.button(
x + pad + (i - 1) % cols * (bw + gap),
by + math.floor((i - 1) / cols) * (row + gap),
bw,
row,
a[1],
actionOptions(a)
)
then
a[3]()
local cols = chromeMenuCols(w)
local bw = (inner - (cols - 1) * gap) / cols
for i, m in ipairs(menu) do
local mx = x + pad + (i - 1) % cols * (bw + gap)
local my = by + row + gap + math.floor((i - 1) / cols) * (row + gap)
local opts = { font = "small", icon = m[2], enabled = m[4], kind = m[1] == "Discard?" and "danger" or "ghost" }
if Kit.button(mx, my, bw, row, m[1], opts) then m[3]() end
end
end
end
@@ -938,13 +916,13 @@ function App.draw()
-- identity painting through each other (#715). The taller bar simply
-- costs the content column height, which scrolls.
S.compactChrome = sh < 500 * s and sw > sh
local titleTwoRow = sw < 600 * s and not S.compactChrome and S.chromeMenu
local titleH = Kit.textHeight("tab")
+ Kit.textHeight("tiny")
+ 28 * s
+ (titleTwoRow and 2 or 1) * (Kit.controlH() + 8 * s)
if S.compactChrome or Kit.desktop then
local titleH
if S.compactChrome or Kit.desktop or sw >= 760 * s then
titleH = Kit.controlH() + 16 * s
else
local menuRows = S.chromeMenu and (chromeMenuCols(sw) == 4 and 1 or 2) or 0
titleH = Kit.textHeight("button") + Kit.textHeight("tiny") + 4 * s + 28 * s
+ (1 + menuRows) * (Kit.controlH() + 8 * s)
end
local tabH = Kit.controlH() + 6 * s
local statusH = (Kit.desktop and 28 or 38) * s
+282
View File
@@ -0,0 +1,282 @@
local Theme = require("Theme")
local Touch = require("TouchEditor")
local PAL = Theme.PAL
local Form = {}
local ERROR_FILL = { 58, 31, 37 }
function Form.thousands(n)
n = tonumber(n)
if not n then return "?" end
local str = tostring(math.floor(math.abs(n)))
str = str:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
return (n < 0 and "-" or "") .. str
end
function Form.range(l)
return Form.thousands(l.lo) .. " – " .. Form.thousands(l.hi)
end
function Form.labelH(Kit)
return Kit.textHeight("small") + 8 * Kit.scale
end
function Form.label(Kit, text, x, y, w, right, color, rightColor)
local rw = right and Kit.textWidth("small", right) + 10 * Kit.scale or 0
Kit.text("small", Kit.ellipsize("small", text, math.max(0, w - rw)), x, y, color or PAL.muted)
if right then
Kit.textRight("small", right, x + w, y, rightColor or PAL.muted)
end
return Form.labelH(Kit)
end
function Form.wrapH(Kit, font, text, w)
local f = Kit.fonts[font] or Kit.fonts.small
if not f or w <= 0 then return 0 end
if not f.getWrap then return f:getHeight() end
local _, lines = f:getWrap(tostring(text), w)
return #lines * f:getHeight()
end
function Form.issueH(Kit, message, w)
if not message then return 0 end
local size, gap = 14 * Kit.scale, 6 * Kit.scale
return math.max(size, Form.wrapH(Kit, "tiny", message, w - size - gap)) + gap
end
function Form.box(Kit, x, y, w, h, text, invalid, color)
local r = Theme.radius()
Theme.fillRounded(x, y, w, h, invalid and ERROR_FILL or PAL.rowBg, 0.7, r)
Theme.stroke(x, y, w, h, r, invalid and PAL.red or PAL.cardBorder, invalid and 0.95 or 0.3,
invalid and 2 * Kit.scale or 1)
local pad = 14 * Kit.scale
Kit.textBold("button", Kit.ellipsize("button", tostring(text), w - 2 * pad), x + pad,
y + (h - Kit.textHeight("button")) / 2, color or (invalid and PAL.red or PAL.heading))
end
function Form.select(S, Kit, id, title, value, options, x, y, w, apply, issue, help)
local shown
for _, o in ipairs(options) do
if o[1] == value then shown = o[2] end
end
shown = shown or ((issue and "Invalid saved value " or "Unknown saved value ") .. tostring(value))
local opts = {
face = "invert",
font = "button",
align = "left",
trailingIcon = "chevron-down",
id = "choice-" .. id,
invalid = issue ~= nil,
}
local h = Kit.buttonHeight(shown, w, opts)
if Kit.button(x, y, w, h, shown, opts) then
Touch.open(S, Kit, {
mode = "choice", id = id, title = title, value = value, options = options,
apply = apply, help = help, issue = issue,
})
end
return h
end
local function digits(v)
return (tostring(v or ""):gsub("[^0-9]", ""):sub(1, 8))
end
function Form.number(S, Kit, id, value, x, y, w, h, apply, issue, preview)
local key = "value-edit-" .. id
local function commit(v)
local n = tonumber(v)
if not n then return require("Ops").say(S, "Enter a whole number") end
if n ~= value then apply(n) end
end
local edit = S.valueEdit
if edit and edit.id == id and Kit.focus ~= key then
S.valueEdit = nil
if edit.draft ~= tostring(value) then commit(edit.draft) end
edit = nil
end
if edit and edit.id == id then
local v = Kit.textfield(key, x, y, w, h, edit.draft, "", {
sanitize = digits,
invalid = issue ~= nil,
onSubmit = function(text)
S.valueEdit = nil
commit(text)
end,
onCancel = function()
S.valueEdit = nil
end,
})
if S.valueEdit == edit then edit.draft = v end
return
end
local opts = { face = "invert", font = "button", align = "left", id = "value-" .. id, invalid = issue ~= nil }
if Kit.button(x, y, w, h, tostring(preview or value), opts) then
S.valueEdit = { id = id, draft = tostring(value) }
Kit.focus = key
Kit._fieldHit, Kit._focusDrawn = true, true
end
end
function Form.slider(S, Kit, id, value, l, x, y, w, h, apply, issue)
local s = Kit.scale
local knob = 9 * s
local tx, tw = x + knob, math.max(1, w - 2 * knob)
local function at(mx)
return math.floor(l.lo + Theme.clamp((mx - tx) / tw, 0, 1) * (l.hi - l.lo) + 0.5)
end
local shown, committed = tonumber(value), false
local d = S._slider
if d and d.id == id then
if Kit.mouseDown then
if not d.live and not d.scroll then
local dx, dy = math.abs(Kit.mouseX - d.x), math.abs(Kit.mouseY - d.y)
if dx > 6 * s and dx >= dy then
d.live = true
elseif dy > 6 * s then
d.scroll = true
end
end
if d.live then
d.value = at(Kit.mouseX)
shown = d.value
end
else
S._slider = nil
if d.live then
committed = true
if d.value ~= value then apply(d.value) end
shown = d.value
end
end
elseif not d and Kit.mouseDown and not Kit.blockClicks and Kit.hit(x, y, w, h) then
S._slider = { id = id, x = Kit.mouseX, y = Kit.mouseY }
end
if Kit.button(x, y, w, h, "", { face = "bare", id = "slider-" .. id }) and not committed then
local v = at(Kit.mouseX)
if v ~= value then apply(v) end
shown = v
end
local ratio = shown and Theme.clamp((shown - l.lo) / math.max(1, l.hi - l.lo), 0, 1) or 0
local color = issue and PAL.red or PAL.blue
local trackH = 6 * s
local cy = y + h / 2
Theme.fillRounded(tx, cy - trackH / 2, tw, trackH, PAL.cardBorder, 0.3, trackH / 2)
if ratio > 0 then
Theme.fillRounded(tx, cy - trackH / 2, tw * ratio, trackH, color, 0.95, trackH / 2)
end
Theme.col(color)
love.graphics.circle("fill", tx + tw * ratio, cy, knob, 32)
return shown ~= tonumber(value) and shown or nil
end
function Form.numberRow(S, Kit, id, title, value, l, x, y, w, apply, issue)
local s, row = Kit.scale, Kit.controlH()
if not require("Legality").integer(value, l.lo, l.hi) then
issue = issue or ("Saved value must be a whole number from " .. l.lo .. " to " .. l.hi)
end
local right = Form.range(l)
if l.remaining ~= nil then right = right .. " · " .. l.remaining .. " free" end
local cy = y + Form.label(Kit, title, x, y, w, right, issue and PAL.red or PAL.muted)
local boxW = math.floor(math.min(math.max(110 * s, Kit.textWidth("button", tostring(l.hi)) + 40 * s), w * 0.42))
local gap = 16 * s
local preview = Form.slider(S, Kit, id, value, l, x + boxW + gap, cy, w - boxW - gap, row, apply, issue)
Form.number(S, Kit, id, value, x, cy, boxW, row, apply, issue, preview)
cy = cy + row
if issue then
cy = cy + 6 * s + Touch.issue(Kit, issue, x, cy + 6 * s, w)
end
return cy - y
end
function Form.segmented(Kit, id, options, value, x, y, w, h, apply, issue)
local s = Kit.scale
local inset, r = 4 * s, Theme.radius()
Theme.fillRounded(x, y, w, h, PAL.rowBg, 0.7, r)
Theme.stroke(x, y, w, h, r, issue and PAL.red or PAL.cardBorder, issue and 0.95 or 0.3, issue and 2 * s or 1)
local segW = w / #options
for i, o in ipairs(options) do
local sx = x + (i - 1) * segW
local active = o[1] == value
if active then
Theme.fillRounded(sx + inset, y + inset, segW - 2 * inset, h - 2 * inset, PAL.ink, 1, r - inset)
elseif not Kit.blockClicks and Kit.hover(sx, y, segW, h) then
Theme.fillRounded(sx + inset, y + inset, segW - 2 * inset, h - 2 * inset, PAL.raised, 1, r - inset)
end
local opts = { face = "bare", font = "button", ink = active and PAL.inverse or PAL.text, id = id .. "-" .. tostring(o[1]) }
if Kit.button(sx, y, segW, h, o[2], opts) and not active then
apply(o[1])
end
end
end
local function flow(Kit, caption, actions, x, y, w, draw)
local s, row = Kit.scale, Kit.controlH()
local gap = 10 * s
local cx, cy = x, y
if caption then
if draw then Kit.caption(x, y + (row - Kit.textHeight("caption")) / 2, caption) end
cx = x + Kit.captionWidth(caption) + 18 * s
end
for _, a in ipairs(actions) do
local opts = {}
for k, v in pairs(a[3] or {}) do opts[k] = v end
opts.font = opts.font or "button"
local bw = math.min(w, Kit.buttonWidth(a[1], opts, row))
if cx > x and cx + bw > x + w then
cx, cy = x, cy + row + gap
end
if draw and Kit.button(cx, cy, bw, row, a[1], opts) then a[2]() end
cx = cx + bw + gap
end
return cy + row - y
end
function Form.flowH(Kit, caption, actions, w)
return flow(Kit, caption, actions, 0, 0, w, false)
end
function Form.flow(Kit, caption, actions, x, y, w)
return flow(Kit, caption, actions, x, y, w, true)
end
function Form.tabsFit(Kit, options, w, h)
local s = Kit.scale
local widths, total = {}, 0
for i, o in ipairs(options) do
local tw = Kit.textWidth("button", o[2]) + 28 * s + (Theme.BOLD_OFFSET or 1)
if (o.errors or 0) > 0 then tw = tw + math.floor(h * 0.42) + math.floor(7 * s) end
widths[i] = math.max(Kit.tapMin(), math.ceil(tw))
total = total + widths[i]
end
return total <= w, widths
end
function Form.tabs(S, Kit, key, options, x, y, w, h, pick)
local fits, widths = Form.tabsFit(Kit, options, w, h)
if not fits then return false end
local s = Kit.scale
local tx = x
for i, o in ipairs(options) do
local active = S[key] == o[1]
local err = (o.errors or 0) > 0
local opts = {
face = "bare",
font = "button",
bold = active,
ink = err and PAL.red or (active and PAL.heading or PAL.muted),
icon = err and "triangle-alert" or nil,
id = "tab-" .. key .. "-" .. tostring(o[1]),
}
if Kit.button(tx, y, widths[i], h, o[2], opts) and not active then
pick(o[1], i)
end
if active then
Theme.fillRounded(tx + 4 * s, y + h - 3 * s, widths[i] - 8 * s, 3 * s, err and PAL.red or PAL.heading, 1, 1.5 * s)
end
tx = tx + widths[i]
end
return true
end
return Form
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -272,7 +272,7 @@ function Kit.trackedControls()
return tracked, trackedN
end
local function audit(class, x, y, w, h, label)
local function audit(class, x, y, w, h, label, id)
if Kit.blockClicks then
return
end
@@ -284,6 +284,9 @@ local function audit(class, x, y, w, h, label)
return
end
local c = Kit._clipRect
if c and (x >= c.x + c.w or y >= c.y + c.h or x + w <= c.x or y + h <= c.y) then
return
end
a[#a + 1] = {
class = class,
x = x,
@@ -291,6 +294,7 @@ local function audit(class, x, y, w, h, label)
w = w,
h = h,
label = tostring(label or ""),
id = id,
clip = c and { x = c.x, y = c.y, w = c.w, h = c.h } or nil,
}
end
@@ -562,7 +566,7 @@ function Kit.button(x, y, w, h, label, opts)
opts.face = "selection"
end
opts.icon = opts.icon or ACTION_ICONS[label]
audit("control", x, y, w, h, label)
audit("control", x, y, w, h, label, opts.id)
local hot = opts.enabled ~= false and Kit.hover(x, y, w, h)
local shown = label
if opts.iconOnly then
@@ -738,6 +742,9 @@ function Kit.textfield(id, x, y, w, h, value, placeholder, opts)
Kit.blur() -- commit/cancel also lowers the soft keyboard (#529)
focused = false
elseif e == "\27" then
if opts and opts.onCancel then
opts.onCancel()
end
Kit.blur()
focused = false
else
+5 -2
View File
@@ -35,8 +35,11 @@ If the file isn't there (or you want another copy), use **Open...**, drop a
Party, Boxes, Items, Events, Map, Pokédex, Trainer and Checks are separate pages.
The Pokemon inspector has Main, Stats, Moves, Origin, Extras and Checks sections.
Phones open the roster and inspector as separate sliding pages; wide windows
show them side by side. Buttons share the launcher's painter and font scale,
Wide windows show the roster beside the inspector; phones stack a party grid
above it on one scrolling page. The inspector opens with the Pokemon's sprite,
types, level and HP, then section tabs (a dropdown when the tabs do not fit),
the section form and a row of tools for that section. Numbers have a typed
value box and a slider that commits when released. Buttons share the launcher's painter and font scale,
with a minimum 44px target. Pages and screen sections use dropdown popup
choosers on both phones and desktop. The current page stays in place behind
the scrim; choosing a destination closes the popup and slides to that section.
+24 -6
View File
@@ -1,5 +1,3 @@
-- Species art and the inspector shell. InspectorBody owns the six forms;
-- phones open them as pages and wide windows dock them next to the roster.
local Theme = require("Theme")
local PAL = Theme.PAL
@@ -91,10 +89,30 @@ function MonEditor.drawSprite(S, Kit, species, x, y, size, mon)
y + size / 2 - Kit.textHeight("micro") / 2, size, PAL.muted)
end
-- The inspector uses sections so a phone gives each editing task its full height.
function MonEditor.draw(S, Kit, x, y, w, h)
Kit.card(x, y, w, h)
require("InspectorBody").draw(S, Kit, x, y, w, h)
function MonEditor.isShiny(S, mon)
if not mon then return false end
local g = Gen.ofState(S)
if g == 3 then
if mon.isShiny ~= nil then return mon.isShiny == true end
local ok, shiny = pcall(require("src.core.game3.pokemon").isShiny, mon)
return ok and shiny == true
elseif g == 2 then
return require("src.pokemon.Stats").isShiny(mon.dvs)
end
return false
end
function MonEditor.displayName(S, mon)
if mon.nickname and mon.nickname ~= "" then return mon.nickname end
local def = S.data and S.data.pokemon and S.data.pokemon[mon.species or mon.speciesId]
if def and def.name then return def.name end
if type(mon.species) == "string" then return mon.species end
if type(mon.name) == "string" and mon.name ~= "" then return mon.name end
return tostring(mon.species or "POKEMON")
end
function MonEditor.draw(S, Kit, x, y, w, h, prelude)
require("InspectorBody").draw(S, Kit, x, y, w, h, prelude)
end
return MonEditor
+107 -128
View File
@@ -1,6 +1,3 @@
-- Party roster and inspector: side by side when wide, sliding pages on phones.
-- Each viewport scrolls with fixed readable controls.
local PartyMod = require("src.pokemon.Party")
local Theme = require("Theme")
local Ops = require("Ops")
@@ -8,15 +5,9 @@ local MonEditor = require("MonEditor")
local PAL = Theme.PAL
local Party = {}
local Motion = require("Motion")
-- Roster column width: the design's 460px at the reference size, but it gives
-- ground to the inspector on a narrow window so neither column collapses.
-- The 300px floor used to be absolute, which on a phone handed the roster
-- three quarters of the panel and left the inspector laying itself out at a
-- negative width (#497); the floor now yields to a share of what there is.
local function rosterWidth(w, s)
return math.max(math.min(300 * s, w * 0.42), math.min(460 * s, w * 0.36))
return Theme.clamp(w * 0.27, 250 * s, 340 * s)
end
-- HP colour follows the game's own health bar thresholds.
@@ -30,44 +21,63 @@ local function hpColor(frac)
return PAL.green
end
local function hpFrac(mon)
local maxHp = mon.maxHp or (mon.stats and mon.stats.hp) or 1
return Ops.clamp((tonumber(mon.hp) or 0) / math.max(maxHp, 1), 0, 1), maxHp
end
local function toolbar(S, Kit, x, y, w, vertical)
local s, row, gap = Kit.scale, Kit.controlH(), 8 * Kit.scale
local selected = S.save.party[S.selectedParty or 0] ~= nil and S.editingMon == S.save.party[S.selectedParty]
local n = #S.save.party
if Kit.iconButton(x, y, row, row, vertical and "arrow-up" or "arrow-left", "Move up",
{ enabled = selected and S.selectedParty > 1 }) then
Ops.partyMove(S, -1)
end
if Kit.iconButton(x + row + gap, y, row, row, vertical and "arrow-down" or "arrow-right", "Move down",
{ enabled = selected and S.selectedParty < n }) then
Ops.partyMove(S, 1)
end
local armed = Ops.armLabel(S, "party-remove", "Remove") ~= "Remove"
local removeW = armed and Kit.buttonWidth("Confirm?", { font = "button", icon = "check" }, row) or row
local rx = x + w - removeW
if armed then
if Kit.button(rx, y, removeW, row, "Confirm?", { kind = "danger", font = "button", icon = "check" }) then
Ops.partyRemove(S)
end
elseif Kit.iconButton(rx, y, row, row, "trash", "Remove", { kind = "danger", enabled = selected }) then
Ops.partyRemove(S)
end
local addOpts = { font = "button", icon = "plus", enabled = n < PartyMod.MAX }
local addW = Kit.buttonWidth("Add", addOpts, row)
if Kit.button(rx - gap - addW, y, addW, row, "Add", addOpts) then
Ops.partyAdd(S)
end
return row
end
local function drawRoster(S, Kit, x, y, listW, h)
local s = Kit.scale
Kit.card(x, y, listW, h)
local pad = 18 * s
local pad = 16 * s
local cx = x + pad
local innerW = listW - 2 * pad
Kit.caption(cx, y + pad, "PARTY")
Kit.textRight(
"mono",
("%d/%d"):format(#S.save.party, PartyMod.MAX),
cx + innerW,
y + pad,
PAL.caption
)
Kit.textRight("mono", ("%d/%d"):format(#S.save.party, PartyMod.MAX), cx + innerW, y + pad, PAL.caption)
-- "+ Add mon" / "Remove" pinned to the card bottom; the roster fills above.
local actH = Kit.controlH()
local actY = y + h - pad - actH
S.toastBottom = math.min(S.toastBottom or actY, actY)
local listTop = y + pad + Kit.textHeight("caption") + 12 * s
local listH = actY - 12 * s - listTop
local listTop = y + pad + Kit.textHeight("caption") + 14 * s
local listH = actY - 14 * s - listTop
if #S.save.party == 0 then
Kit.emptyBox(
cx,
listTop,
innerW,
listH,
"Party is empty - Add creates a Lv5 mon owned by the save's player."
)
Kit.emptyBox(cx, listTop, innerW, listH, "Party is empty. Add creates a Lv5 Pokemon owned by the player.")
else
local rowH = math.max(88 * s, Kit.tapMin() + 38 * s)
local rowH = math.max(76 * s, Kit.tapMin() + 28 * s)
local rowGap = 8 * s
S.selectedParty = Ops.clamp(S.selectedParty or 1, 1, #S.save.party)
-- 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 drawn, shift = Kit.list(S, "partyOffset", cx, listTop, innerW, listH, #S.save.party, rowH + rowGap)
Kit.pushClip(cx, listTop, innerW, listH)
for i = 1, drawn do
@@ -78,120 +88,89 @@ local function drawRoster(S, Kit, x, y, listW, h)
end
local ry = listTop + (i - 1) * (rowH + rowGap) - shift
local selected = (S.editingMon == mon)
if Kit.row(cx, ry, innerW, rowH, selected, PAL.green) then
if Kit.row(cx, ry, innerW, rowH, selected, PAL.text) then
Ops.selectParty(S, slot)
Motion.change(S, "mobileInspector", true, 1)
end
local rpad = 12 * s
local icon = 44 * s
local rpad = 10 * s
local icon = math.min(56 * s, rowH - 12 * s)
MonEditor.drawSprite(S, Kit, mon.species, cx + rpad, ry + (rowH - icon) / 2, icon, mon)
-- right cluster first, so the name knows how much room it has left
local rightW = 52 * s
local chipH = 20 * s
local lvText = ("Lv%d"):format(mon.level)
local chipW = Kit.textWidth("tiny", lvText) + 14 * s
local chipX = cx + innerW - rpad - chipW
Theme.stroke(chipX, ry + 10 * s, chipW, chipH, 6 * s, PAL.cardBorder, 0.3, 1)
Kit.textCenter(
"tiny",
lvText,
chipX,
ry + 10 * s + (chipH - Kit.textHeight("tiny")) / 2,
chipW,
PAL.blueInk
)
if selected then
local ab = Kit.tapMin()
local aw = Kit.tapMin()
local ax = cx + innerW - rpad - 2 * aw - 4 * s
local ay = ry + rowH - 10 * s - ab
if Kit.stepper(ax, ay, aw, ab, "up", { font = "tiny" }) then
Ops.partyMove(S, -1)
end
if Kit.stepper(ax + aw + 4 * s, ay, aw, ab, "down", { font = "tiny" }) then
Ops.partyMove(S, 1)
end
rightW = 2 * aw + 4 * s + rpad
local tx = cx + rpad + icon + 10 * s
local tw = math.max(40 * s, cx + innerW - rpad - tx)
local lvText = ("Lv %d"):format(mon.level or 0)
local lvW = Kit.textWidth("small", lvText)
local nameY = ry + 12 * s
Kit.textRight("small", lvText, tx + tw, nameY + (Kit.textHeight("monoRow") - Kit.textHeight("small")) / 2, PAL.muted)
local shiny = MonEditor.isShiny(S, mon)
local spark = shiny and Kit.textHeight("small") or 0
local name = Kit.ellipsize("monoRow", MonEditor.displayName(S, mon), tw - lvW - spark - 16 * s)
Kit.textBold("monoRow", name, tx, nameY, PAL.heading)
if shiny then
Kit.icon("sparkles", tx + Kit.textWidth("monoRow", name) + 6 * s,
nameY + (Kit.textHeight("monoRow") - spark) / 2, spark, PAL.yellow)
end
local tx = cx + rpad + icon + 12 * s
local tw = math.max(40 * s, (cx + innerW - rightW - 10 * s) - tx)
local monName = (mon.nickname and mon.nickname ~= "" and mon.nickname)
or (type(mon.species) == "string" and mon.species)
or (type(mon.name) == "string" and mon.name ~= "" and mon.name)
or (S.data and S.data.pokemon and S.data.pokemon[mon.species or mon.speciesId] and S.data.pokemon[mon.species or mon.speciesId].name)
or tostring(mon.species or "POKEMON")
local name = Kit.ellipsize("monoRow", monName, tw - 34 * s)
Kit.text("monoRow", name, tx, ry + 10 * s, PAL.heading)
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
local frac = Ops.clamp((mon.hp or 0) / math.max(maxHp, 1), 0, 1)
Kit.meter(tx, ry + rowH / 2 + 2 * s, tw, 6 * s, frac * 100, hpColor(frac))
Kit.text(
"tiny",
("HP %d/%d"):format(mon.hp or 0, maxHp),
tx,
ry + rowH - 10 * s - Kit.textHeight("tiny"),
PAL.muted
)
local frac, maxHp = hpFrac(mon)
Kit.meter(tx, ry + rowH / 2 + 1 * s, tw, 5 * s, frac * 100, hpColor(frac))
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.listScrollbar(S, "partyOffset", cx, listTop, innerW, listH)
end
local halfW = (innerW - 10 * s) / 2
if
Kit.button(
cx,
actY,
halfW,
actH,
"Add mon",
{ font = "small", radius = 9 * s, enabled = #S.save.party < PartyMod.MAX }
)
then
Ops.partyAdd(S)
end
if
Kit.button(
cx + halfW + 10 * s,
actY,
halfW,
actH,
Ops.armLabel(S, "party-remove", "Remove"),
{ kind = "danger", font = "small", radius = 9 * s }
)
then
Ops.partyRemove(S)
Theme.col(PAL.cardBorder, 0.22)
love.graphics.rectangle("fill", x, actY - 8 * s, listW, 1)
toolbar(S, Kit, cx, actY, innerW, true)
end
local function drawStrip(S, Kit, x, y, w)
local s = Kit.scale
local gap = 10 * s
local cy = y
Kit.caption(x, cy, "PARTY")
Kit.textRight("mono", ("%d/%d"):format(#S.save.party, PartyMod.MAX), x + w, cy, PAL.caption)
cy = cy + Kit.textHeight("caption") + 12 * s
local n = #S.save.party
if n == 0 then
local boxH = 80 * s
Kit.emptyBox(x, cy, w, boxH, "Party is empty")
cy = cy + boxH + gap
else
local cols = Theme.clamp(math.floor((w + gap) / (92 * s + gap)), 3, 6)
local tileW = (w - (cols - 1) * gap) / cols
local sprite = math.floor(math.min(tileW - 20 * s, 64 * s))
local tileH = math.ceil(10 * s + sprite + 4 * s + Kit.textHeight("tiny") + 8 * s + 4 * s + 10 * s)
for i, mon in ipairs(S.save.party) do
local col, line = (i - 1) % cols, math.floor((i - 1) / cols)
local tx, ty = x + col * (tileW + gap), cy + line * (tileH + gap)
if Kit.row(tx, ty, tileW, tileH, S.editingMon == mon, PAL.text) then
Ops.selectParty(S, i)
end
MonEditor.drawSprite(S, Kit, mon.species, tx + (tileW - sprite) / 2, ty + 10 * s, sprite, mon)
local ly = ty + 10 * s + sprite + 4 * s
Kit.textCenter("tiny", ("Lv %d"):format(mon.level or 0), tx, ly, tileW, PAL.muted)
if MonEditor.isShiny(S, mon) then
local spark = 14 * s
Kit.icon("sparkles", tx + tileW - spark - 8 * s, ty + 8 * s, spark, PAL.yellow)
end
local frac = hpFrac(mon)
local barW = tileW - 28 * s
Kit.meter(tx + 14 * s, ly + Kit.textHeight("tiny") + 8 * s, barW, 4 * s, frac * 100, hpColor(frac))
end
cy = cy + math.ceil(n / cols) * (tileH + gap)
end
cy = cy + toolbar(S, Kit, x, cy, w, false)
return cy - y
end
function Party.draw(S, Kit, x, y, w, h)
local s = Kit.scale
local gap = 20 * s
S.inspectorBack = false
if w < 800 * s then
if S.mobileInspector == nil then
S.mobileInspector = S.editingMon ~= nil
end
Motion.pages(S, Kit, "mobileInspector", x, y, w, h, function(state, kit, dx, dy, dw, dh)
if state.editingMon and state.mobileInspector then
state.inspectorBack = true
MonEditor.draw(state, kit, dx, dy, dw, dh)
else
drawRoster(state, kit, dx, dy, dw, dh)
end
MonEditor.draw(S, Kit, x, y, w, h, function(px, py, pw)
return drawStrip(S, Kit, px, py, pw)
end)
else
S.inspectorBack = false
local gap = 14 * s
local listW = rosterWidth(w, s)
drawRoster(S, Kit, x, y, listW, h)
MonEditor.draw(S, Kit, x + listW + gap, y, w - listW - gap, h)