From 329a1f4b12b54f8b1cc3e9d07c1963899fc9c68c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:11:29 -0700 Subject: [PATCH] feat: add IsActionInRange, GetActionInfo, and GetActionCount Lua API IsActionInRange(slot) checks if the spell on an action bar slot is within range of the current target, using DBC spell range data and entity positions. Returns 1/0/nil matching the WoW API contract. GetActionInfo(slot) returns action type ("spell"/"item"/"macro"), id, and subType for action bar interrogation by bar addons. GetActionCount(slot) returns item stack count across backpack and bags for consumable tracking on action bars. --- src/addons/lua_engine.cpp | 112 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index ea1635e4..9b4ed756 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -2919,6 +2919,115 @@ static int lua_IsUsableAction(lua_State* L) { return 2; } +// IsActionInRange(slot) → 1 if in range, 0 if out, nil if no range check applicable +static int lua_IsActionInRange(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnil(L); return 1; } + int slot = static_cast(luaL_checknumber(L, 1)) - 1; + const auto& bar = gh->getActionBar(); + if (slot < 0 || slot >= static_cast(bar.size()) || bar[slot].isEmpty()) { + lua_pushnil(L); + return 1; + } + const auto& action = bar[slot]; + uint32_t spellId = 0; + if (action.type == game::ActionBarSlot::SPELL) { + spellId = action.id; + } else { + // Items/macros: no range check for now + lua_pushnil(L); + return 1; + } + if (spellId == 0) { lua_pushnil(L); return 1; } + + auto data = gh->getSpellData(spellId); + if (data.maxRange <= 0.0f) { + // Melee or self-cast spells: no range indicator + lua_pushnil(L); + return 1; + } + + // Need a target to check range against + uint64_t targetGuid = gh->getTargetGuid(); + if (targetGuid == 0) { lua_pushnil(L); return 1; } + auto targetEnt = gh->getEntityManager().getEntity(targetGuid); + 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; +} + +// GetActionInfo(slot) → actionType, id, subType +static int lua_GetActionInfo(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return 0; } + int slot = static_cast(luaL_checknumber(L, 1)) - 1; + const auto& bar = gh->getActionBar(); + if (slot < 0 || slot >= static_cast(bar.size()) || bar[slot].isEmpty()) { + return 0; + } + const auto& action = bar[slot]; + switch (action.type) { + case game::ActionBarSlot::SPELL: + lua_pushstring(L, "spell"); + lua_pushnumber(L, action.id); + lua_pushstring(L, "spell"); + return 3; + case game::ActionBarSlot::ITEM: + lua_pushstring(L, "item"); + lua_pushnumber(L, action.id); + lua_pushstring(L, "item"); + return 3; + case game::ActionBarSlot::MACRO: + lua_pushstring(L, "macro"); + lua_pushnumber(L, action.id); + lua_pushstring(L, "macro"); + return 3; + default: + return 0; + } +} + +// GetActionCount(slot) → count (item stack count or 0) +static int lua_GetActionCount(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); return 1; } + int slot = static_cast(luaL_checknumber(L, 1)) - 1; + const auto& bar = gh->getActionBar(); + if (slot < 0 || slot >= static_cast(bar.size()) || bar[slot].isEmpty()) { + lua_pushnumber(L, 0); + return 1; + } + const auto& action = bar[slot]; + if (action.type == game::ActionBarSlot::ITEM && action.id != 0) { + // Count items across backpack + bags + uint32_t count = 0; + const auto& inv = gh->getInventory(); + for (int i = 0; i < inv.getBackpackSize(); ++i) { + const auto& s = inv.getBackpackSlot(i); + if (!s.empty() && s.item.itemId == action.id) + count += (s.item.stackCount > 0 ? s.item.stackCount : 1); + } + for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) { + int bagSize = inv.getBagSize(b); + for (int i = 0; i < bagSize; ++i) { + const auto& s = inv.getBagSlot(b, i); + if (!s.empty() && s.item.itemId == action.id) + count += (s.item.stackCount > 0 ? s.item.stackCount : 1); + } + } + lua_pushnumber(L, count); + } else { + lua_pushnumber(L, 0); + } + return 1; +} + // GetActionCooldown(slot) → start, duration, enable static int lua_GetActionCooldown(lua_State* L) { auto* gh = getGameHandler(L); @@ -3655,6 +3764,9 @@ void LuaEngine::registerCoreAPI() { {"GetActionTexture", lua_GetActionTexture}, {"IsCurrentAction", lua_IsCurrentAction}, {"IsUsableAction", lua_IsUsableAction}, + {"IsActionInRange", lua_IsActionInRange}, + {"GetActionInfo", lua_GetActionInfo}, + {"GetActionCount", lua_GetActionCount}, {"GetActionCooldown", lua_GetActionCooldown}, {"UseAction", lua_UseAction}, {"CancelUnitBuff", lua_CancelUnitBuff},