diff --git a/include/core/application.hpp b/include/core/application.hpp index 2f961b1e..e172a339 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -176,6 +176,7 @@ private: std::unordered_map facialHairGeosetMap_; std::unordered_map creatureInstances_; // guid → render instanceId std::unordered_map creatureModelIds_; // guid → loaded modelId + std::unordered_map creatureRenderPosCache_; // guid -> last synced render position std::unordered_set deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose std::unordered_map displayIdModelCache_; // displayId → modelId (model caching) uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index dd4021a0..dc34f36f 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -97,6 +97,7 @@ private: int pendingUiOpacity = 65; bool pendingMinimapRotate = false; bool pendingMinimapSquare = false; + bool pendingMinimapNpcDots = false; bool pendingSeparateBags = true; bool pendingAutoLoot = false; bool pendingUseOriginalSoundtrack = true; @@ -105,6 +106,7 @@ private: float uiOpacity_ = 0.65f; bool minimapRotate_ = false; bool minimapSquare_ = false; + bool minimapNpcDots_ = false; bool minimapSettingsApplied_ = false; bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers diff --git a/src/core/application.cpp b/src/core/application.cpp index 57c060a5..656edb9d 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -494,6 +494,7 @@ void Application::reloadExpansionData() { displayDataMap_.clear(); humanoidExtraMap_.clear(); creatureModelIds_.clear(); + creatureRenderPosCache_.clear(); buildCreatureDisplayLookups(); } @@ -1001,7 +1002,27 @@ void Application::update(float deltaTime) { } } - charRenderer->setInstancePosition(instanceId, renderPos); + auto posIt = creatureRenderPosCache_.find(guid); + if (posIt == creatureRenderPosCache_.end()) { + charRenderer->setInstancePosition(instanceId, renderPos); + creatureRenderPosCache_[guid] = renderPos; + } else { + const glm::vec3 prevPos = posIt->second; + const glm::vec2 delta2(renderPos.x - prevPos.x, renderPos.y - prevPos.y); + float planarDist = glm::length(delta2); + float dz = std::abs(renderPos.z - prevPos.z); + + const bool deadOrCorpse = unit->getHealth() == 0; + const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f); + if (deadOrCorpse || largeCorrection) { + charRenderer->setInstancePosition(instanceId, renderPos); + } else if (planarDist > 0.03f || dz > 0.08f) { + // Use movement interpolation so step/run animation can play. + float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f); + charRenderer->moveInstanceTo(instanceId, renderPos, duration); + } + posIt->second = renderPos; + } float renderYaw = entity->getOrientation() + glm::radians(90.0f); charRenderer->setInstanceRotation(instanceId, glm::vec3(0.0f, 0.0f, renderYaw)); } @@ -4135,6 +4156,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Track instance creatureInstances_[guid] = instanceId; creatureModelIds_[guid] = modelId; + creatureRenderPosCache_[guid] = renderPos; LOG_DEBUG("Spawned creature: guid=0x", std::hex, guid, std::dec, " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); } @@ -5307,6 +5329,7 @@ void Application::despawnOnlineCreature(uint64_t guid) { creatureInstances_.erase(it); creatureModelIds_.erase(guid); + creatureRenderPosCache_.erase(guid); LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec); } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e5581e1c..6d36ff7b 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -5755,6 +5755,7 @@ void GameScreen::renderSettingsWindow() { pendingUiOpacity = static_cast(std::lround(uiOpacity_ * 100.0f)); pendingMinimapRotate = minimapRotate_; pendingMinimapSquare = minimapSquare_; + pendingMinimapNpcDots = minimapNpcDots_; if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->setRotateWithCamera(minimapRotate_); @@ -6035,6 +6036,10 @@ void GameScreen::renderSettingsWindow() { } saveSettings(); } + if (ImGui::Checkbox("Show Nearby NPC Dots", &pendingMinimapNpcDots)) { + minimapNpcDots_ = pendingMinimapNpcDots; + saveSettings(); + } // Zoom controls ImGui::Text("Minimap Zoom:"); ImGui::SameLine(); @@ -6083,11 +6088,13 @@ void GameScreen::renderSettingsWindow() { pendingUiOpacity = 65; pendingMinimapRotate = false; pendingMinimapSquare = false; + pendingMinimapNpcDots = false; pendingSeparateBags = true; inventoryScreen.setSeparateBags(true); uiOpacity_ = 0.65f; minimapRotate_ = false; minimapSquare_ = false; + minimapNpcDots_ = false; if (renderer) { if (auto* cameraController = renderer->getCameraController()) { cameraController->setMouseSensitivity(pendingMouseSensitivity); @@ -6287,10 +6294,48 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { sinB = std::sin(bearing); } - if (statuses.empty()) return; - auto* drawList = ImGui::GetForegroundDrawList(); + auto projectToMinimap = [&](const glm::vec3& worldRenderPos, float& sx, float& sy) -> bool { + float dx = worldRenderPos.x - playerRender.x; + float dy = worldRenderPos.y - playerRender.y; + + // Match minimap shader transform exactly. + // Render axes: +X=west, +Y=north. Minimap screen axes: +X=right(east), +Y=down(south). + float rx = -dx * cosB + dy * sinB; + float ry = -dx * sinB - dy * cosB; + + // Scale to minimap pixels + float px = rx / viewRadius * mapRadius; + float py = ry / viewRadius * mapRadius; + + float distFromCenter = std::sqrt(px * px + py * py); + if (distFromCenter > mapRadius - 3.0f) { + return false; + } + + sx = centerX + px; + sy = centerY + py; + return true; + }; + + // Optional base nearby NPC dots (independent of quest status packets). + if (minimapNpcDots_) { + for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { + if (!entity || entity->getType() != game::ObjectType::UNIT) continue; + + auto unit = std::static_pointer_cast(entity); + if (!unit || unit->getHealth() == 0) continue; + + glm::vec3 npcRender = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(npcRender, sx, sy)) continue; + + ImU32 baseDot = unit->isHostile() ? IM_COL32(220, 70, 70, 220) : IM_COL32(245, 245, 245, 210); + drawList->AddCircleFilled(ImVec2(sx, sy), 1.0f, baseDot); + } + } + for (const auto& [guid, status] : statuses) { ImU32 dotColor; const char* marker = nullptr; @@ -6317,28 +6362,8 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); glm::vec3 npcRender = core::coords::canonicalToRender(canonical); - // Offset from player in render coords - float dx = npcRender.x - playerRender.x; - float dy = npcRender.y - playerRender.y; - - // Rotate by camera bearing (minimap north-up rotation) - float rx = dx * cosB - dy * sinB; - float ry = dx * sinB + dy * cosB; - - // Scale to minimap pixels - float px = rx / viewRadius * mapRadius; - float py = -ry / viewRadius * mapRadius; // screen Y is inverted - - // Clamp to circle - float distFromCenter = std::sqrt(px * px + py * py); - if (distFromCenter > mapRadius - 4.0f) { - float scale = (mapRadius - 4.0f) / distFromCenter; - px *= scale; - py *= scale; - } - - float sx = centerX + px; - float sy = centerY + py; + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(npcRender, sx, sy)) continue; // Draw dot with marker text drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, dotColor); @@ -6707,6 +6732,7 @@ void GameScreen::saveSettings() { out << "ui_opacity=" << pendingUiOpacity << "\n"; out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n"; out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n"; + out << "minimap_npc_dots=" << (pendingMinimapNpcDots ? 1 : 0) << "\n"; out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; // Audio @@ -6772,6 +6798,10 @@ void GameScreen::loadSettings() { int v = std::stoi(val); minimapSquare_ = (v != 0); pendingMinimapSquare = minimapSquare_; + } else if (key == "minimap_npc_dots") { + int v = std::stoi(val); + minimapNpcDots_ = (v != 0); + pendingMinimapNpcDots = minimapNpcDots_; } else if (key == "separate_bags") { pendingSeparateBags = (std::stoi(val) != 0); inventoryScreen.setSeparateBags(pendingSeparateBags);