mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 01:34:06 -04:00
691 lines
38 KiB
Lua
691 lines
38 KiB
Lua
local Kit = require("src.ui.kit.Kit")
|
||
local Theme = require("src.ui.kit.Theme")
|
||
local Store = require("src.box.Store")
|
||
local Collection = require("src.box.Collection")
|
||
local UI = require("src.import.BoxUI")
|
||
local Icons = require("src.ui.kit.Icons")
|
||
local PAL = Theme.PAL
|
||
local Tools = {}
|
||
Tools.PAGES = { "List", "Arrange", "Dashboard", "Teams", "Gifts", "Showcase", "Files", "Events", "Migration" }
|
||
Tools.SORTS = { "slot", "name", "species", "level", "hp", "attack", "defense", "speed",
|
||
"special", "spAtk", "spDef", "nature", "ability", "gender", "trainer", "friendship" }
|
||
Tools.SORT_LABELS = { slot = "Slot order", name = "Nickname", species = "Species", level = "Level",
|
||
hp = "HP", attack = "Attack", defense = "Defense", speed = "Speed", special = "Special",
|
||
spAtk = "Sp. Attack", spDef = "Sp. Defense", nature = "Nature", ability = "Ability",
|
||
gender = "Gender", trainer = "Trainer", friendship = "Friendship" }
|
||
Tools.CONTROL_ICONS = { ["stage-slot"] = "save", ["stage-background"] = "paintbrush", ["stage-pattern"] = "grid-2x2",
|
||
["stage-music"] = "music", ["stage-new-piece"] = "package", ["stage-x"] = "arrow-left-right",
|
||
["stage-y"] = "arrow-up-down", ["stage-size"] = "expand", ["stage-rotation"] = "rotate-ccw",
|
||
sort = "arrow-up-down", ["arrange-sort"] = "arrow-up-down", ["migration-destination"] = "gamepad-2" }
|
||
local ACTION_ICONS = { ["migration-preview"] = "eye", ["exports-folder"] = "folder", arrange = "arrow-up-down",
|
||
["apply-marks"] = "check", compare = "arrow-left-right", ["stage-wallpaper"] = "paintbrush", ["stage-flip"] = "arrow-left-right" }
|
||
|
||
local function search(s, query, sort)
|
||
local cache = s._toolsSearch
|
||
if not cache or cache.state ~= s.service.state or cache.size >= 8 then
|
||
cache = { state = s.service.state, results = {}, size = 0 }; s._toolsSearch = cache
|
||
end
|
||
local key = tostring(query) .. "\0" .. tostring(sort)
|
||
if not cache.results[key] then
|
||
cache.results[key], cache.size = Store.search(s.service.state, query, sort), cache.size + 1
|
||
end
|
||
return cache.results[key]
|
||
end
|
||
|
||
local function same(a, b)
|
||
if a == b then return true end
|
||
if type(a) ~= "table" or type(b) ~= "table" then return false end
|
||
for k, v in pairs(a) do if not same(v, b[k]) then return false end end
|
||
for k in pairs(b) do if a[k] == nil then return false end end
|
||
return true
|
||
end
|
||
|
||
local function userExport(s, path, index)
|
||
local fs = s.service.fs
|
||
local bytes = fs.read(path)
|
||
if not bytes then return nil, "Could not read the exported PNG." end
|
||
if not fs.createDirectory("box/exports") then return nil, "Could not create the export folder." end
|
||
local out = "box/exports/showcase-" .. index .. "-" .. os.time() .. ".png"
|
||
local ok, why = fs.write(out, bytes)
|
||
if not ok or fs.read(out) ~= bytes then return nil, why or "Could not verify the exported PNG." end
|
||
local used = false
|
||
for _, box in ipairs(s.service.state.boxes) do if box.wallpaper == path then used = true end end
|
||
if not used and fs.remove and fs.remove(path) then require("src.box.Themes").forget(path) end
|
||
return out
|
||
end
|
||
|
||
local function findEntry(s, id)
|
||
local cache = s._toolsIndex
|
||
if not cache or cache.state ~= s.service.state then
|
||
cache = { state = s.service.state, byId = {} }; s._toolsIndex = cache
|
||
for b = 1, Store.BOXES do
|
||
for slot, entry in pairs(s.service.state.boxes[b].mons) do
|
||
cache.byId[entry.id] = { box = b, slot = slot, entry = entry }
|
||
end
|
||
end
|
||
end
|
||
return cache.byId[id]
|
||
end
|
||
|
||
function Tools.navigation(imp, s, x, y, w, m)
|
||
local gap, h = 8 * m.s, math.max(Kit.tapMin(), m.btnH)
|
||
local navigation = { { id = "Pokémon", label = "Boxes", icon = "package" } }
|
||
for _, page in ipairs(Tools.PAGES) do navigation[#navigation + 1] = { id = page, label = page, icon = UI.ICONS[page] } end
|
||
local helpKey = s.toolsPage or "Box"
|
||
UI.dropdown(imp, x, y, UI.HELP[helpKey] and w - h - gap or w, h, "tools-menu", "Tools: " .. (s.toolsPage or "Boxes"), navigation,
|
||
s.toolsPage or "Pokémon", function(value)
|
||
require("src.box.Showcase").stopMusic(); s.stagePlaying = nil
|
||
UI.change(imp, s, "toolsPage", value ~= "Pokémon" and value or nil, value == "Pokémon" and -1 or 1)
|
||
s.toolsListPage = 1
|
||
end, UI.ICONS[s.toolsPage] or "package")
|
||
if UI.HELP[helpKey] then UI.help(imp, helpKey, x + w - h, y, h) end
|
||
return h + gap
|
||
end
|
||
|
||
function Tools.draw(imp, s, x, y, w, m, api)
|
||
local top, gap, h = y, 8 * m.s, math.max(Kit.tapMin(), m.btnH)
|
||
local function button(id, label, action, disabled, width, bx, active, icon)
|
||
icon = icon or ACTION_ICONS[id] or (id:match("save") and "save" or id:match("remove") and "trash"
|
||
or id:match("export") and "upload" or id:match("import") and "download"
|
||
or id:match("select") and "check" or id:match("restore") and "undo-2"
|
||
or id:match("name") and "pencil" or UI.ICONS[s.toolsPage] or "sliders-horizontal")
|
||
api.button(imp, bx or x, y, width or w, h, "tools-" .. id, label, action, active, disabled, icon)
|
||
end
|
||
local function text(value, muted)
|
||
y = y + Kit.textWrapped("small", value, x, y, w, muted and PAL.muted or PAL.text) + gap
|
||
end
|
||
local function nextRow() y = y + h + gap end
|
||
if not api.navigationDone then y = y + Tools.navigation(imp, s, x, y, w, m) end
|
||
local page = s.toolsPage
|
||
if not page then return y - top, false end
|
||
local refs = api.selected(s.selectedBox)
|
||
local function operation(fn, message) api.perform(imp, fn, message) end
|
||
local function namePrompt(title, initial, callback)
|
||
require("src.import.BoxPrompt").open(imp, title, initial, 64, callback)
|
||
end
|
||
local function cycle(id, label, values, current, apply, column)
|
||
local icon = Tools.CONTROL_ICONS[id] or UI.ICONS[page]
|
||
local function display(value)
|
||
if Tools.SORT_LABELS[value] then return Tools.SORT_LABELS[value] end
|
||
if require("src.core.GameVersion").VERSIONS[value] then return require("src.core.GameVersion").info(value).label end
|
||
if id == "stage-x" or id == "stage-y" then return math.floor(value * 100 + .5) .. "%" end
|
||
if id == "stage-rotation" then return math.floor(value * 180 / math.pi + .5) .. "°" end
|
||
return tostring(value)
|
||
end
|
||
local options = {}; for _, value in ipairs(values) do options[#options + 1] = { id = value, label = display(value), icon = icon } end
|
||
local cw = column and (w - gap) / 2 or w
|
||
UI.dropdown(imp, x + (column == 2 and cw + gap or 0), y, cw, h, "tools-" .. id,
|
||
label .. ": " .. display(current), options, current, apply, icon)
|
||
if column ~= 1 then nextRow() end
|
||
end
|
||
local function actions(items)
|
||
local cw = (w - gap) / 2
|
||
for i, item in ipairs(items) do
|
||
button(item[1], item[2], item[3], item[4], cw, x + (i - 1) % 2 * (cw + gap), item[5], item[6])
|
||
if i % 2 == 0 or i == #items then nextRow() end
|
||
end
|
||
end
|
||
local function sections(key, names)
|
||
s[key] = s[key] or names[1]
|
||
cycle(key, "View", names, s[key], function(value) UI.change(imp, s, key, value) end)
|
||
return s[key]
|
||
end
|
||
local function tiles(rows)
|
||
local cols = w >= 700 * m.s and 4 or 2
|
||
local cw, cardH = (w - (cols - 1) * gap) / cols, 86 * m.s
|
||
for i, row in ipairs(rows) do
|
||
local cx = x + (i - 1) % cols * (cw + gap)
|
||
Theme.card(cx, y, cw, cardH, { shadow = true })
|
||
Icons.draw(row.icon or "package", cx + 12 * m.s, y + 12 * m.s, 22 * m.s, PAL.blue, 1)
|
||
Kit.textBold("button", tostring(row.value), cx + 42 * m.s, y + 12 * m.s, PAL.heading)
|
||
Kit.text("small", row.label, cx + 12 * m.s, y + cardH - 29 * m.s, PAL.muted)
|
||
if i % cols == 0 or i == #rows then y = y + cardH + gap end
|
||
end
|
||
end
|
||
local function picker()
|
||
local options = {}
|
||
for b = 1, Store.BOXES do
|
||
local box = s.service.state.boxes[b]
|
||
local occupied = 0
|
||
for _ in pairs(box.mons) do occupied = occupied + 1 end
|
||
options[#options + 1] = { id = b, label = box.name .. " · " .. occupied .. "/60", icon = "package" }
|
||
end
|
||
UI.dropdown(imp, x, y, w, h, "tools-box-picker", "Box: " .. s.service.state.boxes[s.box].name,
|
||
options, s.box, function(b) UI.change(imp, s, "toolsPage", nil, -1); s.box, s.pageBox, s.view, s.railFirst = b, 1, "box", nil end, "package")
|
||
nextRow()
|
||
end
|
||
if page == "Migration" then
|
||
local Migration=require("src.box.Migration")
|
||
local GameVersion=require("src.core.GameVersion")
|
||
local versions={}
|
||
for _,version in ipairs(GameVersion.ORDER) do
|
||
if require("src.box.Catalog").get(version).ready then versions[#versions+1]=version end
|
||
end
|
||
local selectionKey={}
|
||
for _,ref in ipairs(refs) do selectionKey[#selectionKey+1]=ref.box..":"..ref.slot end
|
||
selectionKey=table.concat(selectionKey,",")
|
||
if s.migrationPreviewKey~=selectionKey or s.migrationPreviewState~=s.service.state then
|
||
s.migrationPreview,s.migrationPreviewKey,s.migrationPreviewState=nil,selectionKey,s.service.state
|
||
end
|
||
if #versions>0 then
|
||
s.migrationVersion=s.migrationVersion or versions[1]
|
||
cycle("migration-destination","Destination",versions,s.migrationVersion,function(value)
|
||
s.migrationVersion,s.migrationPreview=value,nil
|
||
end)
|
||
button("migration-preview","Review "..#refs.." selected",function()
|
||
local preview,why=Migration.preview(s.service.state,refs,s.migrationVersion)
|
||
s.migrationPreview=preview;if not preview then s.notice=why end
|
||
end,#refs==0);nextRow()
|
||
local preview=s.migrationPreview
|
||
if preview then
|
||
local options, ready = {}, 0
|
||
for i, row in ipairs(preview.rows) do
|
||
options[#options + 1] = { id = i, label = row.name .. (row.converted and " · Ready" or " · Blocked"), icon = row.converted and "check" or "lock" }
|
||
if row.converted then ready = ready + 1 end
|
||
end
|
||
text(ready .. " ready · " .. (#preview.rows - ready) .. " blocked")
|
||
s.migrationReviewRow = math.min(s.migrationReviewRow or 1, #preview.rows)
|
||
local row = preview.rows[s.migrationReviewRow]
|
||
if row then
|
||
UI.dropdown(imp, x, y, w, h, "migration-record", row.name, options, s.migrationReviewRow,
|
||
function(value) s.migrationReviewRow = value end, "eye"); nextRow()
|
||
for _, line in ipairs(row.lines) do text(line, true) end
|
||
end
|
||
button("migration-apply","Convert selected",function()
|
||
operation(function() return s.service:migrate(refs,s.migrationVersion,preview) end,"Migration saved with archived originals.")
|
||
s.migrationPreview=nil
|
||
end,not preview.allowed or preview.revision~=s.service.state.revision or s.diskStale);nextRow()
|
||
end
|
||
else text("Import both games first.",true) end
|
||
if #refs==1 then
|
||
local entry=Store.at(s.service.state,refs[1].box,refs[1].slot)
|
||
local originals = entry and entry.archives or {}
|
||
if #originals > 0 then
|
||
s.archiveIndex = math.min(s.archiveIndex or 1, #originals)
|
||
local options = {}; for i, original in ipairs(originals) do options[i] = { id = i,
|
||
label = GameVersion.info(original.version).label .. " · " .. original.display.name, icon = "undo-2" } end
|
||
UI.dropdown(imp, x, y, w, h, "migration-archive", "Archived original", options, s.archiveIndex,
|
||
function(value) s.archiveIndex = value end, "undo-2"); nextRow()
|
||
button("migration-restore","Restore original",function()
|
||
operation(function() return s.service:restoreOriginal(refs[1],s.archiveIndex) end,"Original restored.")
|
||
s.migrationPreview=nil
|
||
end);nextRow()
|
||
end
|
||
end
|
||
UI.help(imp, "MigrationPolicy", x + w - h, y, h); nextRow()
|
||
elseif page == "Events" then
|
||
local Events=require("src.box.Events")
|
||
local source=s.sources[s.sourceIndex]
|
||
local rows=source and Events.list(source.version,2) or {}
|
||
if #rows==0 then text("No tickets for this game.",true) end
|
||
local cache=s._eventStatus
|
||
if not cache or cache.body~=s.pcBody or cache.version~=(source and source.version) then
|
||
cache={body=s.pcBody,version=source and source.version,statuses={}};s._eventStatus=cache
|
||
for _,row in ipairs(rows) do
|
||
local status,why
|
||
if s.pcSave then status,why=Events.status(s.pcSave,row) end
|
||
cache.statuses[row.id]={status=status,why=why}
|
||
end
|
||
end
|
||
local options = {}; for _, row in ipairs(rows) do options[#options + 1] = { id = row.id, label = row.name, icon = "mail" } end
|
||
if #rows > 0 then
|
||
local chosen = rows[1]
|
||
for _, row in ipairs(rows) do if row.id == s.eventChoice then chosen = row end end
|
||
s.eventChoice = chosen.id
|
||
UI.dropdown(imp, x, y, w, h, "event-picker", chosen.name, options, chosen.id,
|
||
function(value) s.eventChoice = value end, "mail"); nextRow()
|
||
local row = chosen
|
||
local status=cache.statuses[row.id].status or "Unavailable"
|
||
text(row.route .. " · " .. status)
|
||
local pending=status=="Ticket pending"
|
||
button("event-"..row.id,pending and "Collect ticket" or row.method=="Record mixing"
|
||
and "Receive ticket" or "Receive card",function()
|
||
operation(function() return s.service:event(source,row.id,pending) end,(pending or row.method=="Record mixing")
|
||
and "Ticket collected. Normal ship requirements still apply." or "Wonder Card received. Collect its ticket here or from the delivery person in-game.")
|
||
end,status~="Available" and not pending);nextRow()
|
||
if cache.statuses[row.id].why then
|
||
UI.help(imp, "Why is this locked?", x, y, h, cache.statuses[row.id].why); nextRow()
|
||
end
|
||
end
|
||
elseif page == "Files" then
|
||
local export = function()
|
||
local ok, path = s.service:exportGCI()
|
||
s.notice = ok and "Exported and verified: " .. path or path
|
||
end
|
||
actions({ { "gci-import", "Import Box save", function() imp:chooseBoxImport() end },
|
||
{ "gci-export", "Export GCI", export, not s.service.state.gciTemplate } })
|
||
button("exports-folder", "Exports folder", function()
|
||
if love.system and love.system.openURL then
|
||
if s.service.fs.createDirectory then s.service.fs.createDirectory("box/exports") end
|
||
love.system.openURL("file://"..love.filesystem.getSaveDirectory().."/box/exports")
|
||
end
|
||
end, imp.android or imp.ios or imp.isNX); nextRow()
|
||
local sync = imp._syncEngine and imp:_syncEngine()
|
||
local busy = sync and sync:busy() or false
|
||
local cached = s._latestBackup
|
||
if not cached or cached.sync ~= sync or cached.busy ~= busy then
|
||
cached = { sync = sync, busy = busy, path = not busy and sync and sync.box and sync.box:latestBackup() or nil }
|
||
s._latestBackup = cached
|
||
end
|
||
local backup = cached.path
|
||
button("sync-restore", "Restore sync backup", function()
|
||
s._latestBackup = nil
|
||
local ok, why = sync:restoreBoxBackup(backup)
|
||
if ok then require("src.import.BoxPanel").refresh(imp).notice = "Box and linked saves restored."
|
||
else s.notice = why end
|
||
end, not backup or sync:busy(), w - h - gap)
|
||
UI.help(imp, "Backup", x + w - h, y, h); nextRow()
|
||
elseif page == "Showcase" then
|
||
local Showcase = require("src.box.Showcase")
|
||
local fullX, fullW = x, w
|
||
local beside = w >= 740 * m.s
|
||
if beside then w = (w - 2 * gap) * .48 end
|
||
s.stageIndex = s.stageIndex or 1
|
||
local function loadStage(index)
|
||
Showcase.stopMusic()
|
||
s.stagePlaying = nil
|
||
s.stageDrafts=s.stageDrafts or {}
|
||
if s.stageDraft then s.stageDrafts[s.stageIndex]=s.stageDraft end
|
||
s.stageIndex, s.stagePiece = index, 1
|
||
s.stageDraft = s.stageDrafts[index] or Store.copy(s.service.state.stages and s.service.state.stages[index] or Showcase.new())
|
||
s.stageUndo = {}
|
||
end
|
||
if not s.stageDraft then loadStage(s.stageIndex) end
|
||
local function edit(fn)
|
||
local before = Store.copy(s.stageDraft)
|
||
fn(s.stageDraft)
|
||
if same(before, s.stageDraft) then return end
|
||
s.stageUndo = s.stageUndo or {}
|
||
s.stageUndo[#s.stageUndo+1] = before
|
||
if #s.stageUndo>20 then table.remove(s.stageUndo,1) end
|
||
end
|
||
cycle("stage-slot", "Saved stage", {1,2,3,4,5}, s.stageIndex, loadStage)
|
||
local draft = s.stageDraft
|
||
text(draft.name)
|
||
local previewH = math.max(190*m.s, math.min(300 * m.s, w*.5))
|
||
require("src.import.ShowcaseEditor").draw(imp,s,x,y,w,previewH,m,edit)
|
||
local previewBottom = y + previewH + gap
|
||
if beside then x, y, w = fullX + w + 2 * gap, top, fullW - w - 2 * gap
|
||
else y = previewBottom end
|
||
local function addOptions()
|
||
local out={}
|
||
if #refs>0 then out[#out+1]={label="Add "..#refs.." selected Pokémon",icon="plus",action=function()
|
||
local added,why=Showcase.add(s.stageDraft,refs,s.service.state)
|
||
if added then edit(function() s.stageDraft=added end);s.stagePiece=#added.pieces else s.notice=why end
|
||
end} end
|
||
for _,row in ipairs(search(s,s.query,"species")) do
|
||
out[#out+1]={label=(row.entry.display.national and ("#"..row.entry.display.national.." · ") or "")..
|
||
row.entry.display.name,entry=row.entry,icon="package",action=function()
|
||
local added,why=Showcase.add(s.stageDraft,{{box=row.box,slot=row.slot}},s.service.state)
|
||
if added then edit(function() s.stageDraft=added end);s.stagePiece=#added.pieces else s.notice=why end
|
||
end}
|
||
end
|
||
return out
|
||
end
|
||
UI.dropdown(imp,x,y,(w-gap)/2,h,"stage-add-pokemon","Add Pokémon",addOptions,nil,nil,"plus")
|
||
local scenery={}
|
||
for _,kind in ipairs(Showcase.PIECES) do scenery[#scenery+1]={label=kind,prop=kind,icon="package",action=function()
|
||
if #s.stageDraft.pieces>=1500 then s.notice="The stage is full.";return end
|
||
edit(function(d) d.pieces[#d.pieces+1]={kind=kind,x=.5,y=.5,scale=1.5,rotation=0,flip=false} end)
|
||
s.stagePiece=#s.stageDraft.pieces
|
||
end} end
|
||
UI.dropdown(imp,x+(w+gap)/2,y,(w-gap)/2,h,"stage-add-scenery","Add scenery",scenery,nil,nil,"plus")
|
||
nextRow()
|
||
local function layerOptions()
|
||
local out={}
|
||
for i=#s.stageDraft.pieces,1,-1 do
|
||
local p=s.stageDraft.pieces[i]
|
||
local row=p.entryId and findEntry(s,p.entryId)
|
||
out[#out+1]={id=i,label=i.." · "..(row and row.entry.display.name or p.kind or "Outside Box"),
|
||
icon="package",entry=row and row.entry}
|
||
end
|
||
return out
|
||
end
|
||
UI.dropdown(imp,x,y,w-2*(h+gap),h,"stage-layers","Layers · front to back",layerOptions,s.stagePiece,
|
||
function(v) s.stagePiece=v end,"copy")
|
||
local layer=s.stagePiece
|
||
if layer and s.stageDraft.pieces[layer] then
|
||
button("stage-back","",function() edit(function(d) s.stagePiece=Showcase.reorder(d,layer,layer-1) end) end,
|
||
layer==1,h,x+w-2*h-gap,false,"arrow-down")
|
||
button("stage-front","",function() edit(function(d) s.stagePiece=Showcase.reorder(d,layer,layer+1) end) end,
|
||
layer==#s.stageDraft.pieces,h,x+w-h,false,"arrow-up")
|
||
end
|
||
nextRow()
|
||
local stageSection = sections("stageSection", { "Scene", "Placement", "Export" })
|
||
if stageSection == "Scene" then
|
||
cycle("stage-background", "Background", Showcase.BACKGROUNDS, draft.background, function(value) edit(function(d) d.background=value end) end, 1)
|
||
cycle("stage-pattern", "Pattern", Showcase.PATTERNS, draft.pattern, function(value) edit(function(d) d.pattern=value end) end, 2)
|
||
cycle("stage-music", "Music", Showcase.MUSIC, draft.music, function(value)
|
||
edit(function(d) d.music=value end); Showcase.stopMusic(); s.stagePlaying = nil
|
||
end)
|
||
button("stage-play", s.stagePlaying and "Stop music" or "Play music", function()
|
||
if s.stagePlaying then Showcase.stopMusic(); s.stagePlaying = nil
|
||
else local ok, why = Showcase.playMusic(draft); if not ok then s.notice=why else s.stagePlaying = true end end
|
||
end,draft.music=="Silent",w,nil,false,s.stagePlaying and "square" or "play");nextRow()
|
||
elseif stageSection == "Placement" then
|
||
local function pieceLabel(i)
|
||
local placement = draft.pieces[i]
|
||
local row = placement.entryId and findEntry(s, placement.entryId)
|
||
return i .. " · " .. (row and row.entry.display.name or placement.kind or "Outside Box")
|
||
end
|
||
local function pieces()
|
||
local out = {}
|
||
for i in ipairs(draft.pieces) do out[i] = { id = i, label = pieceLabel(i), icon = "package" } end
|
||
return out
|
||
end
|
||
s.stagePiece = math.min(s.stagePiece or 1, math.max(1, #draft.pieces))
|
||
UI.dropdown(imp, x, y, w, h, "stage-piece", draft.pieces[s.stagePiece] and pieceLabel(s.stagePiece) or "Choose a placement",
|
||
pieces, s.stagePiece, function(value) s.stagePiece = value end, "package"); nextRow()
|
||
local piece=draft.pieces[s.stagePiece or 1]
|
||
if piece then
|
||
local positions={};for i=0,25 do positions[#positions+1]=i/25 end
|
||
cycle("stage-x","X",positions,piece.x,function(value) edit(function() piece.x=value end) end, 1)
|
||
cycle("stage-y","Y",positions,piece.y,function(value) edit(function() piece.y=value end) end, 2)
|
||
cycle("stage-size","Size",{0.25,0.5,0.75,1,1.5,2,3,4},piece.scale,function(value) edit(function() piece.scale=value end) end, 1)
|
||
cycle("stage-rotation","Turn",{-math.pi,-math.pi/2,0,math.pi/2,math.pi},piece.rotation,function(value) edit(function() piece.rotation=value end) end, 2)
|
||
actions({ { "stage-flip", "Flip horizontally", function() edit(function() piece.flip=not piece.flip end) end }, { "stage-remove", "Remove", function()
|
||
edit(function(d) table.remove(d.pieces,s.stagePiece or 1) end);s.stagePiece=math.max(1,math.min(s.stagePiece,#draft.pieces))
|
||
end } })
|
||
else text("Add a pokémon or some furniture first.", true) end
|
||
elseif stageSection == "Export" then
|
||
actions({ { "stage-export", "Export PNG", function()
|
||
local ok,path=s.service:exportStage(s.stageIndex)
|
||
if ok then ok,path=userExport(s,path,s.stageIndex) end
|
||
s.notice=ok and "PNG exported: "..ok or path
|
||
end, not s.service.state.stages or not s.service.state.stages[s.stageIndex] },
|
||
{ "stage-wallpaper", "Use as wallpaper", function()
|
||
operation(function() return s.service:exportStage(s.stageIndex,s.box) end,"Wallpaper saved.")
|
||
end, not s.service.state.stages or not s.service.state.stages[s.stageIndex] } })
|
||
end
|
||
button("stage-save", "Save stage", function()
|
||
operation(function() return s.service:saveStage(s.stageIndex,draft) end,"Showcase saved.")
|
||
end, false, (w - gap) / 2)
|
||
UI.dropdown(imp, x + (w + gap) / 2, y, (w - gap) / 2, h, "stage-options", "Options", {
|
||
{ label = "Save and use as Box theme", icon = "paintbrush", action = function()
|
||
operation(function()
|
||
local ok,why=s.service:saveStage(s.stageIndex,s.stageDraft)
|
||
if not ok then return nil,why end
|
||
return s.service:exportStage(s.stageIndex,s.box)
|
||
end,"Stage saved as this Box's theme.")
|
||
end },
|
||
{ label = "Undo last edit", icon = "undo-2", disabled = not s.stageUndo or #s.stageUndo == 0,
|
||
action = function() s.stageDraft = table.remove(s.stageUndo); s.stagePiece = 1 end },
|
||
{ label = "Rename stage", icon = "pencil", action = function()
|
||
namePrompt("Stage name", draft.name, function(value) edit(function(d) d.name = value end) end)
|
||
end },
|
||
}, nil, nil, "ellipsis"); nextRow()
|
||
y, x, w = math.max(y, previewBottom), fullX, fullW
|
||
elseif page == "Gifts" then
|
||
local source = s.sources[s.sourceIndex]
|
||
local cache = s._giftProgress
|
||
if not cache or cache.source ~= source or cache.state ~= s.service.state or cache.body ~= s.pcBody then
|
||
local progress, why = s.service:giftProgress(source)
|
||
cache = { source = source, state = s.service.state, body = s.pcBody, progress = progress, why = why }
|
||
s._giftProgress = cache
|
||
end
|
||
local progress, why = cache.progress, cache.why
|
||
if not progress then text(why, true)
|
||
else
|
||
text(progress.current .. " stored by this trainer · peak " .. progress.peak)
|
||
local giftCols = w >= 700 * m.s and 4 or 2
|
||
local giftW = (w - (giftCols - 1) * gap) / giftCols
|
||
for index, award in ipairs(require("src.box.Gifts").AWARDS) do
|
||
local status = index < progress.nextAward and "Claimed" or index == progress.nextAward
|
||
and progress.eligible and "Ready" or "Locked"
|
||
local cx, cardH = x + (index - 1) % giftCols * (giftW + gap), 104 * m.s
|
||
Theme.card(cx, y, giftW, cardH, { shadow = true })
|
||
Icons.draw(status == "Locked" and "lock" or status == "Claimed" and "check" or "package",
|
||
cx + 12 * m.s, y + 12 * m.s, 24 * m.s, status == "Ready" and PAL.green or PAL.blue, 1)
|
||
Kit.textBold("small", award.name .. " egg", cx + 12 * m.s, y + 43 * m.s, PAL.heading)
|
||
Kit.text("micro", award.threshold .. " stored · " .. status, cx + 12 * m.s, y + 73 * m.s, PAL.muted)
|
||
if index % giftCols == 0 or index == #require("src.box.Gifts").AWARDS then y = y + cardH + gap end
|
||
end
|
||
button("claim-egg", progress.award and "Claim " .. progress.award.name .. " egg" or "All gift eggs claimed", function()
|
||
operation(function() return s.service:claimEgg(s.sources[s.sourceIndex]) end, "Gift egg stored. Withdraw it to hatch in the game.")
|
||
end, not progress.eligible); nextRow()
|
||
end
|
||
elseif page == "List" then
|
||
local query = Kit.textfield("box-search", x, y, w, h, s.query, "Search Pokémon")
|
||
if query ~= s.query then s.query, s.toolsListPage = query, 1 end
|
||
nextRow()
|
||
cycle("sort", "Sort", Tools.SORTS, s.sort, function(value) s.sort, s.toolsListPage = value, 1 end)
|
||
local rows = search(s, s.query, s.sort)
|
||
local first, last, index = Kit.pageBounds(s.toolsListPage or 1, #rows, 8)
|
||
s.toolsListPage = index
|
||
for i = first, last do
|
||
local row, d = rows[i], rows[i].entry.display
|
||
local chosen = s.selectedBox[row.box .. ":" .. row.slot] ~= nil
|
||
local bh = h + Kit.textHeight("micro") + gap
|
||
local spriteW = 56 * m.s
|
||
api.button(imp, x + spriteW, y, w - spriteW, bh, "list-" .. row.entry.id,
|
||
(chosen and "Selected · " or "") .. d.name .. " · Lv. " .. d.level
|
||
.. " · " .. s.service.state.boxes[row.box].name .. " / " .. row.slot, function()
|
||
api.selectEntry(s, "box", row)
|
||
end, chosen, false, "package")
|
||
local art = require("src.box.Catalog").art(row.entry)
|
||
if art then require("src.online.OnlineSprites").drawIcon(art, x + 3 * m.s,
|
||
y + (bh - 48 * m.s) / 2, 48 * m.s, math.floor((s.artTime or 0) * 4)) end
|
||
Kit.text("micro", Kit.ellipsize("micro", table.concat({ d.gender or "—", d.nature or "—", d.ability or "—",
|
||
"HP " .. tostring(d.hp or "?"), "Atk " .. tostring(d.attack or "?"), "Def " .. tostring(d.defense or "?"),
|
||
"Spe " .. tostring(d.speed or "?") }, " · "), w - spriteW - 20 * m.s),
|
||
x + spriteW + 10 * m.s, y + h, chosen and PAL.inverse or PAL.muted)
|
||
y = y + bh + gap
|
||
end
|
||
if #rows == 0 then text("No Pokémon match this view.", true) end
|
||
cycle("list-page", "Page", (function()
|
||
local pages = {}; for i = 1, math.max(1, math.ceil(#rows / 8)) do pages[i] = i end; return pages
|
||
end)(), index, function(value) s.toolsListPage = value end)
|
||
actions({ { "summary", "Summary", function() api.openSummary(s) end, not s.inspect, false, "eye" },
|
||
{ "compare", "Compare selected", function() s.comparison = not s.comparison end, #refs == 0 or #refs > 6 } })
|
||
if s.comparison and #refs > 0 and #refs <= 6 then
|
||
local fields = { {"species","Species"},{"level","Level"},{"types","Types"},{"gender","Gender"},
|
||
{"nature","Nature"},{"ability","Ability"},{"hp","HP"},{"attack","Attack"},{"defense","Defense"},
|
||
{"speed","Speed"},{"special","Special"},{"spAtk","Sp. Atk"},{"spDef","Sp. Def"},
|
||
{"friendship","Friendship"},{"trainer","Trainer"},{"item","Held item"},{"moves","Moves"} }
|
||
local entries = {}
|
||
for _,ref in ipairs(refs) do
|
||
local entry=Store.at(s.service.state,ref.box,ref.slot)
|
||
if entry then entries[#entries+1]=entry end
|
||
end
|
||
local labelW=math.min(w*.22,110*m.s)
|
||
local columns=math.max(1,math.min(6,math.floor((w-labelW-gap)/(105*m.s))))
|
||
for start=1,#entries,columns do
|
||
local count=math.min(columns,#entries-start+1)
|
||
local cellW=(w-labelW-gap)/count
|
||
local headerH=0
|
||
for column=1,count do
|
||
local entry=entries[start+column-1]
|
||
headerH=math.max(headerH,Kit.textWrapped("small",entry.display.name,
|
||
x+labelW+gap+(column-1)*cellW,y,cellW-gap,PAL.heading))
|
||
end
|
||
y=y+headerH+gap
|
||
for _,field in ipairs(fields) do
|
||
local values,applicable={},false
|
||
for column=1,count do
|
||
local value=entries[start+column-1].display[field[1]]
|
||
if value~=nil then applicable=true end
|
||
values[column]=value==nil and "N/A" or value=="" and "None" or tostring(value)
|
||
end
|
||
if applicable then
|
||
local rowH=Kit.textWrapped("small",field[2],x,y,labelW,PAL.muted)
|
||
for column=1,count do
|
||
rowH=math.max(rowH,Kit.textWrapped("small",values[column],
|
||
x+labelW+gap+(column-1)*cellW,y,cellW-gap,PAL.text))
|
||
end
|
||
y=y+rowH+gap
|
||
end
|
||
end
|
||
y=y+gap
|
||
end
|
||
end
|
||
elseif page == "Filters" then
|
||
if (s.filterQuery or "") ~= s.query then s.filters = nil end
|
||
s.filters = s.filters or {}
|
||
local function query()
|
||
local parts = {}
|
||
for _, key in ipairs({ "type", "gender", "nature", "ability", "trainer", "mark" }) do
|
||
local value = s.filters[key]
|
||
if value and value ~= "Any" then
|
||
for piece in value:lower():gmatch('[^"]+') do parts[#parts + 1] = key .. ':"' .. piece .. '"' end
|
||
end
|
||
end
|
||
if s.filters.shiny then parts[#parts + 1] = "shiny" end
|
||
if s.filters.egg then parts[#parts + 1] = "egg" end
|
||
s.query, s.pageBox, s.pagePC = table.concat(parts, " "), 1, 1
|
||
s.filterQuery = s.query
|
||
end
|
||
local choiceCache = s._filterChoices
|
||
if not choiceCache or choiceCache.state ~= s.service.state then
|
||
choiceCache = { state = s.service.state }; s._filterChoices = choiceCache
|
||
end
|
||
local function choices(key)
|
||
if choiceCache[key] then return choiceCache[key] end
|
||
local found, out = {}, { "Any" }
|
||
for _, row in ipairs(search(s, "", "slot")) do
|
||
local value = row.entry.display[key == "type" and "types" or key]
|
||
if value then
|
||
local values = key == "type" and (function()
|
||
local types = {}; for item in value:gmatch("[^/]+") do types[#types + 1] = item:match("^%s*(.-)%s*$") end
|
||
return types
|
||
end)() or { tostring(value) }
|
||
for _, v in ipairs(values) do if v ~= "" and not found[v] then found[v], out[#out + 1] = true, v end end
|
||
end
|
||
end
|
||
table.sort(out, function(a, b) if a == "Any" then return true elseif b == "Any" then return false end; return a < b end)
|
||
choiceCache[key] = out
|
||
return out
|
||
end
|
||
for index, key in ipairs({ "type", "gender", "nature", "ability", "trainer" }) do
|
||
cycle("filter-" .. key, key:gsub("^%l", string.upper), choices(key), s.filters[key] or "Any",
|
||
function(value) s.filters[key] = value; query() end, index % 2 == 1 and 1 or 2)
|
||
end
|
||
cycle("filter-mark", "Marked", { "Any", "circle", "square", "triangle", "heart" }, s.filters.mark or "Any",
|
||
function(value) s.filters.mark = value; query() end, 2)
|
||
for index, key in ipairs({ "shiny", "egg" }) do
|
||
button("filter-" .. key, key:gsub("^%l", string.upper) .. " only", function()
|
||
s.filters[key] = not s.filters[key]; query()
|
||
end, false, (w - gap) / 2, x + (index - 1) * (w + gap) / 2, s.filters[key], "check")
|
||
end
|
||
nextRow()
|
||
actions({ { "filter-clear", "Clear", function() s.filters, s.query, s.filterQuery = {}, "", "" end, false, false, "x" },
|
||
{ "filter-view", "Show Pokémon", function() UI.change(imp, s, "toolsPage", nil, -1); s.view = "box" end } })
|
||
elseif page == "Arrange" then
|
||
s.arrangeSection = s.arrangeSection or "Sort"
|
||
local section = s.arrangeSection
|
||
local bw = (w - h - 2 * gap) / 2
|
||
UI.button(imp, x, y, bw, h, "tools-arrange-sort-tab", "Sort a box",
|
||
function() UI.change(imp, s, "arrangeSection", "Sort") end,{face="tab",active=section=="Sort",icon="arrow-up-down"})
|
||
UI.button(imp, x + bw + gap, y, bw, h, "tools-arrange-auto-tab", "Auto Box",
|
||
function() UI.change(imp, s, "arrangeSection", "Auto Box") end,{face="tab",active=section=="Auto Box",icon="arrow-left-right"})
|
||
UI.dropdown(imp, x + w - h, y, h, h, "tools-arrangeSection", "",
|
||
UI.values({ "Move", "Sort", "Auto Box", "Labels" }, nil, "ellipsis"), section,
|
||
function(v) UI.change(imp, s, "arrangeSection", v) end, "ellipsis")
|
||
nextRow()
|
||
if section == "Move" then
|
||
actions({ { "select-box", "Select whole Box", function()
|
||
s.selectedBox, s.multi = {}, true
|
||
for slot in pairs(s.service.state.boxes[s.box].mons) do
|
||
s.selectedBox[s.box .. ":" .. slot] = { box = s.box, slot = slot }
|
||
end
|
||
end }, { "move-group", "Move / swap " .. #refs, function()
|
||
s.moving, s.view, s.toolsPage, s.query, s.sort = refs, "box", nil, "", "slot"
|
||
s.notice = "Choose the group's first destination slot. Occupied slots swap back."
|
||
end, #refs == 0 } })
|
||
picker()
|
||
elseif section == "Sort" or section == "Auto Box" then
|
||
y = y + require("src.import.BoxOrganizer").draw(imp, s, x, y, w, m, api, section == "Sort" and "sort" or "rules")
|
||
elseif section == "Labels" then
|
||
s.editMarkings = s.editMarkings or 0
|
||
for i, label in ipairs({ "Circle", "Square", "Triangle", "Heart" }) do
|
||
local mask = 2^(i - 1)
|
||
local checked = math.floor(s.editMarkings / mask) % 2 == 1
|
||
button("mark-" .. i, label, function()
|
||
s.editMarkings = s.editMarkings + (checked and -mask or mask)
|
||
end, false, (w - gap) / 2, x + (i - 1) % 2 * ((w - gap) / 2 + gap), checked, "check")
|
||
if i % 2 == 0 then nextRow() end
|
||
end
|
||
actions({ { "apply-marks", "Apply marks", function()
|
||
operation(function() return s.service:mark(refs, s.editMarkings) end, "Markings saved.")
|
||
end, #refs == 0 }, { "tags", "Set tags", function()
|
||
namePrompt("Local tags", "", function(value)
|
||
operation(function() return s.service:mark(refs, s.editMarkings, value) end, "Tags saved.")
|
||
end)
|
||
end, #refs == 0, false, "pencil" } })
|
||
end
|
||
elseif page == "Dashboard" then
|
||
local dash = s._dashboard
|
||
if not dash or dash.state ~= s.service.state or dash.save ~= s.pcSave then
|
||
dash = { state = s.service.state, save = s.pcSave, report = Collection.dashboard(s.service.state, s.pcSave) }
|
||
s._dashboard = dash
|
||
end
|
||
local report = dash.report
|
||
tiles({ { value = report.total, label = "Stored", icon = "package" }, { value = report.species, label = "Species", icon = "book-open" },
|
||
{ value = report.eggs, label = "Eggs", icon = "heart" }, { value = report.shiny, label = "Shiny", icon = "award" } })
|
||
text(report.caught .. " caught in the linked game", true)
|
||
local games = {}; for game in pairs(report.byGame) do games[#games + 1] = game end; table.sort(games)
|
||
for _, game in ipairs(games) do text(require("src.core.GameVersion").info(game).label .. ": " .. report.byGame[game]) end
|
||
local forms = {}; for form in pairs(report.forms) do forms[#forms + 1] = form end; table.sort(forms)
|
||
text("Unown forms held: " .. (#forms == 0 and "None" or table.concat(forms, ", ")))
|
||
local duplicateOptions = {}
|
||
if not s._nationalNames then
|
||
s._nationalNames = {}
|
||
local Catalog, GameVersion = require("src.box.Catalog"), require("src.core.GameVersion")
|
||
local versions = { "emerald" }; for i = #GameVersion.ORDER, 1, -1 do versions[#versions + 1] = GameVersion.ORDER[i] end
|
||
for _, version in ipairs(versions) do
|
||
local data = Catalog.get(version)
|
||
if data.names then
|
||
for dex, species in pairs(data.national.toSpecies or {}) do
|
||
s._nationalNames[dex] = s._nationalNames[dex] or data.names[species]
|
||
end
|
||
else
|
||
for _, def in pairs(data.pokemon or {}) do
|
||
if def.dex then s._nationalNames[def.dex] = s._nationalNames[def.dex] or def.name end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
local function speciesName(dex) return s._nationalNames[dex] or "Species #" .. dex end
|
||
for _, row in ipairs(report.duplicates) do
|
||
duplicateOptions[#duplicateOptions + 1] = { id = row.dex, label = speciesName(row.dex) .. " · " .. row.count .. " held", icon = "copy" }
|
||
end
|
||
UI.dropdown(imp, x, y, w, h, "duplicates", "Duplicates · " .. #report.duplicates, duplicateOptions, nil,
|
||
function(dex) UI.change(imp, s, "toolsPage", "List"); s.query = "dex:" .. dex end, "copy"); nextRow()
|
||
local missing = {}; for _, dex in ipairs(report.missing) do missing[#missing + 1] = { id = dex, label = speciesName(dex), icon = "book-open" } end
|
||
UI.dropdown(imp, x, y, w, h, "missing", "Missing species · " .. #missing, missing, nil,
|
||
function(dex) UI.open(imp, speciesName(dex), {}); imp._boxPopup.message = "you don’t have this species in Box yet." end, "book-open"); nextRow()
|
||
elseif page == "Teams" then
|
||
button("save-team", "Save selected team (" .. #refs .. ")", function()
|
||
namePrompt("Team name", "", function(value)
|
||
operation(function() return s.service:preset(value, refs) end, "Team preset saved.")
|
||
end)
|
||
end, #refs == 0 or #refs > 6); nextRow()
|
||
local presets = s.service.state.presets or {}
|
||
s.teamIndex = math.min(s.teamIndex or 1, math.max(1, #presets))
|
||
if #presets > 0 then
|
||
local options = {}; for index, preset in ipairs(presets) do options[index] = { id = index, label = preset.name, icon = "users" } end
|
||
UI.dropdown(imp, x, y, w, h, "team-picker", presets[s.teamIndex].name, options, s.teamIndex,
|
||
function(value) s.teamIndex = value end, "users"); nextRow()
|
||
local index, preset = s.teamIndex, presets[s.teamIndex]
|
||
local members, missing = {}, false
|
||
for _, id in ipairs(preset.ids) do
|
||
local row = findEntry(s, id)
|
||
members[#members + 1] = row and row.entry.display.name or "Outside Box (#" .. id .. ")"
|
||
if not row then missing = true end
|
||
end
|
||
text(table.concat(members, " · "))
|
||
actions({ { "select-team-" .. index, "Select team", function()
|
||
s.selectedBox = {}
|
||
for _, id in ipairs(preset.ids) do
|
||
local row = Store.find(s.service.state, id)
|
||
if row then s.selectedBox[row.box .. ":" .. row.slot] = { box = row.box, slot = row.slot } end
|
||
end
|
||
end, missing }, { "deploy-team-" .. index, "Send team", function()
|
||
operation(function() return s.service:deployPreset(s.sources[s.sourceIndex], index) end, "Team deployed.")
|
||
end, missing or not s.sources[s.sourceIndex], false, "arrow-right" } })
|
||
end
|
||
end
|
||
return y - top, true
|
||
end
|
||
|
||
return Tools
|