Input Functions and Spell Queue
Overview ๐โ
In this module we introduce one of the most (if not the most) important features for scripting: a way to manage input from code. For now, this only includes spell casting. However, stay tuned to the changelogs, since other input methods like movement are planned to be supported in the near future.
The Way Raw Input Functions Work
Similar to what we previously discussed in the buffs page, the raw input functions that the game provides to us have some disadvantages. In this case, they are not FPS-related, but rather usability and safety related. These functions basically send a paquet to the game's server that mimics a legit spell cast or movement. Therefore, spamming raw inputs from code may be dangerous since you might be sending many more requests per seconds than any human would be able to send. So far, this is not a problem for us, but it's something to take into account for the future, as Blizzard anticheat evolves.
The real problem is usability ๐ฅ:
1 - Compatibility between plugins: If your scripts spam input requests, you will make everything else useless. For example, other modules like "Core Interrupt" might want to cast a spell to interrupt an important enemy cast. This usually has more priority than the normal damage rotation, but since you are flooding the server with your requests, the interruptor spell cast request won't have a chance to be sent.
2 - User Experience: If your script spam input requests you make the user unable to cast their own spells manually. As you could imagine, there might me certain situations in which the users have to cast certain spells on their own, so blocking this could be very frustrating them. To fix this, we handle everything in our LUA Spell Queue Module, which will be explained in detail below.
You can still use raw input functions, but at your own risk. We advise you to read thoroughly the previous explanation and check if you really really need to use the raw functions. If you have any question, contact us and we will guide you through without any problem - Better safe than sorry. โค๏ธ
For some items that don't have global cooldown, the raw "Use Item" functions are perfectly fine, just make sure to add checks before the cast so you don't spam when the item isn't ready.
Raw Input Functions ๐โ
Cast Target Spell ๐ฃโ
core.input.cast_target_spell(spell_id: integer, target: game_object) -> boolean
- Cast a spell directly at a target.
-
Parameters:
spell_id: The ID of your chosen spelltarget: The game_object that you want to cast the spell to
- Returns:
trueif the spell was cast,falseif it fizzled
This function JUST sends a cast request to the server. It doesn't check if the enemy is close enough, if you are facing it, if the spell is ready, etc. Therefore, you must apply all these checks before casting. To do so, we created a LUA Spell Helper module that will make the job very easy. Check spell book.
We advise you to check the Spell Book module before jumping into input code. This is the proper way you should be casting spells:
---@type spell_helper
local spell_helper = require("common/utility/spell_helper")
---@type plugin_helper
local plugin_helper = require("common/utility/plugin_helper")
local last_cast_time = 0.0
core.register_on_update_callback(function()
-- if we remove this check, you will see in the console that more than 1 cast request is issued.
-- To avoid this and only send one (this is good practice behaviour), we add a minimum delay of 0.25 seconds
-- for this function to be ran again.
local current_time = core.game_time()
if current_time - last_cast_time < 0.50 then
return false
end
local local_player = core.object_manager.get_local_player()
if not local_player then
return
end
-- since this is just a test, we will just get the hud target
local hud_target = local_player:get_target()
-- only cast the fireball when there is a target selected
if not hud_target then
return
end
-- avoid spamming cast request while already casting
-- NOTE: in your scripts, you might want to do the same for channels.
-- approach 1: take into account network latency
-- local network = plugin_helper:get_latency()
-- local cast_end_time = local_player:get_active_spell_cast_end_time()
-- local cast_delta = math.max(cast_end_time - current_time, 0.0)
-- if cast_delta > (network * 1000) then
-- return
-- end
-- approach 2: more simple, works well in most cases.
local cast_end_time = local_player:get_active_spell_cast_end_time()
if current_time <= cast_end_time then
return
end
local fireball_id = 133
-- check first if the spell is castable, so we avoid sending useless packets (the script will be stuck permanently trying to cast a spell that can't be casted)
local can_cast_fireball = spell_helper:is_spell_castable(fireball_id, local_player, hud_target, false, false)
if not can_cast_fireball then
return
end
local spell_cast = core.input.cast_target_spell(fireball_id, hud_target)
if spell_cast then
core.log("Fireball Cast!")
last_cast_time = current_time
end
end)
This example might be an overkill, specially if you are a beginner and are learning. Feel free to play with the code and go step by step. However, if you want to produce good quality products, consider adding at least all the steps specified in the previous example to your casts.
Cast Position Spell ๐ฃโ
core.input.cast_position_spell(spell_id: integer, position: vec3) -> boolean
- Cast a spell at a specific location in the world.
-
Parameters:
spell_id: Your spell's IDposition: The XYZ coordinates for your spell. See vec3
- Returns:
trueif cast successfully,falseif not
This function is only used for spells that don't require a target game_object, but instead require a target position. This is usually the case for some AOE spells like Blizzard or Flamestrike.
Let's cast a Flamestrike:
---@type spell_helper
local spell_helper = require("common/utility/spell_helper")
---@type plugin_helper
local plugin_helper = require("common/utility/plugin_helper")
local last_cast_time = 0.0
core.register_on_update_callback(function()
-- if we remove this check, you will see in the console that more than 1 cast request is issued.
-- To avoid this and only send one (this is good practice behaviour), we add a minimum delay of 0.25 seconds
-- for this function to be ran again.
local current_time = core.game_time()
if current_time - last_cast_time < 0.50 then
return false
end
local local_player = core.object_manager.get_local_player()
if not local_player then
return
end
-- since this is just a test, we will just get the hud target
local hud_target = local_player:get_target()
-- only cast the fireball when there is a target selected
if not hud_target then
return
end
-- avoid spamming cast request while already casting
-- NOTE: in your scripts, you might want to do the same for channels.
-- approach 1: take into account network latency
-- local network = plugin_helper:get_latency()
-- local cast_end_time = local_player:get_active_spell_cast_end_time()
-- local cast_delta = math.max(cast_end_time - current_time, 0.0)
-- if cast_delta > (network * 1000) then
-- return
-- end
-- approach 2: more simple, works well in most cases.
local cast_end_time = local_player:get_active_spell_cast_end_time()
if current_time <= cast_end_time then
return
end
local flamestrike_id = 2120
-- check first if the spell is castable, so we avoid sending useless packets (the script will be stuck permanently trying to cast a spell that can't be casted)
local can_cast_fireball = spell_helper:is_spell_castable(flamestrike_id, local_player, hud_target, false, false)
if not can_cast_fireball then
return
end
local position_to_cast = hud_target:get_position()
local spell_cast = core.input.cast_position_spell(flamestrike_id, position_to_cast)
if spell_cast then
core.log("Flamestrike Cast On Target Position!")
last_cast_time = current_time
end
end)
As you can see, in the previous example we are casting the spell to the target's position, without any further checks. For AOE spells, you would ideally want to cast on the position that would hit the most enemies, which is usually not the same as your main target's position. To do this, you should use some sort of algorithm to determine which is the actual best point to cast, according to your spell's characteristics. To do this, we have developed the "Spell Prediction" module. See Spell Prediction Module
Use Item ๐ญโ
We have three item usage functions, each with its own purpose:
1- Item Self-Cast
core.input.use_item(item_id: integer) -> boolean
- This function is used for items that don't require a target or a target position.
2- Item Targeted-Cast
core.input.use_item_target(item_id: integer, target: game_object) -> boolean
- This function is used for items that require a target or a target position.
3- Item Position-Cast
core.input.use_item_position(item_id: integer, position: vec3) -> boolean
- Use an item at a specific location. (Note: This feature is still in development)
Most items don't have a global cooldown, so these raw functions are usually fine, as we discussed earlier. However, for items that apply GCD, consider using the spell_queue.
The code for casting items is pretty similar to the code for casting spells. You just have to be careful with the way you check if the item is ready, since it's different from checking if a spell is ready. Below, a simple example on how to cast a health potion:
---@type unit_helper
local unit_helper = require("common/utility/unit_helper")
local last_cast_time = 0.0
core.register_on_update_callback(function()
-- if we remove this check, you will see in the console that more than 1 cast request is issued.
-- To avoid this and only send one (this is good practice behaviour), we add a minimum delay of 0.25 seconds
-- for this function to be ran again.
local current_time = core.game_time()
if current_time - last_cast_time < 5.0 then
return false
end
local local_player = core.object_manager.get_local_player()
if not local_player then
return
end
local cast_end_time = local_player:get_active_spell_cast_end_time()
if current_time <= cast_end_time then
return
end
-- the potion for this example is the "Greater Healing Potion"
local potion_id = 1710
local item_cooldown = local_player:get_item_cooldown(potion_id)
local can_cast_potion = item_cooldown <= 0.0
if not can_cast_potion then
return false
end
-- we add this check so the potion is not attempted to be cast while full HP, since the game won't allow it.
if unit_helper:get_health_percentage(local_player) >= 1.0 then
return false
end
local spell_cast = core.input.use_item(potion_id)
if spell_cast then
core.log("Potion cast!")
last_cast_time = current_time
end
end)
Use Container Item ๐โ
core.input.use_container_item(container_id: integer, slot_id: integer)
- Use an item directly from a bag container by its bag index and slot index.
-
Parameters:
container_id: The container (bag) index.slot_id: The slot index within the container.
Unlike use_item which takes an item ID, this function addresses the item by its physical bag and slot position. This is useful when you need to interact with items that share the same ID but occupy different slots, or when working with container-specific operations.
Vendor Interaction ๐ชโ
buy_item(index, quantity)โ
Purchases an item from the currently open vendor window.
Parameters:index(integer) โ The vendor item index (1-based).quantity(integer) โ The number of items to purchase.
Returns: nil
Example Usage
-- Buy 5 of the first vendor item
core.input.buy_item(1, 5)
repair_all_items(use_guild_bank)โ
Repairs all equipped items at a repair-capable vendor.
Parameters:use_guild_bank(boolean) โtrueto use guild bank funds for the repair;falseto use personal gold.
Returns: nil
Example Usage
-- Repair all items using personal gold
core.input.repair_all_items(false)
-- Repair using guild bank funds
core.input.repair_all_items(true)
Set Target ๐ฏโ
core.input.set_target(unit: game_object) -> boolean
- Set your current target.
- Returns:
trueif targeting was successful,falseif not
Example:
local local_player = core.object_manager.get_local_player()
if local_player then
local player_position = local_player:get_position()
local nearby_enemies = unit_helper:get_enemy_list_around(player_position, 30)
for _, unit in ipairs(nearby_enemies) do
local success = core.input.set_target(unit)
if success then
core.log("New target acquired! ๐ฏ")
break
else
core.log("Targeting failed. They're quick! ๐จ")
end
end
end
Set and Get Focus ๐โ
core.input.set_focus(unit: game_object) -> boolean: Set your focus targetcore.input.get_focus() -> game_object | nil: Retrieve your current focus
Checking your focus:
local current_focus = core.input.get_focus()
if current_focus then
core.log("Current focus: " .. current_focus:get_name() .. " ๐")
else
core.log("No focus set currently")
end