Working on game states

This commit is contained in:
Vladislav Khorev 2026-06-18 20:51:44 +03:00
parent 0ca00f965d
commit e0968b5522
10 changed files with 340 additions and 251 deletions

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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<float>(M_PI) / 180.f;
intObj.state.rotationY = value * static_cast<float>(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: " <<objectName << " " << value << std::endl;
return;
}
@ -538,7 +538,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] 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<float>(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;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -1,6 +1,7 @@
#include "InteractiveObject.h"
#include "render/Renderer.h"
#include "render/TextureManager.h"
#include "../TextModel.h"
#include <iostream>
#include <string>
#include <algorithm>
@ -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<float>(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<void()> 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<void()> 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<void()> 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<void()> 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<float>(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<void()> 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
} // namespace ZL

View File

@ -6,62 +6,30 @@
#include <cstdint>
#include <Eigen/Core>
#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> texture;
std::shared_ptr<Texture> 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<void()> onComplete;
};
std::optional<AnimTask> 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<void()> onComplete = {});
void rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete = {});

View File

@ -0,0 +1,68 @@
#pragma once
#include <string>
#include <optional>
#include <functional>
#include <Eigen/Core>
#include <Eigen/Geometry>
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<void()> 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> animTask;
};
} // namespace ZL