mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
76ede1171e
- 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.
356 lines
12 KiB
Lua
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
|
|
|