mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
1905261c5b
CLOSES #1483, CLOSES #1610, CLOSES #1615, CLOSES #1646, CLOSES #1649, CLOSES #1651, CLOSES #1653, CLOSES #1656, CLOSES #1683, CLOSES #1685, CLOSES #1686, CLOSES #1687, CLOSES #1688, CLOSES #1689, CLOSES #1690, CLOSES #1693, CLOSES #1694, CLOSES #1695, CLOSES #1696, CLOSES #1702, CLOSES #1704, CLOSES #1705, CLOSES #1706, CLOSES #1707, CLOSES #1708, CLOSES #1710, CLOSES #1711, CLOSES #1712, CLOSES #1713, CLOSES #1716, CLOSES #1717, CLOSES #1718, CLOSES #1719, CLOSES #1720, CLOSES #1721, CLOSES #1725, CLOSES #1732, CLOSES #1745, CLOSES #1748, CLOSES #1749, CLOSES #1751, CLOSES #1754
214 lines
7.8 KiB
Lua
214 lines
7.8 KiB
Lua
-- The counter a sleep MOVE writes: BattleCommand_SleepTarget's .random_loop
|
|
-- (engine/battle/effect_commands.asm:3591-3598), which masks with SLP_MASK,
|
|
-- rerolls 0 and SLP_MASK and only then does `inc a` -- so 2-7, never 1 (#1707).
|
|
-- Rest and the disobedience nap keep their own counters and are the controls.
|
|
--
|
|
-- luajit tests/gen2_sleep_counter_test.lua -- ROM-free
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 sleep counter")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local Battle = require("src.battle.gen2.Battle")
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
|
|
-- ---------------------------------------------------------------- fixtures
|
|
|
|
local TYPES = {
|
|
NORMAL = { id = "NORMAL", index = 0, category = "physical" },
|
|
FLYING = { id = "FLYING", index = 2, category = "physical" },
|
|
FIRE = { id = "FIRE", index = 20, category = "special" },
|
|
GRASS = { id = "GRASS", index = 22, category = "special" },
|
|
PSYCHIC = { id = "PSYCHIC", index = 24, category = "special" },
|
|
}
|
|
|
|
local MATCHUPS = {}
|
|
|
|
local MOVES = {
|
|
TACKLE = { id = "TACKLE", name = "TACKLE", power = 35, type = "NORMAL",
|
|
accuracy = 100, pp = 35, effect = "EFFECT_NORMAL_HIT" },
|
|
-- data/moves/moves.asm: SPORE and HYPNOSIS are both EFFECT_SLEEP.
|
|
SPORE = { id = "SPORE", name = "SPORE", power = 0, type = "GRASS",
|
|
accuracy = 100, pp = 15, effect = "EFFECT_SLEEP" },
|
|
HYPNOSIS = { id = "HYPNOSIS", name = "HYPNOSIS", power = 0,
|
|
type = "PSYCHIC", accuracy = 100, pp = 20, effect = "EFFECT_SLEEP" },
|
|
REST = { id = "REST", name = "REST", power = 0, type = "PSYCHIC",
|
|
accuracy = 100, pp = 10, effect = "EFFECT_HEAL" },
|
|
}
|
|
|
|
local GROWTH = {
|
|
GROWTH_MEDIUM_SLOW = { numerator = 6, denominator = 5, squared = -15,
|
|
linear = 100, constant = 140 },
|
|
}
|
|
|
|
local POKEMON = {
|
|
growthRates = GROWTH,
|
|
-- Fast enough to move first against the PIDGEY below at these levels.
|
|
CYNDAQUIL = {
|
|
id = "CYNDAQUIL", index = 155, name = "CYNDAQUIL",
|
|
baseStats = { hp = 39, attack = 52, defense = 43, speed = 65,
|
|
specialAttack = 60, specialDefense = 50 },
|
|
types = { "FIRE", "FIRE" }, catchRate = 45, baseExp = 65,
|
|
growthRate = "GROWTH_MEDIUM_SLOW", 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", "FLYING" }, catchRate = 255, baseExp = 55,
|
|
growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 127,
|
|
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {},
|
|
},
|
|
}
|
|
|
|
local DATA = {
|
|
pokemon = POKEMON,
|
|
moves = MOVES,
|
|
type_chart = { types = TYPES, matchups = MATCHUPS },
|
|
items = {},
|
|
}
|
|
|
|
local perfect = { attack = 15, defense = 15, speed = 15, special = 15 }
|
|
perfect.hp = Mon.hpDV(perfect)
|
|
|
|
-- BattleRandom(n) -> 0..n-1, pinned to one value.
|
|
local function zeroRandom() return 0 end
|
|
local function fixedRandom(value)
|
|
return function(n) return math.min(value, (n or 1) - 1) end
|
|
end
|
|
|
|
local function newBattle(opts)
|
|
opts = opts or {}
|
|
local player = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
player.moves = {
|
|
{ id = "SPORE", pp = 15, maxPp = 15 },
|
|
{ id = "TACKLE", pp = 35, maxPp = 35 },
|
|
}
|
|
local wild = Mon.new(DATA, "PIDGEY", 5, { dvs = perfect })
|
|
wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
local battle = Battle.new({
|
|
data = DATA,
|
|
party = { player },
|
|
wild = wild,
|
|
random = opts.random or zeroRandom,
|
|
})
|
|
battle:takeEvents()
|
|
return battle, player, wild
|
|
end
|
|
|
|
local function texts(events)
|
|
local out = {}
|
|
for _, event in ipairs(events or {}) do
|
|
if event.text then out[#out + 1] = event.text end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function found(events, needle)
|
|
for _, text in ipairs(texts(events)) do
|
|
if text:find(needle, 1, true) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- ------------------------------------------------------- the roll's range
|
|
|
|
-- The whole roll space against .random_loop's window: 2..7 inclusive, six
|
|
-- outcomes, no 1 and no 8 (effect_commands.asm:3591-3598).
|
|
do
|
|
local seen, low, high = {}, math.huge, -math.huge
|
|
for roll = 0, 7 do
|
|
local battle, _, wild = newBattle({ random = fixedRandom(roll) })
|
|
battle:useMove(battle.player, wild, "SPORE")
|
|
eq(wild.status, "sleep", "roll " .. roll .. " slept the target")
|
|
local turns = wild.statusTurns or 0
|
|
seen[turns] = true
|
|
low, high = math.min(low, turns), math.max(high, turns)
|
|
end
|
|
eq(low, 2, "the shortest sleep a sleep move can roll is two turns")
|
|
eq(high, 7, "the longest is seven, SLP_MASK itself never being written")
|
|
check(not seen[1], "one turn is unreachable, so no free-action sleep")
|
|
check(not seen[0], "and zero is unreachable")
|
|
local count = 0
|
|
for _ in pairs(seen) do count = count + 1 end
|
|
eq(count, 6, "six distinct counters, the width of the cart's window")
|
|
end
|
|
|
|
do
|
|
local battle, _, wild = newBattle()
|
|
battle:useMove(battle.player, wild, "SPORE")
|
|
eq(wild.statusTurns, 2, "the lowest roll writes two, not one")
|
|
end
|
|
|
|
-- ------------------------------------------- slept by a faster foe (#1707)
|
|
|
|
-- CheckPlayerTurn decrements before the sleeper acts, so the round the sleep
|
|
-- landed in is the one a 1 would have cost nothing.
|
|
do
|
|
local battle, player, wild = newBattle()
|
|
local hpBefore = player.hp
|
|
local events = battle:takeTurn({ kind = "move", move = "SPORE" })
|
|
eq(wild.status, "sleep", "the slower foe is asleep")
|
|
check(found(events, "is fast asleep!"),
|
|
"and spends the same round's action asleep")
|
|
check(not found(events, "woke up!"),
|
|
"it cannot wake in the round it was slept")
|
|
eq(wild.statusTurns, 1, "one turn of the counter was spent")
|
|
eq(player.hp, hpBefore, "so the sleeper landed no attack of its own")
|
|
end
|
|
|
|
-- The counter still runs out on the next round.
|
|
do
|
|
local battle, _, wild = newBattle()
|
|
battle:takeTurn({ kind = "move", move = "SPORE" })
|
|
local events = battle:takeTurn({ kind = "move", move = "TACKLE" })
|
|
check(found(events, "woke up!"), "the next round wakes it")
|
|
eq(wild.status, nil, "and clears the status byte")
|
|
eq(wild.statusTurns, nil, "and the counter with it")
|
|
end
|
|
|
|
-- Hypnosis shares EFFECT_SLEEP, so it shares the window.
|
|
do
|
|
local battle, _, wild = newBattle()
|
|
battle.player.moves[1] = { id = "HYPNOSIS", pp = 20, maxPp = 20 }
|
|
battle:useMove(battle.player, wild, "HYPNOSIS")
|
|
eq(wild.status, "sleep", "hypnosis slept the target")
|
|
eq(wild.statusTurns, 2, "off the same .random_loop")
|
|
end
|
|
|
|
-- ------------------------------------------------------------- the controls
|
|
|
|
-- Rest writes REST_SLEEP_TURNS + 1 straight into the status byte, no roll
|
|
-- (effect_commands.asm:6015-6027).
|
|
do
|
|
local battle, player, wild = newBattle()
|
|
player.moves = { { id = "REST", pp = 10, maxPp = 10 },
|
|
{ id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
player.hp = 1
|
|
battle:useMove(player, wild, "REST")
|
|
eq(player.status, "sleep", "rest slept the user")
|
|
eq(player.statusTurns, 3, "rest is still exactly three")
|
|
eq(player.hp, player.maxHp, "and still a full heal")
|
|
|
|
local events = battle:takeTurn({ kind = "move", move = "TACKLE" })
|
|
check(found(events, "is fast asleep!"), "the first turn after rest is lost")
|
|
events = battle:takeTurn({ kind = "move", move = "TACKLE" })
|
|
check(found(events, "is fast asleep!"), "so is the second")
|
|
events = battle:takeTurn({ kind = "move", move = "TACKLE" })
|
|
check(found(events, "woke up!"), "and it acts on the third")
|
|
end
|
|
|
|
-- The disobedience nap rerolls only 0 and has no `inc a`, so 1 is legal
|
|
-- there (effect_commands.asm:778-785) and the two rolls are not shared.
|
|
do
|
|
local record = Battle.STATUSES.sleep
|
|
check(type(record.onInflict) == "function", "sleep still has an onInflict")
|
|
local mon = {}
|
|
record.onInflict({ random = zeroRandom }, mon)
|
|
eq(mon.statusTurns, 2, "the status record's own floor is two")
|
|
end
|
|
|
|
S.finish()
|