Fix NPC movement animation sync and minimap marker behavior

- Use interpolated moveInstanceTo() for normal server NPC position deltas so walk/run animations play instead of visual sliding
- Keep hard snaps only for dead units and large corrections/teleports
- Add per-creature render position cache to drive interpolation safely across frames and clear it on spawn/despawn/reset
- Keep minimap controls visible regardless of quest-status availability
- Correct minimap NPC/quest marker projection mapping to match minimap shader transform
- Add optional nearby NPC minimap dots setting (default OFF), exposed in Gameplay > Interface and persisted in settings
This commit is contained in:
Kelsi 2026-02-20 16:40:22 -08:00
parent 504d5d2b15
commit ebaeea43cb
4 changed files with 81 additions and 25 deletions

View file

@ -176,6 +176,7 @@ private:
std::unordered_map<uint32_t, FacialHairGeosets> facialHairGeosetMap_;
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures

View file

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

View file

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

View file

@ -5755,6 +5755,7 @@ void GameScreen::renderSettingsWindow() {
pendingUiOpacity = static_cast<int>(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<game::Unit>(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);