From eac914e073fb2fa7281ce0a0d6aa3088e2169f4a Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 25 Apr 2026 14:39:27 +0300 Subject: [PATCH] minor fixing, adapting for mobile web --- src/Character.cpp | 22 +++- src/Character.h | 5 + src/Game.cpp | 313 +++++++++++++++++++++++++++++++--------------- src/Game.h | 42 +++++-- src/Location.cpp | 130 +++++++++++++++---- src/Location.h | 10 +- 6 files changed, 393 insertions(+), 129 deletions(-) diff --git a/src/Character.cpp b/src/Character.cpp index f057880..a2e4e05 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -205,7 +205,23 @@ void Character::update(int64_t deltaMs) { cb(); } - targetFacingAngle = facingAngle; + // While standing, optionally orient toward another character (e.g. the + // player when this NPC is being talked to). The smoothed rotation block + // below converges facingAngle to targetFacingAngle. + if (faceTarget) { + Eigen::Vector3f toFace = faceTarget->position - position; + toFace.y() = 0.f; + float d = toFace.norm(); + if (d > 1e-3f) { + targetFacingAngle = atan2(toFace.x() / d, -toFace.z() / d); + } + else { + targetFacingAngle = facingAngle; + } + } + else { + targetFacingAngle = facingAngle; + } } } @@ -267,6 +283,10 @@ void Character::update(int64_t deltaMs) { float rotStep = rotationSpeed * static_cast(deltaMs) / 1000.f; if (std::fabs(angleDiff) <= rotStep) { facingAngle = targetFacingAngle; + // One-shot face-target: once aligned, stop tracking. Otherwise the NPC + // would keep snapping to the player every tick after the conversation + // started, even as the player moved away. + faceTarget = nullptr; } else { facingAngle += (angleDiff > 0.f ? rotStep : -rotStep); } diff --git a/src/Character.h b/src/Character.h index 8001006..ee2acc8 100644 --- a/src/Character.h +++ b/src/Character.h @@ -112,6 +112,11 @@ public: float attack_cooldown = 0.f; bool canAttack = false; Character* attackTarget = nullptr; + // While standing still, smoothly rotate once to face this character. + // Auto-cleared by update() the moment this character finishes rotating, + // so it acts as a one-shot orient (e.g. an NPC turning to the player at + // the start of a conversation, then holding that pose). + Character* faceTarget = nullptr; bool isPlayer = false; bool useGpuSkinning = true; //bool useGpuSkinning = false; diff --git a/src/Game.cpp b/src/Game.cpp index cf6e4ad..1ae0f23 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -390,96 +390,51 @@ namespace ZL } #endif -#ifdef __ANDROID__ - if (event.type == SDL_FINGERDOWN) { - int mx = static_cast(event.tfinger.x * Environment::projectionWidth); - int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleDown(static_cast(event.tfinger.fingerId), mx, my); - } - else if (event.type == SDL_FINGERUP) { - int mx = static_cast(event.tfinger.x * Environment::projectionWidth); - int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleUp(static_cast(event.tfinger.fingerId), mx, my); - } - else if (event.type == SDL_FINGERMOTION) { - int mx = static_cast(event.tfinger.x * Environment::projectionWidth); - int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleMotion(static_cast(event.tfinger.fingerId), mx, my); - } -#else - // Emscripten on mobile browser: handle real touch events with per-finger IDs. - // SDL_HINT_TOUCH_MOUSE_EVENTS="0" is set in main.cpp so these don't + // Touch events (real fingers on Android and on mobile browsers via Emscripten). + // SDL_HINT_TOUCH_MOUSE_EVENTS="0" is set in main.cpp so these do not // also fire SDL_MOUSEBUTTONDOWN, preventing double-processing. -#ifdef EMSCRIPTEN - if (event.type == SDL_FINGERDOWN) { + if (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERUP || event.type == SDL_FINGERMOTION) { + int eventX = static_cast(event.tfinger.x * Environment::width); + int eventY = static_cast(event.tfinger.y * Environment::height); int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleDown(static_cast(event.tfinger.fingerId), mx, my); - } - else if (event.type == SDL_FINGERUP) { - int mx = static_cast(event.tfinger.x * Environment::projectionWidth); - int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleUp(static_cast(event.tfinger.fingerId), mx, my); - } - else if (event.type == SDL_FINGERMOTION) { - int mx = static_cast(event.tfinger.x * Environment::projectionWidth); - int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleMotion(static_cast(event.tfinger.fingerId), mx, my); - } -#endif + int64_t fingerId = static_cast(event.tfinger.fingerId); + if (event.type == SDL_FINGERDOWN) { + onPointerDown(fingerId, eventX, eventY, mx, my); + } + else if (event.type == SDL_FINGERUP) { + onPointerUp(fingerId, eventX, eventY, mx, my); + } + else { + onPointerMotion(fingerId, eventX, eventY, mx, my); + } + continue; + } + + // Mouse-left maps to a single virtual touch with MOUSE_FINGER_ID. + // Right mouse is intentionally unused now — camera rotation is on hold-and-drag. if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { if (event.button.button == SDL_BUTTON_LEFT) { - int mx = static_cast((float)event.button.x / Environment::width * Environment::projectionWidth); - int my = static_cast((float)event.button.y / Environment::height * Environment::projectionHeight); - - if (event.type == SDL_MOUSEBUTTONDOWN) { - std::cout << "\n========== MOUSE DOWN EVENT ==========" << std::endl; - handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my); + int eventX = event.button.x; + int eventY = event.button.y; + int mx = static_cast((float)eventX / Environment::width * Environment::projectionWidth); + int my = static_cast((float)eventY / Environment::height * Environment::projectionHeight); - if (menuManager.uiManager.isUiInteractionForFinger(ZL::UiManager::MOUSE_FINGER_ID)) { - std::cout << "[CLICK] UI handled, skipping character movement" << std::endl; - continue; - } - - if (currentLocation) - { - currentLocation->handleDown(ZL::UiManager::MOUSE_FINGER_ID, event.button.x, event.button.y, mx, my); - } - ///..... - } else { - handleUp(ZL::UiManager::MOUSE_FINGER_ID, mx, my); - } - } - else if (event.button.button == SDL_BUTTON_RIGHT) { if (event.type == SDL_MOUSEBUTTONDOWN) { - rightMouseDown = true; - lastMouseX = event.button.x; - lastMouseY = event.button.y; - if (currentLocation) - { - currentLocation->rightMouseDown = true; - currentLocation->lastMouseX = event.button.x; - currentLocation->lastMouseY = event.button.y; - } + onPointerDown(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my); } else { - rightMouseDown = false; - if (currentLocation) - { - currentLocation->rightMouseDown = false; - } + onPointerUp(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my); } } } else if (event.type == SDL_MOUSEMOTION) { - int mx = static_cast((float)event.motion.x / Environment::width * Environment::projectionWidth); - int my = static_cast((float)event.motion.y / Environment::height * Environment::projectionHeight); - handleMotion(ZL::UiManager::MOUSE_FINGER_ID, mx, my); - if (currentLocation) - { - currentLocation->handleMotion(ZL::UiManager::MOUSE_FINGER_ID, event.motion.x, event.motion.y, mx, my); - } + int eventX = event.motion.x; + int eventY = event.motion.y; + int mx = static_cast((float)eventX / Environment::width * Environment::projectionWidth); + int my = static_cast((float)eventY / Environment::height * Environment::projectionHeight); + onPointerMotion(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my); } if (event.type == SDL_MOUSEWHEEL) { @@ -560,7 +515,6 @@ namespace ZL if (event.type == SDL_KEYUP) { } -#endif } render(); @@ -568,39 +522,202 @@ namespace ZL } - void Game::handleDown(int64_t fingerId, int mx, int my) + int Game::countNonUiPointers() const { - int uiX = mx; - int uiY = Environment::projectionHeight - my; - - - menuManager.uiManager.onTouchDown(fingerId, uiX, uiY); - + int n = 0; + for (const auto& kv : activePointers) { + if (!kv.second.capturedByUi) ++n; + } + return n; } - void Game::handleUp(int64_t fingerId, int mx, int my) + void Game::enterCameraDragMode(int eventX, int eventY) { - int uiX = mx; - int uiY = Environment::projectionHeight - my; + cameraDragging = true; + if (currentLocation) { + currentLocation->cameraDragging = true; + // Anchor on the *current* position, not the original press, so the + // camera doesn't snap by however far the finger drifted before the + // movement threshold was crossed. + currentLocation->lastMouseX = eventX; + currentLocation->lastMouseY = eventY; + } + } - // Check BEFORE onTouchUp erases the finger from the map. - // If this finger started on a UI element, don't notify space — - // otherwise space would think the ship-control finger was released. - bool wasUiInteraction = menuManager.uiManager.isUiInteractionForFinger(fingerId); + void Game::exitCameraDragMode() + { + cameraDragging = false; + if (currentLocation) { + currentLocation->cameraDragging = false; + } + } + + void Game::startPinch() + { + // Pick the first two non-UI pointers as pinch anchors. + bool foundA = false, foundB = false; + for (auto& kv : activePointers) { + if (kv.second.capturedByUi) continue; + if (!foundA) { + pinchFingerA = kv.first; + foundA = true; + } + else if (!foundB) { + pinchFingerB = kv.first; + foundB = true; + break; + } + } + if (!foundA || !foundB) return; + + const PointerState& a = activePointers[pinchFingerA]; + const PointerState& b = activePointers[pinchFingerB]; + float dx = static_cast(a.eventX - b.eventX); + float dy = static_cast(a.eventY - b.eventY); + pinchStartDistance = std::sqrt(dx * dx + dy * dy); + pinchStartZoom = Environment::zoom; + pinchActive = true; + } + + void Game::updatePinchZoom() + { + auto itA = activePointers.find(pinchFingerA); + auto itB = activePointers.find(pinchFingerB); + if (itA == activePointers.end() || itB == activePointers.end()) return; + if (pinchStartDistance <= 1.0f) return; + + float dx = static_cast(itA->second.eventX - itB->second.eventX); + float dy = static_cast(itA->second.eventY - itB->second.eventY); + float dist = std::sqrt(dx * dx + dy * dy); + if (dist <= 1.0f) return; + + float newZoom = pinchStartZoom * (pinchStartDistance / dist); + // Match the wheel-zoom lower bound so both gestures clamp the same way. + static const float zoomMin = 2.0f; + if (newZoom < zoomMin) newZoom = zoomMin; + Environment::zoom = newZoom; + } + + void Game::endPinch() + { + pinchActive = false; + pinchStartDistance = 0.0f; + pinchStartZoom = 0.0f; + } + + void Game::onPointerDown(int64_t fingerId, int eventX, int eventY, int mx, int my) + { + PointerState st; + st.eventX = st.downEventX = eventX; + st.eventY = st.downEventY = eventY; + st.mx = st.downMx = mx; + st.my = st.downMy = my; + + const int uiX = mx; + const int uiY = Environment::projectionHeight - my; + menuManager.uiManager.onTouchDown(fingerId, uiX, uiY); + st.capturedByUi = menuManager.uiManager.isUiInteractionForFinger(fingerId); + + activePointers[fingerId] = st; + + if (st.capturedByUi) { + // UI button / slider / text-field grabbed this press; don't drive + // gameplay or pinch from it. + return; + } + + if (countNonUiPointers() >= 2) { + // Second finger landed on the gameplay area: switch to pinch-zoom + // and abandon any in-flight tap/camera-drag. + if (cameraDragging) { + exitCameraDragMode(); + } + hasPrimaryPointer = false; + if (!pinchActive) { + startPinch(); + } + } + else { + hasPrimaryPointer = true; + primaryPointerId = fingerId; + } + } + + void Game::onPointerUp(int64_t fingerId, int eventX, int eventY, int mx, int my) + { + const int uiX = mx; + const int uiY = Environment::projectionHeight - my; menuManager.uiManager.onTouchUp(fingerId, uiX, uiY); + auto it = activePointers.find(fingerId); + if (it == activePointers.end()) return; + PointerState st = it->second; + activePointers.erase(it); + + // Pinch ends as soon as either anchor is lifted. The remaining finger, + // if any, is *not* promoted back to a primary tap — the user was mid- + // gesture, not starting a fresh press. + if (pinchActive && (fingerId == pinchFingerA || fingerId == pinchFingerB)) { + endPinch(); + return; + } + + if (!hasPrimaryPointer || fingerId != primaryPointerId) { + return; + } + + const bool wasDragging = cameraDragging; + if (cameraDragging) { + exitCameraDragMode(); + } + hasPrimaryPointer = false; + + if (st.capturedByUi || wasDragging) { + // UI handled the press, or it was a camera-rotation drag — no tap action. + return; + } + + // Tap → walk-to / interact, using the original press coords so the target + // isn't shifted by tiny finger drift before release. + if (currentLocation) { + currentLocation->handleDown(fingerId, st.downEventX, st.downEventY, st.downMx, st.downMy); + } } - void Game::handleMotion(int64_t fingerId, int mx, int my) + void Game::onPointerMotion(int64_t fingerId, int eventX, int eventY, int mx, int my) { - int uiX = mx; - int uiY = Environment::projectionHeight - my; - - // Check before onTouchMove so the "started on UI" state is preserved - // regardless of what onTouchMove does internally. - bool wasUiInteraction = menuManager.uiManager.isUiInteractionForFinger(fingerId); + const int uiX = mx; + const int uiY = Environment::projectionHeight - my; menuManager.uiManager.onTouchMove(fingerId, uiX, uiY); + auto it = activePointers.find(fingerId); + if (it != activePointers.end()) { + it->second.eventX = eventX; + it->second.eventY = eventY; + it->second.mx = mx; + it->second.my = my; + } + + if (pinchActive) { + updatePinchZoom(); + return; + } + + // Only the primary, non-UI-captured pointer can promote itself into a + // camera-rotation drag once it crosses the movement threshold. + if (hasPrimaryPointer && fingerId == primaryPointerId && it != activePointers.end() + && !it->second.capturedByUi && !cameraDragging) { + int dx = mx - it->second.downMx; + int dy = my - it->second.downMy; + if (dx * dx + dy * dy >= CAMERA_DRAG_PIXEL_THRESHOLD * CAMERA_DRAG_PIXEL_THRESHOLD) { + enterCameraDragMode(eventX, eventY); + } + } + + if (currentLocation) { + // Forwarded for dialogue hover and (when cameraDragging) camera rotation. + currentLocation->handleMotion(fingerId, eventX, eventY, mx, my); + } } } // namespace ZL diff --git a/src/Game.h b/src/Game.h index 7beab7b..cb53c52 100644 --- a/src/Game.h +++ b/src/Game.h @@ -54,21 +54,49 @@ namespace ZL { MenuManager menuManager; private: - bool rightMouseDown = false; - int lastMouseX = 0; - int lastMouseY = 0; + // Unified pointer handling: mouse-left and a single touch share one path. + // A press becomes a tap (interact / walk-to) on release if it never crossed + // CAMERA_DRAG_PIXEL_THRESHOLD; otherwise it becomes a camera-rotation drag. + // Two simultaneous touches enter pinch-zoom instead. + static constexpr int CAMERA_DRAG_PIXEL_THRESHOLD = 12; + + struct PointerState { + int eventX = 0, eventY = 0; // current raw window-pixel coords + int mx = 0, my = 0; // current projection-space coords + int downEventX = 0, downEventY = 0;// where the press started (raw px) + int downMx = 0, downMy = 0; // where the press started (proj) + bool capturedByUi = false; + }; + std::unordered_map activePointers; + bool hasPrimaryPointer = false; + int64_t primaryPointerId = 0; + bool cameraDragging = false; + + bool pinchActive = false; + int64_t pinchFingerA = 0; + int64_t pinchFingerB = 0; + float pinchStartDistance = 0.0f; + float pinchStartZoom = 0.0f; + std::unique_ptr audioPlayer; int64_t getSyncTimeMs(); void processTickCount(); void drawScene(); void drawUI(); - + void drawLoading(); - void handleDown(int64_t fingerId, int mx, int my); - void handleUp(int64_t fingerId, int mx, int my); - void handleMotion(int64_t fingerId, int mx, int my); + void onPointerDown(int64_t fingerId, int eventX, int eventY, int mx, int my); + void onPointerUp(int64_t fingerId, int eventX, int eventY, int mx, int my); + void onPointerMotion(int64_t fingerId, int eventX, int eventY, int mx, int my); + + void enterCameraDragMode(int eventX, int eventY); + void exitCameraDragMode(); + void startPinch(); + void updatePinchZoom(); + void endPinch(); + int countNonUiPointers() const; #ifdef EMSCRIPTEN static Game* s_instance; diff --git a/src/Location.cpp b/src/Location.cpp index c49581e..980fab7 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -19,6 +19,9 @@ namespace ZL extern const char* CONST_ZIP_FILE; static constexpr float CAMERA_FOV_Y = 1.0f / 1.5f; + // How close the player needs to be to a peaceful NPC before the + // on_npc_interact callback fires and the conversation begins. + static constexpr float NPC_TALK_DISTANCE = 1.25f; Location::Location(Renderer& iRenderer, Inventory& iInventory) : renderer(iRenderer) @@ -293,24 +296,65 @@ namespace ZL } Character* Location::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) { + // Every NPC is treated as a vertical cylinder: radius 1.0m, height 1.85m, + // base at npc->position (the model's foot). Intersection = circle hit in the + // XZ plane, then clip the entry/exit t against the [yFoot, yFoot+height] slab. + static constexpr float NPC_CLICK_RADIUS = 1.0f; + static constexpr float NPC_CLICK_HEIGHT = 1.85f; + Character* closestNpc = nullptr; float closestDist = maxDistance; + std::cout << "[RAYCAST_NPC] Starting raycast with " << npcs.size() << " npcs" << std::endl; + for (auto& npc : npcs) { - Eigen::Vector3f toNpc = npc->position - rayOrigin; - float distAlongRay = toNpc.dot(rayDir); - if (distAlongRay < 0.1f) continue; + const float dx = rayOrigin.x() - npc->position.x(); + const float dz = rayOrigin.z() - npc->position.z(); + const float a = rayDir.x() * rayDir.x() + rayDir.z() * rayDir.z(); + if (a < 1e-6f) continue; // purely-vertical ray; not a real click case - Eigen::Vector3f closestPoint = rayOrigin + rayDir * distAlongRay; - float distToNpc = (closestPoint - npc->position).norm(); + const float b = 2.0f * (dx * rayDir.x() + dz * rayDir.z()); + const float c = dx * dx + dz * dz - NPC_CLICK_RADIUS * NPC_CLICK_RADIUS; + const float disc = b * b - 4.0f * a * c; + if (disc < 0.0f) continue; // ray misses the infinite cylinder - float radius = npc->modelScale * 50.0f; + const float sqrtDisc = std::sqrt(disc); + const float tNear = (-b - sqrtDisc) / (2.0f * a); + const float tFar = (-b + sqrtDisc) / (2.0f * a); - if (distToNpc <= radius && distAlongRay < closestDist) { - closestDist = distAlongRay; + float entryT = max(tNear, 0.1f); + float exitT = tFar; + + // Clip against the cylinder's vertical extent. + const float yMin = npc->position.y(); + const float yMax = yMin + NPC_CLICK_HEIGHT; + if (std::abs(rayDir.y()) > 1e-6f) { + const float tYa = (yMin - rayOrigin.y()) / rayDir.y(); + const float tYb = (yMax - rayOrigin.y()) / rayDir.y(); + entryT = max(entryT, min(tYa, tYb)); + exitT = min(exitT, max(tYa, tYb)); + } + else if (rayOrigin.y() < yMin || rayOrigin.y() > yMax) { + continue; // horizontal ray passing above or below the cylinder + } + + if (entryT > exitT) continue; + + std::cout << "[RAYCAST_NPC] " << npc->npcId << " hit at t=" << entryT << std::endl; + + if (entryT < closestDist) { + closestDist = entryT; closestNpc = npc.get(); } } + + if (closestNpc) { + std::cout << "[RAYCAST_NPC] HIT: " << closestNpc->npcId << std::endl; + } + else { + std::cout << "[RAYCAST_NPC] No NPC hit" << std::endl; + } + return closestNpc; } @@ -611,6 +655,26 @@ namespace ZL targetInteractiveObject = nullptr; } } + + // Check if player reached target NPC for interaction. + if (targetInteractNpc && targetInteractNpcIndex >= 0 && player) { + float distToNpc = (player->position - targetInteractNpc->position).norm(); + if (distToNpc <= NPC_TALK_DISTANCE) { + std::cout << "[NPC] Player reached NPC index " << targetInteractNpcIndex + << " (distance " << distToNpc << "); firing on_npc_interact" << std::endl; + // Stop the player at the talk distance and have the NPC turn to face them. + player->setTarget(player->position); + targetInteractNpc->faceTarget = player.get(); + try { + scriptEngine.callNpcInteractCallback(targetInteractNpcIndex); + } + catch (const std::exception& e) { + std::cerr << "[NPC] callback error: " << e.what() << std::endl; + } + targetInteractNpc = nullptr; + targetInteractNpcIndex = -1; + } + } } void Location::handleDown(int64_t fingerId, int eventX, int eventY, int mx, int my) @@ -653,6 +717,8 @@ namespace ZL << player->position.y() << ", " << player->position.z() << ")" << std::endl; targetInteractiveObject = clickedObject; + targetInteractNpc = nullptr; + targetInteractNpcIndex = -1; player->setTarget(clickedObject->position); player->attackTarget = nullptr; std::cout << "[CLICK] Player moving to object..." << std::endl; @@ -671,22 +737,40 @@ namespace ZL } } if (npcIndex != -1) { - if (distance <= clickedNpc->interactionRadius) { - std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " ***" << std::endl; - scriptEngine.callNpcInteractCallback(npcIndex); + targetInteractiveObject = nullptr; + + if (clickedNpc->canAttack) { + // Hostile NPC: combat logic walks the player in via attackTarget; + // don't queue a Lua interaction during a fight. + player->attackTarget = clickedNpc; + targetInteractNpc = nullptr; + targetInteractNpcIndex = -1; + + if (distance <= clickedNpc->interactionRadius) { + std::cout << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl; + scriptEngine.callNpcInteractCallback(npcIndex); + } } else { - std::cout << "[CLICK] Too far from NPC (distance " << distance - << " > " << clickedNpc->interactionRadius << ")" << std::endl; - } - - if (clickedNpc->canAttack) - { - player->attackTarget = clickedNpc; - } - else - { + // Peaceful NPC: walk to them, fire on_npc_interact when within talk distance. player->attackTarget = nullptr; + + if (distance <= NPC_TALK_DISTANCE) { + // Already in talk range — fire immediately, stop, and face the player. + std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " (in range) ***" << std::endl; + player->setTarget(player->position); + clickedNpc->faceTarget = player.get(); + scriptEngine.callNpcInteractCallback(npcIndex); + targetInteractNpc = nullptr; + targetInteractNpcIndex = -1; + } + else { + std::cout << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance + << " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl; + player->setTarget(clickedNpc->position); + targetInteractNpc = clickedNpc; + targetInteractNpcIndex = npcIndex; + } } } @@ -699,6 +783,8 @@ namespace ZL player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z())); player->attackTarget = nullptr; + targetInteractNpc = nullptr; + targetInteractNpcIndex = -1; } else { std::cout << "[CLICK] No valid target found" << std::endl; @@ -719,7 +805,7 @@ namespace ZL ); } - if (rightMouseDown) { + if (cameraDragging) { int dx = eventX - lastMouseX; int dy = eventY - lastMouseY; lastMouseX = eventX; diff --git a/src/Location.h b/src/Location.h index bb8726e..d3e2d36 100644 --- a/src/Location.h +++ b/src/Location.h @@ -40,6 +40,12 @@ namespace ZL Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity(); InteractiveObject* targetInteractiveObject = nullptr; + // "Walk to NPC, then fire on_npc_interact" — mirrors targetInteractiveObject + // so a click from outside interactionRadius still leads to a Lua callback + // once the player gets close enough. + Character* targetInteractNpc = nullptr; + int targetInteractNpcIndex = -1; + std::unique_ptr npcNameText; ScriptEngine scriptEngine; @@ -50,7 +56,9 @@ namespace ZL void buildDebugNavMeshes(); void drawDebugNavigation(); #endif - bool rightMouseDown = false; + // 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; int lastMouseX = 0; int lastMouseY = 0;