Files
gen1recomp/tests/game3_moveteach_deleter_test.lua
2026-09-23 16:34:15 -04:00

338 lines
12 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/party_menu_specials.c:44, :50, :61, :92
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function finish()
if failed > 0 then
print(string.format("[FAIL] %d check(s) failed", failed))
os.exit(1)
end
print("[PASS] game3_moveteach_deleter")
os.exit(0)
end
local Cache = require("tests.game3_cache")
local cacheRoot = Cache.mount("pokemon/learnsets.lua")
if not cacheRoot then
print("[skip] game3_moveteach_deleter: " .. tostring(Cache.reason))
os.exit(0)
end
print("[info] FireRed cache at " .. cacheRoot)
local Pokemon = require("src.core.game3.pokemon")
Pokemon.install(nil)
local MoveLearn = require("src.core.game3.move_learn")
local Natives = require("src.core.game3.scripting.natives")
local SummaryMenu = require("src.ui.game3.summary_menu")
local Ctx = require("src.core.game3.scripting.ctx")
local Flags = require("src.core.game3.scripting.flags")
local TACKLE, GROWL, LEECH_SEED, VINE_WHIP = 33, 45, 73, 22
local CUT = 15
local BULBASAUR = 1
local function monAt(species, level)
local moves, pp, maxPp = Pokemon.movesAtLevel(species, level)
return { species = species, level = level, moves = moves, pp = pp, maxPp = maxPp }
end
local function fourMoveMon()
local mon = monAt(BULBASAUR, 50)
mon.moves = { TACKLE, GROWL, LEECH_SEED, VINE_WHIP }
mon.pp = { 35, 40, 10, 25 }
mon.maxPp = { 35, 40, 10, 25 }
return mon
end
print("[test] 1. MoveDeleterForgetMove clears the slot and shifts the rest up")
do
local mon = fourMoveMon()
check(MoveLearn.forgetMove(mon, 1) == true, "forgetting move slot 1 (0-based) reports success")
check(Pokemon.moveIdAt(mon, 1) == TACKLE, "slot 1 still holds TACKLE, got "
.. tostring(Pokemon.moveName(Pokemon.moveIdAt(mon, 1))))
check(Pokemon.moveIdAt(mon, 2) == LEECH_SEED, "LEECH SEED shifted down into slot 2, got "
.. tostring(Pokemon.moveName(Pokemon.moveIdAt(mon, 2))))
check(Pokemon.moveIdAt(mon, 3) == VINE_WHIP, "VINE WHIP shifted down into slot 3, got "
.. tostring(Pokemon.moveName(Pokemon.moveIdAt(mon, 3))))
check(Pokemon.moveIdAt(mon, 4) == nil, "slot 4 is now empty")
check(mon.pp[2] == 10 and mon.pp[3] == 25, "PP travelled with the shifted moves, got "
.. tostring(mon.pp[2]) .. "/" .. tostring(mon.pp[3]))
check(mon.maxPp[4] == nil and mon.pp[4] == nil, "the freed slot carries no PP")
check(Pokemon.moveSlotCount(mon) == 3, "the mon knows three moves, got "
.. tostring(Pokemon.moveSlotCount(mon)))
end
print("[test] 2. deleting the last slot leaves the others alone")
do
local mon = fourMoveMon()
MoveLearn.forgetMove(mon, 3)
check(Pokemon.moveIdAt(mon, 4) == nil and Pokemon.moveIdAt(mon, 3) == LEECH_SEED,
"VINE WHIP is gone and slot 3 still holds "
.. tostring(Pokemon.moveName(Pokemon.moveIdAt(mon, 3))))
check(MoveLearn.forgetMove(mon, 3) == false, "deleting an empty slot does nothing")
check(MoveLearn.forgetMove(mon, 4) == false, "an out of range slot does nothing")
end
print("[test] 3. PP Ups ride along with the shift")
do
local mon = fourMoveMon()
mon.ppBonuses = { 0, 3, 1, 0 }
MoveLearn.forgetMove(mon, 1)
check(mon.ppBonuses[2] == 1, "the third slot's PP Up bonus moved to slot 2, got "
.. tostring(mon.ppBonuses[2]))
check(mon.ppBonuses[4] == nil, "and the freed slot has none")
local packed = fourMoveMon()
-- pokefirered/src/pokemon.c:3904 RemoveMonPPBonus
packed.ppBonusesPacked = 0 + (3 * 4) + (1 * 16)
MoveLearn.forgetMove(packed, 1)
check(bit.band(bit.rshift(packed.ppBonusesPacked, 2), 3) == 1,
"the packed bonus shifted down too, got "
.. tostring(bit.band(bit.rshift(packed.ppBonusesPacked, 2), 3)))
end
print("[test] 4. GetNumMovesSelectedMonHas feeds the one-move branch")
do
local prevRuntime = package.loaded["src.core.game3.runtime"]
local party = { fourMoveMon(), monAt(BULBASAUR, 5) }
party[2].moves = { TACKLE }
party[2].pp = { 35 }
party[2].maxPp = { 35 }
package.loaded["src.core.game3.runtime"] = {
getSession = function() return { party = party } end,
isActive = function() return true end,
}
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
local handled = select(3, Natives.special(ctx, 0xDF, { log = function() end }))
check(handled == true, "special 0xDF is bound (pokefirered/data/specials.inc:234)")
check(Flags.getVar(nil, ctx, 0x800D) == 4,
"VAR_RESULT is the four known moves, got " .. tostring(Flags.getVar(nil, ctx, 0x800D)))
Flags.setVar(nil, ctx, 0x8004, 1)
Natives.special(ctx, 0xDF, { log = function() end })
check(Flags.getVar(nil, ctx, 0x800D) == 1,
"the one-move mon reports 1, which is the script's CantForgetOnlyMove branch, got "
.. tostring(Flags.getVar(nil, ctx, 0x800D)))
package.loaded["src.core.game3.runtime"] = prevRuntime
end
print("[test] 5. BufferMoveDeleterNicknameAndMove fills STR_VAR_1 and STR_VAR_2")
do
local prevRuntime = package.loaded["src.core.game3.runtime"]
local mon = fourMoveMon()
mon.nickname = "SPROUT"
package.loaded["src.core.game3.runtime"] = {
getSession = function() return { party = { mon } } end,
isActive = function() return true end,
}
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
Flags.setVar(nil, ctx, 0x8005, 2)
local buffered = {}
local handled = select(3, Natives.special(ctx, 0xDE, {
log = function() end,
setStringVar = function(idx, text) buffered[idx] = text end,
}))
check(handled == true, "special 0xDE is bound (pokefirered/data/specials.inc:233)")
check(buffered[1] == "SPROUT", "STR_VAR_1 is the nickname, got " .. tostring(buffered[1]))
check(buffered[2] == Pokemon.moveName(LEECH_SEED),
"STR_VAR_2 is the move in VAR_0x8005, got " .. tostring(buffered[2]))
check(ctx.stringVars[1] == "SPROUT" and ctx.stringVars[2] == buffered[2],
"and the script ctx carries the same pair")
package.loaded["src.core.game3.runtime"] = prevRuntime
end
print("[test] 6. SelectMoveDeleterMove drives the summary screen and MoveDeleterForgetMove")
do
local prevRuntime = package.loaded["src.core.game3.runtime"]
local mon = fourMoveMon()
package.loaded["src.core.game3.runtime"] = {
getSession = function() return { party = { mon } } end,
isActive = function() return true end,
}
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
local yielded = Natives.special(ctx, 0xDC, { log = function() end })
check(yielded == true, "the script waits while the summary screen is up")
check(SummaryMenu.isOpen(), "special 0xDC opened the summary screen")
local pressed = nil
local input = { wasPressed = function(_, key) return key == pressed end }
local function press(key)
pressed = key
SummaryMenu.handleInput(input)
pressed = nil
end
press("down")
press("a")
check(not SummaryMenu.isOpen(), "picking a move closed the screen")
check(ctx.nativePoll and ctx.nativePoll() == true, "and the script resumed")
check(Flags.getVar(nil, ctx, 0x8005) == 1,
"VAR_0x8005 is the chosen move slot, got " .. tostring(Flags.getVar(nil, ctx, 0x8005)))
Natives.special(ctx, 0xDD, { log = function() end })
check(Pokemon.moveIdAt(mon, 2) == LEECH_SEED,
"special 0xDD deleted GROWL and shifted LEECH SEED up, got "
.. tostring(Pokemon.moveName(Pokemon.moveIdAt(mon, 2))))
check(Pokemon.moveSlotCount(mon) == 3, "three moves are left, got "
.. tostring(Pokemon.moveSlotCount(mon)))
package.loaded["src.core.game3.runtime"] = prevRuntime
end
print("[test] 7. backing out of the summary screen returns MAX_MON_MOVES")
do
local prevRuntime = package.loaded["src.core.game3.runtime"]
local mon = fourMoveMon()
package.loaded["src.core.game3.runtime"] = {
getSession = function() return { party = { mon } } end,
isActive = function() return true end,
}
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
Natives.special(ctx, 0xDC, { log = function() end })
local pressed = nil
local input = { wasPressed = function(_, key) return key == pressed end }
pressed = "b"
SummaryMenu.handleInput(input)
pressed = nil
check(Flags.getVar(nil, ctx, 0x8005) == 4,
"VAR_0x8005 is MAX_MON_MOVES so the script loops back to the mon chooser, got "
.. tostring(Flags.getVar(nil, ctx, 0x8005)))
check(Pokemon.moveSlotCount(mon) == 4, "nothing was deleted")
package.loaded["src.core.game3.runtime"] = prevRuntime
end
print("[test] 8. the Move Deleter forgets an HM move")
do
local prevRuntime = package.loaded["src.core.game3.runtime"]
local mon = fourMoveMon()
mon.moves[1] = CUT
package.loaded["src.core.game3.runtime"] = {
getSession = function() return { party = { mon } } end,
isActive = function() return true end,
}
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
Natives.special(ctx, 0xDC, { log = function() end })
local pressed = nil
local input = { wasPressed = function(_, key) return key == pressed end }
pressed = "a"
SummaryMenu.handleInput(input)
pressed = nil
check(not SummaryMenu.isOpen(),
"PSS_MODE_FORGET_MOVE accepts an HM (pokefirered/src/pokemon_summary_screen.c:3772)")
check(SummaryMenu._hmNotice ~= true, "no HM refusal is printed")
check(Flags.getVar(nil, ctx, 0x8005) == 0,
"VAR_0x8005 is CUT's slot, got " .. tostring(Flags.getVar(nil, ctx, 0x8005)))
Natives.special(ctx, 0xDD, { log = function() end })
check(Pokemon.moveIdAt(mon, 1) == GROWL,
"special 0xDD deleted CUT and shifted GROWL up, got "
.. tostring(Pokemon.moveName(Pokemon.moveIdAt(mon, 1))))
check(Pokemon.moveSlotCount(mon) == 3, "three moves are left, got "
.. tostring(Pokemon.moveSlotCount(mon)))
package.loaded["src.core.game3.runtime"] = prevRuntime
end
print("[test] 9. a move-learn summary screen still refuses an HM")
do
local mon = fourMoveMon()
mon.moves[1] = CUT
local picked = "unset"
SummaryMenu.openMenu({ mon }, 1, {
mode = "select_move",
moveToLearn = TACKLE,
onSelectMove = function(slot) picked = slot end,
})
local pressed = nil
local input = { wasPressed = function(_, key) return key == pressed end }
pressed = "a"
SummaryMenu.handleInput(input)
pressed = nil
check(SummaryMenu.isOpen(), "PSS_MODE_SELECT_MOVE keeps the screen up on an HM")
check(SummaryMenu._hmNotice == true,
"the HM refusal is printed (pokefirered/src/pokemon_summary_screen.c:3772)")
check(picked == "unset", "no slot was reported")
SummaryMenu.close()
end
print("[test] 10. the deleter cursor skips empty move slots onto CANCEL")
do
local prevRuntime = package.loaded["src.core.game3.runtime"]
local mon = monAt(BULBASAUR, 5)
mon.moves = { TACKLE, GROWL }
mon.pp = { 35, 40 }
mon.maxPp = { 35, 40 }
package.loaded["src.core.game3.runtime"] = {
getSession = function() return { party = { mon } } end,
isActive = function() return true end,
}
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
Natives.special(ctx, 0xDC, { log = function() end })
local pressed = nil
local input = { wasPressed = function(_, key) return key == pressed end }
local function press(key)
pressed = key
SummaryMenu.handleInput(input)
pressed = nil
end
press("down")
check(SummaryMenu._moveCursor == 2, "down lands on GROWL, got " .. tostring(SummaryMenu._moveCursor))
press("down")
check(SummaryMenu._moveCursor == 5,
"down skips the two empty slots onto CANCEL (pokefirered/src/pokemon_summary_screen.c:3829), got "
.. tostring(SummaryMenu._moveCursor))
press("up")
check(SummaryMenu._moveCursor == 2,
"up from CANCEL skips back to GROWL (pokefirered/src/pokemon_summary_screen.c:3802), got "
.. tostring(SummaryMenu._moveCursor))
press("down")
press("down")
check(SummaryMenu._moveCursor == 1, "down from CANCEL wraps to slot 1, got "
.. tostring(SummaryMenu._moveCursor))
press("up")
check(SummaryMenu._moveCursor == 5, "up from slot 1 wraps to CANCEL, got "
.. tostring(SummaryMenu._moveCursor))
press("a")
check(not SummaryMenu.isOpen(), "A on CANCEL closes the screen")
check(Flags.getVar(nil, ctx, 0x8005) == 4,
"VAR_0x8005 is 4 on CANCEL, got " .. tostring(Flags.getVar(nil, ctx, 0x8005)))
check(Pokemon.moveSlotCount(mon) == 2, "nothing was deleted")
package.loaded["src.core.game3.runtime"] = prevRuntime
end
finish()