Files
gen1recomp/tests/game3_collision_behaviors_test.lua
bryanthaboi 72a17b27dd CLOSES #2444
2026-09-24 08:39:43 -04:00

557 lines
24 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function finish()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
local MB = {
PUDDLE = 0x16,
SHALLOW_WATER = 0x17,
STRENGTH_BUTTON = 0x20,
SAND = 0x21,
ICE = 0x23,
THIN_ICE = 0x26,
CRACKED_ICE = 0x27,
HOT_SPRINGS = 0x28,
SAND_CAVE = 0x2B,
EASTWARD_CURRENT = 0x50,
WESTWARD_CURRENT = 0x51,
NORTHWARD_CURRENT = 0x52,
SOUTHWARD_CURRENT = 0x53,
SPIN_RIGHT = 0x54,
SPIN_LEFT = 0x55,
SPIN_UP = 0x56,
SPIN_DOWN = 0x57,
STOP_SPINNING = 0x58,
CYCLING_ROAD_PULL_DOWN = 0xD0,
CYCLING_ROAD_PULL_DOWN_GRASS = 0xD1,
}
local PLAIN_MID = 721
print("[test] 1. COLL bytes the classifier bakes per behavior")
local ScriptColl = require("src.core.game3.scripting.collision")
local function bake(beh, mapColl, kind)
local byte, cat = ScriptColl.fromCell(PLAIN_MID, mapColl, beh, kind or "route")
return byte, cat
end
for _, beh in ipairs({ MB.PUDDLE, MB.SHALLOW_WATER }) do
for _, kind in ipairs({ "route", "town", "indoor" }) do
local openByte, openCat = bake(beh, 0, kind)
check(openByte == 0x00,
string.format("0x%02X on %s with mapColl 0 bakes walkable ground (0x%02X %s)",
beh, kind, openByte, tostring(openCat)))
local shutByte = bake(beh, 1, kind)
check(shutByte == 0x07,
string.format("0x%02X on %s with mapColl 1 stays solid (0x%02X)", beh, kind, shutByte))
end
end
local sandByte, sandCat = bake(MB.SAND_CAVE, 0)
check(sandByte == 0x00 and sandCat == "SAND",
string.format("MB_SAND_CAVE bakes the same category as MB_SAND (0x%02X %s)",
sandByte, tostring(sandCat)))
check(select(2, bake(MB.SAND, 0)) == sandCat,
"MB_SAND and MB_SAND_CAVE share a category, as MetatileBehavior_IsSand does")
local grassByte, grassCat = bake(MB.CYCLING_ROAD_PULL_DOWN_GRASS, 0)
check(grassByte == 0x18 and grassCat == "TALL_GRASS",
string.format("MB_CYCLING_ROAD_PULL_DOWN_GRASS bakes tall grass (0x%02X %s)",
grassByte, tostring(grassCat)))
local pullByte = bake(MB.CYCLING_ROAD_PULL_DOWN, 0)
check(pullByte == 0x00,
string.format("MB_CYCLING_ROAD_PULL_DOWN stays walkable road (0x%02X)", pullByte))
for _, beh in ipairs({ 0x10, 0x11, 0x12, 0x13, 0x15, 0x1A, 0x1B,
MB.EASTWARD_CURRENT, MB.WESTWARD_CURRENT, MB.NORTHWARD_CURRENT, MB.SOUTHWARD_CURRENT }) do
local byte, cat = bake(beh, 0)
check(byte == 0x29 and cat == "WATER",
string.format("sBehaviorSurfable member 0x%02X still bakes water (0x%02X %s)",
beh, byte, tostring(cat)))
end
print("[test] 2. behavior predicates over every byte")
local Collision = require("src.core.game3.collision")
local PREDS = {
isStrengthButton = { MB.STRENGTH_BUTTON },
isIce = { MB.ICE },
isThinIce = { MB.THIN_ICE },
isCrackedIce = { MB.CRACKED_ICE },
isHotSprings = { MB.HOT_SPRINGS },
isEastwardCurrent = { MB.EASTWARD_CURRENT },
isWestwardCurrent = { MB.WESTWARD_CURRENT },
isNorthwardCurrent = { MB.NORTHWARD_CURRENT },
isSouthwardCurrent = { MB.SOUTHWARD_CURRENT },
isSpinRight = { MB.SPIN_RIGHT },
isSpinLeft = { MB.SPIN_LEFT },
isSpinUp = { MB.SPIN_UP },
isSpinDown = { MB.SPIN_DOWN },
isStopSpinning = { MB.STOP_SPINNING },
isSpinTile = { MB.SPIN_RIGHT, MB.SPIN_LEFT, MB.SPIN_UP, MB.SPIN_DOWN },
isCyclingRoadPullDown = { MB.CYCLING_ROAD_PULL_DOWN, MB.CYCLING_ROAD_PULL_DOWN_GRASS },
isCyclingRoadPullDownGrass = { MB.CYCLING_ROAD_PULL_DOWN_GRASS },
}
local names = {}
for name in pairs(PREDS) do names[#names + 1] = name end
table.sort(names)
for _, name in ipairs(names) do
local fn = Collision[name]
if type(fn) ~= "function" then
check(false, "Collision." .. name .. " exists")
else
local set = {}
for _, beh in ipairs(PREDS[name]) do set[beh] = true end
local wrong = 0
for beh = 0x00, 0xFF do
if (fn(beh) and true or false) ~= (set[beh] == true) then wrong = wrong + 1 end
end
check(wrong == 0, name .. " matches pret over every behavior byte, wrong=" .. wrong)
check(fn(nil) ~= true, name .. "(nil) is false")
end
end
check(Collision.isSurfable(MB.PUDDLE) == false,
"MB_PUDDLE is not in sBehaviorSurfable")
check(Collision.isSurfable(MB.SHALLOW_WATER) == false,
"MB_SHALLOW_WATER is not in sBehaviorSurfable")
check(Collision.isSurfable(0x11) == true, "MB_FAST_WATER is still surfable")
print("[test] 2b. land elevation mismatch on a fake layout")
do
local W, H = 5, 4
local ELEV = {
3, 3, 3, 3, 3,
4, 4, 4, 4, 0,
3, 3, 3, 3, 3,
1, 1, 4, 3, 1,
}
local COLL = {}
for i = 1, W * H do COLL[i] = 0x00 end
COLL[3 * W + 1], COLL[3 * W + 2], COLL[3 * W + 5] = 0x29, 0x29, 0x29
local layout = { width = W, height = H }
function layout:collArray() return COLL end
function layout:collAt(x, y) return COLL[y * W + x + 1] end
function layout:elevAt(x, y) return ELEV[y * W + x + 1] end
function layout:midAt() return 0 end
local fakeDef = { midLayout = layout, warps = {} }
local fakeGame = { data = { maps = {} } }
Collision.bindMap(fakeGame, "FAKE_ELEVATION", fakeDef)
local function try(fx, fy, tx, ty, elev, surfing)
return Collision.canEnter(fakeGame, tx, ty,
{ fromX = fx, fromY = fy, elevation = elev, surfing = surfing or false })
end
local ok, why = try(1, 0, 1, 1, 3)
check(ok == false and why == "elevation",
"elev 3 -> elev 4 on two collision-0 cells is refused (" .. tostring(why) .. ")")
ok, why = try(1, 1, 1, 0, 4)
check(ok == false and why == "elevation",
"elev 4 -> elev 3 back off the plateau is refused (" .. tostring(why) .. ")")
check(try(1, 1, 2, 1, 4) == true, "walking along the plateau at elev 4 is allowed")
check(try(3, 1, 4, 1, 4) == true, "the plateau steps onto the elev 0 stair")
check(try(4, 1, 4, 0, 0) == true, "from the stair (current elev 0) up to elev 3")
check(try(4, 1, 4, 2, 0) == true, "from the stair (current elev 0) down to elev 3")
check(try(1, 0, 1, 1, nil) == true, "no elevation passed keeps the old callers unchanged")
check(try(1, 0, 1, 1, 0) == true, "current elevation 0 never mismatches")
check(try(1, 3, 1, 2, 1, true) == true, "surfing elev 1 onto elev 3 land dismounts")
ok, why = try(1, 3, 2, 3, 1, true)
check(ok == false and why == "elevation",
"surfing elev 1 onto elev 4 land is refused (" .. tostring(why) .. ")")
check(try(0, 3, 1, 3, 1, true) == true, "surfing along elev 1 water")
check(Collision.elevationMismatchOn(fakeDef, 3, 1, 1) == true, "elevationMismatchOn 3 vs 4")
check(Collision.elevationMismatchOn(fakeDef, 3, 4, 1) == false, "elevationMismatchOn map elev 0")
local Player = require("src.core.game3.player")
Player.reset(1, 1, "down")
check(Player.currentElevation == 0, "a spawn starts at currentElevation 0 like the cart")
Player.updateElevation(1, 1)
check(Player.currentElevation == 4 and Player.elevation == 4,
"the next update derives elev 4 from the plateau cell")
Player.cellX, Player.cellY = 3, 1
Player.updateElevation(4, 1, 3, 1)
check(Player.currentElevation == 0 and Player.elevation == 4,
"stepping onto the stair drops currentElevation to 0 and keeps previousElevation 4")
Player.cellX, Player.cellY = 4, 1
Player.updateElevation(4, 2, 4, 1)
check(Player.currentElevation == 3 and Player.elevation == 3,
"stepping off the stair picks up elev 3")
Player.reset(4, 1, "down")
Player.updateElevation(4, 1)
check(Player.currentElevation == 0, "a warp or load onto the stair leaves the player free (elev 0)")
local Objects = require("src.core.game3.objects")
local eo = { cellX = 1, cellY = 1, targetX = 1, targetY = 1, moving = false, currentElevation = 0 }
Objects.updateElevation(eo)
check(eo.currentElevation == 4, "an object on the plateau derives currentElevation 4")
eo.mapDef = fakeDef
eo.cellX, eo.cellY, eo.moving = 1, 2, false
Objects.updateElevation(eo)
check(eo.currentElevation == 3, "an object reads its own mapDef elevation")
print("[test] 2c. forced movement, waterfall, objects, trainer sight and ledges honor elevation")
local FM = require("src.core.game3.forced_movement")
local function forced(x, y, facing, surfing, elev, beh)
Player.reset(x, y, facing)
Player.surfing = surfing
Player.currentElevation = elev
local moved = FM.tryDoMetatileBehaviorForcedMovement(fakeGame, beh)
local tx, ty = Player.targetX, Player.targetY
Player.moving, Player.surfing, Player.dismounting = false, false, false
FM.forced = false
return moved, tx, ty
end
local moved, tx, ty = forced(1, 0, "down", false, 3, MB.SPIN_DOWN)
check(not moved and tx == 1 and ty == 0, "SpinDown from elev 3 into elev 4 is refused")
moved, tx, ty = forced(1, 2, "right", false, 3, MB.SPIN_RIGHT)
check(moved and tx == 2 and ty == 2, "SpinRight along elev 3 still moves")
moved, tx, ty = forced(1, 3, "right", true, 1, MB.EASTWARD_CURRENT)
check(not moved and tx == 1 and ty == 3, "an east current does not push a surfer onto elev 4 land")
moved, tx, ty = forced(4, 3, "left", true, 1, MB.WESTWARD_CURRENT)
check(moved and tx == 3 and ty == 3, "a west current still lands a surfer on elev 3 land")
moved, tx, ty = forced(0, 3, "right", true, 1, MB.EASTWARD_CURRENT)
check(moved and tx == 1 and ty == 3, "an east current still carries a surfer along elev 1 water")
local Field = require("src.core.game3.field")
local FieldMoves = require("src.core.game3.field_moves")
local realCanEnter, realWf, realWfFlag = Collision.canEnter, FieldMoves.isWaterfallBehavior, Field._waterfall
local seen
Collision.canEnter = function(_, _, _, opts) seen = opts return true end
FieldMoves.isWaterfallBehavior = function() return true end
Field._waterfall = nil
Player.reset(0, 2, "down")
Player.surfing = true
Player.currentElevation = 1
Field.forcedMovementPending()
Collision.canEnter, FieldMoves.isWaterfallBehavior, Field._waterfall = realCanEnter, realWf, realWfFlag
Player.surfing = false
check(seen and seen.elevation == 1, "the waterfall push passes the surfer's elevation")
Objects._mapId = "FAKE_ELEVATION"
Objects._byId, Objects._order = {}, {}
local function npc(lid, x, y, elev)
local o = { localId = lid, cellX = x, cellY = y, targetX = x, targetY = y, moving = false,
visible = true, hidden = false, passable = false, currentElevation = elev }
Objects._byId[lid] = o
Objects._order[#Objects._order + 1] = lid
return o
end
local block = npc(1, 3, 2, 4)
check(Objects.blocks(3, 2, nil, 3) == false, "an elev 4 object does not block an elev 3 mover")
check(Objects.blocks(3, 2, nil, 4) == true, "an elev 4 object blocks an elev 4 mover")
check(Objects.blocks(3, 2, nil, 0) == true, "elev 0 movers collide with everything")
check(Objects.blocks(3, 2) == true, "no elevation keeps the old callers unchanged")
block.currentElevation = 0
check(Objects.blocks(3, 2, nil, 3) == true, "an elev 0 object collides with everything")
Player.reset(2, 2, "right")
Player.currentElevation = 4
check(Objects.playerBlocks(2, 2, 3) == false, "an elev 3 object walks through an elev 4 player")
check(Objects.playerBlocks(2, 2, 4) == true, "an elev 4 object bumps an elev 4 player")
check(Objects.playerBlocks(2, 2) == true, "playerBlocks with no elevation is unchanged")
block.currentElevation = 4
check(try(2, 2, 3, 2, 3) == true, "the player walks past an object on another elevation")
local ok2, why2 = try(2, 2, 3, 2, nil)
check(ok2 == false and why2 == "entity", "canEnter without elevation still stops at the object")
Objects._byId, Objects._order = {}, {}
npc(2, 1, 2, 3)
ok2, why2 = try(1, 3, 1, 2, 1, true)
check(ok2 == false and why2 == "entity", "a surfer cannot dismount onto an elev 3 object")
Objects._byId[2].currentElevation = 4
check(try(1, 3, 1, 2, 1, true) == true, "a surfer dismounts past an elev 4 object like GetObjectEventIdByPosition(x, y, 3)")
local TrainerSight = require("src.core.game3.trainer_sight")
Objects._byId, Objects._order = {}, {}
local trainer = { localId = 9, cellX = 1, cellY = 0, facing = "down", sight = 4,
currentElevation = 3, visible = true, hidden = false, moving = false }
Player.reset(1, 2, "up")
Player.currentElevation = 3
local spotted = TrainerSight.checkLineOfSight(trainer, Player, fakeGame)
check(spotted == false, "a trainer does not see across an elev 4 cell")
trainer.cellX, trainer.cellY, trainer.facing = 0, 2, "right"
Player.reset(3, 2, "left")
Player.currentElevation = 3
local dist
spotted, dist = TrainerSight.checkLineOfSight(trainer, Player, fakeGame)
check(spotted == true and dist == 3, "a trainer sees along its own elevation")
npc(3, 1, 2, 4)
check(TrainerSight.checkLineOfSight(trainer, Player, fakeGame) == true,
"an object on another elevation does not block sight")
Objects._byId[3].currentElevation = 3
check(TrainerSight.checkLineOfSight(trainer, Player, fakeGame) == false,
"an object on the same elevation blocks sight")
Objects._byId, Objects._order = {}, {}
Player.currentElevation = 4
check(TrainerSight.checkLineOfSight(trainer, Player, fakeGame) == false,
"an elev 3 trainer does not see an elev 4 player")
trainer.cellX, trainer.cellY, trainer.facing = 4, 0, "down"
Player.reset(4, 1, "up")
Player.currentElevation, Player.elevation = 0, 4
spotted, dist = TrainerSight.checkLineOfSight(trainer, Player, fakeGame)
check(spotted == true and dist == 1, "a player on an elev 0 stair is seen from elev 3")
trainer.cellX, trainer.cellY, trainer.facing = 1, 2, "down"
Player.reset(1, 3, "up")
Player.surfing, Player.currentElevation = true, 1
check(TrainerSight.checkLineOfSight(trainer, Player, fakeGame) == false,
"a land trainer does not see a surfer on elev 1 water")
Player.surfing = false
COLL[1 * W + 2] = 0xA3
npc(4, 1, 2, 3)
local lx, ly = Collision.ledgeLanding(fakeGame, 1, 0, "down")
check(lx == 1 and ly == 2, "a ledge jump lands on an occupied cell like ShouldJumpLedge")
COLL[2 * W + 3] = 0x07
COLL[1 * W + 3] = 0xA3
lx, ly = Collision.ledgeLanding(fakeGame, 2, 0, "down")
check(lx == 2 and ly == 2, "a ledge jump does not test the landing tile")
check(Collision.ledgeLanding(fakeGame, 2, 1, "down") == nil, "a wall is not a ledge")
COLL[1 * W + 2], COLL[1 * W + 3], COLL[2 * W + 3] = 0x00, 0x00, 0x00
Objects.clear()
Collision.clear()
end
print("[test] 3. the imported cache")
local Cache = require("tests.game3_cache")
local cacheRoot = Cache.mount("scripts/events.lua", { native = true })
if not cacheRoot then
print("[skip] game3_collision_behaviors_test map checks: " .. tostring(Cache.reason))
finish()
end
print("[info] FireRed cache at " .. cacheRoot)
local Dataset = require("src.core.game3.dataset")
local game = { data = {} }
Dataset.hydrate(game)
local function bind(mapId)
local def = game.data.maps[mapId]
if not def then
check(false, mapId .. " is in the cache")
return false
end
Collision.bindMap(game, mapId, def)
return true
end
local function countBeh(mapId, beh)
if not bind(mapId) then return nil end
local n = 0
for y = 0, Collision._heightCells - 1 do
for x = 0, Collision._widthCells - 1 do
if Collision.behavior(x, y) == beh then n = n + 1 end
end
end
return n
end
local CELL_COUNTS = {
{ "FR_FOUR_ISLAND", MB.PUDDLE, 28 },
{ "FR_SILPH_CO_1F", MB.PUDDLE, 88 },
{ "FR_ROUTE_21_NORTH", MB.SHALLOW_WATER, 41 },
{ "SEVII_ONE_ISLAND_KINDLE_ROAD", MB.SHALLOW_WATER, 104 },
{ "FR_VICTORY_ROAD_1F", MB.STRENGTH_BUTTON, 1 },
{ "FR_VICTORY_ROAD_2F", MB.STRENGTH_BUTTON, 2 },
{ "FR_FOUR_ISLAND_ICEFALL_CAVE_B1F", MB.ICE, 70 },
{ "FR_FOUR_ISLAND_ICEFALL_CAVE_1F", MB.THIN_ICE, 9 },
{ "FR_ONE_ISLAND_KINDLE_ROAD_EMBER_SPA", MB.HOT_SPRINGS, 37 },
{ "FR_MT_MOON_1F", MB.SAND_CAVE, 143 },
{ "FR_SEAFOAM_ISLANDS_B4F", MB.EASTWARD_CURRENT, 31 },
{ "FR_SEAFOAM_ISLANDS_B4F", MB.NORTHWARD_CURRENT, 142 },
{ "FR_SEAFOAM_ISLANDS_B3F", MB.SOUTHWARD_CURRENT, 45 },
{ "FR_ROCKET_HIDEOUT_B2F", MB.SPIN_RIGHT, 9 },
{ "FR_ROCKET_HIDEOUT_B2F", MB.SPIN_LEFT, 12 },
{ "FR_ROCKET_HIDEOUT_B2F", MB.SPIN_UP, 15 },
{ "FR_ROCKET_HIDEOUT_B2F", MB.SPIN_DOWN, 6 },
{ "FR_ROCKET_HIDEOUT_B2F", MB.STOP_SPINNING, 12 },
{ "FR_ROUTE_17", MB.CYCLING_ROAD_PULL_DOWN, 2041 },
{ "FR_ROUTE_17", MB.CYCLING_ROAD_PULL_DOWN_GRASS, 66 },
}
for _, row in ipairs(CELL_COUNTS) do
local mapId, beh, want = row[1], row[2], row[3]
local got = countBeh(mapId, beh)
check(got == want,
string.format("%s carries %d cells of behavior 0x%02X, got %s",
mapId, want, beh, tostring(got)))
end
if bind("FR_FOUR_ISLAND") and Collision.cell(12, 6) == 0x29 then
print("[skip] the mounted cache was baked before the classifier change; "
.. "re-import to run the walkability checks")
finish()
end
print("[test] 4. shallow water and puddles are walked on foot")
if bind("FR_FOUR_ISLAND") then
check(Collision.behavior(12, 6) == MB.PUDDLE, "FR_FOUR_ISLAND (12,6) is MB_PUDDLE")
check(Collision.isWater(12, 6) == false, "the puddle is not surf water")
check(Collision.canEnter(game, 12, 6, { fromX = 12, fromY = 5, dir = "down" }) == true,
"the player walks into the Four Island puddle on foot")
check(Collision.canEnter(game, 13, 6, { fromX = 12, fromY = 6, dir = "right" }) == true,
"and walks along it")
end
if bind("FR_ROUTE_21_NORTH") then
check(Collision.behavior(15, 23) == MB.SHALLOW_WATER,
"FR_ROUTE_21_NORTH (15,23) is MB_SHALLOW_WATER")
check(Collision.isWater(15, 23) == false, "shallow water is not surf water")
check(Collision.isWalkable(15, 23) == true, "shallow water is walkable")
end
if bind("FR_SILPH_CO_1F") then
check(Collision.behavior(13, 8) == MB.PUDDLE, "FR_SILPH_CO_1F (13,8) is MB_PUDDLE")
check(Collision.cell(13, 8) == 0x07,
"the Silph lobby puddle keeps its map collision and stays solid")
local ok, why = Collision.canEnter(game, 13, 8, { fromX = 13, fromY = 9, dir = "up" })
check(ok == false and why == "tile",
"walking into the Silph lobby puddle is refused (" .. tostring(why) .. ")")
end
-- pokefirered/src/field_player_avatar.c:597 CanStopSurfing
local SURF_CHANNEL = {
{ "FR_SEAFOAM_ISLANDS_B3F", 23, 8 }, { "FR_SEAFOAM_ISLANDS_B3F", 24, 8 },
{ "FR_SEAFOAM_ISLANDS_B4F", 8, 18 }, { "FR_SEAFOAM_ISLANDS_B4F", 9, 18 },
}
for _, row in ipairs(SURF_CHANNEL) do
local mapId, x, y = row[1], row[2], row[3]
if bind(mapId) then
check(Collision.behavior(x, y) == MB.SHALLOW_WATER,
string.format("%s (%d,%d) is MB_SHALLOW_WATER", mapId, x, y))
check(Collision.elevationAt(x, y) == 1,
string.format("%s (%d,%d) sits at elevation 1, inside the current channel",
mapId, x, y))
check(Collision.isWater(x, y) == true,
string.format("%s (%d,%d) stays surf water, so the surfer does not dismount",
mapId, x, y))
check(Collision.canEnter(game, x, y, { surfing = false }) == false,
string.format("%s (%d,%d) refuses a step on foot", mapId, x, y))
end
end
print("[test] 5. sand caves, hot springs and the cycling road")
if bind("FR_MT_MOON_1F") then
check(Collision.behavior(2, 2) == MB.SAND_CAVE, "FR_MT_MOON_1F (2,2) is MB_SAND_CAVE")
check(Collision.isWalkable(2, 2) == true, "the Mt Moon sand floor is walkable")
end
if bind("FR_ONE_ISLAND_KINDLE_ROAD_EMBER_SPA") then
check(Collision.behavior(11, 11) == MB.HOT_SPRINGS, "Ember Spa (11,11) is MB_HOT_SPRINGS")
check(Collision.isWalkable(11, 11) == true, "the Ember Spa pool is waded into")
check(Collision.behavior(5, 4) == MB.HOT_SPRINGS, "Ember Spa (5,4) is MB_HOT_SPRINGS")
check(Collision.isWalkable(5, 4) == false, "the walled-off spring cells stay solid")
end
if bind("FR_ROUTE_17") then
check(Collision.behavior(15, 11) == MB.CYCLING_ROAD_PULL_DOWN_GRASS,
"FR_ROUTE_17 (15,11) is MB_CYCLING_ROAD_PULL_DOWN_GRASS")
check(Collision.isGrass(15, 11) == true,
"the cycling road grass is still a wild encounter tile")
check(Collision.behavior(2, 0) == MB.CYCLING_ROAD_PULL_DOWN,
"FR_ROUTE_17 (2,0) is MB_CYCLING_ROAD_PULL_DOWN")
check(Collision.isGrass(2, 0) == false, "the plain cycling road is not grass")
check(Collision.isWalkable(2, 0) == true, "the plain cycling road is walkable")
end
print("[test] 6. the predicates see the real map cells")
local MAP_PREDS = {
{ "FR_VICTORY_ROAD_1F", 20, 16, "isStrengthButton" },
{ "FR_FOUR_ISLAND_ICEFALL_CAVE_B1F", 16, 2, "isIce" },
{ "FR_FOUR_ISLAND_ICEFALL_CAVE_1F", 8, 3, "isThinIce" },
{ "FR_ONE_ISLAND_KINDLE_ROAD_EMBER_SPA", 11, 11, "isHotSprings" },
{ "FR_SEAFOAM_ISLANDS_B3F", 15, 5, "isSouthwardCurrent" },
{ "FR_SEAFOAM_ISLANDS_B4F", 3, 1, "isNorthwardCurrent" },
{ "FR_ROCKET_HIDEOUT_B2F", 3, 6, "isSpinRight" },
{ "FR_ROCKET_HIDEOUT_B2F", 1, 4, "isStopSpinning" },
{ "FR_ROUTE_17", 2, 0, "isCyclingRoadPullDown" },
}
for _, row in ipairs(MAP_PREDS) do
local mapId, x, y, name = row[1], row[2], row[3], row[4]
if bind(mapId) then
local beh = Collision.behavior(x, y)
check(Collision[name](beh) == true,
string.format("%s (%d,%d) behavior 0x%02X answers %s", mapId, x, y, beh or 0, name))
end
end
print("[test] 7. raised platforms are fenced by elevation")
local ELEV_EDGES = {
{ "FR_SAFARI_ZONE_EAST", 37, 16, 37, 17 },
{ "FR_SAFARI_ZONE_WEST", 19, 11, 19, 12 },
{ "FR_SAFARI_ZONE_NORTH", 40, 14, 40, 15 },
}
for _, row in ipairs(ELEV_EDGES) do
local mapId, fx, fy, tx, ty = row[1], row[2], row[3], row[4], row[5]
if bind(mapId) then
local fe, te = Collision.elevationAt(fx, fy), Collision.elevationAt(tx, ty)
check(fe == 3 and te == 4 and Collision.isWalkable(tx, ty),
string.format("%s (%d,%d) elev %s and (%d,%d) elev %s are both open ground",
mapId, fx, fy, tostring(fe), tx, ty, tostring(te)))
local ok, why = Collision.canEnter(game, tx, ty, { fromX = fx, fromY = fy, elevation = fe })
check(ok == false and why == "elevation",
string.format("%s up onto the plateau is refused (%s)", mapId, tostring(why)))
ok, why = Collision.canEnter(game, fx, fy, { fromX = tx, fromY = ty, elevation = te })
check(ok == false and why == "elevation",
string.format("%s down off the plateau is refused (%s)", mapId, tostring(why)))
end
end
if bind("FR_SAFARI_ZONE_EAST") then
check(Collision.elevationAt(40, 19) == 0, "Safari East (40,19) is an elevation 0 stair")
check(Collision.canEnter(game, 40, 19, { fromX = 40, fromY = 18, elevation = 4 }) == true,
"the plateau steps down onto the stair")
check(Collision.canEnter(game, 40, 20, { fromX = 40, fromY = 19, elevation = 0 }) == true,
"the stair steps down onto the sand")
end
local sweepMaps, sweepEdges, sweepWrong = 0, 0, 0
local ids = {}
for mapId in pairs(game.data.maps) do ids[#ids + 1] = mapId end
table.sort(ids)
for _, mapId in ipairs(ids) do
local def = game.data.maps[mapId]
if def.midLayout and def.midLayout.elevAt then
Collision.bindMap(game, mapId, def)
local found = false
for y = 0, Collision._heightCells - 1 do
for x = 0, Collision._widthCells - 1 do
local a = Collision.elevationAt(x, y)
for _, d in ipairs({ { 1, 0 }, { 0, 1 } }) do
local nx, ny = x + d[1], y + d[2]
local b = Collision.elevationAt(nx, ny)
if a and b and a ~= b and a ~= 0 and b ~= 0 and a ~= 15 and b ~= 15
and a ~= 1 and b ~= 1
and Collision.isWalkable(x, y) and Collision.isWalkable(nx, ny)
and not Collision.isWater(x, y) and not Collision.isWater(nx, ny) then
found = true
sweepEdges = sweepEdges + 1
if Collision.canEnter(game, nx, ny, { fromX = x, fromY = y, elevation = a }) ~= false
or Collision.canEnter(game, x, y, { fromX = nx, fromY = ny, elevation = b }) ~= false then
sweepWrong = sweepWrong + 1
if sweepWrong <= 5 then
print(string.format("[info] open elevation edge %s (%d,%d)<->(%d,%d)", mapId, x, y, nx, ny))
end
end
end
end
end
end
if found then sweepMaps = sweepMaps + 1 end
end
end
check(sweepEdges > 200 and sweepMaps >= 15,
string.format("the sweep finds the FRLG elevation edges (%d edges on %d maps)", sweepEdges, sweepMaps))
check(sweepWrong == 0, "every land elevation edge refuses both directions, open=" .. sweepWrong)
finish()