mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
219 lines
8.0 KiB
Lua
219 lines
8.0 KiB
Lua
-- Parity test: trainer next-mon after an AI switch must skip fainted slots.
|
|
--
|
|
-- Agatha (and any switchChance / switch AI) can withdraw mon 1, send mon 2,
|
|
-- then later return to mon 1. If mon 2 was KO'd in between, enemyMonFainted
|
|
-- used to do enemyIndex+1 and send the already-fainted Golbat back out.
|
|
-- The FIGHT menu reappears with an empty enemy HP bar; executeAction then
|
|
-- returns immediately on target.mon.hp <= 0 -- a softlock.
|
|
--
|
|
-- pokered's EnemySendOutFirstMon / AnyEnemyPokemonAliveCheck scan the party
|
|
-- for the first mon with HP remaining (core.asm), not "current index + 1".
|
|
--
|
|
-- Self-contained; run via `luajit tests/parity_ai_switch_faint.lua`.
|
|
-- Also picked up by tests/run_tests.lua's parity_* glob.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
|
|
local Data = require("src.core.Data")
|
|
if not (Data.pokemon and Data.pokemon.RATTATA) then Data:load() end
|
|
local TypeChart = require("src.battle.TypeChart")
|
|
TypeChart.load(Data)
|
|
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local BattleState = require("src.battle.BattleState")
|
|
local S = require("tests.harness").suite("parity ai switch faint")
|
|
local check, eq = S.check, S.eq
|
|
|
|
-- Minimal game stub matching what BattleState.newTrainer / enemyMonFainted
|
|
-- touch. SET style skips the SHIFT "change POKéMON?" ChoiceBox.
|
|
local function freshGame()
|
|
return {
|
|
data = Data,
|
|
save = {
|
|
party = { Pokemon.new(Data, "BULBASAUR", 50) },
|
|
player = { name = "RED" },
|
|
inventory = {},
|
|
options = { battleStyle = "set" },
|
|
pokedex = { seen = {}, owned = {} },
|
|
flags = {},
|
|
money = 0,
|
|
},
|
|
stack = { push = function() end, pop = function() end, top = function() end },
|
|
}
|
|
end
|
|
|
|
-- Drain act/say rows until the enemy swap act runs (or the queue empties).
|
|
local function pumpUntilSwap(b, limit)
|
|
limit = limit or 200
|
|
local n = 0
|
|
while #b.queue > 0 and n < limit do
|
|
n = n + 1
|
|
local item = table.remove(b.queue, 1)
|
|
if item.fn then
|
|
b.nextInsert = 0
|
|
item.fn()
|
|
-- the send-out act replaces self.enemy; stop once a living mon is in
|
|
if b.enemy and b.enemy.mon and b.enemy.mon.hp > 0 then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return b.enemy and b.enemy.mon and b.enemy.mon.hp > 0
|
|
end
|
|
|
|
-- #143: after AI-switch reorder, do not send a fainted slot.
|
|
do
|
|
local Game = freshGame()
|
|
local b = BattleState.newTrainer(Game, "OPP_AGATHA", 1)
|
|
check(#b.enemyParty >= 3, "Agatha has enough party slots for the scenario")
|
|
|
|
-- Simulate: switched to slot 2, KO'd it, switched back to slot 1, KO slot 1.
|
|
b.enemyParty[1].hp = 0
|
|
b.enemyParty[2].hp = 0
|
|
-- slots 3+ stay at full HP from construction
|
|
b.enemyIndex = 1
|
|
b.enemy.mon = b.enemyParty[1]
|
|
b.participants = { [Game.save.party[1]] = true }
|
|
|
|
b:enemyMonFainted()
|
|
check(pumpUntilSwap(b), "a living reserve is sent out after the KO")
|
|
eq(b.enemyIndex, 3, "next index is the first living mon, not fainted slot 2")
|
|
check(b.enemy.mon.hp > 0, "sent-out mon has HP (no empty-bar softlock)")
|
|
check(b.enemy.mon == b.enemyParty[3], "sent-out battler is party slot 3")
|
|
check(b.result ~= "win", "battle continues while reserves remain")
|
|
end
|
|
|
|
-- Sequential KOs (no AI switch) still advance one slot at a time.
|
|
do
|
|
local Game = freshGame()
|
|
local b = BattleState.newTrainer(Game, "OPP_YOUNGSTER", 1)
|
|
b.enemyParty[1].hp = 0
|
|
b.enemyIndex = 1
|
|
b.enemy.mon = b.enemyParty[1]
|
|
b.participants = { [Game.save.party[1]] = true }
|
|
b:enemyMonFainted()
|
|
check(pumpUntilSwap(b), "youngster still sends the second mon")
|
|
eq(b.enemyIndex, 2, "without AI switch, first living mon is still index 2")
|
|
check(b.enemy.mon.hp > 0, "second mon is healthy")
|
|
end
|
|
|
|
-- No living reserves: victory even if enemyIndex is mid-party (AI left
|
|
-- earlier slots dead and the active one was not the last index).
|
|
do
|
|
local Game = freshGame()
|
|
local b = BattleState.newTrainer(Game, "OPP_AGATHA", 1)
|
|
for _, mon in ipairs(b.enemyParty) do mon.hp = 0 end
|
|
b.enemyIndex = 2
|
|
b.enemy.mon = b.enemyParty[2]
|
|
b.participants = { [Game.save.party[1]] = true }
|
|
b:enemyMonFainted()
|
|
eq(b.result, "win", "all-fainted party ends the trainer battle")
|
|
eq(b.afterQueue, "finish", "victory finishes after the queue")
|
|
end
|
|
|
|
-- #162: SHIFT style with 2+ party slots announces the next mon and
|
|
-- offers a free switch (TrainerAboutToUseText + YES/NO). Party count
|
|
-- gates the offer (pokered wPartyCount), not living-HP count -- a fainted
|
|
-- reserve still unlocks the prompt.
|
|
do
|
|
local Game = freshGame()
|
|
Game.save.options.battleStyle = "shift"
|
|
local reserve = Pokemon.new(Data, "SQUIRTLE", 40)
|
|
reserve.hp = 0
|
|
table.insert(Game.save.party, reserve)
|
|
local b = BattleState.newTrainer(Game, "OPP_YOUNGSTER", 1)
|
|
check(#b.enemyParty >= 2, "youngster has a second mon")
|
|
b.enemyParty[1].hp = 0
|
|
b.enemyIndex = 1
|
|
b.enemy.mon = b.enemyParty[1]
|
|
b.participants = { [Game.save.party[1]] = true }
|
|
b:enemyMonFainted()
|
|
local about, willChoice, sent = false, false, false
|
|
local n = 0
|
|
while #b.queue > 0 and n < 200 do
|
|
n = n + 1
|
|
local item = table.remove(b.queue, 1)
|
|
if item.fn then
|
|
b.nextInsert = 0
|
|
item.fn()
|
|
elseif item.text then
|
|
if item.text:find("about to use", 1, true) then about = true end
|
|
if item.text:find("change POKéMON", 1, true) and item.choice then
|
|
willChoice = true
|
|
item.choice(false) -- decline; enemy still sends out
|
|
end
|
|
if item.text:find("sent", 1, true) then sent = true end
|
|
end
|
|
end
|
|
check(about, "SHIFT announces trainer about-to-use")
|
|
check(willChoice, "SHIFT queues Will-change with YES/NO choice")
|
|
check(sent, "enemy send-out still follows after declining")
|
|
check(b.enemy.mon.hp > 0, "second enemy mon is out after the offer")
|
|
end
|
|
|
|
-- SET style (and single-mon parties) skip the offer entirely.
|
|
do
|
|
local Game = freshGame()
|
|
Game.save.options.battleStyle = "set"
|
|
table.insert(Game.save.party, Pokemon.new(Data, "SQUIRTLE", 40))
|
|
local b = BattleState.newTrainer(Game, "OPP_YOUNGSTER", 1)
|
|
b.enemyParty[1].hp = 0
|
|
b.enemyIndex = 1
|
|
b.enemy.mon = b.enemyParty[1]
|
|
b.participants = { [Game.save.party[1]] = true }
|
|
b:enemyMonFainted()
|
|
local about = false
|
|
for _, item in ipairs(b.queue) do
|
|
if item.text and item.text:find("about to use", 1, true) then about = true end
|
|
end
|
|
check(not about, "SET style skips about-to-use")
|
|
end
|
|
|
|
-- #275: taking the SHIFT offer hands the whole exp share to the mon coming
|
|
-- in. EnemySendOutFirstMon zeroes wPartyGainExpFlags before jumping to
|
|
-- SwitchPlayerMon, which FLAG_SETs only the incoming mon's bit
|
|
-- (core.asm:1436-1443, 2424-2433), so the mon that was out when the enemy
|
|
-- fainted stops counting; leaving it in halved the switch-in's exp.
|
|
do
|
|
local Game = freshGame()
|
|
Game.save.options.battleStyle = "shift"
|
|
local reserve = Pokemon.new(Data, "SQUIRTLE", 40)
|
|
table.insert(Game.save.party, reserve)
|
|
local b = BattleState.newTrainer(Game, "OPP_YOUNGSTER", 1)
|
|
b.enemyParty[1].hp = 0
|
|
b.enemyIndex = 1
|
|
b.enemy.mon = b.enemyParty[1]
|
|
b.participants = { [Game.save.party[1]] = true }
|
|
-- the prompt's YES opens the battle party menu; answer it in place
|
|
local Screens = require("src.ui.Screens")
|
|
local origPush = Screens.push
|
|
Screens.push = function(_, id, opts)
|
|
if id == "PartyMenu" and opts and opts.onSwitch then
|
|
opts.onSwitch(reserve)
|
|
end
|
|
end
|
|
local ok, err = pcall(function()
|
|
b:enemyMonFainted()
|
|
local n = 0
|
|
while #b.queue > 0 and n < 400 do
|
|
n = n + 1
|
|
local item = table.remove(b.queue, 1)
|
|
if item.fn then
|
|
b.nextInsert = 0
|
|
item.fn()
|
|
elseif item.choice and item.text
|
|
and item.text:find("change POKéMON", 1, true) then
|
|
item.choice(true) -- take the free switch
|
|
end
|
|
end
|
|
end)
|
|
Screens.push = origPush
|
|
check(ok, "the SHIFT switch pumped without error: " .. tostring(err))
|
|
check(b.player.mon == reserve, "the SHIFT switch sent the reserve out")
|
|
check(b.participants[reserve] == true, "the switch-in gains exp")
|
|
check(b.participants[Game.save.party[1]] == nil,
|
|
"the mon that was out when the enemy fainted drops out (#275)")
|
|
end
|
|
|
|
S.finish()
|