mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
346 lines
14 KiB
Lua
346 lines
14 KiB
Lua
#!/usr/bin/env luajit
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
require("tests.game3_cache").mountOrSkip("game3_battle_terrain_test")
|
|
|
|
package.loaded["src.core.game3.audio"] = setmetatable({}, {
|
|
__index = function() return function() end end,
|
|
})
|
|
|
|
local failed, passed = 0, 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
passed = passed + 1
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
|
|
local function eq(a, b, msg)
|
|
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
|
end
|
|
|
|
local function finish()
|
|
print(string.format("%d passed, %d failed", passed, failed))
|
|
if failed > 0 then
|
|
print("BATTLE_TERRAIN FAIL")
|
|
os.exit(1)
|
|
end
|
|
print("BATTLE_TERRAIN PASS")
|
|
os.exit(0)
|
|
end
|
|
|
|
local BattleBg = require("src.core.game3.battle.bg")
|
|
local T = BattleBg.TERRAIN
|
|
local MT = BattleBg.MAP_TYPE
|
|
local SCENE = BattleBg.MAP_BATTLE_SCENE
|
|
|
|
print("[test] 1. BATTLE_TERRAIN ids match pret include/constants/battle.h:287")
|
|
do
|
|
local ids = {
|
|
GRASS = 0, LONG_GRASS = 1, SAND = 2, UNDERWATER = 3, WATER = 4,
|
|
POND = 5, MOUNTAIN = 6, CAVE = 7, BUILDING = 8, PLAIN = 9,
|
|
LINK = 10, GYM = 11, LEADER = 12, INDOOR_2 = 13, INDOOR_1 = 14,
|
|
LORELEI = 15, BRUNO = 16, AGATHA = 17, LANCE = 18, CHAMPION = 19,
|
|
}
|
|
local n = 0
|
|
for name, id in pairs(ids) do
|
|
eq(T[name], id, "BATTLE_TERRAIN_" .. name)
|
|
n = n + 1
|
|
end
|
|
eq(n, 20, "20 terrain ids")
|
|
end
|
|
|
|
print("[test] 2. Every sBattleTerrainTable row has its own sheet key")
|
|
do
|
|
local Extract = require("src.import.gba.battle_chrome_extract")
|
|
local keys = Extract.TERRAIN_KEYS or {}
|
|
local all = {}
|
|
for id = 0, 19 do
|
|
if keys[id] then all[keys[id]] = true end
|
|
end
|
|
BattleBg.setAvailableSheets(all)
|
|
local seen = {}
|
|
for id = 0, 19 do
|
|
local key = BattleBg.sheetKey(id)
|
|
eq(key, keys[id], "terrain " .. id .. " draws its own baked sheet")
|
|
check(seen[key] == nil, "terrain " .. id .. " sheet " .. tostring(key) .. " is not shared")
|
|
seen[key] = id
|
|
end
|
|
BattleBg.setAvailableSheets(nil)
|
|
end
|
|
|
|
print("[test] 3. BattleSetup_GetTerrainId rows (pokefirered/src/battle_setup.c:466)")
|
|
do
|
|
local MB_NORMAL = 0x00
|
|
local MB_TALL_GRASS = 0x02
|
|
local MB_CAVE = 0x08
|
|
local MB_INDOOR_ENCOUNTER = 0x0B
|
|
local MB_MOUNTAIN_TOP = 0x0C
|
|
local MB_POND_WATER = 0x10
|
|
local MB_FAST_WATER = 0x11
|
|
local MB_DEEP_WATER = 0x12
|
|
local MB_OCEAN_WATER = 0x15
|
|
local MB_SHALLOW_WATER = 0x17
|
|
local MB_SAND = 0x21
|
|
local MB_CYCLING_ROAD_PULL_DOWN_GRASS = 0xD1
|
|
|
|
local rows = {
|
|
{ MB_TALL_GRASS, MT.ROUTE, T.GRASS, "tall grass on a route is GRASS" },
|
|
{ MB_TALL_GRASS, MT.TOWN, T.GRASS, "tall grass in a town is GRASS" },
|
|
{ MB_CYCLING_ROAD_PULL_DOWN_GRASS, MT.ROUTE, T.GRASS, "cycling road grass is GRASS" },
|
|
{ MB_TALL_GRASS, MT.UNDERGROUND, T.GRASS, "tall grass beats the map type" },
|
|
{ MB_SAND, MT.ROUTE, T.SAND, "sand is SAND" },
|
|
{ MB_SHALLOW_WATER, MT.ROUTE, T.SAND, "shallow flowing water is SAND" },
|
|
{ MB_SAND, MT.INDOOR, T.SAND, "sand beats MAP_TYPE_INDOOR" },
|
|
{ MB_CAVE, MT.UNDERGROUND, T.CAVE, "cave floor underground is CAVE" },
|
|
{ MB_NORMAL, MT.UNDERGROUND, T.CAVE, "plain floor underground is CAVE" },
|
|
{ MB_INDOOR_ENCOUNTER, MT.UNDERGROUND, T.BUILDING, "indoor encounter underground is BUILDING" },
|
|
{ MB_POND_WATER, MT.UNDERGROUND, T.POND, "surfable underground is POND" },
|
|
{ MB_NORMAL, MT.INDOOR, T.BUILDING, "indoor is BUILDING" },
|
|
{ MB_NORMAL, MT.SECRET_BASE, T.BUILDING, "secret base is BUILDING" },
|
|
{ MB_NORMAL, MT.UNDERWATER, T.UNDERWATER, "underwater map is UNDERWATER" },
|
|
{ MB_POND_WATER, MT.OCEAN_ROUTE, T.WATER, "surfable on an ocean route is WATER" },
|
|
{ MB_NORMAL, MT.OCEAN_ROUTE, T.PLAIN, "dry land on an ocean route is PLAIN" },
|
|
{ MB_DEEP_WATER, MT.ROUTE, T.WATER, "deep water is WATER" },
|
|
{ MB_FAST_WATER, MT.ROUTE, T.WATER, "fast water is WATER" },
|
|
{ MB_OCEAN_WATER, MT.ROUTE, T.WATER, "ocean water is WATER" },
|
|
{ MB_POND_WATER, MT.ROUTE, T.POND, "pond water is POND" },
|
|
{ MB_MOUNTAIN_TOP, MT.ROUTE, T.MOUNTAIN, "mountain top is MOUNTAIN" },
|
|
{ MB_NORMAL, MT.ROUTE, T.PLAIN, "bare route tile is PLAIN" },
|
|
{ MB_NORMAL, MT.NONE, T.PLAIN, "MAP_TYPE_NONE falls through to PLAIN" },
|
|
}
|
|
for _, row in ipairs(rows) do
|
|
eq(BattleBg.resolveFromBehavior(row[1], nil, row[2]), row[3], row[4])
|
|
end
|
|
|
|
-- pokefirered/src/metatile_behavior.c:440 MetatileBehavior_IsLongGrass is FALSE in FRLG
|
|
local sawLongGrass = false
|
|
for beh = 0, 255 do
|
|
for _, mt in ipairs({ MT.NONE, MT.TOWN, MT.CITY, MT.ROUTE, MT.UNDERGROUND,
|
|
MT.UNDERWATER, MT.OCEAN_ROUTE, MT.UNKNOWN, MT.INDOOR, MT.SECRET_BASE }) do
|
|
if BattleBg.resolveFromBehavior(beh, nil, mt) == T.LONG_GRASS then sawLongGrass = true end
|
|
end
|
|
end
|
|
check(not sawLongGrass, "no behavior reaches LONG_GRASS on FRLG")
|
|
end
|
|
|
|
print("[test] 4. No behavior available falls back to the map kind")
|
|
do
|
|
eq(BattleBg.resolveFromBehavior(nil, "cave", nil), T.CAVE, "nil behavior + cave kind is CAVE")
|
|
eq(BattleBg.resolveFromBehavior(nil, "indoor", nil), T.BUILDING, "nil behavior + indoor kind is BUILDING")
|
|
eq(BattleBg.resolveFromBehavior(nil, "route", nil), T.GRASS, "nil behavior + route kind is GRASS")
|
|
eq(BattleBg.resolveFromMapKind("water"), T.WATER, "resolveFromMapKind still answers")
|
|
eq(BattleBg.resolveFromBehavior(0x00, "cave", nil), T.CAVE, "cave kind stands in for MAP_TYPE_UNDERGROUND")
|
|
eq(BattleBg.resolveFromBehavior(0x00, "route", nil), T.PLAIN, "route kind stands in for MAP_TYPE_ROUTE")
|
|
end
|
|
|
|
print("[test] 5. GetBattleTerrainOverride (pokefirered/src/battle_bg.c:1048)")
|
|
do
|
|
eq(BattleBg.resolveOverride(T.CAVE, {}), T.CAVE, "no override keeps the map terrain")
|
|
eq(BattleBg.resolveOverride(T.CAVE, { link = true }), T.LINK, "a link battle is LINK")
|
|
eq(BattleBg.resolveOverride(T.CAVE, { trainerTower = true }), T.LINK, "a trainer tower battle is LINK")
|
|
eq(BattleBg.resolveOverride(T.CAVE, { pokedude = true }), T.GRASS, "a pokedude battle is GRASS")
|
|
eq(BattleBg.resolveOverride(T.CAVE, { trainer = true, trainerClass = 84 }), T.LEADER,
|
|
"TRAINER_CLASS_LEADER is LEADER")
|
|
eq(BattleBg.resolveOverride(T.CAVE, { trainer = true, trainerClass = 90 }), T.CHAMPION,
|
|
"TRAINER_CLASS_CHAMPION is CHAMPION")
|
|
eq(BattleBg.resolveOverride(T.CAVE, { trainer = true, trainerClass = 87 }), T.CAVE,
|
|
"TRAINER_CLASS_ELITE_FOUR takes no class override")
|
|
eq(BattleBg.resolveOverride(T.CAVE, { trainerClass = 84 }), T.CAVE,
|
|
"a wild battle ignores the trainer class")
|
|
eq(BattleBg.resolveOverride(T.GRASS, { mapBattleScene = SCENE.NORMAL }), T.GRASS,
|
|
"MAP_BATTLE_SCENE_NORMAL keeps gBattleTerrain")
|
|
local scenes = {
|
|
{ SCENE.GYM, T.GYM }, { SCENE.INDOOR_1, T.INDOOR_1 }, { SCENE.INDOOR_2, T.INDOOR_2 },
|
|
{ SCENE.LORELEI, T.LORELEI }, { SCENE.BRUNO, T.BRUNO }, { SCENE.AGATHA, T.AGATHA },
|
|
{ SCENE.LANCE, T.LANCE }, { SCENE.LINK, T.LINK },
|
|
}
|
|
for _, row in ipairs(scenes) do
|
|
eq(BattleBg.resolveOverride(T.GRASS, { mapBattleScene = row[1] }), row[2],
|
|
"map battle scene " .. row[1])
|
|
end
|
|
eq(BattleBg.resolveOverride(T.GRASS, { mapBattleScene = 99 }), T.PLAIN,
|
|
"an unknown map battle scene is PLAIN")
|
|
eq(BattleBg.resolveOverride(T.GRASS, { link = true, mapBattleScene = SCENE.GYM }), T.LINK,
|
|
"link wins over the map battle scene")
|
|
end
|
|
|
|
print("[test] 6. A cache without a terrain's sheet degrades to the nearest one")
|
|
do
|
|
BattleBg.setAvailableSheets({ grass = true, building = true })
|
|
eq(BattleBg.sheetKey(T.LONG_GRASS), "grass", "long grass degrades to grass")
|
|
eq(BattleBg.sheetKey(T.SAND), "grass", "sand degrades to grass")
|
|
eq(BattleBg.sheetKey(T.CAVE), "building", "cave with no cave art degrades to building")
|
|
eq(BattleBg.sheetKey(T.WATER), "grass", "water degrades to grass")
|
|
eq(BattleBg.sheetKey(T.GRASS), "grass", "grass is drawn as grass")
|
|
|
|
BattleBg.setAvailableSheets({ grass = true, plain = true, water = true, cave = true })
|
|
eq(BattleBg.sheetKey(T.POND), "water", "pond degrades to water")
|
|
eq(BattleBg.sheetKey(T.UNDERWATER), "water", "underwater degrades to water")
|
|
eq(BattleBg.sheetKey(T.MOUNTAIN), "cave", "mountain degrades to cave")
|
|
eq(BattleBg.sheetKey(T.BUILDING), "plain", "building degrades to plain")
|
|
eq(BattleBg.sheetKey(T.LEADER), "grass", "leader with no indoor art ends at grass")
|
|
eq(BattleBg.sheetKey(T.CHAMPION), "grass", "champion with no indoor art ends at grass")
|
|
|
|
BattleBg.setAvailableSheets({})
|
|
for id = 0, 19 do
|
|
check(type(BattleBg.sheetKey(id)) == "string", "terrain " .. id .. " still names a sheet")
|
|
end
|
|
eq(BattleBg.sheetKey(255), "building", "an out of range terrain names building")
|
|
|
|
BattleBg.setAvailableSheets(nil)
|
|
end
|
|
|
|
print("[test] 7. Battle.start resolves the terrain from the behavior it is handed")
|
|
do
|
|
local Moves = require("src.core.game3.battle.moves")
|
|
local ROM = {
|
|
[33] = { effect = 0, power = 35, type = 0, accuracy = 95, pp = 35,
|
|
secondaryChance = 0, target = 0, priority = 0, flags = 51 },
|
|
}
|
|
Moves._romLoaded = true
|
|
Moves._rom = ROM
|
|
Moves.loadRomPack = function()
|
|
Moves._romLoaded = true
|
|
Moves._rom = ROM
|
|
return true
|
|
end
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
Pokemon.install(nil)
|
|
local Battle = require("src.core.game3.battle")
|
|
|
|
local function start(extra)
|
|
Battle.abort()
|
|
local session = {
|
|
name = "RED",
|
|
party = {
|
|
{ species = 1, name = "BULBASAUR", level = 10, hp = 30, maxHp = 30,
|
|
attack = 12, defense = 12, spAtk = 12, spDef = 12, speed = 12,
|
|
moves = { 33 }, pp = { 35 } },
|
|
},
|
|
dex = { seen = {}, owned = {} },
|
|
}
|
|
local opts = {
|
|
headless = true,
|
|
autoFight = false,
|
|
wild = true,
|
|
session = session,
|
|
playerParty = session.party,
|
|
rng = function(_, hi) return hi end,
|
|
foe = { species = 16, level = 8, hp = 24, maxHp = 24,
|
|
attack = 10, defense = 10, spAtk = 10, spDef = 10, speed = 30,
|
|
moves = { 33 }, pp = { 35 } },
|
|
}
|
|
for k, v in pairs(extra or {}) do opts[k] = v end
|
|
Battle.start(opts)
|
|
return Battle.getState()
|
|
end
|
|
|
|
local st = start({ mapKind = "indoor", mapType = 4, mapBehavior = 0x08 })
|
|
eq(st.terrain, T.CAVE, "Mt Moon: MAP_TYPE_UNDERGROUND + MB_CAVE is CAVE")
|
|
eq(BattleBg.terrainId(), T.CAVE, "the draw path sees CAVE")
|
|
Battle.abort()
|
|
|
|
st = start({ mapKind = "route", mapType = 3, mapBehavior = 0x10 })
|
|
eq(st.terrain, T.POND, "surfing a route pond is POND")
|
|
Battle.abort()
|
|
|
|
st = start({ mapKind = "route", mapType = 3, mapBehavior = 0x15 })
|
|
eq(st.terrain, T.WATER, "surfing ocean water is WATER")
|
|
Battle.abort()
|
|
|
|
st = start({ mapKind = "route", mapType = 3, mapBehavior = 0x02 })
|
|
eq(st.terrain, T.GRASS, "route tall grass is GRASS")
|
|
Battle.abort()
|
|
|
|
st = start({ mapKind = "indoor", mapType = 8, mapBehavior = 0x0B })
|
|
eq(st.terrain, T.BUILDING, "an indoor encounter is BUILDING")
|
|
Battle.abort()
|
|
|
|
st = start({ mapKind = "route" })
|
|
eq(st.terrain, T.GRASS, "the headless map kind path still resolves")
|
|
Battle.abort()
|
|
|
|
st = start({ mapKind = "route", mapType = 3, mapBehavior = 0x02, terrain = T.CAVE })
|
|
eq(st.terrain, T.CAVE, "an explicit opts.terrain wins")
|
|
Battle.abort()
|
|
|
|
st = start({ mapKind = "indoor", mapType = 8, mapBehavior = 0x00,
|
|
mapBattleScene = SCENE.GYM })
|
|
eq(BattleBg.terrainId(), T.GYM, "a gym map draws the GYM background")
|
|
eq(st.terrain, T.BUILDING, "and gBattleTerrain keeps the un-overridden id")
|
|
Battle.abort()
|
|
end
|
|
|
|
print("[test] 8. The imported cache bakes every sheet these ids ask for")
|
|
do
|
|
local Cache = require("tests.game3_cache")
|
|
local root = Cache.root("meta.json")
|
|
if not root then
|
|
print("[skip] cache sheet check: " .. tostring(Cache.reason))
|
|
else
|
|
local f = io.open(root .. "/pokemon/battle/manifest.lua", "rb")
|
|
if not f then
|
|
print("[skip] cache sheet check: no pokemon/battle/manifest.lua in " .. root)
|
|
else
|
|
local src = f:read("*a")
|
|
f:close()
|
|
local m = assert(loadstring(src))()
|
|
local have = m.terrains or {}
|
|
BattleBg.setAvailableSheets(have)
|
|
local Extract = require("src.import.gba.battle_chrome_extract")
|
|
local fallbacks = {}
|
|
for id = 0, 19 do
|
|
local want = Extract.TERRAIN_KEYS[id]
|
|
if BattleBg.sheetKey(id) ~= want then
|
|
fallbacks[#fallbacks + 1] = string.format("%d(%s->%s)", id, tostring(want),
|
|
tostring(BattleBg.sheetKey(id)))
|
|
end
|
|
end
|
|
local list = (#fallbacks == 0) and "none" or table.concat(fallbacks, ", ")
|
|
print("[info] terrain ids still falling back on this cache: " .. list)
|
|
if (tonumber(m.format) or 0) >= 6 then
|
|
check(#fallbacks == 0, "a format 6 cache bakes every terrain sheet (" .. list .. ")")
|
|
else
|
|
print("[skip] cache format " .. tostring(m.format) .. " predates the terrain bake")
|
|
end
|
|
BattleBg.setAvailableSheets(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
print("[test] 9. the map battle scene comes off the live map def (overworld.c:1270)")
|
|
do
|
|
local BattleBridge = require("src.core.game3.battle_bridge")
|
|
local CacheFs = require("src.import.CacheFs")
|
|
local realReadActive, realRead = CacheFs.readActive, CacheFs.read
|
|
CacheFs.readActive = function() error("mapBattleScene must not read the cache") end
|
|
CacheFs.read = function() error("mapBattleScene must not read the cache") end
|
|
|
|
local game = { data = { maps = {
|
|
FR_VIRIDIAN_CITY_GYM = { battleType = SCENE.GYM },
|
|
FR_PALLET_TOWN = { battleType = SCENE.NORMAL },
|
|
} } }
|
|
eq(BattleBridge.mapBattleScene("FR_VIRIDIAN_CITY_GYM", game), SCENE.GYM,
|
|
"a gym map def hands back MAP_BATTLE_SCENE_GYM")
|
|
eq(BattleBridge.mapBattleScene("FR_PALLET_TOWN", game), SCENE.NORMAL,
|
|
"an outdoor map def hands back MAP_BATTLE_SCENE_NORMAL")
|
|
eq(BattleBridge.mapBattleScene("FR_NOT_A_MAP", game), nil,
|
|
"a map with no def has no scene")
|
|
eq(BattleBridge.mapBattleScene(nil, game), nil, "a nil map id has no scene")
|
|
|
|
local Runtime = require("src.core.game3.runtime")
|
|
local realGame = Runtime._game
|
|
Runtime._game = game
|
|
eq(BattleBridge.mapBattleScene("FR_VIRIDIAN_CITY_GYM"), SCENE.GYM,
|
|
"with no game argument it reads the running game's maps")
|
|
Runtime._game = realGame
|
|
|
|
CacheFs.readActive, CacheFs.read = realReadActive, realRead
|
|
end
|
|
|
|
finish()
|