minor fixing, adapting for mobile web

This commit is contained in:
Vladislav Khorev 2026-04-25 14:39:27 +03:00
parent 44cc0fba67
commit eac914e073
6 changed files with 393 additions and 129 deletions

View File

@ -205,7 +205,23 @@ void Character::update(int64_t deltaMs) {
cb(); 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<float>(deltaMs) / 1000.f; float rotStep = rotationSpeed * static_cast<float>(deltaMs) / 1000.f;
if (std::fabs(angleDiff) <= rotStep) { if (std::fabs(angleDiff) <= rotStep) {
facingAngle = targetFacingAngle; 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 { } else {
facingAngle += (angleDiff > 0.f ? rotStep : -rotStep); facingAngle += (angleDiff > 0.f ? rotStep : -rotStep);
} }

View File

@ -112,6 +112,11 @@ public:
float attack_cooldown = 0.f; float attack_cooldown = 0.f;
bool canAttack = false; bool canAttack = false;
Character* attackTarget = nullptr; 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 isPlayer = false;
bool useGpuSkinning = true; bool useGpuSkinning = true;
//bool useGpuSkinning = false; //bool useGpuSkinning = false;

View File

@ -390,96 +390,51 @@ namespace ZL
} }
#endif #endif
#ifdef __ANDROID__ // Touch events (real fingers on Android and on mobile browsers via Emscripten).
if (event.type == SDL_FINGERDOWN) { // SDL_HINT_TOUCH_MOUSE_EVENTS="0" is set in main.cpp so these do not
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
else if (event.type == SDL_FINGERUP) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
else if (event.type == SDL_FINGERMOTION) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(static_cast<int64_t>(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
// also fire SDL_MOUSEBUTTONDOWN, preventing double-processing. // also fire SDL_MOUSEBUTTONDOWN, preventing double-processing.
#ifdef EMSCRIPTEN if (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERUP || event.type == SDL_FINGERMOTION) {
if (event.type == SDL_FINGERDOWN) { int eventX = static_cast<int>(event.tfinger.x * Environment::width);
int eventY = static_cast<int>(event.tfinger.y * Environment::height);
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(static_cast<int64_t>(event.tfinger.fingerId), mx, my); int64_t fingerId = static_cast<int64_t>(event.tfinger.fingerId);
}
else if (event.type == SDL_FINGERUP) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
else if (event.type == SDL_FINGERMOTION) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
#endif
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.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
if (event.button.button == SDL_BUTTON_LEFT) { if (event.button.button == SDL_BUTTON_LEFT) {
int mx = static_cast<int>((float)event.button.x / Environment::width * Environment::projectionWidth); int eventX = event.button.x;
int my = static_cast<int>((float)event.button.y / Environment::height * Environment::projectionHeight); int eventY = event.button.y;
int mx = static_cast<int>((float)eventX / Environment::width * Environment::projectionWidth);
if (event.type == SDL_MOUSEBUTTONDOWN) { int my = static_cast<int>((float)eventY / Environment::height * Environment::projectionHeight);
std::cout << "\n========== MOUSE DOWN EVENT ==========" << std::endl;
handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
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) { if (event.type == SDL_MOUSEBUTTONDOWN) {
rightMouseDown = true; onPointerDown(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my);
lastMouseX = event.button.x;
lastMouseY = event.button.y;
if (currentLocation)
{
currentLocation->rightMouseDown = true;
currentLocation->lastMouseX = event.button.x;
currentLocation->lastMouseY = event.button.y;
}
} }
else { else {
rightMouseDown = false; onPointerUp(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my);
if (currentLocation)
{
currentLocation->rightMouseDown = false;
}
} }
} }
} }
else if (event.type == SDL_MOUSEMOTION) { else if (event.type == SDL_MOUSEMOTION) {
int mx = static_cast<int>((float)event.motion.x / Environment::width * Environment::projectionWidth); int eventX = event.motion.x;
int my = static_cast<int>((float)event.motion.y / Environment::height * Environment::projectionHeight); int eventY = event.motion.y;
handleMotion(ZL::UiManager::MOUSE_FINGER_ID, mx, my); int mx = static_cast<int>((float)eventX / Environment::width * Environment::projectionWidth);
if (currentLocation) int my = static_cast<int>((float)eventY / Environment::height * Environment::projectionHeight);
{ onPointerMotion(ZL::UiManager::MOUSE_FINGER_ID, eventX, eventY, mx, my);
currentLocation->handleMotion(ZL::UiManager::MOUSE_FINGER_ID, event.motion.x, event.motion.y, mx, my);
}
} }
if (event.type == SDL_MOUSEWHEEL) { if (event.type == SDL_MOUSEWHEEL) {
@ -560,7 +515,6 @@ namespace ZL
if (event.type == SDL_KEYUP) { if (event.type == SDL_KEYUP) {
} }
#endif
} }
render(); 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 n = 0;
int uiY = Environment::projectionHeight - my; for (const auto& kv : activePointers) {
if (!kv.second.capturedByUi) ++n;
}
menuManager.uiManager.onTouchDown(fingerId, uiX, uiY); return n;
} }
void Game::handleUp(int64_t fingerId, int mx, int my) void Game::enterCameraDragMode(int eventX, int eventY)
{ {
int uiX = mx; cameraDragging = true;
int uiY = Environment::projectionHeight - my; 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. void Game::exitCameraDragMode()
// If this finger started on a UI element, don't notify space — {
// otherwise space would think the ship-control finger was released. cameraDragging = false;
bool wasUiInteraction = menuManager.uiManager.isUiInteractionForFinger(fingerId); 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<float>(a.eventX - b.eventX);
float dy = static_cast<float>(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<float>(itA->second.eventX - itB->second.eventX);
float dy = static_cast<float>(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); 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; const int uiX = mx;
int uiY = Environment::projectionHeight - my; const 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);
menuManager.uiManager.onTouchMove(fingerId, uiX, uiY); 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 } // namespace ZL

View File

@ -54,21 +54,49 @@ namespace ZL {
MenuManager menuManager; MenuManager menuManager;
private: private:
bool rightMouseDown = false; // Unified pointer handling: mouse-left and a single touch share one path.
int lastMouseX = 0; // A press becomes a tap (interact / walk-to) on release if it never crossed
int lastMouseY = 0; // 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<int64_t, PointerState> 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<AudioPlayerAsync> audioPlayer; std::unique_ptr<AudioPlayerAsync> audioPlayer;
int64_t getSyncTimeMs(); int64_t getSyncTimeMs();
void processTickCount(); void processTickCount();
void drawScene(); void drawScene();
void drawUI(); void drawUI();
void drawLoading(); void drawLoading();
void handleDown(int64_t fingerId, int mx, int my); void onPointerDown(int64_t fingerId, int eventX, int eventY, int mx, int my);
void handleUp(int64_t fingerId, int mx, int my); void onPointerUp(int64_t fingerId, int eventX, int eventY, int mx, int my);
void handleMotion(int64_t fingerId, 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 #ifdef EMSCRIPTEN
static Game* s_instance; static Game* s_instance;

View File

@ -19,6 +19,9 @@ namespace ZL
extern const char* CONST_ZIP_FILE; extern const char* CONST_ZIP_FILE;
static constexpr float CAMERA_FOV_Y = 1.0f / 1.5f; 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) Location::Location(Renderer& iRenderer, Inventory& iInventory)
: renderer(iRenderer) : renderer(iRenderer)
@ -293,24 +296,65 @@ namespace ZL
} }
Character* Location::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) { 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; Character* closestNpc = nullptr;
float closestDist = maxDistance; float closestDist = maxDistance;
std::cout << "[RAYCAST_NPC] Starting raycast with " << npcs.size() << " npcs" << std::endl;
for (auto& npc : npcs) { for (auto& npc : npcs) {
Eigen::Vector3f toNpc = npc->position - rayOrigin; const float dx = rayOrigin.x() - npc->position.x();
float distAlongRay = toNpc.dot(rayDir); const float dz = rayOrigin.z() - npc->position.z();
if (distAlongRay < 0.1f) continue; 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; const float b = 2.0f * (dx * rayDir.x() + dz * rayDir.z());
float distToNpc = (closestPoint - npc->position).norm(); 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) { float entryT = max(tNear, 0.1f);
closestDist = distAlongRay; 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(); 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; return closestNpc;
} }
@ -611,6 +655,26 @@ namespace ZL
targetInteractiveObject = nullptr; 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) 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; << player->position.y() << ", " << player->position.z() << ")" << std::endl;
targetInteractiveObject = clickedObject; targetInteractiveObject = clickedObject;
targetInteractNpc = nullptr;
targetInteractNpcIndex = -1;
player->setTarget(clickedObject->position); player->setTarget(clickedObject->position);
player->attackTarget = nullptr; player->attackTarget = nullptr;
std::cout << "[CLICK] Player moving to object..." << std::endl; std::cout << "[CLICK] Player moving to object..." << std::endl;
@ -671,22 +737,40 @@ namespace ZL
} }
} }
if (npcIndex != -1) { if (npcIndex != -1) {
if (distance <= clickedNpc->interactionRadius) { targetInteractiveObject = nullptr;
std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " ***" << std::endl;
scriptEngine.callNpcInteractCallback(npcIndex); 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 { else {
std::cout << "[CLICK] Too far from NPC (distance " << distance // Peaceful NPC: walk to them, fire on_npc_interact when within talk distance.
<< " > " << clickedNpc->interactionRadius << ")" << std::endl;
}
if (clickedNpc->canAttack)
{
player->attackTarget = clickedNpc;
}
else
{
player->attackTarget = nullptr; 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->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
player->attackTarget = nullptr; player->attackTarget = nullptr;
targetInteractNpc = nullptr;
targetInteractNpcIndex = -1;
} }
else { else {
std::cout << "[CLICK] No valid target found" << std::endl; std::cout << "[CLICK] No valid target found" << std::endl;
@ -719,7 +805,7 @@ namespace ZL
); );
} }
if (rightMouseDown) { if (cameraDragging) {
int dx = eventX - lastMouseX; int dx = eventX - lastMouseX;
int dy = eventY - lastMouseY; int dy = eventY - lastMouseY;
lastMouseX = eventX; lastMouseX = eventX;

View File

@ -40,6 +40,12 @@ namespace ZL
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity(); Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
InteractiveObject* targetInteractiveObject = nullptr; 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<TextRenderer> npcNameText; std::unique_ptr<TextRenderer> npcNameText;
ScriptEngine scriptEngine; ScriptEngine scriptEngine;
@ -50,7 +56,9 @@ namespace ZL
void buildDebugNavMeshes(); void buildDebugNavMeshes();
void drawDebugNavigation(); void drawDebugNavigation();
#endif #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 lastMouseX = 0;
int lastMouseY = 0; int lastMouseY = 0;