CLOSES #1206, CLOSES #1189, CLOSES #1175, CLOSES #1129, CLOSES #1119, CLOSES #1089, CLOSES #1073, CLOSES #1069, CLOSES #1065, CLOSES #1010, CLOSES #990, CLOSES #989, CLOSES #988, CLOSES #978, CLOSES #944, CLOSES #936, CLOSES #1221, CLOSES #1220, CLOSES #1193, CLOSES #1101, CLOSES #1066, CLOSES #1056, CLOSES #1041, CLOSES #984, CLOSES #1225, CLOSES #1214, CLOSES #1011, CLOSES #1035, CLOSES #1219, CLOSES #1048, CLOSES #1047, CLOSES #964, CLOSES #949, CLOSES #1149, CLOSES #1007, CLOSES #914, CLOSES #1115, CLOSES #1146, CLOSES #999, CLOSES #1207, CLOSES #1029, CLOSES #1120, CLOSES #987, CLOSES #983

This commit is contained in:
bryanthaboi
2026-08-13 13:07:20 -04:00
parent 833388c235
commit 37051a26b5
43 changed files with 752 additions and 135 deletions
+82
View File
@@ -0,0 +1,82 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local ItemEffects = require("src.core.gen2.ItemEffects")
local Game2 = require("src.core.Game2")
local Screens = require("src.ui.Screens")
local EvolutionAnim = require("src.ui.gen2.EvolutionAnim")
local data = {
pokemon = {
SUNKERN = {
evolutions = {
{ method = "EVOLVE_ITEM", item = "SUN_STONE", into = "SUNFLORA" },
},
},
},
}
local mon = { species = "SUNKERN", item = nil }
T.eq(ItemEffects.partyAction("SUN_STONE"), "stone",
"SUN STONE opens the party target flow")
local result = ItemEffects.useOnMon("SUN_STONE", mon, data)
T.check(result.used, "SUN STONE succeeds on SUNKERN")
T.eq(result.evolution and result.evolution.into, "SUNFLORA",
"SUN STONE selects SUNFLORA")
mon.item = "EVERSTONE"
result = ItemEffects.useOnMon("SUN_STONE", mon, data)
T.check(not result.used, "held EVERSTONE refuses the evolution")
local function startStone()
local partyOptions, evolutionOptions
local game = setmetatable({
data = {
pokemon = data.pokemon,
screens = {
Gen2PartyMenu = function(_, options)
partyOptions = options
return {}
end,
Gen2EvolutionAnim = function(_, options)
evolutionOptions = options
return {}
end,
},
},
save = {
party = { { species = "SUNKERN", item = nil } },
inventory = { SUN_STONE = 1 },
},
stack = { pop = function() end, push = function() end },
}, Game2)
Screens.invalidate()
game:usePartyItem("SUN_STONE")
partyOptions.onChoose(nil, game.save.party[1])
return game, evolutionOptions
end
local game, evolution = startStone()
T.check(evolution.force, "SUN STONE evolution sets wForceEvolution")
T.eq(game.save.inventory.SUN_STONE, 1,
"SUN STONE remains until evolution succeeds")
local animation = EvolutionAnim.new({ data = { pokemon = data.pokemon } }, {
mon = game.save.party[1], entry = evolution and evolution.entry,
force = evolution and evolution.force,
})
T.check(not animation:cancelPressed({ wasPressed = function(_, key)
return key == "b"
end }), "B cannot cancel a forced stone evolution")
evolution.onDone({ canceled = true })
T.eq(game.save.inventory.SUN_STONE, 1,
"a canceled evolution does not consume SUN STONE")
game, evolution = startStone()
evolution.onDone({ evolved = { species = "SUNFLORA" } })
T.eq(game.save.inventory.SUN_STONE, nil,
"a completed evolution consumes SUN STONE")
Screens.invalidate()
T.finish("gen2 sun stone bug 1219")
+5 -4
View File
@@ -65,7 +65,7 @@ T.check(pushed[1].text:find(BYE, 1, true) == nil,
pushed[1].onDone()
T.eq(#pushed, 1, "the farewell waits for the bow")
T.eq(nurse.facing, "up", "image index $14: the nurse bows")
T.eq(nurse.frameOverride, 3, "image index $1: the nurse bows")
T.check(fakeSelf.emote ~= nil, "the bow is a world hold, not a text pause")
local hold = fakeSelf.emote or {}
T.eq(hold.npc, nurse, "the hold is anchored on the nurse")
@@ -79,11 +79,12 @@ if hold.onDone then hold.onDone() end
T.eq(#pushed, 2, "the farewell follows the bow")
local farewell = pushed[2] or {}
T.eq(farewell.text, BYE, "second box is the farewell text")
T.eq(nurse.facing, "up", "she is still bowed while the farewell prints")
T.eq(nurse.frameOverride, nil, "the bow ends before the farewell prints")
T.eq(nurse.facing, "down", "the nurse faces the player for the farewell")
if farewell.onDone then farewell.onDone() end
T.eq(nurse.facing, "down", "the trailing UpdateSprites faces her back")
T.eq(faced, 1, "she is turned back exactly once")
T.eq(nurse.facing, "down", "the trailing UpdateSprites keeps her facing")
T.eq(faced, 2, "she is turned back before and after the farewell")
T.eq(finished, 1, "control returns to the player once, after the farewell")
-- === no nurse sprite (the Yellow/rest-stop callers): no bow, same text
+55
View File
@@ -0,0 +1,55 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local GenSave = require("src.save_convert.GenSave")
local romText = require("src.core.RomText")
local localized = {
text = {
_MoveIsDisabledText = "The move {RAM:wNameBuffer} from {USER} is disabled!",
},
}
T.eq(romText(localized, "_MoveIsDisabledText", "%s's %s is disabled!", {
USER = "PIKACHU", ["RAM:wNameBuffer"] = "THUNDER",
}), "The move THUNDER from PIKACHU is disabled!",
"named ROM tokens survive translation reordering")
GenSave.setCharmap(loadfile("src/save_convert/data/charmap.lua")())
local data = {
pokemon = {}, moves = {}, items = {}, maps = {},
hiddenItems = loadfile("src/save_convert/data/hidden_items.lua")(),
}
local save = {
player = { name = "RED", id = 1, map = "PALLET_TOWN", x = 0, y = 0 },
rival = { name = "BLUE" }, party = {}, boxes = {}, inventory = {},
pcItems = {}, flags = {}, pokedex = { seen = {}, owned = {} },
hiddenTaken = {
VIRIDIAN_FOREST_1_18 = true,
VIRIDIAN_CITY_14_4 = true,
},
}
for i = 1, 12 do save.boxes[i] = {} end
local bytes = GenSave.encode(save, data, nil)
local decoded = GenSave.decode(bytes, data)
T.check(decoded.hiddenTaken.VIRIDIAN_FOREST_1_18,
"Viridian Forest hidden potion imports from wObtainedHiddenItemsFlags")
T.check(decoded.hiddenTaken.VIRIDIAN_CITY_14_4,
"Viridian City hidden potion imports from wObtainedHiddenItemsFlags")
T.check(not decoded.hiddenTaken.ROUTE_9_14_7,
"an uncollected hidden item remains available")
love = love or require("tests.love_stub")
local start = require("src.ui.StartMenu").new({
data = {}, save = {
flags = {}, party = {}, inventory = {}, options = {},
player = { name = "RED" }, pokedex = { owned = {} },
},
})
local pokemonRow
for _, row in ipairs(start.items) do
if row.label == "POKéMON" then pokemonRow = row break end
end
T.check(pokemonRow and pokemonRow.keepOpen,
"the empty-party POKéMON row leaves the start menu open")
T.finish("open menu bugs 949 and 1149")
+3 -3
View File
@@ -60,14 +60,14 @@ for _, r in ipairs(rows) do
end
check(not hasRecord, "CHAMPIONS_ROOM rival script no longer calls record_hall_of_fame")
-- The post-battle walk takes the right-hand detour before heading north, so
-- The post-battle walk takes the left-hand detour before heading north, so
-- the player does not visibly pass through the rival at (4,2).
local route = {}
for _, r in ipairs(rows) do
if r[1] == "move_player" then route[#route + 1] = r end
end
eq(route[#route - 1] and route[#route - 1][2], "right",
"walk-out route first moves right around the rival")
eq(route[#route - 1] and route[#route - 1][2], "left",
"walk-out route first moves left around the rival")
eq(route[#route] and route[#route][2], "up",
"walk-out route then heads north to Hall of Fame")
+3 -3
View File
@@ -203,13 +203,13 @@ do
eq(tb.waitFrames, 60, "for the 60 frames AnimationBlinkEnemyMon takes")
end
-- the OPTIONS animation toggle still gates the whole thing; the sound does not
-- engine/battle/animations.asm PlayApplyingAttackAnimation
do
local tb = freshBattle()
Game.save.options.animations = false
tb:applyHitFx({ animType = 5, sfx = "Damage" })
eq(tb.fx.shakeProg, nil, "animations off arms no shake")
eq(tb.fx.blink, nil, "and no blink")
check(tb.fx.shakeProg ~= nil, "animations off keeps the hit shake")
eq(tb.fx.blink, nil, "type 5 remains a shake, not a blink")
Game.save.options.animations = true
end
+93
View File
@@ -0,0 +1,93 @@
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 Yellow disabled Pikachu")
local check, eq = S.check, S.eq
local Data = require("src.core.Data")
Data:load()
local GameVersion = require("src.core.GameVersion")
local Follower = require("src.world.PikachuFollower")
local ItemEffects = require("src.inventory.ItemEffects")
local PartyMenu = require("src.ui.PartyMenu")
local BoxMenu = require("src.ui.BoxMenu")
local TextBox = require("src.render.TextBox")
local oldVersion = GameVersion.get()
GameVersion.set("yellow")
local save = {
player = { id = 7, name = "RED" },
party = {
{ species = "PIKACHU", otId = 7, ot = "RED", hp = 20 },
{ species = "PIDGEY", otId = 7, ot = "RED", hp = 20 },
},
}
check(Follower.isStarterPikachu(save, save.party[1]), "the player Pikachu is identified")
check(not Follower.isStarterPikachu(save, save.party[2]), "other party members are not starter Pikachu")
local pika = { pikachuFollower = true, cellX = 3, cellY = 4, facing = "down" }
local seel = { def = { name = "POKEMONFANCLUB_SEEL" }, cellX = 1, cellY = 4 }
local moves = {}
local ow = {
map = { id = "POKEMON_FAN_CLUB" },
player = { cellX = 3, cellY = 5, facing = "up" },
npcs = { pika, seel },
scriptMove = function(_, npc, dir, tiles, done)
moves[#moves + 1] = { dir, tiles }
npc.facing = dir
if done then done() end
end,
}
Follower.onFanClubEntered({ save = save, data = Data }, ow)
check(ow.pikachuFanClubScene, "Fan Club disables normal Pikachu following")
check(ow.pikachuMapScriptActive, "Fan Club sets the map-script flag")
eq(ow.player.facing, "down", "Fan Club resets the player direction")
eq(moves[1] and moves[1][1], "up", "Fan Club starts with slide-up displacement")
eq(moves[1] and moves[1][2], 1, "Fan Club slide-up spans one tile")
eq(moves[2] and moves[2][1], "right", "Fan Club then walks right")
eq(moves[2] and moves[2][2], 3, "Fan Club walks right three tiles")
eq(moves[3] and moves[3][1], "up", "Fan Club ends walking up")
eq(moves[3] and moves[3][2], 1, "Fan Club final up spans one tile")
eq(seel.movementStatus, 2, "Fan Club puts Seel into movement delay")
eq(seel.facing, "down", "Fan Club turns Seel down")
check(Follower.isFollowingDisabled(ow), "disabled Fan Club follower blocks starter selection")
local sleepOw = {
map = { id = "PEWTER_POKECENTER" },
player = { cellX = 3, cellY = 5 },
npcs = { pika },
pikachuPewterSleepScene = true,
}
local result = ItemEffects.use(Data, save, "POKE_FLUTE", nil, nil, nil, sleepOw)
eq(result, "flute_wake_pikachu", "Poké Flute is allowed next to sleeping Pikachu")
check(Follower.isFollowingDisabled(sleepOw), "sleeping Pikachu disables normal follower actions")
local pushed = {}
local partyGame = {
save = save,
overworld = sleepOw,
stack = { push = function(_, state) pushed[#pushed + 1] = state end },
input = { wasPressed = function(_, key) return key == "a" end },
}
PartyMenu.new(partyGame):update()
check(getmetatable(pushed[#pushed]) == TextBox,
"sleeping Pikachu cannot be selected from the party menu")
local boxGame = {
save = save,
overworld = sleepOw,
data = { pokemon = { PIKACHU = { name = "PIKACHU" }, PIDGEY = { name = "PIDGEY" } }, text = {} },
stack = { push = function(_, state) pushed[#pushed + 1] = state end },
}
local pc = BoxMenu.new(boxGame)
pc.items[2].onSelect()
local depositList = pushed[#pushed]
depositList.onChoose(depositList.items[1], depositList)
check(getmetatable(pushed[#pushed]) == TextBox,
"sleeping Pikachu cannot be deposited into Bill's PC")
eq(#save.party, 2, "PC refusal leaves the party unchanged")
GameVersion.set(oldVersion)
S.finish()
+6 -7
View File
@@ -891,8 +891,7 @@ do
check(tb:lockedAction(tb.enemy) == nil, "victim is free after the release")
end
-- #2: recoil and drain use the RAW computed damage, not the HP-capped
-- amount dealt
-- engine/battle/core.asm ApplyDamageToEnemyPokemon
do
Game.save.party = { Pokemon.new(Data, "BULBASAUR", 20) }
local rb = BattleState.newWild(Game, "RATTATA", 3)
@@ -903,8 +902,8 @@ do
rb.rng = mkseq({ 0, 255, 255 })
local hpBefore = rb.player.mon.hp
rb:performMove(rb.player, rb.enemy, { id = "TAKE_DOWN", pp = 10 })
eq(hpBefore - rb.player.mon.hp, math.floor(raw / 4),
"recoil is raw damage / 4 even when only 1 HP was dealt")
eq(hpBefore - rb.player.mon.hp, 1,
"recoil is capped damage / 4 with a minimum of 1")
local db = BattleState.newWild(Game, "RATTATA", 3)
db.enemy.mon.hp = 1
@@ -914,9 +913,9 @@ do
check(rawD >= 4, "raw MEGA DRAIN damage is meaningful (" .. rawD .. ")")
db.rng = mkseq({ 0, 255, 255 })
db:performMove(db.player, db.enemy, { id = "MEGA_DRAIN", pp = 10 })
eq(db.player.mon.hp - 1, math.floor(rawD / 2),
"drain heals raw damage / 2 even when only 1 HP was dealt")
eq(db.lastDamage, math.floor(rawD / 2),
eq(db.player.mon.hp - 1, 1,
"drain heals capped damage / 2 with a minimum of 1")
eq(db.lastDamage, 1,
"drain halves wDamage in place (Counter would see the half)")
end