-- Current tiles: DoPlayerMovement's .CheckTile, HI_NYBBLE_CURRENT arm. -- -- luajit tests/gen2_currents_test.lua -- -- $30-$3f are CURRENT tiles (engine/overworld/player_movement.asm): the low two -- bits index .water_table, so COLL_WATERFALL $33 and COLL_CURRENT_DOWN $3b both -- force DOWN. The arm runs ABOVE .CheckTurning and .TryStep and returns -- PLAYERMOVEMENT_CONTINUE, so it overrides the d-pad outright rather than being -- refused by it. -- -- The port mapped the whole $3x block to plain WATER and had no current arm at -- all, which cost two things: the plunge down a waterfall needed the player to -- hold DOWN (and never animated as a plunge), and a surfing player could walk -- UP a waterfall column with the d-pad -- silently bypassing the HM07 + -- ENGINE_RISINGBADGE gate at Tohjo Falls and Whirl Islands. package.path = "./?.lua;./?/init.lua;" .. package.path local S = require("tests.harness").suite("gen2 currents") local check, eq = S.check, S.eq local Permissions = require("src.world.gen2.Permissions") local World = require("src.world.gen2.World") local Player = require("src.world.gen2.Player") local FieldMoves = require("src.world.gen2.FieldMoves") -- ---- the table ------------------------------------------------------------- -- -- `and $f0 / cp HI_NYBBLE_CURRENT` then `maskbits NUM_DIRECTIONS`. eq(Permissions.currentDirection(0x30), "right", "COLL_WATERFALL_RIGHT") eq(Permissions.currentDirection(0x31), "left", "COLL_WATERFALL_LEFT") eq(Permissions.currentDirection(0x32), "up", "COLL_WATERFALL_UP") eq(Permissions.currentDirection(0x33), "down", "COLL_WATERFALL is DOWN") eq(Permissions.currentDirection(0x38), "right", "COLL_CURRENT_RIGHT") eq(Permissions.currentDirection(0x39), "left", "COLL_CURRENT_LEFT") eq(Permissions.currentDirection(0x3a), "up", "COLL_CURRENT_UP") eq(Permissions.currentDirection(0x3b), "down", "COLL_CURRENT_DOWN is DOWN too") -- The nybble is what is tested, not the four named constants, so the garbage -- rows in between behave the same way the cart's would. eq(Permissions.currentDirection(0x36), "up", "COLL_36 rides the same nybble") eq(Permissions.currentDirection(0x29), nil, "plain COLL_WATER forces nothing") eq(Permissions.currentDirection(0x23), nil, "and neither does ice") eq(Permissions.currentDirection(0x24), nil, "COLL_WHIRLPOOL is tested above the nybble and takes FORCE_TURN instead") eq(Permissions.currentDirection(nil), nil, "no tile, no current") check(Permissions.isWater(0x33), "a waterfall tile is still WATER to surf on") check(Permissions.isWaterfall(0x33) and Permissions.isWaterfall(0x3b), "CheckWaterfallTile still pairs the two the climb loops on") -- ---- the step -------------------------------------------------------------- -- -- A column of $33 down the middle of a pond: (5,4) is the top of the fall, -- (5,5) and (5,6) below it, plain water everywhere else. local COLL_WATER, COLL_WATERFALL = 0x29, 0x33 local FALLS = { [4 * 100 + 5] = COLL_WATERFALL, [5 * 100 + 5] = COLL_WATERFALL, [6 * 100 + 5] = COLL_WATERFALL, } local function currentWorld(px, py) local game = { data = { items = {}, moves = {}, pokemon = {} }, save = { player = { name = "GOLD", badges = {} }, party = {}, inventory = {} }, } local world = World.new(game) game.world = world local cells = {} for y = 0, 9 do for x = 0, 9 do cells[y * 100 + x] = COLL_WATER end end for key, value in pairs(FALLS) do cells[key] = value end local map map = { id = "TOHJO_FALLS", width = 5, height = 5, def = { objects = {}, bgEvents = {}, environment = "CAVE", tileset = "TILESET_CAVE", width = 5, height = 5 }, cellCollision = function(_, x, y) return cells[y * 100 + x] or COLL_WATER end, inBounds = function(_, x, y) return x >= 0 and y >= 0 and x < 10 and y < 10 end, isWalkable = function(_, x, y) return Permissions.isWalkable(map:cellCollision(x, y)) end, warpAt = function() return nil end, } world.map = map world.maps = { TOHJO_FALLS = map.def } world.player = Player.new(px, py, "up") world.entities = { world.player } world.npcs = {} world.playerState = FieldMoves.PLAYER_SURF world.encounters = {} world.pollTimeOfDay = function() end -- Nothing here draws, and nothing here is about the wild roll. world.noWildEncounters = true world.updatePeople = function() end return world, game end -- World:pollInput is the caller's job (Game2 does it once a fixed step); -- `held` is the d-pad the arm is supposed to be overriding. local function runSteps(world, frames, held) for _ = 1, frames do world.heldDir = held world:step() end end -- A surfing player standing on the top of the fall is carried DOWN with no -- input at all: `.CheckTile` picks the direction before .CheckTurning ever -- looks at the d-pad. do local world = currentWorld(5, 4) eq(world.heldDir, nil, "no direction is held") runSteps(world, 64) eq(world.player.cellX, 5, "the plunge stays in the column") check(world.player.cellY > 4, "and the current carried the player down it with no press") check(world.player.cellY >= 7, "past the last waterfall tile and out onto open water") end -- The same tile refuses to be climbed: holding UP on a current tile is not a -- bump, it is a DOWN step. do local world = currentWorld(5, 5) runSteps(world, 48, "up") check(world.player.cellY > 5, "holding UP inside the column still moves the player DOWN") eq(world.player.cellX, 5, "and never off the column") end -- Which is what the HM07 gate at Tohjo Falls and Whirl Islands rests on: a -- surfing player below the fall can step onto its bottom tile and is thrown -- straight back off it, so the top is unreachable however long UP is held. do local world = currentWorld(5, 8) local highest = world.player.cellY for _ = 1, 300 do world.heldDir = "up" world:step() if world.player.cellY < highest then highest = world.player.cellY end end check(highest >= 6, "the d-pad never carries a surfing player above the fall's bottom tile") check(world.player.cellY >= 6, "and it ends below it too") end -- Off the column it is an ordinary surf step again, so the current is a -- property of the TILE and not a mode the player gets stuck in. do local world = currentWorld(3, 6) runSteps(world, 48, "up") check(world.player.cellY < 6, "plain water still answers the d-pad") end -- The scripted climb is untouched: it moves the player with Player:scriptStep -- under World:busy, which returns above the input block this arm lives in. do local world = currentWorld(5, 7) world.fieldMove = { phase = "waterfall" } local before = world.player.cellY world:waterfallStep() runSteps(world, 20) check(world.player.cellY < before, "World:waterfallStep still climbs while the field move owns the world") end S.finish()