Merge pull request #831 from KikiManjaro/order-dex-in-file-editor

This commit is contained in:
bryanthaboi
2026-08-04 19:04:30 -04:00
committed by GitHub
4 changed files with 176 additions and 1 deletions
+114
View File
@@ -39,6 +39,7 @@ print("== save editor task 7 tests (Events + Dex) ==")
local Ops = require("Ops") local Ops = require("Ops")
local State = require("State") local State = require("State")
local Catalog = require("Catalog")
local function newState() local function newState()
local S = State.new() local S = State.new()
@@ -209,5 +210,118 @@ do
check(owned > 0, "the dex was not wiped by the unrelated click") check(owned > 0, "the dex was not wiped by the unrelated click")
end end
-- Dex sort -----------------------------------------------------------
do
-- Ops.dexList orders the grid by the active mode. This block drives the
-- real generated data (State.new alone carries no Data), so the vanilla
-- 1-151 numbering is what the "dex" mode asserts against.
local Data = require("src.core.Data")
Data:load()
local S = State.new()
S.data = Data
S.cat = Catalog.build(Data)
S.save = require("src.core.SaveData").newGame()
-- default mode is "dex": number order
eq(S.dexSort, "dex", "a fresh state sorts the dex by number by default")
local byDex = Ops.dexList(S)
eq(#byDex, #S.cat.species, "dexList covers every species")
eq(byDex[1], "BULBASAUR", "dex order starts at #1")
eq(byDex[4], "CHARMANDER", "dex order puts Charmander fourth")
eq(byDex[25], "PIKACHU", "dex order puts Pikachu at #25")
eq(byDex[151], "MEW", "dex order ends at #151")
-- "name" mode: alphabetical by display name
Ops.dexSort(S, "name")
eq(S.dexSort, "name", "dexSort switches the mode")
local byName = Ops.dexList(S)
eq(#byName, #S.cat.species, "the name sort covers every species too")
eq(byName[1], "ABRA", "the name sort leads with ABRA")
local sorted = true
for i = 2, #byName do
local a = S.data.pokemon[byName[i - 1]]
local b = S.data.pokemon[byName[i]]
local an = (a and a.name or byName[i - 1]):lower()
local bn = (b and b.name or byName[i]):lower()
if an > bn then sorted = false break end
end
check(sorted, "the name sort is alphabetical over display names")
local mi = nil
for i, id in ipairs(byName) do
if id == "MEW" then mi = i
elseif id == "MR_MIME" and mi then
check(i > mi, "MEW sorts before MR.MIME in the name sort")
end
end
local fIdx, mIdx = nil, nil
for i, id in ipairs(byName) do
if id == "NIDORAN_F" then fIdx = i elseif id == "NIDORAN_M" then mIdx = i end
end
check(fIdx and mIdx and fIdx < mIdx, "NIDORAN_F sorts before NIDORAN_M")
-- switching back to the number order restores the original sequence
Ops.dexSort(S, "dex")
local back = Ops.dexList(S)
eq(back[1], "BULBASAUR", "switching back restores number order")
end
do
-- the switch is view-only: it resets the grid scroll but never dirties the
-- save, and a re-click on the active mode is a narrated no-op
local S = newState()
S.data = { pokemon = { BULBASAUR = { dex = 1, name = "BULBASAUR" },
CHARMANDER = { dex = 4, name = "CHARMANDER" },
PIKACHU = { dex = 25, name = "PIKACHU" },
SQUIRTLE = { dex = 7, name = "SQUIRTLE" } } }
S.cat = { species = { "BULBASAUR", "CHARMANDER", "SQUIRTLE", "PIKACHU" },
items = {}, moves = {} }
S.dexOffset = 9
S.dirty = false
check(Ops.dexSort(S, "name") == true, "dexSort switches the mode without dirtying")
eq(S.dirty, false, "a sort never dirties the save")
eq(S.dexOffset, 0, "changing the sort resets the grid scroll")
eq(S.status, "", "a sort leaves the status bar alone")
S.dexOffset = 4
check(Ops.dexSort(S, "name") == false, "re-clicking the active mode is a no-op")
eq(S.dexOffset, 4, "a no-op sort leaves the scroll alone")
eq(S.dirty, false, "a no-op sort does not dirty either")
eq(S.status, "", "a no-op sort does not narrate either")
check(Ops.dexSort(S, "bogus") == false, "an unknown mode is refused")
eq(S.dexSort, "name", "a refused mode leaves the sort unchanged")
-- the keyed list sorts against this mini dataset too
local byDex = Ops.dexList(S)
eq(byDex[1], "BULBASAUR", "mini-catalog dex order is #1 first")
eq(byDex[2], "CHARMANDER", "mini-catalog dex order is #4 second")
Ops.dexSort(S, "name")
eq(Ops.dexList(S)[1], "BULBASAUR", "mini-catalog name order leads with BULBASAUR")
end
do
-- robustness: a mod-shaped partial record (no name, no dex) must not crash
-- the sort or disappear from the grid -- it just sorts last
local S = newState()
S.data = { pokemon = { BULBASAUR = { dex = 1, name = "BULBASAUR" },
PARTIAL = { baseStats = { hp = 40 } } } }
S.cat = { species = { "BULBASAUR", "PARTIAL" }, items = {}, moves = {} }
local byDex = Ops.dexList(S)
eq(#byDex, 2, "a partial record still appears in the dex order")
eq(byDex[2], "PARTIAL", "a record without a dex number sorts last")
Ops.dexSort(S, "name")
local byName = Ops.dexList(S)
eq(#byName, 2, "a partial record still appears in the name order")
eq(byName[2], "PARTIAL", "a record without a name sorts last, by its id")
-- and with no data/catalog at all, the list degrades to empty, not nil
local bare = State.new()
eq(#Ops.dexList(bare), 0, "a state with no catalog yields an empty list")
end
print(string.format("save editor task 7 tests: %d passed, %d failed", passed, failed)) print(string.format("save editor task 7 tests: %d passed, %d failed", passed, failed))
if failed > 0 then os.exit(1) end if failed > 0 then os.exit(1) end
+52
View File
@@ -717,6 +717,58 @@ function Ops.dexClear(S)
return Ops.mark(S, "Pokedex wiped") return Ops.mark(S, "Pokedex wiped")
end end
-- ------------------------------------------------------------------ dex sort
-- The DEX grid's row order. Sorting is view-only: it never touches the save,
-- so the list itself is computed here (pure, testable) and the switch is
-- narrated through Ops.say, never Ops.mark.
--
-- "dex" -- by Pokedex number (1-151), the panel default
-- "name" -- by display name, alphabetical (case-insensitive)
--
-- A species whose record lacks the sort key (a partial mod record) sorts
-- last, ordered by its id, so the grid can never drop a row or crash.
-- table.sort is not stable, so every sort carries the id as a tiebreak and
-- the order is fully deterministic.
local SORT_KEYS = {
dex = function(def, id)
return def and def.dex or math.huge
end,
name = function(def, id)
local name = def and def.name
return (name and tostring(name):lower()) or tostring(id):lower()
end,
}
function Ops.dexList(S)
local list = S and S.cat and S.cat.species
if not list then return {} end
local make = SORT_KEYS[S.dexSort == "name" and "name" or "dex"]
local data = S.data
local rows = {}
for _, id in ipairs(list) do
rows[#rows + 1] = { key = make(data and data.pokemon and data.pokemon[id], id),
id = id }
end
table.sort(rows, function(a, b)
if a.key ~= b.key then return a.key < b.key end
return a.id < b.id
end)
local out = {}
for i, r in ipairs(rows) do out[i] = r.id end
return out
end
-- View-only verb: switching the DEX grid's order resets its scroll but never
-- dirties the save or narrates in the status bar (the active chip carries
-- the mode). Returns true when the mode changed, false on a no-op.
function Ops.dexSort(S, mode)
if mode ~= "name" and mode ~= "dex" then return false end
if S.dexSort == mode then return false end
S.dexSort = mode
S.dexOffset = 0
return true
end
-- -------------------------------------------------------------------- map -- -------------------------------------------------------------------- map
-- Outdoor is detected the way the game treats LAST_MAP sources: -- Outdoor is detected the way the game treats LAST_MAP sources:
-- OVERWORLD/PLATEAU tilesets, maps with connections, or fly spots the save -- OVERWORLD/PLATEAU tilesets, maps with connections, or fly spots the save
+1
View File
@@ -77,6 +77,7 @@ function State.new()
eventsOffset = 0, eventsOffset = 0,
-- dex -- dex
dexSort = "dex", -- how the DEX grid is ordered: "dex" (by number) | "name" (A-Z)
dexOffset = 0, dexOffset = 0,
-- map -- map
+9 -1
View File
@@ -24,7 +24,7 @@ function M.draw(S, Kit, x, y, w, h)
local s = Kit.scale local s = Kit.scale
local pad = 20 * s local pad = 20 * s
local dex = Ops.dex(S) local dex = Ops.dex(S)
local species = S.cat.species local species = Ops.dexList(S)
local seen, owned, total = Ops.dexCounts(S) local seen, owned, total = Ops.dexCounts(S)
Kit.card(x, y, w, h) Kit.card(x, y, w, h)
@@ -44,12 +44,20 @@ function M.draw(S, Kit, x, y, w, h)
-- and FLOW, wrapping to further rows when even one is too narrow, so the -- and FLOW, wrapping to further rows when even one is too narrow, so the
-- cluster can never paint over the headline or over itself. -- cluster can never paint over the headline or over itself.
local actH = 34 * s local actH = 34 * s
-- The two sort chips are view-only (Ops.dexSort never dirties the save);
-- the active mode reads as the accent chip, the other as ghost. They ride
-- the same wrap-aware cluster as the bulk actions so a narrow window flows
-- them to their own rows instead of painting over the headline (#715).
local buttons = { local buttons = {
{ label = "Own party + boxes", kind = "ghost", fn = Ops.dexStamp }, { label = "Own party + boxes", kind = "ghost", fn = Ops.dexStamp },
{ label = "See all", kind = "accent", fn = Ops.dexSeeAll }, { label = "See all", kind = "accent", fn = Ops.dexSeeAll },
{ label = "Own all", kind = "good", fn = Ops.dexOwnAll }, { label = "Own all", kind = "good", fn = Ops.dexOwnAll },
{ label = Ops.armLabel(S, "dex-clear", "Wipe dex"), kind = "danger", { label = Ops.armLabel(S, "dex-clear", "Wipe dex"), kind = "danger",
fn = Ops.dexClear }, fn = Ops.dexClear },
{ label = "Dex #", kind = (S.dexSort ~= "name") and "accent" or "ghost",
fn = function(s) Ops.dexSort(s, "dex") end },
{ label = "A-Z", kind = (S.dexSort == "name") and "accent" or "ghost",
fn = function(s) Ops.dexSort(s, "name") end },
} }
local clusterW = -10 * s local clusterW = -10 * s
for _, b in ipairs(buttons) do for _, b in ipairs(buttons) do