mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
CLOSES #1181, CLOSES #1212, CLOSES #1214, CLOSES #1224, CLOSES #1230, CLOSES #1249, CLOSES #1271, CLOSES #1272, CLOSES #1273, CLOSES #1298, CLOSES #1305, CLOSES #1307, CLOSES #1318, CLOSES #1328, CLOSES #1330, CLOSES #1331, CLOSES #1333, CLOSES #1334, CLOSES #1335, CLOSES #1340, CLOSES #1345, CLOSES #1346, CLOSES #1360, CLOSES #1362 (#1395)
* CLOSES #1181, CLOSES #1212, CLOSES #1214, CLOSES #1224, CLOSES #1230, CLOSES #1249, CLOSES #1271, CLOSES #1272, CLOSES #1273, CLOSES #1298, CLOSES #1305, CLOSES #1307, CLOSES #1318, CLOSES #1328, CLOSES #1330, CLOSES #1331, CLOSES #1333, CLOSES #1334, CLOSES #1335, CLOSES #1340, CLOSES #1345, CLOSES #1346, CLOSES #1360, CLOSES #1362 * conv
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
-- engine/battle/effect_commands.asm:1553-1614 BattleCommand_CheckHit
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local Battle = require("src.battle.gen2.Battle")
|
||||
local Effects = require("src.battle.gen2.Effects")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
local TYPES = {
|
||||
NORMAL = { id = "NORMAL", index = 0, category = "physical" },
|
||||
FIGHTING = { id = "FIGHTING", index = 1, category = "physical" },
|
||||
}
|
||||
|
||||
local MOVES = {
|
||||
TACKLE = { id = "TACKLE", name = "TACKLE", power = 35, type = "NORMAL",
|
||||
accuracy = 100, pp = 35, effect = "EFFECT_NORMAL_HIT" },
|
||||
SWIFT = { id = "SWIFT", name = "SWIFT", power = 60, type = "NORMAL",
|
||||
accuracy = 100, pp = 20, effect = "EFFECT_ALWAYS_HIT" },
|
||||
VITAL_THROW = { id = "VITAL_THROW", name = "VITAL THROW", power = 70,
|
||||
type = "FIGHTING", accuracy = 100, pp = 10,
|
||||
effect = "EFFECT_ALWAYS_HIT" },
|
||||
}
|
||||
|
||||
local POKEMON = {
|
||||
growthRates = {
|
||||
GROWTH_MEDIUM_FAST = { numerator = 1, denominator = 1, squared = 0,
|
||||
linear = 0, constant = 0 },
|
||||
},
|
||||
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 = {},
|
||||
},
|
||||
}
|
||||
|
||||
local DATA = {
|
||||
pokemon = POKEMON,
|
||||
moves = MOVES,
|
||||
type_chart = { types = TYPES, matchups = {} },
|
||||
items = {
|
||||
BRIGHTPOWDER = { id = "BRIGHTPOWDER", name = "BRIGHTPOWDER",
|
||||
heldEffect = "HELD_BRIGHTPOWDER", heldParameter = 51 },
|
||||
},
|
||||
}
|
||||
|
||||
local perfect = { attack = 15, defense = 15, speed = 15, special = 15 }
|
||||
perfect.hp = Mon.hpDV(perfect)
|
||||
|
||||
local function alwaysHighRoll(n)
|
||||
return 99 % math.max(1, n or 1)
|
||||
end
|
||||
|
||||
local function newBattle()
|
||||
local player = Mon.new(DATA, "MACHOP", 15, { dvs = perfect })
|
||||
player.moves = {
|
||||
{ id = "TACKLE", pp = 35, maxPp = 35 },
|
||||
{ id = "SWIFT", pp = 20, maxPp = 20 },
|
||||
{ id = "VITAL_THROW", pp = 10, maxPp = 10 },
|
||||
}
|
||||
local wild = Mon.new(DATA, "MACHOP", 15, { dvs = perfect })
|
||||
wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||
return Battle.new({ data = DATA, party = { player }, wild = wild,
|
||||
random = alwaysHighRoll }), player, wild
|
||||
end
|
||||
|
||||
local function moveEvent(events)
|
||||
for _, event in ipairs(events or {}) do
|
||||
if event.kind == "move" then return event end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function findText(events, text)
|
||||
for _, event in ipairs(events or {}) do
|
||||
if event.kind == "message" and event.text == text then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function use(battle, player, wild, moveId)
|
||||
wild.hp = wild.maxHp
|
||||
battle:useMove(player, wild, moveId)
|
||||
local events = battle:takeEvents()
|
||||
return (moveEvent(events) or {}).missed == true, events
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle()
|
||||
battle.stages.enemy.evasion = Effects.MAX_STAGE
|
||||
T.check(use(battle, player, wild, "TACKLE"),
|
||||
"at +6 evasion a 100-accuracy move misses on a high roll")
|
||||
T.check(not use(battle, player, wild, "SWIFT"),
|
||||
"SWIFT ignores the evasion stage")
|
||||
T.check(wild.hp < wild.maxHp, "and the hit lands damage")
|
||||
T.check(not use(battle, player, wild, "VITAL_THROW"),
|
||||
"VITAL THROW carries the same effect")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle()
|
||||
battle.stages.player.accuracy = -Effects.MAX_STAGE
|
||||
T.check(use(battle, player, wild, "TACKLE"),
|
||||
"at -6 accuracy a 100-accuracy move misses on a high roll")
|
||||
T.check(not use(battle, player, wild, "SWIFT"),
|
||||
"SWIFT ignores the accuracy stage")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle()
|
||||
wild.item = "BRIGHTPOWDER"
|
||||
T.check(use(battle, player, wild, "TACKLE"),
|
||||
"BRIGHTPOWDER alone is enough to miss on a 99 roll")
|
||||
T.check(not use(battle, player, wild, "SWIFT"),
|
||||
"the `ret z` sits ahead of .BrightPowder too")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle()
|
||||
battle:volatile(wild).vanished = true
|
||||
battle:volatile(wild).chargeMove = "FLY"
|
||||
local missed, events = use(battle, player, wild, "SWIFT")
|
||||
T.check(missed, "a FLY target still dodges SWIFT")
|
||||
T.check(findText(events, "MACHOP's attack missed!"), "with CheckHit's .Miss")
|
||||
T.eq(wild.hp, wild.maxHp, "and takes nothing")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle()
|
||||
battle:volatile(wild).vanished = true
|
||||
battle:volatile(wild).chargeMove = "DIG"
|
||||
T.check(use(battle, player, wild, "SWIFT"), "a DIG target dodges it too")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle()
|
||||
battle:volatile(wild).protect = true
|
||||
local missed, events = use(battle, player, wild, "SWIFT")
|
||||
T.check(missed, "PROTECT still turns SWIFT aside")
|
||||
T.check(findText(events, "MACHOP protected itself!"), "with .Protect's line")
|
||||
T.eq(wild.hp, wild.maxHp, "and takes nothing")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle()
|
||||
T.check(not use(battle, player, wild, "SWIFT"),
|
||||
"with no stage moved at all SWIFT still hits")
|
||||
T.check(not use(battle, player, wild, "TACKLE"),
|
||||
"and a plain move only fails the 99 roll once a stage has moved")
|
||||
end
|
||||
|
||||
T.finish("gen2 always-hit bug 1272")
|
||||
@@ -0,0 +1,268 @@
|
||||
-- engine/battle/move_effects/pay_day.asm:13, engine/battle/core.asm:8014-8042
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local T = require("tests.harness")
|
||||
local Battle = require("src.battle.gen2.Battle")
|
||||
local BattleState = require("src.ui.gen2.BattleState")
|
||||
local Effects = require("src.battle.gen2.Effects")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local Prize = require("src.battle.gen2.Prize")
|
||||
|
||||
local LEVEL = 15
|
||||
|
||||
local TYPES = {
|
||||
NORMAL = { id = "NORMAL", index = 0, category = "physical" },
|
||||
}
|
||||
|
||||
local MOVES = {
|
||||
TACKLE = { id = "TACKLE", name = "TACKLE", power = 35, type = "NORMAL",
|
||||
accuracy = 100, pp = 35, effect = "EFFECT_NORMAL_HIT" },
|
||||
PAY_DAY = { id = "PAY_DAY", name = "PAY DAY", power = 40, type = "NORMAL",
|
||||
accuracy = 100, pp = 20, effect = "EFFECT_PAY_DAY" },
|
||||
}
|
||||
|
||||
local POKEMON = {
|
||||
growthRates = {
|
||||
GROWTH_MEDIUM_FAST = { numerator = 1, denominator = 1, squared = 0,
|
||||
linear = 0, constant = 0 },
|
||||
},
|
||||
MEOWTH = {
|
||||
id = "MEOWTH", index = 52, name = "MEOWTH",
|
||||
baseStats = { hp = 40, attack = 45, defense = 35, speed = 90,
|
||||
specialAttack = 40, specialDefense = 40 },
|
||||
types = { "NORMAL", "NORMAL" }, catchRate = 255, baseExp = 69,
|
||||
growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 127,
|
||||
levelMoves = { { level = 1, move = "PAY_DAY" } }, evolutions = {},
|
||||
},
|
||||
RATTATA = {
|
||||
id = "RATTATA", index = 19, name = "RATTATA",
|
||||
baseStats = { hp = 30, attack = 56, defense = 35, speed = 72,
|
||||
specialAttack = 25, specialDefense = 35 },
|
||||
types = { "NORMAL", "NORMAL" }, catchRate = 255, baseExp = 51,
|
||||
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 highRoll(n) return 99 % math.max(1, n or 1) end
|
||||
|
||||
local function newSave()
|
||||
return { player = { name = "GOLD", money = 1000, id = 4242 },
|
||||
mom = { savedMoney = 500 }, party = {} }
|
||||
end
|
||||
|
||||
local function newBattle(save)
|
||||
local player = Mon.new(DATA, "MEOWTH", LEVEL, { dvs = perfect })
|
||||
player.moves = {
|
||||
{ id = "PAY_DAY", pp = 20, maxPp = 20 },
|
||||
{ id = "TACKLE", pp = 35, maxPp = 35 },
|
||||
}
|
||||
local wild = Mon.new(DATA, "RATTATA", LEVEL, { dvs = perfect })
|
||||
wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||
wild.maxHp = 999
|
||||
wild.hp = 999
|
||||
local battle = Battle.new({ data = DATA, party = { player }, wild = wild,
|
||||
random = highRoll, save = save })
|
||||
return battle, player, wild
|
||||
end
|
||||
|
||||
local function eventWithText(events, text)
|
||||
for _, event in ipairs(events or {}) do
|
||||
if event.text == text then return event end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function findText(events, text)
|
||||
return eventWithText(events, text) ~= nil
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle(newSave())
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
T.eq(battle.payDay, 2 * LEVEL, "one connected hit is 2 x the user's level")
|
||||
T.check(findText(battle:takeEvents(), "Coins scattered\neverywhere!"),
|
||||
"CoinsScatteredText rides the hit")
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
T.eq(battle.payDay, 4 * LEVEL, "a second hit adds the same again")
|
||||
battle:useMove(player, wild, "TACKLE")
|
||||
T.eq(battle.payDay, 4 * LEVEL, "another move adds nothing")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle(newSave())
|
||||
battle.stages.enemy.evasion = Effects.MAX_STAGE
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
T.eq(battle.payDay, nil, "a miss accumulates nothing")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle(newSave())
|
||||
battle.amuletCoin = true
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
T.eq(battle.payDay, 4 * LEVEL,
|
||||
"the Amulet Coin does not double the running counter")
|
||||
end
|
||||
|
||||
do
|
||||
local battle, player, wild = newBattle(newSave())
|
||||
local enemy = battle.enemy
|
||||
enemy.level = 40
|
||||
battle:useMove(enemy, player, "PAY_DAY")
|
||||
T.eq(battle.payDay, 80, "an enemy's PAY DAY pays the player, at ITS level")
|
||||
T.check(wild == battle.enemy, "the enemy is the wild mon")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
T.eq(Prize.payDay(save, 30, false), 30, "the payout returns what it paid")
|
||||
T.eq(save.player.money, 1030, "and writes the wallet directly")
|
||||
T.eq(save.mom.savedMoney, 500, "with no Mom split")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
T.eq(Prize.payDay(save, 30, true), 60, "the Amulet Coin doubles the total")
|
||||
T.eq(save.player.money, 1060, "once, at payout")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
T.eq(Prize.payDay(save, 0, true), nil, "an empty counter pays nothing")
|
||||
T.eq(Prize.payDay(save, nil, false), nil, "and neither does an unset one")
|
||||
T.eq(save.player.money, 1000, "the wallet is untouched")
|
||||
T.eq(Prize.payDay(nil, 30, false), nil, "a save-less battle pays nothing")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
save.player.money = Prize.MAX_MONEY - 10
|
||||
Prize.payDay(save, 500, true)
|
||||
T.eq(save.player.money, Prize.MAX_MONEY, "AddBattleMoneyToAccount clamps")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
wild.hp = 0
|
||||
T.check(battle:resolveFaints(), "the enemy faint ends the battle")
|
||||
T.eq(battle.outcome, "win", "on the win arm")
|
||||
T.eq(save.player.money, 1000 + 2 * LEVEL, "which is where CheckPayDay runs")
|
||||
T.eq(battle.payDay, nil, "and the counter is cleared")
|
||||
local paid = eventWithText(battle:takeEvents(), "GOLD picked up ¥30!")
|
||||
T.check(paid, "BattleText_PlayerPickedUpPayDayMoney")
|
||||
T.eq(paid and paid.kind, "money", "on the same event kind the prize uses")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle.amuletCoin = true
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
wild.hp = 0
|
||||
battle:resolveFaints()
|
||||
T.eq(save.player.money, 1000 + 4 * LEVEL,
|
||||
"the Amulet Coin doubles once on the way out")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
player.hp = 0
|
||||
T.check(battle:resolveFaints(), "a whiteout ends the battle")
|
||||
T.eq(battle.outcome, "lose", "on the lose arm")
|
||||
T.eq(save.player.money, 1000, "which pays nothing")
|
||||
T.eq(battle.payDay, 2 * LEVEL, "the counter is dropped, not banked")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
battle:endBattle("run")
|
||||
T.eq(save.player.money, 1000, "running away pays nothing")
|
||||
end
|
||||
|
||||
local function newScreen(save, battle, opts)
|
||||
opts = opts or {}
|
||||
local pushed = {}
|
||||
local screen = setmetatable({
|
||||
save = save, battle = battle, picHidden = {},
|
||||
contest = opts.contest, tutorial = opts.tutorial,
|
||||
contestCaught = false,
|
||||
push = function(self, event) pushed[#pushed + 1] = event end,
|
||||
name = function(_, mon) return mon.species end,
|
||||
hasPokedex = function() return false end,
|
||||
currentBox = function() return 1 end,
|
||||
contestCatch = function(self) self.contestCaught = true end,
|
||||
}, { __index = BattleState })
|
||||
return screen, pushed
|
||||
end
|
||||
|
||||
local function caughtEnemy(battle)
|
||||
local enemy = battle.enemy
|
||||
enemy.hp = 1
|
||||
return enemy
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
local screen, pushed = newScreen(save, battle)
|
||||
screen:pushCaught(caughtEnemy(battle), "POKE_BALL")
|
||||
T.eq(save.player.money, 1000 + 2 * LEVEL, "a capture pays out")
|
||||
T.eq(battle.payDay, nil, "and clears the counter")
|
||||
T.check(findText(pushed, "GOLD picked up ¥30!"), "with the payout line")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
local screen = newScreen(save, battle, { contest = true })
|
||||
screen:pushCaught(caughtEnemy(battle), "PARK_BALL")
|
||||
T.check(screen.contestCaught, "the contest arm still holds the mon")
|
||||
T.eq(save.player.money, 1000 + 2 * LEVEL, "and a contest catch pays too")
|
||||
T.eq(battle.payDay, nil, "clearing the counter with it")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
local screen = newScreen(save, battle, { tutorial = true })
|
||||
screen:pushCaught(caughtEnemy(battle), "POKE_BALL")
|
||||
T.eq(save.player.money, 1000, "the catching tutorial pays nothing")
|
||||
T.eq(battle.payDay, 2 * LEVEL, "and leaves the counter alone")
|
||||
end
|
||||
|
||||
do
|
||||
local save = newSave()
|
||||
local battle, player, wild = newBattle(save)
|
||||
battle:useMove(player, wild, "PAY_DAY")
|
||||
wild.hp = 0
|
||||
battle:resolveFaints()
|
||||
local screen = newScreen(save, battle, { contest = true })
|
||||
screen:pushPayDay()
|
||||
T.eq(save.player.money, 1000 + 2 * LEVEL,
|
||||
"a contest KO that already paid does not pay a second time")
|
||||
end
|
||||
|
||||
T.finish("gen2 pay day bug 1212")
|
||||
@@ -0,0 +1,147 @@
|
||||
-- engine/battle_anims/anim_commands.asm:905-960 GetSubstitutePic
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local T = require("tests.harness")
|
||||
local AnimRunner = require("src.battle.gen2.AnimRunner")
|
||||
local Assets = require("src.render.Assets")
|
||||
local BattleState = require("src.ui.gen2.BattleState")
|
||||
|
||||
local MONSTER = { getDimensions = function() return 16, 96 end }
|
||||
Assets.image = function(path)
|
||||
if path == "assets/generated/sprites/monster.png" then return MONSTER end
|
||||
return nil
|
||||
end
|
||||
|
||||
local drawn
|
||||
love.graphics.draw = function(image, a, b, c)
|
||||
if type(a) == "table" then
|
||||
drawn[#drawn + 1] = { image = image, quad = a, x = b, y = c }
|
||||
else
|
||||
drawn[#drawn + 1] = { image = image, x = a, y = b }
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local runner = { env = { battleTurn = 0 }, picOverride = {} }
|
||||
AnimRunner.COMMANDS.raisesub(runner)
|
||||
T.eq(runner.picOverride.player, "substitute",
|
||||
"anim_raisesub puts the doll up on the acting side")
|
||||
AnimRunner.COMMANDS.dropsub(runner)
|
||||
T.eq(runner.picOverride.player, false,
|
||||
"anim_dropsub writes false, not nil")
|
||||
T.check(runner.picOverride.player ~= nil,
|
||||
"so LowerSub reads as an override and not as `no animation running`")
|
||||
end
|
||||
|
||||
do
|
||||
local runner = { env = { battleTurn = 1 }, picOverride = {} }
|
||||
AnimRunner.COMMANDS.raisesub(runner)
|
||||
T.eq(runner.picOverride.enemy, "substitute", "and the enemy side too")
|
||||
T.eq(runner.picOverride.player, nil, "without touching the other one")
|
||||
end
|
||||
|
||||
local function newAnim(overrides)
|
||||
return {
|
||||
bg = { hidden = {}, picSize = {}, slide = {}, monShade = {} },
|
||||
picOverride = overrides or {},
|
||||
}
|
||||
end
|
||||
|
||||
local function newScreen(anim)
|
||||
local image = { getDimensions = function() return 56, 56 end }
|
||||
local back = { getDimensions = function() return 48, 48 end }
|
||||
return setmetatable({
|
||||
picHidden = {}, anim = anim,
|
||||
pic = function(_, _, isBack) return isBack and back or image end,
|
||||
picScale = function() return 1 end,
|
||||
faintSink = function() return 0 end,
|
||||
}, { __index = BattleState }), image, back
|
||||
end
|
||||
|
||||
do
|
||||
local screen = newScreen(newAnim({ enemy = "substitute" }))
|
||||
T.eq(screen:animPicState("enemy").pic, "substitute",
|
||||
"animPicState surfaces the runner's override")
|
||||
end
|
||||
|
||||
do
|
||||
local screen, image = newScreen(newAnim({ enemy = "substitute" }))
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "GASTLY", volatile = {} }, false)
|
||||
T.eq(#drawn, 1, "one blit")
|
||||
T.eq(drawn[1].image, MONSTER, "the doll, not the frontpic")
|
||||
T.check(drawn[1].image ~= image, "the mon's own pic is not drawn")
|
||||
T.eq(drawn[1].x, 112, "enemy doll x, GetSubstitutePic's cols 2-3")
|
||||
T.eq(drawn[1].y, 40, "enemy doll y, rows 5-6 of the 7-tall box")
|
||||
local quad = drawn[1].quad or {}
|
||||
T.eq(quad.x, 0, "cut from the facing-DOWN frame")
|
||||
T.eq(quad.y, 0, "at the top of monster.png")
|
||||
T.eq(quad.w, 16, "16 wide")
|
||||
T.eq(quad.h, 16, "16 tall")
|
||||
end
|
||||
|
||||
do
|
||||
local screen = newScreen(newAnim({ player = "substitute" }))
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "TYPHLOSION", volatile = {} }, true)
|
||||
T.eq(#drawn, 1, "one blit")
|
||||
T.eq(drawn[1].x, 32, "player doll x, cols 2-3 of the 6-wide box")
|
||||
T.eq(drawn[1].y, 80, "player doll y, rows 4-5 of the 6-tall box")
|
||||
T.eq((drawn[1].quad or {}).y, 16, "cut from the facing-UP frame")
|
||||
end
|
||||
|
||||
do
|
||||
local anim = newAnim({ enemy = "substitute" })
|
||||
anim.bg.slide.enemy = 8
|
||||
local screen = newScreen(anim)
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "GASTLY", volatile = {} }, false)
|
||||
T.eq(drawn[1].x, 120, "REMOVE_MON / ENTER_MON still slide the doll")
|
||||
end
|
||||
|
||||
do
|
||||
local screen, image = newScreen(nil)
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "GASTLY", volatile = { substitute = 22 } }, false)
|
||||
T.eq(drawn[1].image, MONSTER, "with no animation running the volatile wins")
|
||||
T.check(drawn[1].image ~= image, "and the frontpic stays down")
|
||||
end
|
||||
|
||||
do
|
||||
local screen, image = newScreen(nil)
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "GASTLY", volatile = {} }, false)
|
||||
T.eq(drawn[1].image, image, "no substitute, no doll")
|
||||
T.eq(drawn[1].x, 96, "the frontpic sits in its own box")
|
||||
T.eq(drawn[1].y, 0, "at hlcoord 12, 0")
|
||||
end
|
||||
|
||||
do
|
||||
local screen, image = newScreen(newAnim({ enemy = false }))
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "GASTLY", volatile = { substitute = 22 } }, false)
|
||||
T.eq(drawn[1].image, image,
|
||||
"a dropsub override beats the still-standing substitute volatile")
|
||||
T.eq(drawn[1].x, 96, "and the mon is drawn in its own box")
|
||||
end
|
||||
|
||||
do
|
||||
local screen = newScreen(newAnim({ enemy = nil }))
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "GASTLY", volatile = { substitute = 22 } }, false)
|
||||
T.eq(drawn[1].image, MONSTER,
|
||||
"an animation that never touched the pic leaves the doll up")
|
||||
end
|
||||
|
||||
do
|
||||
local screen, image = newScreen(newAnim({ enemy = "transform" }))
|
||||
drawn = {}
|
||||
screen:drawPic({ species = "GASTLY", volatile = { substitute = 22 } }, false)
|
||||
T.eq(drawn[1].image, image,
|
||||
"and any other override is not the doll either")
|
||||
end
|
||||
|
||||
T.finish("gen2 substitute doll bug 1271")
|
||||
@@ -0,0 +1,215 @@
|
||||
-- data/types/type_matchups.asm:112-116, engine/battle/effect_commands.asm:1305-1313
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local T = require("tests.harness")
|
||||
local Game2 = require("src.core.Game2")
|
||||
local Damage = require("src.battle.gen2.Damage")
|
||||
|
||||
local TYPES = {
|
||||
"NORMAL", "FIGHTING", "FLYING", "POISON", "GROUND", "ROCK", "BIRD", "BUG",
|
||||
"GHOST", "STEEL", "CURSE_TYPE", "FIRE", "WATER", "GRASS", "ELECTRIC",
|
||||
"PSYCHIC_TYPE", "ICE", "DRAGON", "DARK",
|
||||
}
|
||||
|
||||
local DEFAULT_ROWS = [[
|
||||
NORMAL ROCK 5
|
||||
NORMAL STEEL 5
|
||||
FIRE FIRE 5
|
||||
FIRE WATER 5
|
||||
FIRE GRASS 20
|
||||
FIRE ICE 20
|
||||
FIRE BUG 20
|
||||
FIRE ROCK 5
|
||||
FIRE DRAGON 5
|
||||
FIRE STEEL 20
|
||||
WATER FIRE 20
|
||||
WATER WATER 5
|
||||
WATER GRASS 5
|
||||
WATER GROUND 20
|
||||
WATER ROCK 20
|
||||
WATER DRAGON 5
|
||||
ELECTRIC WATER 20
|
||||
ELECTRIC ELECTRIC 5
|
||||
ELECTRIC GRASS 5
|
||||
ELECTRIC GROUND 0
|
||||
ELECTRIC FLYING 20
|
||||
ELECTRIC DRAGON 5
|
||||
GRASS FIRE 5
|
||||
GRASS WATER 20
|
||||
GRASS GRASS 5
|
||||
GRASS POISON 5
|
||||
GRASS GROUND 20
|
||||
GRASS FLYING 5
|
||||
GRASS BUG 5
|
||||
GRASS ROCK 20
|
||||
GRASS DRAGON 5
|
||||
GRASS STEEL 5
|
||||
ICE WATER 5
|
||||
ICE GRASS 20
|
||||
ICE ICE 5
|
||||
ICE GROUND 20
|
||||
ICE FLYING 20
|
||||
ICE DRAGON 20
|
||||
ICE STEEL 5
|
||||
ICE FIRE 5
|
||||
FIGHTING NORMAL 20
|
||||
FIGHTING ICE 20
|
||||
FIGHTING POISON 5
|
||||
FIGHTING FLYING 5
|
||||
FIGHTING PSYCHIC_TYPE 5
|
||||
FIGHTING BUG 5
|
||||
FIGHTING ROCK 20
|
||||
FIGHTING DARK 20
|
||||
FIGHTING STEEL 20
|
||||
POISON GRASS 20
|
||||
POISON POISON 5
|
||||
POISON GROUND 5
|
||||
POISON ROCK 5
|
||||
POISON GHOST 5
|
||||
POISON STEEL 0
|
||||
GROUND FIRE 20
|
||||
GROUND ELECTRIC 20
|
||||
GROUND GRASS 5
|
||||
GROUND POISON 20
|
||||
GROUND FLYING 0
|
||||
GROUND BUG 5
|
||||
GROUND ROCK 20
|
||||
GROUND STEEL 20
|
||||
FLYING ELECTRIC 5
|
||||
FLYING GRASS 20
|
||||
FLYING FIGHTING 20
|
||||
FLYING BUG 20
|
||||
FLYING ROCK 5
|
||||
FLYING STEEL 5
|
||||
PSYCHIC_TYPE FIGHTING 20
|
||||
PSYCHIC_TYPE POISON 20
|
||||
PSYCHIC_TYPE PSYCHIC_TYPE 5
|
||||
PSYCHIC_TYPE DARK 0
|
||||
PSYCHIC_TYPE STEEL 5
|
||||
BUG FIRE 5
|
||||
BUG GRASS 20
|
||||
BUG FIGHTING 5
|
||||
BUG POISON 5
|
||||
BUG FLYING 5
|
||||
BUG PSYCHIC_TYPE 20
|
||||
BUG GHOST 5
|
||||
BUG DARK 20
|
||||
BUG STEEL 5
|
||||
ROCK FIRE 20
|
||||
ROCK ICE 20
|
||||
ROCK FIGHTING 5
|
||||
ROCK GROUND 5
|
||||
ROCK FLYING 20
|
||||
ROCK BUG 20
|
||||
ROCK STEEL 5
|
||||
GHOST NORMAL 0
|
||||
GHOST PSYCHIC_TYPE 20
|
||||
GHOST DARK 5
|
||||
GHOST STEEL 5
|
||||
GHOST GHOST 20
|
||||
DRAGON DRAGON 20
|
||||
DRAGON STEEL 5
|
||||
DARK FIGHTING 5
|
||||
DARK PSYCHIC_TYPE 20
|
||||
DARK GHOST 20
|
||||
DARK DARK 5
|
||||
DARK STEEL 5
|
||||
STEEL FIRE 5
|
||||
STEEL WATER 5
|
||||
STEEL ELECTRIC 5
|
||||
STEEL ICE 20
|
||||
STEEL ROCK 20
|
||||
STEEL STEEL 5
|
||||
]]
|
||||
|
||||
local FORESIGHT_ROWS = [[
|
||||
NORMAL GHOST 0
|
||||
FIGHTING GHOST 0
|
||||
]]
|
||||
|
||||
local function parse(text)
|
||||
local rows = {}
|
||||
for attacker, defender, multiplier in
|
||||
text:gmatch("(%u[%u_]*)%s+(%u[%u_]*)%s+(%d+)") do
|
||||
rows[#rows + 1] = { attacker = attacker, defender = defender,
|
||||
multiplier = tonumber(multiplier) }
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function serialize(rows)
|
||||
local parts = {}
|
||||
for _, row in ipairs(rows) do
|
||||
parts[#parts + 1] = ("{attacker=%q,defender=%q,multiplier=%d}")
|
||||
:format(row.attacker, row.defender, row.multiplier)
|
||||
end
|
||||
return "{" .. table.concat(parts, ",") .. "}"
|
||||
end
|
||||
|
||||
local DEFAULT = parse(DEFAULT_ROWS)
|
||||
local FORESIGHT = parse(FORESIGHT_ROWS)
|
||||
|
||||
T.eq(#DEFAULT, 108, "the fixture carries the cart's 108 default rows")
|
||||
T.eq(#FORESIGHT, 2, "and the two rows the `db -2` marker precedes")
|
||||
|
||||
love.filesystem.write("data/generated/type_chart.lua",
|
||||
("return { generation = 2, matchups = %s, foresightMatchups = %s }")
|
||||
:format(serialize(DEFAULT), serialize(FORESIGHT)))
|
||||
|
||||
local function bootChart()
|
||||
local game = setmetatable({ data = {} }, Game2)
|
||||
game.applyOptions = function() end
|
||||
pcall(Game2.load, game)
|
||||
return game.data.type_chart
|
||||
end
|
||||
|
||||
local function resolve(matchups)
|
||||
local out = {}
|
||||
for _, attacker in ipairs(TYPES) do
|
||||
for _, defender in ipairs(TYPES) do
|
||||
out[attacker .. ">" .. defender] =
|
||||
Damage.typeMultiplier(attacker, { defender }, matchups)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local chart = bootChart()
|
||||
T.check(chart and chart.matchups, "Gold's chart reaches self.data.type_chart")
|
||||
|
||||
local before = resolve(DEFAULT)
|
||||
local after = resolve(chart.matchups)
|
||||
|
||||
T.eq(before["NORMAL>GHOST"], 10,
|
||||
"unmerged, a NORMAL move is neutral against a GHOST")
|
||||
T.eq(before["FIGHTING>GHOST"], 10,
|
||||
"unmerged, a FIGHTING move is neutral against a GHOST")
|
||||
|
||||
T.eq(after["NORMAL>GHOST"], 0, "merged, NORMAL does nothing to GHOST")
|
||||
T.eq(after["FIGHTING>GHOST"], 0, "merged, FIGHTING does nothing to GHOST")
|
||||
T.eq(after["GHOST>NORMAL"], 0, "and GHOST still does nothing to NORMAL")
|
||||
|
||||
T.eq(#chart.matchups, #DEFAULT + #FORESIGHT,
|
||||
"the merge appends exactly the foresight rows")
|
||||
T.eq(#(chart.foresightMatchups or {}), #FORESIGHT,
|
||||
"and leaves foresightMatchups in place for Fingerprint")
|
||||
|
||||
local moved = {}
|
||||
for pair, value in pairs(after) do
|
||||
if before[pair] ~= value then moved[#moved + 1] = pair end
|
||||
end
|
||||
table.sort(moved)
|
||||
T.eq(#moved, 2, "exactly two of the 19x19 pairs move")
|
||||
T.eq(moved[1], "FIGHTING>GHOST", "the first is FIGHTING against GHOST")
|
||||
T.eq(moved[2], "NORMAL>GHOST", "the second is NORMAL against GHOST")
|
||||
|
||||
local twice = bootChart()
|
||||
T.eq(#twice.matchups, #DEFAULT + #FORESIGHT,
|
||||
"a second load appends the rows once, not twice")
|
||||
T.eq(Damage.typeMultiplier("NORMAL", { "GHOST", "POISON" }, twice.matchups), 0,
|
||||
"a dual GHOST/POISON target is still immune to NORMAL")
|
||||
|
||||
T.finish("gen2 type chart foresight merge bug 1268")
|
||||
@@ -0,0 +1,81 @@
|
||||
-- The Pewter museum ticket clerk's money box (#1335): the closure re-reads
|
||||
-- the balance, so the thank-you box shows 50 less than the ask did, and
|
||||
-- the decline boxes keep the box up too (scripts/Museum1F.asm:71-112).
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
}
|
||||
|
||||
local M = assert(loadfile("data/scripts/story2.lua"))()
|
||||
local clerk = M.MUSEUM_1F.talk.TEXT_MUSEUM1F_SCIENTIST1
|
||||
|
||||
local pushed
|
||||
local function mkGame(cash)
|
||||
pushed = {}
|
||||
return {
|
||||
data = { text = {} },
|
||||
save = { money = cash, flags = {} },
|
||||
stack = { push = function(_, box) pushed[#pushed + 1] = box end },
|
||||
}
|
||||
end
|
||||
|
||||
-- YES with enough money: the prompt carries the money closure and reads
|
||||
-- the pre-payment balance; the thank-you box re-reads the reduced one
|
||||
local g = mkGame(3000)
|
||||
local doneFired = false
|
||||
clerk(g, nil, nil, function() doneFired = true end)
|
||||
local ask = pushed[1]
|
||||
T.check(ask.opts ~= nil and ask.opts.money ~= nil and ask.opts.choice ~= nil,
|
||||
"the ask box carries both a money closure and the YES/NO choice")
|
||||
T.eq(ask.opts.money(), 3000, "the ask box reads 3000 before paying")
|
||||
ask.opts.choice(true)
|
||||
local thanks = pushed[2]
|
||||
T.check(tostring(thanks.text):find("Thank you", 1, true) ~= nil,
|
||||
"YES opens the thank-you box")
|
||||
T.check(thanks.opts ~= nil and thanks.opts.money ~= nil,
|
||||
"the thank-you box carries a money closure")
|
||||
T.eq(thanks.opts.money(), 2950,
|
||||
"the thank-you box re-reads the balance, 50 less than the ask")
|
||||
T.check(g.save.flags.EVENT_BOUGHT_MUSEUM_TICKET, "the ticket flag is set")
|
||||
thanks.onDone()
|
||||
T.check(doneFired, "the done callback still chains through")
|
||||
|
||||
-- YES but short on cash: the decline box also carries the money opt now
|
||||
-- (Part 1: scripts/Museum1F.asm keeps MONEY_BOX up on the whole exchange)
|
||||
g = mkGame(20)
|
||||
clerk(g, nil, nil, function() end)
|
||||
pushed[1].opts.choice(true)
|
||||
local broke = pushed[2]
|
||||
T.check(tostring(broke.text):find("enough money", 1, true) ~= nil,
|
||||
"short on cash opens the not-enough-money box")
|
||||
T.check(broke.opts ~= nil and broke.opts.money ~= nil,
|
||||
"the not-enough-money box carries the money opt")
|
||||
T.eq(broke.opts.money(), 20, "and its closure reads the untouched balance")
|
||||
T.eq(g.save.money, 20, "money is not spent on the broke path")
|
||||
|
||||
-- NO: the come-again box keeps the money opt too
|
||||
g = mkGame(3000)
|
||||
clerk(g, nil, nil, function() end)
|
||||
pushed[1].opts.choice(false)
|
||||
local decline = pushed[2]
|
||||
T.check(tostring(decline.text):find("Come again", 1, true) ~= nil,
|
||||
"declining opens the come-again box")
|
||||
T.check(decline.opts ~= nil and decline.opts.money ~= nil,
|
||||
"the come-again box carries the money opt")
|
||||
T.eq(decline.opts.money(), 3000, "and its closure reads the unspent balance")
|
||||
|
||||
-- already ticketed: take-your-time only, no money box (a ticket holder
|
||||
-- is already inside, past the rope)
|
||||
g = mkGame(3000)
|
||||
g.save.flags.EVENT_BOUGHT_MUSEUM_TICKET = true
|
||||
clerk(g, nil, nil, function() end)
|
||||
T.check(tostring(pushed[1].text):find("Take your time", 1, true) ~= nil,
|
||||
"an existing ticket holder gets the take-your-time line")
|
||||
T.check(pushed[1].opts == nil, "and no money box on that branch")
|
||||
|
||||
T.finish("museum_money_box_bug1335")
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Oak's lab poster/e-mail hidden events (#1331, #1333): the extractor's
|
||||
-- hidden_event whitelist drops both, so the posters and PC e-mail were
|
||||
-- dead A presses until the flavor script's onInteract hook.
|
||||
-- engine/events/hidden_events/oaks_lab_posters.asm:1
|
||||
-- engine/events/hidden_events/oaks_lab_email.asm:1
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
}
|
||||
|
||||
local M = assert(loadfile("data/scripts/flavor/oaks_lab.lua"))()
|
||||
local hook = M.OAKS_LAB.onInteract
|
||||
T.check(type(hook) == "function", "OAKS_LAB.onInteract exists")
|
||||
|
||||
local pushed
|
||||
local function mkGame(owned)
|
||||
pushed = {}
|
||||
return {
|
||||
data = { text = {
|
||||
_PushStartText = "PUSH",
|
||||
_SaveOptionText = "SAVE",
|
||||
_StrengthsAndWeaknessesText = "TYPES",
|
||||
_OakLabEmailText = "EMAIL",
|
||||
} },
|
||||
save = { pokedex = { owned = owned } },
|
||||
stack = { push = function(_, box) pushed[#pushed + 1] = box end },
|
||||
}
|
||||
end
|
||||
local owUp = { player = { facing = "up" } }
|
||||
local owDown = { player = { facing = "down" } }
|
||||
|
||||
-- left poster: unconditional, any facing (#1331)
|
||||
local g = mkGame({})
|
||||
T.eq(hook(g, owDown, 4, 0), true, "left poster consumes the press")
|
||||
T.eq(#pushed, 1, "left poster pushes one box")
|
||||
T.eq(pushed[1].text, "PUSH", "left poster prints PushStartText")
|
||||
|
||||
-- right poster, fewer than 2 species owned: SaveOptionText (#1331)
|
||||
g = mkGame({ [1] = true })
|
||||
T.eq(hook(g, owDown, 5, 0), true, "right poster consumes the press (<2 owned)")
|
||||
T.eq(pushed[1].text, "SAVE", "right poster prints SaveOptionText below 2 owned")
|
||||
|
||||
-- right poster, 2+ species owned: StrengthsAndWeaknessesText (#1331)
|
||||
g = mkGame({ [1] = true, [4] = true })
|
||||
T.eq(hook(g, owDown, 5, 0), true, "right poster consumes the press (2+ owned)")
|
||||
T.eq(pushed[1].text, "TYPES",
|
||||
"right poster prints StrengthsAndWeaknessesText at 2+ owned")
|
||||
|
||||
-- right poster, exactly at the boundary (still <2, one species owned twice
|
||||
-- under different keys does not double-count)
|
||||
g = mkGame({ [1] = true })
|
||||
T.eq(hook(g, owDown, 5, 0), true, "right poster boundary case consumes")
|
||||
T.eq(pushed[1].text, "SAVE", "one owned species stays on the SAVE line")
|
||||
|
||||
-- e-mail tiles, facing up: OakLabEmailText (#1333)
|
||||
g = mkGame({})
|
||||
T.eq(hook(g, owUp, 0, 1), true, "e-mail tile x=0 consumes facing up")
|
||||
T.eq(pushed[1].text, "EMAIL", "e-mail tile x=0 prints OakLabEmailText")
|
||||
g = mkGame({})
|
||||
T.eq(hook(g, owUp, 1, 1), true, "e-mail tile x=1 consumes facing up")
|
||||
T.eq(pushed[1].text, "EMAIL", "e-mail tile x=1 prints OakLabEmailText")
|
||||
|
||||
-- e-mail tiles, any other facing: explicit FALSE so the press falls
|
||||
-- through to tryBookshelf, and nothing is pushed (#1333)
|
||||
g = mkGame({})
|
||||
T.eq(hook(g, owDown, 0, 1), false, "e-mail tile x=0 returns false facing down")
|
||||
T.eq(#pushed, 0, "wrong facing prints nothing")
|
||||
g = mkGame({})
|
||||
T.eq(hook(g, { player = { facing = "left" } }, 1, 1), false,
|
||||
"e-mail tile x=1 returns false facing left")
|
||||
T.eq(#pushed, 0, "wrong facing prints nothing")
|
||||
|
||||
-- unrelated cell: falls through, nothing printed
|
||||
g = mkGame({})
|
||||
T.eq(hook(g, owUp, 3, 3), false, "an unrelated cell returns false")
|
||||
T.eq(#pushed, 0, "an unrelated cell prints nothing")
|
||||
|
||||
T.finish("oaks_lab_posters_email_bug1331_1333")
|
||||
@@ -0,0 +1,68 @@
|
||||
-- "This POKéMON is really energetic!" prints before the received-mon box
|
||||
-- (#1334). starterBall's numeric jump targets moved down one row for the
|
||||
-- new line, so this asserts the targets by ROW CONTENT, not by index, and
|
||||
-- would catch a stale target the next time a row is inserted above them.
|
||||
-- scripts/OaksLab.asm:919
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
|
||||
local M = assert(loadfile("data/scripts/oaks_lab.lua"))()
|
||||
local rows = M.talk.TEXT_OAKSLAB_CHARMANDER_POKE_BALL
|
||||
T.check(type(rows) == "table", "starterBall rows loaded")
|
||||
|
||||
-- the energetic line lands between the ask and the sound/received pair
|
||||
local askRow
|
||||
for i, row in ipairs(rows) do
|
||||
if row[1] == "ask" then askRow = i end
|
||||
end
|
||||
T.check(askRow ~= nil, "found the ask row")
|
||||
T.eq(rows[askRow + 1][1], "jump_if_false", "the ask is followed by its jump")
|
||||
T.same(rows[askRow + 2], { "show_text", "_OaksLabMonEnergeticText" },
|
||||
"the energetic line is the row right after the NO jump")
|
||||
T.eq(rows[askRow + 3][1], "text_sound",
|
||||
"the sound cue stays on the RECEIVED line, not the energetic one")
|
||||
T.eq(rows[askRow + 4][1], "show_text",
|
||||
"and the received-mon text follows the sound cue")
|
||||
T.eq(rows[askRow + 4][2], "_OaksLabReceivedMonText",
|
||||
"specifically the received-mon text")
|
||||
|
||||
-- jump_if_true (row 2): the EVENT_GOT_STARTER short-circuit must land on
|
||||
-- the leftover-ball beat, "face_object 5 down" -- asserted by content
|
||||
local gotStarterJump
|
||||
for i, row in ipairs(rows) do
|
||||
if row[1] == "check_flag" and row[2] == "EVENT_GOT_STARTER" then
|
||||
gotStarterJump = rows[i + 1]
|
||||
break
|
||||
end
|
||||
end
|
||||
T.check(gotStarterJump ~= nil and gotStarterJump[1] == "jump_if_true",
|
||||
"found the EVENT_GOT_STARTER jump_if_true")
|
||||
local target1 = gotStarterJump[2]
|
||||
T.check(type(target1) == "number", "the target is a numeric row index")
|
||||
T.same(rows[target1], { "face_object", 5, "down" },
|
||||
"jump_if_true lands on the leftover-ball face_object row")
|
||||
|
||||
-- jump_if_false (row 4): the EVENT_FOLLOWED_OAK_INTO_LAB gate must land on
|
||||
-- the pre-pick line, "show_text _OaksLabThoseArePokeBallsText"
|
||||
local followedOakJump
|
||||
for i, row in ipairs(rows) do
|
||||
if row[1] == "check_flag" and row[2] == "EVENT_FOLLOWED_OAK_INTO_LAB" then
|
||||
followedOakJump = rows[i + 1]
|
||||
break
|
||||
end
|
||||
end
|
||||
T.check(followedOakJump ~= nil and followedOakJump[1] == "jump_if_false",
|
||||
"found the EVENT_FOLLOWED_OAK_INTO_LAB jump_if_false")
|
||||
local target2 = followedOakJump[2]
|
||||
T.check(type(target2) == "number", "the target is a numeric row index")
|
||||
T.same(rows[target2], { "show_text", "_OaksLabThoseArePokeBallsText" },
|
||||
"jump_if_false lands on the pre-pick ThoseArePokeBalls row")
|
||||
|
||||
-- the leftover-ball beat this jump lands on must never fall through into
|
||||
-- the pre-pick line beneath it (#601 remnant); confirm a terminating jump
|
||||
-- sits between them
|
||||
T.eq(rows[target1 + 2][1], "jump",
|
||||
"the leftover-ball beat ends on its own jump before the pre-pick row")
|
||||
|
||||
T.finish("oaks_lab_starter_energetic_bug1334")
|
||||
@@ -76,6 +76,8 @@ for _, name in ipairs({ "openOaksPC", "dexRating" }) do
|
||||
("Game upvalue on %s"):format(name))
|
||||
end
|
||||
T.check(setUpvalue(OW.openPC, "Game", fakeGame), "Game upvalue on openPC")
|
||||
T.check(setUpvalue(OW.openPC, "TextBox", textBoxStub),
|
||||
"TextBox upvalue on openPC")
|
||||
|
||||
local fakeSelf = setmetatable({}, { __index = OW })
|
||||
|
||||
@@ -111,8 +113,14 @@ end
|
||||
reset()
|
||||
local done = false
|
||||
fakeSelf:openPC(function() done = true end)
|
||||
-- engine/menus/pc.asm:5
|
||||
local pcOn = lastPush()
|
||||
T.eq(pcOn.kind, "text", "openPC opens with the turned-on text")
|
||||
T.check(tostring(pcOn.text):find("turned on", 1, true) ~= nil,
|
||||
"the opening box is TurnedOnPC1Text")
|
||||
pcOn.onDone()
|
||||
local menu = lastPush()
|
||||
T.eq(menu.kind, "menu", "openPC pushes the PC menu")
|
||||
T.eq(menu.kind, "menu", "the PC menu follows the box")
|
||||
local oak
|
||||
for _, item in ipairs(menu.items) do
|
||||
if item.label == "PROF.OAK's PC" then oak = item end
|
||||
@@ -121,16 +129,16 @@ T.check(oak ~= nil, "PROF.OAK's PC is offered once the Pokédex is had")
|
||||
oak.onSelect()
|
||||
-- drop the menu's Turn_On_PC and the row's Enter_PC
|
||||
plays = {}
|
||||
T.eq(pushed[2].kind, "text", "selection opens the access text")
|
||||
T.check(tostring(pushed[2].text):find("Accessed", 1, true) ~= nil,
|
||||
T.eq(pushed[3].kind, "text", "selection opens the access text")
|
||||
T.check(tostring(pushed[3].text):find("Accessed", 1, true) ~= nil,
|
||||
"first session box is the access text")
|
||||
runChain()
|
||||
T.check(done, "session completes")
|
||||
T.check(pushed[3].opts ~= nil and pushed[3].opts.choice ~= nil,
|
||||
T.check(pushed[4].opts ~= nil and pushed[4].opts.choice ~= nil,
|
||||
"the rated question carries the YES/NO choice")
|
||||
T.check(tostring(pushed[3].text):find("rated", 1, true) ~= nil,
|
||||
T.check(tostring(pushed[4].text):find("rated", 1, true) ~= nil,
|
||||
"second session box asks for the rating")
|
||||
local ratingBox = pushed[4]
|
||||
local ratingBox = pushed[5]
|
||||
T.check(ratingBox.opts and ratingBox.opts.auto ~= nil
|
||||
and ratingBox.opts.auto.wait ~= nil,
|
||||
"rating box sounds the jingle then waits for a button")
|
||||
@@ -142,7 +150,7 @@ T.eq(#plays, 0, "no jingle while the evaluation is printing")
|
||||
ratingBox.opts.auto.sound()
|
||||
T.eq(#plays, 1, "jingle fires once the rating text is printed")
|
||||
T.eq(plays[1], "Pokedex_Rating", "jingle is the Pokedex_Rating fanfare")
|
||||
T.check(tostring(pushed[5].text):find("Closed link", 1, true) ~= nil,
|
||||
T.check(tostring(pushed[6].text):find("Closed link", 1, true) ~= nil,
|
||||
"the closing link prints at the end of the session")
|
||||
|
||||
-- === declining the rating skips the evaluation but still closes the link
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
-- HandlePoisonBurnLeechSeed runs once per side per round; a mod that
|
||||
-- reads battle.ruleset.residualAfterMove twice (executeAction:3487 and
|
||||
-- endOfTurn:2657) could otherwise fire both arms. #1181's b.residualDone
|
||||
-- latch (residualFor ~2612, endOfTurn ~2670, cleared ~2685) closes that.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
local Font = require("src.render.Font")
|
||||
Font.load(Data)
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local TypeChart = require("src.battle.TypeChart")
|
||||
TypeChart.load(Data)
|
||||
|
||||
local function newBattle(opts)
|
||||
opts = opts or {}
|
||||
local save = SaveData.newGame()
|
||||
save.party = { Pokemon.new(Data, "FIXMON_A", 30), Pokemon.new(Data, "FIXMON_A", 28) }
|
||||
local game = { data = Data, save = save,
|
||||
stack = { top = function() return nil end, push = function() end } }
|
||||
local battle = BattleState.newWild(game, "FIXMON_C", 30)
|
||||
battle.rng = function() return 0 end
|
||||
battle.enemyAction = function() return { id = "FIX_SCRATCH", pp = 35 } end
|
||||
if opts.playerFaster then
|
||||
battle.player.curStats.speed = 200
|
||||
battle.enemy.curStats.speed = 1
|
||||
else
|
||||
battle.enemy.curStats.speed = 200
|
||||
battle.player.curStats.speed = 1
|
||||
end
|
||||
return battle
|
||||
end
|
||||
|
||||
local function drain(battle)
|
||||
local rows = {}
|
||||
for _ = 1, 800 do
|
||||
local item = table.remove(battle.queue, 1)
|
||||
if not item then return rows end
|
||||
if item.text then rows[#rows + 1] = { text = item.text } end
|
||||
if item.fn then
|
||||
battle.nextInsert = 0
|
||||
item.fn()
|
||||
end
|
||||
end
|
||||
error("the turn queue never drained")
|
||||
end
|
||||
|
||||
local function countTicks(rows)
|
||||
local player, enemy = 0, 0
|
||||
for _, r in ipairs(rows) do
|
||||
if r.text and r.text:find("hurt by poison", 1, true) then
|
||||
if r.text:find("Enemy", 1, true) then enemy = enemy + 1
|
||||
else player = player + 1 end
|
||||
end
|
||||
end
|
||||
return player, enemy
|
||||
end
|
||||
|
||||
local function assertCleared(b, label)
|
||||
T.check(b.player.residualDone == nil, label .. ": player residualDone cleared")
|
||||
T.check(b.enemy.residualDone == nil, label .. ": enemy residualDone cleared")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = true })
|
||||
b.player.mon.status = "PSN"
|
||||
b:resolveTurn({ id = "FIX_TACKLE", pp = 35 })
|
||||
local pt, et = countTicks(drain(b))
|
||||
T.eq(pt, 1, "faster attacker: poison ticks exactly once")
|
||||
T.eq(et, 0, "faster attacker: no enemy tick")
|
||||
assertCleared(b, "faster attacker")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = false })
|
||||
b.player.mon.status = "PSN"
|
||||
b:resolveTurn({ id = "FIX_TACKLE", pp = 35 })
|
||||
local pt, et = countTicks(drain(b))
|
||||
T.eq(pt, 1, "slower attacker: poison ticks exactly once")
|
||||
T.eq(et, 0, "slower attacker: no enemy tick")
|
||||
assertCleared(b, "slower attacker")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = false })
|
||||
b.player.mon.status = "PSN"
|
||||
b:itemUsed({})
|
||||
local pt, et = countTicks(drain(b))
|
||||
T.eq(pt, 1, "item round: poison ticks exactly once")
|
||||
T.eq(et, 0, "item round: no enemy tick")
|
||||
assertCleared(b, "item round")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = false })
|
||||
b.player.mon.status = "PSN"
|
||||
b.catchAttempt = function() return false, 0 end
|
||||
local ballId
|
||||
for id in pairs(Data.items or {}) do
|
||||
if id:find("BALL") then ballId = id break end
|
||||
end
|
||||
if not ballId then
|
||||
Data.items = Data.items or {}
|
||||
Data.items.POKE_BALL = { name = "POKE BALL" }
|
||||
ballId = "POKE_BALL"
|
||||
end
|
||||
b:throwBall(ballId)
|
||||
local pt, et = countTicks(drain(b))
|
||||
T.eq(pt, 1, "ball round: poison ticks exactly once")
|
||||
T.eq(et, 0, "ball round: no enemy tick")
|
||||
assertCleared(b, "ball round")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = false })
|
||||
b.player.mon.status = "PSN"
|
||||
b.runRoll = function() return false end
|
||||
b:tryRun()
|
||||
local pt, et = countTicks(drain(b))
|
||||
T.eq(pt, 1, "failed run: poison ticks exactly once")
|
||||
T.eq(et, 0, "failed run: no enemy tick")
|
||||
assertCleared(b, "failed run")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = false })
|
||||
b.player.mon.status = "PSN"
|
||||
b:resolveSwitch(b.game.save.party[2])
|
||||
local pt, et = countTicks(drain(b))
|
||||
T.eq(pt, 0, "switch round: a fresh battler ticks zero")
|
||||
T.eq(et, 0, "switch round: still no enemy tick")
|
||||
assertCleared(b, "switch round")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = false })
|
||||
b.player.mon.status = "PSN"
|
||||
b.enemy.mon.status = "PSN"
|
||||
local php0, ehp0 = b.player.mon.hp, b.enemy.mon.hp
|
||||
b:resolveTurn({ id = "FIX_TACKLE", pp = 35 })
|
||||
local rows = drain(b)
|
||||
local pt, et = countTicks(rows)
|
||||
T.eq(pt, 1, "both sides poisoned: player side ticks once")
|
||||
T.eq(et, 1, "both sides poisoned: enemy side ticks once too")
|
||||
T.check(b.player.mon.hp < php0, "both sides poisoned: player actually lost HP")
|
||||
T.check(b.enemy.mon.hp < ehp0, "both sides poisoned: enemy actually lost HP")
|
||||
assertCleared(b, "both sides poisoned")
|
||||
end
|
||||
|
||||
do
|
||||
local b = newBattle({ playerFaster = false })
|
||||
b.player.mon.status = "PSN"
|
||||
b.player.mon.hp = 999
|
||||
b.player.mon.stats.hp = 999
|
||||
local lastLoss
|
||||
for round = 1, 5 do
|
||||
local hp0 = b.player.mon.hp
|
||||
b:resolveTurn({ id = "FIX_TACKLE", pp = 35 })
|
||||
local pt, et = countTicks(drain(b))
|
||||
T.eq(pt, 1, ("round %d of 5: poison ticks exactly once"):format(round))
|
||||
T.eq(et, 0, ("round %d of 5: no enemy tick"):format(round))
|
||||
assertCleared(b, ("round %d of 5"):format(round))
|
||||
local loss = hp0 - b.player.mon.hp
|
||||
T.check(loss > 0, ("round %d of 5: HP actually dropped"):format(round))
|
||||
if lastLoss then
|
||||
T.eq(loss, lastLoss, ("round %d of 5: same tick size as the round before"):format(round))
|
||||
end
|
||||
lastLoss = loss
|
||||
end
|
||||
end
|
||||
|
||||
T.finish("residual latch (#1181)")
|
||||
@@ -0,0 +1,68 @@
|
||||
-- The wall TOWN MAP prints TownMapText before the map screen opens (#1330).
|
||||
-- engine/events/hidden_events/town_map.asm:1
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
|
||||
local screenPushes = {}
|
||||
package.loaded["src.ui.Screens"] = {
|
||||
push = function(_, id, opts)
|
||||
screenPushes[#screenPushes + 1] = { id = id, opts = opts }
|
||||
end,
|
||||
}
|
||||
|
||||
local OW = require("src.world.OverworldController")
|
||||
|
||||
local function setUpvalue(fn, name, val)
|
||||
local i = 1
|
||||
while true do
|
||||
local n = debug.getupvalue(fn, i)
|
||||
if not n then return false end
|
||||
if n == name then debug.setupvalue(fn, i, val); return true end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local pushed
|
||||
local textBoxStub = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
}
|
||||
local fakeGame = {
|
||||
data = { text = { _TownMapText = "A TOWN MAP." } },
|
||||
stack = { push = function(_, box) pushed = box end },
|
||||
}
|
||||
T.check(setUpvalue(OW.tryBookshelf, "TextBox", textBoxStub),
|
||||
"TextBox upvalue on tryBookshelf")
|
||||
T.check(setUpvalue(OW.tryBookshelf, "Game", fakeGame),
|
||||
"Game upvalue on tryBookshelf")
|
||||
|
||||
local map = {
|
||||
def = { tileset = "HOUSE" },
|
||||
inBounds = function() return true end,
|
||||
cellTile = function() return 0x3D end,
|
||||
}
|
||||
local fakeSelf = setmetatable({ player = { facing = "up" }, map = map },
|
||||
{ __index = OW })
|
||||
|
||||
pushed, screenPushes = nil, {}
|
||||
local result = fakeSelf:tryBookshelf(5, 2)
|
||||
T.eq(result, true, "tryBookshelf consumes the wall TOWN MAP tile")
|
||||
T.check(pushed ~= nil, "a text box was pushed")
|
||||
T.eq(pushed.text, "A TOWN MAP.", "the box carries TownMapText")
|
||||
T.eq(#screenPushes, 0, "the TownMap screen has not opened yet")
|
||||
T.check(type(pushed.onDone) == "function", "the box has an onDone")
|
||||
|
||||
pushed.onDone()
|
||||
T.eq(#screenPushes, 1, "onDone is what opens the screen")
|
||||
T.eq(screenPushes[1].id, "TownMap", "the pushed screen id is TownMap")
|
||||
|
||||
-- a screen id a mod removed must not crash the interaction
|
||||
pushed, screenPushes = nil, {}
|
||||
package.loaded["src.ui.Screens"].push = function() error("no such screen") end
|
||||
fakeSelf:tryBookshelf(5, 2)
|
||||
local ok = pcall(pushed.onDone)
|
||||
T.check(ok, "Screens.push stays wrapped in pcall")
|
||||
|
||||
T.finish("town_map_bookshelf_bug1330")
|
||||
Reference in New Issue
Block a user