-- Headless tests for the save editor's map browser behaviour. -- Run from repo root: luajit tests/save_editor_task8_tests.lua -- (also chained from tests/run_save_editor_tests.lua so CI covers it) -- -- The spawn-point writes and the outdoor rule live in tools/save-editor/Ops.lua -- and are asserted there; the panel-owned bits still tested here are the ones -- with no other home: map selection, keyboard panning and the zoom clamp. package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua" .. ";./tools/save-editor/panels/?.lua" local love_stub = require("tests.love_stub") love = love_stub local passed, failed = 0, 0 local function check(cond, msg) if cond then passed = passed + 1 else failed = failed + 1 print("FAIL: " .. msg) end end local function eq(a, b, msg) check(a == b, msg .. string.format(" (got %s, want %s)", tostring(a), tostring(b))) end print("== save editor task 8 (map browser) tests ==") local Data = require("src.core.Data") Data:load() local SaveData = require("src.core.SaveData") local State = require("State") local Ops = require("Ops") local MapLoader = require("src.world.MapLoader") local MapBrowser = require("MapBrowser") local function newState() local S = State.new() S.data = Data S.save = SaveData.newGame() S.mapId = S.save.player.map -- PALLET_TOWN return S end -- outdoor detection --------------------------------------------------- do local S = newState() local pallet = MapLoader.load(Data, "PALLET_TOWN") check(Ops.isOutdoor(S, pallet), "PALLET_TOWN is outdoor (OVERWORLD tileset)") local indoor for id in pairs(Data.maps) do local ok, map = pcall(MapLoader.load, Data, id) if ok and not Ops.isOutdoor(S, map) then indoor = map break end end check(indoor ~= nil, "the dataset has at least one indoor map") -- a visited fly spot counts as outdoor even when the tileset does not if indoor then S.save.visited = S.save.visited or {} S.save.visited[indoor.id] = true check(Ops.isOutdoor(S, indoor), "a visited map counts as outdoor even with an indoor tileset") S.save.visited[indoor.id] = nil end end -- spawn points -------------------------------------------------------- do local S = newState() S.mapId = "VIRIDIAN_CITY" S.mapClickCell = nil S.dirty = false check(Ops.setPlayerHere(S) == false, "setPlayerHere refuses with no cell selected") check(S.dirty == false, "a refused setPlayerHere does not dirty the save") check(S.status:match("Click a cell first") ~= nil, "a refused setPlayerHere explains itself") S.mapClickCell = { cx = 7, cy = 9 } check(Ops.setPlayerHere(S) == true, "setPlayerHere writes with a cell selected") eq(S.save.player.map, "VIRIDIAN_CITY", "setPlayerHere moves the player's map") eq(S.save.player.x, 7, "setPlayerHere moves the player's x") eq(S.save.player.y, 9, "setPlayerHere moves the player's y") check(S.dirty, "setPlayerHere dirties the save") check(Ops.setLastHeal(S) == true, "setLastHeal writes with a cell selected") eq(S.save.lastHeal.map, "VIRIDIAN_CITY", "setLastHeal records the map") eq(S.save.lastHeal.x, 7, "setLastHeal records the cell") end do -- lastOutdoor refuses a map the game would not accept as an outdoor source local S = newState() local indoor for id in pairs(Data.maps) do local ok, map = pcall(MapLoader.load, Data, id) if ok and not Ops.isOutdoor(S, map) then indoor = map break end end check(indoor ~= nil, "found an indoor map to try") S.mapId = indoor.id S.mapClickCell = { cx = 1, cy = 1 } S.dirty = false check(Ops.setLastOutdoor(S, indoor) == false, "setLastOutdoor refuses a non-outdoor map") check(S.dirty == false, "a refused setLastOutdoor does not dirty the save") check(S.status:match("outdoor") ~= nil, "a refused setLastOutdoor explains itself") local pallet = MapLoader.load(Data, "PALLET_TOWN") S.mapId = "PALLET_TOWN" check(Ops.setLastOutdoor(S, pallet) == true, "setLastOutdoor accepts an outdoor map") eq(S.save.lastOutdoor.id, "PALLET_TOWN", "setLastOutdoor records the map id") eq(S.save.lastOutdoor.x, 1, "setLastOutdoor records the cell") end -- panel-owned view state ---------------------------------------------- do local S = newState() MapBrowser.select(S, "VIRIDIAN_CITY") eq(S.mapId, "VIRIDIAN_CITY", "select switches the viewed map") eq(S.mapClickCell, nil, "select drops the previous cell selection") eq(S._mapCenteredFor, nil, "select defers centring to the next draw, which knows the viewport size") check(S.status:match("VIRIDIAN_CITY") ~= nil, "select says what it is showing") end do local S = newState() S.mapCamX, S.mapCamY = 0, 0 MapBrowser.keypressed(S, "right") eq(S.mapCamX, 16, "right pans one cell east") MapBrowser.keypressed(S, "left") eq(S.mapCamX, 0, "left pans back") MapBrowser.keypressed(S, "down") eq(S.mapCamY, 16, "down pans one cell south") MapBrowser.keypressed(S, "w") eq(S.mapCamY, 0, "WASD pans as well as the arrows") local before = S.mapCamX MapBrowser.keypressed(S, "escape") eq(S.mapCamX, before, "a non-pan key does not move the camera") end do local S = newState() S.mapZoom = 2 MapBrowser.wheelmoved(S, 1) check(S.mapZoom > 2, "wheel up zooms in") for _ = 1, 40 do MapBrowser.wheelmoved(S, 1) end eq(S.mapZoom, 4, "zoom clamps at 4x") for _ = 1, 80 do MapBrowser.wheelmoved(S, -1) end eq(S.mapZoom, 0.03125, "zoom can show large maps on a phone") end do local S = newState() S._mapViewW, S._mapViewH = 280, 160 MapBrowser.fit(S, { widthCells = 10, heightCells = 16 }) check(S.mapZoom * 160 <= 280 and S.mapZoom * 256 <= 160, "Fit shows the whole indoor map") check(S.mapAutoFit, "Fit keeps the map fitted after resizing") local cx = S.mapCamX + S._mapViewW / (2 * S.mapZoom) local cy = S.mapCamY + S._mapViewH / (2 * S.mapZoom) MapBrowser.wheelmoved(S, 1) eq(S.mapCamX + S._mapViewW / (2 * S.mapZoom), cx, "zoom keeps the viewed center fixed horizontally") eq(S.mapCamY + S._mapViewH / (2 * S.mapZoom), cy, "zoom keeps the viewed center fixed vertically") check(not S.mapAutoFit, "manual zoom leaves Fit mode") local zoom = S.mapZoom MapBrowser.wheelmoved(S, 0) eq(S.mapZoom, zoom, "zero wheel motion does not change zoom") end do local S = newState() S.tab, S.mapSection, S._mapStacked = "map", "view", true S.mapZoom, S.mapCamX, S.mapCamY = 1, 10, 20 S._mapViewRect = { x = 20, y = 100, w = 280, h = 300 } check(not MapBrowser.touchpressed(S, "outside", 10, 90), "pinch ignores touches outside the map") check(not MapBrowser.touchpressed(S, "first", 100, 200), "first finger starts a normal pan") check(MapBrowser.touchpressed(S, "second", 200, 200), "second finger starts a pinch") check(MapBrowser.touchmoved(S, "second", 300, 200), "pinch movement is handled") eq(S.mapZoom, 2, "doubling the finger distance doubles zoom") eq(S.mapCamX + (200 - 20) / S.mapZoom, 140, "pinch keeps its map point under the midpoint") eq(S.mapCamY + (200 - 100) / S.mapZoom, 120, "pinch preserves its vertical anchor") check(MapBrowser.touchreleased(S, "second"), "lifting a finger ends the pinch") check(S._mapPinch == nil and S._mapTouches.first ~= nil, "remaining finger can keep panning") MapBrowser.clearTouches(S) check(S._mapTouches == nil, "navigation clears map gestures") S.navPopup = {} check(not MapBrowser.touchpressed(S, "modal", 100, 200), "a popup shields map gestures") end print(string.format("save editor task 8 tests: %d passed, %d failed", passed, failed)) if failed > 0 then os.exit(1) end