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

230 lines
9.8 KiB
Lua

-- ROCK SMASH's wild encounter (engine/events/treemons.asm RockMonEncounter).
--
-- luajit tests/gen2_rock_smash_test.lua
--
-- The smash itself already worked: an object carrying SPRITEMOVEDATA_SMASHABLE_
-- ROCK runs `jumpstd SmashRockScript`, which farsjumps AskRockSmashScript and
-- then RockSmashScript, and the rock disappears. What never happened was the
-- BATTLE at the end of it, because RockMonEncounter was a deliberate stub and
-- RockMonMaps was not extracted -- so the `readmem wTempWildMonSpecies` two
-- rows later read the VM's own sparse store, saw 0, and the `iffalse` skipped
-- the `randomwildmon / startbattle` pair every single time. No wild mon has
-- ever come out of a rock in this port, which also makes wild SHUCKLE
-- unobtainable.
--
-- The routine is TreeMonEncounter's twin over a different map table:
--
-- GetTreeMonSet RockMonMaps CIANWOOD_CITY, ROUTE_40,
-- DARK_CAVE_VIOLET_ENTRANCE, SLOWPOKE_WELL_B1F
-- GetTreeMons TREEMON_SET_ROCK
-- RandomRange 10 / cp 4 40 percent, BETWEEN the lookup and the pick
-- SelectTreeMon the set's FIRST list: 90 KRABBY / 10 SHUCKLE
--
-- and it writes no wScriptVar at all, which is why the script reads its answer
-- out of WRAM instead.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 rock smash")
local check, eq = S.check, S.eq
local World = require("src.world.gen2.World")
local CallAsm = require("src.script.gen2.CallAsm")
-- The site the cart's RockSmashScript carries, `bank:addr` out of
-- pokegold-symbols/pokegold.sym.
eq(CallAsm.SITES["2e:63a1"], "RockMonEncounter",
"RockMonEncounter still sits at 2e:63a1")
check(CallAsm.HANDLERS.RockMonEncounter ~= nil,
"and it is a handler now, not a stub")
check(CallAsm.STUBS.RockMonEncounter == nil, "with no stub left behind it")
eq(CallAsm.STUB_REASONS.RockMonEncounter, nil, "nor a stub reason")
local DATA = {
items = {},
moves = { TACKLE = { name = "TACKLE", pp = 35 } },
pokemon = {
KRABBY = { name = "KRABBY", index = 98, types = { "WATER", "WATER" },
baseStats = { hp = 30, attack = 105, defense = 90, speed = 50,
specialAttack = 25, specialDefense = 25 },
growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } },
SHUCKLE = { name = "SHUCKLE", index = 213, types = { "BUG", "ROCK" },
baseStats = { hp = 20, attack = 10, defense = 230, speed = 5,
specialAttack = 10, specialDefense = 230 },
growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } },
GEODUDE = { name = "GEODUDE", index = 74, types = { "ROCK", "GROUND" },
baseStats = { hp = 40, attack = 80, defense = 100, speed = 20,
specialAttack = 30, specialDefense = 30 },
growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } },
},
}
-- data/wild/treemons.asm TreeMonSet_Rock, and one grass row for the same map so
-- a mis-wired `randomwildmon` would be visible as a GEODUDE rather than as
-- nothing.
local ENCOUNTERS = {
rocks = { DARK_CAVE_VIOLET_ENTRANCE = "TREEMON_SET_ROCK" },
treeSets = {
TREEMON_SET_ROCK = {
common = { { chance = 90, species = "KRABBY", level = 15 },
{ chance = 10, species = "SHUCKLE", level = 15 } },
rare = { { chance = 100, species = "GEODUDE", level = 3 } },
},
},
grass = {
DARK_CAVE_VIOLET_ENTRANCE = { map = "DARK_CAVE_VIOLET_ENTRANCE",
rates = { MORN = 256, DAY = 256, NITE = 256 },
slots = { MORN = {}, DAY = {}, NITE = {} } },
},
}
local function rockWorld(mapId)
local game = {
data = DATA,
save = { player = { name = "GOLD", badges = {} },
party = { { species = "GEODUDE", level = 20, hp = 30, maxHp = 30 } },
inventory = {} },
}
local world = World.new(game)
game.world = world
world.maps = { [mapId] = { id = mapId, group = 3, map = 1, width = 2,
height = 2, blocks = { 1, 2, 3, 4 }, objects = {}, warps = {},
environment = "CAVE" } }
world.map = { id = mapId, def = world.maps[mapId], width = 2, height = 2,
cellCollision = function() return 0 end }
world.encounters = ENCOUNTERS
world.player = { cellX = 0, cellY = 0 }
world.daytime = "DAY"
return world
end
-- constants/pokemon_constants.asm, the indices `startbattle` fights by.
local KRABBY_INDEX, SHUCKLE_INDEX = 98, 213
-- ---- the 40 percent --------------------------------------------------------
--
-- `ld a, 10 / call RandomRange / cp 4 / jr nc, .no_battle`: 0..3 of ten. The
-- roll is taken BEFORE SelectTreeMon, so a miss costs nothing else.
do
local world = rockWorld("DARK_CAVE_VIOLET_ENTRANCE")
local rolls = {}
world.rockmonRandom = function(n) rolls[#rolls + 1] = n return 4 end
eq(world:rockMonEncounter(), 0, "a 4 out of ten is no encounter")
eq(#rolls, 1, "and the list is never even rolled on")
check(world.tempWildMon == nil, "wTempWildMonSpecies is left at zero")
world.rockmonRandom = function() return 9 end
eq(world:rockMonEncounter(), 0, "and so is a 9")
end
do
local world = rockWorld("DARK_CAVE_VIOLET_ENTRANCE")
-- 3 passes `cp 4`, and 3 lands in the 90 percent KRABBY bracket.
world.rockmonRandom = function() return 3 end
eq(world:rockMonEncounter(), KRABBY_INDEX,
"a 3 out of ten smashes a KRABBY out of the rock")
eq(world.tempWildMon.level, 15, "at TreeMonSet_Rock's own level")
-- SelectTreeMon walks the chance column as a running total, so only a roll
-- of 90..99 reaches the SHUCKLE row.
local calls = 0
world.rockmonRandom = function()
calls = calls + 1
return calls == 1 and 0 or 95
end
eq(world:rockMonEncounter(), SHUCKLE_INDEX,
"and a 95 out of a hundred reaches the 10 percent SHUCKLE")
end
-- GetTreeMonSet's `.not_in_table`: a map RockMonMaps does not name has no rock
-- mon however hard it is hit.
do
local world = rockWorld("ROUTE_31")
world.rockmonRandom = function() return 0 end
eq(world:rockMonEncounter(), 0, "a map outside RockMonMaps yields nothing")
end
-- ---- the WRAM byte the script reads back -----------------------------------
--
-- RockSmashScript is `callasm RockMonEncounter / readmem wTempWildMonSpecies /
-- iffalse .done / randomwildmon / startbattle`. The port's VM keeps its own
-- sparse store for the addresses a SCRIPT owns; $d117 is one the ENGINE owns,
-- so World answers for it and the `iffalse` sees the byte that was just
-- written.
do
local world = rockWorld("DARK_CAVE_VIOLET_ENTRANCE")
eq(world:scriptReadMem(0xd117), 0, "nothing smashed reads back as zero")
eq(world:scriptReadMem(0xd6a8), nil,
"and an address the World does not own stays the VM's own")
world.rockmonRandom = function() return 3 end
world:rockMonEncounter()
eq(world:scriptReadMem(0xd117), KRABBY_INDEX,
"a hit reads back as the species the iffalse branches on")
-- Script_randomwildmon only clears wBattleScriptFlags; the pair `startbattle`
-- fights is the one already staged. Reading the map's grass list here
-- instead would have fought whatever Dark Cave holds, not the rock's mon.
local rolled = world:rollWild()
eq(rolled and rolled.species, KRABBY_INDEX,
"randomwildmon takes the staged pair rather than rolling the grass")
eq(rolled and rolled.level, 15, "at the level the rock rolled")
eq(world:scriptReadMem(0xd117), 0, "and the byte is consumed by the battle")
end
-- CallAsm.run is the entry point World:callAsm reaches, and the handler must
-- leave wScriptVar alone: the cart's routine writes none.
do
local world = rockWorld("DARK_CAVE_VIOLET_ENTRANCE")
world.rockmonRandom = function() return 3 end
eq(CallAsm.run(world, "RockMonEncounter"), nil,
"the handler returns no wScriptVar, exactly as the asm does not write one")
eq(world:scriptReadMem(0xd117), KRABBY_INDEX,
"but it did stage the mon")
eq(CallAsm.dispatch(world, nil, 0x2e, 0x63a1), nil,
"and dispatching by the script's own bank:addr reaches the same routine")
end
-- ---- the real script rows, out of the cache --------------------------------
do
local cache = os.getenv("GOLD_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
end
local scriptChunk = loadfile(cache .. "/data/generated/scripts.lua")
local encChunk = loadfile(cache .. "/data/generated/encounters.lua")
if not (scriptChunk and encChunk) then
check(true, "no gold cache: RockSmashScript rows (SKIP)")
else
local scripts = scriptChunk()
local encounters = encChunk()
local rows = scripts["03:4f35"]
check(rows ~= nil, "RockSmashScript is in the cache at 03:4f35")
eq(rows and rows[9] and rows[9].op, "callasm",
"row 9 is the callasm this suite is about")
eq(rows and rows[9] and rows[9].args and rows[9].args[1], 0x2e,
"in bank $2e")
eq(rows and rows[10] and rows[10].op, "readmem", "row 10 reads the byte")
local args = rows and rows[10] and rows[10].args
eq(args and (args[1] + args[2] * 256), 0xd117,
"and the address it reads is wTempWildMonSpecies")
eq(rows and rows[11] and rows[11].op, "iffalse", "row 11 branches on it")
eq(rows and rows[12] and rows[12].op, "randomwildmon",
"row 12 is the randomwildmon that takes the staged pair")
eq(rows and rows[13] and rows[13].op, "startbattle", "row 13 fights it")
-- RockMonMaps, straight off the cart now.
local rocks = encounters.rocks or {}
eq(rocks.CIANWOOD_CITY, "TREEMON_SET_ROCK", "RockMonMaps: Cianwood")
eq(rocks.ROUTE_40, "TREEMON_SET_ROCK", "Route 40")
eq(rocks.DARK_CAVE_VIOLET_ENTRANCE, "TREEMON_SET_ROCK",
"Dark Cave Violet Entrance")
eq(rocks.SLOWPOKE_WELL_B1F, "TREEMON_SET_ROCK", "and Slowpoke Well B1F")
local rockSet = encounters.treeSets and encounters.treeSets.TREEMON_SET_ROCK
eq(rockSet and rockSet.common[1].species, "KRABBY", "90 percent KRABBY")
eq(rockSet and rockSet.common[2].species, "SHUCKLE",
"and 10 percent SHUCKLE, the only wild SHUCKLE in the game")
end
end
S.finish()