From e0968b55226b3c2d26902adc70a481456ddf5a58 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Thu, 18 Jun 2026 20:51:44 +0300 Subject: [PATCH] Working on game states --- proj-web/CMakeLists.txt | 1 + proj-windows/CMakeLists.txt | 1 + src/Location.cpp | 58 ++++----- src/LocationEditor.cpp | 104 +++++++-------- src/ScriptEngine.cpp | 18 +-- src/items/GameObjectLoader.cpp | 87 ++++++------- src/items/GameObjectLoader.h | 5 + src/items/InteractiveObject.cpp | 195 ++++++++++++++++++----------- src/items/InteractiveObject.h | 54 ++------ src/items/InteractiveObjectState.h | 68 ++++++++++ 10 files changed, 340 insertions(+), 251 deletions(-) create mode 100644 src/items/InteractiveObjectState.h diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt index 0f509a7..6ed480f 100644 --- a/proj-web/CMakeLists.txt +++ b/proj-web/CMakeLists.txt @@ -119,6 +119,7 @@ set(SOURCES ../src/items/ItemRegistry.cpp ../src/items/InteractiveObject.h ../src/items/InteractiveObject.cpp + ../src/items/InteractiveObjectState.h ../src/dialogue/DialogueTypes.h ../src/dialogue/DialogueDatabase.h ../src/dialogue/DialogueDatabase.cpp diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index afb731e..3fae3ab 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -74,6 +74,7 @@ add_executable(witcher001 ../src/items/ItemRegistry.cpp ../src/items/InteractiveObject.h ../src/items/InteractiveObject.cpp + ../src/items/InteractiveObjectState.h ../src/dialogue/DialogueTypes.h ../src/dialogue/DialogueDatabase.h ../src/dialogue/DialogueDatabase.cpp diff --git a/src/Location.cpp b/src/Location.cpp index f1aeed7..75e8d89 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -456,7 +456,7 @@ namespace ZL for (auto& intObj : interactiveObjects) { //std::cout << "[RAYCAST] Checking object: " << intObj.loadedObject.name << " (active: " << intObj.isActive << ")" << std::endl; - if (!intObj.isActive || intObj.isAnimating) { + if (!intObj.state.isActive || intObj.state.isAnimating) { //std::cout << "[RAYCAST] -> Object inactive or animating, skipping" << std::endl; continue; } @@ -470,10 +470,10 @@ namespace ZL //std::cout << "[RAYCAST] Position: (" << intObj.position.x() << ", " << intObj.position.y() << ", " // << intObj.position.z() << "), Radius: " << intObj.interactionRadius << std::endl; - const bool hasBox = !(intObj.boundsMin.isZero() && intObj.boundsMax.isZero()); + const bool hasBox = !(intObj.state.boundsMin.isZero() && intObj.state.boundsMax.isZero()); if (hasBox) { - const Eigen::Vector3f worldMin = intObj.position + intObj.boundsMin; - const Eigen::Vector3f worldMax = intObj.position + intObj.boundsMax; + const Eigen::Vector3f worldMin = intObj.state.position + intObj.state.boundsMin; + const Eigen::Vector3f worldMax = intObj.state.position + intObj.state.boundsMax; float tMin = 0.1f; float tMax = FLT_MAX; bool miss = false; @@ -498,7 +498,7 @@ namespace ZL closestObject = &intObj; } } else { - Eigen::Vector3f toObject = intObj.position - rayOrigin; + Eigen::Vector3f toObject = intObj.state.position - rayOrigin; float distanceAlongRay = toObject.dot(rayDir); @@ -507,9 +507,9 @@ namespace ZL } Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay; - float distToObject = (closestPointOnRay - intObj.position).norm(); + float distToObject = (closestPointOnRay - intObj.state.position).norm(); - if (distToObject <= intObj.interactionRadius && distanceAlongRay < closestDistance) { + if (distToObject <= intObj.state.interactionRadius && distanceAlongRay < closestDistance) { closestDistance = distanceAlongRay; closestObject = &intObj; } @@ -632,7 +632,7 @@ namespace ZL } for (auto& intObj : interactiveObjects) { - if (intObj.isActive) { + if (intObj.state.isActive) { intObj.draw(renderer); } } @@ -724,9 +724,9 @@ namespace ZL } for (auto& intObj : interactiveObjects) { - if (intObj.castShadow && intObj.loadedObject.texture) { + if (intObj.state.castShadow && intObj.loadedObject.texture) { renderer.PushMatrix(); - renderer.TranslateMatrix(intObj.position); + renderer.TranslateMatrix(intObj.state.position); renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh); renderer.PopMatrix(); } @@ -818,7 +818,7 @@ namespace ZL CheckGlError(__FILE__, __LINE__); for (auto& intObj : interactiveObjects) { - if (intObj.isActive) { + if (intObj.state.isActive) { intObj.draw(renderer); } } @@ -911,7 +911,7 @@ namespace ZL } for (auto& intObj : interactiveObjects) { - if (intObj.isActive) { + if (intObj.state.isActive) { intObj.drawDarklands(renderer); } } @@ -971,9 +971,9 @@ namespace ZL renderer.DrawVertexRenderStruct(gameObj.mesh); } for (auto& intObj : interactiveObjects) { - if (intObj.castShadowNight && intObj.loadedObject.texture) { + if (intObj.state.castShadowNight && intObj.loadedObject.texture) { renderer.PushMatrix(); - renderer.TranslateMatrix(intObj.position); + renderer.TranslateMatrix(intObj.state.position); renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh); renderer.PopMatrix(); } @@ -1144,7 +1144,7 @@ namespace ZL } for (auto& intObj : interactiveObjects) { - if (intObj.isActive) { + if (intObj.state.isActive) { intObj.draw(renderer); } } @@ -1492,22 +1492,22 @@ namespace ZL for (auto& [idx, t] : npcBumpsPlayerCooldown) t -= deltaS; // Check if player reached target interactive object - if (!tutorialInteractiveObjectsLocked && targetInteractiveObject && player && !targetInteractiveObject->isAnimating) { - const Eigen::Vector3f& approachTarget = targetInteractiveObject->hasInteractionPosition - ? targetInteractiveObject->interactionPosition - : targetInteractiveObject->position; + if (!tutorialInteractiveObjectsLocked && targetInteractiveObject && player && !targetInteractiveObject->state.isAnimating) { + const Eigen::Vector3f& approachTarget = targetInteractiveObject->state.hasInteractionPosition + ? targetInteractiveObject->state.interactionPosition + : targetInteractiveObject->state.position; float distToObject = (player->state.position - approachTarget).norm(); // If player is close enough to pick up the item - if (distToObject <= targetInteractiveObject->approachRadius) { + if (distToObject <= targetInteractiveObject->state.approachRadius) { std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl; std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->loadedObject.name << std::endl; // Call custom activate function if specified, otherwise use fallback try { - if (!targetInteractiveObject->activateFunctionName.empty()) { - std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->activateFunctionName << std::endl; - scriptEngine.callActivateFunction(targetInteractiveObject->activateFunctionName); + if (!targetInteractiveObject->state.activateFunctionName.empty()) { + std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->state.activateFunctionName << std::endl; + scriptEngine.callActivateFunction(targetInteractiveObject->state.activateFunctionName); } else { std::cout << "[PICKUP] Using fallback callback" << std::endl; @@ -1632,19 +1632,19 @@ namespace ZL // First check if we clicked on interactive object InteractiveObject* clickedObject = raycastInteractiveObjects(camPos, rayDir); - if (clickedObject && player && clickedObject->isActive) { + if (clickedObject && player && clickedObject->state.isActive) { std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->loadedObject.name << " ***" << std::endl; - std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", " - << clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl; + std::cout << "[CLICK] Object position: (" << clickedObject->state.position.x() << ", " + << clickedObject->state.position.y() << ", " << clickedObject->state.position.z() << ")" << std::endl; std::cout << "[CLICK] Player position: (" << player->state.position.x() << ", " << player->state.position.y() << ", " << player->state.position.z() << ")" << std::endl; targetInteractiveObject = clickedObject; targetInteractNpcIndex = -1; targetTeleportZone = nullptr; - player->setTarget(clickedObject->hasInteractionPosition - ? clickedObject->interactionPosition - : clickedObject->position); + player->setTarget(clickedObject->state.hasInteractionPosition + ? clickedObject->state.interactionPosition + : clickedObject->state.position); player->state.attackTargetIndex = CharacterState::kNoTarget; std::cout << "[CLICK] Player moving to object..." << std::endl; } diff --git a/src/LocationEditor.cpp b/src/LocationEditor.cpp index bd2698f..e286144 100644 --- a/src/LocationEditor.cpp +++ b/src/LocationEditor.cpp @@ -212,12 +212,12 @@ namespace ZL const bool isSelected = (idx == selectedInteractiveObjectIndex); // --- bounds box --- - if (obj.boundsMin != zero || obj.boundsMax != zero) { + if (obj.state.boundsMin != zero || obj.state.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(); + const float x0 = obj.state.boundsMin.x(), x1 = obj.state.boundsMax.x(); + const float y0 = obj.state.boundsMin.y(), y1 = obj.state.boundsMax.y(); + const float z0 = obj.state.boundsMin.z(), z1 = obj.state.boundsMax.z(); Eigen::Vector3f corners[8] = { { x0, y0, z0 }, { x1, y0, z0 }, @@ -227,12 +227,12 @@ namespace ZL }; // 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.state.scale != 1.f) { + for (auto& c : corners) c *= obj.state.scale; } - if (obj.rotationY != 0.f) { - const float cosR = std::cos(obj.rotationY); - const float sinR = std::sin(obj.rotationY); + if (obj.state.rotationY != 0.f) { + const float cosR = std::cos(obj.state.rotationY); + const float sinR = std::sin(obj.state.rotationY); for (auto& c : corners) { const float nx = c.x() * cosR - c.z() * sinR; const float nz = c.x() * sinR + c.z() * cosR; @@ -240,7 +240,7 @@ namespace ZL c.z() = nz; } } - for (auto& c : corners) c += obj.position; + for (auto& c : corners) c += obj.state.position; VertexRenderStruct mesh; mesh.data = CreateBoundsBoxMesh(corners, color); @@ -249,8 +249,8 @@ namespace ZL } // --- interaction position pole --- - if (obj.hasInteractionPosition) { - const Eigen::Vector3f& p = obj.interactionPosition; + if (obj.state.hasInteractionPosition) { + const Eigen::Vector3f& p = obj.state.interactionPosition; static constexpr float hw = 0.05f; // half width/depth static constexpr float hh = 1.5f; // half height (pole = 3.0 tall) @@ -312,8 +312,8 @@ namespace ZL 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; + target.state.interactionPosition = Eigen::Vector3f(worldHit.x(), 0.f, worldHit.z()); + target.state.hasInteractionPosition = true; boundsClickCount = 0; // cancel any in-progress bounds placement buildInteractiveObjectBoundsMeshes(); std::cout << "[IO_EDITOR] Interaction position set on object " << selectedInteractiveObjectIndex @@ -326,17 +326,17 @@ namespace ZL // 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); + Eigen::Vector3f local = w - obj.state.position; + if (obj.state.rotationY != 0.f) { + const float c = std::cos(-obj.state.rotationY); + const float s = std::sin(-obj.state.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; + if (obj.state.scale > 1e-6f && obj.state.scale != 1.f) + local /= obj.state.scale; return local; }; @@ -350,9 +350,9 @@ namespace ZL const Eigen::Vector3f localB = worldToLocal(worldHit); auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex]; - target.boundsMin = Eigen::Vector3f( + target.state.boundsMin = Eigen::Vector3f( min(localA.x(), localB.x()), 0.f, min(localA.z(), localB.z())); - target.boundsMax = Eigen::Vector3f( + target.state.boundsMax = Eigen::Vector3f( max(localA.x(), localB.x()), 1.f, max(localA.z(), localB.z())); boundsClickCount = 0; @@ -360,8 +360,8 @@ namespace ZL 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"; + << " min=(" << target.state.boundsMin.x() << ", " << target.state.boundsMin.z() << ")" + << " max=(" << target.state.boundsMax.x() << ", " << target.state.boundsMax.z() << ")\n"; } } @@ -382,34 +382,34 @@ namespace ZL 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(); + item["texturePath"] = obj.loadedObject.creationInfo.texturePath; + if (!obj.loadedObject.creationInfo.textureDarkandsPath.empty()) + item["textureDarkandsPath"] = obj.loadedObject.creationInfo.textureDarkandsPath; + item["meshPath"] = obj.loadedObject.creationInfo.meshPath; + item["rotationX"] = obj.loadedObject.creationInfo.meshRotationX; + item["rotationY"] = obj.loadedObject.creationInfo.meshRotationY; + item["rotationZ"] = obj.loadedObject.creationInfo.meshRotationZ; + item["positionX"] = obj.state.jsonPositionX; + item["positionY"] = obj.state.jsonPositionY; + item["positionZ"] = obj.state.jsonPositionZ; + item["scale"] = obj.loadedObject.creationInfo.meshScale; + item["interactionRadius"] = obj.state.interactionRadius; + item["approachRadius"] = obj.state.approachRadius; + if (!obj.state.activateFunctionName.empty()) + item["activateFunction"] = obj.state.activateFunctionName; + item["pivotX"] = obj.state.pivot.x(); + item["pivotY"] = obj.state.pivot.y(); + item["pivotZ"] = obj.state.pivot.z(); + item["boundsMinX"] = obj.state.boundsMin.x(); + item["boundsMinY"] = obj.state.boundsMin.y(); + item["boundsMinZ"] = obj.state.boundsMin.z(); + item["boundsMaxX"] = obj.state.boundsMax.x(); + item["boundsMaxY"] = obj.state.boundsMax.y(); + item["boundsMaxZ"] = obj.state.boundsMax.z(); + if (obj.state.hasInteractionPosition) { + item["interactionPositionX"] = obj.state.interactionPosition.x(); + item["interactionPositionY"] = obj.state.interactionPosition.y(); + item["interactionPositionZ"] = obj.state.interactionPosition.z(); } j["objects"].push_back(item); } diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index ad823be..49f1928 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -141,7 +141,7 @@ namespace ZL { api.set_function("deactivate_interactive_object", [loc](const std::string& objectName) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name == objectName) { - intObj.isActive = false; + intObj.state.isActive = false; std::cout << "[script] deactivate_interactive_object: " << objectName << std::endl; return; } @@ -153,7 +153,7 @@ namespace ZL { api.set_function("activate_interactive_object", [loc](const std::string& objectName) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name == objectName) { - intObj.isActive = true; + intObj.state.isActive = true; std::cout << "[script] activate_interactive_object: " << objectName << std::endl; return; } @@ -164,7 +164,7 @@ namespace ZL { api.set_function("set_object_rotation", [loc](const std::string& objectName, float value) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name == objectName) { - intObj.rotationY = value * static_cast(M_PI) / 180.f; + intObj.state.rotationY = value * static_cast(M_PI) / 180.f; std::cout << "[script] set_object_rotation: " << objectName << " " << value << std::endl; return; } @@ -175,7 +175,7 @@ namespace ZL { api.set_function("set_object_alpha", [loc](const std::string& objectName, float value) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name == objectName) { - intObj.alpha = value; + intObj.state.alpha = value; std::cout << "[script] set_object_alpha: " <interactiveObjects) { if (intObj.loadedObject.name != name) continue; - if (intObj.isAnimating) { + if (intObj.state.isAnimating) { std::cerr << "[script] move_object: '" << name << "' is already animating\n"; return; } @@ -565,7 +565,7 @@ namespace ZL { float durationSec, sol::object onComplete) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name != name) continue; - if (intObj.isAnimating) { + if (intObj.state.isAnimating) { std::cerr << "[script] rotate_object: '" << name << "' is already animating\n"; return; } @@ -581,7 +581,7 @@ namespace ZL { }; } const float angleRad = angleDeg * static_cast(M_PI) / 180.f; - intObj.rotateTo(intObj.rotationY + angleRad, durationSec, std::move(cb)); + intObj.rotateTo(intObj.state.rotationY + angleRad, durationSec, std::move(cb)); return; } std::cerr << "[script] rotate_object: object '" << name << "' not found\n"; @@ -593,7 +593,7 @@ namespace ZL { float durationSec, sol::object onComplete) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name != name) continue; - if (intObj.isAnimating) { + if (intObj.state.isAnimating) { std::cerr << "[script] fade_object: '" << name << "' is already animating\n"; return; } @@ -620,7 +620,7 @@ namespace ZL { float durationSec, sol::object onComplete) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name != name) continue; - if (intObj.isAnimating) { + if (intObj.state.isAnimating) { std::cerr << "[script] scale_object: '" << name << "' is already animating\n"; return; } diff --git a/src/items/GameObjectLoader.cpp b/src/items/GameObjectLoader.cpp index 5983ffa..a42dd4b 100644 --- a/src/items/GameObjectLoader.cpp +++ b/src/items/GameObjectLoader.cpp @@ -118,10 +118,45 @@ namespace ZL { return objects; } + LoadedGameObjectState GameObjectData::toLoadedGameObjectState() const { + LoadedGameObjectState st; + st.texturePath = texturePath; + st.textureDarkandsPath = textureDarkandsPath; + st.meshPath = meshPath; + st.meshRotationX = rotationX; + st.meshRotationY = rotationY; + st.meshRotationZ = rotationZ; + st.meshScale = scale; + return st; + } + + InteractiveObjectState InteractiveObjectData::toInteractiveObjectState() const { + InteractiveObjectState st; + st.objectInfo = base.toLoadedGameObjectState(); + st.interactionRadius = interactionRadius; + st.approachRadius = approachRadius; + st.castShadow = castShadow; + st.castShadowNight = castShadowNight; + st.isActive = isActive; + st.activateFunctionName = activateFunctionName; + st.pivot = Eigen::Vector3f(pivotX, pivotY, pivotZ); + st.boundsMin = Eigen::Vector3f(boundsMinX, boundsMinY, boundsMinZ); + st.boundsMax = Eigen::Vector3f(boundsMaxX, boundsMaxY, boundsMaxZ); + st.hasInteractionPosition = hasInteractionPosition; + if (hasInteractionPosition) + st.interactionPosition = Eigen::Vector3f(interactionPositionX, interactionPositionY, interactionPositionZ); + st.jsonPositionX = base.positionX; + st.jsonPositionY = base.positionY; + st.jsonPositionZ = base.positionZ; + // state.position is set by InteractiveObject::createFromState() after mesh-center computation + return st; + } + LoadedGameObject GameObjectLoader::buildLoadedObject(const GameObjectData& data, Renderer& renderer, const std::string& zipPath) { LoadedGameObject obj; obj.name = data.name; + obj.creationInfo = data.toLoadedGameObjectState(); obj.texture = renderer.textureManager.LoadFromPng(data.texturePath, zipPath); if (!data.textureDarkandsPath.empty()) @@ -192,54 +227,9 @@ namespace ZL { for (const auto& data : loadInteractiveFromJson(jsonPath, zipPath)) { std::cout << "Loading interactive object: " << data.base.name << std::endl; try { - InteractiveObject intObj; - intObj.loadedObject = buildLoadedObject(data.base, renderer, zipPath); - intObj.interactionRadius = data.interactionRadius; - intObj.approachRadius = data.approachRadius; - intObj.castShadow = data.castShadow; - intObj.castShadowNight = data.castShadowNight; - intObj.isActive = data.isActive; - 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(); - - // Center mesh at origin; store corrected world position separately. - if (!intObj.loadedObject.mesh.data.PositionData.empty()) { - Eigen::Vector3f meshMin = intObj.loadedObject.mesh.data.PositionData[0]; - Eigen::Vector3f meshMax = intObj.loadedObject.mesh.data.PositionData[0]; - for (const auto& v : intObj.loadedObject.mesh.data.PositionData) { - meshMin = meshMin.cwiseMin(v); - meshMax = meshMax.cwiseMax(v); - } - Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f; - - intObj.loadedObject.mesh.data.Move(-meshCenter); - intObj.loadedObject.mesh.RefreshVBO(); - - intObj.position = Eigen::Vector3f(data.base.positionX, data.base.positionY, data.base.positionZ) + meshCenter; - } else { - intObj.position = Eigen::Vector3f(data.base.positionX, data.base.positionY, data.base.positionZ); - } - - interactiveObjects.push_back(std::move(intObj)); + InteractiveObject intObj = InteractiveObject::createFromState( + data.toInteractiveObjectState(), renderer, zipPath); + intObj.loadedObject.name = data.base.name; if (!data.activateFunctionName.empty()) std::cout << "Successfully loaded interactive: " << data.base.name @@ -247,6 +237,7 @@ namespace ZL { else std::cout << "Successfully loaded interactive: " << data.base.name << std::endl; + interactiveObjects.push_back(std::move(intObj)); } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load interactive '" << data.base.name << "': " << e.what() << std::endl; } diff --git a/src/items/GameObjectLoader.h b/src/items/GameObjectLoader.h index 73f6e4b..04cb282 100644 --- a/src/items/GameObjectLoader.h +++ b/src/items/GameObjectLoader.h @@ -7,6 +7,7 @@ #include "external/nlohmann/json.hpp" #include "TextModel.h" #include "InteractiveObject.h" +#include "InteractiveObjectState.h" #include "../CharacterState.h" namespace ZL { @@ -25,6 +26,8 @@ namespace ZL { float positionY = 0.0f; float positionZ = 0.0f; float scale = 1.0f; + + LoadedGameObjectState toLoadedGameObjectState() const; }; struct InteractiveObjectData { @@ -42,6 +45,8 @@ namespace ZL { bool castShadow = true; bool castShadowNight = true; bool isActive = true; + + InteractiveObjectState toInteractiveObjectState() const; }; struct NpcData { diff --git a/src/items/InteractiveObject.cpp b/src/items/InteractiveObject.cpp index 06e130d..f4a9a71 100644 --- a/src/items/InteractiveObject.cpp +++ b/src/items/InteractiveObject.cpp @@ -1,6 +1,7 @@ #include "InteractiveObject.h" #include "render/Renderer.h" #include "render/TextureManager.h" +#include "../TextModel.h" #include #include #include @@ -14,133 +15,188 @@ namespace ZL { using std::max; #endif + InteractiveObject InteractiveObject::createFromState(InteractiveObjectState st, + Renderer& renderer, + const std::string& zipPath) + { + InteractiveObject obj; + obj.state = std::move(st); + + const LoadedGameObjectState& info = obj.state.objectInfo; + + obj.loadedObject.texture = renderer.textureManager.LoadFromPng(info.texturePath, zipPath); + if (!info.textureDarkandsPath.empty()) + obj.loadedObject.textureDarklands = renderer.textureManager.LoadFromPng(info.textureDarkandsPath, zipPath); + + if (info.meshPath.size() > 4 && info.meshPath.compare(info.meshPath.size() - 4, 4, ".bin") == 0) + obj.loadedObject.mesh.data = LoadModelFromBinFile(info.meshPath, zipPath); + else + obj.loadedObject.mesh.data = LoadFromTextFile02(info.meshPath, zipPath); + + constexpr float kDeg2Rad = static_cast(M_PI) / 180.0f; + Eigen::Quaternionf rot = Eigen::Quaternionf::Identity(); + if (info.meshRotationX != 0.0f) + rot = Eigen::Quaternionf(Eigen::AngleAxisf(info.meshRotationX * kDeg2Rad, Eigen::Vector3f::UnitX())) * rot; + if (info.meshRotationY != 0.0f) + rot = Eigen::Quaternionf(Eigen::AngleAxisf(info.meshRotationY * kDeg2Rad, Eigen::Vector3f::UnitY())) * rot; + if (info.meshRotationZ != 0.0f) + rot = Eigen::Quaternionf(Eigen::AngleAxisf(info.meshRotationZ * kDeg2Rad, Eigen::Vector3f::UnitZ())) * rot; + obj.loadedObject.mesh.data.RotateByMatrix(rot.toRotationMatrix()); + + if (info.meshScale != 1.0f) + obj.loadedObject.mesh.data.Scale(info.meshScale); + + obj.loadedObject.mesh.RefreshVBO(); + + // Center mesh at origin; adjust world position by the centering offset. + if (!obj.loadedObject.mesh.data.PositionData.empty()) { + Eigen::Vector3f meshMin = obj.loadedObject.mesh.data.PositionData[0]; + Eigen::Vector3f meshMax = obj.loadedObject.mesh.data.PositionData[0]; + for (const auto& v : obj.loadedObject.mesh.data.PositionData) { + meshMin = meshMin.cwiseMin(v); + meshMax = meshMax.cwiseMax(v); + } + const Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f; + obj.loadedObject.mesh.data.Move(-meshCenter); + obj.loadedObject.mesh.RefreshVBO(); + obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX, + obj.state.jsonPositionY, + obj.state.jsonPositionZ) + meshCenter; + } else { + obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX, + obj.state.jsonPositionY, + obj.state.jsonPositionZ); + } + + return obj; + } + void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function onComplete) { - if (isAnimating) return; + if (state.isAnimating) return; AnimTask task; - task.type = AnimTask::Type::Move; - task.startPos = position; - task.startRotY = rotationY; - task.startScale = scale; - task.targetPos = target; - task.targetRotY = rotationY; - task.targetScale = scale; + task.type = AnimTask::Type::Move; + task.startPos = state.position; + task.startRotY = state.rotationY; + task.startScale = state.scale; + task.targetPos = target; + task.targetRotY = state.rotationY; + task.targetScale = state.scale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); - animTask = std::move(task); - isAnimating = true; + state.animTask = std::move(task); + state.isAnimating = true; } void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function onComplete) { - if (isAnimating) return; + if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Rotate; - task.startPos = position; - task.startRotY = rotationY; - task.startScale = scale; - task.targetPos = position; + task.startPos = state.position; + task.startRotY = state.rotationY; + task.startScale = state.scale; + task.targetPos = state.position; task.targetRotY = targetRotY; - task.targetScale = scale; + task.targetScale = state.scale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); - animTask = std::move(task); - isAnimating = true; + state.animTask = std::move(task); + state.isAnimating = true; } void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function onComplete) { - if (isAnimating) return; + if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Scale; - task.startPos = position; - task.startRotY = rotationY; - task.startScale = scale; - task.targetPos = position; - task.targetRotY = rotationY; + task.startPos = state.position; + task.startRotY = state.rotationY; + task.startScale = state.scale; + task.targetPos = state.position; + task.targetRotY = state.rotationY; task.targetScale = targetScale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); - animTask = std::move(task); - isAnimating = true; + state.animTask = std::move(task); + state.isAnimating = true; } void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function onComplete) { - if (isAnimating) return; + if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Fade; - task.startPos = position; - task.startRotY = rotationY; - task.startScale = alpha; // reuse startScale to hold start alpha - task.targetPos = position; - task.targetRotY = rotationY; - task.targetScale = targetAlpha; // reuse targetScale to hold target alpha + task.startPos = state.position; + task.startRotY = state.rotationY; + task.startScale = state.alpha; // reuse startScale to hold start alpha + task.targetPos = state.position; + task.targetRotY = state.rotationY; + task.targetScale = targetAlpha; // reuse targetScale to hold target alpha task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); - animTask = std::move(task); - isAnimating = true; + state.animTask = std::move(task); + state.isAnimating = true; } void InteractiveObject::update(int64_t deltaMs) { - if (!isAnimating || !animTask) return; + if (!state.isAnimating || !state.animTask) return; - AnimTask& task = *animTask; + AnimTask& task = *state.animTask; task.elapsedMs += static_cast(deltaMs); const float t = min(task.elapsedMs / task.durationMs, 1.0f); switch (task.type) { case AnimTask::Type::Move: - position = task.startPos + (task.targetPos - task.startPos) * t; + state.position = task.startPos + (task.targetPos - task.startPos) * t; break; case AnimTask::Type::Rotate: - rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t; + state.rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t; break; case AnimTask::Type::Scale: - scale = task.startScale + (task.targetScale - task.startScale) * t; + state.scale = task.startScale + (task.targetScale - task.startScale) * t; break; case AnimTask::Type::Fade: - alpha = task.startScale + (task.targetScale - task.startScale) * t; + state.alpha = task.startScale + (task.targetScale - task.startScale) * t; break; } if (t >= 1.0f) { - // Snap to exact target - position = task.targetPos; - rotationY = task.targetRotY; + state.position = task.targetPos; + state.rotationY = task.targetRotY; if (task.type == AnimTask::Type::Fade) - alpha = task.targetScale; // targetScale reused to hold targetAlpha for Fade + state.alpha = task.targetScale; else - scale = task.targetScale; + state.scale = task.targetScale; std::function cb = std::move(task.onComplete); - animTask.reset(); - isAnimating = false; + state.animTask.reset(); + state.isAnimating = false; if (cb) cb(); } } void InteractiveObject::drawDarklands(Renderer& renderer) const { - if (!isActive || !loadedObject.textureDarklands) return; + if (!state.isActive || !loadedObject.textureDarklands) return; - if (alpha < 0.999f) { + if (state.alpha < 0.999f) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } renderer.PushMatrix(); - renderer.TranslateMatrix(position); - if (rotationY != 0.f) { - renderer.TranslateMatrix(pivot); - renderer.RotateMatrix(Eigen::AngleAxisf(rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); - renderer.TranslateMatrix(-pivot); + renderer.TranslateMatrix(state.position); + if (state.rotationY != 0.f) { + renderer.TranslateMatrix(state.pivot); + renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); + renderer.TranslateMatrix(-state.pivot); } - if (scale != 1.f) - renderer.ScaleMatrix(scale); + if (state.scale != 1.f) + renderer.ScaleMatrix(state.scale); renderer.RenderUniform1i(textureUniformName, 0); - renderer.RenderUniform1f("uAlpha", alpha); + renderer.RenderUniform1f("uAlpha", state.alpha); glBindTexture(GL_TEXTURE_2D, loadedObject.textureDarklands->getTexID()); renderer.DrawVertexRenderStruct(loadedObject.mesh); renderer.PopMatrix(); @@ -149,30 +205,29 @@ namespace ZL { } void InteractiveObject::draw(Renderer& renderer) const { - if (!isActive || !loadedObject.texture) return; + if (!state.isActive || !loadedObject.texture) return; - if (alpha < 0.999f) { + if (state.alpha < 0.999f) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } renderer.PushMatrix(); - renderer.TranslateMatrix(position); - if (rotationY != 0.f) { - renderer.TranslateMatrix(pivot); - renderer.RotateMatrix(Eigen::AngleAxisf(rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); - renderer.TranslateMatrix(-pivot); + renderer.TranslateMatrix(state.position); + if (state.rotationY != 0.f) { + renderer.TranslateMatrix(state.pivot); + renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); + renderer.TranslateMatrix(-state.pivot); } - if (scale != 1.f) - renderer.ScaleMatrix(scale); + if (state.scale != 1.f) + renderer.ScaleMatrix(state.scale); renderer.RenderUniform1i(textureUniformName, 0); - renderer.RenderUniform1f("uAlpha", alpha); + renderer.RenderUniform1f("uAlpha", state.alpha); glBindTexture(GL_TEXTURE_2D, loadedObject.texture->getTexID()); renderer.DrawVertexRenderStruct(loadedObject.mesh); renderer.PopMatrix(); - // Restore uAlpha so subsequent fog-shader users (character weapons, etc.) are not affected. renderer.RenderUniform1f("uAlpha", 1.0f); } -} // namespace ZL \ No newline at end of file +} // namespace ZL diff --git a/src/items/InteractiveObject.h b/src/items/InteractiveObject.h index 1e5689b..b94a02c 100644 --- a/src/items/InteractiveObject.h +++ b/src/items/InteractiveObject.h @@ -6,62 +6,30 @@ #include #include #include "render/Renderer.h" +#include "InteractiveObjectState.h" namespace ZL { class Renderer; struct LoadedGameObject { + std::string name; // identity key — stays at top level + LoadedGameObjectState creationInfo; // paths + transforms for save/recreate std::shared_ptr texture; 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 { - LoadedGameObject loadedObject; // name, texture, mesh - Eigen::Vector3f position; - Eigen::Vector3f pivot = Eigen::Vector3f::Zero(); // local-space offset to rotation pivot - float rotationY = 0.0f; // Y-axis rotation in radians - 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; + LoadedGameObject loadedObject; // rendering assets + InteractiveObjectState state; // all mutable + creation state - bool hasInteractionPosition = false; - bool isActive = true; - bool isAnimating = false; // true while a timed animation is running - bool castShadow = true; // included in day shadow depth pass regardless of isActive - bool castShadowNight = true; // included in night shadow depth pass regardless of isActive - std::string activateFunctionName; - - struct AnimTask { - enum class Type { Move, Rotate, Scale, Fade } type; - Eigen::Vector3f startPos; - float startRotY; - float startScale; - Eigen::Vector3f targetPos; - float targetRotY; - float targetScale; - float durationMs; - float elapsedMs = 0.f; - std::function onComplete; - }; - - std::optional animTask; - - InteractiveObject() : interactionRadius(2.0f) {} + // Factory: creates a fully-loaded InteractiveObject from a state descriptor. + // Loads textures and mesh from state.objectInfo, computes mesh centering, + // and sets state.position = jsonPosition + meshCenter. + static InteractiveObject createFromState(InteractiveObjectState st, + Renderer& renderer, + const std::string& zipPath = ""); void moveTo(const Eigen::Vector3f& target, float durationSec, std::function onComplete = {}); void rotateTo(float targetRotY, float durationSec, std::function onComplete = {}); diff --git a/src/items/InteractiveObjectState.h b/src/items/InteractiveObjectState.h new file mode 100644 index 0000000..4e4c45a --- /dev/null +++ b/src/items/InteractiveObjectState.h @@ -0,0 +1,68 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace ZL { + +// Animation task for timed position/rotation/scale/alpha transitions. +// Moved here from InteractiveObject so InteractiveObjectState can own it. +struct AnimTask { + enum class Type { Move, Rotate, Scale, Fade } type; + Eigen::Vector3f startPos; + float startRotY = 0.f; + float startScale = 1.f; + Eigen::Vector3f targetPos; + float targetRotY = 0.f; + float targetScale = 1.f; + float durationMs = 1000.f; + float elapsedMs = 0.f; + std::function onComplete; // non-serializable +}; + +// Paths and baked mesh transforms needed to recreate a LoadedGameObject from disk. +struct LoadedGameObjectState { + std::string texturePath; + std::string textureDarkandsPath; + std::string meshPath; + float meshRotationX = 0.f; + float meshRotationY = 0.f; + float meshRotationZ = 0.f; + float meshScale = 1.f; +}; + +// All serializable state for an InteractiveObject: creation info + runtime mutable state. +// Mirrors CharacterState / CharacterCreationInfo but kept as a single flat class +// since interactive objects don't need the two-level separation. +class InteractiveObjectState { +public: + // --- Creation info (fixed at load time) --- + LoadedGameObjectState objectInfo; + float interactionRadius = 2.f; + float approachRadius = 2.f; + Eigen::Vector3f pivot = Eigen::Vector3f::Zero(); + Eigen::Vector3f boundsMin = Eigen::Vector3f::Zero(); + Eigen::Vector3f boundsMax = Eigen::Vector3f::Zero(); + bool hasInteractionPosition = false; + Eigen::Vector3f interactionPosition = Eigen::Vector3f::Zero(); + bool castShadow = true; + bool castShadowNight = true; + std::string activateFunctionName; + // Original JSON position before mesh-centering offset — needed for re-serialization. + float jsonPositionX = 0.f; + float jsonPositionY = 0.f; + float jsonPositionZ = 0.f; + + // --- Runtime mutable state --- + Eigen::Vector3f position = Eigen::Vector3f::Zero(); + float rotationY = 0.f; + float scale = 1.f; + float alpha = 1.f; + bool isActive = true; + bool isAnimating = false; + std::optional animTask; +}; + +} // namespace ZL