mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 04:31:09 +02:00
CLOSES #339, CLOSES #354, CLOSES #360, CLOSES #372, CLOSES #373, CLOSES #374, CLOSES #375, CLOSES #378, CLOSES #379, CLOSES #383, CLOSES #384, CLOSES #385, CLOSES #391, CLOSES #392, CLOSES #393, CLOSES #394, CLOSES #395, CLOSES #396, CLOSES #397, CLOSES #398, CLOSES #413, CLOSES #420, CLOSES #423, CLOSES #424, CLOSES #425, CLOSES #426, CLOSES #427, CLOSES #429, CLOSES #430, CLOSES #431, CLOSES #433, CLOSES #435, CLOSES #436, CLOSES #438, CLOSES #439, CLOSES #441, CLOSES #442, CLOSES #444
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
-- Parity test: a ball thrown at the POKEMON_TOWER_6F RESTLESS SOUL is
|
||||
-- always dodged, scope or no scope.
|
||||
--
|
||||
-- ItemUseBall reaches the $10 "can't be caught" anim data by TWO
|
||||
-- independent routes (engine/items/item_effects.asm):
|
||||
--
|
||||
-- :149-153 callfar IsGhostBattle / ld b, $10 / jp z, .setAnimData
|
||||
-- :166-175 .notOldManBattle -- wCurMap == POKEMON_TOWER_6F and
|
||||
-- wEnemyMonSpecies2 == RESTLESS_SOUL -> the same $10
|
||||
--
|
||||
-- The port only had the first, as the scope-less disguise flag
|
||||
-- self.ghost. Once the SILPH_SCOPE revealed the MAROWAK the battle was
|
||||
-- an ordinary wild one, so throwBall ran the capture roll and a MASTER
|
||||
-- BALL caught it outright. That result is "caught", not "win" or the
|
||||
-- POKE DOLL escape, so PokemonTower6F's script never set
|
||||
-- EVENT_BEAT_GHOST_MAROWAK and the (10,16) trigger re-fired forever
|
||||
-- (#444). The map+species half sits BEFORE .loop, hence before the
|
||||
-- MASTER_BALL shortcut, so even a Master Ball is dodged.
|
||||
--
|
||||
-- Run-away parity is the other side of this: only IsGhostBattle grants
|
||||
-- the free escape (engine/battle/core.asm TryRunningFromBattle), so a
|
||||
-- revealed MAROWAK keeps normal flee rolls and self.ghost stays the sole
|
||||
-- gate there.
|
||||
--
|
||||
-- Self-contained; run via `luajit tests/parity_marowak_ball.lua`.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local S = require("tests.harness").suite("parity marowak ball")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
|
||||
-- ---- 1. the 6F script arms noCatch with and without the scope -----------
|
||||
do
|
||||
local realTextBox = package.loaded["src.render.TextBox"]
|
||||
local realBattleState = package.loaded["src.battle.BattleState"]
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(_, text, done) return { text = text, done = done } end,
|
||||
}
|
||||
local made = {}
|
||||
package.loaded["src.battle.BattleState"] = {
|
||||
newWild = function(_, species, level)
|
||||
local b = { species = species, level = level, ghost = false }
|
||||
b.makeGhost = function(self) self.ghost = true end
|
||||
made[#made + 1] = b
|
||||
return b
|
||||
end,
|
||||
}
|
||||
|
||||
local tower = dofile("data/scripts/story3.lua").POKEMON_TOWER_6F
|
||||
local function trigger(inventory)
|
||||
local pushed = {}
|
||||
local game = {
|
||||
save = { inventory = inventory, flags = {} },
|
||||
data = { text = {} },
|
||||
stack = { push = function(_, box) pushed[#pushed + 1] = box end },
|
||||
}
|
||||
local ow = {
|
||||
player = {},
|
||||
scriptMove = function() end,
|
||||
afterBattle = function() end,
|
||||
}
|
||||
check(tower.onStep(game, ow, 10, 16), "the trigger fires on (10,16)")
|
||||
pushed[1].done()
|
||||
return made[#made]
|
||||
end
|
||||
|
||||
local noScope = trigger({})
|
||||
check(noScope.ghost, "without the scope the battle is still disguised")
|
||||
check(noScope.noCatch, "and noCatch is set")
|
||||
|
||||
local withScope = trigger({ SILPH_SCOPE = 1 })
|
||||
check(not withScope.ghost, "with the scope the disguise is gone")
|
||||
check(withScope.noCatch,
|
||||
"but noCatch survives it -- balls are dodged either way")
|
||||
|
||||
package.loaded["src.render.TextBox"] = realTextBox
|
||||
package.loaded["src.battle.BattleState"] = realBattleState
|
||||
end
|
||||
|
||||
-- ---- 2. throwBall takes the dodge branch on noCatch alone ---------------
|
||||
local realSound = package.loaded["src.core.Sound"]
|
||||
package.loaded["src.core.Sound"] = { play = function() end }
|
||||
|
||||
-- A real BattleState minus the pieces the decision does not touch: the
|
||||
-- capture roll and the ball chain record that they were reached, which is
|
||||
-- exactly the bug (a MASTER BALL catching the revealed MAROWAK).
|
||||
local function throw(flags, ball)
|
||||
local self = setmetatable({
|
||||
kind = "wild",
|
||||
ghost = flags.ghost or false,
|
||||
noCatch = flags.noCatch or false,
|
||||
queue = {},
|
||||
rolled = false,
|
||||
chained = false,
|
||||
enemyMoved = false,
|
||||
turnEnded = false,
|
||||
data = { items = { MASTER_BALL = { name = "MASTER BALL" },
|
||||
POKE_BALL = { name = "POKé BALL" } },
|
||||
text = {} },
|
||||
game = { save = { player = { name = "RED" } } },
|
||||
player = {},
|
||||
enemy = {},
|
||||
}, BattleState)
|
||||
self.ballDef = function() return nil end
|
||||
self.catchAttempt = function(s) s.rolled = true return false, 3 end
|
||||
self.ballChain = function(s) s.chained = true end
|
||||
self.enemyAction = function() return {} end
|
||||
self.executeAction = function(s) s.enemyMoved = true end
|
||||
self.endOfTurn = function(s) s.turnEnded = true end
|
||||
self:throwBall(ball)
|
||||
-- the whole outcome lives in the act() closure throwBall queues, and
|
||||
-- that closure queues more rows, so drain like updateQueue does: run
|
||||
-- each fn row once, with nextInsert pointing at it.
|
||||
local ran = {}
|
||||
local more = true
|
||||
while more do
|
||||
more = false
|
||||
for i, row in ipairs(self.queue) do
|
||||
if row.fn and not ran[row] then
|
||||
ran[row] = true
|
||||
self.nextInsert = i
|
||||
row.fn()
|
||||
more = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local texts = {}
|
||||
for _, row in ipairs(self.queue) do
|
||||
if row.text then texts[#texts + 1] = tostring(row.text) end
|
||||
end
|
||||
self.texts = table.concat(texts, "|")
|
||||
return self
|
||||
end
|
||||
|
||||
local function assertDodge(b, label)
|
||||
check(not b.rolled, label .. ": no capture roll")
|
||||
check(not b.chained, label .. ": no wobble chain")
|
||||
check(b.texts:find("It dodged the", 1, true) ~= nil,
|
||||
label .. ": ItemUseBallText00 line 1")
|
||||
check(b.texts:find("can't be caught", 1, true) ~= nil,
|
||||
label .. ": ItemUseBallText00 line 2")
|
||||
check(b.enemyMoved, label .. ": the turn is spent, the foe moves")
|
||||
check(b.turnEnded, label .. ": and the turn ends")
|
||||
end
|
||||
|
||||
assertDodge(throw({ ghost = true }, "POKE_BALL"), "IsGhostBattle exit")
|
||||
assertDodge(throw({ noCatch = true }, "POKE_BALL"), ".notOldManBattle exit")
|
||||
-- the regression itself: revealed by the scope, so ghost is false
|
||||
assertDodge(throw({ noCatch = true }, "MASTER_BALL"), "MASTER BALL")
|
||||
|
||||
do
|
||||
local plain = throw({}, "MASTER_BALL")
|
||||
check(plain.rolled,
|
||||
"an ordinary wild mon still rolls -- the guard is not global")
|
||||
end
|
||||
|
||||
-- The dodged toss keeps the arc the thrown ball picked (TossBallAnimation
|
||||
-- reads wCurItem), so the Master Ball flicker is not lost.
|
||||
do
|
||||
local b = throw({ noCatch = true }, "MASTER_BALL")
|
||||
local anim
|
||||
for _, row in ipairs(b.queue) do
|
||||
if row.anim then anim = row.anim break end
|
||||
end
|
||||
eq("ULTRATOSS_ANIM", anim, "a dodged MASTER BALL still tosses as ULTRATOSS")
|
||||
end
|
||||
|
||||
package.loaded["src.core.Sound"] = realSound
|
||||
|
||||
-- ---- 3. noCatch grants no free escape ----------------------------------
|
||||
do
|
||||
local function roll(flags)
|
||||
local b = { ghost = flags.ghost or false, noCatch = flags.noCatch or false,
|
||||
runAttempts = 1, rng = function() return 255 end }
|
||||
return BattleState.runRollVanilla(b, 10, 100)
|
||||
end
|
||||
check(roll({ ghost = true }), "IsGhostBattle still always escapes")
|
||||
check(not roll({ noCatch = true }),
|
||||
"a revealed MAROWAK takes the normal flee roll")
|
||||
end
|
||||
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user