Files
gen1recomp/tests/save_editor_gen3_ev_iv_tests.lua
1jamie 76ede1171e feat: Gen 3 save editor, GBA extraction resilience, and UI/asset polish
- Save Editor (Gen 3):
  - Added full support for EV, IV, Nature, Ability, and Ribbons editing for Gen 3 Pokémon.
  - Implemented stat calculations, personality-based nature derivation, and Gen 3 save data structures.
  - Added comprehensive unit test suite in tests/save_editor_gen3_ev_iv_tests.lua.

- GBA Extraction & Graphics:
  - Added zero-copy ARM-safe ROM accessor and fast LZ77 string decompression.
  - Fixed bag chrome decompression string-buffer indexing preventing solid-color renders.
  - Fixed TM Case HM badge tile offset and palette mapping.
  - Added builtin moves fallback in src/core/game3/battle/builtin_moves.lua.
  - Improved parallel extraction resilience, desktop fallback search paths, and prefix handling.

- Android & Launchers:
  - Added dynamic high-res app launcher shortcuts and color palettes for all supported games.
  - Updated Android icon generator and intent dispatch handlers.
2026-09-25 16:20:40 -05:00

356 lines
12 KiB
Lua

-- Unit tests for Save Editor Gen 3 EV and IV tracking, editing, caps, and rendering.
-- Run with: luajit tests/save_editor_gen3_ev_iv_tests.lua
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua"
.. ";./tools/save-editor/panels/?.lua"
_G.love = require("tests.love_stub")
local MonOps = require("MonOps")
local Ops = require("Ops")
local Gen = require("Gen")
local Pokemon = require("src.core.game3.pokemon")
Pokemon._stats = { [25] = { hp = 35, atk = 55, def = 40, spe = 90, spa = 50, spd = 50 } }
Pokemon._names = { [25] = "PIKACHU" }
Pokemon._byName = { PIKACHU = 25 }
Pokemon._speciesMeta = { [25] = { growthRate = 0, genderRatio = 127 } }
Pokemon._abilities = { [25] = { 9, 0 } }
Pokemon._abilityNames = { [9] = "STATIC" }
local MonEditor = require("panels.MonEditor")
local Kit = require("Kit")
local passed = 0
local failed = 0
local function check(cond, msg)
if cond then
passed = passed + 1
else
failed = failed + 1
print("FAIL: " .. tostring(msg))
end
end
local function checkEq(actual, expected, msg)
if actual == expected then
passed = passed + 1
else
failed = failed + 1
print(("FAIL: %s (got %s, want %s)"):format(tostring(msg), tostring(actual), tostring(expected)))
end
end
print("== Save Editor Gen 3 EV / IV Tests ==")
local function freshMonG3()
return {
species = 25, -- Pikachu
speciesId = 25,
level = 50,
personality = 0,
ivs = { hp = 31, atk = 31, def = 31, spe = 31, spa = 31, spd = 31 },
evs = { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 },
stats = {},
}
end
-- 1. Single-stat EV clamping (0 .. 255)
do
local mon = freshMonG3()
local val = MonOps.setEv({}, mon, "spe", 252, 3)
checkEq(val, 252, "setEv sets 252 Spe EV")
checkEq(mon.evs.spe, 252, "mon.evs.spe updated to 252")
-- Negative clamping
val = MonOps.setEv({}, mon, "spe", -10, 3)
checkEq(val, 0, "setEv clamps negative value to 0")
checkEq(mon.evs.spe, 0, "mon.evs.spe updated to 0")
-- Upper bound 255 clamping
val = MonOps.setEv({}, mon, "atk", 300, 3)
checkEq(val, 255, "setEv clamps value > 255 to 255")
checkEq(mon.evs.atk, 255, "mon.evs.atk updated to 255")
end
-- 2. 510 Total EV Cap Enforcement
do
local mon = freshMonG3()
MonOps.setEv({}, mon, "atk", 255, 3)
MonOps.setEv({}, mon, "spe", 255, 3)
checkEq(mon.evs.atk + mon.evs.spe, 510, "two stats maxed to 510 total")
-- Attempt to add EV to a 3rd stat when 510 total reached
local val = MonOps.setEv({}, mon, "hp", 10, 3)
checkEq(val, 0, "setEv returns 0 when total EV cap 510 is full")
checkEq(mon.evs.hp, 0, "mon.evs.hp remains 0")
-- Partial addition when close to 510
MonOps.setEv({}, mon, "spe", 250, 3) -- total is now 255 + 250 = 505 (5 remaining)
val = MonOps.setEv({}, mon, "hp", 20, 3)
checkEq(val, 5, "setEv truncates to remaining capacity (5)")
checkEq(mon.evs.hp, 5, "mon.evs.hp set to 5")
checkEq(mon.evs.atk + mon.evs.spe + mon.evs.hp, 510, "total equals exactly 510")
end
-- 3. Clear EVs
do
local mon = freshMonG3()
MonOps.setEv({}, mon, "atk", 252, 3)
MonOps.setEv({}, mon, "spe", 252, 3)
MonOps.setEv({}, mon, "hp", 4, 3)
MonOps.clearEvs({}, mon, 3)
for _, k in ipairs({ "hp", "atk", "def", "spe", "spa", "spd" }) do
checkEq(mon.evs[k], 0, "clearEvs zeroes " .. k)
end
end
-- 4. IV Setting and Clamping (0 .. 31)
do
local mon = freshMonG3()
local val = MonOps.setIv({}, mon, "spe", 31, 3)
checkEq(val, 31, "setIv sets 31 Spe IV")
checkEq(mon.ivs.spe, 31, "mon.ivs.spe updated to 31")
val = MonOps.setIv({}, mon, "spe", -5, 3)
checkEq(val, 0, "setIv clamps negative IV to 0")
checkEq(mon.ivs.spe, 0, "mon.ivs.spe updated to 0")
val = MonOps.setIv({}, mon, "atk", 50, 3)
checkEq(val, 31, "setIv clamps IV > 31 to 31")
checkEq(mon.ivs.atk, 31, "mon.ivs.atk updated to 31")
-- Max all IVs
MonOps.setIv({}, mon, "hp", 0, 3)
MonOps.setIv({}, mon, "atk", 0, 3)
MonOps.maxIvs({}, mon, 3)
for _, k in ipairs({ "hp", "atk", "def", "spe", "spa", "spd" }) do
checkEq(mon.ivs[k], 31, "maxIvs sets " .. k .. " to 31")
end
end
-- 5. Ops wrappers
do
local S = {
data = {},
save = { version = "firered", engine = "game3", generation = 3 },
version = "firered",
editingMon = freshMonG3(),
}
local mon = S.editingMon
Ops.setEv(S, mon, "spe", 252)
checkEq(mon.evs.spe, 252, "Ops.setEv sets speed EV")
Ops.clearEvs(S, mon)
checkEq(mon.evs.spe, 0, "Ops.clearEvs clears speed EV")
Ops.setIv(S, mon, "spa", 30)
checkEq(mon.ivs.spa, 30, "Ops.setIv sets spAtk IV")
Ops.maxIvs(S, mon)
checkEq(mon.ivs.spa, 31, "Ops.maxIvs maxes spAtk IV")
end
-- 6. Real-time Stat Recalculation Impact
do
local mon = freshMonG3()
local baseStats = Pokemon.calcStats(25, 50, mon.ivs, mon.evs, 0)
checkEq(baseStats.speed, 110, "Base Speed at Lv50 with 31 IV, 0 EV is 110")
MonOps.setEv({}, mon, "spe", 252, 3)
local boostedStats = Pokemon.calcStats(25, 50, mon.ivs, mon.evs, 0)
checkEq(boostedStats.speed, 142, "Boosted Speed at Lv50 with 31 IV, 252 EV is 142")
checkEq(boostedStats.speed - baseStats.speed, 32, "252 Spe EVs at Lv50 with 31 IV gives +32 Speed")
-- With 0 IVs
local bareStats = Pokemon.calcStats(25, 50, { spe = 0 }, { spe = 0 }, 0)
local maxEvStats = Pokemon.calcStats(25, 50, { spe = 0 }, { spe = 252 }, 0)
checkEq(maxEvStats.speed - bareStats.speed, 31, "252 Spe EVs at Lv50 with 0 IV gives +31 Speed")
end
-- 7. MonEditor.draw Smoke Tests (Gen 3 wide and narrow)
do
local S = {
data = { moves = {} },
save = { version = "firered", engine = "game3", generation = 3 },
version = "firered",
editingMon = freshMonG3(),
inspectorScroll = 0,
}
-- Wide mode
local okWide, errWide = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 600, 700)
end)
check(okWide, "MonEditor.draw renders Gen 3 wide mode without error: " .. tostring(errWide))
-- Narrow mode
local okNarrow, errNarrow = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 400, 700)
end)
check(okNarrow, "MonEditor.draw renders Gen 3 narrow mode without error: " .. tostring(errNarrow))
end
-- 8. MonEditor.draw Smoke Tests (Gen 1 & Gen 2 non-regression)
do
local S1 = {
data = { moves = {}, pokemon = { PIKACHU = { name = "PIKACHU" } } },
save = { version = "red", generation = 1 },
version = "red",
editingMon = { species = "PIKACHU", level = 50, dvs = { attack = 15, defense = 15, speed = 15, special = 15, hp = 15 } },
inspectorScroll = 0,
}
local okG1, errG1 = pcall(function()
MonEditor.draw(S1, Kit, 10, 10, 500, 700)
end)
check(okG1, "MonEditor.draw renders Gen 1 without error: " .. tostring(errG1))
local S2 = {
data = { moves = {}, pokemon = { PIKACHU = { name = "PIKACHU" } } },
save = { version = "gold", generation = 2 },
version = "gold",
editingMon = { species = "PIKACHU", level = 50, dvs = { attack = 15, defense = 15, speed = 15, special = 15, hp = 15 } },
inspectorScroll = 0,
}
local okG2, errG2 = pcall(function()
MonEditor.draw(S2, Kit, 10, 10, 500, 700)
end)
check(okG2, "MonEditor.draw renders Gen 2 without error: " .. tostring(errG2))
end
-- 9. PP Calculation and PP Ups Formula
do
-- Base 10: 10 + floor(10 * 0.2 * N) = 10, 12, 14, 16
checkEq(MonOps.calcMaxPp(10, 0), 10, "Base 10 PP + 0 Ups = 10")
checkEq(MonOps.calcMaxPp(10, 1), 12, "Base 10 PP + 1 Up = 12")
checkEq(MonOps.calcMaxPp(10, 2), 14, "Base 10 PP + 2 Ups = 14")
checkEq(MonOps.calcMaxPp(10, 3), 16, "Base 10 PP + 3 Ups = 16")
-- Base 5: 5 + floor(5 * 0.2 * N) = 5, 6, 7, 8
checkEq(MonOps.calcMaxPp(5, 0), 5, "Base 5 PP + 0 Ups = 5")
checkEq(MonOps.calcMaxPp(5, 1), 6, "Base 5 PP + 1 Up = 6")
checkEq(MonOps.calcMaxPp(5, 2), 7, "Base 5 PP + 2 Ups = 7")
checkEq(MonOps.calcMaxPp(5, 3), 8, "Base 5 PP + 3 Ups = 8")
-- Base 40: 40 + floor(40 * 0.2 * N) = 40, 48, 56, 64
checkEq(MonOps.calcMaxPp(40, 0), 40, "Base 40 PP + 0 Ups = 40")
checkEq(MonOps.calcMaxPp(40, 1), 48, "Base 40 PP + 1 Up = 48")
checkEq(MonOps.calcMaxPp(40, 2), 56, "Base 40 PP + 2 Ups = 56")
checkEq(MonOps.calcMaxPp(40, 3), 64, "Base 40 PP + 3 Ups = 64")
end
-- 10. Gen 3 PP Ups and PP Editing
do
local mon = freshMonG3()
mon.moves = { 85, 98, 0, 0 } -- Thunderbolt (base 15), Quick Attack (base 30)
mon.pp = { 15, 30, 0, 0 }
mon.maxPp = { 15, 30, 0, 0 }
mon.ppBonusesPacked = 0
local data = {
moves = {
[85] = { name = "THUNDERBOLT", pp = 15 },
[98] = { name = "QUICK ATTACK", pp = 30 },
}
}
-- Check initial PP Ups
checkEq(MonOps.getPpUps(mon, 1), 0, "Slot 1 initial PP Ups is 0")
checkEq(MonOps.getPpUps(mon, 2), 0, "Slot 2 initial PP Ups is 0")
-- Set Slot 1 to 3 PP Ups (15 -> 24 max PP)
MonOps.setPpUps(data, mon, 1, 3, 3)
checkEq(MonOps.getPpUps(mon, 1), 3, "Slot 1 PP Ups is 3")
checkEq(mon.maxPp[1], 24, "Slot 1 max PP updated to 24")
checkEq(mon.ppBonusesPacked, 3, "ppBonusesPacked is 3")
-- Set Slot 2 to 2 PP Ups (30 -> 42 max PP)
MonOps.setPpUps(data, mon, 2, 2, 3)
checkEq(MonOps.getPpUps(mon, 2), 2, "Slot 2 PP Ups is 2")
checkEq(mon.maxPp[2], 42, "Slot 2 max PP updated to 42")
-- Slot 1 has 3 in bits 0-1, Slot 2 has 2 in bits 2-3 (2 << 2 = 8): 3 | 8 = 11
checkEq(mon.ppBonusesPacked, 11, "ppBonusesPacked packed bitfield is 11")
-- Set current PP
MonOps.setPp(data, mon, 1, 20, 3)
checkEq(mon.pp[1], 20, "Slot 1 current PP set to 20")
-- Set current PP above max PP (clamped to maxPp 24)
MonOps.setPp(data, mon, 1, 50, 3)
checkEq(mon.pp[1], 24, "Slot 1 current PP clamped to max 24")
-- Set current PP below 0 (clamped to 0)
MonOps.setPp(data, mon, 1, -5, 3)
checkEq(mon.pp[1], 0, "Slot 1 current PP clamped to min 0")
-- Max all PP ups
MonOps.maxAllPpUps(data, mon, 3)
checkEq(MonOps.getPpUps(mon, 1), 3, "Slot 1 maxed to 3 PP Ups")
checkEq(MonOps.getPpUps(mon, 2), 3, "Slot 2 maxed to 3 PP Ups")
checkEq(mon.maxPp[1], 24, "Slot 1 max PP is 24")
checkEq(mon.maxPp[2], 48, "Slot 2 max PP is 48 (30 + 18)")
end
-- 11. Gen 1 / Gen 2 PP Ups and PP Editing
do
local mon = {
species = "PIKACHU",
level = 50,
moves = {
{ id = "THUNDERBOLT", pp = 15, maxPp = 15 },
{ id = "QUICK_ATTACK", pp = 30, maxPp = 30 },
}
}
local data = {
moves = {
THUNDERBOLT = { name = "THUNDERBOLT", pp = 15 },
QUICK_ATTACK = { name = "QUICK ATTACK", pp = 30 },
}
}
checkEq(MonOps.getPpUps(mon, 1), 0, "Gen 1 slot 1 PP Ups is 0")
MonOps.setPpUps(data, mon, 1, 3, 1)
checkEq(MonOps.getPpUps(mon, 1), 3, "Gen 1 slot 1 PP Ups set to 3")
checkEq(mon.moves[1].maxPp, 24, "Gen 1 slot 1 maxPp is 24")
MonOps.setPp(data, mon, 1, 10, 1)
checkEq(mon.moves[1].pp, 10, "Gen 1 slot 1 pp set to 10")
MonOps.maxAllPpUps(data, mon, 1)
checkEq(mon.moves[1].maxPp, 24, "Gen 1 slot 1 maxed to 24")
checkEq(mon.moves[2].maxPp, 48, "Gen 1 slot 2 maxed to 48")
end
-- 12. MonEditor.draw with Moves and PP controls smoke test
do
local S = {
data = {
moves = {
[85] = { name = "THUNDERBOLT", pp = 15 },
[98] = { name = "QUICK ATTACK", pp = 30 },
}
},
save = { version = "firered", engine = "game3", generation = 3 },
version = "firered",
editingMon = freshMonG3(),
inspectorScroll = 0,
}
S.editingMon.moves = { 85, 98, nil, nil }
S.editingMon.pp = { 15, 30, nil, nil }
S.editingMon.maxPp = { 15, 30, nil, nil }
S.editingMon.ppBonusesPacked = 0
local okWide, errWide = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 600, 700)
end)
check(okWide, "MonEditor.draw renders Gen 3 with moves and PP controls (wide): " .. tostring(errWide))
local okNarrow, errNarrow = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 400, 700)
end)
check(okNarrow, "MonEditor.draw renders Gen 3 with moves and PP controls (narrow): " .. tostring(errNarrow))
end
print(string.format("== EV/IV/PP Tests: %d passed, %d failed ==", passed, failed))
if failed > 0 then os.exit(1) end