Fix combat interaction, creature skin fallback, clipping, and minimap marker anchoring

- Right-click attack fallback for non-interactable hostile creatures
- Robust creature skin path resolution for WotLK/non-humanoid display skin fields
- Strengthened client-side anti-overlap spacing for active melee targets (including wolf/worg models)
- Minimap questgiver markers now use live minimap view radius and exact minimap center to prevent player-relative drift
This commit is contained in:
Kelsi 2026-02-20 16:27:21 -08:00
parent 017bdf9033
commit 504d5d2b15
3 changed files with 127 additions and 10 deletions

View file

@ -39,6 +39,7 @@ public:
void setSquareShape(bool square) { squareShape = square; }
bool isSquareShape() const { return squareShape; }
float getViewRadius() const { return viewRadius; }
void zoomIn() { viewRadius = std::max(100.0f, viewRadius - 50.0f); }
void zoomOut() { viewRadius = std::min(800.0f, viewRadius + 50.0f); }

View file

@ -922,10 +922,18 @@ void Application::update(float deltaTime) {
if (renderer && gameHandler && renderer->getCharacterRenderer()) {
auto* charRenderer = renderer->getCharacterRenderer();
glm::vec3 playerPos(0.0f);
glm::vec3 playerRenderPos(0.0f);
bool havePlayerPos = false;
float playerCollisionRadius = 0.65f;
if (auto playerEntity = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid())) {
playerPos = glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ());
playerRenderPos = core::coords::canonicalToRender(playerPos);
havePlayerPos = true;
glm::vec3 pc;
float pr = 0.0f;
if (getRenderBoundsForGuid(gameHandler->getPlayerGuid(), pc, pr)) {
playerCollisionRadius = std::clamp(pr * 0.35f, 0.45f, 1.1f);
}
}
const float syncRadiusSq = 320.0f * 320.0f;
for (const auto& [guid, instanceId] : creatureInstances_) {
@ -939,6 +947,60 @@ void Application::update(float deltaTime) {
}
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
// Visual collision guard: keep hostile melee units from rendering inside the
// player's model while attacking. This is client-side only (no server position change).
auto unit = std::static_pointer_cast<game::Unit>(entity);
const uint64_t currentTargetGuid = gameHandler->hasTarget() ? gameHandler->getTargetGuid() : 0;
const uint64_t autoAttackGuid = gameHandler->getAutoAttackTargetGuid();
const bool isCombatTarget = (guid == currentTargetGuid || guid == autoAttackGuid);
bool clipGuardEligible = havePlayerPos &&
unit->getHealth() > 0 &&
(unit->isHostile() ||
gameHandler->isAggressiveTowardPlayer(guid) ||
isCombatTarget);
if (clipGuardEligible) {
float creatureCollisionRadius = 0.8f;
glm::vec3 cc;
float cr = 0.0f;
if (getRenderBoundsForGuid(guid, cc, cr)) {
creatureCollisionRadius = std::clamp(cr * 0.45f, 0.65f, 1.9f);
}
float minSep = std::max(playerCollisionRadius + creatureCollisionRadius, 1.9f);
if (isCombatTarget) {
// Stronger spacing for the actively engaged attacker to avoid bite-overlap.
minSep = std::max(minSep, 2.2f);
}
// Species/model-specific spacing for wolf-like creatures (their lunge anims
// often put head/torso inside the player capsule).
auto mit = creatureModelIds_.find(guid);
if (mit != creatureModelIds_.end()) {
if (const auto* md = charRenderer->getModelData(mit->second)) {
std::string modelName = md->name;
std::transform(modelName.begin(), modelName.end(), modelName.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (modelName.find("wolf") != std::string::npos ||
modelName.find("worg") != std::string::npos) {
minSep = std::max(minSep, 2.45f);
}
}
}
glm::vec2 d2(renderPos.x - playerRenderPos.x, renderPos.y - playerRenderPos.y);
float distSq2 = glm::dot(d2, d2);
if (distSq2 < (minSep * minSep)) {
glm::vec2 dir2(1.0f, 0.0f);
if (distSq2 > 1e-6f) {
dir2 = d2 * (1.0f / std::sqrt(distSq2));
}
glm::vec2 clamped2 = glm::vec2(playerRenderPos.x, playerRenderPos.y) + dir2 * minSep;
renderPos.x = clamped2.x;
renderPos.y = clamped2.y;
}
}
charRenderer->setInstancePosition(instanceId, renderPos);
float renderYaw = entity->getOrientation() + glm::radians(90.0f);
charRenderer->setInstanceRotation(instanceId, glm::vec3(0.0f, 0.0f, renderYaw));
@ -3634,17 +3696,59 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Apply creature skin textures (for non-humanoid creatures)
if (!hasHumanoidTexture && modelData) {
auto resolveCreatureSkinPath = [&](const std::string& skinField) -> std::string {
if (skinField.empty()) return "";
std::string raw = skinField;
std::replace(raw.begin(), raw.end(), '/', '\\');
auto isSpace = [](unsigned char c) { return std::isspace(c) != 0; };
raw.erase(raw.begin(), std::find_if(raw.begin(), raw.end(), [&](unsigned char c) { return !isSpace(c); }));
raw.erase(std::find_if(raw.rbegin(), raw.rend(), [&](unsigned char c) { return !isSpace(c); }).base(), raw.end());
if (raw.empty()) return "";
auto hasBlpExt = [](const std::string& p) {
if (p.size() < 4) return false;
std::string ext = p.substr(p.size() - 4);
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return ext == ".blp";
};
auto addCandidate = [](std::vector<std::string>& out, const std::string& p) {
if (p.empty()) return;
if (std::find(out.begin(), out.end(), p) == out.end()) out.push_back(p);
};
std::vector<std::string> candidates;
const bool hasDir = (raw.find('\\') != std::string::npos || raw.find('/') != std::string::npos);
const bool hasExt = hasBlpExt(raw);
if (hasDir) {
addCandidate(candidates, raw);
if (!hasExt) addCandidate(candidates, raw + ".blp");
} else {
addCandidate(candidates, modelDir + raw);
if (!hasExt) addCandidate(candidates, modelDir + raw + ".blp");
addCandidate(candidates, raw);
if (!hasExt) addCandidate(candidates, raw + ".blp");
}
for (const auto& c : candidates) {
if (assetManager->fileExists(c)) return c;
}
return "";
};
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
const auto& tex = modelData->textures[ti];
std::string skinPath;
// Creature skin types: 11 = skin1, 12 = skin2, 13 = skin3
if (tex.type == 11 && !dispData.skin1.empty()) {
skinPath = modelDir + dispData.skin1 + ".blp";
skinPath = resolveCreatureSkinPath(dispData.skin1);
} else if (tex.type == 12 && !dispData.skin2.empty()) {
skinPath = modelDir + dispData.skin2 + ".blp";
skinPath = resolveCreatureSkinPath(dispData.skin2);
} else if (tex.type == 13 && !dispData.skin3.empty()) {
skinPath = modelDir + dispData.skin3 + ".blp";
skinPath = resolveCreatureSkinPath(dispData.skin3);
}
if (!skinPath.empty()) {
@ -3653,6 +3757,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), skinTex);
LOG_DEBUG("Applied creature skin texture: ", skinPath, " to slot ", ti);
}
} else if ((tex.type == 11 && !dispData.skin1.empty()) ||
(tex.type == 12 && !dispData.skin2.empty()) ||
(tex.type == 13 && !dispData.skin3.empty())) {
LOG_WARNING("Creature skin texture not found for displayId ", displayId,
" slot ", ti, " type ", tex.type,
" (skin fields: '", dispData.skin1, "', '",
dispData.skin2, "', '", dispData.skin3, "')");
}
}
}

View file

@ -1498,7 +1498,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
gameHandler.lootTarget(target->getGuid());
} else {
// Interact with friendly NPCs; hostile units just get targeted
// Interact with service NPCs; otherwise treat non-interactable living units
// as attackable fallback (covers bad faction-template classification).
auto isSpiritNpc = [&]() -> bool {
constexpr uint32_t NPC_FLAG_SPIRIT_GUIDE = 0x00004000;
constexpr uint32_t NPC_FLAG_SPIRIT_HEALER = 0x00008000;
@ -1512,9 +1513,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
(name.find("spirit guide") != std::string::npos);
};
bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc();
if (!unit->isHostile() && (unit->isInteractable() || allowSpiritInteract)) {
bool canInteractNpc = unit->isInteractable() || allowSpiritInteract;
bool shouldAttackByFallback = !canInteractNpc;
if (!unit->isHostile() && canInteractNpc) {
gameHandler.interactWithNpc(target->getGuid());
} else if (unit->isHostile()) {
} else if (unit->isHostile() || shouldAttackByFallback) {
gameHandler.startAutoAttack(target->getGuid());
}
}
@ -6265,11 +6268,13 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
float mapRadius = mapSize * 0.5f;
float centerX = screenW - margin - mapRadius;
float centerY = margin + mapRadius;
float viewRadius = 400.0f;
float viewRadius = minimap->getViewRadius();
// Player position in render coords
auto& mi = gameHandler.getMovementInfo();
glm::vec3 playerRender = core::coords::canonicalToRender(glm::vec3(mi.x, mi.y, mi.z));
// Use the exact same minimap center as Renderer::renderWorld() to keep markers anchored.
glm::vec3 playerRender = camera->getPosition();
if (renderer->getCharacterInstanceId() != 0) {
playerRender = renderer->getCharacterPosition();
}
// Camera bearing for minimap rotation
float bearing = 0.0f;