-- 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()
