mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
Merge pull request #831 from KikiManjaro/order-dex-in-file-editor
This commit is contained in:
@@ -39,6 +39,7 @@ print("== save editor task 7 tests (Events + Dex) ==")
|
||||
|
||||
local Ops = require("Ops")
|
||||
local State = require("State")
|
||||
local Catalog = require("Catalog")
|
||||
|
||||
local function newState()
|
||||
local S = State.new()
|
||||
@@ -209,5 +210,118 @@ do
|
||||
check(owned > 0, "the dex was not wiped by the unrelated click")
|
||||
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))
|
||||
if failed > 0 then os.exit(1) end
|
||||
|
||||
@@ -717,6 +717,58 @@ function Ops.dexClear(S)
|
||||
return Ops.mark(S, "Pokedex wiped")
|
||||
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
|
||||
-- Outdoor is detected the way the game treats LAST_MAP sources:
|
||||
-- OVERWORLD/PLATEAU tilesets, maps with connections, or fly spots the save
|
||||
|
||||
@@ -77,6 +77,7 @@ function State.new()
|
||||
eventsOffset = 0,
|
||||
|
||||
-- dex
|
||||
dexSort = "dex", -- how the DEX grid is ordered: "dex" (by number) | "name" (A-Z)
|
||||
dexOffset = 0,
|
||||
|
||||
-- map
|
||||
|
||||
@@ -24,7 +24,7 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
local pad = 20 * s
|
||||
local dex = Ops.dex(S)
|
||||
local species = S.cat.species
|
||||
local species = Ops.dexList(S)
|
||||
local seen, owned, total = Ops.dexCounts(S)
|
||||
|
||||
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
|
||||
-- cluster can never paint over the headline or over itself.
|
||||
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 = {
|
||||
{ label = "Own party + boxes", kind = "ghost", fn = Ops.dexStamp },
|
||||
{ label = "See all", kind = "accent", fn = Ops.dexSeeAll },
|
||||
{ label = "Own all", kind = "good", fn = Ops.dexOwnAll },
|
||||
{ label = Ops.armLabel(S, "dex-clear", "Wipe dex"), kind = "danger",
|
||||
fn = Ops.dexClear },
|
||||
{ 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
|
||||
for _, b in ipairs(buttons) do
|
||||
|
||||
Reference in New Issue
Block a user