Files
gen1recomp/tests/native/box_ui/main.lua
T
2026-10-06 11:00:16 -04:00

477 lines
25 KiB
Lua

local root = assert(os.getenv("BOX_UI_REPO"))
io.stdout:setvbuf("no")
package.path = root .. "/?.lua;" .. root .. "/?/init.lua;" .. package.path
local imp
local co, pending, controls, checks = nil, nil, {}, 0
local awards={}
local function check(value, label) checks = checks + 1; assert(value, label) end
function love.load()
local CacheFs = require("src.import.CacheFs")
local CacheBlob = require("src.import.CacheBlob")
CacheFs.readAt = function(path)
local file
for cache in assert(os.getenv("BOX_UI_CACHES")):gmatch("[^:]+") do
file=io.open(cache.."/"..path,"rb")
if file then break end
end
if not file then return nil end
local bytes = file:read("*a"); file:close()
return CacheBlob.decode(path, bytes)
end
local SaveData = require("src.core.SaveData")
local Serializer = require("src.core.SaveSerializer")
local Store = require("src.box.Store")
local Service = require("src.box.Service")
local Catalog = require("src.box.Catalog")
local state = Store.new()
local save = { version = "emerald", generation = 3, engine = "game3", name = "Demo",
trainerId=42,secretId=97,flags={},vars={},modData={},bag={pockets={}},
party = { { species = 25, level = 20, hp = 52, otId = 42, otSecretId = 97, personality = 345 } },
storage = { currentBox = 1, boxes = { { name = "Demo PC", wallpaper = 1, mons = {} } } } }
local species = {1,4,7,25,133,152,155,158,175,246,151,251,52,54,56,58,60,63,66,69,72,74,77,79,81,83,84,86,88,90,92,95,96,98,100,102,104,108,109,111,113,114,115,116,118,120,122,123,127,128,129,131,132,137,147,149,154,157,160,165}
for i, id in ipairs(species) do
local mon = { species = id, level = 12 + i, hp = 40, personality = i * 1001,
otId = 42, otSecretId = 97, otName = "Demo", moves = { 33,45 }, pp={30,35}, friendship=70, language=2,
ivs={hp=15,atk=15,def=15,spe=15,spa=15,spd=15},evs={},heldItem=0,
exp=require("src.core.game3.summary_data").expForLevel(Catalog.get("emerald").meta[id].growthRate,12+i), metGame=3,metLevel=5,metLocation=7,pokeball=4 }
if i == 2 then
mon.markings, mon.pokerus, mon.modernFatefulEncounter = 5, 0x23, false
mon.contest = { cool=0, beauty=80, cute=70, smart=45, tough=54, sheen=15 }
mon.ribbons = { cool=2, champion=1, artist=1 }
end
if i <= 24 then save.storage.boxes[1].mons[i] = Store.copy(mon) end
state.boxes[1].mons[i] = { id = i, version = "emerald", generation = 3,
mon = mon, display = Catalog.describe("emerald", mon) }
end
state.boxes[1].name, state.boxes[2].name, state.boxes[3].name = "Kanto collection", "Starters", "Rare finds"
for i = 1, 12 do
local entry = Store.copy(state.boxes[1].mons[i]); entry.id = 60 + i
state.boxes[2].mons[i] = entry
end
for i = 1, 6 do
local entry = Store.copy(state.boxes[1].mons[i + 8]); entry.id = 72 + i
state.boxes[3].mons[i * 3] = entry
end
state.nextId = 79
local Showcase=require("src.box.Showcase")
local stage=Showcase.new();stage.name="Starter garden";stage.pattern="Dots"
stage=assert(Showcase.add(stage,{{box=1,slot=1},{box=1,slot=2},{box=1,slot=3}},state))
for i,piece in ipairs(stage.pieces) do piece.x=.28+(i-1)*.22;piece.y=.5;piece.scale=1.4 end
stage.pieces[#stage.pieces+1]={kind="Platform",x=.5,y=.75,scale=2,rotation=0,flip=false}
state=assert(Showcase.save(state,1,stage))
local fs = SaveData.persistenceFs()
fs.createDirectory("box"); fs.createDirectory("saves/emerald")
fs.write(Store.PATH, Serializer.encode(state))
fs.write("saves/emerald/slot1.lua", Serializer.encode(save))
local second = Store.copy(save); second.name = "Other trainer"; second.trainerId = 99
if os.getenv("BOX_UI_ID_SYNC_ONLY") then
second.name, second.secretId = "May", 123
for _, mon in ipairs(require("src.core.TrainerIdSync").monsOf(second, 3)) do
mon.otId, mon.otSecretId, mon.otName = 99, 123, "May"
mon.personality = require("bit").bxor(99, 123, 12345) * 65536 + 12345
end
local opts = SaveData.loadOptions()
opts.saveSlots = opts.saveSlots or {}
opts.saveSlots.emerald = { active = "slot1", list = { "slot1", "slot2" } }
SaveData.saveOptions(opts)
end
fs.write("saves/emerald/slot2.lua", Serializer.encode(second))
imp = require("src.import.RomImporter").new(function() end, { launcher = true })
imp.tab = "box"
imp.mods, imp.modStraysChecked = {}, true
imp.findErrors, imp._findFetch, imp.findLoading, imp.findNotice = {}, nil, false, nil
imp._refreshFindSources = function() end
imp._refreshFind = function() end
imp.findLoaded, imp.findSources = true, { { feed = "https://example.invalid/mods.json" } }
imp.findIndex = { mods = {}, carts = {} }
imp._ensureMods = function() end
imp._ensureFind = function() end
local pcRows = {}
for i, mon in ipairs(save.storage.boxes[1].mons) do
pcRows[i] = { where = "box", box = 1, index = i, key = "box:1:" .. i,
source = "Demo PC", mon = mon, entry = { version = "emerald", generation = 3,
mon = mon, display = Catalog.describe("emerald", mon) } }
end
imp._boxState = { service = assert(Service.open()), sources = {
{ version = "emerald", slotId = "slot1", path = "saves/emerald/slot1.lua", label = "Emerald · Demo" },
{ version = "emerald", slotId = "slot2", path = "saves/emerald/slot2.lua", label = "Emerald · Other trainer" } },
sourceIndex = 1, box = 1, pcBox = 1, query = "", sort = "slot", pcRows = pcRows,
pcBody = Serializer.encode(save), selectedBox = {}, selectedPC = {}, pageBox = 1,
pagePC = 1, sourcePage = 1, pcSave = save, multi = true, view = "box" }
for _, slot in ipairs({ 1, 2 }) do imp._boxState.selectedBox["1:" .. slot] = { box = 1, slot = slot } end
imp._boxState.inspect = imp._boxState.service.state.boxes[1].mons[2]
local View = require("src.import.LauncherView")
local original = View.btn
View.btn = function(owner, x, y, w, h, id, label, opts)
controls[id] = { x=x, y=y, w=w, h=h, label=label, enabled=opts.enabled ~= false, icon=opts.icon }
return original(owner, x, y, w, h, id, label, opts)
end
local Icons=require("src.ui.kit.Icons")
local drawIcon=Icons.draw
Icons.draw=function(icon,x,y,...)
if icon=="award" then awards[#awards+1]={x=x,y=y} end
return drawIcon(icon,x,y,...)
end
co = coroutine.create(run)
end
local UI = require("src.import.BoxUI")
local Transition = require("src.ui.kit.Transition")
local function wait(seconds)
local untilAt = love.timer.getTime() + (seconds or .3)
repeat coroutine.yield() until love.timer.getTime() >= untilAt
end
local function click(id)
wait()
local o=imp._boxState and imp._boxState.organizer
local r = assert(controls[id], "missing control " .. id.." (rule "..tostring(o and o.ruleIndex)..
", mode "..tostring(o and o.config.mode)..", page "..tostring(imp._boxState and imp._boxState.toolsPage)..")")
if not imp._boxPopup and not imp._modConfirm and not imp._idSyncResult then
local view = imp._tabRegionRect
if r.y < view.y or r.y+r.h > view.y+view.h then
imp._tabScroll.box = math.max(0, math.min(imp._tabScrollMax.box, imp._tabScroll.box + r.y - view.y - 20))
wait(); r = assert(controls[id])
end
end
check(r.w > 0 and r.y >= 0 and r.y+r.h <= love.graphics.getHeight(), id .. " reachable")
imp:mousepressed(r.x+r.w/2, r.y+r.h/2, 1)
if imp.mousereleased then imp:mousereleased(r.x+r.w/2, r.y+r.h/2, 1) end
wait()
end
local function shot(name)
wait(); pending = name
repeat coroutine.yield() until pending == nil
wait(.05)
end
local function close()
imp:keypressed("escape"); wait()
check(not imp._boxPopup, "escape closed the popup")
end
local function page(name)
imp._tabScroll.box = 0
if name=="Filters" then
imp._boxState.toolsPage=nil;wait();click("box-filters")
check(imp._boxState.toolsPage=="Filters","filter button opens filters")
return
end
click("box-tools-menu")
local option
for i,row in ipairs(imp._boxPopup.options) do if row.id == name then option=i end end
assert(option, name)
imp._boxPopup.index, imp._boxPopup.reveal = option, true
imp:keypressed("return"); wait()
check(imp._boxState.toolsPage == (name ~= "Pokémon" and name or nil), "tool picker chose " .. name)
end
local function chooseValue(id, value)
click(id)
local index
for i, option in ipairs(imp._boxPopup.options) do if option.id == value then index = i; break end end
assert(index, "missing choice " .. tostring(value))
imp._boxPopup.index, imp._boxPopup.reveal = index, true
imp:keypressed("return"); wait()
end
local function boxInventory(state)
local out = {}
for _, box in ipairs(state.boxes) do
for _, entry in pairs(box.mons) do out[entry.id] = require("src.core.SaveSerializer").encode(entry.mon) end
end
return require("src.core.SaveSerializer").encode(out)
end
local function pcInventory(save)
local out = {}
for _, row in ipairs(require("src.box.Records").candidates(save, 3)) do
if row.where == "box" then out[#out + 1] = require("src.core.SaveSerializer").encode(row.mon) end
end
table.sort(out)
return require("src.core.SaveSerializer").encode(out)
end
function run()
wait()
if os.getenv("BOX_UI_STUDIO_ONLY") then
page("Showcase")
shot("ui-40-showcase-editor-desktop.png")
click("box-stage-add-pokemon")
check(#imp._boxPopup.options>50,"Pokémon picker makes stored collection available")
shot("ui-41-showcase-add-pokemon-desktop.png")
imp._boxPopup.index=2;imp:keypressed("return");wait()
local s=imp._boxState
check(#s.stageDraft.pieces==5,"picker adds a Pokémon directly to the stage")
click("box-stage-add-scenery")
local tree
for i,row in ipairs(imp._boxPopup.options) do if row.label=="Tree" then tree=i end end
imp._boxPopup.index=tree;imp:keypressed("return");wait()
check(s.stageDraft.pieces[s.stagePiece].kind=="Tree","new scenery is a real tree")
local r=s.stageRect
local p=s.stageDraft.pieces[s.stagePiece]
local cx,cy=r.x+p.x*r.w,r.y+p.y*r.h
imp:mousepressed(cx,cy,1);imp:mousemoved(cx+30,cy-20);imp:mousereleased(cx+30,cy-20,1);wait()
check(p.x>.5 and p.y<.5,"native scene dragging changes placement")
local handles=s.stageHandles
local resize=handles[1]
imp:mousepressed(resize.x+resize.w/2,resize.y+resize.h/2,1)
imp:mousemoved(resize.x+resize.w/2+35,resize.y+resize.h/2-10)
imp:mousereleased(0,0,1);wait()
check(p.scale>1.5,"hold resize changes scenery scale")
local flip=s.stageHandles[3]
imp:mousepressed(flip.x+flip.w/2,flip.y+flip.h/2,1);wait()
check(p.flipY,"vertical flip toggles from pop-out")
flip=s.stageHandles[3]
imp:mousepressed(flip.x+flip.w/2,flip.y+flip.h/2,1);wait()
check(not p.flipY,"vertical flip can be restored from pop-out")
click("box-tools-stage-back")
check(s.stagePiece==#s.stageDraft.pieces-1,"layer arrow sends selected object backward")
shot("ui-42-showcase-selected-tools-desktop.png")
chooseValue("box-tools-stageSection","Scene")
chooseValue("box-tools-stage-music","Night")
local Showcase=require("src.box.Showcase")
local fingerprints={}
for i,pattern in ipairs({"Crosses","Bricks","Grid"}) do
chooseValue("box-tools-stage-pattern",pattern)
imp._tabScroll.box=0
shot("ui-"..(46+i).."-showcase-"..pattern:lower().."-desktop.png")
local canvas=love.graphics.newCanvas(400,200)
love.graphics.push("all");love.graphics.setCanvas(canvas);love.graphics.origin();love.graphics.setScissor()
local empty=Showcase.new();empty.pattern=pattern;Showcase.draw(s.service.state,empty,0,0,400,200)
love.graphics.pop()
local data=canvas:newImageData();fingerprints[i]=love.data.hash("sha256",data:getString())
data:release();canvas:release()
end
check(fingerprints[1]~=fingerprints[2] and fingerprints[2]~=fingerprints[3] and fingerprints[1]~=fingerprints[3],
"Crosses, Bricks and Grid render different native pixels")
click("box-stage-options")
imp._boxPopup.index=1;imp:keypressed("return");wait()
check(s.service.state.boxes[1].theme=="Showcase","saved scene is applied as Box theme")
check(s.service.fs.getInfo(s.service.state.boxes[1].wallpaper),"scene wallpaper PNG exists")
check(s.service.state.boxes[1].showcaseMusic=="Night","applying scene saves its chosen music")
page("Pokémon")
local track,owner,music=Showcase.currentMusic()
check(track=="Night" and owner=="box" and music:isPlaying(),"applied scene music plays on native Box board")
shot("ui-50-applied-stage-desktop.png")
page("Themes");click("box-theme-picker")
local saved
for i,row in ipairs(imp._boxPopup.options) do if row.label:find("Stage 1",1,true) then saved=i end end
check(saved~=nil,"saved stages are selectable from Themes")
shot("ui-43-saved-stage-theme-desktop.png");close()
page("Showcase");love.window.setMode(390,844,{resizable=true});imp._tabScroll.box=0;wait()
shot("ui-44-showcase-editor-portrait.png")
page("Pokémon")
s.query="bULba";wait();shot("ui-45-search-context-portrait.png")
check(#require("src.box.Store").search(s.service.state,s.query,"slot")>0,"mixed-case Pokémon lookup matches")
page("List");shot("ui-46-list-sprites-portrait.png")
love.window.setMode(1360,860,{resizable=true});s.query="";page("Pokémon")
local Store=require("src.box.Store")
local edited=Store.copy(s.service.state);edited.revision=edited.revision+1
local shiny=edited.boxes[1].mons[1]
shiny.mon.personality=require("bit").bxor(42,97,12345)*65536+12345
shiny.display=require("src.box.Catalog").describe("emerald",shiny.mon)
check(shiny.display.shiny,"move fixture is actually shiny")
assert(s.service:commit(edited));s.selectedBox={["1:1"]={box=1,slot=1}};s.inspect=nil;wait()
local function marked(r)
for _,a in ipairs(awards) do if a.x>=r.x and a.x<=r.x+r.w and a.y>=r.y and a.y<=r.y+r.h then return true end end
return false
end
check(marked(controls["box-box-1:1"]),"occupied shiny slot has its marker")
click("box-options");imp._boxPopup.index=1;imp:keypressed("return");wait()
chooseValue("box-active-box-picker",2);click("box-box-2:20")
chooseValue("box-active-box-picker",1)
check(s.service.state.boxes[1].mons[1]==nil,"moving Pokémon empties its old slot")
check(not marked(controls["box-box-1:1"]),"empty former slot has no leftover shiny marker")
shot("ui-51-moved-shiny-empty-slot-desktop.png")
imp:_switchTab("mods");wait()
imp:_switchTab("box")
local tr=assert(Transition.get("tabs"),"entering Box keeps its slide")
check(tr.from=="mods" and tr.to=="box" and love.timer.getTime()-tr.t0<.05,"native Box slide starts after refresh")
wait(.03)
tr=assert(Transition.get("tabs"),"default Box transition survives initial native rendering")
check(tr.p>0 and tr.p<1,"default native Box slide renders an intermediate frame")
tr.t0=love.timer.getTime()
tr.dur=1.5
shot("ui-52-box-tab-slide-desktop.png")
tr.dur=.18;wait()
print("BOX_UI_NATIVE_PASS "..checks.." checks; "..love.filesystem.getSaveDirectory())
love.event.quit(0);return
end
if os.getenv("BOX_UI_ID_SYNC_ONLY") then
imp.activeSlot.emerald = "slot1"
imp:askIdSync("emerald"); wait()
check(imp._modConfirm.lines[1]:find("Demo", 1, true), "confirmation names actual trainer")
check(imp._modConfirm.lines[2]:find("secret ID", 1, true), "confirmation covers full trainer identity")
shot("ui-36-id-sync-confirm-desktop.png")
love.window.setMode(390,844,{resizable=true}); wait()
shot("ui-37-id-sync-confirm-portrait.png")
local accept = imp._modConfirm.onYes
imp._modConfirm = nil
accept()
while imp._idSync do wait(.05) end
check(imp._idSyncResult and imp._idSyncResult.title == "ID Sync", "native launcher job succeeds")
local SaveData = require("src.core.SaveData")
local other = SaveData.decode(SaveData.readSlotSource("emerald", "slot2"))
check(other.name == "Demo" and other.trainerId == 42 and other.secretId == 97,
"native disk save matches selected trainer")
check(not require("src.core.game3.pokemon").isTradedMon(other.party[1], other),
"native mon obeys after reload")
check(require("src.core.game3.pokemon").isShiny(other.party[1]), "native shiny remains shiny after reload")
check(not require("src.core.TrainerIdSync").newJob({scope="emerald",slot="slot1"}).error,
"completed journal leaves storage usable")
shot("ui-38-id-sync-result-portrait.png")
love.window.setMode(1360,860,{resizable=true}); wait()
shot("ui-39-id-sync-result-desktop.png")
print("BOX_UI_NATIVE_PASS "..checks.." checks; "..love.filesystem.getSaveDirectory())
love.event.quit(0)
return
end
if not os.getenv("BOX_UI_ORGANIZER_ONLY") then
if not os.getenv("BOX_UI_DETAILS_ONLY") then
check(controls["box-choose-save"].y == controls["box-tools-menu"].y, "desktop toolbar shares one row")
check(controls["box-withdraw"].icon ~= nil, "transfer has an icon")
check(controls["box-withdraw"].label:find("Send 2 to game",1,true), "transfer direction is named")
shot("ui-01-board-desktop.png")
click("box-choose-save"); check(#imp._boxPopup.options==2,"game picker shows playthroughs")
shot("ui-02-game-picker-desktop.png"); close()
click("box-help-Game"); check(imp._boxPopup.message==UI.HELP.Game,"game help explains the PC")
shot("ui-03-game-help-desktop.png"); close()
click("box-tools-menu"); check(#imp._boxPopup.options==11,"all tools remain reachable; filters have their own button")
shot("ui-04-tool-picker-desktop.png"); close()
page("Filters"); shot("ui-05-filters-desktop.png")
click("box-tools-filter-type"); shot("ui-06-filter-picker-desktop.png"); close()
page("Gifts"); shot("ui-07-gifts-desktop.png")
page("Dashboard"); shot("ui-08-dashboard-desktop.png")
page("Showcase"); shot("ui-09-showcase-desktop.png")
click("box-tools-stageSection"); imp._boxPopup.index=2; imp:keypressed("return"); wait()
check(imp._boxState.stageSection=="Placement","placement section chosen")
shot("ui-10-placement-desktop.png")
page("Pokémon")
click("box-options"); shot("ui-11-options-desktop.png"); close()
love.window.setMode(390,844,{resizable=true}); imp._tabScroll.box=0; wait()
shot("ui-12-board-portrait.png")
click("box-tools-menu"); shot("ui-13-tool-picker-portrait.png"); close()
page("Filters"); shot("ui-14-filters-portrait.png")
click("box-help-Game"); shot("ui-15-help-portrait.png"); close()
page("Showcase"); shot("ui-16-showcase-portrait.png")
page("Pokémon")
click("box-view-pc"); check(imp._boxState.view=="pc","game PC view chosen")
imp._tabScroll.box=0; shot("ui-17-game-pc-portrait.png")
click("box-view-box")
click("box-transfer-target"); shot("ui-18-destination-portrait.png")
imp:keypressed("end"); wait(); imp:keypressed("return"); wait()
check(imp._boxState.pcBox==14,"destination picker reaches the last PC box")
shot("ui-19-transfer-toolbar-portrait.png")
imp._tabScroll.box=0
Transition.armed=true; UI.change(imp,imp._boxState,"view","pc",1)
local tr=assert(Transition.get("box")); tr.t0=love.timer.getTime()-.03
pending="ui-20-slide-portrait.png"; coroutine.yield()
wait()
check(not Transition.active("box"),"slide settles")
end
love.window.setMode(1360,860,{resizable=true}); imp._tabScroll.box=0; wait()
click("box-summary"); check(imp._boxState.summary,"summary opens")
shot("ui-21-summary-desktop.png")
click("box-summary-section"); imp._boxPopup.index=2; imp:keypressed("return"); wait()
check(imp._boxState.summarySection=="Moves","moves section chosen")
shot("ui-22-moves-desktop.png")
click("box-summary-section"); imp._boxPopup.index=3; imp:keypressed("return"); wait()
check(imp._boxState.summarySection=="Stats","stats section chosen")
shot("ui-23-stats-desktop.png")
click("box-summary-section"); imp._boxPopup.index=4; imp:keypressed("return"); wait()
shot("ui-24-origin-desktop.png")
local r=assert(controls["box-summary-back"])
local viewport=imp._tabRegionRect
imp._tabScroll.box=math.max(0,math.min(imp._tabScrollMax.box,imp._tabScroll.box+r.y-viewport.y-20)); wait()
shot("ui-25-ribbons-desktop.png")
love.window.setMode(390,844,{resizable=true}); imp._tabScroll.box=0; wait()
click("box-summary-section"); imp._boxPopup.index=1; imp:keypressed("return"); wait()
shot("ui-26-overview-portrait.png")
click("box-summary-section"); imp._boxPopup.index=2; imp:keypressed("return"); wait()
shot("ui-27-moves-portrait.png")
click("box-summary-section"); imp._boxPopup.index=3; imp:keypressed("return"); wait()
shot("ui-28-stats-portrait.png")
r=assert(controls["box-summary-back"]); viewport=imp._tabRegionRect
imp._tabScroll.box=math.max(0,math.min(imp._tabScrollMax.box,imp._tabScroll.box+r.y-viewport.y-20)); wait()
shot("ui-29-contest-portrait.png")
end
if not os.getenv("BOX_UI_DETAILS_ONLY") then
if imp._boxState.summary then click("box-summary-back") end
love.window.setMode(390,844,{resizable=true}); imp._tabScroll.box = 0; wait()
page("Arrange")
chooseValue("box-tools-arrangeSection", "Auto Box")
chooseValue("box-organize-first-box", 2)
chooseValue("box-organize-field", "species")
chooseValue("box-organize-value", "PIKACHU")
check(imp._boxState.organizer.config.rules[1].firstBox == 2, "Auto Box destination is selected")
check(imp._boxState.organizer.config.rules[1].conditions[1].value == "PIKACHU", "Auto Box species condition is selected")
imp._tabScroll.box = 0; shot("ui-30-auto-box-portrait.png")
local warehouseBefore = boxInventory(imp._boxState.service.state)
click("box-organize-preview")
check(boxInventory(imp._boxState.service.state) == warehouseBefore, "rule preview leaves warehouse unchanged")
check(imp._boxState.organizer.preview.report.moved > 0, "rule preview reports actual moves")
check(imp._boxState.organizer.ruleIndex==1,"sticky preview does not click rule underneath")
shot("ui-31-auto-box-preview-portrait.png")
close()
love.window.setMode(1360,860,{resizable=true}); imp._tabScroll.box = 0; wait()
shot("ui-32-auto-box-desktop.png")
check(imp._boxState.organizer.preview ~= nil, "closing movement review keeps the plan")
click("box-organize-apply")
check(boxInventory(imp._boxState.service.state) == warehouseBefore, "Auto Box preserves every native record and entry ID")
local at = require("src.box.Store").find(imp._boxState.service.state, 4)
check(at.box == 2, "Auto Box applies species rule")
click("box-organize-presets")
imp._boxPopup.index = #imp._boxPopup.options; imp:keypressed("return"); wait()
check(imp._boxPrompt ~= nil, "preset uses the existing name prompt")
imp:textinput("Pikachu box"); imp:keypressed("return"); wait()
check(imp._boxState.service.state.organizerProfiles[1].name == "Pikachu box", "preset saved from UI")
chooseValue("box-organize-field", "egg")
click("box-organize-presets")
imp._boxPopup.index = 1; imp:keypressed("return"); wait()
check(imp._boxState.organizer.config.rules[1].conditions[1].value == "PIKACHU", "saved preset reloads its conditions")
click("box-organize-rule-options")
imp._boxPopup.index = 1; imp:keypressed("return"); wait()
check(#imp._boxState.organizer.config.rules[1].conditions == 2, "rule editor adds a second condition")
click("box-organize-rule-options")
imp._boxPopup.index = 4; imp:keypressed("return"); wait()
check(imp._boxState.organizer.config.rules[1].match == "any", "rule editor switches to any condition")
click("box-organize-rule-options")
imp._boxPopup.index = 2; imp:keypressed("return"); wait()
check(#imp._boxState.organizer.config.rules[1].conditions == 1, "rule editor removes selected condition")
chooseValue("box-tools-arrangeSection", "Sort")
click("box-organize-target-pc")
imp._tabScroll.box = 0; shot("ui-33-game-sort-desktop.png")
local pcBefore = pcInventory(imp._boxState.pcSave)
local partyBefore = require("src.core.SaveSerializer").encode(imp._boxState.pcSave.party)
click("box-organize-preview")
check(imp._boxState.organizer.preview.report.moved > 0, "Game PC preview reports actual moves")
check(pcInventory(imp._boxState.pcSave) == pcBefore, "Game PC preview leaves save unchanged")
shot("ui-34-game-sort-preview-desktop.png")
close()
click("box-organize-apply")
check(pcInventory(imp._boxState.pcSave) == pcBefore, "Game PC apply preserves all native records")
check(require("src.core.SaveSerializer").encode(imp._boxState.pcSave.party) == partyBefore, "Game PC sort leaves party untouched")
local source = imp._boxState.sources[imp._boxState.sourceIndex]
check(imp._boxState.service.fs.getInfo(source.path .. ".box-bak"), "Game PC sort keeps a native save backup")
page("Pokémon"); click("box-view-pc")
imp._boxState.pcBox = 1; imp._tabScroll.box = 0
shot("ui-35-sorted-game-pc-desktop.png")
end
require("src.box.Cry").stop(); require("src.box.Showcase").stopMusic()
print("BOX_UI_NATIVE_PASS "..checks.." checks; "..love.filesystem.getSaveDirectory())
love.event.quit(0)
end
function love.update(dt)
if imp then imp:update(dt) end
if co and coroutine.status(co)~="dead" then
local ok, why=coroutine.resume(co)
if not ok then print("BOX_UI_NATIVE_FAIL "..tostring(why)); love.event.quit(1) end
end
end
function love.draw()
if imp then controls={};awards={};imp:draw() end
if pending then
local name=pending; pending=nil
love.graphics.captureScreenshot(function(data)
assert(love.filesystem.write(name,data:encode("png"):getString()))
end)
end
end
function love.errorhandler(why)
print("BOX_UI_NATIVE_FAIL "..tostring(why)); return function() return 1 end
end