mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
557 lines
24 KiB
Lua
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()
|