Files
gen1recomp/tests/gen2_specialty_balls_test.lua
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

190 lines
7.9 KiB
Lua

-- Kurt's balls and the specialty multipliers
-- (engine/items/item_effects.asm BallMultiplierFunctionTable).
--
-- luajit tests/gen2_specialty_balls_test.lua
--
-- ROM-free. Heavy Ball's dex-weight brackets (Lugia's 4760 tenths-lb turns
-- catch rate 3 into 23, the walkthrough's whole reason to brew one), Level
-- Ball's three rungs, Lure Ball's fishing gate, and the three deliberate
-- cart bugs: Fast Ball knows exactly three species, Love Ball boosts
-- SAME-sex pairs, Moon Ball never boosts. Friend Ball is happiness only,
-- proven through the real catch in BattleState.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 specialty balls")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
local Battle = require("src.battle.gen2.Battle")
local BattleState = require("src.ui.gen2.BattleState")
local Catching = require("src.battle.gen2.Catching")
local Input = require("src.core.Input")
local Mon = require("src.battle.gen2.Mon")
-- ---- Heavy Ball -----------------------------------------------------------
do
-- Lugia: dex weight 4760 (tenths of a pound) converts to 2158 tenths-kg
-- through the cart's w/2 - w/32 - w/64 walk; high byte 8 is the +20 rung.
eq(Catching.heavyBallBoost(4760), 20, "Lugia sits in the +20 bracket")
eq(Catching.specialtyRate(3, "HEAVY_BALL", { weight = 4760 }), 23,
"catch rate 3 becomes 23 -- against the Ultra Ball's 6")
-- A light mon LOSES 20, floored at 1 (`ld b, $1` on underflow).
eq(Catching.heavyBallBoost(40), -20, "a 4 lb mon is in the -20 bracket")
eq(Catching.specialtyRate(3, "HEAVY_BALL", { weight = 40 }), 1,
"and the subtraction floors at 1")
-- Snorlax: 10140 tenths-lb -> 4599 tenths-kg, high byte 17: +40.
eq(Catching.heavyBallBoost(10140), 40, "Snorlax earns the +40 rung")
eq(Catching.specialtyRate(25, "HEAVY_BALL", {}), 25,
"no weight supplied, no change")
end
-- ---- Level Ball -----------------------------------------------------------
do
local function level(rate, player, enemy)
return Catching.specialtyRate(rate, "LEVEL_BALL",
{ playerLevel = player, level = enemy })
end
eq(level(30, 10, 10), 30, "an equal level is no boost")
eq(level(30, 20, 15), 60, "below the player's level: x2")
eq(level(30, 40, 15), 120, "below half: x4")
eq(level(30, 44, 10), 240, "below a quarter: x8")
eq(level(30, 40, 10), 120,
"the rungs are strict: 10 is NOT below floor(40/4)")
eq(level(200, 44, 10), 255, "capped at 255 like every sla")
end
-- ---- Lure Ball ------------------------------------------------------------
do
eq(Catching.specialtyRate(45, "LURE_BALL", { fishing = true }), 135,
"x3 on a BATTLETYPE_FISH encounter")
eq(Catching.specialtyRate(45, "LURE_BALL", {}), 45,
"and nothing anywhere else")
end
-- ---- Fast Ball's bug ------------------------------------------------------
do
for species in pairs({ MAGNEMITE = 1, GRIMER = 1, TANGELA = 1 }) do
eq(Catching.specialtyRate(45, "FAST_BALL", { species = species }), 180,
species .. " is one of the three the bug leaves covered")
end
eq(Catching.specialtyRate(45, "FAST_BALL", { species = "DRATINI" }), 45,
"DRATINI flees on the cart too, but the broken loop never sees it")
end
-- ---- Moon Ball's bug ------------------------------------------------------
do
eq(Catching.specialtyRate(45, "MOON_BALL",
{ evolveItem = "MOON_STONE" }), 45,
"a Moon Stone evolver gets nothing: the check reads Gen 1's constant")
eq(Catching.specialtyRate(45, "MOON_BALL",
{ evolveItem = "MOON_STONE", fixBugs = true }), 180,
"fixBugs restores the intended x4")
end
-- ---- Love Ball's bug ------------------------------------------------------
do
local function love_(gender, playerGender, fix)
return Catching.specialtyRate(30, "LOVE_BALL", {
species = "NIDORAN_F", playerSpecies = "NIDORAN_F",
gender = gender, playerGender = playerGender, fixBugs = fix,
})
end
eq(love_("female", "female"), 240,
"same species, SAME sex: the x8 the `ret nz` bug ships")
eq(love_("female", "male"), 30, "opposite sexes get nothing")
eq(love_("female", "male", true), 240, "fixBugs flips it back")
eq(Catching.specialtyRate(30, "LOVE_BALL", {
species = "NIDORAN_F", playerSpecies = "PIDGEY",
gender = "female", playerGender = "female" }), 30,
"different species never boost")
end
-- ---- through Catching.rate ------------------------------------------------
do
-- Full HP Lugia at rate 3: (3*maxHp - 2*hp) with the >=256 shift path,
-- both balls through the same formula; the Heavy Ball's 23 must beat the
-- Ultra Ball's 6 on the final rate too.
local heavy = Catching.rate({ maxHp = 200, hp = 200, catchRate = 3,
ball = "HEAVY_BALL", weight = 4760 })
local ultra = Catching.rate({ maxHp = 200, hp = 200, catchRate = 3,
ball = "ULTRA_BALL" })
check(heavy > ultra,
("the brewed ball out-catches the bought one (%d > %d)"):format(
heavy, ultra))
end
-- ---- Friend Ball happiness through the real catch -------------------------
do
local TYPES = { NORMAL = { id = "NORMAL", index = 0,
category = "physical" } }
local MOVES = { TACKLE = { id = "TACKLE", name = "TACKLE", power = 35,
type = "NORMAL", accuracy = 95, pp = 35,
effect = "EFFECT_NORMAL_HIT" } }
local POKEMON = {
growthRates = { GROWTH_MEDIUM_FAST = { numerator = 1, denominator = 1,
squared = 0, linear = 0, constant = 0 } },
CYNDAQUIL = {
id = "CYNDAQUIL", index = 155, name = "CYNDAQUIL",
baseStats = { hp = 39, attack = 52, defense = 43, speed = 65,
specialAttack = 60, specialDefense = 50 },
types = { "NORMAL", "NORMAL" }, catchRate = 45, baseExp = 65,
growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 31,
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {},
},
PIDGEY = {
id = "PIDGEY", index = 16, name = "PIDGEY",
baseStats = { hp = 40, attack = 45, defense = 40, speed = 56,
specialAttack = 35, specialDefense = 35 },
types = { "NORMAL", "NORMAL" }, catchRate = 255, baseExp = 55,
growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 127,
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {},
},
}
local DATA = {
pokemon = POKEMON,
moves = MOVES,
type_chart = { types = TYPES, matchups = {} },
items = {
FRIEND_BALL = { id = "FRIEND_BALL", name = "FRIEND BALL",
pocket = "BALL" },
POKE_BALL = { id = "POKE_BALL", name = "POKe BALL", pocket = "BALL" },
},
}
local perfect = { attack = 15, defense = 15, speed = 15, special = 15 }
perfect.hp = Mon.hpDV(perfect)
local function catchWith(ball)
Input:init()
local player = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
player.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
local wild = Mon.new(DATA, "PIDGEY", 5, { dvs = perfect })
wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
local save = { party = { player }, inventory = { [ball] = 1 } }
local game = {
data = DATA, save = save, input = Input, options = {},
stack = { push = function() end, pop = function() end,
top = function() return nil end },
}
-- random 0: the catch roll always lands (rate 255 PIDGEY).
local battle = Battle.new({ data = DATA, party = { player },
wild = wild, save = save, random = function() return 0 end })
local screen = BattleState.new(game, { battle = battle, save = save })
screen:useItem(ball)
return battle, save, wild
end
local battle, save, wild = catchWith("FRIEND_BALL")
eq(battle.outcome, "caught", "the Friend Ball catch lands")
eq(wild.happiness, Catching.FRIEND_BALL_HAPPINESS,
"and the caught mon's happiness is set to 200")
eq(save.party[2], wild, "in the party slot it landed in")
battle, save, wild = catchWith("POKE_BALL")
eq(battle.outcome, "caught", "the control catch lands too")
eq(wild.happiness, 70, "an ordinary ball leaves the base 70")
end
S.finish()