mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
230 lines
9.8 KiB
Lua
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()
|