From 4b868f47c295b21da299ce411355c9c367f4bf71 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Tue, 2 Jun 2026 12:11:37 +0300 Subject: [PATCH] Added editor functionality to edit bound boxes --- proj-web/CMakeLists.txt | 2 + proj-windows/CMakeLists.txt | 2 + .../config2/interactive_objects_dorm.json | 22 +- src/Game.cpp | 57 +- src/Game.h | 2 +- src/Location.cpp | 345 +++--------- src/Location.h | 26 +- src/LocationEditor.cpp | 510 ++++++++++++++++++ src/LocationEditor.h | 70 +++ src/items/GameObjectLoader.cpp | 31 ++ src/items/GameObjectLoader.h | 5 + src/items/InteractiveObject.h | 14 + 12 files changed, 779 insertions(+), 307 deletions(-) create mode 100644 src/LocationEditor.cpp create mode 100644 src/LocationEditor.h diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt index 4234524..51b8ee3 100644 --- a/proj-web/CMakeLists.txt +++ b/proj-web/CMakeLists.txt @@ -99,6 +99,8 @@ set(SOURCES ../src/MenuManager.cpp ../src/Location.h ../src/Location.cpp + ../src/LocationEditor.h + ../src/LocationEditor.cpp ../src/GameConstants.h ../src/GameConstants.cpp ../src/ScriptEngine.h diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index 1bb17ca..4182765 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -56,6 +56,8 @@ add_executable(witcher001 ../src/MenuManager.cpp ../src/Location.h ../src/Location.cpp + ../src/LocationEditor.h + ../src/LocationEditor.cpp ../src/GameConstants.h ../src/GameConstants.cpp ../src/ScriptEngine.h diff --git a/resources/config2/interactive_objects_dorm.json b/resources/config2/interactive_objects_dorm.json index c6f2497..cb8cb71 100644 --- a/resources/config2/interactive_objects_dorm.json +++ b/resources/config2/interactive_objects_dorm.json @@ -10,8 +10,18 @@ "positionX": 6.6644, "positionY": 0.9, "positionZ": -12.5262, + "approachRadius": 0.6, + "boundsMaxX": 0.32435035705566406, + "boundsMaxY": 0.5, + "boundsMaxZ": 0.5178079605102539, + "boundsMinX": -0.19553518295288086, + "boundsMinY": -0.5, + "boundsMinZ": -0.2698993682861328, + "interactionPositionX": 6.694502353668213, + "interactionPositionY": 0.0, + "interactionPositionZ": -12.80740737915039, "scale": 1.0, - "interactionRadius": 0.3, + "interactionRadius": 0.0, "activateFunction": "on_phone_pickup" }, { @@ -24,6 +34,16 @@ "positionX": 5.84296, "positionY": 0.9, "positionZ": -12.4661, + "approachRadius": 0.6, + "boundsMaxX": 0.17081403732299805, + "boundsMaxY": 0.5, + "boundsMaxZ": 0.35796260833740234, + "boundsMinX": -0.22043895721435547, + "boundsMinY": -0.5, + "boundsMinZ": -0.25891780853271484, + "interactionPositionX": 5.819052696228027, + "interactionPositionY": 0.0, + "interactionPositionZ": -12.76319408416748, "scale": 1.0, "interactionRadius": 0.3, "activateFunction": "on_journal_pickup" diff --git a/src/Game.cpp b/src/Game.cpp index a0bf3fe..0ee056f 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -694,9 +694,9 @@ namespace ZL } else if (event.button.button == SDL_BUTTON_RIGHT && event.type == SDL_MOUSEBUTTONUP - && navigationEditorMode + && editorMode == EditorMode::Navigation && currentLocation) { - currentLocation->navigationEditorHandleRightClick(); + currentLocation->editor.handleRightClick(); } } else if (event.type == SDL_MOUSEMOTION) { @@ -727,16 +727,20 @@ namespace ZL if (event.type == SDL_KEYDOWN && event.key.repeat == 0) { switch (event.key.keysym.sym) { case SDLK_0: - startDarklandsTransition(); - break; case SDLK_1: - //if (audioPlayer) audioPlayer->playSoundAsync("audio/background.wav"); - break; case SDLK_2: - //if (audioPlayer) audioPlayer->playMusicAsync("audio/lullaby-music-vol20-186394--online-audio-convert.com.ogg"); - break; case SDLK_3: - //if (audioPlayer) audioPlayer->stopMusicAsync(); + case SDLK_4: + case SDLK_5: + case SDLK_6: + case SDLK_7: + case SDLK_8: + case SDLK_9: + if (editorMode == EditorMode::InteractiveObjects && currentLocation) { + currentLocation->editor.selectInteractiveObject(event.key.keysym.sym - SDLK_0); + } else if (event.key.keysym.sym == SDLK_0) { + startDarklandsTransition(); + } break; case SDLK_f: currentLocation->dialogueSystem.startDialogue("dialog_start001"); @@ -747,14 +751,25 @@ namespace ZL break; case SDLK_n: - navigationEditorMode = !navigationEditorMode; + if (editorMode == EditorMode::None) + editorMode = EditorMode::Navigation; + else if (editorMode == EditorMode::Navigation) + editorMode = EditorMode::InteractiveObjects; + else + editorMode = EditorMode::None; if (currentLocation) { - currentLocation->navigationEditorMode = navigationEditorMode; - if (navigationEditorMode) { - currentLocation->navigationEditorBuildNavMeshes(); - } + currentLocation->editorMode = editorMode; + if (editorMode == EditorMode::Navigation) + currentLocation->editor.buildNavMeshes(); + else if (editorMode == EditorMode::InteractiveObjects) + currentLocation->editor.buildInteractiveObjectBoundsMeshes(); + } + { + const char* modeName = (editorMode == EditorMode::Navigation) ? "Navigation" + : (editorMode == EditorMode::InteractiveObjects) ? "InteractiveObjects" + : "None"; + std::cout << "[EDITOR] Mode: " << modeName << std::endl; } - std::cout << "[NAV_EDITOR] Mode: " << (navigationEditorMode ? "ON" : "OFF") << std::endl; break; case SDLK_o: @@ -802,22 +817,22 @@ namespace ZL break; case SDLK_b: - if (navigationEditorMode && currentLocation) { - currentLocation->navigationEditorSave(); + if (editorMode != EditorMode::None && currentLocation) { + currentLocation->editor.saveAll(); } break; case SDLK_j: - if (navigationEditorMode && currentLocation) { - currentLocation->gameObjectEditorPlaceTree(); + if (editorMode != EditorMode::None && currentLocation) { + currentLocation->editor.placeTree(); } else { menuManager.toggleQuestJournal(); } break; case SDLK_v: - if (navigationEditorMode && currentLocation) { - currentLocation->gameObjectEditorSave(); + if (editorMode != EditorMode::None && currentLocation) { + currentLocation->editor.saveObjects(); } break; diff --git a/src/Game.h b/src/Game.h index b919052..5e7dc84 100644 --- a/src/Game.h +++ b/src/Game.h @@ -45,7 +45,7 @@ namespace ZL { std::unordered_map> locations; std::shared_ptr currentLocation; - bool navigationEditorMode = false; + EditorMode editorMode = EditorMode::None; // Global darklands state — persists across location transitions. bool isDarklands = false; diff --git a/src/Location.cpp b/src/Location.cpp index 7137f0e..5ad7da4 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -4,11 +4,8 @@ #include #include "render/TextureManager.h" #include "TextModel.h" -#include #include #include -#include -#include #include #include #include @@ -47,8 +44,8 @@ namespace ZL Location::Location(Renderer& iRenderer, Inventory& iInventory) : renderer(iRenderer) , inventory(iInventory) + , editor(*this) { - } void Location::setup(const LocationSetup& params) @@ -303,8 +300,8 @@ namespace ZL activeNavigationIndex = 0; navigation = navigationMaps.empty() ? nullptr : &navigationMaps[0]; - if (navigationEditorMode && navigation) { - navigationEditorBuildNavMeshes(); + if (editorMode == EditorMode::Navigation && navigation) { + editor.buildNavMeshes(); } static constexpr float kDynamicObstacleInfluenceDist = 6.0f; @@ -365,237 +362,13 @@ namespace ZL if (npc) npc->forceReplan(); } - if (navigationEditorMode) { - navigationEditorBuildNavMeshes(); + if (editorMode == EditorMode::Navigation) { + editor.buildNavMeshes(); } std::cout << "[NAV] Switched to navigation map " << index << "\n"; return true; } - void Location::navigationEditorBuildNavMeshes() - { - navigationEditorNavMeshes.clear(); - if (!navigation) return; - const float y = navigation->getFloorY() + 0.02f; - const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); - for (const auto& obs : navigation->getObstaclePolygons()) { - if (obs.polygon.size() < 3) continue; - VertexRenderStruct mesh; - mesh.data = CreatePolygonFloor(obs.polygon, y, red); - mesh.RefreshVBO(); - navigationEditorNavMeshes.push_back(std::move(mesh)); - } - } - - void Location::navigationEditorDrawNavigation() - { - renderer.shaderManager.PushShader("defaultColor"); - renderer.SetMatrix(); - for (const auto& mesh : navigationEditorNavMeshes) { - renderer.DrawVertexRenderStruct(mesh); - } - renderer.shaderManager.PopShader(); - renderer.SetMatrix(); - } - - void Location::navigationEditorRebuildPointsMesh() - { - VertexDataStruct data; - const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f); - const float y = navigation ? navigation->getFloorY() + 0.05f : 0.05f; - const float s = 0.2f; - - for (const auto& pt : navigationEditorPoints) { - // Small upward-pointing equilateral triangle in the XZ plane - Eigen::Vector3f v0(pt.x(), y, pt.z() - s * 1.15f); - Eigen::Vector3f v1(pt.x() - s, y, pt.z() + s * 0.58f); - Eigen::Vector3f v2(pt.x() + s, y, pt.z() + s * 0.58f); - - data.PositionData.push_back(v0); - data.PositionData.push_back(v1); - data.PositionData.push_back(v2); - data.ColorData.push_back(yellow); - data.ColorData.push_back(yellow); - data.ColorData.push_back(yellow); - } - - navigationEditorPointsMesh.data = std::move(data); - navigationEditorPointsMesh.RefreshVBO(); - } - - void Location::navigationEditorDrawPoints() - { - if (navigationEditorPoints.empty()) return; - renderer.shaderManager.PushShader("defaultColor"); - renderer.SetMatrix(); - renderer.DrawVertexRenderStruct(navigationEditorPointsMesh); - renderer.shaderManager.PopShader(); - renderer.SetMatrix(); - } - - void Location::navigationEditorHandleLeftClick(const Eigen::Vector3f& hit, bool ctrlHeld) - { - if (ctrlHeld) { - const float removeRadius = 1.0f; - for (auto it = navigationEditorPoints.begin(); it != navigationEditorPoints.end(); ++it) { - const float dx = it->x() - hit.x(); - const float dz = it->z() - hit.z(); - if (dx * dx + dz * dz <= removeRadius * removeRadius) { - navigationEditorPoints.erase(it); - navigationEditorRebuildPointsMesh(); - std::cout << "[NAV_EDITOR] Removed point, " << navigationEditorPoints.size() << " remaining\n"; - return; - } - } - std::cout << "[NAV_EDITOR] No point found within " << removeRadius << " units of click\n"; - } else { - navigationEditorPoints.push_back(hit); - navigationEditorRebuildPointsMesh(); - std::cout << "[NAV_EDITOR] Added point (" << hit.x() << ", " << hit.z() - << "), total: " << navigationEditorPoints.size() << "\n"; - } - } - - void Location::navigationEditorHandleRightClick() - { - if (navigationEditorPoints.size() < 3) { - std::cout << "[NAV_EDITOR] Need at least 3 points to form a polygon (have " - << navigationEditorPoints.size() << "); clearing\n"; - navigationEditorPoints.clear(); - navigationEditorRebuildPointsMesh(); - return; - } - - PathFinder::ObstaclePolygon poly; - poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter); - for (const auto& pt : navigationEditorPoints) { - poly.polygon.emplace_back(pt.x(), pt.z()); - } - - if (navigation) navigation->addObstaclePolygon(poly); - std::cout << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with " - << poly.polygon.size() << " vertices\n"; - - // Add a red mesh for the new obstacle polygon so it's visible immediately. - { - const float y = navigation ? navigation->getFloorY() + 0.02f : 0.02f; - const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); - VertexRenderStruct mesh; - mesh.data = CreatePolygonFloor(poly.polygon, y, red); - mesh.RefreshVBO(); - navigationEditorNavMeshes.push_back(std::move(mesh)); - } - - navigationEditorPoints.clear(); - navigationEditorRebuildPointsMesh(); - } - - void Location::navigationEditorSave() - { - std::string baseName; - for (int i = 1; ; ++i) { - char buf[32]; - snprintf(buf, sizeof(buf), "saved_mesh%03d", i); - baseName = buf; - if (!std::filesystem::exists(baseName + ".json")) break; - } - - if (!navigation) return; - - if (navigation->saveConfig(baseName + ".json")) - std::cout << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n"; - else - std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n"; - - if (navigation->saveGrid(baseName + ".txt")) - std::cout << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n"; - else - std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n"; - } - - void Location::navigationEditorReload() - { - setupNavigation(navigationMapPaths); - std::cout << "[NAV_EDITOR] Reloaded navigation maps (" << navigationMapPaths.size() << " maps)\n"; - } - - void Location::gameObjectEditorPlaceTree() - { - if (!player) return; - - static std::mt19937 rng(std::random_device{}()); - std::uniform_real_distribution scaleDist(0.8f, 1.2f); - std::uniform_real_distribution rotDist(0.0f, 360.0f); - - GameObjectData data; - data.name = "editor_tree_" + std::to_string(++editorPlacedObjectCounter); - data.texturePath = "resources/w/exterior/tree001.png"; - data.meshPath = "resources/w/exterior/tree003.txt"; - data.rotationX = 0.0f; - data.rotationY = rotDist(rng); - data.rotationZ = 0.0f; - data.positionX = player->position.x(); - data.positionY = player->position.y(); - data.positionZ = player->position.z(); - data.scale = scaleDist(rng); - - LoadedGameObject obj = GameObjectLoader::buildLoadedObject(data, renderer, CONST_ZIP_FILE); - obj.mesh.data.Move({ data.positionX, data.positionY, data.positionZ }); - obj.mesh.RefreshVBO(); - - gameObjects[data.name] = std::move(obj); - editorPlacedObjects.push_back(data); - - std::cout << "[GAME_EDITOR] Placed '" << data.name << "' at (" - << data.positionX << ", " << data.positionZ - << ") scale=" << data.scale << " rotY=" << data.rotationY << "\n"; - } - - void Location::gameObjectEditorSave() - { - if (editorPlacedObjects.empty()) { - std::cout << "[GAME_EDITOR] No editor-placed objects to save\n"; - return; - } - - std::string baseName; - for (int i = 1; ; ++i) { - char buf[32]; - snprintf(buf, sizeof(buf), "saved_objects%03d", i); - baseName = buf; - if (!std::filesystem::exists(baseName + ".json")) break; - } - - using json = nlohmann::json; - json j; - j["objects"] = json::array(); - for (const auto& d : editorPlacedObjects) { - json obj; - obj["name"] = d.name; - obj["texturePath"] = d.texturePath; - obj["meshPath"] = d.meshPath; - obj["rotationX"] = d.rotationX; - obj["rotationY"] = d.rotationY; - obj["rotationZ"] = d.rotationZ; - obj["positionX"] = d.positionX; - obj["positionY"] = d.positionY; - obj["positionZ"] = d.positionZ; - obj["scale"] = d.scale; - j["objects"].push_back(obj); - } - - const std::string filename = baseName + ".json"; - std::ofstream out(filename); - if (out.is_open()) { - out << j.dump(4); - std::cout << "[GAME_EDITOR] Saved " << editorPlacedObjects.size() - << " object(s) to " << filename << "\n"; - } else { - std::cerr << "[GAME_EDITOR] Failed to open " << filename << " for writing\n"; - } - } - - InteractiveObject* Location::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) { if (interactiveObjects.empty()) { //std::cout << "[RAYCAST] No interactive objects to check" << std::endl; @@ -626,27 +399,49 @@ namespace ZL //std::cout << "[RAYCAST] Position: (" << intObj.position.x() << ", " << intObj.position.y() << ", " // << intObj.position.z() << "), Radius: " << intObj.interactionRadius << std::endl; - Eigen::Vector3f toObject = intObj.position - rayOrigin; - //std::cout << "[RAYCAST] Vector to object: (" << toObject.x() << ", " << toObject.y() << ", " << toObject.z() << ")" << std::endl; + const bool hasBox = !(intObj.boundsMin.isZero() && intObj.boundsMax.isZero()); + if (hasBox) { + const Eigen::Vector3f worldMin = intObj.position + intObj.boundsMin; + const Eigen::Vector3f worldMax = intObj.position + intObj.boundsMax; + float tMin = 0.1f; + float tMax = FLT_MAX; + bool miss = false; + for (int axis = 0; axis < 3; ++axis) { + if (std::abs(rayDir[axis]) < 1e-8f) { + if (rayOrigin[axis] < worldMin[axis] || rayOrigin[axis] > worldMax[axis]) { + miss = true; + break; + } + } else { + const float invD = 1.0f / rayDir[axis]; + float t0 = (worldMin[axis] - rayOrigin[axis]) * invD; + float t1 = (worldMax[axis] - rayOrigin[axis]) * invD; + if (invD < 0.0f) std::swap(t0, t1); + tMin = max(tMin, t0); + tMax = min(tMax, t1); + if (tMax < tMin) { miss = true; break; } + } + } + if (!miss && tMin < closestDistance) { + closestDistance = tMin; + closestObject = &intObj; + } + } else { + Eigen::Vector3f toObject = intObj.position - rayOrigin; - float distanceAlongRay = toObject.dot(rayDir); - //std::cout << "[RAYCAST] Distance along ray: " << distanceAlongRay << std::endl; + float distanceAlongRay = toObject.dot(rayDir); - if (distanceAlongRay < 0.1f) { - //std::cout << "[RAYCAST] -> Object behind camera, skipping" << std::endl; - continue; - } + if (distanceAlongRay < 0.1f) { + continue; + } - Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay; - float distToObject = (closestPointOnRay - intObj.position).norm(); + Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay; + float distToObject = (closestPointOnRay - intObj.position).norm(); - //std::cout << "[RAYCAST] Distance to object: " << distToObject - // << " (interaction radius: " << intObj.interactionRadius << ")" << std::endl; - - if (distToObject <= intObj.interactionRadius && distanceAlongRay < closestDistance) { - //std::cout << "[RAYCAST] *** HIT DETECTED! ***" << std::endl; - closestDistance = distanceAlongRay; - closestObject = &intObj; + if (distToObject <= intObj.interactionRadius && distanceAlongRay < closestDistance) { + closestDistance = distanceAlongRay; + closestObject = &intObj; + } } } /* @@ -780,9 +575,12 @@ namespace ZL for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); - if (navigationEditorMode) { - navigationEditorDrawNavigation(); - navigationEditorDrawPoints(); + if (editorMode == EditorMode::Navigation) { + editor.drawNavigation(); + editor.drawPoints(); + } + if (editorMode == EditorMode::InteractiveObjects) { + editor.drawInteractiveObjectBounds(); } renderer.PopMatrix(); @@ -979,9 +777,12 @@ namespace ZL for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); #endif - if (navigationEditorMode) { - navigationEditorDrawNavigation(); - navigationEditorDrawPoints(); + if (editorMode == EditorMode::Navigation) { + editor.drawNavigation(); + editor.drawPoints(); + } + if (editorMode == EditorMode::InteractiveObjects) { + editor.drawInteractiveObjectBounds(); } CheckGlError(__FILE__, __LINE__); @@ -1050,9 +851,12 @@ namespace ZL for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); - if (navigationEditorMode) { - navigationEditorDrawNavigation(); - navigationEditorDrawPoints(); + if (editorMode == EditorMode::Navigation) { + editor.drawNavigation(); + editor.drawPoints(); + } + if (editorMode == EditorMode::InteractiveObjects) { + editor.drawInteractiveObjectBounds(); } renderer.PopMatrix(); @@ -1323,10 +1127,13 @@ namespace ZL // Check if player reached target interactive object if (targetInteractiveObject && player && !targetInteractiveObject->isAnimating) { - float distToObject = (player->position - targetInteractiveObject->position).norm(); + const Eigen::Vector3f& approachTarget = targetInteractiveObject->hasInteractionPosition + ? targetInteractiveObject->interactionPosition + : targetInteractiveObject->position; + float distToObject = (player->position - approachTarget).norm(); // If player is close enough to pick up the item - if (distToObject <= targetInteractiveObject->interactionRadius + 1.0f) { + if (distToObject <= targetInteractiveObject->approachRadius) { std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl; std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->loadedObject.name << std::endl; @@ -1409,12 +1216,22 @@ namespace ZL Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized(); - if (navigationEditorMode) { + if (editorMode == EditorMode::Navigation) { if (rayDir.y() < -0.001f) { const float t = -camPos.y() / rayDir.y(); const Eigen::Vector3f hit = camPos + rayDir * t; const bool ctrlHeld = (SDL_GetModState() & KMOD_CTRL) != 0; - navigationEditorHandleLeftClick(hit, ctrlHeld); + editor.handleLeftClick(hit, ctrlHeld); + } + return; + } + + if (editorMode == EditorMode::InteractiveObjects) { + if (rayDir.y() < -0.001f) { + const float t = -camPos.y() / rayDir.y(); + const Eigen::Vector3f hit = camPos + rayDir * t; + const bool ctrlHeld = (SDL_GetModState() & KMOD_CTRL) != 0; + editor.handleInteractiveObjectClick(hit, ctrlHeld); } return; } @@ -1435,7 +1252,9 @@ namespace ZL targetInteractNpc = nullptr; targetInteractNpcIndex = -1; targetTeleportZone = nullptr; - player->setTarget(clickedObject->position); + player->setTarget(clickedObject->hasInteractionPosition + ? clickedObject->interactionPosition + : clickedObject->position); player->attackTarget = nullptr; std::cout << "[CLICK] Player moving to object..." << std::endl; } diff --git a/src/Location.h b/src/Location.h index 1886f89..9e878a7 100644 --- a/src/Location.h +++ b/src/Location.h @@ -13,6 +13,7 @@ #include "dialogue/DialogueSystem.h" #include "SparkEmitter.h" #include "TeleportZone.h" +#include "LocationEditor.h" #include #include #include @@ -99,28 +100,10 @@ namespace ZL std::function requestAdvanceDarklandsHud; // Navigation editor — toggle with 'N', save with 'B', right-click to finalize polygon - bool navigationEditorMode = false; + EditorMode editorMode = EditorMode::None; + LocationEditor editor; - std::vector navigationEditorPoints; - VertexRenderStruct navigationEditorPointsMesh; - std::vector navigationEditorNavMeshes; std::vector navigationMapPaths; - int navigationEditorObstacleCounter = 0; - - void navigationEditorBuildNavMeshes(); - void navigationEditorDrawNavigation(); - void navigationEditorRebuildPointsMesh(); - void navigationEditorDrawPoints(); - void navigationEditorHandleLeftClick(const Eigen::Vector3f& hit, bool ctrlHeld); - void navigationEditorHandleRightClick(); - void navigationEditorSave(); - void navigationEditorReload(); - - void gameObjectEditorPlaceTree(); - void gameObjectEditorSave(); - - std::vector editorPlacedObjects; - int editorPlacedObjectCounter = 0; // Set by Game when the user's primary pointer (left mouse / single touch) // has crossed the tap-vs-drag threshold and is now rotating the camera. @@ -155,8 +138,9 @@ namespace ZL int getDialogueFlag(const std::string& flag) const; protected: + friend class LocationEditor; Renderer& renderer; - Inventory& inventory; + Inventory& inventory; private: void resolveCharacterCollisions(); diff --git a/src/LocationEditor.cpp b/src/LocationEditor.cpp new file mode 100644 index 0000000..f7bd5ae --- /dev/null +++ b/src/LocationEditor.cpp @@ -0,0 +1,510 @@ +#include "LocationEditor.h" +#include "Location.h" +#include "Character.h" +#include "utils/Utils.h" +#include "render/OpenGlExtensions.h" +#include "TextModel.h" +#include "external/nlohmann/json.hpp" +#include +#include +#include +#include +#include +#include + +namespace ZL +{ + extern const char* CONST_ZIP_FILE; + + LocationEditor::LocationEditor(Location& location) + : loc(location) + { + } + + void LocationEditor::buildNavMeshes() + { + navigationEditorNavMeshes.clear(); + if (!loc.navigation) return; + const float y = loc.navigation->getFloorY() + 0.02f; + const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); + for (const auto& obs : loc.navigation->getObstaclePolygons()) { + if (obs.polygon.size() < 3) continue; + VertexRenderStruct mesh; + mesh.data = CreatePolygonFloor(obs.polygon, y, red); + mesh.RefreshVBO(); + navigationEditorNavMeshes.push_back(std::move(mesh)); + } + } + + void LocationEditor::drawNavigation() + { + loc.renderer.shaderManager.PushShader("defaultColor"); + loc.renderer.SetMatrix(); + for (const auto& mesh : navigationEditorNavMeshes) { + loc.renderer.DrawVertexRenderStruct(mesh); + } + loc.renderer.shaderManager.PopShader(); + loc.renderer.SetMatrix(); + } + + void LocationEditor::rebuildPointsMesh() + { + VertexDataStruct data; + const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f); + const float y = loc.navigation ? loc.navigation->getFloorY() + 0.05f : 0.05f; + const float s = 0.2f; + + for (const auto& pt : navigationEditorPoints) { + Eigen::Vector3f v0(pt.x(), y, pt.z() - s * 1.15f); + Eigen::Vector3f v1(pt.x() - s, y, pt.z() + s * 0.58f); + Eigen::Vector3f v2(pt.x() + s, y, pt.z() + s * 0.58f); + + data.PositionData.push_back(v0); + data.PositionData.push_back(v1); + data.PositionData.push_back(v2); + data.ColorData.push_back(yellow); + data.ColorData.push_back(yellow); + data.ColorData.push_back(yellow); + } + + navigationEditorPointsMesh.data = std::move(data); + navigationEditorPointsMesh.RefreshVBO(); + } + + void LocationEditor::drawPoints() + { + if (navigationEditorPoints.empty()) return; + loc.renderer.shaderManager.PushShader("defaultColor"); + loc.renderer.SetMatrix(); + loc.renderer.DrawVertexRenderStruct(navigationEditorPointsMesh); + loc.renderer.shaderManager.PopShader(); + loc.renderer.SetMatrix(); + } + + void LocationEditor::handleLeftClick(const Eigen::Vector3f& hit, bool ctrlHeld) + { + if (ctrlHeld) { + const float removeRadius = 1.0f; + for (auto it = navigationEditorPoints.begin(); it != navigationEditorPoints.end(); ++it) { + const float dx = it->x() - hit.x(); + const float dz = it->z() - hit.z(); + if (dx * dx + dz * dz <= removeRadius * removeRadius) { + navigationEditorPoints.erase(it); + rebuildPointsMesh(); + std::cout << "[NAV_EDITOR] Removed point, " << navigationEditorPoints.size() << " remaining\n"; + return; + } + } + std::cout << "[NAV_EDITOR] No point found within " << removeRadius << " units of click\n"; + } else { + navigationEditorPoints.push_back(hit); + rebuildPointsMesh(); + std::cout << "[NAV_EDITOR] Added point (" << hit.x() << ", " << hit.z() + << "), total: " << navigationEditorPoints.size() << "\n"; + } + } + + void LocationEditor::handleRightClick() + { + if (navigationEditorPoints.size() < 3) { + std::cout << "[NAV_EDITOR] Need at least 3 points to form a polygon (have " + << navigationEditorPoints.size() << "); clearing\n"; + navigationEditorPoints.clear(); + rebuildPointsMesh(); + return; + } + + PathFinder::ObstaclePolygon poly; + poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter); + for (const auto& pt : navigationEditorPoints) { + poly.polygon.emplace_back(pt.x(), pt.z()); + } + + if (loc.navigation) loc.navigation->addObstaclePolygon(poly); + std::cout << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with " + << poly.polygon.size() << " vertices\n"; + + { + const float y = loc.navigation ? loc.navigation->getFloorY() + 0.02f : 0.02f; + const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); + VertexRenderStruct mesh; + mesh.data = CreatePolygonFloor(poly.polygon, y, red); + mesh.RefreshVBO(); + navigationEditorNavMeshes.push_back(std::move(mesh)); + } + + navigationEditorPoints.clear(); + rebuildPointsMesh(); + } + + void LocationEditor::save() + { + std::string baseName; + for (int i = 1; ; ++i) { + char buf[32]; + snprintf(buf, sizeof(buf), "saved_mesh%03d", i); + baseName = buf; + if (!std::filesystem::exists(baseName + ".json")) break; + } + + if (!loc.navigation) return; + + if (loc.navigation->saveConfig(baseName + ".json")) + std::cout << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n"; + else + std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n"; + + if (loc.navigation->saveGrid(baseName + ".txt")) + std::cout << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n"; + else + std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n"; + } + + void LocationEditor::reload() + { + loc.setupNavigation(loc.navigationMapPaths); + std::cout << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n"; + } + + static VertexDataStruct CreateBoundsBoxMesh(const Eigen::Vector3f corners[8], const Eigen::Vector3f& color) + { + VertexDataStruct data; + + auto addTriangle = [&](const Eigen::Vector3f& a, const Eigen::Vector3f& b, const Eigen::Vector3f& c) { + data.PositionData.push_back(a); + data.PositionData.push_back(b); + data.PositionData.push_back(c); + data.ColorData.push_back(color); + data.ColorData.push_back(color); + data.ColorData.push_back(color); + }; + + auto addFace = [&](int a, int b, int c, int d) { + addTriangle(corners[a], corners[b], corners[c]); + addTriangle(corners[a], corners[c], corners[d]); + }; + + // corners: 0-3 = bottom ring (y=min), 4-7 = top ring (y=max) + // 0=(x0,y0,z0) 1=(x1,y0,z0) 2=(x1,y0,z1) 3=(x0,y0,z1) + // 4=(x0,y1,z0) 5=(x1,y1,z0) 6=(x1,y1,z1) 7=(x0,y1,z1) + addFace(0, 1, 2, 3); // bottom + addFace(7, 6, 5, 4); // top + addFace(0, 4, 5, 1); // front (z=min) + addFace(3, 2, 6, 7); // back (z=max) + addFace(0, 3, 7, 4); // left (x=min) + addFace(1, 5, 6, 2); // right (x=max) + + return data; + } + + void LocationEditor::buildInteractiveObjectBoundsMeshes() + { + interactiveObjectBoundsMeshes.clear(); + interactionPositionMeshes.clear(); + + const Eigen::Vector3f zero = Eigen::Vector3f::Zero(); + const Eigen::Vector3f colorDefault(0.1f, 0.9f, 0.6f); // teal + const Eigen::Vector3f colorSelected(1.0f, 1.0f, 0.0f); // yellow + const Eigen::Vector3f colorPole(1.0f, 0.55f, 0.0f); // orange + + int idx = 0; + for (const auto& obj : loc.interactiveObjects) { + const bool isSelected = (idx == selectedInteractiveObjectIndex); + + // --- bounds box --- + if (obj.boundsMin != zero || obj.boundsMax != zero) { + const Eigen::Vector3f& color = isSelected ? colorSelected : colorDefault; + + const float x0 = obj.boundsMin.x(), x1 = obj.boundsMax.x(); + const float y0 = obj.boundsMin.y(), y1 = obj.boundsMax.y(); + const float z0 = obj.boundsMin.z(), z1 = obj.boundsMax.z(); + + Eigen::Vector3f corners[8] = { + { x0, y0, z0 }, { x1, y0, z0 }, + { x1, y0, z1 }, { x0, y0, z1 }, + { x0, y1, z0 }, { x1, y1, z0 }, + { x1, y1, z1 }, { x0, y1, z1 }, + }; + + // Apply the same TRS the draw function applies: scale → rotateY → translate + if (obj.scale != 1.f) { + for (auto& c : corners) c *= obj.scale; + } + if (obj.rotationY != 0.f) { + const float cosR = std::cos(obj.rotationY); + const float sinR = std::sin(obj.rotationY); + for (auto& c : corners) { + const float nx = c.x() * cosR - c.z() * sinR; + const float nz = c.x() * sinR + c.z() * cosR; + c.x() = nx; + c.z() = nz; + } + } + for (auto& c : corners) c += obj.position; + + VertexRenderStruct mesh; + mesh.data = CreateBoundsBoxMesh(corners, color); + mesh.RefreshVBO(); + interactiveObjectBoundsMeshes.push_back(std::move(mesh)); + } + + // --- interaction position pole --- + if (obj.hasInteractionPosition) { + const Eigen::Vector3f& p = obj.interactionPosition; + static constexpr float hw = 0.05f; // half width/depth + static constexpr float hh = 1.5f; // half height (pole = 3.0 tall) + + Eigen::Vector3f corners[8] = { + { p.x()-hw, p.y()-hh, p.z()-hw }, { p.x()+hw, p.y()-hh, p.z()-hw }, + { p.x()+hw, p.y()-hh, p.z()+hw }, { p.x()-hw, p.y()-hh, p.z()+hw }, + { p.x()-hw, p.y()+hh, p.z()-hw }, { p.x()+hw, p.y()+hh, p.z()-hw }, + { p.x()+hw, p.y()+hh, p.z()+hw }, { p.x()-hw, p.y()+hh, p.z()+hw }, + }; + + VertexRenderStruct mesh; + mesh.data = CreateBoundsBoxMesh(corners, colorPole); + mesh.RefreshVBO(); + interactionPositionMeshes.push_back(std::move(mesh)); + } + + ++idx; + } + } + + void LocationEditor::drawInteractiveObjectBounds() + { + if (interactiveObjectBoundsMeshes.empty() && interactionPositionMeshes.empty()) return; + + loc.renderer.shaderManager.PushShader("defaultColor"); + loc.renderer.SetMatrix(); + for (const auto& mesh : interactiveObjectBoundsMeshes) { + loc.renderer.DrawVertexRenderStruct(mesh); + } + for (const auto& mesh : interactionPositionMeshes) { + loc.renderer.DrawVertexRenderStruct(mesh); + } + loc.renderer.shaderManager.PopShader(); + loc.renderer.SetMatrix(); + } + + void LocationEditor::selectInteractiveObject(int index) + { + if (index < 0 || index >= static_cast(loc.interactiveObjects.size())) { + std::cout << "[IO_EDITOR] Index " << index << " out of range (" + << loc.interactiveObjects.size() << " objects)\n"; + return; + } + selectedInteractiveObjectIndex = index; + boundsClickCount = 0; + buildInteractiveObjectBoundsMeshes(); + std::cout << "[IO_EDITOR] Selected object " << index + << " (" << loc.interactiveObjects[index].loadedObject.name << ")\n"; + } + + void LocationEditor::handleInteractiveObjectClick(const Eigen::Vector3f& worldHit, bool ctrlHeld) + { + if (selectedInteractiveObjectIndex < 0 || + selectedInteractiveObjectIndex >= static_cast(loc.interactiveObjects.size())) { + std::cout << "[IO_EDITOR] No valid object selected\n"; + return; + } + + if (ctrlHeld) { + // Set the interaction position for the selected object (world XZ, Y=0). + auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex]; + target.interactionPosition = Eigen::Vector3f(worldHit.x(), 0.f, worldHit.z()); + target.hasInteractionPosition = true; + boundsClickCount = 0; // cancel any in-progress bounds placement + buildInteractiveObjectBoundsMeshes(); + std::cout << "[IO_EDITOR] Interaction position set on object " << selectedInteractiveObjectIndex + << " (" << target.loadedObject.name << ")" + << " at (" << worldHit.x() << ", " << worldHit.z() << ")\n"; + return; + } + + const auto& obj = loc.interactiveObjects[selectedInteractiveObjectIndex]; + + // Convert world-space hit to object local space (inverse of scale → rotateY → translate) + auto worldToLocal = [&](const Eigen::Vector3f& w) -> Eigen::Vector3f { + Eigen::Vector3f local = w - obj.position; + if (obj.rotationY != 0.f) { + const float c = std::cos(-obj.rotationY); + const float s = std::sin(-obj.rotationY); + const float nx = local.x() * c - local.z() * s; + const float nz = local.x() * s + local.z() * c; + local.x() = nx; + local.z() = nz; + } + if (obj.scale > 1e-6f && obj.scale != 1.f) + local /= obj.scale; + return local; + }; + + if (boundsClickCount == 0) { + boundsClickA = worldHit; + boundsClickCount = 1; + std::cout << "[IO_EDITOR] First corner placed at world (" + << worldHit.x() << ", " << worldHit.z() << ") — click again for second corner\n"; + } else { + const Eigen::Vector3f localA = worldToLocal(boundsClickA); + const Eigen::Vector3f localB = worldToLocal(worldHit); + + auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex]; + target.boundsMin = Eigen::Vector3f( + min(localA.x(), localB.x()), 0.f, min(localA.z(), localB.z())); + target.boundsMax = Eigen::Vector3f( + max(localA.x(), localB.x()), 1.f, max(localA.z(), localB.z())); + + boundsClickCount = 0; + buildInteractiveObjectBoundsMeshes(); + + std::cout << "[IO_EDITOR] Bounds set on object " << selectedInteractiveObjectIndex + << " (" << target.loadedObject.name << ")" + << " min=(" << target.boundsMin.x() << ", " << target.boundsMin.z() << ")" + << " max=(" << target.boundsMax.x() << ", " << target.boundsMax.z() << ")\n"; + } + } + + void LocationEditor::saveInteractiveObjects() + { + std::string baseName; + for (int i = 1; ; ++i) { + char buf[32]; + snprintf(buf, sizeof(buf), "saved_interactive%03d", i); + baseName = buf; + if (!std::filesystem::exists(baseName + ".json")) break; + } + + using json = nlohmann::json; + json j; + j["objects"] = json::array(); + + for (const auto& obj : loc.interactiveObjects) { + json item; + item["name"] = obj.loadedObject.name; + item["texturePath"] = obj.loadedObject.texturePath; + if (!obj.loadedObject.textureDarkandsPath.empty()) + item["textureDarkandsPath"] = obj.loadedObject.textureDarkandsPath; + item["meshPath"] = obj.loadedObject.meshPath; + item["rotationX"] = obj.loadedObject.meshRotationX; + item["rotationY"] = obj.loadedObject.meshRotationY; + item["rotationZ"] = obj.loadedObject.meshRotationZ; + item["positionX"] = obj.jsonPositionX; + item["positionY"] = obj.jsonPositionY; + item["positionZ"] = obj.jsonPositionZ; + item["scale"] = obj.loadedObject.meshScale; + item["interactionRadius"] = obj.interactionRadius; + item["approachRadius"] = obj.approachRadius; + if (!obj.activateFunctionName.empty()) + item["activateFunction"] = obj.activateFunctionName; + item["pivotX"] = obj.pivot.x(); + item["pivotY"] = obj.pivot.y(); + item["pivotZ"] = obj.pivot.z(); + item["boundsMinX"] = obj.boundsMin.x(); + item["boundsMinY"] = obj.boundsMin.y(); + item["boundsMinZ"] = obj.boundsMin.z(); + item["boundsMaxX"] = obj.boundsMax.x(); + item["boundsMaxY"] = obj.boundsMax.y(); + item["boundsMaxZ"] = obj.boundsMax.z(); + if (obj.hasInteractionPosition) { + item["interactionPositionX"] = obj.interactionPosition.x(); + item["interactionPositionY"] = obj.interactionPosition.y(); + item["interactionPositionZ"] = obj.interactionPosition.z(); + } + j["objects"].push_back(item); + } + + const std::string filename = baseName + ".json"; + std::ofstream out(filename); + if (out.is_open()) { + out << j.dump(4); + std::cout << "[IO_EDITOR] Saved " << loc.interactiveObjects.size() + << " interactive object(s) to " << filename << "\n"; + } else { + std::cerr << "[IO_EDITOR] Failed to open " << filename << " for writing\n"; + } + } + + void LocationEditor::saveAll() + { + save(); + saveInteractiveObjects(); + } + + void LocationEditor::placeTree() + { + if (!loc.player) return; + + static std::mt19937 rng(std::random_device{}()); + std::uniform_real_distribution scaleDist(0.8f, 1.2f); + std::uniform_real_distribution rotDist(0.0f, 360.0f); + + GameObjectData data; + data.name = "editor_tree_" + std::to_string(++editorPlacedObjectCounter); + data.texturePath = "resources/w/exterior/tree001.png"; + data.meshPath = "resources/w/exterior/tree003.txt"; + data.rotationX = 0.0f; + data.rotationY = rotDist(rng); + data.rotationZ = 0.0f; + data.positionX = loc.player->position.x(); + data.positionY = loc.player->position.y(); + data.positionZ = loc.player->position.z(); + data.scale = scaleDist(rng); + + LoadedGameObject obj = GameObjectLoader::buildLoadedObject(data, loc.renderer, CONST_ZIP_FILE); + obj.mesh.data.Move({ data.positionX, data.positionY, data.positionZ }); + obj.mesh.RefreshVBO(); + + loc.gameObjects[data.name] = std::move(obj); + editorPlacedObjects.push_back(data); + + std::cout << "[GAME_EDITOR] Placed '" << data.name << "' at (" + << data.positionX << ", " << data.positionZ + << ") scale=" << data.scale << " rotY=" << data.rotationY << "\n"; + } + + void LocationEditor::saveObjects() + { + if (editorPlacedObjects.empty()) { + std::cout << "[GAME_EDITOR] No editor-placed objects to save\n"; + return; + } + + std::string baseName; + for (int i = 1; ; ++i) { + char buf[32]; + snprintf(buf, sizeof(buf), "saved_objects%03d", i); + baseName = buf; + if (!std::filesystem::exists(baseName + ".json")) break; + } + + using json = nlohmann::json; + json j; + j["objects"] = json::array(); + for (const auto& d : editorPlacedObjects) { + json obj; + obj["name"] = d.name; + obj["texturePath"] = d.texturePath; + obj["meshPath"] = d.meshPath; + obj["rotationX"] = d.rotationX; + obj["rotationY"] = d.rotationY; + obj["rotationZ"] = d.rotationZ; + obj["positionX"] = d.positionX; + obj["positionY"] = d.positionY; + obj["positionZ"] = d.positionZ; + obj["scale"] = d.scale; + j["objects"].push_back(obj); + } + + const std::string filename = baseName + ".json"; + std::ofstream out(filename); + if (out.is_open()) { + out << j.dump(4); + std::cout << "[GAME_EDITOR] Saved " << editorPlacedObjects.size() + << " object(s) to " << filename << "\n"; + } else { + std::cerr << "[GAME_EDITOR] Failed to open " << filename << " for writing\n"; + } + } + +} // namespace ZL diff --git a/src/LocationEditor.h b/src/LocationEditor.h new file mode 100644 index 0000000..a5c373e --- /dev/null +++ b/src/LocationEditor.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include "render/Renderer.h" +#include "navigation/PathFinder.h" +#include "items/GameObjectLoader.h" +#include + +namespace ZL { + +class Location; // forward declaration — LocationEditor.cpp includes Location.h + +enum class EditorMode { + None, + Navigation, + InteractiveObjects + // GameObjects // future +}; + +class LocationEditor { +public: + explicit LocationEditor(Location& location); + + // Navigation editor state + std::vector navigationEditorPoints; + VertexRenderStruct navigationEditorPointsMesh; + std::vector navigationEditorNavMeshes; + int navigationEditorObstacleCounter = 0; + + // Interactive object editor state + std::vector interactiveObjectBoundsMeshes; + std::vector interactionPositionMeshes; + int selectedInteractiveObjectIndex = 0; + int boundsClickCount = 0; // 0 = waiting for first corner, 1 = waiting for second + Eigen::Vector3f boundsClickA = Eigen::Vector3f::Zero(); + + // Game object editor state + std::vector editorPlacedObjects; + int editorPlacedObjectCounter = 0; + + // Navigation editor methods + void buildNavMeshes(); + void drawNavigation(); + void rebuildPointsMesh(); + void drawPoints(); + void handleLeftClick(const Eigen::Vector3f& hit, bool ctrlHeld); + void handleRightClick(); + void save(); + void reload(); + + // Interactive object editor methods + void buildInteractiveObjectBoundsMeshes(); + void drawInteractiveObjectBounds(); + void selectInteractiveObject(int index); + void handleInteractiveObjectClick(const Eigen::Vector3f& worldHit, bool ctrlHeld); + void saveInteractiveObjects(); + + // Save all editor data at once (nav mesh + interactive objects) + void saveAll(); + + // Game object editor methods + void placeTree(); + void saveObjects(); + +private: + Location& loc; +}; + +} // namespace ZL diff --git a/src/items/GameObjectLoader.cpp b/src/items/GameObjectLoader.cpp index 7474a9b..b4c6d2e 100644 --- a/src/items/GameObjectLoader.cpp +++ b/src/items/GameObjectLoader.cpp @@ -93,10 +93,23 @@ namespace ZL { InteractiveObjectData data; data.base = parseGameObjectData(item); data.interactionRadius = item.value("interactionRadius", 2.0f); + data.approachRadius = item.value("approachRadius", data.interactionRadius); data.activateFunctionName = item.value("activateFunction", ""); data.pivotX = item.value("pivotX", 0.0f); data.pivotY = item.value("pivotY", 0.0f); data.pivotZ = item.value("pivotZ", 0.0f); + data.boundsMinX = item.value("boundsMinX", 0.0f); + data.boundsMinY = item.value("boundsMinY", 0.0f); + data.boundsMinZ = item.value("boundsMinZ", 0.0f); + data.boundsMaxX = item.value("boundsMaxX", 0.0f); + data.boundsMaxY = item.value("boundsMaxY", 0.0f); + data.boundsMaxZ = item.value("boundsMaxZ", 0.0f); + if (item.contains("interactionPositionX")) { + data.hasInteractionPosition = true; + data.interactionPositionX = item.value("interactionPositionX", 0.0f); + data.interactionPositionY = item.value("interactionPositionY", 0.0f); + data.interactionPositionZ = item.value("interactionPositionZ", 0.0f); + } if (!data.base.meshPath.empty()) objects.push_back(std::move(data)); } @@ -182,8 +195,26 @@ namespace ZL { InteractiveObject intObj; intObj.loadedObject = buildLoadedObject(data.base, renderer, zipPath); intObj.interactionRadius = data.interactionRadius; + intObj.approachRadius = data.approachRadius; intObj.activateFunctionName = data.activateFunctionName; intObj.pivot = Eigen::Vector3f(data.pivotX, data.pivotY, data.pivotZ); + intObj.boundsMin = Eigen::Vector3f(data.boundsMinX, data.boundsMinY, data.boundsMinZ); + intObj.boundsMax = Eigen::Vector3f(data.boundsMaxX, data.boundsMaxY, data.boundsMaxZ); + intObj.hasInteractionPosition = data.hasInteractionPosition; + if (data.hasInteractionPosition) + intObj.interactionPosition = Eigen::Vector3f(data.interactionPositionX, data.interactionPositionY, data.interactionPositionZ); + + // Store source data needed for serialization. + intObj.loadedObject.texturePath = data.base.texturePath; + intObj.loadedObject.textureDarkandsPath = data.base.textureDarkandsPath; + intObj.loadedObject.meshPath = data.base.meshPath; + intObj.loadedObject.meshRotationX = data.base.rotationX; + intObj.loadedObject.meshRotationY = data.base.rotationY; + intObj.loadedObject.meshRotationZ = data.base.rotationZ; + intObj.loadedObject.meshScale = data.base.scale; + intObj.jsonPositionX = data.base.positionX; + intObj.jsonPositionY = data.base.positionY; + intObj.jsonPositionZ = data.base.positionZ; intObj.loadedObject.mesh.RefreshVBO(); diff --git a/src/items/GameObjectLoader.h b/src/items/GameObjectLoader.h index 5774ff9..4cb0819 100644 --- a/src/items/GameObjectLoader.h +++ b/src/items/GameObjectLoader.h @@ -29,10 +29,15 @@ namespace ZL { struct InteractiveObjectData { GameObjectData base; float interactionRadius = 2.0f; + float approachRadius = 2.0f; std::string activateFunctionName; float pivotX = 0.0f; float pivotY = 0.0f; float pivotZ = 0.0f; + float boundsMinX = 0.0f, boundsMinY = 0.0f, boundsMinZ = 0.0f; + float boundsMaxX = 0.0f, boundsMaxY = 0.0f, boundsMaxZ = 0.0f; + bool hasInteractionPosition = false; + float interactionPositionX = 0.0f, interactionPositionY = 0.0f, interactionPositionZ = 0.0f; }; struct NpcData { diff --git a/src/items/InteractiveObject.h b/src/items/InteractiveObject.h index 4ef4c51..5feb978 100644 --- a/src/items/InteractiveObject.h +++ b/src/items/InteractiveObject.h @@ -16,6 +16,12 @@ namespace ZL { std::shared_ptr textureDarklands; VertexRenderStruct mesh; std::string name; + // Source paths and baked transform — populated by GameObjectLoader, used for serialization. + std::string texturePath; + std::string textureDarkandsPath; + std::string meshPath; + float meshRotationX = 0.f, meshRotationY = 0.f, meshRotationZ = 0.f; + float meshScale = 1.f; }; struct InteractiveObject { @@ -26,6 +32,14 @@ namespace ZL { float scale = 1.0f; // uniform scale float alpha = 1.0f; // opacity: 1=opaque, 0=fully transparent float interactionRadius; + float approachRadius = 2.0f; + Eigen::Vector3f boundsMin = Eigen::Vector3f::Zero(); + Eigen::Vector3f boundsMax = Eigen::Vector3f::Zero(); + Eigen::Vector3f interactionPosition = Eigen::Vector3f::Zero(); + // Original JSON position before mesh-centering offset applied at load time — needed for serialization. + float jsonPositionX = 0.f, jsonPositionY = 0.f, jsonPositionZ = 0.f; + + bool hasInteractionPosition = false; bool isActive = true; bool isAnimating = false; // true while a timed animation is running std::string activateFunctionName;