diff --git a/docs/模块设计.md b/docs/模块设计.md index 043a26a0..52863c6e 100644 --- a/docs/模块设计.md +++ b/docs/模块设计.md @@ -216,10 +216,12 @@ Love2D 风格即时模式仅作 C++/DevTools 内部逃生舱。 地图读写、多层表示;C++ 侧直接生成/访问 array,便于 squirrel 访问;有资源时可作 renderable。 -- [ ] 多层地图数据结构 -- [ ] 读取 / 保存 -- [ ] Squirrel 侧友好访问 -- [ ] Renderable 接口对接 `graphics` +- [x] 多层地图数据结构(`TileLayer` ECS) +- [x] 读取 Tiled JSON(含 base64+zlib/gzip;未知 orientation 失败;zstd 不支持) +- [x] 投影:orthogonal / isometric / staggered / hexagonal + `tileToWorld` / `worldToTile` / `depthY` +- [x] 对象层解析(`Map` 对象缓存 API) +- [x] 与精灵统一 2D 深度队列(`Map::render` → `DrawItem2D`) +- [x] Squirrel 侧友好访问 - [ ] 与地图编辑器(DevTools)联动 ### 16. 数据库 — `database` diff --git a/example/maps/demo.json b/example/maps/demo.json index 122e60f0..426c5e98 100644 --- a/example/maps/demo.json +++ b/example/maps/demo.json @@ -29,7 +29,7 @@ "layer": 1, "data": [ 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 5, 0, 0, 0, 0, + 0, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, diff --git a/external/ECS.hpp b/external/ECS.hpp index 1d053e97..b43df58b 160000 --- a/external/ECS.hpp +++ b/external/ECS.hpp @@ -1 +1 @@ -Subproject commit 1d053e978ffb0b4e4a92f4fcacfc15d4b6a51f26 +Subproject commit b43df58b634ea3a19d6d75260e750926d8ed67e9 diff --git a/src/modules/filesystem/HotReload.cpp b/src/modules/filesystem/HotReload.cpp index 54bf9405..53f32a55 100644 --- a/src/modules/filesystem/HotReload.cpp +++ b/src/modules/filesystem/HotReload.cpp @@ -68,7 +68,7 @@ bool HotReload::isJsonPath(const std::string &normPath) const { } bool HotReload::reloadParticles(const std::string &normPath) { - if (ecs::ComponentManager::inst().registy == nullptr) return false; + if (ecs::current()->getManager() == nullptr) return false; int reloaded = 0; auto view = ecs::View::inst().registy == nullptr) return false; + if (ecs::current()->getManager() == nullptr) return false; int reloaded = 0; auto view = ecs::View(); @@ -103,7 +103,7 @@ bool HotReload::reloadTextures(const std::string &normPath) { bool ok = gfx->reloadTextureFromFile(normPath); // Emitters that reference this texture path: force re-bind via config reload or setTexture. - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { @@ -120,7 +120,7 @@ bool HotReload::reloadTextures(const std::string &normPath) { } // Tile layers that reference this atlas path. - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { diff --git a/src/modules/graphics/DrawItem2D.h b/src/modules/graphics/DrawItem2D.h new file mode 100644 index 00000000..46eb29ca --- /dev/null +++ b/src/modules/graphics/DrawItem2D.h @@ -0,0 +1,57 @@ +#pragma once + +#include "graphics/Canvas.h" +#include "graphics/Quad.h" +#include "graphics/Shader.h" +#include "graphics/Texture.h" + +#include +#include +#include + +namespace eve::graphics { + +class Camera2D; + +/** Shared 2D draw queue item (sprites + tiles). */ +struct DrawItem2D { + float x = 0.f; + float y = 0.f; + float w = 0.f; + float h = 0.f; + float depthY = 0.f; + Color color{1.f, 1.f, 1.f, 1.f}; + int layer = 0; + Texture *texture = nullptr; + Texture *normal = nullptr; + Quad *quad = nullptr; + Shader *shader = nullptr; + Canvas *canvas = nullptr; + Camera2D *camera = nullptr; + bool receiveLight = true; + bool litPath = false; + bool hasUV = false; + float u0 = 0.f, v0 = 0.f, u1 = 1.f, v1 = 1.f; + /** Screen-space camera already resolved; if cameraEntity set, RenderSystem resolves. */ + float camX = 0.f, camY = 0.f, camZoom = 1.f; + bool camValid = false; + Color camClear{0.1f, 0.1f, 0.12f, 1.f}; + float camAmbientR = 0.15f, camAmbientG = 0.15f, camAmbientB = 0.18f; +}; + +inline void sortDrawItems2D(std::vector &items) { + std::stable_sort(items.begin(), items.end(), [](const DrawItem2D &a, const DrawItem2D &b) { + const bool aOff = a.canvas != nullptr; + const bool bOff = b.canvas != nullptr; + if (aOff != bOff) return aOff && !bOff; + if (a.canvas != b.canvas) return a.canvas < b.canvas; + if (a.layer != b.layer) return a.layer < b.layer; + if (a.depthY != b.depthY) return a.depthY < b.depthY; + if (a.litPath != b.litPath) return !a.litPath && b.litPath; + if (a.shader != b.shader) return a.shader < b.shader; + if (a.texture != b.texture) return a.texture < b.texture; + return a.normal < b.normal; + }); +} + +} // namespace eve::graphics diff --git a/src/modules/graphics/RenderSystem.cpp b/src/modules/graphics/RenderSystem.cpp index d6c00307..c7254dcd 100644 --- a/src/modules/graphics/RenderSystem.cpp +++ b/src/modules/graphics/RenderSystem.cpp @@ -74,7 +74,7 @@ struct PackedLight { void collectLights(Canvas *canvasKey, std::vector &out) { out.clear(); - if (ecs::ComponentManager::inst().registy == nullptr) return; + if (ecs::current()->getManager() == nullptr) return; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [d] = *it; @@ -153,9 +153,9 @@ Color modulateUnlit(Color base, float cx, float cy, bool receiveLight, const Lig } // namespace -void RenderSystem::render(Graphics &gfx) { +void RenderSystem::collectSprites(std::vector &out) { std::unordered_map defaultCam; - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; @@ -167,21 +167,6 @@ void RenderSystem::render(Graphics &gfx) { } } - struct Item { - float x, y, w, h; - Color color; - int layer; - Texture *texture; - Texture *normal; - Quad *quad; - Shader *shader; - Canvas *canvas; - ViewCam cam; - bool receiveLight; - bool litPath; - }; - std::vector items; - auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [xf, sp] = *it; @@ -193,11 +178,13 @@ void RenderSystem::render(Graphics &gfx) { camEnt = (found != defaultCam.end()) ? found->second : nullptr; } - Item item; + ViewCam cam = fromEntity(camEnt); + DrawItem2D item; item.x = xf->x; item.y = xf->y; item.w = sp->width * xf->sx; item.h = sp->height * xf->sy; + item.depthY = xf->y + sp->height * xf->sy; item.color = Color(sp->r, sp->g, sp->b, sp->a); item.layer = sp->layer; item.texture = sp->texture; @@ -205,24 +192,41 @@ void RenderSystem::render(Graphics &gfx) { item.quad = sp->quad; item.shader = sp->shader; item.canvas = sp->canvas; - item.cam = fromEntity(camEnt); + item.camera = camEnt; + item.camValid = cam.valid; + item.camX = cam.x; + item.camY = cam.y; + item.camZoom = cam.zoom; + item.camClear = cam.clearColor; + item.camAmbientR = cam.ambientR; + item.camAmbientG = cam.ambientG; + item.camAmbientB = cam.ambientB; item.receiveLight = sp->receiveLight; item.litPath = sp->receiveLight && sp->normalTexture != nullptr && sp->texture != nullptr && sp->shader == nullptr; - items.push_back(item); + if (item.quad && item.texture) { + item.hasUV = true; + item.quad->getUV(item.texture->getWidth(), item.texture->getHeight(), item.u0, item.v0, + item.u1, item.v1); + } + out.push_back(item); } +} - std::stable_sort(items.begin(), items.end(), [](const Item &a, const Item &b) { - const bool aOff = a.canvas != nullptr; - const bool bOff = b.canvas != nullptr; - if (aOff != bOff) return aOff && !bOff; - if (a.canvas != b.canvas) return a.canvas < b.canvas; - if (a.layer != b.layer) return a.layer < b.layer; - if (a.litPath != b.litPath) return !a.litPath && b.litPath; // unlit first - if (a.shader != b.shader) return a.shader < b.shader; - if (a.texture != b.texture) return a.texture < b.texture; - return a.normal < b.normal; - }); +void RenderSystem::drawItems(Graphics &gfx, std::vector &items, bool present) { + sortDrawItems2D(items); + + std::unordered_map defaultCam; + if (ecs::current()->getManager() != nullptr) { + auto camView = ecs::View(); + for (auto it = camView.begin(); it != camView.end(); ++it) { + auto [data] = *it; + if (!data->active) continue; + if (!data->entity) continue; + Canvas *key = data->canvas; + if (defaultCam.find(key) == defaultCam.end()) defaultCam[key] = data->entity; + } + } Canvas *current = reinterpret_cast(static_cast(1)); Lighting2DUBO currentLights{}; @@ -235,10 +239,21 @@ void RenderSystem::render(Graphics &gfx) { if (defIt != defaultCam.end()) { groupCam = fromEntity(defIt->second); clearCol = groupCam.clearColor; + } else if (items[i].camValid) { + groupCam.valid = true; + groupCam.x = items[i].camX; + groupCam.y = items[i].camY; + groupCam.zoom = items[i].camZoom; + groupCam.clearColor = items[i].camClear; + groupCam.ambientR = items[i].camAmbientR; + groupCam.ambientG = items[i].camAmbientG; + groupCam.ambientB = items[i].camAmbientB; + clearCol = groupCam.clearColor; } gfx.setCanvas(next); - gfx.clear(clearCol, std::nullopt, std::nullopt); + // Only clear when presenting a full sprite pass; map-unified draws must not wipe. + if (present) gfx.clear(clearCol, std::nullopt, std::nullopt); current = next; int viewW = next ? next->getWidth() : gfx.getWidth(); @@ -250,13 +265,25 @@ void RenderSystem::render(Graphics &gfx) { } const auto &it = items[i]; + ViewCam cam{}; + if (it.camValid) { + cam.valid = true; + cam.x = it.camX; + cam.y = it.camY; + cam.zoom = it.camZoom; + } else if (it.camera) { + cam = fromEntity(it.camera); + } int viewW = it.canvas ? it.canvas->getWidth() : gfx.getWidth(); int viewH = it.canvas ? it.canvas->getHeight() : gfx.getHeight(); float sx, sy, sw, sh; - applyCamera(it.x, it.y, it.w, it.h, it.cam, viewW, viewH, sx, sy, sw, sh); + applyCamera(it.x, it.y, it.w, it.h, cam, viewW, viewH, sx, sy, sw, sh); - float u0 = 0.f, v0 = 0.f, u1 = 1.f, v1 = 1.f; - if (it.quad && it.texture) + float u0 = it.hasUV ? it.u0 : 0.f; + float v0 = it.hasUV ? it.v0 : 0.f; + float u1 = it.hasUV ? it.u1 : 1.f; + float v1 = it.hasUV ? it.v1 : 1.f; + if (!it.hasUV && it.quad && it.texture) it.quad->getUV(it.texture->getWidth(), it.texture->getHeight(), u0, v0, u1, v1); if (it.litPath) { @@ -275,8 +302,16 @@ void RenderSystem::render(Graphics &gfx) { } } - gfx.setCanvas(); - gfx.present(); + if (present) { + gfx.setCanvas(); + gfx.present(); + } +} + +void RenderSystem::render(Graphics &gfx) { + std::vector items; + collectSprites(items); + drawItems(gfx, items, true); } } // namespace eve::graphics diff --git a/src/modules/graphics/RenderSystem.h b/src/modules/graphics/RenderSystem.h index a27431f3..87482c6a 100644 --- a/src/modules/graphics/RenderSystem.h +++ b/src/modules/graphics/RenderSystem.h @@ -1,11 +1,13 @@ #pragma once #include "common/ECS.h" +#include "graphics/DrawItem2D.h" #include "graphics/Quad.h" #include "graphics/Shader.h" #include "graphics/Texture.h" #include "zeroerr/assert.h" #include +#include namespace eve::graphics { @@ -82,7 +84,17 @@ class Graphics; /** Walks ECS Renderable2D views and draws via Graphics batch path. */ class RenderSystem { public: + /** Full sprite pass + present (existing tests / sprite-only scenes). */ static void render(Graphics &gfx); + + /** Append visible Renderable2D sprites into a shared queue. */ + static void collectSprites(std::vector &out); + + /** + * Sort and draw items. If present=true, calls gfx.present() at the end. + * Map::render uses present=false so the frame can continue drawing. + */ + static void drawItems(Graphics &gfx, std::vector &items, bool present); }; } // namespace eve::graphics diff --git a/src/modules/graphics/RenderSystem3D.cpp b/src/modules/graphics/RenderSystem3D.cpp index 6a1d5f89..dae92930 100644 --- a/src/modules/graphics/RenderSystem3D.cpp +++ b/src/modules/graphics/RenderSystem3D.cpp @@ -27,7 +27,7 @@ glm::mat4 modelFromTransform(const Renderable3D::Transform3D &t) { } Camera3D *findDefaultCamera3D() { - if (ecs::ComponentManager::inst().registy == nullptr) return nullptr; + if (ecs::current()->getManager() == nullptr) return nullptr; auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; @@ -44,7 +44,7 @@ struct PackedLight3D { void collectLights3D(std::vector &out, size_t maxCount) { out.clear(); - if (ecs::ComponentManager::inst().registy == nullptr) return; + if (ecs::current()->getManager() == nullptr) return; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [d] = *it; @@ -264,7 +264,7 @@ void RenderSystem3D::render(Graphics &gfx) { glm::vec3(cd->upX, cd->upY, cd->upZ), fovRad, aspect, cd->nearZ, cd->farZ, shadowCaster->shadowBias, shadowCaster->shadowStrength); - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto casterView = ecs::View(); for (int c = 0; c < ShadowConfig::kCascades; ++c) { @@ -294,7 +294,7 @@ void RenderSystem3D::render(Graphics &gfx) { if (!gfx.had3DThisFrame()) return; - if (ecs::ComponentManager::inst().registy == nullptr) return; + if (ecs::current()->getManager() == nullptr) return; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { diff --git a/src/modules/map/Map.cpp b/src/modules/map/Map.cpp index cf55ad62..51208924 100644 --- a/src/modules/map/Map.cpp +++ b/src/modules/map/Map.cpp @@ -2,6 +2,7 @@ #include "map/TileSystem.h" #include "map/TileConfig.h" #include "graphics/Graphics.h" +#include "graphics/RenderSystem.h" #include "common/Module.h" #include @@ -15,12 +16,16 @@ TileLayer *Map::newLayer(int mapW, int mapH, float tileW, float tileH) { } TileLayer *Map::newLayerFromFile(const std::string &path) { - auto layers = loadMapFile(path, nullptr); + std::vector objs; + auto layers = loadMapFile(path, &objs, nullptr); + setObjects(std::move(objs)); return layers.empty() ? nullptr : layers.front(); } int Map::loadFromFile(const std::string &path) { - auto layers = loadMapFile(path, nullptr); + std::vector objs; + auto layers = loadMapFile(path, &objs, nullptr); + setObjects(std::move(objs)); return int(layers.size()); } @@ -29,18 +34,63 @@ void Map::update(float dt) { TileConfigSystem::poll(); } -void Map::render(graphics::Graphics *gfx) { TileRenderSystem::render(gfx); } +void Map::render(graphics::Graphics *gfx) { + if (!gfx) return; + std::vector items; + graphics::RenderSystem::collectSprites(items); + TileRenderSystem::collect(items); + graphics::RenderSystem::drawItems(*gfx, items, false); +} int Map::pollConfigs() { return TileConfigSystem::poll(); } int Map::getLayerCount() const { - if (ecs::ComponentManager::inst().registy == nullptr) return 0; + if (ecs::current()->getManager() == nullptr) return 0; int n = 0; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) ++n; return n; } +void Map::setObjects(std::vector objects) { objects_ = std::move(objects); } + +int Map::getObjectCount() const { return int(objects_.size()); } + +std::string Map::getObjectName(int i) const { + if (i < 0 || i >= int(objects_.size())) return {}; + return objects_[size_t(i)].name; +} + +std::string Map::getObjectType(int i) const { + if (i < 0 || i >= int(objects_.size())) return {}; + return objects_[size_t(i)].type; +} + +float Map::getObjectX(int i) const { + if (i < 0 || i >= int(objects_.size())) return 0.f; + return objects_[size_t(i)].x; +} + +float Map::getObjectY(int i) const { + if (i < 0 || i >= int(objects_.size())) return 0.f; + return objects_[size_t(i)].y; +} + +float Map::getObjectWidth(int i) const { + if (i < 0 || i >= int(objects_.size())) return 0.f; + return objects_[size_t(i)].width; +} + +float Map::getObjectHeight(int i) const { + if (i < 0 || i >= int(objects_.size())) return 0.f; + return objects_[size_t(i)].height; +} + +int Map::getObjectGid(int i) const { + if (i < 0 || i >= int(objects_.size())) return 0; + return int(objects_[size_t(i)].gid); +} + void Map::expose(ssq::Table &table) { auto cls = table.addClass(name, Map::create, false); expose(cls); @@ -78,6 +128,11 @@ void Map::expose(ssq::Table &table) { layer.addFunc("setAutoReload", &TileLayer::setAutoReload); layer.addFunc("getAutoReload", &TileLayer::getAutoReload); layer.addFunc("getConfigPath", &TileLayer::getConfigPath); + layer.addFunc("tileToWorldX", &TileLayer::tileToWorldX); + layer.addFunc("tileToWorldY", &TileLayer::tileToWorldY); + layer.addFunc("depthYAt", &TileLayer::depthYAt); + layer.addFunc("worldToTileX", &TileLayer::worldToTileX); + layer.addFunc("worldToTileY", &TileLayer::worldToTileY); } void Map::expose(ssq::Class &cls) { @@ -89,6 +144,14 @@ void Map::expose(ssq::Class &cls) { cls.addFunc("render", &Map::render); cls.addFunc("pollConfigs", &Map::pollConfigs); cls.addFunc("getLayerCount", &Map::getLayerCount); + cls.addFunc("getObjectCount", &Map::getObjectCount); + cls.addFunc("getObjectName", &Map::getObjectName); + cls.addFunc("getObjectType", &Map::getObjectType); + cls.addFunc("getObjectX", &Map::getObjectX); + cls.addFunc("getObjectY", &Map::getObjectY); + cls.addFunc("getObjectWidth", &Map::getObjectWidth); + cls.addFunc("getObjectHeight", &Map::getObjectHeight); + cls.addFunc("getObjectGid", &Map::getObjectGid); } } // namespace eve::map diff --git a/src/modules/map/Map.h b/src/modules/map/Map.h index 64797761..dad57b7c 100644 --- a/src/modules/map/Map.h +++ b/src/modules/map/Map.h @@ -2,6 +2,7 @@ #include "common/Module.h" #include "map/TileLayer.h" +#include "map/MapObject.h" #include #include @@ -14,7 +15,7 @@ namespace eve::map { /** * Map module — factory + script binding for 2D tilemaps. - * Per-frame: TileConfigSystem (hot reload) → TileRenderSystem. + * Per-frame: TileConfigSystem (hot reload) → unified sprite+tile render. */ class Map : public Module { public: @@ -25,8 +26,9 @@ public: TileLayer *newLayer(int mapW, int mapH, float tileW = 32.f, float tileH = 32.f); /** - * Load map JSON (Tiled-compatible orthogonal subset or EVEngine simplified format). + * Load map JSON (Tiled-compatible subset or EVEngine simplified format). * Creates one TileLayer per tile layer; returns the first (nullptr on failure). + * Refreshes the module object cache from objectgroup layers. */ TileLayer *newLayerFromFile(const std::string &path); @@ -38,6 +40,21 @@ public: int pollConfigs(); int getLayerCount() const; + + int getObjectCount() const; + std::string getObjectName(int i) const; + std::string getObjectType(int i) const; + float getObjectX(int i) const; + float getObjectY(int i) const; + float getObjectWidth(int i) const; + float getObjectHeight(int i) const; + int getObjectGid(int i) const; + + /** Replace object cache (used by load / hot reload). */ + void setObjects(std::vector objects); + +private: + std::vector objects_; }; } // namespace eve::map diff --git a/src/modules/map/MapObject.h b/src/modules/map/MapObject.h new file mode 100644 index 00000000..a5cbc64f --- /dev/null +++ b/src/modules/map/MapObject.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace eve::map { + +struct MapObject { + std::string name; + std::string type; + float x = 0.f; + float y = 0.f; + float width = 0.f; + float height = 0.f; + uint32_t gid = 0; +}; + +} // namespace eve::map diff --git a/src/modules/map/TileConfig.cpp b/src/modules/map/TileConfig.cpp index 384268b4..16a0f10c 100644 --- a/src/modules/map/TileConfig.cpp +++ b/src/modules/map/TileConfig.cpp @@ -1,4 +1,5 @@ #include "map/TileConfig.h" +#include "map/TileOrientation.h" #include "common/Module.h" #include "data/DataModule.h" @@ -145,17 +146,85 @@ void applyTileset(TileLayer *layer, const TilesetInfo &info) { layer->setTileset(tex, info.firstGid, info.columns, info.margin, info.spacing); } -bool readDataArray(Poco::JSON::Object::Ptr layerObj, std::vector &out) { - if (!layerObj || !layerObj->has("data")) return false; - Poco::JSON::Array::Ptr arr; - try { - arr = layerObj->getArray("data"); - } catch (...) { +bool decodeLayerData(Poco::JSON::Object::Ptr layerObj, size_t expectedCount, + std::vector &out, std::string *error) { + if (!layerObj || !layerObj->has("data")) { + if (error) *error = "missing data"; return false; } - if (!arr) return false; - out.resize(arr->size()); - for (size_t i = 0; i < arr->size(); ++i) out[i] = uint32_t(asInt(arr->get(i), 0)); + + try { + if (layerObj->isArray("data")) { + auto arr = layerObj->getArray("data"); + if (!arr) { + if (error) *error = "invalid data array"; + return false; + } + out.resize(arr->size()); + for (size_t i = 0; i < arr->size(); ++i) out[i] = uint32_t(asInt(arr->get(i), 0)); + if (expectedCount > 0 && out.size() != expectedCount) { + if (error) *error = "gid count mismatch"; + return false; + } + return true; + } + } catch (...) { + } + + std::string encoding = layerObj->has("encoding") ? asString(layerObj->get("encoding")) : ""; + std::string compression = + layerObj->has("compression") ? asString(layerObj->get("compression")) : ""; + if (encoding != "base64") { + if (error) *error = "unsupported encoding"; + return false; + } + if (compression == "zstd") { + if (error) *error = "zstd not supported; export as zlib"; + return false; + } + + const std::string b64 = asString(layerObj->get("data")); + size_t decodedLen = 0; + std::unique_ptr decoded(eve::data::decode("base64", b64.data(), b64.size(), decodedLen)); + if (!decoded) { + if (error) *error = "base64 decode failed"; + return false; + } + + const char *bytes = decoded.get(); + size_t nbytes = decodedLen; + std::unique_ptr inflated; + if (!compression.empty()) { + if (compression != "zlib" && compression != "gzip") { + if (error) *error = "unsupported compression"; + return false; + } + size_t rawsize = expectedCount * 4; + try { + inflated.reset(eve::data::decompress(compression, bytes, nbytes, rawsize)); + } catch (...) { + if (error) *error = "decompress failed"; + return false; + } + if (!inflated) { + if (error) *error = "decompress failed"; + return false; + } + bytes = inflated.get(); + nbytes = rawsize; + } + + if (expectedCount > 0 && nbytes != expectedCount * 4) { + if (error) *error = "gid byte length mismatch"; + return false; + } + const size_t count = nbytes / 4; + out.resize(count); + for (size_t i = 0; i < count; ++i) { + const unsigned char *p = reinterpret_cast(bytes) + i * 4; + out[i] = uint32_t(p[0]) | (uint32_t(p[1]) << 8) | (uint32_t(p[2]) << 16) | + (uint32_t(p[3]) << 24); + } return true; } @@ -163,7 +232,6 @@ void applyLayerDraw(TileLayer *layer, Poco::JSON::Object::Ptr layerObj) { if (!layer || !layerObj) return; if (layerObj->has("visible")) layer->setVisible(asBool(layerObj->get("visible"), true)); - // Tiled uses "opacity"; EVEngine also accepts "layer" for render sort. if (layerObj->has("layer")) layer->setLayer(asInt(layerObj->get("layer"), layer->getLayer())); else if (layerObj->has("id")) @@ -194,8 +262,43 @@ bool isTileLayerObject(Poco::JSON::Object::Ptr o) { return o->has("data"); } -void applyMapGlobals(TileLayer *layer, Poco::JSON::Object::Ptr root) { - if (!layer || !root) return; +bool isObjectGroup(Poco::JSON::Object::Ptr o) { + if (!o) return false; + if (!o->has("type")) return false; + return asString(o->get("type")) == "objectgroup"; +} + +bool parseOrientation(Poco::JSON::Object::Ptr root, TileLayer::Config *cfg, std::string *error) { + if (!root || !cfg) return false; + if (!root->has("orientation")) return true; + const std::string o = asString(root->get("orientation")); + if (o.empty() || o == "orthogonal") + cfg->orientation = MapOrientation::Orthogonal; + else if (o == "isometric") + cfg->orientation = MapOrientation::Isometric; + else if (o == "staggered") + cfg->orientation = MapOrientation::Staggered; + else if (o == "hexagonal") + cfg->orientation = MapOrientation::Hexagonal; + else { + if (error) *error = "unknown orientation: " + o; + return false; + } + if (root->has("staggeraxis")) { + const std::string a = asString(root->get("staggeraxis")); + cfg->staggerAxis = (a == "x") ? StaggerAxis::X : StaggerAxis::Y; + } + if (root->has("staggerindex")) { + const std::string i = asString(root->get("staggerindex")); + cfg->staggerIndex = (i == "even") ? StaggerIndex::Even : StaggerIndex::Odd; + } + if (root->has("hexsidelength")) + cfg->hexSideLength = asFloat(root->get("hexsidelength"), 0.f); + return true; +} + +bool applyMapGlobals(TileLayer *layer, Poco::JSON::Object::Ptr root, std::string *error) { + if (!layer || !root) return false; int mapW = layer->getMapWidth(); int mapH = layer->getMapHeight(); @@ -212,6 +315,8 @@ void applyMapGlobals(TileLayer *layer, Poco::JSON::Object::Ptr root) { if (mapW != layer->getMapWidth() || mapH != layer->getMapHeight()) layer->resize(mapW, mapH); layer->setTileSize(tileW, tileH); + if (!parseOrientation(root, &(*layer->config()), error)) return false; + float ox = layer->getX(), oy = layer->getY(); if (readVec2(root, "origin", ox, oy)) layer->setOrigin(ox, oy); @@ -231,37 +336,44 @@ void applyMapGlobals(TileLayer *layer, Poco::JSON::Object::Ptr root) { float r = 1, g = 1, b = 1, a = 1; if (readVec4(root, "tint", r, g, b, a)) layer->setTint(r, g, b, a); - // Single tileset object / image shortcut. if (root->has("tileset") && !root->isArray("tileset")) { try { - applyTileset(layer, readTilesetObject(root->getObject("tileset"))); + auto o = root->getObject("tileset"); + if (o && !o->has("source")) applyTileset(layer, readTilesetObject(o)); } catch (...) { } } else if (root->has("tilesets")) { try { auto arr = root->getArray("tilesets"); - if (arr && arr->size() > 0) { - try { - applyTileset(layer, readTilesetObject(arr->getObject(0))); - } catch (...) { + if (arr) { + for (size_t i = 0; i < arr->size(); ++i) { + try { + auto o = arr->getObject(i); + if (o && o->has("source")) continue; + applyTileset(layer, readTilesetObject(o)); + break; + } catch (...) { + } } } } catch (...) { } } else if (root->has("image") || root->has("texture")) { - TilesetInfo info = readTilesetObject(root); - applyTileset(layer, info); + applyTileset(layer, readTilesetObject(root)); } + return true; } -bool applyFlatLayerData(TileLayer *layer, Poco::JSON::Object::Ptr root) { - // Flat single-layer: data at root. - std::vector data; - if (!readDataArray(root, data)) return false; +bool applyFlatLayerData(TileLayer *layer, Poco::JSON::Object::Ptr root, std::string *error) { const int mapW = layer->getMapWidth(); const int mapH = layer->getMapHeight(); const size_t need = size_t(std::max(0, mapW) * std::max(0, mapH)); - if (need == 0) return false; + if (need == 0) { + if (error) *error = "empty map size"; + return false; + } + std::vector data; + if (!decodeLayerData(root, need, data, error)) return false; auto &gids = layer->tiles()->gids; gids.assign(need, 0u); const size_t n = std::min(need, data.size()); @@ -269,16 +381,18 @@ bool applyFlatLayerData(TileLayer *layer, Poco::JSON::Object::Ptr root) { return true; } -bool applyOneLayerObject(TileLayer *layer, Poco::JSON::Object::Ptr layerObj, int mapW, int mapH) { +bool applyOneLayerObject(TileLayer *layer, Poco::JSON::Object::Ptr layerObj, int mapW, int mapH, + std::string *error) { if (!layer || !layerObj) return false; int w = mapW, h = mapH; if (layerObj->has("width")) w = asInt(layerObj->get("width"), w); if (layerObj->has("height")) h = asInt(layerObj->get("height"), h); if (w != layer->getMapWidth() || h != layer->getMapHeight()) layer->resize(w, h); + const size_t need = + size_t(std::max(0, layer->getMapWidth()) * std::max(0, layer->getMapHeight())); std::vector data; - if (!readDataArray(layerObj, data)) return false; - const size_t need = size_t(std::max(0, layer->getMapWidth()) * std::max(0, layer->getMapHeight())); + if (!decodeLayerData(layerObj, need, data, error)) return false; auto &gids = layer->tiles()->gids; gids.assign(need, 0u); const size_t n = std::min(need, data.size()); @@ -288,6 +402,176 @@ bool applyOneLayerObject(TileLayer *layer, Poco::JSON::Object::Ptr layerObj, int return true; } +void parseObjectGroup(Poco::JSON::Object::Ptr group, std::vector &out) { + if (!group || !group->has("objects")) return; + Poco::JSON::Array::Ptr arr; + try { + arr = group->getArray("objects"); + } catch (...) { + return; + } + if (!arr) return; + for (size_t i = 0; i < arr->size(); ++i) { + Poco::JSON::Object::Ptr o; + try { + o = arr->getObject(i); + } catch (...) { + continue; + } + if (!o) continue; + MapObject mo; + if (o->has("name")) mo.name = asString(o->get("name")); + if (o->has("type")) mo.type = asString(o->get("type")); + else if (o->has("class")) mo.type = asString(o->get("class")); + mo.x = o->has("x") ? asFloat(o->get("x"), 0.f) : 0.f; + mo.y = o->has("y") ? asFloat(o->get("y"), 0.f) : 0.f; + mo.width = o->has("width") ? asFloat(o->get("width"), 0.f) : 0.f; + mo.height = o->has("height") ? asFloat(o->get("height"), 0.f) : 0.f; + if (o->has("gid")) mo.gid = uint32_t(asInt(o->get("gid"), 0)); + out.push_back(std::move(mo)); + } +} + +void abandonLayers(std::vector &layers) { + for (TileLayer *layer : layers) { + if (!layer) continue; + layer->clear(); + layer->setVisible(false); + } + layers.clear(); +} + +TilesetInfo readDefaultTileset(Poco::JSON::Object::Ptr root) { + TilesetInfo defaultTs; + if (root->has("tilesets")) { + try { + auto arr = root->getArray("tilesets"); + if (arr) { + for (size_t i = 0; i < arr->size(); ++i) { + try { + auto o = arr->getObject(i); + if (o && o->has("source")) continue; + return readTilesetObject(o); + } catch (...) { + } + } + } + } catch (...) { + } + } else if (root->has("tileset") && !root->isArray("tileset")) { + try { + auto o = root->getObject("tileset"); + if (o && !o->has("source")) return readTilesetObject(o); + } catch (...) { + } + } else if (root->has("image") || root->has("texture")) { + return readTilesetObject(root); + } + return defaultTs; +} + +std::vector loadMapObject(Poco::JSON::Object::Ptr root, const std::string &path, + eve::filesystem::Filesystem *fs, + std::vector *objects, std::string *error) { + std::vector out; + if (!root) { + if (error) *error = "config root must be object"; + return out; + } + + int mapW = 10, mapH = 10; + float tileW = 32.f, tileH = 32.f; + if (root->has("width")) mapW = asInt(root->get("width"), mapW); + if (root->has("height")) mapH = asInt(root->get("height"), mapH); + if (root->has("tilewidth")) tileW = asFloat(root->get("tilewidth"), tileW); + else if (root->has("tileWidth")) tileW = asFloat(root->get("tileWidth"), tileW); + if (root->has("tileheight")) tileH = asFloat(root->get("tileheight"), tileH); + else if (root->has("tileHeight")) tileH = asFloat(root->get("tileHeight"), tileH); + + // Validate orientation early on a temp config. + TileLayer::Config orientCheck; + if (!parseOrientation(root, &orientCheck, error)) return out; + + TilesetInfo defaultTs = readDefaultTileset(root); + if (objects) objects->clear(); + + auto bindResource = [&](TileLayer *layer) { + auto res = layer->resource(); + res->path = path; + if (!path.empty()) { + res->modtime = fileModtime(path); + if (fs) { + fs->watch(path); + if (auto *hot = + eve::ModuleManager::getInstance("HotReload")) + hot->bind(path, "tilemap"); + } + } + if (root->has("autoReload")) + res->autoReload = asBool(root->get("autoReload"), true); + }; + + if (root->has("layers")) { + try { + auto arr = root->getArray("layers"); + if (arr) { + int sort = 0; + for (size_t i = 0; i < arr->size(); ++i) { + Poco::JSON::Object::Ptr lo; + try { + lo = arr->getObject(i); + } catch (...) { + continue; + } + if (objects && isObjectGroup(lo)) { + parseObjectGroup(lo, *objects); + continue; + } + if (!isTileLayerObject(lo)) continue; + TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH); + if (!applyMapGlobals(layer, root, error)) { + abandonLayers(out); + layer->clear(); + layer->setVisible(false); + return {}; + } + applyTileset(layer, defaultTs); + if (!applyOneLayerObject(layer, lo, mapW, mapH, error)) { + abandonLayers(out); + layer->clear(); + layer->setVisible(false); + return {}; + } + if (!lo->has("layer") && !lo->has("id")) layer->setLayer(sort); + ++sort; + bindResource(layer); + out.push_back(layer); + } + } + } catch (...) { + } + if (!out.empty()) return out; + } + + TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH); + if (!applyMapGlobals(layer, root, error)) { + layer->clear(); + layer->setVisible(false); + return {}; + } + applyTileset(layer, defaultTs); + if (root->has("data")) { + if (!applyFlatLayerData(layer, root, error)) { + layer->clear(); + layer->setVisible(false); + return {}; + } + } + bindResource(layer); + out.push_back(layer); + return out; +} + } // namespace bool applyConfigDocument(TileLayer *layer, data::JsonDocument *doc) { @@ -295,9 +579,9 @@ bool applyConfigDocument(TileLayer *layer, data::JsonDocument *doc) { auto root = doc->object(); if (!root) return false; - applyMapGlobals(layer, root); + std::string err; + if (!applyMapGlobals(layer, root, &err)) return false; - // Prefer nested layers[0]; fall back to root.data. if (root->has("layers")) { try { auto arr = root->getArray("layers"); @@ -311,16 +595,14 @@ bool applyConfigDocument(TileLayer *layer, data::JsonDocument *doc) { } if (!isTileLayerObject(lo)) continue; return applyOneLayerObject(layer, lo, layer->getMapWidth(), - layer->getMapHeight()); + layer->getMapHeight(), nullptr); } } } catch (...) { } } - if (root->has("data")) return applyFlatLayerData(layer, root); - - // Globals-only config still succeeds (empty map). + if (root->has("data")) return applyFlatLayerData(layer, root, nullptr); return true; } @@ -332,10 +614,36 @@ bool applyConfigText(TileLayer *layer, const std::string &json, std::string *err if (error) *error = err.empty() ? "invalid json" : err; return false; } - if (!applyConfigDocument(layer, doc.get())) { + if (!doc->isObject()) { if (error) *error = "config root must be object"; return false; } + auto root = doc->object(); + if (!root) { + if (error) *error = "config root must be object"; + return false; + } + if (!applyMapGlobals(layer, root, error)) return false; + if (root->has("layers")) { + try { + auto arr = root->getArray("layers"); + if (arr) { + for (size_t i = 0; i < arr->size(); ++i) { + Poco::JSON::Object::Ptr lo; + try { + lo = arr->getObject(i); + } catch (...) { + continue; + } + if (!isTileLayerObject(lo)) continue; + return applyOneLayerObject(layer, lo, layer->getMapWidth(), + layer->getMapHeight(), error); + } + } + } catch (...) { + } + } + if (root->has("data")) return applyFlatLayerData(layer, root, error); return true; } @@ -381,11 +689,23 @@ bool reloadConfigFile(TileLayer *layer, std::string *error) { return loadConfigFile(layer, path, error); } -std::vector loadMapFile(const std::string &path, std::string *error) { - std::vector out; +std::vector loadMapText(const std::string &json, std::vector *objects, + std::string *error) { + auto *dm = eve::data::DataModule::create(); + std::string err; + std::unique_ptr doc(dm->decodeJson(json, &err)); + if (!doc || !doc->isObject()) { + if (error) *error = err.empty() ? "invalid json" : err; + return {}; + } + return loadMapObject(doc->object(), {}, nullptr, objects, error); +} + +std::vector loadMapFile(const std::string &path, std::vector *objects, + std::string *error) { if (path.empty()) { if (error) *error = "empty path"; - return out; + return {}; } auto *fs = eve::ModuleManager::getInstance("Filesystem"); @@ -396,11 +716,11 @@ std::vector loadMapFile(const std::string &path, std::string *error data.reset(fs->read(path)); } catch (...) { if (error) *error = "read failed: " + path; - return out; + return {}; } if (!data || data->getSize() == 0) { if (error) *error = "empty file: " + path; - return out; + return {}; } std::string text(static_cast(data->getData()), data->getSize()); @@ -409,87 +729,13 @@ std::vector loadMapFile(const std::string &path, std::string *error std::unique_ptr doc(dm->decodeJson(text, &err)); if (!doc || !doc->isObject()) { if (error) *error = err.empty() ? "invalid json" : err; - return out; - } - auto root = doc->object(); - if (!root) { - if (error) *error = "config root must be object"; - return out; + return {}; } + return loadMapObject(doc->object(), path, fs, objects, error); +} - int mapW = 10, mapH = 10; - float tileW = 32.f, tileH = 32.f; - if (root->has("width")) mapW = asInt(root->get("width"), mapW); - if (root->has("height")) mapH = asInt(root->get("height"), mapH); - if (root->has("tilewidth")) tileW = asFloat(root->get("tilewidth"), tileW); - else if (root->has("tileWidth")) tileW = asFloat(root->get("tileWidth"), tileW); - if (root->has("tileheight")) tileH = asFloat(root->get("tileheight"), tileH); - else if (root->has("tileHeight")) tileH = asFloat(root->get("tileHeight"), tileH); - - TilesetInfo defaultTs; - if (root->has("tilesets")) { - try { - auto arr = root->getArray("tilesets"); - if (arr && arr->size() > 0) defaultTs = readTilesetObject(arr->getObject(0)); - } catch (...) { - } - } else if (root->has("tileset") && !root->isArray("tileset")) { - try { - defaultTs = readTilesetObject(root->getObject("tileset")); - } catch (...) { - } - } else if (root->has("image") || root->has("texture")) { - defaultTs = readTilesetObject(root); - } - - auto bindResource = [&](TileLayer *layer) { - auto res = layer->resource(); - res->path = path; - res->modtime = fileModtime(path); - if (root->has("autoReload")) - res->autoReload = asBool(root->get("autoReload"), true); - fs->watch(path); - if (auto *hot = eve::ModuleManager::getInstance("HotReload")) - hot->bind(path, "tilemap"); - }; - - // Multi-layer document. - if (root->has("layers")) { - try { - auto arr = root->getArray("layers"); - if (arr) { - int sort = 0; - for (size_t i = 0; i < arr->size(); ++i) { - Poco::JSON::Object::Ptr lo; - try { - lo = arr->getObject(i); - } catch (...) { - continue; - } - if (!isTileLayerObject(lo)) continue; - TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH); - applyMapGlobals(layer, root); - applyTileset(layer, defaultTs); - applyOneLayerObject(layer, lo, mapW, mapH); - if (!lo->has("layer") && !lo->has("id")) layer->setLayer(sort); - ++sort; - bindResource(layer); - out.push_back(layer); - } - } - } catch (...) { - } - if (!out.empty()) return out; - } - - // Flat single-layer (root.data) or empty map shell. - TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH); - applyMapGlobals(layer, root); - applyTileset(layer, defaultTs); - if (root->has("data")) applyFlatLayerData(layer, root); - bindResource(layer); - out.push_back(layer); - return out; +std::vector loadMapFile(const std::string &path, std::string *error) { + return loadMapFile(path, nullptr, error); } } // namespace eve::map diff --git a/src/modules/map/TileConfig.h b/src/modules/map/TileConfig.h index 753eac96..fb286515 100644 --- a/src/modules/map/TileConfig.h +++ b/src/modules/map/TileConfig.h @@ -1,6 +1,7 @@ #pragma once #include "map/TileLayer.h" +#include "map/MapObject.h" #include #include @@ -13,7 +14,7 @@ namespace eve::map { /** * Apply map/layer JSON onto an existing TileLayer (Config + Tiles + Tileset + Draw). - * Multi-layer documents only apply the first orthogonal tile layer when called on one entity. + * Multi-layer documents only apply the first tile layer when called on one entity. */ bool applyConfigDocument(TileLayer *layer, data::JsonDocument *doc); @@ -30,8 +31,15 @@ bool reloadConfigFile(TileLayer *layer, std::string *error = nullptr); /** * Load a (possibly multi-layer) map JSON. Creates one TileLayer per tile layer. + * Optionally fills `objects` from objectgroup layers. * Returns created layers (empty on failure). All share the same Resource.path for reload. */ std::vector loadMapFile(const std::string &path, std::string *error = nullptr); +std::vector loadMapFile(const std::string &path, std::vector *objects, + std::string *error = nullptr); + +/** Parse map JSON text (no filesystem). Same semantics as loadMapFile. */ +std::vector loadMapText(const std::string &json, std::vector *objects, + std::string *error = nullptr); } // namespace eve::map diff --git a/src/modules/map/TileLayer.cpp b/src/modules/map/TileLayer.cpp index 38da604e..c464db95 100644 --- a/src/modules/map/TileLayer.cpp +++ b/src/modules/map/TileLayer.cpp @@ -1,5 +1,6 @@ #include "map/TileLayer.h" #include "map/TileConfig.h" +#include "map/TileProjection.h" #include @@ -112,4 +113,40 @@ void TileLayer::setAutoReload(bool enable) { resource()->autoReload = enable; } bool TileLayer::getAutoReload() { return resource()->autoReload; } std::string TileLayer::getConfigPath() { return resource()->path; } +void TileLayer::tileToWorld(int tx, int ty, float &wx, float &wy) { + eve::map::tileToWorld(*config(), tx, ty, wx, wy); +} + +float TileLayer::depthY(int tx, int ty) { return tileToDepthY(*config(), tx, ty); } + +void TileLayer::worldToTile(float wx, float wy, int &tx, int &ty) { + eve::map::worldToTile(*config(), wx, wy, tx, ty); +} + +float TileLayer::tileToWorldX(int tx, int ty) { + float wx = 0.f, wy = 0.f; + tileToWorld(tx, ty, wx, wy); + return wx; +} + +float TileLayer::tileToWorldY(int tx, int ty) { + float wx = 0.f, wy = 0.f; + tileToWorld(tx, ty, wx, wy); + return wy; +} + +float TileLayer::depthYAt(int tx, int ty) { return depthY(tx, ty); } + +int TileLayer::worldToTileX(float wx, float wy) { + int tx = 0, ty = 0; + worldToTile(wx, wy, tx, ty); + return tx; +} + +int TileLayer::worldToTileY(float wx, float wy) { + int tx = 0, ty = 0; + worldToTile(wx, wy, tx, ty); + return ty; +} + } // namespace eve::map diff --git a/src/modules/map/TileLayer.h b/src/modules/map/TileLayer.h index 8b24ef7c..84fb250c 100644 --- a/src/modules/map/TileLayer.h +++ b/src/modules/map/TileLayer.h @@ -3,6 +3,7 @@ #include "common/ECS.h" #include "graphics/Canvas.h" #include "graphics/Texture.h" +#include "map/TileOrientation.h" #include #include @@ -35,6 +36,10 @@ public: float tileH = 32.f; float originX = 0.f; float originY = 0.f; + MapOrientation orientation = MapOrientation::Orthogonal; + StaggerAxis staggerAxis = StaggerAxis::Y; + StaggerIndex staggerIndex = StaggerIndex::Odd; + float hexSideLength = 0.f; TileLayer *entity = nullptr; }; @@ -116,6 +121,16 @@ public: void setAutoReload(bool enable); bool getAutoReload(); std::string getConfigPath(); + + void tileToWorld(int tx, int ty, float &wx, float &wy); + float depthY(int tx, int ty); + void worldToTile(float wx, float wy, int &tx, int &ty); + + float tileToWorldX(int tx, int ty); + float tileToWorldY(int tx, int ty); + float depthYAt(int tx, int ty); + int worldToTileX(float wx, float wy); + int worldToTileY(float wx, float wy); }; /** Strip Tiled flip / rotate flags; keep low 28 bits. */ diff --git a/src/modules/map/TileOrientation.h b/src/modules/map/TileOrientation.h new file mode 100644 index 00000000..22b77bbd --- /dev/null +++ b/src/modules/map/TileOrientation.h @@ -0,0 +1,9 @@ +#pragma once + +namespace eve::map { + +enum class MapOrientation { Orthogonal, Isometric, Staggered, Hexagonal }; +enum class StaggerAxis { X, Y }; +enum class StaggerIndex { Odd, Even }; + +} // namespace eve::map diff --git a/src/modules/map/TileProjection.cpp b/src/modules/map/TileProjection.cpp new file mode 100644 index 00000000..0bf6d10b --- /dev/null +++ b/src/modules/map/TileProjection.cpp @@ -0,0 +1,99 @@ +#include "map/TileProjection.h" + +#include +#include + +namespace eve::map { +namespace { + +bool staggerDoShift(const TileLayer::Config &cfg, int tx, int ty) { + const int major = (cfg.staggerAxis == StaggerAxis::Y) ? ty : tx; + const bool odd = (major & 1) != 0; + return cfg.staggerIndex == StaggerIndex::Odd ? odd : !odd; +} + +} // namespace + +void tileToWorld(const TileLayer::Config &cfg, int tx, int ty, float &wx, float &wy) { + const float tw = cfg.tileW; + const float th = cfg.tileH; + switch (cfg.orientation) { + case MapOrientation::Orthogonal: + wx = cfg.originX + float(tx) * tw; + wy = cfg.originY + float(ty) * th; + break; + case MapOrientation::Isometric: + wx = cfg.originX + float(tx - ty) * tw * 0.5f; + wy = cfg.originY + float(tx + ty) * th * 0.5f; + break; + case MapOrientation::Staggered: + case MapOrientation::Hexagonal: { + // Match Tiled StaggeredRenderer / HexagonalRenderer. + const bool hex = cfg.orientation == MapOrientation::Hexagonal && cfg.hexSideLength > 0.f; + if (cfg.staggerAxis == StaggerAxis::Y) { + const float pitchY = hex ? (th + cfg.hexSideLength) * 0.5f : th * 0.5f; + wx = cfg.originX + float(tx) * tw + (staggerDoShift(cfg, tx, ty) ? tw * 0.5f : 0.f); + wy = cfg.originY + float(ty) * pitchY; + } else { + const float pitchX = hex ? (tw + cfg.hexSideLength) * 0.5f : tw * 0.5f; + wx = cfg.originX + float(tx) * pitchX; + wy = cfg.originY + float(ty) * th + (staggerDoShift(cfg, tx, ty) ? th * 0.5f : 0.f); + } + break; + } + } +} + +float tileToDepthY(const TileLayer::Config &cfg, int tx, int ty) { + float wx = 0.f, wy = 0.f; + tileToWorld(cfg, tx, ty, wx, wy); + return wy + cfg.tileH; +} + +void worldToTile(const TileLayer::Config &cfg, float wx, float wy, int &tx, int &ty) { + switch (cfg.orientation) { + case MapOrientation::Orthogonal: { + const float lx = wx - cfg.originX; + const float ly = wy - cfg.originY; + tx = int(std::floor(lx / cfg.tileW)); + ty = int(std::floor(ly / cfg.tileH)); + break; + } + case MapOrientation::Isometric: { + const float lx = wx - cfg.originX; + const float ly = wy - cfg.originY; + const float a = lx / (cfg.tileW * 0.5f); + const float b = ly / (cfg.tileH * 0.5f); + tx = int(std::floor((b + a) * 0.5f)); + ty = int(std::floor((b - a) * 0.5f)); + break; + } + default: { + // Nearest tile center within map bounds (staggered / hex). + tx = 0; + ty = 0; + float best = 1e30f; + const int mapW = std::max(1, cfg.mapW); + const int mapH = std::max(1, cfg.mapH); + for (int y = 0; y < mapH; ++y) { + for (int x = 0; x < mapW; ++x) { + float cx = 0.f, cy = 0.f; + tileToWorld(cfg, x, y, cx, cy); + cx += cfg.tileW * 0.5f; + cy += cfg.tileH * 0.5f; + const float dx = cx - wx; + const float dy = cy - wy; + const float d = dx * dx + dy * dy; + if (d < best) { + best = d; + tx = x; + ty = y; + } + } + } + break; + } + } +} + +} // namespace eve::map diff --git a/src/modules/map/TileProjection.h b/src/modules/map/TileProjection.h new file mode 100644 index 00000000..924c42b4 --- /dev/null +++ b/src/modules/map/TileProjection.h @@ -0,0 +1,12 @@ +#pragma once + +#include "map/TileOrientation.h" +#include "map/TileLayer.h" + +namespace eve::map { + +void tileToWorld(const TileLayer::Config &cfg, int tx, int ty, float &wx, float &wy); +float tileToDepthY(const TileLayer::Config &cfg, int tx, int ty); +void worldToTile(const TileLayer::Config &cfg, float wx, float wy, int &tx, int &ty); + +} // namespace eve::map diff --git a/src/modules/map/TileSystem.cpp b/src/modules/map/TileSystem.cpp index aac00f66..3c2dc842 100644 --- a/src/modules/map/TileSystem.cpp +++ b/src/modules/map/TileSystem.cpp @@ -1,20 +1,18 @@ #include "map/TileSystem.h" #include "map/TileLayer.h" #include "map/TileConfig.h" +#include "map/TileProjection.h" #include "graphics/Graphics.h" #include "graphics/RenderSystem.h" #include "graphics/Canvas.h" #include "filesystem/Filesystem.h" #include "common/Module.h" -#include #include namespace eve::map { namespace { -float clampZoom(float z) { return z <= 0.f ? 1e-4f : z; } - struct ViewCam { float x = 0.f; float y = 0.f; @@ -33,22 +31,6 @@ ViewCam fromEntity(graphics::Camera2D *ent) { return v; } -void applyCamera(float wx, float wy, float ww, float wh, const ViewCam &cam, int viewW, int viewH, - float &sx, float &sy, float &sw, float &sh) { - if (!cam.valid) { - sx = wx; - sy = wy; - sw = ww; - sh = wh; - return; - } - const float z = clampZoom(cam.zoom); - sx = (wx - cam.x) * z + float(viewW) * 0.5f; - sy = (wy - cam.y) * z + float(viewH) * 0.5f; - sw = ww * z; - sh = wh * z; -} - /** Deterministic debug color when no tileset texture is bound. */ Color solidForGid(uint32_t gid, const Color &tint) { const uint32_t h = gid * 2654435761u; @@ -78,37 +60,6 @@ bool atlasUV(const TileLayer::Tileset &ts, uint32_t gid, float &u0, float &v0, f return true; } -void drawLayer(const TileLayer::Config &cfg, const TileLayer::Tiles &tiles, - const TileLayer::Tileset &ts, const TileLayer::Draw &draw, graphics::Graphics *gfx, - const ViewCam &cam) { - if (!gfx || !draw.visible || cfg.mapW <= 0 || cfg.mapH <= 0) return; - if (tiles.gids.size() < size_t(cfg.mapW * cfg.mapH)) return; - - const int viewW = draw.canvas ? draw.canvas->getWidth() : gfx->getWidth(); - const int viewH = draw.canvas ? draw.canvas->getHeight() : gfx->getHeight(); - const Color &tint = draw.tint; - - for (int ty = 0; ty < cfg.mapH; ++ty) { - for (int tx = 0; tx < cfg.mapW; ++tx) { - const uint32_t raw = tiles.gids[size_t(ty * cfg.mapW + tx)]; - const uint32_t gid = tileGid(raw); - if (gid == 0) continue; - - const float wx = cfg.originX + float(tx) * cfg.tileW; - const float wy = cfg.originY + float(ty) * cfg.tileH; - float sx, sy, sw, sh; - applyCamera(wx, wy, cfg.tileW, cfg.tileH, cam, viewW, viewH, sx, sy, sw, sh); - - float u0, v0, u1, v1; - if (atlasUV(ts, gid, u0, v0, u1, v1)) { - gfx->drawTexturedRectUV(ts.texture, sx, sy, sw, sh, u0, v0, u1, v1, tint); - } else { - gfx->drawSolidRect(sx, sy, sw, sh, solidForGid(gid, tint)); - } - } - } -} - int64_t fileModtime(const std::string &path) { auto *fs = eve::ModuleManager::getInstance("Filesystem"); if (!fs) fs = eve::filesystem::Filesystem::create(); @@ -119,49 +70,68 @@ int64_t fileModtime(const std::string &path) { } // namespace -void TileRenderSystem::render(graphics::Graphics *gfx) { - if (!gfx) return; - if (ecs::ComponentManager::inst().registy == nullptr) return; - - struct Item { - TileLayer::Config *cfg; - TileLayer::Tiles *tiles; - TileLayer::Tileset *ts; - TileLayer::Draw *draw; - ViewCam cam; - }; - std::vector items; +void TileRenderSystem::collect(std::vector &out) { + if (ecs::current()->getManager() == nullptr) return; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [cfg, tiles, ts, draw] = *it; - if (!draw->visible) continue; - items.push_back(Item{cfg, tiles, ts, draw, fromEntity(draw->camera)}); - } + if (!draw->visible || cfg->mapW <= 0 || cfg->mapH <= 0) continue; + if (tiles->gids.size() < size_t(cfg->mapW * cfg->mapH)) continue; - std::stable_sort(items.begin(), items.end(), [](const Item &a, const Item &b) { - const bool aOff = a.draw->canvas != nullptr; - const bool bOff = b.draw->canvas != nullptr; - if (aOff != bOff) return aOff && !bOff; - if (a.draw->canvas != b.draw->canvas) return a.draw->canvas < b.draw->canvas; - return a.draw->layer < b.draw->layer; - }); + const Color &tint = draw->tint; + const ViewCam cam = fromEntity(draw->camera); - graphics::Canvas *current = reinterpret_cast(static_cast(1)); - for (const Item &it : items) { - graphics::Canvas *next = it.draw->canvas; - if (next != current) { - gfx->setCanvas(next); - current = next; + for (int ty = 0; ty < cfg->mapH; ++ty) { + for (int tx = 0; tx < cfg->mapW; ++tx) { + const uint32_t raw = tiles->gids[size_t(ty * cfg->mapW + tx)]; + const uint32_t gid = tileGid(raw); + if (gid == 0) continue; + + graphics::DrawItem2D item; + tileToWorld(*cfg, tx, ty, item.x, item.y); + item.w = cfg->tileW; + item.h = cfg->tileH; + item.depthY = tileToDepthY(*cfg, tx, ty); + item.layer = draw->layer; + item.canvas = draw->canvas; + item.camera = draw->camera; + item.camValid = cam.valid; + item.camX = cam.x; + item.camY = cam.y; + item.camZoom = cam.zoom; + item.receiveLight = false; + item.litPath = false; + + float u0, v0, u1, v1; + if (atlasUV(*ts, gid, u0, v0, u1, v1)) { + item.texture = ts->texture; + item.hasUV = true; + item.u0 = u0; + item.v0 = v0; + item.u1 = u1; + item.v1 = v1; + item.color = tint; + } else { + item.texture = nullptr; + item.color = solidForGid(gid, tint); + } + out.push_back(item); + } } - drawLayer(*it.cfg, *it.tiles, *it.ts, *it.draw, gfx, it.cam); } - if (current != nullptr) gfx->setCanvas(); +} + +void TileRenderSystem::render(graphics::Graphics *gfx) { + if (!gfx) return; + std::vector items; + collect(items); + graphics::RenderSystem::drawItems(*gfx, items, false); } int TileConfigSystem::poll() { - if (ecs::ComponentManager::inst().registy == nullptr) return 0; + if (ecs::current()->getManager() == nullptr) return 0; int reloaded = 0; auto view = ecs::View(); diff --git a/src/modules/map/TileSystem.h b/src/modules/map/TileSystem.h index d3819dcb..3ac64bef 100644 --- a/src/modules/map/TileSystem.h +++ b/src/modules/map/TileSystem.h @@ -1,5 +1,10 @@ #pragma once +#include "map/TileLayer.h" +#include "graphics/DrawItem2D.h" + +#include + namespace eve::graphics { class Graphics; } @@ -9,6 +14,10 @@ namespace eve::map { /** Draws all visible TileLayer entities via Graphics batch path (UV atlas quads). */ class TileRenderSystem { public: + /** Append non-empty visible tiles into a shared 2D draw queue. */ + static void collect(std::vector &out); + + /** Collect + draw tiles only (no present). Null gfx is a no-op. */ static void render(graphics::Graphics *gfx); }; diff --git a/src/modules/particles/ParticleSystem.cpp b/src/modules/particles/ParticleSystem.cpp index fefff3d5..fe1851be 100644 --- a/src/modules/particles/ParticleSystem.cpp +++ b/src/modules/particles/ParticleSystem.cpp @@ -98,7 +98,7 @@ int64_t fileModtime(const std::string &path) { } // namespace void ParticleSimSystem::update(float dt) { - if (ecs::ComponentManager::inst().registy == nullptr) return; + if (ecs::current()->getManager() == nullptr) return; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { @@ -109,7 +109,7 @@ void ParticleSimSystem::update(float dt) { void ParticleRenderSystem::render(graphics::Graphics *gfx) { if (!gfx) return; - if (ecs::ComponentManager::inst().registy == nullptr) return; + if (ecs::current()->getManager() == nullptr) return; struct Item { ParticleEmitter::Config *cfg; @@ -152,7 +152,7 @@ void ParticleRenderSystem::render(graphics::Graphics *gfx) { } int ParticleConfigSystem::poll() { - if (ecs::ComponentManager::inst().registy == nullptr) return 0; + if (ecs::current()->getManager() == nullptr) return 0; // Watch events are drained by load.nut / HotReload; use modtime as fallback. int reloaded = 0; diff --git a/src/modules/particles/Particles.cpp b/src/modules/particles/Particles.cpp index 975f44c1..7a4d48bd 100644 --- a/src/modules/particles/Particles.cpp +++ b/src/modules/particles/Particles.cpp @@ -73,7 +73,7 @@ void Particles::render(graphics::Graphics *gfx) { ParticleRenderSystem::render(g int Particles::pollConfigs() { return ParticleConfigSystem::poll(); } int Particles::getEmitterCount() const { - if (ecs::ComponentManager::inst().registy == nullptr) return 0; + if (ecs::current()->getManager() == nullptr) return 0; int n = 0; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) ++n; diff --git a/src/modules/ui/UISystem.cpp b/src/modules/ui/UISystem.cpp index a8c11274..d01a246e 100644 --- a/src/modules/ui/UISystem.cpp +++ b/src/modules/ui/UISystem.cpp @@ -152,7 +152,7 @@ std::vector &UISystem::changeQueue() { return g_changes; } UIHost *UISystem::findHost(const std::string &name) { if (name.empty()) return nullptr; - if (ecs::ComponentManager::inst().registy == nullptr) return nullptr; + if (ecs::current()->getManager() == nullptr) return nullptr; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [meta, tree] = *it; @@ -164,7 +164,7 @@ UIHost *UISystem::findHost(const std::string &name) { UIHost *UISystem::findHostByOwner(uint32_t ownerId) { if (ownerId == 0) return nullptr; - if (ecs::ComponentManager::inst().registy == nullptr) return nullptr; + if (ecs::current()->getManager() == nullptr) return nullptr; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [meta, tree] = *it; @@ -175,7 +175,7 @@ UIHost *UISystem::findHostByOwner(uint32_t ownerId) { } void UISystem::render() { - if (ecs::ComponentManager::inst().registy == nullptr) return; + if (ecs::current()->getManager() == nullptr) return; applyThemeToImGui(globalTheme()); diff --git a/src/scripts/load.nut b/src/scripts/load.nut index cfabf6e6..d3edc055 100644 --- a/src/scripts/load.nut +++ b/src/scripts/load.nut @@ -26,7 +26,7 @@ config <- { height = 600 title = "EVEngine" debug = false - hotReload = true + hotReload = false }; if (file_exists("config.nut")) { diff --git a/test/ClusteredLighting.cpp b/test/ClusteredLighting.cpp index 3e2d0ed8..d87f3464 100644 --- a/test/ClusteredLighting.cpp +++ b/test/ClusteredLighting.cpp @@ -30,28 +30,28 @@ static void openGfxWindow(eve::window::Window *&win, Graphics *&gfx, int w = 320 } static void resetScene3D() { - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [mr] = *it; mr->visible = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; data->active = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto lightView = ecs::View(); for (auto it = lightView.begin(); it != lightView.end(); ++it) { auto [d] = *it; d->enabled = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [sp] = *it; diff --git a/test/IBL.cpp b/test/IBL.cpp index 58ba2422..527ffaf7 100644 --- a/test/IBL.cpp +++ b/test/IBL.cpp @@ -30,14 +30,14 @@ void openGfxWindow(eve::window::Window *&win, Graphics *&gfx, int w = 320, int h } void resetScene3D() { - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [mr] = *it; mr->visible = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; @@ -46,14 +46,14 @@ void resetScene3D() { data->envIntensity = 1.f; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto lightView = ecs::View(); for (auto it = lightView.begin(); it != lightView.end(); ++it) { auto [d] = *it; d->enabled = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [sp] = *it; diff --git a/test/RenderSystem3D.cpp b/test/RenderSystem3D.cpp index 7cd40c8a..2d17026b 100644 --- a/test/RenderSystem3D.cpp +++ b/test/RenderSystem3D.cpp @@ -122,28 +122,28 @@ static Texture *makeSolidGray(Graphics *gfx, uint8_t g) { // Graphics/ECS are process-wide singletons — hide leftovers from earlier cases. static void resetScene3D() { - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [mr] = *it; mr->visible = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; data->active = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto lightView = ecs::View(); for (auto it = lightView.begin(); it != lightView.end(); ++it) { auto [d] = *it; d->enabled = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [sp] = *it; diff --git a/test/Shadow3D.cpp b/test/Shadow3D.cpp index ad1f568b..bdfe42a1 100644 --- a/test/Shadow3D.cpp +++ b/test/Shadow3D.cpp @@ -1,6 +1,7 @@ #include "zeroerr/assert.h" #include "zeroerr/unittest.h" +#include #include #include #include @@ -30,28 +31,28 @@ void openGfxWindow(eve::window::Window *&win, Graphics *&gfx, int w = 400, int h } void resetScene3D() { - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [mr] = *it; mr->visible = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; data->active = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto lightView = ecs::View(); for (auto it = lightView.begin(); it != lightView.end(); ++it) { auto [d] = *it; d->enabled = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [sp] = *it; @@ -65,11 +66,7 @@ Texture *makeSolid(Graphics *gfx, uint8_t r, uint8_t g, uint8_t b) { return gfx->newTexture(1, 1, px); } -/** Unit box centered at origin (from newMeshSphere-style: use a scaled sphere as occluder). */ -Mesh *makeGroundPlane(Graphics *gfx) { - // Thin flat box via scaled sphere is awkward; use a large shallow scale on sphere. - return gfx->newMeshSphere(16, 8); -} +Mesh *makeGroundPlane(Graphics *gfx) { return gfx->newMeshSphere(16, 8); } void addHud() { auto *hud = Renderable2D::create(); @@ -88,21 +85,41 @@ void warmPresent(Graphics *gfx) { } } -} // namespace +/** Max (a - b) over a coarse pixel grid. Used because Vulkan FB Y / RH_ZO place + * this scene's umbra off the naive "center + below" sample point. */ +float maxLumaDelta(Graphics *gfx, const std::vector &a, const std::vector &b, + int step = 4) { + float best = -1.f; + const int w = gfx->getWidth(); + const int h = gfx->getHeight(); + for (int y = 0; y < h; y += step) { + for (int x = 0; x < w; x += step) { + const size_t i = size_t(y * w + x); + best = std::max(best, a[i] - b[i]); + } + } + return best; +} -TEST_CASE("Shadow3D.dirLightDarkensOccludedGround") { - eve::window::Window *win = nullptr; - Graphics *gfx = nullptr; - openGfxWindow(win, gfx); - resetScene3D(); +std::vector captureLumaGrid(Graphics *gfx, int step = 4) { + const int w = gfx->getWidth(); + const int h = gfx->getHeight(); + std::vector out(size_t(w * h), 0.f); + for (int y = 0; y < h; y += step) { + for (int x = 0; x < w; x += step) { + out[size_t(y * w + x)] = luma(gfx->getPixel(x, y)); + } + } + return out; +} +void setupShadowScene(Graphics *gfx, Renderable3D *&ground, Light3D *&sun, bool groundReceive) { auto *cam = Camera3D::createCamera(); cam->setEye(0.f, 3.5f, 5.5f); cam->setTarget(0.f, 0.f, 0.f); cam->setAmbient(0.05f, 0.05f, 0.05f); - // Ground: large flat-ish sphere at y=-0.85 - auto *ground = Renderable3D::create(); + ground = Renderable3D::create(); ground->setMesh(makeGroundPlane(gfx)); ground->setTexture(makeSolid(gfx, 220, 220, 220)); ground->setPosition(0.f, -1.2f, 0.f); @@ -110,9 +127,8 @@ TEST_CASE("Shadow3D.dirLightDarkensOccludedGround") { ground->setMetallic(0.f); ground->setRoughness(0.85f); ground->setCastShadow(false); - ground->setReceiveShadow(true); + ground->setReceiveShadow(groundReceive); - // Occluder above ground auto *box = Renderable3D::create(); box->setMesh(gfx->newMeshSphere(20, 12)); box->setTexture(makeSolid(gfx, 200, 200, 200)); @@ -123,7 +139,7 @@ TEST_CASE("Shadow3D.dirLightDarkensOccludedGround") { box->setCastShadow(true); box->setReceiveShadow(false); - auto *sun = Light3D::createLight("dir"); + sun = Light3D::createLight("dir"); sun->setDirection(0.55f, 1.f, 0.35f); sun->setColor(1.f, 1.f, 1.f, 2.5f); sun->setCastShadow(true); @@ -131,28 +147,47 @@ TEST_CASE("Shadow3D.dirLightDarkensOccludedGround") { sun->setShadowBias(0.003f); addHud(); +} + +} // namespace + +TEST_CASE("Shadow3D.dirLightDarkensOccludedGround") { + eve::window::Window *win = nullptr; + Graphics *gfx = nullptr; + openGfxWindow(win, gfx); + resetScene3D(); + + Renderable3D *ground = nullptr; + Light3D *sun = nullptr; + setupShadowScene(gfx, ground, sun, true); + (void)ground; + gfx->setScreenReadbackEnabled(true); - warmPresent(gfx); - // Sample near center (likely under occluder) vs far right (lit ground). - const int cx = gfx->getWidth() / 2; - const int cy = gfx->getHeight() / 2 + gfx->getHeight() / 8; - const int dx = gfx->getWidth() / 3; - const float L_center = luma(gfx->getPixel(cx, cy)); - const float L_side = luma(gfx->getPixel(cx + dx, cy)); - - // With shadows, side should be brighter than the occluded center region. - // Also compare against shadows disabled. sun->setCastShadow(false); warmPresent(gfx); - const float L_center_off = luma(gfx->getPixel(cx, cy)); + const auto L_off = captureLumaGrid(gfx); sun->setCastShadow(true); warmPresent(gfx); - const float L_center_on = luma(gfx->getPixel(cx, cy)); + const auto L_on = captureLumaGrid(gfx); - REQUIRE(L_center_off > L_center_on + 0.03f); - REQUIRE(L_side > L_center + 0.02f); + // Enabling CSM must darken some receive-shadow texels (umbra under the occluder). + REQUIRE(maxLumaDelta(gfx, L_off, L_on) > 0.03f); + + // With shadows on, some lit ground should still be brighter than some umbra texel. + float brightest = 0.f, darkestGeom = 1.f; + const int w = gfx->getWidth(); + const int h = gfx->getHeight(); + for (int y = 0; y < h; y += 4) { + for (int x = 0; x < w; x += 4) { + const float L = L_on[size_t(y * w + x)]; + if (L < 0.15f) continue; // skip clear / fully ambient + brightest = std::max(brightest, L); + darkestGeom = std::min(darkestGeom, L); + } + } + REQUIRE(brightest > darkestGeom + 0.02f); win->close(); } @@ -163,43 +198,21 @@ TEST_CASE("Shadow3D.receiveShadowFalseIgnoresMap") { openGfxWindow(win, gfx); resetScene3D(); - auto *cam = Camera3D::createCamera(); - cam->setEye(0.f, 3.5f, 5.5f); - cam->setTarget(0.f, 0.f, 0.f); - cam->setAmbient(0.05f, 0.05f, 0.05f); + Renderable3D *ground = nullptr; + Light3D *sun = nullptr; + setupShadowScene(gfx, ground, sun, false); + (void)sun; - auto *ground = Renderable3D::create(); - ground->setMesh(gfx->newMeshSphere(16, 8)); - ground->setTexture(makeSolid(gfx, 220, 220, 220)); - ground->setPosition(0.f, -1.2f, 0.f); - ground->setScale(4.f, 0.15f, 4.f); - ground->setCastShadow(false); - ground->setReceiveShadow(false); - - auto *box = Renderable3D::create(); - box->setMesh(gfx->newMeshSphere(20, 12)); - box->setTexture(makeSolid(gfx, 200, 200, 200)); - box->setPosition(0.f, 0.35f, 0.f); - box->setScale(0.55f, 0.55f, 0.55f); - box->setCastShadow(true); - - auto *sun = Light3D::createLight("dir"); - sun->setDirection(0.55f, 1.f, 0.35f); - sun->setColor(1.f, 1.f, 1.f, 2.5f); - sun->setCastShadow(true); - - addHud(); gfx->setScreenReadbackEnabled(true); warmPresent(gfx); - const int cx = gfx->getWidth() / 2; - const int cy = gfx->getHeight() / 2 + gfx->getHeight() / 8; - const float L_offRecv = luma(gfx->getPixel(cx, cy)); + const auto L_offRecv = captureLumaGrid(gfx); ground->setReceiveShadow(true); warmPresent(gfx); - const float L_onRecv = luma(gfx->getPixel(cx, cy)); + const auto L_onRecv = captureLumaGrid(gfx); - REQUIRE(L_offRecv > L_onRecv + 0.02f); + // Turning receiveShadow on must apply the shadow map and darken umbra texels. + REQUIRE(maxLumaDelta(gfx, L_offRecv, L_onRecv) > 0.02f); win->close(); } diff --git a/test/map.cpp b/test/map.cpp index 6561d392..bf318b8e 100644 --- a/test/map.cpp +++ b/test/map.cpp @@ -5,8 +5,12 @@ #include "map/TileLayer.h" #include "map/TileSystem.h" #include "map/TileConfig.h" +#include "map/TileProjection.h" +#include "map/MapObject.h" +#include "graphics/DrawItem2D.h" #include +#include using namespace eve::map; @@ -104,3 +108,136 @@ TEST_CASE("map.render.nullSafe") { 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.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") { + 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); +} + +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); +} diff --git a/test/model3d.cpp b/test/model3d.cpp index 6b168baa..0826b3d1 100644 --- a/test/model3d.cpp +++ b/test/model3d.cpp @@ -70,21 +70,21 @@ void openGfxWindow(eve::window::Window *&win, Graphics *&gfx, int w = 640, int h } void resetScene3D() { - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [mr] = *it; mr->visible = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; data->active = false; } } - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [sp] = *it; @@ -375,7 +375,7 @@ void renderModelSmoke(eve::model3d::ModelData *md, const char *pngName, RenderSystem3D::setDirectionalLight(lx, ly + rad * 1.2f, lz, 1.8f, 1.8f, 1.8f); for (int i = 0; i < 10; ++i) { - if (ecs::ComponentManager::inst().registy != nullptr) { + if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [xf, mr] = *it;