#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "common/Capability.h" #include "data/DataModule.h" #include "graphics/AmbientOcclusion.h" #include "graphics/AntiAliasing.h" #include "graphics/Canvas.h" #include "graphics/DrawItem2D.h" #include "graphics/Font.h" #include "graphics/GBuffer.h" #include "graphics/GlobalIllumination.h" #include "graphics/Graphics.h" #include "graphics/Grass.h" #include "graphics/Light.h" #include "graphics/Material.h" #include "graphics/Mesh.h" #include "graphics/Outline.h" #include "graphics/Quad.h" #include "graphics/RenderControl.h" #include "graphics/ScreenSpaceReflection.h" #include "graphics/Shader.h" #include "graphics/Texture.h" #include "graphics/Volumetric.h" #include "graphics/Water.h" #include "graphics/Waterfall.h" #include "map/DualGrid.h" #include "map/Fov.h" #include "map/Map.h" #include "map/MapObject.h" #include "map/Pathfinder.h" #include "map/TileCollision.h" #include "map/TileConfig.h" #include "map/TileLayer.h" #include "map/TileProjection.h" #include "map/TileSystem.h" #include "window/Window.h" #include #include #include #include #include using namespace eve::map; namespace { class CollisionSinkMock final : public ITileCollisionSink { public: void replaceTileCollision(const void *source, const TileCollisionRect *rects, size_t count) override { layer = source; values.assign(rects, rects + count); } const void *layer = nullptr; std::vector values; }; } // namespace namespace { void hideAllTileLayers() { if (ecs::current()->getManager() == nullptr) return; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [draw] = *it; draw->visible = false; } } } // namespace TEST_CASE("map.newLayer.ecsView") { auto *mod = Map::create(); int before = mod->getLayerCount(); TileLayer *layer = mod->newLayer(4, 3, 16.f, 16.f); REQUIRE(layer != nullptr); CHECK_EQ(layer->getMapWidth(), 4); CHECK_EQ(layer->getMapHeight(), 3); CHECK_EQ(layer->getTileWidth(), 16.f); CHECK_EQ(layer->getTileHeight(), 16.f); CHECK_EQ(mod->getLayerCount(), before + 1); CHECK(layer->config()->entity == layer); } TEST_CASE("map.navigationProfile.drivesPathCollisionAndFov") { auto *mod = Map::create(); auto *layer = mod->newLayer(3, 1, 16.f, 16.f); layer->setTileNavigationProfile(7, false, 1.f, 0xff, 0xff, true, 0x20); layer->setTile(1, 0, 7); Pathfinder pathfinder(layer); Path *path = pathfinder.findPath(0, 0, 2, 0); REQUIRE(path != nullptr); CHECK_EQ(path->getLength(), 0); delete path; CHECK_EQ(mod->publishCollision(layer), 1); Fov fov(layer); fov.syncFromLayer(); CHECK(fov.isOpaque(1, 0)); } TEST_CASE("map.layer.setGetTile") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(3, 2, 32.f, 32.f); CHECK_EQ(layer->getTile(0, 0), 0); layer->setTile(1, 1, 5); CHECK_EQ(layer->getTile(1, 1), 5); layer->setTile(-1, 0, 9); // out of range no-op CHECK_EQ(layer->getTile(0, 0), 0); layer->fill(7); CHECK_EQ(layer->getTile(0, 0), 7); CHECK_EQ(layer->getTile(2, 1), 7); layer->clear(); CHECK_EQ(layer->getTile(1, 0), 0); } TEST_CASE("map.layer.resizeClears") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 2, 8.f, 8.f); layer->setTile(0, 0, 3); layer->resize(4, 1); CHECK_EQ(layer->getMapWidth(), 4); CHECK_EQ(layer->getMapHeight(), 1); CHECK_EQ(layer->getTile(0, 0), 0); } TEST_CASE("map.layer.chunkIndexAndRevision") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(96, 65, 8.f, 8.f); CHECK_EQ(layer->getChunkSize(), 32); CHECK_EQ(layer->getChunkCount(), 9); CHECK_EQ(layer->getNonEmptyChunkCount(), 0); const int revision = layer->getRevision(); layer->setTile(0, 0, 1); layer->setTile(64, 64, 2); CHECK_EQ(layer->getNonEmptyChunkCount(), 2); CHECK_GT(layer->getRevision(), revision); const int unchanged = layer->getRevision(); layer->setTile(64, 64, 2); CHECK_EQ(layer->getRevision(), unchanged); layer->setTile(0, 0, 0); CHECK_EQ(layer->getNonEmptyChunkCount(), 1); } TEST_CASE("map.layer.fillRectClipsAndPublishesOnce") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(64, 64, 8.f, 8.f); const int before = layer->getRevision(); layer->fillRect(-2, -2, 5, 5, 7); CHECK_EQ(layer->getTile(0, 0), 7); CHECK_EQ(layer->getTile(2, 2), 7); CHECK_EQ(layer->getTile(3, 3), 0); CHECK_EQ(layer->getNonEmptyChunkCount(), 1); CHECK_EQ(layer->getRevision(), before + 1); layer->fillRect(31, 31, 2, 2, 8); CHECK_EQ(layer->getNonEmptyChunkCount(), 4); } TEST_CASE("map.render.sparseChunksBoundCollectionWork") { hideAllTileLayers(); auto *mod = Map::create(); TileLayer *layer = mod->newLayer(1024, 1024, 8.f, 8.f); layer->setTile(1, 1, 1); layer->setTile(900, 900, 2); std::vector items; TileRenderSystem::collect(items); CHECK_EQ(int(items.size()), 2); CHECK_EQ(TileRenderSystem::lastVisitedChunkCount(), 2); CHECK_EQ(TileRenderSystem::lastVisitedCellCount(), 2 * 32 * 32); layer->setVisible(false); } TEST_CASE("map.layer.applyConfigFlat") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 2, 16.f, 16.f); const char *json = R"({ "width": 2, "height": 2, "tilewidth": 16, "tileheight": 16, "data": [1, 2, 3, 4], "origin": [10, 20], "layer": 2, "visible": true, "tint": [1, 0.5, 0.25, 1] })"; CHECK(layer->applyConfig(json)); CHECK_EQ(layer->getTile(0, 0), 1); CHECK_EQ(layer->getTile(1, 0), 2); CHECK_EQ(layer->getTile(0, 1), 3); CHECK_EQ(layer->getTile(1, 1), 4); CHECK_EQ(layer->getX(), 10.f); CHECK_EQ(layer->getY(), 20.f); CHECK_EQ(layer->getLayer(), 2); CHECK(layer->isVisible()); } TEST_CASE("map.layer.applyConfigMultiLayerPicksFirst") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 1, 8.f, 8.f); const char *json = R"({ "width": 2, "height": 1, "tilewidth": 8, "tileheight": 8, "layers": [ { "type": "tilelayer", "width": 2, "height": 1, "data": [9, 8] }, { "type": "objectgroup", "objects": [] }, { "type": "tilelayer", "width": 2, "height": 1, "data": [1, 1] } ] })"; CHECK(layer->applyConfig(json)); CHECK_EQ(layer->getTile(0, 0), 9); CHECK_EQ(layer->getTile(1, 0), 8); } TEST_CASE("map.tileGid.masksFlipFlags") { CHECK_EQ(tileGid(0), 0u); CHECK_EQ(tileGid(5), 5u); CHECK_EQ(tileGid(0x80000005u), 5u); } TEST_CASE("map.tileset.irregularVisualMetadata") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 1, 64.f, 32.f); layer->setTileVisual(7, 10, 20, 96, 128, 48.f, 104.f, 3.f); layer->setTileMetadata(7, 2, 1, false, 4.f); REQUIRE(layer->getTileVisualCount() == 1); const auto &visual = layer->tileset()->visuals[0]; CHECK_EQ(visual.gid, 7); CHECK_EQ(visual.x, 10); CHECK_EQ(visual.y, 20); CHECK_EQ(visual.width, 96); CHECK_EQ(visual.height, 128); CHECK_EQ(visual.pivotX, 48.f); CHECK_EQ(visual.pivotY, 104.f); CHECK_EQ(visual.sortBias, 3.f); CHECK_EQ(visual.footprintW, 2); CHECK(!visual.walkable); CHECK_EQ(visual.cost, 4.f); layer->clearTileVisuals(); CHECK_EQ(layer->getTileVisualCount(), 0); layer->setVisible(false); } TEST_CASE("map.tileset.manifestShapeInMapConfig") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(1, 1, 64.f, 32.f); const char *json = R"({ "width":1,"height":1,"orientation":"isometric","data":[7], "tileset":{ "image":"missing-atlas.png","firstGid":1, "tiles":[{ "gid":7,"region":[10,20,96,128],"pivot":[48,104], "footprint":[2,1],"walkable":false,"cost":4,"sortBias":3 }] } })"; CHECK(layer->applyConfig(json)); REQUIRE(layer->getTileVisualCount() == 1); const auto &visual = layer->tileset()->visuals[0]; CHECK_EQ(visual.gid, 7); CHECK_EQ(visual.width, 96); CHECK_EQ(visual.pivotY, 104.f); CHECK_EQ(visual.footprintW, 2); CHECK(!visual.walkable); layer->setVisible(false); } TEST_CASE("map.layer.applyTiledInfiniteChunks") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(1, 1, 16.f, 16.f); const char *json = R"({ "infinite":true,"width":0,"height":0,"tilewidth":16,"tileheight":16, "layers":[{"type":"tilelayer","chunks":[ {"x":-2,"y":-1,"width":2,"height":2,"data":[1,2,3,4]}, {"x":0,"y":-1,"width":2,"height":2,"data":[5,6,7,8]} ]}] })"; CHECK(layer->applyConfig(json)); CHECK_EQ(layer->getMapWidth(), 4); CHECK_EQ(layer->getMapHeight(), 2); CHECK_EQ(layer->getTile(0, 0), 1); CHECK_EQ(layer->getTile(3, 1), 8); CHECK_EQ(layer->getX(), -32.f); CHECK_EQ(layer->getY(), -16.f); } TEST_CASE("map.tileset.v2AnimationCustomDataAndTerrain") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(3, 3, 16.f, 16.f); layer->addTileAnimationFrame(10, 11, 100); layer->addTileAnimationFrame(10, 12, 150); CHECK_EQ(layer->getTileAnimationFrameCount(10), 2); layer->setTileDataString(10, "biome", "forest"); layer->setTileDataNumber(10, "damage", 2.5f); layer->setTileDataBool(10, "wet", true); CHECK_EQ(layer->getTileDataType(10, "damage"), std::string("number")); CHECK_EQ(layer->getTileDataString(10, "biome"), std::string("forest")); CHECK_EQ(layer->getTileDataNumber(10, "damage"), 2.5f); CHECK(layer->getTileDataBool(10, "wet")); layer->setTerrainRule(20, 1, 0); layer->setTerrainRule(21, 1, 1 << 3); layer->paintTerrain(1, 1, 1); CHECK_EQ(layer->getTerrain(1, 1), 1); CHECK_EQ(layer->getTile(1, 1), 20); layer->paintTerrain(2, 1, 1); CHECK_EQ(layer->getTile(1, 1), 21); layer->clearTileAnimation(10); CHECK_EQ(layer->getTileAnimationFrameCount(10), 0); } TEST_CASE("map.collision.greedyMergeAndCapabilityPublish") { Map map; TileLayer *layer = map.newLayer(5, 4, 16.f, 8.f); layer->setTileMetadata(9, 1, 1, false); layer->fillRect(1, 1, 3, 2, 9); CollisionSinkMock sink; eve::cap::addListener(&sink); CHECK(map.publishCollision(layer) == 1); CHECK(map.getCollisionRectCount() == 1); CHECK(map.getCollisionRectX(0) == 16.f); CHECK(map.getCollisionRectY(0) == 8.f); CHECK(map.getCollisionRectWidth(0) == 48.f); CHECK(map.getCollisionRectHeight(0) == 16.f); CHECK(sink.layer == layer); REQUIRE(sink.values.size() == 1); CHECK(sink.values[0].width == 48.f); eve::cap::removeListener(&sink); } TEST_CASE("map.tileset.v2ManifestReadsTiledFields") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(1, 1, 16.f, 16.f); const char *json = R"({ "width":1,"height":1,"data":[1], "tileset":{"firstGid":1,"columns":4,"tileWidth":16,"tileHeight":16, "tiles":[{"id":0,"animation":[{"tileid":1,"duration":80},{"tileid":2,"duration":120}], "terrain":2,"neighborMask":0, "properties":[{"name":"damage","type":"float","value":3.5}]}]} })"; CHECK(layer->applyConfig(json)); CHECK_EQ(layer->getTileAnimationFrameCount(1), 2); CHECK_EQ(layer->getTileDataType(1, "damage"), std::string("float")); CHECK_EQ(layer->getTileDataString(1, "damage"), std::string("3.5")); layer->paintTerrain(0, 0, 2); CHECK_EQ(layer->getTile(0, 0), 1); } TEST_CASE("map.autotile.wallAndWaterfallResolveExactDirtyRegion") { auto *mod = Map::create(); TileLayer *wall = mod->newLayer(3, 5, 16.f, 16.f); wall->defineAutotileFamily(4, "wall", 17); wall->setAutotileRule(40, 4, 1 << 5, 1); // top: connected south wall->setAutotileRule(41, 4, (1 << 1) | (1 << 5), 1); // body wall->setAutotileRule(42, 4, 1 << 1, 1); // foot: connected north const int before = wall->getRevision(); wall->paintTerrainRect(1, 1, 1, 3, 4); CHECK_EQ(wall->getRevision(), before + 1); CHECK_EQ(wall->getTile(1, 1), 40); CHECK_EQ(wall->getTile(1, 2), 41); CHECK_EQ(wall->getTile(1, 3), 42); TileLayer *waterfall = mod->newLayer(1, 3, 16.f, 16.f); waterfall->defineAutotileFamily(8, "waterfall", 23); waterfall->setAutotileRule(80, 8, 1 << 5, 1); waterfall->setAutotileRule(81, 8, (1 << 1) | (1 << 5), 1); waterfall->setAutotileRule(82, 8, 1 << 1, 1); waterfall->paintTerrainRect(0, 0, 1, 3, 8); CHECK_EQ(waterfall->getTile(0, 0), 80); CHECK_EQ(waterfall->getTile(0, 1), 81); CHECK_EQ(waterfall->getTile(0, 2), 82); waterfall->addTileAnimationFrame(81, 81, 120); waterfall->addTileAnimationFrame(81, 83, 120); CHECK_EQ(waterfall->getTileAnimationFrameCount(81), 2); } TEST_CASE("map.rpgmaker.importsPlanesPassageAndSemanticsTransactionally") { const std::string dataDir = std::string(EVENGINE_SOURCE_DIR) + "/test/data/rpgmaker/"; auto imported = importRpgMakerMap(dataDir + "Map001.json", dataDir + "Tilesets.json", "RPG Maker MZ"); REQUIRE(imported.ok()); auto receipt = std::move(imported).value(); CHECK_EQ(receipt.sourceEngine, std::string("RPG Maker MZ")); CHECK_EQ(receipt.tilesetId, 1); REQUIRE(receipt.layers.size() == 4); TileLayer *ground = receipt.layers.front(); CHECK_EQ(ground->getTile(0, 0), 1); CHECK_EQ(ground->getTile(1, 0), 2); CHECK_EQ(ground->resource()->sourceVersion, std::string("MV/MZ")); REQUIRE(receipt.navigationLayer != nullptr); Pathfinder pathfinder(receipt.navigationLayer); CHECK(!pathfinder.isWalkable(0, 0)); CHECK(pathfinder.isWalkable(1, 0)); REQUIRE(ground->tileset()->atlases.size() >= 6); auto failed = importRpgMakerMap(dataDir + "missing.json", dataDir + "Tilesets.json"); CHECK(!failed.ok()); REQUIRE(failed.error() != nullptr); CHECK_EQ(failed.error()->source(), std::string("map.rpgmaker")); } TEST_CASE("map.rpgmaker.decodesA1A3A4QuarterTileTablesAndAnimation") { const auto water = decodeRpgMakerTileVisual(2048); REQUIRE(water.subtileFrames.size() == 4); CHECK_EQ(water.subtileFrames[0].parts.size(), size_t(4)); CHECK_NE(water.subtileFrames[0].parts[0].x, water.subtileFrames[1].parts[0].x); const auto waterfall = decodeRpgMakerTileVisual(2048 + 5 * 48); REQUIRE(waterfall.subtileFrames.size() == 3); CHECK_NE(waterfall.subtileFrames[0].parts[0].y, waterfall.subtileFrames[1].parts[0].y); const auto roof = decodeRpgMakerTileVisual(4352); REQUIRE(roof.subtileFrames.size() == 1); CHECK_EQ(roof.subtileFrames[0].parts.size(), size_t(4)); const auto wall = decodeRpgMakerTileVisual(5888 + 8 * 48); REQUIRE(wall.subtileFrames.size() == 1); CHECK_EQ(wall.subtileFrames[0].parts.size(), size_t(4)); } TEST_CASE("map.tileset.importsTiledWangConnectivity") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 1, 16.f, 16.f); const char *json = R"({ "width":2,"height":1,"data":[0,0], "tileset":{"firstgid":10,"columns":4,"tilewidth":16,"tileheight":16, "wangsets":[{"name":"shore","wangtiles":[ {"tileid":2,"wangid":[0,0,1,0,0,0,0,0]}, {"tileid":3,"wangid":[0,0,0,0,0,0,1,0]} ]}]} })"; CHECK(layer->applyConfig(json)); REQUIRE(layer->tileset()->terrainRules.size() == 2); CHECK_EQ(layer->tileset()->terrainRules[0].gid, 12); CHECK_EQ(layer->tileset()->terrainRules[0].terrain, 1); CHECK_EQ(layer->tileset()->terrainRules[0].neighborMask, 1 << 3); CHECK_EQ(layer->tileset()->terrainRules[1].gid, 13); CHECK_EQ(layer->tileset()->terrainRules[1].neighborMask, 1 << 7); layer->setVisible(false); } TEST_CASE("map.tileset.loadsExternalTsjRelativeToMap") { std::string error; const std::string dataDir = std::string(EVENGINE_SOURCE_DIR) + "/test/data/"; auto layers = loadMapFile(dataDir + "tiled_external_map.json", &error); CHECK_EQ(error, std::string()); REQUIRE(layers.size() == 1); TileLayer *layer = layers.front(); CHECK_EQ(layer->getTile(0, 0), 20); CHECK_EQ(layer->getTile(1, 0), 30); CHECK_EQ(layer->getTilesetFirstGid(), 20); REQUIRE(layer->tileset()->atlases.size() == 2); CHECK_EQ(layer->tileset()->atlases[1].firstGid, 30); CHECK_EQ(layer->getTileDataType(20, "wet"), std::string("bool")); CHECK_EQ(layer->getTileDataType(30, "walkable"), std::string("bool")); REQUIRE(layer->tileset()->terrainRules.size() == 1); CHECK_EQ(layer->tileset()->terrainRules[0].gid, 20); CHECK_EQ(layer->tileset()->terrainRules[0].neighborMask, 1 << 3); CHECK_EQ(layer->resource()->texturePath, dataDir + "shore-missing.png"); REQUIRE(layer->resource()->dependencyPaths.size() == 2); CHECK_EQ(layer->resource()->dependencyPaths[0], dataDir + "shore.tsj"); CHECK_EQ(layer->resource()->dependencyPaths[1], dataDir + "walls.tsj"); Pathfinder pathfinder(layer); CHECK(pathfinder.isWalkable(0, 0)); CHECK(!pathfinder.isWalkable(1, 0)); layer->setVisible(false); } TEST_CASE("map.tileset.loadsExternalTsxWithMetadataAnimationAndWang") { std::string error; const std::string path = std::string(EVENGINE_SOURCE_DIR) + "/test/data/tiled_external_tsx_map.json"; auto layers = loadMapFile(path, &error); CHECK_EQ(error, std::string()); REQUIRE(layers.size() == 1); TileLayer *layer = layers.front(); CHECK_EQ(layer->getTile(0, 0), 40); CHECK_EQ(layer->getTilesetFirstGid(), 40); CHECK_EQ(layer->getTileAnimationFrameCount(40), 2); CHECK_EQ(layer->getTileDataString(40, "terrainTag"), std::string("3")); REQUIRE(layer->tileset()->terrainRules.size() == 1); CHECK_EQ(layer->tileset()->terrainRules[0].gid, 40); CHECK_EQ(layer->tileset()->terrainRules[0].neighborMask, 1 << 1); Pathfinder pathfinder(layer); CHECK(!pathfinder.isWalkable(0, 0)); Map map; CHECK_EQ(map.publishCollision(layer), 1); CHECK_EQ(map.getCollisionRectWidth(0), 8.f); layer->setVisible(false); } TEST_CASE("map.tiled.flattensNestedGroupsWithInheritedPresentation") { const std::string json = R"({ "width":2,"height":1,"tilewidth":16,"tileheight":16, "layers":[{"type":"group","offsetx":7,"offsety":9,"opacity":0.5, "layers":[{"type":"tilelayer","width":2,"height":1,"data":[1,0]}]}] })"; std::string error; auto layers = loadMapText(json, nullptr, &error); CHECK_EQ(error, std::string()); REQUIRE(layers.size() == 1); CHECK_EQ(layers[0]->getX(), 7.f); CHECK_EQ(layers[0]->getY(), 9.f); CHECK_EQ(layers[0]->draw()->tint.a, 0.5f); } TEST_CASE("map.import.failureRollsBackObservableLayerState") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 1, 16.f, 16.f); layer->setTile(0, 0, 7); const int revision = layer->getRevision(); std::string error; CHECK(!applyConfigText(layer, R"({"width":4,"height":4,"data":[1]})", &error)); CHECK(!error.empty()); CHECK_EQ(layer->getMapWidth(), 2); CHECK_EQ(layer->getMapHeight(), 1); CHECK_EQ(layer->getTile(0, 0), 7); CHECK_EQ(layer->getRevision(), revision); } TEST_CASE("map.tiled.preservesAndRendersUnsignedFlipFlags") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 1, 16.f, 16.f); layer->setLayer(999); CHECK(layer->applyConfig(R"({"width":2,"height":1,"data":[2147483649,536870913]})")); CHECK_EQ(tileGid(uint32_t(layer->getTile(0, 0))), uint32_t(1)); std::vector items; TileRenderSystem::collect(items); items.erase(std::remove_if(items.begin(), items.end(), [](const auto &item) { return item.layer != 999; }), items.end()); REQUIRE(items.size() == 2); const auto &horizontal = items[0]; const auto &diagonal = items[1]; CHECK(horizontal.flipX); CHECK_EQ(horizontal.rotation, 0.f); CHECK_EQ(diagonal.rotation, 270.f); CHECK(diagonal.flipX); } TEST_CASE("map.render.nullSafe") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 2, 8.f, 8.f); layer->fill(1); TileRenderSystem::render(nullptr); } TEST_CASE("map.projection.orthogonal") { TileLayer::Config cfg; cfg.mapW = 10; cfg.mapH = 10; cfg.tileW = 32.f; cfg.tileH = 32.f; cfg.originX = 10.f; cfg.originY = 20.f; cfg.orientation = MapOrientation::Orthogonal; float wx = 0, wy = 0; tileToWorld(cfg, 2, 3, wx, wy); CHECK_EQ(wx, 10.f + 2 * 32.f); CHECK_EQ(wy, 20.f + 3 * 32.f); CHECK_EQ(tileToDepthY(cfg, 2, 3), 20.f + 4 * 32.f); int tx = -1, ty = -1; worldToTile(cfg, wx + 1.f, wy + 1.f, tx, ty); CHECK_EQ(tx, 2); CHECK_EQ(ty, 3); } TEST_CASE("map.projection.isometric") { TileLayer::Config cfg; cfg.tileW = 64.f; cfg.tileH = 32.f; cfg.originX = 0.f; cfg.originY = 0.f; cfg.orientation = MapOrientation::Isometric; float wx = 0, wy = 0; tileToWorld(cfg, 1, 0, wx, wy); CHECK_EQ(wx, 32.f); CHECK_EQ(wy, 16.f); tileToWorld(cfg, 0, 1, wx, wy); CHECK_EQ(wx, -32.f); CHECK_EQ(wy, 16.f); CHECK_EQ(tileToDepthY(cfg, 1, 0), 16.f + 32.f); } TEST_CASE("map.projection.isometric.renderSpacingRoundTrip") { TileLayer::Config cfg; cfg.mapW = 8; cfg.mapH = 8; cfg.tileW = 64.f; cfg.tileH = 32.f; cfg.cellGapX = 16.f; cfg.cellGapY = 8.f; cfg.originX = 100.f; cfg.originY = 20.f; cfg.orientation = MapOrientation::Isometric; float wx = 0.f, wy = 0.f; tileToWorld(cfg, 3, 2, wx, wy); CHECK_EQ(wx, 140.f); CHECK_EQ(wy, 120.f); int tx = -1, ty = -1; worldToTile(cfg, wx, wy, tx, ty); CHECK_EQ(tx, 3); CHECK_EQ(ty, 2); } TEST_CASE("map.layer.renderSpacingApi") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 2, 80.f, 40.f); layer->setRenderSpacing(1.25f, 1.5f); CHECK_EQ(layer->getCellGapX(), 20.f); CHECK_EQ(layer->getCellGapY(), 20.f); CHECK_EQ(layer->getRenderSpacingX(), 1.25f); CHECK_EQ(layer->getRenderSpacingY(), 1.5f); } TEST_CASE("map.projection.staggeredYOdd") { TileLayer::Config cfg; cfg.tileW = 64.f; cfg.tileH = 32.f; cfg.originX = 0.f; cfg.originY = 0.f; cfg.orientation = MapOrientation::Staggered; cfg.staggerAxis = StaggerAxis::Y; cfg.staggerIndex = StaggerIndex::Odd; float wx = 0, wy = 0; tileToWorld(cfg, 0, 0, wx, wy); CHECK_EQ(wx, 0.f); CHECK_EQ(wy, 0.f); tileToWorld(cfg, 0, 1, wx, wy); CHECK_EQ(wx, 32.f); CHECK_EQ(wy, 16.f); } TEST_CASE("map.config.orientationIsometric") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 2, 64.f, 32.f); const char *json = R"({ "width": 2, "height": 2, "tilewidth": 64, "tileheight": 32, "orientation": "isometric", "data": [1, 0, 0, 1] })"; CHECK(layer->applyConfig(json)); CHECK_EQ(static_cast(layer->config()->orientation), static_cast(MapOrientation::Isometric)); } TEST_CASE("map.config.unknownOrientationFails") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(1, 1, 8.f, 8.f); CHECK(!layer->applyConfig(R"({"width":1,"height":1,"orientation":"banana","data":[1]})")); } TEST_CASE("map.decode.zstdFails") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(1, 1, 8.f, 8.f); const char *json = R"({"width":1,"height":1,"tilewidth":8,"tileheight":8, "encoding":"base64","compression":"zstd","data":"AAAA"})"; CHECK(!layer->applyConfig(json)); } TEST_CASE("map.objects.parseObjectGroup") { const char *json = R"({ "width": 2, "height": 1, "tilewidth": 16, "tileheight": 16, "layers": [ { "type": "tilelayer", "width": 2, "height": 1, "data": [1, 0] }, { "type": "objectgroup", "objects": [ { "name": "spawn", "type": "player", "x": 8, "y": 24, "width": 16, "height": 16 } ]} ] })"; std::vector objs; std::string err; auto layers = loadMapText(json, &objs, &err); CHECK(err.empty()); REQUIRE(layers.size() == 1); REQUIRE(objs.size() == 1); CHECK_EQ(objs[0].name, "spawn"); CHECK_EQ(objs[0].type, "player"); CHECK_EQ(objs[0].x, 8.f); CHECK_EQ(objs[0].y, 24.f); } TEST_CASE("map.render.tilesContributeDepthY") { hideAllTileLayers(); auto *mod = Map::create(); TileLayer *layer = mod->newLayer(1, 2, 32.f, 32.f); layer->config()->orientation = MapOrientation::Orthogonal; layer->setOrigin(0, 0); layer->setTile(0, 0, 1); layer->setTile(0, 1, 1); std::vector items; TileRenderSystem::collect(items); REQUIRE(items.size() == 2); eve::graphics::sortDrawItems2D(items); CHECK(items[0].depthY < items[1].depthY); layer->clear(); layer->setVisible(false); } TEST_CASE("map.drawItem.sortByDepthY") { using eve::graphics::DrawItem2D; std::vector items(2); items[0].layer = 0; items[0].depthY = 100.f; items[0].canvas = nullptr; items[1].layer = 0; items[1].depthY = 50.f; items[1].canvas = nullptr; eve::graphics::sortDrawItems2D(items); CHECK_EQ(items[0].depthY, 50.f); CHECK_EQ(items[1].depthY, 100.f); } TEST_CASE("map.projection.isometric.worldToTile") { TileLayer::Config cfg; cfg.tileW = 64.f; cfg.tileH = 32.f; cfg.originX = 0.f; cfg.originY = 0.f; cfg.orientation = MapOrientation::Isometric; float wx = 0, wy = 0; tileToWorld(cfg, 1, 0, wx, wy); int tx = -1, ty = -1; worldToTile(cfg, wx, wy, tx, ty); CHECK_EQ(tx, 1); CHECK_EQ(ty, 0); tileToWorld(cfg, 0, 1, wx, wy); worldToTile(cfg, wx, wy, tx, ty); CHECK_EQ(tx, 0); CHECK_EQ(ty, 1); } TEST_CASE("map.projection.staggeredYOdd.worldToTileAndDepth") { TileLayer::Config cfg; cfg.mapW = 4; cfg.mapH = 4; cfg.tileW = 64.f; cfg.tileH = 32.f; cfg.originX = 0.f; cfg.originY = 0.f; cfg.orientation = MapOrientation::Staggered; cfg.staggerAxis = StaggerAxis::Y; cfg.staggerIndex = StaggerIndex::Odd; float wx = 0, wy = 0; tileToWorld(cfg, 0, 1, wx, wy); CHECK_EQ(wx, 32.f); CHECK_EQ(wy, 16.f); CHECK_EQ(tileToDepthY(cfg, 0, 1), 16.f + 32.f); // Nearest tile center int tx = -1, ty = -1; worldToTile(cfg, wx + 32.f, wy + 16.f, tx, ty); CHECK_EQ(tx, 0); CHECK_EQ(ty, 1); } TEST_CASE("map.projection.staggeredXEven") { TileLayer::Config cfg; cfg.mapW = 3; cfg.mapH = 3; cfg.tileW = 64.f; cfg.tileH = 32.f; cfg.orientation = MapOrientation::Staggered; cfg.staggerAxis = StaggerAxis::X; cfg.staggerIndex = StaggerIndex::Even; float wx = 0, wy = 0; tileToWorld(cfg, 0, 0, wx, wy); // major=tx=0 even → staggerIndex Even → shift (since !odd) CHECK_EQ(wx, 0.f); CHECK_EQ(wy, 16.f); tileToWorld(cfg, 1, 0, wx, wy); CHECK_EQ(wx, 32.f); CHECK_EQ(wy, 0.f); } TEST_CASE("map.projection.hexagonalYOdd") { TileLayer::Config cfg; cfg.mapW = 3; cfg.mapH = 3; cfg.tileW = 64.f; cfg.tileH = 32.f; cfg.hexSideLength = 16.f; cfg.orientation = MapOrientation::Hexagonal; cfg.staggerAxis = StaggerAxis::Y; cfg.staggerIndex = StaggerIndex::Odd; float wx = 0, wy = 0; tileToWorld(cfg, 0, 0, wx, wy); CHECK_EQ(wx, 0.f); CHECK_EQ(wy, 0.f); // pitchY = (32+16)/2 = 24; row 1 odd → +tw/2 tileToWorld(cfg, 0, 1, wx, wy); CHECK_EQ(wx, 32.f); CHECK_EQ(wy, 24.f); CHECK_EQ(tileToDepthY(cfg, 0, 1), 24.f + 32.f); int tx = -1, ty = -1; worldToTile(cfg, wx + 32.f, wy + 16.f, tx, ty); CHECK_EQ(tx, 0); CHECK_EQ(ty, 1); } TEST_CASE("map.decode.base64ZlibRoundTrip") { // 2x2 little-endian GIDs: 1,2,3,4 const unsigned char raw[16] = {1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0}; eve::data::CompressedData *cdata = eve::data::compress("zlib", reinterpret_cast(raw), 16, -1); REQUIRE(cdata != nullptr); const char *compressed = reinterpret_cast(cdata->getData()); const size_t compressedSize = cdata->getSize(); size_t b64len = 0; char *b64raw = eve::data::encode("base64", compressed, compressedSize, b64len); REQUIRE(b64raw != nullptr); REQUIRE(b64len > 0); std::string b64(b64raw, b64len); delete[] b64raw; std::string json = std::string(R"({ "width": 2, "height": 2, "tilewidth": 16, "tileheight": 16, "layers": [{ "type": "tilelayer", "width": 2, "height": 2, "encoding": "base64", "compression": "zlib", "data": ")") + b64 + R"("}]})"; std::string err; auto layers = loadMapText(json, nullptr, &err); CHECK(err.empty()); REQUIRE(layers.size() == 1); CHECK_EQ(layers[0]->getTile(0, 0), 1); CHECK_EQ(layers[0]->getTile(1, 0), 2); CHECK_EQ(layers[0]->getTile(0, 1), 3); CHECK_EQ(layers[0]->getTile(1, 1), 4); delete cdata; } TEST_CASE("map.decode.base64Uncompressed") { const unsigned char raw[16] = {1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0}; size_t b64len = 0; char *b64raw = eve::data::encode("base64", reinterpret_cast(raw), 16, b64len); REQUIRE(b64raw != nullptr); std::string b64(b64raw, b64len); delete[] b64raw; std::string json = std::string(R"({ "width": 2, "height": 2, "tilewidth": 16, "tileheight": 16, "encoding": "base64", "compression": "", "data": ")") + b64 + R"("})"; auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 2, 16.f, 16.f); CHECK(layer->applyConfig(json)); CHECK_EQ(layer->getTile(0, 0), 1); CHECK_EQ(layer->getTile(1, 1), 4); } TEST_CASE("map.load.tiledLikeIsometricWithObjects") { const char *json = R"({ "width": 2, "height": 2, "tilewidth": 64, "tileheight": 32, "orientation": "isometric", "layers": [ { "type": "tilelayer", "width": 2, "height": 2, "data": [1, 0, 0, 2] }, { "type": "objectgroup", "objects": [ { "name": "npc", "type": "talker", "x": 10, "y": 20, "width": 8, "height": 8, "gid": 3 } ]} ] })"; std::vector objs; std::string err; auto layers = loadMapText(json, &objs, &err); CHECK(err.empty()); REQUIRE(layers.size() == 1); CHECK_EQ(static_cast(layers[0]->config()->orientation), static_cast(MapOrientation::Isometric)); CHECK_EQ(layers[0]->getTile(0, 0), 1); CHECK_EQ(layers[0]->getTile(1, 1), 2); float wx = 0, wy = 0; layers[0]->tileToWorld(1, 0, wx, wy); CHECK_EQ(wx, 32.f); CHECK_EQ(wy, 16.f); REQUIRE(objs.size() == 1); CHECK_EQ(objs[0].name, "npc"); CHECK_EQ(objs[0].type, "talker"); CHECK_EQ(objs[0].gid, 3); } TEST_CASE("map.dualGrid.maskAndFrameTable") { CHECK_EQ(dualGridMaskFromCorners(false, false, false, false), 0); CHECK_EQ(dualGridMaskFromCorners(true, false, false, false), 1); CHECK_EQ(dualGridMaskFromCorners(false, true, false, false), 2); CHECK_EQ(dualGridMaskFromCorners(false, false, true, false), 4); CHECK_EQ(dualGridMaskFromCorners(false, false, false, true), 8); CHECK_EQ(dualGridMaskFromCorners(true, true, true, true), 15); // Diagonal TL+BR (classic dual-grid case that blob edge-masks miss). CHECK_EQ(dualGridMaskFromCorners(true, false, false, true), 9); CHECK_EQ(dualGridDefaultFrame(0), -1); CHECK_EQ(dualGridDefaultFrame(15), 6); CHECK_EQ(dualGridDefaultFrame(9), 4); CHECK_EQ(dualGridDefaultFrameTable()[1], 15); } TEST_CASE("map.dualGrid.resolveHalfOffsetAndSize") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(2, 2, 32.f, 32.f); TileLayer *display = mod->newLayer(1, 1, 8.f, 8.f); logic->setOrigin(10.f, 20.f); logic->setTile(0, 0, 1); logic->setTile(1, 1, 1); DualGridOptions opts; opts.useDefaultFrameTable = false; // gid = firstGid + mask opts.firstDisplayGid = 10; opts.hideLogic = true; std::string err; CHECK(resolveDualGrid(logic, display, opts, &err)); CHECK(err.empty()); CHECK_EQ(display->getMapWidth(), 3); CHECK_EQ(display->getMapHeight(), 3); CHECK_EQ(display->getTileWidth(), 32.f); CHECK_EQ(display->getX(), 10.f - 16.f); CHECK_EQ(display->getY(), 20.f - 16.f); CHECK(!logic->isVisible()); CHECK(display->isVisible()); // Display (1,1) samples logic (0,0),(1,0),(0,1),(1,1) → TL+BR = mask 9 CHECK_EQ(dualGridMaskAt(*logic, 1, 1, 0), 9); CHECK_EQ(display->getTile(1, 1), 10 + 9); // Display (0,0) samples only out-of-bounds + logic(0,0) as BR → mask 8 CHECK_EQ(dualGridMaskAt(*logic, 0, 0, 0), 8); CHECK_EQ(display->getTile(0, 0), 10 + 8); // Empty neighborhood CHECK_EQ(display->getTile(2, 0), 0); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.resolveRejectsSameLayer") { auto *mod = Map::create(); TileLayer *layer = mod->newLayer(2, 2, 16.f, 16.f); std::string err; CHECK(!resolveDualGrid(layer, layer, DualGridOptions{}, &err)); CHECK(!err.empty()); CHECK(!mod->resolveDualGrid(nullptr, layer)); CHECK(!mod->lastDualGridError().empty()); layer->clear(); layer->setVisible(false); } TEST_CASE("map.dualGrid.filledGidFilter") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(1, 1, 16.f, 16.f); TileLayer *display = mod->newLayer(1, 1, 16.f, 16.f); logic->setTile(0, 0, 5); DualGridOptions opts; opts.filledGid = 9; // 5 should not count opts.useDefaultFrameTable = false; opts.firstDisplayGid = 1; opts.hideLogic = false; CHECK(resolveDualGrid(logic, display, opts, nullptr)); // All display cells empty when filled filter mismatches CHECK_EQ(display->getTile(0, 0), 0); CHECK_EQ(display->getTile(1, 0), 0); CHECK_EQ(display->getTile(0, 1), 0); CHECK_EQ(display->getTile(1, 1), 0); CHECK(logic->isVisible()); CHECK(mod->resolveDualGridFilled(logic, display, 5)); // logic(0,0) is BR of display(0,0) and TL of display(1,1). CHECK_EQ(dualGridMaskAt(*logic, 0, 0, 5), 8); CHECK_EQ(dualGridMaskAt(*logic, 1, 1, 5), 1); CHECK_EQ(display->getTile(0, 0), 1 + dualGridDefaultFrame(8)); CHECK_EQ(display->getTile(1, 1), 1 + dualGridDefaultFrame(1)); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.halfOffset.orthogonalIsoStaggerHex") { TileLayer::Config ortho; ortho.orientation = MapOrientation::Orthogonal; ortho.tileW = 32.f; ortho.tileH = 32.f; float ox = 0, oy = 0; dualGridHalfOffset(ortho, ox, oy); CHECK_EQ(ox, -16.f); CHECK_EQ(oy, -16.f); TileLayer::Config iso; iso.orientation = MapOrientation::Isometric; iso.tileW = 64.f; iso.tileH = 32.f; dualGridHalfOffset(iso, ox, oy); CHECK_EQ(ox, 0.f); CHECK_EQ(oy, -16.f); TileLayer::Config stagY; stagY.orientation = MapOrientation::Staggered; stagY.staggerAxis = StaggerAxis::Y; stagY.tileW = 64.f; stagY.tileH = 32.f; dualGridHalfOffset(stagY, ox, oy); CHECK_EQ(ox, -32.f); CHECK_EQ(oy, -8.f); // pitchY = th/2 = 16 → half = 8 TileLayer::Config stagX; stagX.orientation = MapOrientation::Staggered; stagX.staggerAxis = StaggerAxis::X; stagX.tileW = 64.f; stagX.tileH = 32.f; dualGridHalfOffset(stagX, ox, oy); CHECK_EQ(ox, -16.f); // pitchX = tw/2 = 32 → half = 16 CHECK_EQ(oy, -16.f); TileLayer::Config hexY; hexY.orientation = MapOrientation::Hexagonal; hexY.staggerAxis = StaggerAxis::Y; hexY.tileW = 64.f; hexY.tileH = 32.f; hexY.hexSideLength = 16.f; dualGridHalfOffset(hexY, ox, oy); // pitchY = (32+16)/2 = 24 → half = 12 CHECK_EQ(ox, -32.f); CHECK_EQ(oy, -12.f); TileLayer::Config hexX; hexX.orientation = MapOrientation::Hexagonal; hexX.staggerAxis = StaggerAxis::X; hexX.tileW = 64.f; hexX.tileH = 32.f; hexX.hexSideLength = 16.f; dualGridHalfOffset(hexX, ox, oy); // pitchX = (64+16)/2 = 40 → half = 20 CHECK_EQ(ox, -20.f); CHECK_EQ(oy, -16.f); } TEST_CASE("map.dualGrid.resolve.isometric") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(2, 2, 64.f, 32.f); TileLayer *display = mod->newLayer(1, 1, 8.f, 8.f); logic->config()->orientation = MapOrientation::Isometric; logic->setOrigin(100.f, 200.f); logic->setTile(0, 0, 1); logic->setTile(1, 1, 1); DualGridOptions opts; opts.useDefaultFrameTable = false; opts.firstDisplayGid = 1; opts.hideLogic = true; CHECK(resolveDualGrid(logic, display, opts, nullptr)); CHECK_EQ(static_cast(display->config()->orientation), static_cast(MapOrientation::Isometric)); CHECK_EQ(display->getMapWidth(), 3); CHECK_EQ(display->getMapHeight(), 3); CHECK_EQ(display->getX(), 100.f); // iso: offX = 0 CHECK_EQ(display->getY(), 200.f - 16.f); // offY = -tileH/2 CHECK_EQ(mod->dualGridOffsetX(logic), 0.f); CHECK_EQ(mod->dualGridOffsetY(logic), -16.f); // Index-space mask unchanged from orthogonal dual-grid. CHECK_EQ(dualGridMaskAt(*logic, 1, 1, 0), 9); CHECK_EQ(display->getTile(1, 1), 1 + 9); // display(1,0) == logic fractional (0.5, -0.5) under iso projection: // wx = ox + (1-0)*tw/2 = ox+32, wy = (oy-th/2) + (1+0)*th/2 = oy CHECK_EQ(display->tileToWorldX(1, 0), 100.f + 32.f); CHECK_EQ(display->tileToWorldY(1, 0), 200.f); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.resolve.staggeredYOdd") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(3, 2, 64.f, 32.f); TileLayer *display = mod->newLayer(1, 1, 8.f, 8.f); { auto lc = logic->config(); lc->orientation = MapOrientation::Staggered; lc->staggerAxis = StaggerAxis::Y; lc->staggerIndex = StaggerIndex::Odd; } logic->setOrigin(0.f, 0.f); logic->fill(1); DualGridOptions opts; opts.useDefaultFrameTable = false; opts.firstDisplayGid = 20; CHECK(resolveDualGrid(logic, display, opts, nullptr)); CHECK_EQ(static_cast(display->config()->orientation), static_cast(MapOrientation::Staggered)); CHECK_EQ(static_cast(display->config()->staggerAxis), static_cast(StaggerAxis::Y)); CHECK_EQ(static_cast(display->config()->staggerIndex), static_cast(StaggerIndex::Odd)); CHECK_EQ(display->getMapWidth(), 4); CHECK_EQ(display->getMapHeight(), 3); CHECK_EQ(display->getX(), -32.f); CHECK_EQ(display->getY(), -8.f); // Interior display(1,1) sees four filled logic cells → mask 15 CHECK_EQ(dualGridMaskAt(*logic, 1, 1, 0), 15); CHECK_EQ(display->getTile(1, 1), 20 + 15); // Corner display(0,0) only BR → mask 8 CHECK_EQ(display->getTile(0, 0), 20 + 8); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.resolve.staggeredXEven") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(2, 3, 64.f, 32.f); TileLayer *display = mod->newLayer(1, 1, 8.f, 8.f); { auto lc = logic->config(); lc->orientation = MapOrientation::Staggered; lc->staggerAxis = StaggerAxis::X; lc->staggerIndex = StaggerIndex::Even; } logic->setOrigin(10.f, 20.f); logic->setTile(0, 1, 1); logic->setTile(1, 1, 1); DualGridOptions opts; opts.useDefaultFrameTable = false; opts.firstDisplayGid = 1; CHECK(resolveDualGrid(logic, display, opts, nullptr)); CHECK_EQ(static_cast(display->config()->staggerAxis), static_cast(StaggerAxis::X)); CHECK_EQ(static_cast(display->config()->staggerIndex), static_cast(StaggerIndex::Even)); CHECK_EQ(display->getX(), 10.f - 16.f); CHECK_EQ(display->getY(), 20.f - 16.f); // display(1,2) samples logic (0,1),(1,1),(0,2),(1,2) → TL+TR = mask 3 CHECK_EQ(dualGridMaskAt(*logic, 1, 2, 0), 3); CHECK_EQ(display->getTile(1, 2), 1 + 3); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.resolve.hexagonal") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(2, 2, 64.f, 32.f); TileLayer *display = mod->newLayer(1, 1, 8.f, 8.f); { auto lc = logic->config(); lc->orientation = MapOrientation::Hexagonal; lc->staggerAxis = StaggerAxis::Y; lc->staggerIndex = StaggerIndex::Odd; lc->hexSideLength = 16.f; } logic->setOrigin(5.f, 7.f); logic->setTile(0, 0, 1); DualGridOptions opts; opts.useDefaultFrameTable = true; opts.firstDisplayGid = 1; CHECK(resolveDualGrid(logic, display, opts, nullptr)); CHECK_EQ(static_cast(display->config()->orientation), static_cast(MapOrientation::Hexagonal)); CHECK_EQ(display->config()->hexSideLength, 16.f); CHECK_EQ(display->getX(), 5.f - 32.f); CHECK_EQ(display->getY(), 7.f - 12.f); // pitchY=24 → half=12 CHECK_EQ(display->getTile(0, 0), 1 + dualGridDefaultFrame(8)); CHECK_EQ(display->getTile(1, 1), 1 + dualGridDefaultFrame(1)); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.flipFlagsCountAsFilled") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(1, 1, 16.f, 16.f); TileLayer *display = mod->newLayer(1, 1, 16.f, 16.f); // Tiled horizontal flip flag on GID 1 logic->setTile(0, 0, int(0x80000001u)); DualGridOptions opts; opts.useDefaultFrameTable = false; opts.firstDisplayGid = 1; CHECK(resolveDualGrid(logic, display, opts, nullptr)); CHECK_EQ(dualGridMaskAt(*logic, 0, 0, 0), 8); CHECK_EQ(display->getTile(0, 0), 1 + 8); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.noHalfOffsetKeepsOrigin") { auto *mod = Map::create(); TileLayer *logic = mod->newLayer(2, 1, 32.f, 32.f); TileLayer *display = mod->newLayer(1, 1, 8.f, 8.f); logic->config()->orientation = MapOrientation::Isometric; logic->setOrigin(40.f, 50.f); logic->setTile(0, 0, 1); DualGridOptions opts; opts.applyHalfOffset = false; opts.useDefaultFrameTable = false; opts.firstDisplayGid = 1; opts.hideLogic = false; CHECK(resolveDualGrid(logic, display, opts, nullptr)); CHECK_EQ(display->getX(), 40.f); CHECK_EQ(display->getY(), 50.f); CHECK_EQ(static_cast(display->config()->orientation), static_cast(MapOrientation::Isometric)); CHECK(logic->isVisible()); logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.dualGrid.render.isometricDisplayDepth") { hideAllTileLayers(); auto *mod = Map::create(); TileLayer *logic = mod->newLayer(2, 2, 64.f, 32.f); TileLayer *display = mod->newLayer(1, 1, 64.f, 32.f); logic->config()->orientation = MapOrientation::Isometric; logic->setOrigin(0.f, 0.f); logic->fill(1); DualGridOptions opts; opts.useDefaultFrameTable = false; opts.firstDisplayGid = 1; CHECK(resolveDualGrid(logic, display, opts, nullptr)); std::vector items; TileRenderSystem::collect(items); // Interior + border dual cells that are non-empty on a full 2x2 logic map: // display 3x3, only mask0 empty — all 9 have some contribution from filled // neighbors except none are fully empty when logic is full... actually edges // still have non-zero masks. All 9 display cells should be non-empty. CHECK_EQ(items.size(), 9u); eve::graphics::sortDrawItems2D(items); for (size_t i = 1; i < items.size(); ++i) { CHECK(items[i - 1].depthY <= items[i].depthY); } logic->clear(); display->clear(); logic->setVisible(false); display->setVisible(false); } TEST_CASE("map.render.orthoAndIsoPreview") { hideAllTileLayers(); auto *win = eve::window::Window::create(); auto *gfx = eve::graphics::Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 640; s.height = 400; s.centered = true; REQUIRE(win->setWindowSettings(s)); auto *mod = Map::create(); // Left: orthogonal concentric rings (solidForGid colors, no atlas needed). TileLayer *ortho = mod->newLayer(10, 8, 28.f, 28.f); ortho->config()->orientation = MapOrientation::Orthogonal; ortho->setOrigin(40.f, 40.f); for (int y = 0; y < 8; ++y) { for (int x = 0; x < 10; ++x) { const int ring = std::min({x, y, 9 - x, 7 - y}); ortho->setTile(x, y, uint32_t(1 + ring)); } } ortho->setVisible(true); // Right: isometric diamond so projection is visibly different. TileLayer *iso = mod->newLayer(6, 6, 48.f, 24.f); iso->config()->orientation = MapOrientation::Isometric; iso->setOrigin(420.f, 80.f); for (int y = 0; y < 6; ++y) { for (int x = 0; x < 6; ++x) { iso->setTile(x, y, uint32_t(3 + ((x + y) % 4))); } } iso->setVisible(true); std::vector items; TileRenderSystem::collect(items); REQUIRE(items.size() == 10u * 8u + 6u * 6u); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.12f, 1.f); for (int frame = 0; frame < 75; ++frame) { ortho->setOrigin(40.f + float(frame) * 0.15f, 40.f); iso->setOrigin(420.f, 80.f + float(std::sin(float(frame) * 0.08f)) * 6.f); gfx->clearScreen(); mod->render(gfx); gfx->present(); SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) break; } SDL_Delay(16); } ortho->setVisible(false); iso->setVisible(false); win->close(); }