From 38c9fdad6bd7d80bbfa41102378504fe74dd1d39 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 20:51:53 -0800 Subject: [PATCH] Improve targeting, minimap, and bridge collisions --- include/game/game_handler.hpp | 2 ++ include/rendering/m2_renderer.hpp | 1 + include/rendering/minimap.hpp | 3 ++ include/rendering/wmo_renderer.hpp | 1 + include/ui/game_screen.hpp | 2 ++ src/game/game_handler.cpp | 16 +++++++++ src/rendering/m2_renderer.cpp | 31 +++++++++++++---- src/rendering/minimap.cpp | 22 ++++++++++-- src/rendering/wmo_renderer.cpp | 32 ++++++++++++----- src/ui/game_screen.cpp | 56 ++++++++++++++++++++++++++---- 10 files changed, 142 insertions(+), 24 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 7aabc307..ea9e738c 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -529,6 +529,8 @@ public: void update(float deltaTime); private: + void autoTargetAttacker(uint64_t attackerGuid); + /** * Handle incoming packet from world server */ diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 2f3cadbe..817ade3b 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -52,6 +52,7 @@ struct M2ModelGPU { bool collisionSteppedFountain = false; bool collisionSteppedLowPlatform = false; bool collisionPlanter = false; + bool collisionBridge = false; bool collisionSmallSolidProp = false; bool collisionNarrowVerticalProp = false; bool collisionTreeTrunk = false; diff --git a/include/rendering/minimap.hpp b/include/rendering/minimap.hpp index c84822b3..afefed88 100644 --- a/include/rendering/minimap.hpp +++ b/include/rendering/minimap.hpp @@ -33,6 +33,8 @@ public: void toggle() { enabled = !enabled; } void setViewRadius(float radius) { viewRadius = radius; } + void setRotateWithCamera(bool rotate) { rotateWithCamera = rotate; } + bool isRotateWithCamera() const { return rotateWithCamera; } // Public accessors for WorldMap GLuint getOrLoadTileTexture(int tileX, int tileY); @@ -76,6 +78,7 @@ private: int mapSize = 200; float viewRadius = 400.0f; // world units visible in minimap radius bool enabled = true; + bool rotateWithCamera = true; // Throttling float updateIntervalSec = 0.25f; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index f0903f2f..faf8fe85 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -321,6 +321,7 @@ private: std::vector groups; glm::vec3 boundingBoxMin; glm::vec3 boundingBoxMax; + bool isLowPlatform = false; // Texture handles for this model (indexed by texture path order) std::vector textures; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index c4cfe421..9e53a719 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -70,9 +70,11 @@ private: float pendingMouseSensitivity = 0.2f; bool pendingInvertMouse = false; int pendingUiOpacity = 65; + bool pendingMinimapRotate = true; // UI element transparency (0.0 = fully transparent, 1.0 = fully opaque) float uiOpacity_ = 0.65f; + bool minimapRotate_ = true; /** * Render player info window diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index caefc5b5..b90d54b6 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3068,6 +3068,13 @@ void GameHandler::updateCombatText(float deltaTime) { combatText.end()); } +void GameHandler::autoTargetAttacker(uint64_t attackerGuid) { + if (attackerGuid == 0 || attackerGuid == playerGuid) return; + if (targetGuid != 0) return; + if (!entityManager.hasEntity(attackerGuid)) return; + setTarget(attackerGuid); +} + void GameHandler::handleAttackStart(network::Packet& packet) { AttackStartData data; if (!AttackStartParser::parse(packet, data)) return; @@ -3075,6 +3082,9 @@ void GameHandler::handleAttackStart(network::Packet& packet) { if (data.attackerGuid == playerGuid) { autoAttacking = true; autoAttackTarget = data.victimGuid; + } else if (data.victimGuid == playerGuid && data.attackerGuid != 0) { + hostileAttackers_.insert(data.attackerGuid); + autoTargetAttacker(data.attackerGuid); } } @@ -3221,6 +3231,7 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { if (isPlayerTarget && data.attackerGuid != 0) { hostileAttackers_.insert(data.attackerGuid); + autoTargetAttacker(data.attackerGuid); } if (data.isMiss()) { @@ -3241,6 +3252,11 @@ void GameHandler::handleSpellDamageLog(network::Packet& packet) { SpellDamageLogData data; if (!SpellDamageLogParser::parse(packet, data)) return; + if (data.targetGuid == playerGuid && data.attackerGuid != 0) { + hostileAttackers_.insert(data.attackerGuid); + autoTargetAttacker(data.attackerGuid); + } + bool isPlayerSource = (data.attackerGuid == playerGuid); auto type = data.isCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::SPELL_DAMAGE; addCombatText(type, static_cast(data.damage), data.spellId, isPlayerSource); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 759d712c..afdb592a 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -668,9 +668,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("stormwindplanter") != std::string::npos) || (lowerName.find("stormwindwindowplanter") != std::string::npos); bool lowPlatformShape = (horiz > 1.8f && vert > 0.2f && vert < 1.8f); + bool bridgeName = + (lowerName.find("bridge") != std::string::npos) || + (lowerName.find("plank") != std::string::npos) || + (lowerName.find("walkway") != std::string::npos); gpuModel.collisionSteppedLowPlatform = (!gpuModel.collisionSteppedFountain) && (knownStormwindPlanter || + bridgeName || (likelyCurbName && (lowPlatformShape || lowWideShape))); + gpuModel.collisionBridge = bridgeName; bool isPlanter = (lowerName.find("planter") != std::string::npos); gpuModel.collisionPlanter = isPlanter; @@ -702,6 +708,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("vine") != std::string::npos) || (lowerName.find("lily") != std::string::npos) || (lowerName.find("weed") != std::string::npos) || + (lowerName.find("wheat") != std::string::npos) || (lowerName.find("pumpkin") != std::string::npos) || (lowerName.find("firefly") != std::string::npos) || (lowerName.find("fireflies") != std::string::npos) || @@ -2329,18 +2336,18 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) continue; } - if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x || - glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y || - glZ < instance.worldBoundsMin.z - 2.0f || glZ > instance.worldBoundsMax.z + 2.0f) { - continue; - } - auto it = models.find(instance.modelId); if (it == models.end()) continue; if (instance.scale <= 0.001f) continue; const M2ModelGPU& model = it->second; if (model.collisionNoBlock) continue; + float zMargin = model.collisionBridge ? 25.0f : 2.0f; + if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x || + glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y || + glZ < instance.worldBoundsMin.z - zMargin || glZ > instance.worldBoundsMax.z + zMargin) { + continue; + } glm::vec3 localMin, localMax; getTightCollisionBounds(model, localMin, localMax); @@ -2351,6 +2358,9 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) float footprintPad = 0.0f; if (model.collisionSteppedLowPlatform) { footprintPad = model.collisionPlanter ? 0.22f : 0.16f; + if (model.collisionBridge) { + footprintPad = 0.35f; + } } if (localPos.x < localMin.x - footprintPad || localPos.x > localMax.x + footprintPad || localPos.y < localMin.y - footprintPad || localPos.y > localMax.y + footprintPad) { @@ -2372,6 +2382,9 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) maxStepUp = 2.5f; } else if (model.collisionSteppedLowPlatform) { maxStepUp = model.collisionPlanter ? 3.0f : 2.4f; + if (model.collisionBridge) { + maxStepUp = 25.0f; + } } if (worldTop.z > glZ + maxStepUp) continue; @@ -2455,6 +2468,9 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, maxStepUp = 2.5f; } else if (model.collisionSteppedLowPlatform) { maxStepUp = model.collisionPlanter ? 2.8f : 2.4f; + if (model.collisionBridge) { + maxStepUp = 25.0f; + } } bool stepableLowObject = (effectiveTop <= localFrom.z + maxStepUp); bool climbingAttempt = (localPos.z > localFrom.z + 0.18f); @@ -2464,6 +2480,9 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, // Let low curb/planter blocks be stepable without sticky side shoves. climbAllowance = 1.00f; } + if (model.collisionBridge) { + climbAllowance = 3.0f; + } if (model.collisionSmallSolidProp) { climbAllowance = 1.05f; } diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index 24d22a2e..77c134e7 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -141,6 +141,7 @@ bool Minimap::initialize(int size) { uniform sampler2D uComposite; uniform vec2 uPlayerUV; uniform float uRotation; + uniform float uArrowRotation; uniform float uZoomRadius; out vec4 FragColor; @@ -158,6 +159,12 @@ bool Minimap::initialize(int size) { return (u >= 0.0) && (v >= 0.0) && (u + v <= 1.0); } + vec2 rot2(vec2 v, float ang) { + float c = cos(ang); + float s = sin(ang); + return vec2(v.x * c - v.y * s, v.x * s + v.y * c); + } + void main() { vec2 centered = TexCoord - 0.5; float dist = length(centered); @@ -185,7 +192,7 @@ bool Minimap::initialize(int size) { } // Player arrow at center (always points up = forward) - vec2 ap = centered; + vec2 ap = rot2(centered, -uArrowRotation); vec2 tip = vec2(0.0, 0.035); vec2 lt = vec2(-0.018, -0.016); vec2 rt = vec2(0.018, -0.016); @@ -490,9 +497,18 @@ void Minimap::renderQuad(const Camera& playerCamera, const glm::vec3& centerWorl // renderX = wowY (west), renderY = wowX (north) // Facing north: fwd=(0,1,0) → bearing=0 // Facing east: fwd=(-1,0,0) → bearing=π/2 - glm::vec3 fwd = playerCamera.getForward(); - float rotation = std::atan2(-fwd.x, fwd.y); + float rotation = 0.0f; + if (rotateWithCamera) { + glm::vec3 fwd = playerCamera.getForward(); + rotation = std::atan2(-fwd.x, fwd.y); + } quadShader->setUniform("uRotation", rotation); + float arrowRotation = 0.0f; + if (!rotateWithCamera) { + glm::vec3 fwd = playerCamera.getForward(); + arrowRotation = std::atan2(-fwd.x, fwd.y); + } + quadShader->setUniform("uArrowRotation", arrowRotation); quadShader->setUniform("uComposite", 0); glActiveTexture(GL_TEXTURE0); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index c043965a..20c90500 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -272,6 +272,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { modelData.id = id; modelData.boundingBoxMin = model.boundingBoxMin; modelData.boundingBoxMax = model.boundingBoxMax; + { + glm::vec3 ext = model.boundingBoxMax - model.boundingBoxMin; + float horiz = std::max(ext.x, ext.y); + float vert = ext.z; + modelData.isLowPlatform = (vert < 6.0f && horiz > 20.0f); + } core::Logger::getInstance().info(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z, ") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")"); @@ -1637,6 +1643,7 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ QueryTimer timer(&queryTimeMs, &queryCallCount); std::optional bestFloor; + bool bestFromLowPlatform = false; // World-space ray: from high above, pointing straight down glm::vec3 worldOrigin(glX, glY, glZ + 500.0f); @@ -1653,17 +1660,19 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ continue; } - // Broad-phase reject in world space to avoid expensive matrix transforms. - if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x || - glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y || - glZ < instance.worldBoundsMin.z - 2.0f || glZ > instance.worldBoundsMax.z + 4.0f) { - continue; - } - auto it = loadedModels.find(instance.modelId); if (it == loadedModels.end()) continue; const ModelData& model = it->second; + float zMarginDown = model.isLowPlatform ? 20.0f : 2.0f; + float zMarginUp = model.isLowPlatform ? 20.0f : 4.0f; + + // Broad-phase reject in world space to avoid expensive matrix transforms. + if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x || + glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y || + glZ < instance.worldBoundsMin.z - zMarginDown || glZ > instance.worldBoundsMax.z + zMarginUp) { + continue; + } // World-space pre-pass: check which groups' world XY bounds contain // the query point. For a vertical ray this eliminates most groups @@ -1720,9 +1729,11 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ glm::vec3 hitLocal = localOrigin + localDir * t; glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f)); - if (hitWorld.z <= glZ + 0.5f) { + float allowAbove = model.isLowPlatform ? 12.0f : 0.5f; + if (hitWorld.z <= glZ + allowAbove) { if (!bestFloor || hitWorld.z > *bestFloor) { bestFloor = hitWorld.z; + bestFromLowPlatform = model.isLowPlatform; } } } @@ -1735,7 +1746,10 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ // Only update cache if we found a floor that's close to query height, // to avoid caching wrong floors when player is on different stories. if (bestFloor && *bestFloor >= glZ - 6.0f) { - precomputedFloorGrid[gridKey] = *bestFloor; + float cacheAbove = bestFromLowPlatform ? 12.0f : 2.0f; + if (*bestFloor <= glZ + cacheAbove) { + precomputedFloorGrid[gridKey] = *bestFloor; + } } return bestFloor; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index f326018d..6efcdd95 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -3591,6 +3591,12 @@ void GameScreen::renderSettingsWindow() { } } pendingUiOpacity = static_cast(uiOpacity_ * 100.0f + 0.5f); + pendingMinimapRotate = minimapRotate_; + if (renderer) { + if (auto* minimap = renderer->getMinimap()) { + minimap->setRotateWithCamera(minimapRotate_); + } + } settingsInit = true; } @@ -3659,8 +3665,10 @@ void GameScreen::renderSettingsWindow() { ImGui::Text("Interface"); ImGui::SliderInt("UI Opacity", &pendingUiOpacity, 20, 100, "%d%%"); + ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate); if (ImGui::Button("Restore Interface Defaults", ImVec2(-1, 0))) { pendingUiOpacity = 65; + pendingMinimapRotate = true; } ImGui::Spacing(); @@ -3669,12 +3677,16 @@ void GameScreen::renderSettingsWindow() { if (ImGui::Button("Apply", ImVec2(-1, 0))) { uiOpacity_ = static_cast(pendingUiOpacity) / 100.0f; + minimapRotate_ = pendingMinimapRotate; saveSettings(); window->setVsync(pendingVsync); window->setFullscreen(pendingFullscreen); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); if (renderer) { renderer->setShadowsEnabled(pendingShadows); + if (auto* minimap = renderer->getMinimap()) { + minimap->setRotateWithCamera(minimapRotate_); + } if (auto* music = renderer->getMusicManager()) { music->setVolume(pendingMusicVolume); } @@ -3782,8 +3794,6 @@ void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) { void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { const auto& statuses = gameHandler.getNpcQuestStatuses(); - if (statuses.empty()) return; - auto* renderer = core::Application::getInstance().getRenderer(); auto* camera = renderer ? renderer->getCamera() : nullptr; auto* minimap = renderer ? renderer->getMinimap() : nullptr; @@ -3805,10 +3815,37 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { glm::vec3 playerRender = core::coords::canonicalToRender(glm::vec3(mi.x, mi.y, mi.z)); // Camera bearing for minimap rotation - glm::vec3 fwd = camera->getForward(); - float bearing = std::atan2(-fwd.x, fwd.y); - float cosB = std::cos(bearing); - float sinB = std::sin(bearing); + float bearing = 0.0f; + float cosB = 1.0f; + float sinB = 0.0f; + if (minimapRotate_) { + glm::vec3 fwd = camera->getForward(); + bearing = std::atan2(-fwd.x, fwd.y); + cosB = std::cos(bearing); + sinB = std::sin(bearing); + } + + // Draw north indicator when rotating (points to world north on screen). + if (minimapRotate_) { + auto* drawList = ImGui::GetForegroundDrawList(); + ImU32 fill = IM_COL32(0, 0, 0, 230); + ImU32 outline = IM_COL32(0, 0, 0, 220); + float tipDist = mapRadius - 8.0f; + float baseDist = tipDist - 10.0f; + float nAng = -bearing; // map rotated by bearing; north rotates opposite + float cN = std::cos(nAng); + float sN = std::sin(nAng); + + auto rot = [&](float x, float y) -> ImVec2 { + return ImVec2(centerX + x * cN - y * sN, centerY + x * sN + y * cN); + }; + + ImVec2 textPos = rot(0.0f, -(baseDist + 9.0f)); + drawList->AddText(ImVec2(textPos.x - 4.0f, textPos.y), outline, "N"); + drawList->AddText(ImVec2(textPos.x - 4.0f, textPos.y), fill, "N"); + } + + if (statuses.empty()) return; auto* drawList = ImGui::GetForegroundDrawList(); @@ -3895,6 +3932,7 @@ void GameScreen::saveSettings() { } out << "ui_opacity=" << pendingUiOpacity << "\n"; + out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n"; LOG_INFO("Settings saved to ", path); } @@ -3918,6 +3956,12 @@ void GameScreen::loadSettings() { uiOpacity_ = static_cast(v) / 100.0f; } } catch (...) {} + } else if (key == "minimap_rotate") { + try { + int v = std::stoi(val); + minimapRotate_ = (v != 0); + pendingMinimapRotate = minimapRotate_; + } catch (...) {} } } LOG_INFO("Settings loaded from ", path);