mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
242 lines
9.4 KiB
Lua
242 lines
9.4 KiB
Lua
-- Badge battle boosts: BadgeStatBoosts (engine/battle/core.asm:6534) and
|
|
-- DoBadgeTypeBoosts (engine/battle/misc.asm:146).
|
|
--
|
|
-- luajit tests/gen2_badge_boosts_test.lua
|
|
--
|
|
-- ROM-free. Every Johto badge on the boost walk raises the PLAYER's
|
|
-- in-battle stat by 1/8 -- Zephyr on Attack, Mineral on Defense and Plain on
|
|
-- Speed (the deliberate bit swap), Glacier on Special Attack with the buggy
|
|
-- Special Defense re-check -- and an owned badge whose BadgeTypeBoosts row
|
|
-- matches the move's type adds an eighth to the damage ahead of STAB.
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 badge boosts")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local Battle = require("src.battle.gen2.Battle")
|
|
local Damage = require("src.battle.gen2.Damage")
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
|
|
-- ---------------------------------------------------------------- fixtures
|
|
|
|
local TYPES = {
|
|
NORMAL = { id = "NORMAL", index = 0, category = "physical" },
|
|
FLYING = { id = "FLYING", index = 2, category = "physical" },
|
|
ROCK = { id = "ROCK", index = 5, category = "physical" },
|
|
FIRE = { id = "FIRE", index = 20, category = "special" },
|
|
WATER = { id = "WATER", index = 21, category = "special" },
|
|
}
|
|
|
|
local MOVES = {
|
|
TACKLE = { id = "TACKLE", name = "TACKLE", power = 35, type = "NORMAL",
|
|
accuracy = 100, pp = 35, effect = "EFFECT_NORMAL_HIT" },
|
|
ROCK_THROW = { id = "ROCK_THROW", name = "ROCK THROW", power = 50,
|
|
type = "ROCK", accuracy = 100, pp = 15, effect = "EFFECT_NORMAL_HIT" },
|
|
GUST = { id = "GUST", name = "GUST", power = 40, type = "FLYING",
|
|
accuracy = 100, pp = 35, effect = "EFFECT_NORMAL_HIT" },
|
|
}
|
|
|
|
local GROWTH = {
|
|
GROWTH_MEDIUM_FAST = { numerator = 1, denominator = 1, squared = 0,
|
|
linear = 0, constant = 0 },
|
|
}
|
|
|
|
local POKEMON = {
|
|
growthRates = GROWTH,
|
|
MACHOP = {
|
|
id = "MACHOP", index = 66, name = "MACHOP",
|
|
baseStats = { hp = 70, attack = 80, defense = 50, speed = 35,
|
|
specialAttack = 35, specialDefense = 35 },
|
|
types = { "NORMAL", "NORMAL" }, catchRate = 180, baseExp = 75,
|
|
growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 63,
|
|
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", "FLYING" }, 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 = {},
|
|
}
|
|
|
|
local perfect = { attack = 15, defense = 15, speed = 15, special = 15 }
|
|
perfect.hp = Mon.hpDV(perfect)
|
|
|
|
local function newBattle(badges, kantoBadges)
|
|
local player = Mon.new(DATA, "MACHOP", 20, { dvs = perfect })
|
|
player.moves = {
|
|
{ id = "TACKLE", pp = 35, maxPp = 35 },
|
|
{ id = "ROCK_THROW", pp = 15, maxPp = 15 },
|
|
{ id = "GUST", pp = 35, maxPp = 35 },
|
|
}
|
|
local wild = Mon.new(DATA, "PIDGEY", 20, { dvs = perfect })
|
|
wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
local save = { player = { id = 7, badges = badges or {},
|
|
kantoBadges = kantoBadges or {} } }
|
|
local battle = Battle.new({ data = DATA, party = { player }, wild = wild,
|
|
save = save, random = function(n) return (n or 1) > 1 and 1 or 0 end })
|
|
return battle, player, wild, save
|
|
end
|
|
|
|
-- ---- BoostStat: +1/8, capped at 999 ---------------------------------------
|
|
do
|
|
eq(Battle.boostStat(80), 90, "80 + 80/8 = 90")
|
|
eq(Battle.boostStat(7), 7, "a stat under 8 gains nothing (plain shift)")
|
|
eq(Battle.boostStat(999), 999, "the 999 cap holds")
|
|
eq(Battle.boostStat(950), 999, "and clips a boost past it")
|
|
end
|
|
|
|
-- ---- the stat walk: Zephyr/Mineral/Plain/Glacier --------------------------
|
|
do
|
|
local battle, player = newBattle({})
|
|
local base = player.stats.attack
|
|
eq(battle:battleStat(player, "attack"), base,
|
|
"no badge, no boost")
|
|
|
|
battle, player = newBattle({ ZEPHYR = true })
|
|
eq(battle:battleStat(player, "attack"), Battle.boostStat(base),
|
|
"ZEPHYRBADGE boosts Attack by 1/8")
|
|
eq(battle:battleStat(player, "defense"), player.stats.defense,
|
|
"and only Attack")
|
|
|
|
battle, player = newBattle({ MINERAL = true })
|
|
eq(battle:battleStat(player, "defense"),
|
|
Battle.boostStat(player.stats.defense),
|
|
"MINERALBADGE lands on Defense (the PlainBadge bit swap)")
|
|
|
|
battle, player = newBattle({ PLAIN = true })
|
|
eq(battle:battleStat(player, "speed"),
|
|
Battle.boostStat(player.stats.speed),
|
|
"PLAINBADGE lands on Speed")
|
|
|
|
battle, player = newBattle({ GLACIER = true })
|
|
eq(battle:battleStat(player, "specialAttack"),
|
|
Battle.boostStat(player.stats.specialAttack),
|
|
"GLACIERBADGE boosts Special Attack")
|
|
|
|
-- The ENEMY never gets badge boosts: BadgeStatBoosts runs against
|
|
-- wBattleMon only.
|
|
local wild = battle.enemy
|
|
eq(battle:battleStat(wild, "attack"), wild.stats.attack,
|
|
"the enemy's stats are never badge boosted")
|
|
|
|
-- Positional keying, the same fallback FieldMoves.hasBadge accepts.
|
|
battle, player = newBattle({ true })
|
|
eq(battle:battleStat(player, "attack"), Battle.boostStat(base),
|
|
"badges keyed by bit position read the same")
|
|
end
|
|
|
|
-- ---- the buggy Glacier Special Defense re-check ---------------------------
|
|
-- `srl a` runs on BoostStat's leftover cap arithmetic (core.asm:6584's own
|
|
-- "this check is buggy" comment), so whether SpDef gets the second boost
|
|
-- depends on the BOOSTED Special Attack value.
|
|
do
|
|
-- high(v) = 0: a = 0 - 3 - borrow; odd only when low(v) >= LOW(999).
|
|
eq(Battle.glacierBoostsSpDef(100), false, "SpA 100: no SpDef boost")
|
|
eq(Battle.glacierBoostsSpDef(240), true, "SpA 240: boosted")
|
|
-- high(v) = 1: odd while the borrow holds.
|
|
eq(Battle.glacierBoostsSpDef(300), true, "SpA 300: boosted")
|
|
eq(Battle.glacierBoostsSpDef(500), false, "SpA 500: not boosted")
|
|
-- high(v) = 2: only the no-borrow tail.
|
|
eq(Battle.glacierBoostsSpDef(600), false, "SpA 600: not boosted")
|
|
eq(Battle.glacierBoostsSpDef(750), true, "SpA 750: boosted")
|
|
-- high(v) = 3 under the cap always borrows.
|
|
eq(Battle.glacierBoostsSpDef(800), true, "SpA 800: boosted")
|
|
-- At or past the cap `a` leaves as LOW(999) = $e7, odd.
|
|
eq(Battle.glacierBoostsSpDef(999), true, "capped SpA: boosted")
|
|
|
|
local battle, player = newBattle({ GLACIER = true })
|
|
local spAtk = Battle.boostStat(player.stats.specialAttack)
|
|
local want = Battle.glacierBoostsSpDef(spAtk)
|
|
and Battle.boostStat(player.stats.specialDefense)
|
|
or player.stats.specialDefense
|
|
eq(battle:battleStat(player, "specialDefense"), want,
|
|
"battleStat routes SpDef through the buggy re-check")
|
|
|
|
battle, player = newBattle({})
|
|
eq(battle:battleStat(player, "specialDefense"),
|
|
player.stats.specialDefense,
|
|
"no Glacier, no SpDef boost ever (the srl lands on a zero bit)")
|
|
end
|
|
|
|
-- ---- Plain Badge Speed feeds turn order -----------------------------------
|
|
do
|
|
local battle, player = newBattle({ PLAIN = true })
|
|
eq(battle:effectiveSpeed(player),
|
|
Damage.applyStage(Battle.boostStat(player.stats.speed), 0),
|
|
"effectiveSpeed reads the boosted Speed")
|
|
end
|
|
|
|
-- ---- DoBadgeTypeBoosts in the damage pipe ---------------------------------
|
|
do
|
|
-- Hand-sized numbers: level 10, power 40, attack 50, defense 40.
|
|
-- base = (floor(10*2/5)+2) * 40 * 50 / 40 / 50 -> 6, +MIN_DAMAGE = 8.
|
|
local args = {
|
|
level = 10, power = 40, moveType = "ROCK",
|
|
attacker = { attack = 50, types = {}, stages = {} },
|
|
defender = { defense = 40, types = {}, stages = {} },
|
|
types = TYPES, matchups = {},
|
|
variation = 100,
|
|
}
|
|
local plain = Damage.calc(args)
|
|
eq(plain, 8, "the unboosted pipe leaves 8")
|
|
args.badgeTypeBoost = true
|
|
eq(Damage.calc(args), 9,
|
|
"the badge boost adds an eighth (at least 1) ahead of STAB")
|
|
end
|
|
|
|
-- ---- the boost table walks Johto then Kanto -------------------------------
|
|
do
|
|
local battle = newBattle({ ZEPHYR = true })
|
|
eq(battle:badgeTypeBoost(battle.player, "FLYING"), true,
|
|
"ZEPHYRBADGE boosts FLYING moves")
|
|
eq(battle:badgeTypeBoost(battle.player, "ROCK"), false,
|
|
"but not ROCK (BOULDERBADGE's row, not owned)")
|
|
eq(battle:badgeTypeBoost(battle.enemy, "FLYING"), false,
|
|
"and never on the enemy's turn (hBattleTurn gate)")
|
|
|
|
battle = newBattle({}, { BOULDER = true })
|
|
eq(battle:badgeTypeBoost(battle.player, "ROCK"), true,
|
|
"BOULDERBADGE (wKantoBadges) boosts ROCK -- Brock's own line")
|
|
eq(battle:badgeTypeBoost(battle.player, "NORMAL"), false,
|
|
"PLAINBADGE's NORMAL row stays shut without the badge")
|
|
|
|
battle = newBattle({}, { VOLCANO = true, EARTH = true })
|
|
eq(battle:badgeTypeBoost(battle.player, "FIRE"), true,
|
|
"VOLCANOBADGE boosts FIRE")
|
|
eq(battle:badgeTypeBoost(battle.player, "GROUND"), true,
|
|
"EARTHBADGE boosts GROUND")
|
|
end
|
|
|
|
-- ---- through the real hit -------------------------------------------------
|
|
do
|
|
-- Same battle twice, the only difference the badge: the ROCK move's damage
|
|
-- must grow and the NORMAL move's must not.
|
|
local function hit(badges, kanto, move)
|
|
local battle = newBattle(badges, kanto)
|
|
local enemy = battle.enemy
|
|
local before = enemy.hp
|
|
battle:useMove(battle.player, enemy, move)
|
|
return before - enemy.hp
|
|
end
|
|
local bare = hit({}, {}, "ROCK_THROW")
|
|
local badged = hit({}, { BOULDER = true }, "ROCK_THROW")
|
|
check(badged > bare,
|
|
("BOULDERBADGE raises ROCK THROW's damage (%d -> %d)"):format(
|
|
bare, badged))
|
|
eq(hit({}, { BOULDER = true }, "TACKLE"), hit({}, {}, "TACKLE"),
|
|
"a NORMAL move is not BOULDERBADGE's business")
|
|
end
|
|
|
|
S.finish()
|