From 42b776dbf8e9a437e04c1ea8d1289b403aaffc0a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 21 Mar 2026 12:23:08 -0700 Subject: [PATCH] feat: add IsSpellInRange for healing and range-check addons Returns 1 if the spell can reach the target, 0 if out of range, nil if range can't be determined. Compares player-to-target distance against the spell's maxRange from Spell.dbc via SpellDataResolver. Used by healing addons (Healbot, VuhDo, Clique) to check if heals can reach party members, and by action bar addons for range coloring. --- src/addons/lua_engine.cpp | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 2f5bfa4c..dd81bf98 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -540,6 +540,49 @@ static int lua_UnitDetailedThreatSituation(lua_State* L) { return 5; } +// IsSpellInRange(spellName, unit) → 0 or 1 (nil if can't determine) +static int lua_IsSpellInRange(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnil(L); return 1; } + const char* spellNameOrId = luaL_checkstring(L, 1); + const char* uid = luaL_optstring(L, 2, "target"); + + // Resolve spell ID + uint32_t spellId = 0; + if (spellNameOrId[0] >= '0' && spellNameOrId[0] <= '9') { + spellId = static_cast(strtoul(spellNameOrId, nullptr, 10)); + } else { + std::string nameLow(spellNameOrId); + for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + for (char& c : sn) c = static_cast(std::tolower(static_cast(c))); + if (sn == nameLow) { spellId = sid; break; } + } + } + if (spellId == 0) { lua_pushnil(L); return 1; } + + // Get spell max range from DBC + auto data = gh->getSpellData(spellId); + if (data.maxRange <= 0.0f) { lua_pushnil(L); return 1; } + + // Resolve target position + std::string uidStr(uid); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushnil(L); return 1; } + auto targetEnt = gh->getEntityManager().getEntity(guid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { lua_pushnil(L); return 1; } + + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + float dist = std::sqrt(dx*dx + dy*dy + dz*dz); + lua_pushnumber(L, dist <= data.maxRange ? 1 : 0); + return 1; +} + // UnitIsVisible(unit) → boolean (entity exists in the client's entity manager) static int lua_UnitIsVisible(lua_State* L) { const char* uid = luaL_optstring(L, 1, "target"); @@ -3371,6 +3414,7 @@ void LuaEngine::registerCoreAPI() { {"IsSpellKnown", lua_IsSpellKnown}, {"GetSpellCooldown", lua_GetSpellCooldown}, {"GetSpellPowerCost", lua_GetSpellPowerCost}, + {"IsSpellInRange", lua_IsSpellInRange}, {"HasTarget", lua_HasTarget}, {"TargetUnit", lua_TargetUnit}, {"ClearTarget", lua_ClearTarget},