Files
gen1recomp/tests/mod_battle_snapshot_test.lua
T
2026-08-16 02:56:03 +02:00

247 lines
10 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local S = require("tests.harness").suite("mod battle snapshot")
local check, eq = S.check, S.eq
local Gen1BattleState = require("src.battle.BattleState")
check(Gen1BattleState.isBattleState == true,
"Gen 1 battle states carry the discovery marker")
local TypeChart = require("src.battle.TypeChart")
TypeChart.load({ type_chart = {
types = { NORMAL = { name = "NORMAL", category = "physical" } },
matchups = {},
} })
local Damage = require("src.battle.Damage")
local attacker, defender = { stages = {} }, { stages = {} }
eq(Damage.accuracyThreshold({ oneIn256Miss = true }, { accuracy = 100 },
attacker, defender), 255, "faithful accuracy keeps the 1-in-256 miss")
eq(Damage.accuracyThreshold({ oneIn256Miss = false }, { accuracy = 100 },
attacker, defender), 256, "clean accuracy exposes a certain hit")
local Catching = require("src.battle.Catching")
eq(Catching.chance("MASTER_BALL", { hp = 1, stats = { hp = 1 } },
{ catchRate = 1 }), 100, "Master Ball preview is certain")
check(Catching.chance("MOD_BALL", { hp = 1, stats = { hp = 1 } },
{ catchRate = 1 }, nil, { ballDef = { attempt = function() end } }) == nil,
"custom ball logic does not receive a guessed preview")
local mon = { species = "TESTMON", level = 5, hp = 18,
stats = { hp = 20 }, moves = {} }
local game = {
data = {
pokemon = { TESTMON = { name = "TESTMON", catchRate = 255 } },
moves = { TACKLE = { name = "TACKLE", type = "NORMAL", power = 35,
accuracy = 95, pp = 35 } },
items = { POTION = { name = "POTION" },
POKE_BALL = { name = "POKE BALL" } },
},
save = { party = { mon }, inventory = { POTION = 1, POKE_BALL = 1 } },
stack = { states = {} },
}
local battle = {
isBattleState = true, phase = "menu", queue = {},
ruleset = { oneIn256Miss = true },
player = { mon = mon, curTypes = { "NORMAL" }, stages = {},
curMoves = { { id = "TACKLE", pp = 35 } } },
enemy = { mon = { species = "TESTMON", level = 4, hp = 12,
stats = { hp = 12 }, moves = {} }, curTypes = { "NORMAL" }, stages = {} },
}
function battle:battleKind() return "wild" end
function battle:effectRecord() return { accuracyChecked = true } end
function battle:visibleText() return { "Wild TESTMON appeared!" } end
function battle:menuLockedAction() return nil end
function battle:chooseMenu(choice)
self.chosenMenu = choice
if choice == "fight" then self.phase = "moveSelect" end
return true
end
function battle:chooseMove(slot)
self.chosenMove = slot
self.phase = "messages"
return true
end
function battle:cancelMove() self.phase = "menu" return true end
function battle:catchChance(ball)
return require("src.battle.Catching").chance(ball, self.enemy.mon,
game.data.pokemon[self.enemy.mon.species])
end
game.stack.states = { battle }
local api = require("src.battle.BattleAPI").new(game)
local snapshot = api:snapshot()
check(snapshot and snapshot.kind == "wild" and snapshot.prompt == "menu",
"Gen 1 battle is exposed")
eq(snapshot.player.maxHp, 20, "Gen 1 max HP comes from battle stats")
eq(snapshot.moves[1].name, "TACKLE", "move records are copied")
eq(snapshot.message[1], "Wild TESTMON appeared!", "battle text is copied")
eq(#snapshot.items, 2, "medicine and balls are exposed")
check(type(snapshot.items[1].catchChance) == "number"
or type(snapshot.items[2].catchChance) == "number",
"stock catch chance is available")
snapshot.player.hp = 0
snapshot.moves[1].pp = 0
eq(mon.hp, 18, "changing a snapshot cannot change a Pokemon")
eq(battle.player.curMoves[1].pp, 35,
"changing a snapshot cannot change a move")
local same = api:snapshot()
eq(same.revision, snapshot.revision, "unchanged battle keeps its revision")
battle.enemy.mon.hp = 5
check(api:snapshot().revision > same.revision,
"observable battle changes advance the revision")
game.stack.states = {}
check(api:snapshot() == nil, "Gen 1 returns nil outside a battle")
game.stack.states = { battle }
local menu = api:snapshot()
local ok, err = api:submit({ id = 1, revision = menu.revision - 1,
kind = "menu", choice = "fight" })
check(not ok and err == "stale battle context",
"Gen 1 rejects a stale intent")
ok, err = api:submit({ id = 1, revision = menu.revision,
kind = "menu", choice = "missing" })
check(not ok and err == "unknown battle menu choice",
"Gen 1 rejects an unknown menu choice")
check(api:submit({ id = 1, revision = menu.revision,
kind = "menu", choice = "fight" }), "Gen 1 accepts a menu intent")
eq(battle.chosenMenu, "fight", "Gen 1 uses the semantic menu path")
ok, err = api:submit({ id = 1, revision = menu.revision,
kind = "menu", choice = "fight" })
check(not ok and err == "replayed intent", "Gen 1 rejects a replayed intent")
local moveMenu = api:snapshot()
ok, err = api:submit({ id = 2, revision = moveMenu.revision,
kind = "move", slot = 9 })
check(not ok and err == "invalid move slot",
"Gen 1 rejects an invalid move slot")
check(api:submit({ id = 2, revision = moveMenu.revision,
kind = "move", slot = 1 }), "Gen 1 accepts a valid move")
eq(battle.chosenMove, 1, "Gen 1 uses the semantic move path")
battle.phase = "moveSelect"
local back = api:snapshot()
check(api:submit({ id = 3, revision = back.revision, kind = "back" }),
"Gen 1 accepts move-menu back")
eq(battle.phase, "menu", "Gen 1 back restores the command menu")
local player2 = { species = "CHIKORITA", level = 5, hp = 20,
maxHp = 21, moves = { { id = "TACKLE", pp = 35, maxPp = 35 } } }
local enemy2 = { species = "RATTATA", level = 3, hp = 12, maxHp = 12,
moves = {} }
local battle2 = { player = player2, enemy = enemy2, party = { player2 },
wild = true, turn = 0 }
function battle2:moveDisabled() return false end
local screen2 = { screenId = "Gen2BattleState", battle = battle2,
phase = "menu", menuIndex = 1, moveIndex = 1 }
function screen2:chooseMenu(choice)
self.chosenMenu = choice
if choice == "fight" then self.phase = "moves" end
return true
end
function screen2:chooseMove(slot)
self.chosenMove = slot
self.phase = "resolving"
return true
end
function screen2:cancelMove() self.phase = "menu" return true end
local game2 = {
data = {
pokemon = { CHIKORITA = { name = "CHIKORITA" },
RATTATA = { name = "RATTATA" } },
moves = { TACKLE = { name = "TACKLE", type = "NORMAL",
power = 35, accuracy = 95, pp = 35 } },
},
save = { party = { player2 } }, stack = { states = { screen2 } },
}
local api2 = require("src.battle.gen2.BattleAPI").new(game2)
local snapshot2 = api2:snapshot()
check(snapshot2 and snapshot2.kind == "wild" and snapshot2.prompt == "menu",
"Gold battle is discovered through its screen id")
eq(snapshot2.player.maxHp, 21, "Gold max HP uses the mon field")
eq(snapshot2.moves[1].name, "TACKLE", "Gold moves are copied")
snapshot2.player.hp = 0
snapshot2.moves[1].pp = 0
eq(player2.hp, 20, "changing a snapshot cannot change a Gold Pokemon")
eq(player2.moves[1].pp, 35,
"changing a snapshot cannot change a Gold move")
screen2.message = "A wild RATTATA appeared!"
screen2.phase = "resolving"
local message2 = api2:snapshot()
eq(message2.prompt, "advance", "Gold message state is exposed")
check(message2.revision > snapshot2.revision,
"Gold battle changes advance the revision")
game2.stack.states = {}
check(api2:snapshot() == nil, "Gold returns nil outside a battle")
game2.stack.states = { screen2 }
screen2.message = nil
screen2.phase = "menu"
local menu2 = api2:snapshot()
ok, err = api2:submit({ id = 1, revision = menu2.revision - 1,
kind = "menu", choice = "fight" })
check(not ok and err == "stale battle context",
"Gold rejects a stale intent")
ok, err = api2:submit({ id = 1, revision = menu2.revision,
kind = "menu", choice = "missing" })
check(not ok and err == "unknown battle menu choice",
"Gold rejects an unknown menu choice")
check(api2:submit({ id = 1, revision = menu2.revision,
kind = "menu", choice = "fight" }), "Gold accepts a menu intent")
eq(screen2.chosenMenu, "fight", "Gold uses the semantic menu path")
local moveMenu2 = api2:snapshot()
ok, err = api2:submit({ id = 2, revision = moveMenu2.revision,
kind = "move", slot = 9 })
check(not ok and err == "invalid move slot",
"Gold rejects an invalid move slot")
check(api2:submit({ id = 2, revision = moveMenu2.revision,
kind = "move", slot = 1 }), "Gold accepts a valid move")
eq(screen2.chosenMove, 1, "Gold uses the semantic move path")
screen2.phase = "moves"
local back2 = api2:snapshot()
check(api2:submit({ id = 3, revision = back2.revision, kind = "back" }),
"Gold accepts move-menu back")
eq(screen2.phase, "menu", "Gold back restores the command menu")
do
local Data = require("tests.modkit").fixtures.fresh()
local Pokemon = require("src.pokemon.Pokemon")
local SaveData = require("src.core.SaveData")
local save = SaveData.newGame()
save.party = { Pokemon.new(Data, "FIXMON_A", 20) }
local pressed = {}
local game3 = { data = Data, save = save, input = {
wasPressed = function(_, key) return pressed[key] == true end,
isDown = function() return false end,
}, stack = { states = {} } }
function game3.stack:top() return self.states[#self.states] end
function game3.stack:push(state) self.states[#self.states + 1] = state end
local real = Gen1BattleState.newWild(game3, "FIXMON_B", 12)
real.phase, real.queue, real.introSlide = "menu", {}, nil
game3.stack.states = { real }
pressed.a = true
real:update(1 / 60)
pressed.a = nil
eq(real.phase, "moveSelect", "native Gen 1 FIGHT uses the semantic path")
pressed.b = true
real:update(1 / 60)
pressed.b = nil
eq(real.phase, "menu", "native Gen 1 move-menu back still works")
end
local Loader = require("src.mods.Loader")
local fs = { read = function() end, getInfo = function() end,
getDirectoryItems = function() return {} end }
local mod = { path = "mods/snapshot_test", manifest = {
id = "snapshot_test", version = "1.0.0", permissionSet = {},
} }
local loader1 = Loader.new({ fs = fs, generation = 1 })
loader1.game = game
check(loader1:_api(mod).battle:snapshot().kind == "wild",
"mod.battle selects the Gen 1 facade")
local loader2 = Loader.new({ fs = fs, generation = 2 })
loader2.game = game2
check(loader2:_api(mod).battle:snapshot().kind == "wild",
"mod.battle selects the Gen 2 facade")
S.finish()