diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index 8b431532..99349c47 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -127,7 +127,10 @@ inline const char* getSpellCastResultString(uint8_t result, int powerType = -1) case 24: return "Charmed"; case 25: return "Chest in use"; case 26: return "Confused"; - case 27: return nullptr; // DONT_REPORT — suppress message + // DONT_REPORT carries no localized reason of its own. Returning null + // sent it through the unknown-code fallback and exposed "error 27" to + // players, which is less useful than a safe generic explanation. + case 27: return "You can't do that right now"; case 28: return "Equipped item required"; case 29: return "Equipped item class (mainhand)"; case 30: return "Equipped item class (mainhand)"; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index aefbb399..d11622ef 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -190,6 +190,13 @@ private: pipeline::M2Model data; // Original model data std::vector bindPose; // Inverse bind pose matrices + // Tight bind-pose bounds from rendered vertices. M2 header + // boundingBox/boundingRadius describe collision geometry on many + // creatures and can cover little more than their feet. + glm::vec3 visualBoundMin{0.0f}; + glm::vec3 visualBoundMax{0.0f}; + float visualBoundRadius = 0.0f; + // Textures loaded from BLP (indexed by texture array position) std::vector textureIds; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index d4e973d7..60f747e7 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1636,6 +1636,16 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) { M2ModelGPU gpuModel; gpuModel.data = model; + if (!model.vertices.empty()) { + gpuModel.visualBoundMin = glm::vec3(std::numeric_limits::max()); + gpuModel.visualBoundMax = glm::vec3(-std::numeric_limits::max()); + for (const auto& vertex : model.vertices) { + gpuModel.visualBoundMin = glm::min(gpuModel.visualBoundMin, vertex.position); + gpuModel.visualBoundMax = glm::max(gpuModel.visualBoundMax, vertex.position); + } + gpuModel.visualBoundRadius = + glm::length(gpuModel.visualBoundMax - gpuModel.visualBoundMin) * 0.5f; + } const auto classification = classifyM2Model( model.name, model.boundMin, model.boundMax, model.vertices.size(), model.particleEmitters.size()); @@ -3744,14 +3754,25 @@ bool CharacterRenderer::getInstanceBounds(uint32_t instanceId, glm::vec3& outCen const auto& inst = it->second; const auto& model = mIt->second.data; - glm::vec3 localCenter = (model.boundMin + model.boundMax) * 0.5f; - float radius = model.boundRadius; + glm::vec3 boundsMin = mIt->second.visualBoundMin; + glm::vec3 boundsMax = mIt->second.visualBoundMax; + float radius = mIt->second.visualBoundRadius; if (radius <= 0.001f) { - radius = glm::length(model.boundMax - model.boundMin) * 0.5f; + boundsMin = model.boundMin; + boundsMax = model.boundMax; + radius = model.boundRadius; + if (radius <= 0.001f) { + radius = glm::length(boundsMax - boundsMin) * 0.5f; + } } float scale = std::max(0.001f, inst.scale); - outCenter = inst.position + localCenter * scale; + const glm::vec3 localCenter = (boundsMin + boundsMax) * 0.5f; + glm::mat4 rotation(1.0f); + rotation = glm::rotate(rotation, inst.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); + rotation = glm::rotate(rotation, inst.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); + rotation = glm::rotate(rotation, inst.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); + outCenter = inst.position + glm::vec3(rotation * glm::vec4(localCenter * scale, 0.0f)); outRadius = std::max(0.5f, radius * scale); return true; } diff --git a/tests/test_spell_classification.cpp b/tests/test_spell_classification.cpp index 9223a870..3307a18d 100644 --- a/tests/test_spell_classification.cpp +++ b/tests/test_spell_classification.cpp @@ -1,6 +1,7 @@ #include #include "game/spell_classification.hpp" +#include "game/spell_defines.hpp" #include #include @@ -35,6 +36,11 @@ private: } // namespace +TEST_CASE("DONT_REPORT cast failures remain player-facing", "[spell][failure]") { + REQUIRE(std::string(wowee::game::getSpellCastResultString(27)) == + "You can't do that right now"); +} + // --------------------------------------------------------------------------- // Range classification // ---------------------------------------------------------------------------