diff --git a/resources/config2/navigation_dorm.json b/resources/config2/navigation_dorm.json index bc1cef4..4219425 100644 --- a/resources/config2/navigation_dorm.json +++ b/resources/config2/navigation_dorm.json @@ -1,9 +1,9 @@ { - "cellSize": 0.4, - "agentRadius": 0.45, + "cellSize": 0.1, + "agentRadius": 0.25, "floorY": 0.0, - "objectPadding": 0.25, - "boundaryPadding": 0.35, + "objectPadding": 0.15, + "boundaryPadding": 0.15, "areas": [ { "name": "main_corridor", @@ -29,5 +29,193 @@ } ], "obstacles": [ - ] + { + "name": "editor_obstacle_1", + "polygon": [ + [ + 1.4521160125732422, + -15.647087097167969 + ], + [ + 0.7836513519287109, + -15.604467391967773 + ], + [ + 0.8457736968994141, + -17.430662155151367 + ], + [ + 1.3778166770935059, + -17.40001678466797 + ] + ] + }, + { + "name": "editor_obstacle_2", + "polygon": [ + [ + 1.238701343536377, + -17.41904640197754 + ], + [ + 1.2542033195495605, + -16.888349533081055 + ], + [ + 8.557634353637695, + -16.686010360717773 + ], + [ + 8.580190658569336, + -17.49493980407715 + ] + ] + }, + { + "name": "editor_obstacle_3", + "polygon": [ + [ + 8.581539154052734, + -17.44879150390625 + ], + [ + 9.2276029586792, + -17.467662811279297 + ], + [ + 9.311224937438965, + -11.442724227905273 + ], + [ + 8.525369644165039, + -11.465954780578613 + ] + ] + }, + { + "name": "editor_obstacle_4", + "polygon": [ + [ + 8.59122085571289, + -11.583345413208008 + ], + [ + 8.580953598022461, + -11.144271850585938 + ], + [ + 3.9417855739593506, + -11.05494213104248 + ], + [ + 3.879462718963623, + -11.607364654541016 + ] + ] + }, + { + "name": "editor_obstacle_5", + "polygon": [ + [ + 4.017904758453369, + -11.611408233642578 + ], + [ + 4.006087303161621, + -13.59709644317627 + ], + [ + 3.777372121810913, + -13.521135330200195 + ], + [ + 3.7294042110443115, + -11.210392951965332 + ] + ] + }, + { + "name": "editor_obstacle_6", + "polygon": [ + [ + 2.783853769302368, + -13.538301467895508 + ], + [ + 2.766845703125, + -13.329962730407715 + ], + [ + 3.921206474304199, + -13.34058952331543 + ], + [ + 3.937540054321289, + -13.572001457214355 + ] + ] + }, + { + "name": "editor_obstacle_7", + "polygon": [ + [ + 2.046168327331543, + -13.493659019470215 + ], + [ + 2.006760597229004, + -13.261573791503906 + ], + [ + 1.1306328773498535, + -13.21288776397705 + ], + [ + 1.190417766571045, + -13.537941932678223 + ] + ] + }, + { + "name": "editor_obstacle_8", + "polygon": [ + [ + 1.2021913528442383, + -14.716050148010254 + ], + [ + 1.3182783126831055, + -11.532548904418945 + ], + [ + 1.0644679069519043, + -11.525135040283203 + ], + [ + 0.9918317794799805, + -14.802279472351074 + ] + ] + }, + { + "name": "editor_obstacle_9", + "polygon": [ + [ + 1.2470345497131348, + -11.599748611450195 + ], + [ + 3.9473354816436768, + -11.655533790588379 + ], + [ + 3.9574456214904785, + -11.309428215026855 + ], + [ + 1.2795448303222656, + -11.27739143371582 + ] + ] + }] } diff --git a/resources/dialogue/dorm_dialogues.json b/resources/dialogue/dorm_dialogues.json index 527e5a9..7319b85 100644 --- a/resources/dialogue/dorm_dialogues.json +++ b/resources/dialogue/dorm_dialogues.json @@ -25,6 +25,42 @@ "type": "End" } ] + }, + { + "id": "dialog_taxi001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", + "text": "Прежде чем выходить наружу, я должен заказать такси до универа.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_second_floor001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", + "text": "На втором этаже женское общежитие, мне там делать нечего.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] } ], "cutscenes": [ diff --git a/src/Game.cpp b/src/Game.cpp index c3955cc..8eaf19b 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -473,6 +473,12 @@ namespace ZL onPointerUp(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my); } } + else if (event.button.button == SDL_BUTTON_RIGHT + && event.type == SDL_MOUSEBUTTONUP + && navigationEditorMode + && currentLocation) { + currentLocation->navigationEditorHandleRightClick(); + } } else if (event.type == SDL_MOUSEMOTION) { int eventX = event.motion.x; @@ -514,6 +520,17 @@ namespace ZL currentLocation->dialogueSystem.startDialogue("test_cutscene_pan_dialogue"); break; + case SDLK_n: + navigationEditorMode = !navigationEditorMode; + if (currentLocation) { + currentLocation->navigationEditorMode = navigationEditorMode; + if (navigationEditorMode) { + currentLocation->navigationEditorBuildNavMeshes(); + } + } + std::cout << "[NAV_EDITOR] Mode: " << (navigationEditorMode ? "ON" : "OFF") << std::endl; + break; + case SDLK_o: //y = y + 0.002; //currentLocation->player->hp = 200; @@ -543,6 +560,12 @@ namespace ZL activateSlowMoEffect(); break; + case SDLK_b: + if (navigationEditorMode && currentLocation) { + currentLocation->navigationEditorSave(); + } + break; + case SDLK_j: menuManager.toggleQuestJournal(); break; diff --git a/src/Game.h b/src/Game.h index 320e8be..9ba3ea0 100644 --- a/src/Game.h +++ b/src/Game.h @@ -45,6 +45,8 @@ namespace ZL { std::unordered_map> locations; std::shared_ptr currentLocation; + bool navigationEditorMode = false; + Inventory inventory; InteractiveObject* pickedUpObject = nullptr; diff --git a/src/Location.cpp b/src/Location.cpp index d1ae4e8..feb4194 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -7,12 +7,14 @@ #include #include #include +#include #include #include #include #include "GameConstants.h" #include "Character.h" #include "external/nlohmann/json.hpp" +#include namespace ZL @@ -163,6 +165,8 @@ namespace ZL false });*/ + navigationEditorConfigPath = params.navigationJsonPath; + scriptEngine.init(this, &inventory, params.scriptPath); @@ -233,9 +237,9 @@ namespace ZL // NPC + player are handled as dynamic obstacles, so they are intentionally NOT in JSON. navigation.build({}, navigationJsonPath, CONST_ZIP_FILE); -#ifdef SHOW_PATH - buildDebugNavMeshes(); -#endif + if (navigationEditorMode) { + navigationEditorBuildNavMeshes(); + } static constexpr float kDynamicObstacleInfluenceDist = 6.0f; @@ -278,34 +282,145 @@ namespace ZL } } -#ifdef SHOW_PATH - void Location::buildDebugNavMeshes() + void Location::navigationEditorBuildNavMeshes() { - debugNavMeshes.clear(); - const auto& areas = navigation.getAreas(); - float y = navigation.getFloorY() + 0.02f; - Eigen::Vector3f red(1.0f, 0.0f, 0.0f); - - for (const auto& area : areas) { - if (area.polygon.size() < 3) continue; + navigationEditorNavMeshes.clear(); + 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(area.polygon, y, red); + mesh.data = CreatePolygonFloor(obs.polygon, y, red); mesh.RefreshVBO(); - debugNavMeshes.push_back(std::move(mesh)); + navigationEditorNavMeshes.push_back(std::move(mesh)); } } - void Location::drawDebugNavigation() + void Location::navigationEditorDrawNavigation() { renderer.shaderManager.PushShader("defaultColor"); renderer.SetMatrix(); - for (const auto& mesh : debugNavMeshes) { + for (const auto& mesh : navigationEditorNavMeshes) { renderer.DrawVertexRenderStruct(mesh); } renderer.shaderManager.PopShader(); renderer.SetMatrix(); } -#endif + + void Location::navigationEditorRebuildPointsMesh() + { + VertexDataStruct data; + const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f); + const float y = navigation.getFloorY() + 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()); + } + + 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.getFloorY() + 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 filename; + for (int i = 1; ; ++i) { + char buf[32]; + snprintf(buf, sizeof(buf), "saved_mesh%03d.json", i); + filename = buf; + if (!std::filesystem::exists(filename)) break; + } + + if (navigation.saveConfig(filename)) { + std::cout << "[NAV_EDITOR] Saved navigation config to: " << filename << "\n"; + } else { + std::cerr << "[NAV_EDITOR] Failed to save to: " << filename << "\n"; + } + } + + void Location::navigationEditorReload() + { + setupNavigation(navigationEditorConfigPath); + std::cout << "[NAV_EDITOR] Reloaded navigation from: " << navigationEditorConfigPath << "\n"; + } InteractiveObject* Location::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) { @@ -483,9 +598,10 @@ namespace ZL for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); -#ifdef SHOW_PATH - drawDebugNavigation(); -#endif + if (navigationEditorMode) { + navigationEditorDrawNavigation(); + navigationEditorDrawPoints(); + } renderer.PopMatrix(); @@ -677,6 +793,12 @@ namespace ZL for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); #endif + + if (navigationEditorMode) { + navigationEditorDrawNavigation(); + navigationEditorDrawPoints(); + } + CheckGlError(__FILE__, __LINE__); renderer.PopMatrix(); @@ -954,6 +1076,16 @@ namespace ZL Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized(); + if (navigationEditorMode) { + 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); + } + return; + } + std::cout << "[CLICK] Camera position: (" << camPos.x() << ", " << camPos.y() << ", " << camPos.z() << ")" << std::endl; std::cout << "[CLICK] Ray direction: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl; diff --git a/src/Location.h b/src/Location.h index 9f6ce5a..caccc72 100644 --- a/src/Location.h +++ b/src/Location.h @@ -73,11 +73,24 @@ namespace ZL TeleportZone* targetTeleportZone = nullptr; std::function onTeleport; -#ifdef SHOW_PATH - std::vector debugNavMeshes; - void buildDebugNavMeshes(); - void drawDebugNavigation(); -#endif + // Navigation editor — toggle with 'N', save with 'B', right-click to finalize polygon + bool navigationEditorMode = false; + + std::vector navigationEditorPoints; + VertexRenderStruct navigationEditorPointsMesh; + std::vector navigationEditorNavMeshes; + std::string navigationEditorConfigPath; + 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(); + // 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. bool cameraDragging = false; diff --git a/src/navigation/PathFinder.cpp b/src/navigation/PathFinder.cpp index 51c6e9a..f775107 100644 --- a/src/navigation/PathFinder.cpp +++ b/src/navigation/PathFinder.cpp @@ -3,6 +3,7 @@ #include "utils/Utils.h" #include #include +#include #include #include #include @@ -409,6 +410,58 @@ bool PathFinder::isWalkable(const Eigen::Vector3f& point) const return worldToCell(point, cell) && isCellWalkable(cell, walkable); } +void PathFinder::addObstaclePolygon(const ObstaclePolygon& polygon) +{ + obstaclePolygons.push_back(polygon); + if (ready) { + rebuildWalkableGrid(); + } +} + +bool PathFinder::saveConfig(const std::string& path) const +{ + json root; + root["cellSize"] = cellSize; + root["agentRadius"] = agentRadius; + root["floorY"] = floorY; + root["objectPadding"] = objectPadding; + root["boundaryPadding"] = boundaryPadding; + + json areasArr = json::array(); + for (const auto& area : areas) { + json areaObj; + areaObj["name"] = area.name; + areaObj["available"] = area.available; + json poly = json::array(); + for (const auto& pt : area.polygon) { + poly.push_back(json::array({ pt.x(), pt.y() })); + } + areaObj["polygon"] = poly; + areasArr.push_back(areaObj); + } + root["areas"] = areasArr; + + json obstArr = json::array(); + for (const auto& obs : obstaclePolygons) { + json obsObj; + obsObj["name"] = obs.name; + json poly = json::array(); + for (const auto& pt : obs.polygon) { + poly.push_back(json::array({ pt.x(), pt.y() })); + } + obsObj["polygon"] = poly; + obstArr.push_back(obsObj); + } + root["obstacles"] = obstArr; + + std::ofstream f(path); + if (!f.is_open()) { + return false; + } + f << root.dump(2); + return f.good(); +} + void PathFinder::loadConfig(const std::string& configPath, const std::string& zipPath) { cellSize = 0.4f; diff --git a/src/navigation/PathFinder.h b/src/navigation/PathFinder.h index 60134f0..7604c72 100644 --- a/src/navigation/PathFinder.h +++ b/src/navigation/PathFinder.h @@ -50,8 +50,12 @@ public: bool isReady() const { return ready; } bool isWalkable(const Eigen::Vector3f& point) const; const std::vector& getAreas() const { return areas; } + const std::vector& getObstaclePolygons() const { return obstaclePolygons; } float getFloorY() const { return floorY; } + void addObstaclePolygon(const ObstaclePolygon& polygon); + bool saveConfig(const std::string& path) const; + private: float cellSize = 0.4f;