| @@ -0,0 +1,40 @@ | |||
| 1 | + | TargetBot.Creature.calculatePriority = function(creature, config, path) | |
| 2 | + | -- config is based on creature_editor | |
| 3 | + | local priority = 0 | |
| 4 | + | ||
| 5 | + | -- extra priority if it's current target | |
| 6 | + | if g_game.getAttackingCreature() == creature then | |
| 7 | + | priority = priority + 1 | |
| 8 | + | end | |
| 9 | + | ||
| 10 | + | -- check if distance is fine, if not then attack only if already attacked | |
| 11 | + | if #path > config.maxDistance then | |
| 12 | + | return priority | |
| 13 | + | end | |
| 14 | + | ||
| 15 | + | -- add config priority | |
| 16 | + | priority = priority + config.priority | |
| 17 | + | ||
| 18 | + | -- extra priority for close distance | |
| 19 | + | local path_length = #path | |
| 20 | + | if path_length == 1 then | |
| 21 | + | priority = priority + 3 | |
| 22 | + | elseif path_length <= 3 then | |
| 23 | + | priority = priority + 1 | |
| 24 | + | end | |
| 25 | + | ||
| 26 | + | -- extra priority for low health | |
| 27 | + | if config.chase and creature:getHealthPercent() < 30 then | |
| 28 | + | priority = priority + 5 | |
| 29 | + | elseif creature:getHealthPercent() < 20 then | |
| 30 | + | priority = priority + 2.5 | |
| 31 | + | elseif creature:getHealthPercent() < 40 then | |
| 32 | + | priority = priority + 1.5 | |
| 33 | + | elseif creature:getHealthPercent() < 60 then | |
| 34 | + | priority = priority + 0.5 | |
| 35 | + | elseif creature:getHealthPercent() < 80 then | |
| 36 | + | priority = priority + 0.2 | |
| 37 | + | end | |
| 38 | + | ||
| 39 | + | return priority | |
| 40 | + | end |
| @@ -0,0 +1,295 @@ | |||
| 1 | + | TargetBot.Looting = {} | |
| 2 | + | TargetBot.Looting.list = {} -- list of containers to loot | |
| 3 | + | ||
| 4 | + | local ui | |
| 5 | + | local items = {} | |
| 6 | + | local containers = {} | |
| 7 | + | local itemsById = {} | |
| 8 | + | local containersById = {} | |
| 9 | + | local dontSave = false | |
| 10 | + | ||
| 11 | + | TargetBot.Looting.setup = function() | |
| 12 | + | ui = UI.createWidget("TargetBotLootingPanel") | |
| 13 | + | UI.Container(TargetBot.Looting.onItemsUpdate, true, nil, ui.items) | |
| 14 | + | UI.Container(TargetBot.Looting.onContainersUpdate, true, nil, ui.containers) | |
| 15 | + | ui.everyItem.onClick = function() | |
| 16 | + | ui.everyItem:setOn(not ui.everyItem:isOn()) | |
| 17 | + | TargetBot.save() | |
| 18 | + | end | |
| 19 | + | ui.maxDangerPanel.value.onTextChange = function() | |
| 20 | + | local value = tonumber(ui.maxDangerPanel.value:getText()) | |
| 21 | + | if not value then | |
| 22 | + | ui.maxDangerPanel.value:setText(0) | |
| 23 | + | end | |
| 24 | + | if dontSave then return end | |
| 25 | + | TargetBot.save() | |
| 26 | + | end | |
| 27 | + | ui.minCapacityPanel.value.onTextChange = function() | |
| 28 | + | local value = tonumber(ui.minCapacityPanel.value:getText()) | |
| 29 | + | if not value then | |
| 30 | + | ui.minCapacityPanel.value:setText(0) | |
| 31 | + | end | |
| 32 | + | if dontSave then return end | |
| 33 | + | TargetBot.save() | |
| 34 | + | end | |
| 35 | + | end | |
| 36 | + | ||
| 37 | + | TargetBot.Looting.onItemsUpdate = function() | |
| 38 | + | if dontSave then return end | |
| 39 | + | TargetBot.save() | |
| 40 | + | TargetBot.Looting.updateItemsAndContainers() | |
| 41 | + | end | |
| 42 | + | ||
| 43 | + | TargetBot.Looting.onContainersUpdate = function() | |
| 44 | + | if dontSave then return end | |
| 45 | + | TargetBot.save() | |
| 46 | + | TargetBot.Looting.updateItemsAndContainers() | |
| 47 | + | end | |
| 48 | + | ||
| 49 | + | TargetBot.Looting.update = function(data) | |
| 50 | + | dontSave = true | |
| 51 | + | TargetBot.Looting.list = {} | |
| 52 | + | ui.items:setItems(data['items'] or {}) | |
| 53 | + | ui.containers:setItems(data['containers'] or {}) | |
| 54 | + | ui.everyItem:setOn(data['everyItem']) | |
| 55 | + | ui.maxDangerPanel.value:setText(data['maxDanger'] or 10) | |
| 56 | + | ui.minCapacityPanel.value:setText(data['minCapacity'] or 100) | |
| 57 | + | TargetBot.Looting.updateItemsAndContainers() | |
| 58 | + | dontSave = false | |
| 59 | + | end | |
| 60 | + | ||
| 61 | + | TargetBot.Looting.save = function(data) | |
| 62 | + | data['items'] = ui.items:getItems() | |
| 63 | + | data['containers'] = ui.containers:getItems() | |
| 64 | + | data['maxDanger'] = tonumber(ui.maxDangerPanel.value:getText()) | |
| 65 | + | data['minCapacity'] = tonumber(ui.minCapacityPanel.value:getText()) | |
| 66 | + | data['everyItem'] = ui.everyItem:isOn() | |
| 67 | + | end | |
| 68 | + | ||
| 69 | + | TargetBot.Looting.updateItemsAndContainers = function() | |
| 70 | + | items = ui.items:getItems() | |
| 71 | + | containers = ui.containers:getItems() | |
| 72 | + | itemsById = {} | |
| 73 | + | containersById = {} | |
| 74 | + | for i, item in ipairs(items) do | |
| 75 | + | itemsById[item.id] = 1 | |
| 76 | + | end | |
| 77 | + | for i, container in ipairs(containers) do | |
| 78 | + | containersById[container.id] = 1 | |
| 79 | + | end | |
| 80 | + | end | |
| 81 | + | ||
| 82 | + | local waitTill = 0 | |
| 83 | + | local waitingForContainer = nil | |
| 84 | + | local status = "" | |
| 85 | + | local lastFoodConsumption = 0 | |
| 86 | + | ||
| 87 | + | TargetBot.Looting.getStatus = function() | |
| 88 | + | return status | |
| 89 | + | end | |
| 90 | + | ||
| 91 | + | TargetBot.Looting.process = function(targets, dangerLevel) | |
| 92 | + | if (not items[1] and not ui.everyItem:isOn()) or not containers[1] then | |
| 93 | + | status = "" | |
| 94 | + | return false | |
| 95 | + | end | |
| 96 | + | if dangerLevel > tonumber(ui.maxDangerPanel.value:getText()) then | |
| 97 | + | status = "High danger" | |
| 98 | + | return false | |
| 99 | + | end | |
| 100 | + | if player:getFreeCapacity() < tonumber(ui.minCapacityPanel.value:getText()) then | |
| 101 | + | status = "No cap" | |
| 102 | + | TargetBot.Looting.list = {} | |
| 103 | + | return false | |
| 104 | + | end | |
| 105 | + | local loot = TargetBot.Looting.list[1] | |
| 106 | + | if loot == nil then | |
| 107 | + | status = "" | |
| 108 | + | return false | |
| 109 | + | end | |
| 110 | + | ||
| 111 | + | if waitTill > now then | |
| 112 | + | return true | |
| 113 | + | end | |
| 114 | + | local containers = g_game.getContainers() | |
| 115 | + | local lootContainers = TargetBot.Looting.getLootContainers(containers) | |
| 116 | + | ||
| 117 | + | -- check if there's container for loot and has empty space for it | |
| 118 | + | if not lootContainers[1] then | |
| 119 | + | -- there's no space, don't loot | |
| 120 | + | status = "No space" | |
| 121 | + | return false | |
| 122 | + | end | |
| 123 | + | ||
| 124 | + | status = "Looting" | |
| 125 | + | ||
| 126 | + | for index, container in pairs(containers) do | |
| 127 | + | if container.lootContainer then | |
| 128 | + | TargetBot.Looting.lootContainer(lootContainers, container) | |
| 129 | + | return true | |
| 130 | + | end | |
| 131 | + | end | |
| 132 | + | ||
| 133 | + | local pos = player:getPosition() | |
| 134 | + | local dist = math.max(math.abs(pos.x-loot.pos.x), math.abs(pos.y-loot.pos.y)) | |
| 135 | + | if loot.tries > 30 or loot.pos.z ~= pos.z or dist > 20 then | |
| 136 | + | table.remove(TargetBot.Looting.list, 1) | |
| 137 | + | return true | |
| 138 | + | end | |
| 139 | + | ||
| 140 | + | local tile = g_map.getTile(loot.pos) | |
| 141 | + | if dist >= 3 or not tile then | |
| 142 | + | loot.tries = loot.tries + 1 | |
| 143 | + | TargetBot.walkTo(loot.pos, 20, { ignoreNonPathable = true, precision = 2 }) | |
| 144 | + | return true | |
| 145 | + | end | |
| 146 | + | ||
| 147 | + | local container = tile:getTopUseThing() | |
| 148 | + | if not container or not container:isContainer() then | |
| 149 | + | table.remove(TargetBot.Looting.list, 1) | |
| 150 | + | return true | |
| 151 | + | end | |
| 152 | + | ||
| 153 | + | g_game.open(container) | |
| 154 | + | waitTill = now + 1000 -- give it 1s to open | |
| 155 | + | waitingForContainer = container:getId() | |
| 156 | + | loot.tries = loot.tries + 10 | |
| 157 | + | ||
| 158 | + | return true | |
| 159 | + | end | |
| 160 | + | ||
| 161 | + | TargetBot.Looting.getLootContainers = function(containers) | |
| 162 | + | local lootContainers = {} | |
| 163 | + | local openedContainersById = {} | |
| 164 | + | local toOpen = nil | |
| 165 | + | for index, container in pairs(containers) do | |
| 166 | + | openedContainersById[container:getContainerItem():getId()] = 1 | |
| 167 | + | if containersById[container:getContainerItem():getId()] and not container.lootContainer then | |
| 168 | + | if container:getItemsCount() < container:getCapacity() then | |
| 169 | + | table.insert(lootContainers, container) | |
| 170 | + | else -- it's full, open next container if possible | |
| 171 | + | for slot, item in ipairs(container:getItems()) do | |
| 172 | + | if item:isContainer() and containersById[item:getId()] then | |
| 173 | + | toOpen = {item, container} | |
| 174 | + | break | |
| 175 | + | end | |
| 176 | + | end | |
| 177 | + | end | |
| 178 | + | end | |
| 179 | + | end | |
| 180 | + | if not lootContainers[1] then | |
| 181 | + | if toOpen then | |
| 182 | + | g_game.open(toOpen[1], toOpen[2]) | |
| 183 | + | waitTill = now + 500 -- wait 0.5s | |
| 184 | + | return lootContainers | |
| 185 | + | end | |
| 186 | + | -- check containers one more time, maybe there's any loot container | |
| 187 | + | for index, container in pairs(containers) do | |
| 188 | + | if not containersById[container:getContainerItem():getId()] and not container.lootContainer then | |
| 189 | + | for slot, item in ipairs(container:getItems()) do | |
| 190 | + | if item:isContainer() and containersById[item:getId()] then | |
| 191 | + | g_game.open(item) | |
| 192 | + | waitTill = now + 500 -- wait 0.5s | |
| 193 | + | return lootContainers | |
| 194 | + | end | |
| 195 | + | end | |
| 196 | + | end | |
| 197 | + | end | |
| 198 | + | -- can't find any lootContainer, let's check slots, maybe there's one | |
| 199 | + | for slot = InventorySlotFirst, InventorySlotLast do | |
| 200 | + | local item = getInventoryItem(slot) | |
| 201 | + | if item and item:isContainer() and not openedContainersById[item:getId()] then | |
| 202 | + | -- container which is not opened yet, let's open it | |
| 203 | + | g_game.open(item) | |
| 204 | + | waitTill = now + 500 -- wait 0.5s | |
| 205 | + | return lootContainers | |
| 206 | + | end | |
| 207 | + | end | |
| 208 | + | end | |
| 209 | + | return lootContainers | |
| 210 | + | end | |
| 211 | + | ||
| 212 | + | TargetBot.Looting.lootContainer = function(lootContainers, container) | |
| 213 | + | -- loot items | |
| 214 | + | local nextContainer = nil | |
| 215 | + | for i, item in ipairs(container:getItems()) do | |
| 216 | + | if item:isContainer() and not itemsById[item:getId()] then | |
| 217 | + | nextContainer = item | |
| 218 | + | elseif itemsById[item:getId()] or (ui.everyItem:isOn() and not item:isContainer()) then | |
| 219 | + | item.lootTries = (item.lootTries or 0) + 1 | |
| 220 | + | if item.lootTries < 5 then -- if can't be looted within 0.5s then skip it | |
| 221 | + | return TargetBot.Looting.lootItem(lootContainers, item) | |
| 222 | + | end | |
| 223 | + | elseif storage.foodItems and storage.foodItems[1] and lastFoodConsumption + 5000 < now then | |
| 224 | + | for _, food in ipairs(storage.foodItems) do | |
| 225 | + | if item:getId() == food.id then | |
| 226 | + | g_game.use(item) | |
| 227 | + | lastFoodConsumption = now | |
| 228 | + | return | |
| 229 | + | end | |
| 230 | + | end | |
| 231 | + | end | |
| 232 | + | end | |
| 233 | + | ||
| 234 | + | -- no more items to loot, open next container | |
| 235 | + | if nextContainer then | |
| 236 | + | nextContainer.lootTries = (nextContainer.lootTries or 0) + 1 | |
| 237 | + | if nextContainer.lootTries < 2 then -- max 0.6s to open it | |
| 238 | + | g_game.open(nextContainer, container) | |
| 239 | + | waitTill = now + 300 -- give it 0.3s to open | |
| 240 | + | waitingForContainer = nextContainer:getId() | |
| 241 | + | return | |
| 242 | + | end | |
| 243 | + | end | |
| 244 | + | ||
| 245 | + | -- looting finished, remove container from list | |
| 246 | + | container.lootContainer = false | |
| 247 | + | g_game.close(container) | |
| 248 | + | table.remove(TargetBot.Looting.list, 1) | |
| 249 | + | end | |
| 250 | + | ||
| 251 | + | TargetBot.Looting.lootItem = function(lootContainers, item) | |
| 252 | + | if item:isStackable() then | |
| 253 | + | local count = item:getCount() | |
| 254 | + | for _, container in ipairs(lootContainers) do | |
| 255 | + | for slot, citem in ipairs(container:getItems()) do | |
| 256 | + | if item:getId() == citem:getId() and citem:getCount() < 100 then | |
| 257 | + | g_game.move(item, container:getSlotPosition(slot - 1), count) | |
| 258 | + | waitTill = now + 300 -- give it 0.3s to move item | |
| 259 | + | return | |
| 260 | + | end | |
| 261 | + | end | |
| 262 | + | end | |
| 263 | + | end | |
| 264 | + | ||
| 265 | + | local container = lootContainers[1] | |
| 266 | + | g_game.move(item, container:getSlotPosition(container:getItemsCount()), 1) | |
| 267 | + | waitTill = now + 300 -- give it 0.3s to move item | |
| 268 | + | end | |
| 269 | + | ||
| 270 | + | onContainerOpen(function(container, previousContainer) | |
| 271 | + | if container:getContainerItem():getId() == waitingForContainer then | |
| 272 | + | container.lootContainer = true | |
| 273 | + | waitingForContainer = nil | |
| 274 | + | end | |
| 275 | + | end) | |
| 276 | + | ||
| 277 | + | onCreatureDisappear(function(creature) | |
| 278 | + | if not TargetBot.isOn() then return end | |
| 279 | + | if not creature:isMonster() then return end | |
| 280 | + | local pos = player:getPosition() | |
| 281 | + | local mpos = creature:getPosition() | |
| 282 | + | local name = creature:getName() | |
| 283 | + | if pos.z ~= mpos.z or math.max(math.abs(pos.x-mpos.x), math.abs(pos.y-mpos.y)) > 6 then return end | |
| 284 | + | schedule(20, function() -- check in 20ms if there's container (dead body) on that tile | |
| 285 | + | if not containers[1] then return end | |
| 286 | + | if TargetBot.Looting.list[20] then return end -- too many items to loot | |
| 287 | + | local tile = g_map.getTile(mpos) | |
| 288 | + | if not tile then return end | |
| 289 | + | local container = tile:getTopUseThing() | |
| 290 | + | if not container or not container:isContainer() then return end | |
| 291 | + | if not findPath(player:getPosition(), mpos, 6, {ignoreNonPathable=true, ignoreCreatures=true, ignoreCost=true}) then return end | |
| 292 | + | table.insert(TargetBot.Looting.list, {pos=mpos, creature=name, container=container:getId(), added=now, tries=0}) | |
| 293 | + | container:setMarked('#000088') | |
| 294 | + | end) | |
| 295 | + | end) |
| @@ -0,0 +1,83 @@ | |||
| 1 | + | TargetBotLootingPanel < Panel | |
| 2 | + | layout: | |
| 3 | + | type: verticalBox | |
| 4 | + | fit-children: true | |
| 5 | + | ||
| 6 | + | HorizontalSeparator | |
| 7 | + | margin-top: 5 | |
| 8 | + | ||
| 9 | + | Label | |
| 10 | + | margin-top: 5 | |
| 11 | + | text: Items to loot | |
| 12 | + | text-align: center | |
| 13 | + | ||
| 14 | + | BotContainer | |
| 15 | + | id: items | |
| 16 | + | margin-top: 3 | |
| 17 | + | ||
| 18 | + | BotSwitch | |
| 19 | + | id: everyItem | |
| 20 | + | !text: tr("Loot every item") | |
| 21 | + | margin-top: 2 | |
| 22 | + | ||
| 23 | + | Label | |
| 24 | + | margin-top: 5 | |
| 25 | + | text: Containers for loot | |
| 26 | + | text-align: center | |
| 27 | + | ||
| 28 | + | BotContainer | |
| 29 | + | id: containers | |
| 30 | + | margin-top: 3 | |
| 31 | + | height: 45 | |
| 32 | + | ||
| 33 | + | Panel | |
| 34 | + | id: maxDangerPanel | |
| 35 | + | height: 20 | |
| 36 | + | margin-top: 5 | |
| 37 | + | ||
| 38 | + | BotTextEdit | |
| 39 | + | id: value | |
| 40 | + | anchors.right: parent.right | |
| 41 | + | anchors.top: parent.top | |
| 42 | + | anchors.bottom: parent.bottom | |
| 43 | + | margin-right: 6 | |
| 44 | + | width: 80 | |
| 45 | + | ||
| 46 | + | Label | |
| 47 | + | anchors.left: parent.left | |
| 48 | + | anchors.verticalCenter: prev.verticalCenter | |
| 49 | + | text: Max. danger: | |
| 50 | + | margin-left: 5 | |
| 51 | + | ||
| 52 | + | Panel | |
| 53 | + | id: minCapacityPanel | |
| 54 | + | height: 20 | |
| 55 | + | margin-top: 3 | |
| 56 | + | ||
| 57 | + | BotTextEdit | |
| 58 | + | id: value | |
| 59 | + | anchors.right: parent.right | |
| 60 | + | anchors.top: parent.top | |
| 61 | + | anchors.bottom: parent.bottom | |
| 62 | + | margin-right: 6 | |
| 63 | + | width: 80 | |
| 64 | + | ||
| 65 | + | Label | |
| 66 | + | anchors.left: parent.left | |
| 67 | + | anchors.verticalCenter: prev.verticalCenter | |
| 68 | + | text: Min. capacity: | |
| 69 | + | margin-left: 5 | |
| 70 | + | ||
| 71 | + | Label | |
| 72 | + | margin-top: 3 | |
| 73 | + | margin-left: 20 | |
| 74 | + | margin-right: 20 | |
| 75 | + | !text: tr("Drag item or click on any of empty slot") | |
| 76 | + | text-align: center | |
| 77 | + | text-wrap: true | |
| 78 | + | text-auto-resize: true | |
| 79 | + | ||
| 80 | + | BotButton | |
| 81 | + | margin-top: 3 | |
| 82 | + | text: Help & Tutorials | |
| 83 | + | @onClick: g_platform.openUrl("http://bot.otclient.ovh/") |
| @@ -0,0 +1,267 @@ | |||
| 1 | + | local targetbotMacro = nil | |
| 2 | + | local config = nil | |
| 3 | + | local lastAction = 0 | |
| 4 | + | local cavebotAllowance = 0 | |
| 5 | + | ||
| 6 | + | -- ui | |
| 7 | + | local configWidget = UI.Config() | |
| 8 | + | local ui = UI.createWidget("TargetBotPanel") | |
| 9 | + | ||
| 10 | + | ui.list = ui.listPanel.list -- shortcut | |
| 11 | + | TargetBot.targetList = ui.list | |
| 12 | + | TargetBot.Looting.setup() | |
| 13 | + | ||
| 14 | + | ui.status.left:setText("Status:") | |
| 15 | + | ui.status.right:setText("Off") | |
| 16 | + | ui.target.left:setText("Target:") | |
| 17 | + | ui.target.right:setText("-") | |
| 18 | + | ui.config.left:setText("Config:") | |
| 19 | + | ui.config.right:setText("-") | |
| 20 | + | ui.danger.left:setText("Danger:") | |
| 21 | + | ui.danger.right:setText("0") | |
| 22 | + | ||
| 23 | + | ui.editor.debug.onClick = function() | |
| 24 | + | local on = ui.editor.debug:isOn() | |
| 25 | + | ui.editor.debug:setOn(not on) | |
| 26 | + | if on then | |
| 27 | + | for _, spec in ipairs(getSpectators()) do | |
| 28 | + | spec:clearText() | |
| 29 | + | end | |
| 30 | + | end | |
| 31 | + | end | |
| 32 | + | ||
| 33 | + | -- main loop, controlled by config | |
| 34 | + | targetbotMacro = macro(100, function() | |
| 35 | + | local pos = player:getPosition() | |
| 36 | + | local creatures = g_map.getSpectatorsInRange(pos, false, 6, 6) -- 12x12 area | |
| 37 | + | if #creatures > 10 then -- if there are too many monsters around, limit area | |
| 38 | + | creatures = g_map.getSpectatorsInRange(pos, false, 3, 3) -- 6x6 area | |
| 39 | + | end | |
| 40 | + | local highestPriority = 0 | |
| 41 | + | local dangerLevel = 0 | |
| 42 | + | local targets = 0 | |
| 43 | + | local highestPriorityParams = nil | |
| 44 | + | for i, creature in ipairs(creatures) do | |
| 45 | + | local path = findPath(player:getPosition(), creature:getPosition(), 7, {ignoreLastCreature=true, ignoreNonPathable=true, ignoreCost=true}) | |
| 46 | + | if creature:isMonster() and path then | |
| 47 | + | local params = TargetBot.Creature.calculateParams(creature, path) -- return {craeture, config, danger, priority} | |
| 48 | + | dangerLevel = dangerLevel + params.danger | |
| 49 | + | if params.priority > 0 then | |
| 50 | + | targets = targets + 1 | |
| 51 | + | if params.priority > highestPriority then | |
| 52 | + | highestPriority = params.priority | |
| 53 | + | highestPriorityParams = params | |
| 54 | + | end | |
| 55 | + | if ui.editor.debug:isOn() then | |
| 56 | + | creature:setText(params.config.name .. "\n" .. params.priority) | |
| 57 | + | end | |
| 58 | + | end | |
| 59 | + | end | |
| 60 | + | end | |
| 61 | + | ||
| 62 | + | -- reset walking | |
| 63 | + | TargetBot.walkTo(nil) | |
| 64 | + | ||
| 65 | + | -- looting | |
| 66 | + | local looting = TargetBot.Looting.process(targets, dangerLevel) | |
| 67 | + | local lootingStatus = TargetBot.Looting.getStatus() | |
| 68 | + | ||
| 69 | + | ui.danger.right:setText(dangerLevel) | |
| 70 | + | if highestPriorityParams and not isInPz() then | |
| 71 | + | ui.target.right:setText(highestPriorityParams.creature:getName()) | |
| 72 | + | ui.config.right:setText(highestPriorityParams.config.name) | |
| 73 | + | TargetBot.Creature.attack(highestPriorityParams, targets, looting) | |
| 74 | + | if lootingStatus:len() > 0 then | |
| 75 | + | TargetBot.setStatus("Attack & " .. lootingStatus) | |
| 76 | + | elseif cavebotAllowance > now then | |
| 77 | + | TargetBot.setStatus("Luring using CaveBot") | |
| 78 | + | else | |
| 79 | + | TargetBot.setStatus("Attacking") | |
| 80 | + | end | |
| 81 | + | TargetBot.walk() | |
| 82 | + | lastAction = now | |
| 83 | + | return | |
| 84 | + | end | |
| 85 | + | ||
| 86 | + | ui.target.right:setText("-") | |
| 87 | + | ui.config.right:setText("-") | |
| 88 | + | if looting then | |
| 89 | + | TargetBot.walk() | |
| 90 | + | lastAction = now | |
| 91 | + | end | |
| 92 | + | if lootingStatus:len() > 0 then | |
| 93 | + | TargetBot.setStatus(lootingStatus) | |
| 94 | + | else | |
| 95 | + | TargetBot.setStatus("Waiting") | |
| 96 | + | end | |
| 97 | + | end) | |
| 98 | + | ||
| 99 | + | -- config, its callback is called immediately, data can be nil | |
| 100 | + | config = Config.setup("targetbot_configs", configWidget, "json", function(name, enabled, data) | |
| 101 | + | if not data then | |
| 102 | + | ui.status.right:setText("Off") | |
| 103 | + | return targetbotMacro.setOff() | |
| 104 | + | end | |
| 105 | + | TargetBot.Creature.resetConfigs() | |
| 106 | + | for _, value in ipairs(data["targeting"] or {}) do | |
| 107 | + | TargetBot.Creature.addConfig(value) | |
| 108 | + | end | |
| 109 | + | TargetBot.Looting.update(data["looting"] or {}) | |
| 110 | + | ||
| 111 | + | -- add configs | |
| 112 | + | if enabled then | |
| 113 | + | ui.status.right:setText("On") | |
| 114 | + | else | |
| 115 | + | ui.status.right:setText("Off") | |
| 116 | + | end | |
| 117 | + | ||
| 118 | + | targetbotMacro.setOn(enabled) | |
| 119 | + | targetbotMacro.delay = nil | |
| 120 | + | end) | |
| 121 | + | ||
| 122 | + | -- setup ui | |
| 123 | + | ui.editor.buttons.add.onClick = function() | |
| 124 | + | TargetBot.Creature.edit(nil, function(newConfig) | |
| 125 | + | TargetBot.Creature.addConfig(newConfig, true) | |
| 126 | + | TargetBot.save() | |
| 127 | + | end) | |
| 128 | + | end | |
| 129 | + | ||
| 130 | + | ui.editor.buttons.edit.onClick = function() | |
| 131 | + | local entry = ui.list:getFocusedChild() | |
| 132 | + | if not entry then return end | |
| 133 | + | TargetBot.Creature.edit(entry.value, function(newConfig) | |
| 134 | + | entry:setText(newConfig.name) | |
| 135 | + | entry.value = newConfig | |
| 136 | + | TargetBot.Creature.resetConfigsCache() | |
| 137 | + | TargetBot.save() | |
| 138 | + | end) | |
| 139 | + | end | |
| 140 | + | ||
| 141 | + | ui.editor.buttons.remove.onClick = function() | |
| 142 | + | local entry = ui.list:getFocusedChild() | |
| 143 | + | if not entry then return end | |
| 144 | + | entry:destroy() | |
| 145 | + | TargetBot.Creature.resetConfigsCache() | |
| 146 | + | TargetBot.save() | |
| 147 | + | end | |
| 148 | + | ||
| 149 | + | -- public function, you can use them in your scripts | |
| 150 | + | TargetBot.isActive = function() -- return true if attacking or looting takes place | |
| 151 | + | return lastAction + 300 > now | |
| 152 | + | end | |
| 153 | + | ||
| 154 | + | TargetBot.isCaveBotActionAllowed = function() | |
| 155 | + | return cavebotAllowance > now | |
| 156 | + | end | |
| 157 | + | ||
| 158 | + | TargetBot.setStatus = function(text) | |
| 159 | + | return ui.status.right:setText(text) | |
| 160 | + | end | |
| 161 | + | ||
| 162 | + | TargetBot.isOn = function() | |
| 163 | + | return config.isOn() | |
| 164 | + | end | |
| 165 | + | ||
| 166 | + | TargetBot.isOff = function() | |
| 167 | + | return config.isOff() | |
| 168 | + | end | |
| 169 | + | ||
| 170 | + | TargetBot.setOn = function(val) | |
| 171 | + | if val == false then | |
| 172 | + | return TargetBot.setOff(true) | |
| 173 | + | end | |
| 174 | + | config.setOn() | |
| 175 | + | end | |
| 176 | + | ||
| 177 | + | TargetBot.setOff = function(val) | |
| 178 | + | if val == false then | |
| 179 | + | return TargetBot.setOn(true) | |
| 180 | + | end | |
| 181 | + | config.setOff() | |
| 182 | + | end | |
| 183 | + | ||
| 184 | + | TargetBot.delay = function(value) | |
| 185 | + | targetbotMacro.delay = now + value | |
| 186 | + | end | |
| 187 | + | ||
| 188 | + | TargetBot.save = function() | |
| 189 | + | local data = {targeting={}, looting={}} | |
| 190 | + | for _, entry in ipairs(ui.list:getChildren()) do | |
| 191 | + | table.insert(data.targeting, entry.value) | |
| 192 | + | end | |
| 193 | + | TargetBot.Looting.save(data.looting) | |
| 194 | + | config.save(data) | |
| 195 | + | end | |
| 196 | + | ||
| 197 | + | TargetBot.allowCaveBot = function(time) | |
| 198 | + | cavebotAllowance = now + time | |
| 199 | + | end | |
| 200 | + | ||
| 201 | + | -- attacks | |
| 202 | + | local lastSpell = 0 | |
| 203 | + | local lastAttackSpell = 0 | |
| 204 | + | ||
| 205 | + | TargetBot.saySpell = function(text, delay) | |
| 206 | + | if type(text) ~= 'string' or text:len() < 1 then return end | |
| 207 | + | if not delay then delay = 500 end | |
| 208 | + | if g_game.getProtocolVersion() < 1090 then | |
| 209 | + | lastAttackSpell = now -- pause attack spells, healing spells are more important | |
| 210 | + | end | |
| 211 | + | if lastSpell + delay < now then | |
| 212 | + | say(text) | |
| 213 | + | lastSpell = now | |
| 214 | + | return true | |
| 215 | + | end | |
| 216 | + | return false | |
| 217 | + | end | |
| 218 | + | ||
| 219 | + | TargetBot.sayAttackSpell = function(text, delay) | |
| 220 | + | if type(text) ~= 'string' or text:len() < 1 then return end | |
| 221 | + | if not delay then delay = 2000 end | |
| 222 | + | if lastAttackSpell + delay < now then | |
| 223 | + | say(text) | |
| 224 | + | lastAttackSpell = now | |
| 225 | + | return true | |
| 226 | + | end | |
| 227 | + | return false | |
| 228 | + | end | |
| 229 | + | ||
| 230 | + | local lastItemUse = 0 | |
| 231 | + | local lastRuneAttack = 0 | |
| 232 | + | ||
| 233 | + | TargetBot.useItem = function(item, subType, target, delay) | |
| 234 | + | if not delay then delay = 200 end | |
| 235 | + | if lastItemUse + delay < now then | |
| 236 | + | local thing = g_things.getThingType(item) | |
| 237 | + | if not thing or not thing:isFluidContainer() then | |
| 238 | + | subType = g_game.getClientVersion() >= 860 and 0 or 1 | |
| 239 | + | end | |
| 240 | + | if g_game.getClientVersion() < 780 then | |
| 241 | + | local tmpItem = g_game.findPlayerItem(item, subType) | |
| 242 | + | if not tmpItem then return end | |
| 243 | + | g_game.useWith(tmpItem, target, subType) -- using item from bp | |
| 244 | + | else | |
| 245 | + | g_game.useInventoryItemWith(item, target, subType) -- hotkey | |
| 246 | + | end | |
| 247 | + | lastItemUse = now | |
| 248 | + | end | |
| 249 | + | end | |
| 250 | + | ||
| 251 | + | TargetBot.useAttackItem = function(item, subType, target, delay) | |
| 252 | + | if not delay then delay = 2000 end | |
| 253 | + | if lastRuneAttack + delay < now then | |
| 254 | + | local thing = g_things.getThingType(item) | |
| 255 | + | if not thing or not thing:isFluidContainer() then | |
| 256 | + | subType = g_game.getClientVersion() >= 860 and 0 or 1 | |
| 257 | + | end | |
| 258 | + | if g_game.getClientVersion() < 780 then | |
| 259 | + | local tmpItem = g_game.findPlayerItem(item, subType) | |
| 260 | + | if not tmpItem then return end | |
| 261 | + | g_game.useWith(tmpItem, target, subType) -- using item from bp | |
| 262 | + | else | |
| 263 | + | g_game.useInventoryItemWith(item, target, subType) -- hotkey | |
| 264 | + | end | |
| 265 | + | lastRuneAttack = now | |
| 266 | + | end | |
| 267 | + | end |
| @@ -0,0 +1,115 @@ | |||
| 1 | + | TargetBotEntry < Label | |
| 2 | + | background-color: alpha | |
| 3 | + | text-offset: 2 0 | |
| 4 | + | focusable: true | |
| 5 | + | ||
| 6 | + | $focus: | |
| 7 | + | background-color: #00000055 | |
| 8 | + | ||
| 9 | + | TargetBotDualLabel < Panel | |
| 10 | + | height: 18 | |
| 11 | + | margin-left: 3 | |
| 12 | + | margin-right: 4 | |
| 13 | + | ||
| 14 | + | Label | |
| 15 | + | id: left | |
| 16 | + | anchors.top: parent.top | |
| 17 | + | anchors.left: parent.left | |
| 18 | + | text-auto-resize: true | |
| 19 | + | ||
| 20 | + | Label | |
| 21 | + | id: right | |
| 22 | + | anchors.top: parent.top | |
| 23 | + | anchors.right: parent.right | |
| 24 | + | text-auto-resize: true | |
| 25 | + | ||
| 26 | + | TargetBotPanel < Panel | |
| 27 | + | layout: | |
| 28 | + | type: verticalBox | |
| 29 | + | fit-children: true | |
| 30 | + | ||
| 31 | + | HorizontalSeparator | |
| 32 | + | margin-top: 2 | |
| 33 | + | margin-bottom: 5 | |
| 34 | + | ||
| 35 | + | TargetBotDualLabel | |
| 36 | + | id: status | |
| 37 | + | TargetBotDualLabel | |
| 38 | + | id: target | |
| 39 | + | TargetBotDualLabel | |
| 40 | + | id: config | |
| 41 | + | TargetBotDualLabel | |
| 42 | + | id: danger | |
| 43 | + | ||
| 44 | + | Panel | |
| 45 | + | id: listPanel | |
| 46 | + | height: 40 | |
| 47 | + | ||
| 48 | + | TextList | |
| 49 | + | id: list | |
| 50 | + | anchors.fill: parent | |
| 51 | + | vertical-scrollbar: listScrollbar | |
| 52 | + | margin-right: 15 | |
| 53 | + | focusable: false | |
| 54 | + | auto-focus: first | |
| 55 | + | ||
| 56 | + | VerticalScrollBar | |
| 57 | + | id: listScrollbar | |
| 58 | + | anchors.top: parent.top | |
| 59 | + | anchors.bottom: parent.bottom | |
| 60 | + | anchors.right: parent.right | |
| 61 | + | pixels-scroll: true | |
| 62 | + | step: 10 | |
| 63 | + | ||
| 64 | + | BotSwitch | |
| 65 | + | id: configButton | |
| 66 | + | @onClick: | | |
| 67 | + | self:setOn(not self:isOn()) | |
| 68 | + | self:getParent().listPanel:setHeight(self:isOn() and 100 or 40) | |
| 69 | + | self:getParent().editor:setVisible(self:isOn()) | |
| 70 | + | ||
| 71 | + | $on: | |
| 72 | + | text: Hide target editor | |
| 73 | + | ||
| 74 | + | $!on: | |
| 75 | + | text: Show target editor | |
| 76 | + | ||
| 77 | + | Panel | |
| 78 | + | id: editor | |
| 79 | + | visible: false | |
| 80 | + | layout: | |
| 81 | + | type: verticalBox | |
| 82 | + | fit-children: true | |
| 83 | + | ||
| 84 | + | Panel | |
| 85 | + | id: buttons | |
| 86 | + | height: 20 | |
| 87 | + | margin-top: 2 | |
| 88 | + | ||
| 89 | + | Button | |
| 90 | + | id: add | |
| 91 | + | anchors.top: parent.top | |
| 92 | + | anchors.bottom: parent.bottom | |
| 93 | + | anchors.left: parent.left | |
| 94 | + | text: Add | |
| 95 | + | width: 56 | |
| 96 | + | ||
| 97 | + | Button | |
| 98 | + | id: edit | |
| 99 | + | anchors.top: parent.top | |
| 100 | + | anchors.bottom: parent.bottom | |
| 101 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 102 | + | text: Edit | |
| 103 | + | width: 56 | |
| 104 | + | ||
| 105 | + | Button | |
| 106 | + | id: remove | |
| 107 | + | anchors.top: parent.top | |
| 108 | + | anchors.bottom: parent.bottom | |
| 109 | + | anchors.right: parent.right | |
| 110 | + | text: Remove | |
| 111 | + | width: 56 | |
| 112 | + | ||
| 113 | + | BotSwitch | |
| 114 | + | id: debug | |
| 115 | + | text: Show target priority |
| @@ -0,0 +1,28 @@ | |||
| 1 | + | local dest | |
| 2 | + | local maxDist | |
| 3 | + | local params | |
| 4 | + | ||
| 5 | + | TargetBot.walkTo = function(_dest, _maxDist, _params) | |
| 6 | + | dest = _dest | |
| 7 | + | maxDist = _maxDist | |
| 8 | + | params = _params | |
| 9 | + | end | |
| 10 | + | ||
| 11 | + | -- called every 100ms if targeting or looting is active | |
| 12 | + | TargetBot.walk = function() | |
| 13 | + | if not dest then return end | |
| 14 | + | if player:isWalking() then return end | |
| 15 | + | local pos = player:getPosition() | |
| 16 | + | if pos.z ~= dest.z then return end | |
| 17 | + | local dist = math.max(math.abs(pos.x-dest.x), math.abs(pos.y-dest.y)) | |
| 18 | + | if params.precision and params.precision >= dist then return end | |
| 19 | + | if params.marginMin and params.marginMax then | |
| 20 | + | if dist >= params.marginMin and dist <= params.marginMax then | |
| 21 | + | return | |
| 22 | + | end | |
| 23 | + | end | |
| 24 | + | local path = getPath(pos, dest, maxDist, params) | |
| 25 | + | if path then | |
| 26 | + | walk(path[1]) | |
| 27 | + | end | |
| 28 | + | end |
| @@ -0,0 +1,147 @@ | |||
| 1 | + | -- tools tab | |
| 2 | + | setDefaultTab("Tools") | |
| 3 | + | ||
| 4 | + | -- allows to test/edit bot lua scripts ingame, you can have multiple scripts like this, just change storage.ingame_lua | |
| 5 | + | UI.Button("Ingame macro editor", function(newText) | |
| 6 | + | UI.MultilineEditorWindow(storage.ingame_macros or "", {title="Macro editor", description="You can add your custom macros (or any other lua code) here"}, function(text) | |
| 7 | + | storage.ingame_macros = text | |
| 8 | + | reload() | |
| 9 | + | end) | |
| 10 | + | end) | |
| 11 | + | UI.Button("Ingame hotkey editor", function(newText) | |
| 12 | + | UI.MultilineEditorWindow(storage.ingame_hotkeys or "", {title="Hotkeys editor", description="You can add your custom hotkeys/singlehotkeys here"}, function(text) | |
| 13 | + | storage.ingame_hotkeys = text | |
| 14 | + | reload() | |
| 15 | + | end) | |
| 16 | + | end) | |
| 17 | + | ||
| 18 | + | UI.Separator() | |
| 19 | + | ||
| 20 | + | for _, scripts in ipairs({storage.ingame_macros, storage.ingame_hotkeys}) do | |
| 21 | + | if type(scripts) == "string" and scripts:len() > 3 then | |
| 22 | + | local status, result = pcall(function() | |
| 23 | + | assert(load(scripts, "ingame_editor"))() | |
| 24 | + | end) | |
| 25 | + | if not status then | |
| 26 | + | error("Ingame edior error:\n" .. result) | |
| 27 | + | end | |
| 28 | + | end | |
| 29 | + | end | |
| 30 | + | ||
| 31 | + | UI.Separator() | |
| 32 | + | ||
| 33 | + | UI.Button("Zoom In map [ctrl + =]", function() zoomIn() end) | |
| 34 | + | UI.Button("Zoom Out map [ctrl + -]", function() zoomOut() end) | |
| 35 | + | ||
| 36 | + | UI.Separator() | |
| 37 | + | ||
| 38 | + | local moneyIds = {3031, 3035} -- gold coin, platinium coin | |
| 39 | + | macro(1000, "Exchange money", function() | |
| 40 | + | local containers = g_game.getContainers() | |
| 41 | + | for index, container in pairs(containers) do | |
| 42 | + | if not container.lootContainer then -- ignore monster containers | |
| 43 | + | for i, item in ipairs(container:getItems()) do | |
| 44 | + | if item:getCount() == 100 then | |
| 45 | + | for m, moneyId in ipairs(moneyIds) do | |
| 46 | + | if item:getId() == moneyId then | |
| 47 | + | return g_game.use(item) | |
| 48 | + | end | |
| 49 | + | end | |
| 50 | + | end | |
| 51 | + | end | |
| 52 | + | end | |
| 53 | + | end | |
| 54 | + | end) | |
| 55 | + | ||
| 56 | + | macro(1000, "Stack items", function() | |
| 57 | + | local containers = g_game.getContainers() | |
| 58 | + | local toStack = {} | |
| 59 | + | for index, container in pairs(containers) do | |
| 60 | + | if not container.lootContainer then -- ignore monster containers | |
| 61 | + | for i, item in ipairs(container:getItems()) do | |
| 62 | + | if item:isStackable() and item:getCount() < 100 then | |
| 63 | + | local stackWith = toStack[item:getId()] | |
| 64 | + | if stackWith then | |
| 65 | + | g_game.move(item, stackWith[1], math.min(stackWith[2], item:getCount())) | |
| 66 | + | return | |
| 67 | + | end | |
| 68 | + | toStack[item:getId()] = {container:getSlotPosition(i - 1), 100 - item:getCount()} | |
| 69 | + | end | |
| 70 | + | end | |
| 71 | + | end | |
| 72 | + | end | |
| 73 | + | end) | |
| 74 | + | ||
| 75 | + | macro(10000, "Anti Kick", function() | |
| 76 | + | local dir = player:getDirection() | |
| 77 | + | turn((dir + 1) % 4) | |
| 78 | + | turn(dir) | |
| 79 | + | end) | |
| 80 | + | ||
| 81 | + | UI.Separator() | |
| 82 | + | UI.Label("Drop items:") | |
| 83 | + | if type(storage.dropItems) ~= "table" then | |
| 84 | + | storage.dropItems = {283, 284, 285} | |
| 85 | + | end | |
| 86 | + | ||
| 87 | + | local foodContainer = UI.Container(function(widget, items) | |
| 88 | + | storage.dropItems = items | |
| 89 | + | end, true) | |
| 90 | + | foodContainer:setHeight(35) | |
| 91 | + | foodContainer:setItems(storage.dropItems) | |
| 92 | + | ||
| 93 | + | macro(5000, "drop items", function() | |
| 94 | + | if not storage.dropItems[1] then return end | |
| 95 | + | if TargetBot and TargetBot.isActive() then return end -- pause when attacking | |
| 96 | + | for _, container in pairs(g_game.getContainers()) do | |
| 97 | + | for __, item in ipairs(container:getItems()) do | |
| 98 | + | for i, dropItem in ipairs(storage.dropItems) do | |
| 99 | + | if item:getId() == dropItem.id then | |
| 100 | + | if item:isStackable() then | |
| 101 | + | return g_game.move(item, player:getPosition(), item:getCount()) | |
| 102 | + | else | |
| 103 | + | return g_game.move(item, player:getPosition(), dropItem.count) -- count is also subtype | |
| 104 | + | end | |
| 105 | + | end | |
| 106 | + | end | |
| 107 | + | end | |
| 108 | + | end | |
| 109 | + | end) | |
| 110 | + | ||
| 111 | + | UI.Separator() | |
| 112 | + | ||
| 113 | + | UI.Label("Mana training") | |
| 114 | + | if type(storage.manaTrain) ~= "table" then | |
| 115 | + | storage.manaTrain = {on=false, title="MP%", text="utevo lux", min=80, max=100} | |
| 116 | + | end | |
| 117 | + | ||
| 118 | + | local manatrainmacro = macro(1000, function() | |
| 119 | + | if TargetBot and TargetBot.isActive() then return end -- pause when attacking | |
| 120 | + | local mana = math.min(100, math.floor(100 * (player:getMana() / player:getMaxMana()))) | |
| 121 | + | if storage.manaTrain.max >= mana and mana >= storage.manaTrain.min then | |
| 122 | + | say(storage.manaTrain.text) | |
| 123 | + | end | |
| 124 | + | end) | |
| 125 | + | manatrainmacro.setOn(storage.manaTrain.on) | |
| 126 | + | ||
| 127 | + | UI.DualScrollPanel(storage.manaTrain, function(widget, newParams) | |
| 128 | + | storage.manaTrain = newParams | |
| 129 | + | manatrainmacro.setOn(storage.manaTrain.on) | |
| 130 | + | end) | |
| 131 | + | ||
| 132 | + | UI.Separator() | |
| 133 | + | ||
| 134 | + | macro(60000, "Send message on trade", function() | |
| 135 | + | local trade = getChannelId("advertising") | |
| 136 | + | if not trade then | |
| 137 | + | trade = getChannelId("trade") | |
| 138 | + | end | |
| 139 | + | if trade and storage.autoTradeMessage:len() > 0 then | |
| 140 | + | sayChannel(trade, storage.autoTradeMessage) | |
| 141 | + | end | |
| 142 | + | end) | |
| 143 | + | UI.TextEdit(storage.autoTradeMessage or "I'm using OTClientV8!", function(widget, text) | |
| 144 | + | storage.autoTradeMessage = text | |
| 145 | + | end) | |
| 146 | + | ||
| 147 | + | UI.Separator() |
| @@ -0,0 +1,254 @@ | |||
| 1 | + | MainWindow | |
| 2 | + | id: editWindow | |
| 3 | + | !text: tr("Config editor & manager") | |
| 4 | + | @onEscape: self:hide() | |
| 5 | + | size: 550 570 | |
| 6 | + | $mobile: | |
| 7 | + | size: 550 240 | |
| 8 | + | ||
| 9 | + | Panel | |
| 10 | + | id: manager | |
| 11 | + | anchors.top: parent.top | |
| 12 | + | anchors.left: parent.left | |
| 13 | + | anchors.right: parent.right | |
| 14 | + | height: 152 | |
| 15 | + | ||
| 16 | + | Label | |
| 17 | + | anchors.top: parent.top | |
| 18 | + | anchors.left: parent.left | |
| 19 | + | anchors.right: parent.right | |
| 20 | + | text-auto-resize: true | |
| 21 | + | text-align: center | |
| 22 | + | text-wrap: true | |
| 23 | + | !text: tr("Config Manager\nYou can use config manager to share configs between different machines, especially smartphones. After you configure your config, you can upload it, then you'll get unique hash code which you can use on diffent machinge (for eg. mobile phone) to download it.") | |
| 24 | + | ||
| 25 | + | HorizontalSeparator | |
| 26 | + | anchors.top: prev.bottom | |
| 27 | + | anchors.left: parent.left | |
| 28 | + | anchors.right: parent.right | |
| 29 | + | margin-top: 3 | |
| 30 | + | height: 2 | |
| 31 | + | ||
| 32 | + | Panel | |
| 33 | + | id: upload | |
| 34 | + | anchors.top: prev.bottom | |
| 35 | + | anchors.left: parent.left | |
| 36 | + | anchors.right: parent.horizontalCenter | |
| 37 | + | anchors.bottom: parent.bottom | |
| 38 | + | margin-top: 3 | |
| 39 | + | ||
| 40 | + | Label | |
| 41 | + | anchors.top: parent.top | |
| 42 | + | anchors.left: parent.left | |
| 43 | + | anchors.right: parent.right | |
| 44 | + | text-auto-resize: true | |
| 45 | + | text-align: center | |
| 46 | + | text-wrap: true | |
| 47 | + | !text: tr("Upload config") | |
| 48 | + | ||
| 49 | + | Label | |
| 50 | + | anchors.top: prev.bottom | |
| 51 | + | anchors.left: parent.left | |
| 52 | + | anchors.right: parent.right | |
| 53 | + | margin-top: 7 | |
| 54 | + | text-auto-resize: true | |
| 55 | + | text-align: center | |
| 56 | + | text-wrap: true | |
| 57 | + | !text: tr("Select config to upload") | |
| 58 | + | ||
| 59 | + | ComboBox | |
| 60 | + | id: config | |
| 61 | + | anchors.top: prev.bottom | |
| 62 | + | anchors.left: parent.left | |
| 63 | + | anchors.right: parent.right | |
| 64 | + | margin-top: 4 | |
| 65 | + | margin-left: 20 | |
| 66 | + | margin-right: 20 | |
| 67 | + | text-offset: 3 0 | |
| 68 | + | ||
| 69 | + | Button | |
| 70 | + | id: submit | |
| 71 | + | anchors.top: prev.bottom | |
| 72 | + | anchors.left: parent.left | |
| 73 | + | anchors.right: parent.right | |
| 74 | + | !text: tr('Upload config') | |
| 75 | + | margin-top: 4 | |
| 76 | + | margin-left: 40 | |
| 77 | + | margin-right: 40 | |
| 78 | + | @onClick: modules.game_bot.uploadConfig() | |
| 79 | + | ||
| 80 | + | Panel | |
| 81 | + | id: download | |
| 82 | + | anchors.top: prev.top | |
| 83 | + | anchors.left: parent.horizontalCenter | |
| 84 | + | anchors.right: parent.right | |
| 85 | + | anchors.bottom: parent.bottom | |
| 86 | + | ||
| 87 | + | Label | |
| 88 | + | anchors.top: parent.top | |
| 89 | + | anchors.left: parent.left | |
| 90 | + | anchors.right: parent.right | |
| 91 | + | text-auto-resize: true | |
| 92 | + | text-align: center | |
| 93 | + | text-wrap: true | |
| 94 | + | !text: tr("Download config") | |
| 95 | + | ||
| 96 | + | Label | |
| 97 | + | anchors.top: prev.bottom | |
| 98 | + | anchors.left: parent.left | |
| 99 | + | anchors.right: parent.right | |
| 100 | + | margin-top: 7 | |
| 101 | + | text-auto-resize: true | |
| 102 | + | text-align: center | |
| 103 | + | text-wrap: true | |
| 104 | + | !text: tr("Enter config hash code") | |
| 105 | + | ||
| 106 | + | TextEdit | |
| 107 | + | id: config | |
| 108 | + | anchors.top: prev.bottom | |
| 109 | + | anchors.left: parent.left | |
| 110 | + | anchors.right: parent.right | |
| 111 | + | margin-top: 4 | |
| 112 | + | margin-left: 20 | |
| 113 | + | margin-right: 20 | |
| 114 | + | ||
| 115 | + | Button | |
| 116 | + | id: submit | |
| 117 | + | anchors.top: prev.bottom | |
| 118 | + | anchors.left: parent.left | |
| 119 | + | anchors.right: parent.right | |
| 120 | + | !text: tr('Download config') | |
| 121 | + | margin-top: 4 | |
| 122 | + | margin-left: 40 | |
| 123 | + | margin-right: 40 | |
| 124 | + | @onClick: modules.game_bot.downloadConfig() | |
| 125 | + | ||
| 126 | + | HorizontalSeparator | |
| 127 | + | anchors.top: prev.bottom | |
| 128 | + | anchors.left: parent.left | |
| 129 | + | anchors.right: parent.right | |
| 130 | + | margin-top: 3 | |
| 131 | + | height: 2 | |
| 132 | + | ||
| 133 | + | Panel | |
| 134 | + | anchors.top: prev.bottom | |
| 135 | + | anchors.left: parent.left | |
| 136 | + | anchors.right: parent.right | |
| 137 | + | margin-top: 5 | |
| 138 | + | height: 330 | |
| 139 | + | $mobile: | |
| 140 | + | visible: false | |
| 141 | + | ||
| 142 | + | Label | |
| 143 | + | anchors.top: parent.top | |
| 144 | + | anchors.left: parent.left | |
| 145 | + | anchors.right: parent.right | |
| 146 | + | text-auto-resize: true | |
| 147 | + | text-align: center | |
| 148 | + | text-wrap: true | |
| 149 | + | !text: tr("Bot configs are stored in:") | |
| 150 | + | ||
| 151 | + | TextEdit | |
| 152 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 153 | + | anchors.top: prev.bottom | |
| 154 | + | height: 20 | |
| 155 | + | width: 400 | |
| 156 | + | margin-top: 5 | |
| 157 | + | editable: false | |
| 158 | + | !text: g_resources.getWriteDir() .. "bot" | |
| 159 | + | text-align: center | |
| 160 | + | ||
| 161 | + | Button | |
| 162 | + | id: documentationButton | |
| 163 | + | !text: tr('Click here to open bot directory') | |
| 164 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 165 | + | anchors.top: prev.bottom | |
| 166 | + | margin-top: 5 | |
| 167 | + | width: 250 | |
| 168 | + | @onClick: g_platform.openDir(g_resources.getWriteDir() .. "bot") | |
| 169 | + | ||
| 170 | + | Label | |
| 171 | + | margin-top: 5 | |
| 172 | + | anchors.top: prev.bottom | |
| 173 | + | anchors.left: parent.left | |
| 174 | + | anchors.right: parent.right | |
| 175 | + | text-auto-resize: true | |
| 176 | + | text-align: center | |
| 177 | + | text-wrap: true | |
| 178 | + | !text: tr("Every directory in bot directory is treated as different config.\nTo create new config just create new directory.") | |
| 179 | + | ||
| 180 | + | Label | |
| 181 | + | margin-top: 5 | |
| 182 | + | anchors.top: prev.bottom | |
| 183 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 184 | + | height: 175 | |
| 185 | + | image-source: configs.png | |
| 186 | + | image-fixed-ratio: true | |
| 187 | + | image-size: 500 175 | |
| 188 | + | ||
| 189 | + | Label | |
| 190 | + | margin-top: 3 | |
| 191 | + | anchors.top: prev.bottom | |
| 192 | + | anchors.left: parent.left | |
| 193 | + | anchors.right: parent.right | |
| 194 | + | text-auto-resize: true | |
| 195 | + | text-align: center | |
| 196 | + | text-wrap: true | |
| 197 | + | !text: tr("Inside config directory put .lua and .otui files.\nEvery file will be loaded and executed in alphabetical order, .otui first and then .lua.") | |
| 198 | + | ||
| 199 | + | Label | |
| 200 | + | margin-top: 3 | |
| 201 | + | anchors.top: prev.bottom | |
| 202 | + | anchors.left: parent.left | |
| 203 | + | anchors.right: parent.right | |
| 204 | + | text-auto-resize: true | |
| 205 | + | text-align: center | |
| 206 | + | text-wrap: true | |
| 207 | + | !text: tr("To reload configs just press On and Off in bot window.\nTo learn more about bot click Tutorials button.") | |
| 208 | + | ||
| 209 | + | Button | |
| 210 | + | !text: tr('Documentation') | |
| 211 | + | anchors.bottom: parent.bottom | |
| 212 | + | anchors.left: parent.left | |
| 213 | + | width: 118 | |
| 214 | + | @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?documentation") | |
| 215 | + | ||
| 216 | + | Button | |
| 217 | + | !text: tr('Tutorials') | |
| 218 | + | anchors.bottom: parent.bottom | |
| 219 | + | anchors.left: prev.right | |
| 220 | + | margin-left: 5 | |
| 221 | + | width: 80 | |
| 222 | + | @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?tutorials") | |
| 223 | + | ||
| 224 | + | Button | |
| 225 | + | !text: tr('Scripts') | |
| 226 | + | anchors.bottom: parent.bottom | |
| 227 | + | anchors.left: prev.right | |
| 228 | + | margin-left: 5 | |
| 229 | + | width: 80 | |
| 230 | + | @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?scripts") | |
| 231 | + | ||
| 232 | + | Button | |
| 233 | + | !text: tr('Forum') | |
| 234 | + | anchors.bottom: parent.bottom | |
| 235 | + | anchors.left: prev.right | |
| 236 | + | margin-left: 5 | |
| 237 | + | width: 80 | |
| 238 | + | @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?forum") | |
| 239 | + | ||
| 240 | + | Button | |
| 241 | + | !text: tr('Discord') | |
| 242 | + | anchors.bottom: parent.bottom | |
| 243 | + | anchors.left: prev.right | |
| 244 | + | margin-left: 5 | |
| 245 | + | width: 80 | |
| 246 | + | @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?discord") | |
| 247 | + | ||
| 248 | + | Button | |
| 249 | + | id: cancelButton | |
| 250 | + | !text: tr('Close') | |
| 251 | + | anchors.bottom: parent.bottom | |
| 252 | + | anchors.right: parent.right | |
| 253 | + | width: 60 | |
| 254 | + | @onClick: self:getParent():hide() |
| @@ -0,0 +1,336 @@ | |||
| 1 | + | function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, reloadCallback, websockets) | |
| 2 | + | -- load lua and otui files | |
| 3 | + | local configFiles = g_resources.listDirectoryFiles("/bot/" .. config, true, false) | |
| 4 | + | local luaFiles = {} | |
| 5 | + | local uiFiles = {} | |
| 6 | + | for i, file in ipairs(configFiles) do | |
| 7 | + | local ext = file:split(".") | |
| 8 | + | if ext[#ext]:lower() == "lua" then | |
| 9 | + | table.insert(luaFiles, file) | |
| 10 | + | end | |
| 11 | + | if ext[#ext]:lower() == "ui" or ext[#ext]:lower() == "otui" then | |
| 12 | + | table.insert(uiFiles, file) | |
| 13 | + | end | |
| 14 | + | end | |
| 15 | + | ||
| 16 | + | if #luaFiles == 0 then | |
| 17 | + | return error("Config (/bot/" .. config .. ") doesn't have lua files") | |
| 18 | + | end | |
| 19 | + | ||
| 20 | + | -- init bot variables | |
| 21 | + | local context = {} | |
| 22 | + | context.configDir = "/bot/".. config | |
| 23 | + | context.tabs = tabs | |
| 24 | + | context.mainTab = context.tabs:addTab("Main", g_ui.createWidget('BotPanel')).tabPanel.content | |
| 25 | + | context.panel = context.mainTab | |
| 26 | + | context.saveConfig = saveConfigCallback | |
| 27 | + | context.reload = reloadCallback | |
| 28 | + | ||
| 29 | + | context.storage = storage | |
| 30 | + | if context.storage._macros == nil then | |
| 31 | + | context.storage._macros = {} -- active macros | |
| 32 | + | end | |
| 33 | + | ||
| 34 | + | -- websockets, macros, hotkeys, scheduler, icons, callbacks | |
| 35 | + | context._websockets = websockets | |
| 36 | + | context._macros = {} | |
| 37 | + | context._hotkeys = {} | |
| 38 | + | context._scheduler = {} | |
| 39 | + | context._callbacks = { | |
| 40 | + | onKeyDown = {}, | |
| 41 | + | onKeyUp = {}, | |
| 42 | + | onKeyPress = {}, | |
| 43 | + | onTalk = {}, | |
| 44 | + | onTextMessage = {}, | |
| 45 | + | onAddThing = {}, | |
| 46 | + | onRemoveThing = {}, | |
| 47 | + | onCreatureAppear = {}, | |
| 48 | + | onCreatureDisappear = {}, | |
| 49 | + | onCreaturePositionChange = {}, | |
| 50 | + | onCreatureHealthPercentChange = {}, | |
| 51 | + | onUse = {}, | |
| 52 | + | onUseWith = {}, | |
| 53 | + | onContainerOpen = {}, | |
| 54 | + | onContainerClose = {}, | |
| 55 | + | onContainerUpdateItem = {}, | |
| 56 | + | onMissle = {}, | |
| 57 | + | onAnimatedText = {}, | |
| 58 | + | onStaticText = {}, | |
| 59 | + | onChannelList = {}, | |
| 60 | + | onOpenChannel = {}, | |
| 61 | + | onCloseChannel = {}, | |
| 62 | + | onChannelEvent = {}, | |
| 63 | + | onTurn = {} | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | -- basic functions & classes | |
| 67 | + | context.print = print | |
| 68 | + | context.pairs = pairs | |
| 69 | + | context.ipairs = ipairs | |
| 70 | + | context.tostring = tostring | |
| 71 | + | context.math = math | |
| 72 | + | context.table = table | |
| 73 | + | context.string = string | |
| 74 | + | context.tonumber = tonumber | |
| 75 | + | context.type = type | |
| 76 | + | context.pcall = pcall | |
| 77 | + | context.os = { | |
| 78 | + | time = os.time, | |
| 79 | + | date = os.date, | |
| 80 | + | difftime = os.difftime, | |
| 81 | + | date = os.date, | |
| 82 | + | clock = os.clock | |
| 83 | + | } | |
| 84 | + | context.load = function(str) return assert(load(str, nil, nil, context)) end | |
| 85 | + | context.loadstring = context.load | |
| 86 | + | context.assert = assert | |
| 87 | + | context.dofile = function(file) assert(load(g_resources.readFileContents("/bot/" .. config .. "/" .. file), file, nil, context))() end | |
| 88 | + | context.gcinfo = gcinfo | |
| 89 | + | context.tr = tr | |
| 90 | + | context.json = json | |
| 91 | + | context.base64 = base64 | |
| 92 | + | context.regexMatch = regexMatch | |
| 93 | + | context.getDistanceBetween = function(p1, p2) | |
| 94 | + | return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) | |
| 95 | + | end | |
| 96 | + | context.isMobile = g_app.isMobile | |
| 97 | + | context.getVersion = g_app.getVersion | |
| 98 | + | ||
| 99 | + | -- classes | |
| 100 | + | context.g_resources = g_resources | |
| 101 | + | context.g_game = g_game | |
| 102 | + | context.g_map = g_map | |
| 103 | + | context.g_ui = g_ui | |
| 104 | + | context.g_sounds = g_sounds | |
| 105 | + | context.g_window = g_window | |
| 106 | + | context.g_mouse = g_mouse | |
| 107 | + | context.g_things = g_things | |
| 108 | + | context.g_platform = { | |
| 109 | + | openUrl = g_platform.openUrl, | |
| 110 | + | openDir = g_platform.openDir, | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | context.Item = Item | |
| 114 | + | context.Creature = Creature | |
| 115 | + | context.ThingType = ThingType | |
| 116 | + | context.Effect = Effect | |
| 117 | + | context.Missile = Missile | |
| 118 | + | context.Player = Player | |
| 119 | + | context.Monster = Monster | |
| 120 | + | context.StaticText = StaticText | |
| 121 | + | context.HTTP = HTTP | |
| 122 | + | context.OutputMessage = OutputMessage | |
| 123 | + | context.modules = modules | |
| 124 | + | ||
| 125 | + | -- log functions | |
| 126 | + | context.info = function(text) return msgCallback("info", tostring(text)) end | |
| 127 | + | context.warn = function(text) return msgCallback("warn", tostring(text)) end | |
| 128 | + | context.error = function(text) return msgCallback("error", tostring(text)) end | |
| 129 | + | context.warning = context.warn | |
| 130 | + | ||
| 131 | + | -- init context | |
| 132 | + | context.now = g_clock.millis() | |
| 133 | + | context.time = g_clock.millis() | |
| 134 | + | context.player = g_game.getLocalPlayer() | |
| 135 | + | ||
| 136 | + | -- init functions | |
| 137 | + | G.botContext = context | |
| 138 | + | dofiles("functions") | |
| 139 | + | context.Panels = {} | |
| 140 | + | dofiles("panels") | |
| 141 | + | G.botContext = nil | |
| 142 | + | ||
| 143 | + | -- run ui scripts | |
| 144 | + | for i, file in ipairs(uiFiles) do | |
| 145 | + | g_ui.importStyle(file) | |
| 146 | + | end | |
| 147 | + | ||
| 148 | + | -- run lua script | |
| 149 | + | for i, file in ipairs(luaFiles) do | |
| 150 | + | assert(load(g_resources.readFileContents(file), file, nil, context))() | |
| 151 | + | context.panel = context.mainTab -- reset default tab | |
| 152 | + | end | |
| 153 | + | ||
| 154 | + | return { | |
| 155 | + | script = function() | |
| 156 | + | context.now = g_clock.millis() | |
| 157 | + | context.time = g_clock.millis() | |
| 158 | + | ||
| 159 | + | for i, macro in ipairs(context._macros) do | |
| 160 | + | if macro.lastExecution + macro.timeout <= context.now and macro.enabled then | |
| 161 | + | local status, result = pcall(function() | |
| 162 | + | if macro.callback(macro) then | |
| 163 | + | macro.lastExecution = context.now | |
| 164 | + | end | |
| 165 | + | end) | |
| 166 | + | if not status then | |
| 167 | + | context.error("Macro: " .. macro.name .. " execution error: " .. result) | |
| 168 | + | end | |
| 169 | + | end | |
| 170 | + | end | |
| 171 | + | ||
| 172 | + | while #context._scheduler > 0 and context._scheduler[1].execution <= g_clock.millis() do | |
| 173 | + | local status, result = pcall(function() | |
| 174 | + | context._scheduler[1].callback() | |
| 175 | + | end) | |
| 176 | + | if not status then | |
| 177 | + | context.error("Schedule execution error: " .. result) | |
| 178 | + | end | |
| 179 | + | table.remove(context._scheduler, 1) | |
| 180 | + | end | |
| 181 | + | end, | |
| 182 | + | callbacks = { | |
| 183 | + | onKeyDown = function(keyCode, keyboardModifiers) | |
| 184 | + | local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers) | |
| 185 | + | for i, macro in ipairs(context._macros) do | |
| 186 | + | if macro.switch and macro.hotkey == keyDesc then | |
| 187 | + | macro.switch:onClick() | |
| 188 | + | end | |
| 189 | + | end | |
| 190 | + | local hotkey = context._hotkeys[keyDesc] | |
| 191 | + | if hotkey then | |
| 192 | + | if hotkey.single then | |
| 193 | + | if hotkey.callback() then | |
| 194 | + | hotkey.lastExecution = context.now | |
| 195 | + | end | |
| 196 | + | end | |
| 197 | + | if hotkey.switch then | |
| 198 | + | hotkey.switch:setOn(true) | |
| 199 | + | end | |
| 200 | + | end | |
| 201 | + | for i, callback in ipairs(context._callbacks.onKeyDown) do | |
| 202 | + | callback(keyDesc) | |
| 203 | + | end | |
| 204 | + | end, | |
| 205 | + | onKeyUp = function(keyCode, keyboardModifiers) | |
| 206 | + | local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers) | |
| 207 | + | local hotkey = context._hotkeys[keyDesc] | |
| 208 | + | if hotkey then | |
| 209 | + | if hotkey.switch then | |
| 210 | + | hotkey.switch:setOn(false) | |
| 211 | + | end | |
| 212 | + | end | |
| 213 | + | for i, callback in ipairs(context._callbacks.onKeyUp) do | |
| 214 | + | callback(keyDesc) | |
| 215 | + | end | |
| 216 | + | end, | |
| 217 | + | onKeyPress = function(keyCode, keyboardModifiers, autoRepeatTicks) | |
| 218 | + | local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers) | |
| 219 | + | local hotkey = context._hotkeys[keyDesc] | |
| 220 | + | if hotkey and not hotkey.single then | |
| 221 | + | if hotkey.callback() then | |
| 222 | + | hotkey.lastExecution = context.now | |
| 223 | + | end | |
| 224 | + | end | |
| 225 | + | for i, callback in ipairs(context._callbacks.onKeyPress) do | |
| 226 | + | callback(keyDesc, autoRepeatTicks) | |
| 227 | + | end | |
| 228 | + | end, | |
| 229 | + | onTalk = function(name, level, mode, text, channelId, pos) | |
| 230 | + | for i, callback in ipairs(context._callbacks.onTalk) do | |
| 231 | + | callback(name, level, mode, text, channelId, pos) | |
| 232 | + | end | |
| 233 | + | end, | |
| 234 | + | onTextMessage = function(mode, text) | |
| 235 | + | for i, callback in ipairs(context._callbacks.onTextMessage) do | |
| 236 | + | callback(mode, text) | |
| 237 | + | end | |
| 238 | + | end, | |
| 239 | + | onAddThing = function(tile, thing) | |
| 240 | + | for i, callback in ipairs(context._callbacks.onAddThing) do | |
| 241 | + | callback(tile, thing) | |
| 242 | + | end | |
| 243 | + | end, | |
| 244 | + | onRemoveThing = function(tile, thing) | |
| 245 | + | for i, callback in ipairs(context._callbacks.onRemoveThing) do | |
| 246 | + | callback(tile, thing) | |
| 247 | + | end | |
| 248 | + | end, | |
| 249 | + | onCreatureAppear = function(creature) | |
| 250 | + | for i, callback in ipairs(context._callbacks.onCreatureAppear) do | |
| 251 | + | callback(creature) | |
| 252 | + | end | |
| 253 | + | end, | |
| 254 | + | onCreatureDisappear = function(creature) | |
| 255 | + | for i, callback in ipairs(context._callbacks.onCreatureDisappear) do | |
| 256 | + | callback(creature) | |
| 257 | + | end | |
| 258 | + | end, | |
| 259 | + | onCreaturePositionChange = function(creature, newPos, oldPos) | |
| 260 | + | for i, callback in ipairs(context._callbacks.onCreaturePositionChange) do | |
| 261 | + | callback(creature, newPos, oldPos) | |
| 262 | + | end | |
| 263 | + | end, | |
| 264 | + | onCreatureHealthPercentChange = function(creature, healthPercent) | |
| 265 | + | for i, callback in ipairs(context._callbacks.onCreatureHealthPercentChange) do | |
| 266 | + | callback(creature, healthPercent) | |
| 267 | + | end | |
| 268 | + | end, | |
| 269 | + | onUse = function(pos, itemId, stackPos, subType) | |
| 270 | + | for i, callback in ipairs(context._callbacks.onUse) do | |
| 271 | + | callback(pos, itemId, stackPos, subType) | |
| 272 | + | end | |
| 273 | + | end, | |
| 274 | + | onUseWith = function(pos, itemId, target, subType) | |
| 275 | + | for i, callback in ipairs(context._callbacks.onUseWith) do | |
| 276 | + | callback(pos, itemId, target, subType) | |
| 277 | + | end | |
| 278 | + | end, | |
| 279 | + | onContainerOpen = function(container, previousContainer) | |
| 280 | + | for i, callback in ipairs(context._callbacks.onContainerOpen) do | |
| 281 | + | callback(container, previousContainer) | |
| 282 | + | end | |
| 283 | + | end, | |
| 284 | + | onContainerClose = function(container) | |
| 285 | + | for i, callback in ipairs(context._callbacks.onContainerClose) do | |
| 286 | + | callback(container) | |
| 287 | + | end | |
| 288 | + | end, | |
| 289 | + | onContainerUpdateItem = function(container, slot, item) | |
| 290 | + | for i, callback in ipairs(context._callbacks.onContainerUpdateItem) do | |
| 291 | + | callback(container, slot, item) | |
| 292 | + | end | |
| 293 | + | end, | |
| 294 | + | onMissle = function(missle) | |
| 295 | + | for i, callback in ipairs(context._callbacks.onMissle) do | |
| 296 | + | callback(missle) | |
| 297 | + | end | |
| 298 | + | end, | |
| 299 | + | onAnimatedText = function(thing, text) | |
| 300 | + | for i, callback in ipairs(context._callbacks.onAnimatedText) do | |
| 301 | + | callback(thing, text) | |
| 302 | + | end | |
| 303 | + | end, | |
| 304 | + | onStaticText = function(thing, text) | |
| 305 | + | for i, callback in ipairs(context._callbacks.onStaticText) do | |
| 306 | + | callback(thing, text) | |
| 307 | + | end | |
| 308 | + | end, | |
| 309 | + | onChannelList = function(channels) | |
| 310 | + | for i, callback in ipairs(context._callbacks.onChannelList) do | |
| 311 | + | callback(channels) | |
| 312 | + | end | |
| 313 | + | end, | |
| 314 | + | onOpenChannel = function(channelId, channelName) | |
| 315 | + | for i, callback in ipairs(context._callbacks.onOpenChannel) do | |
| 316 | + | callback(channels) | |
| 317 | + | end | |
| 318 | + | end, | |
| 319 | + | onCloseChannel = function(channelId) | |
| 320 | + | for i, callback in ipairs(context._callbacks.onCloseChannel) do | |
| 321 | + | callback(channelId) | |
| 322 | + | end | |
| 323 | + | end, | |
| 324 | + | onChannelEvent = function(channelId, name, event) | |
| 325 | + | for i, callback in ipairs(context._callbacks.onChannelEvent) do | |
| 326 | + | callback(channelId, name, event) | |
| 327 | + | end | |
| 328 | + | end, | |
| 329 | + | onTurn = function(creature, direction) | |
| 330 | + | for i, callback in ipairs(context._callbacks.onTurn) do | |
| 331 | + | callback(creature, direction) | |
| 332 | + | end | |
| 333 | + | end, | |
| 334 | + | } | |
| 335 | + | } | |
| 336 | + | end |
| @@ -0,0 +1,198 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | -- callback(callbackType, callback) | |
| 4 | + | context.callback = function(callbackType, callback) | |
| 5 | + | if not context._callbacks[callbackType] then | |
| 6 | + | return error("Wrong callback type: " .. callbackType) | |
| 7 | + | end | |
| 8 | + | if callbackType == "onAddThing" or callbackType == "onRemoveThing" then | |
| 9 | + | g_game.enableTileThingLuaCallback(true) | |
| 10 | + | end | |
| 11 | + | ||
| 12 | + | local desc = "lua" | |
| 13 | + | local info = debug.getinfo(2, "Sl") | |
| 14 | + | if info then | |
| 15 | + | desc = info.short_src .. ":" .. info.currentline | |
| 16 | + | end | |
| 17 | + | ||
| 18 | + | local callbackData = {} | |
| 19 | + | table.insert(context._callbacks[callbackType], function(...) | |
| 20 | + | if not callbackData.delay or callbackData.delay < context.now then | |
| 21 | + | local prevExecution = context._currentExecution | |
| 22 | + | context._currentExecution = callbackData | |
| 23 | + | local start = g_clock.realMillis() | |
| 24 | + | callback(...) | |
| 25 | + | local executionTime = g_clock.realMillis() - start | |
| 26 | + | if executionTime > 100 then | |
| 27 | + | context.warning("Slow " .. callbackType .. " (" .. executionTime .. "ms): " .. desc) | |
| 28 | + | end | |
| 29 | + | context._currentExecution = prevExecution | |
| 30 | + | end | |
| 31 | + | end) | |
| 32 | + | local cb = context._callbacks[callbackType] | |
| 33 | + | return { | |
| 34 | + | remove = function() | |
| 35 | + | local index = nil | |
| 36 | + | for i, cb2 in ipairs(context._callbacks[callbackType]) do | |
| 37 | + | if cb == cb2 then | |
| 38 | + | index = i | |
| 39 | + | end | |
| 40 | + | end | |
| 41 | + | if index then | |
| 42 | + | table.remove(context._callbacks[callbackType], index) | |
| 43 | + | end | |
| 44 | + | end | |
| 45 | + | } | |
| 46 | + | end | |
| 47 | + | ||
| 48 | + | -- onKeyDown(callback) -- callback = function(keys) | |
| 49 | + | context.onKeyDown = function(callback) | |
| 50 | + | return context.callback("onKeyDown", callback) | |
| 51 | + | end | |
| 52 | + | ||
| 53 | + | -- onKeyPress(callback) -- callback = function(keys) | |
| 54 | + | context.onKeyPress = function(callback) | |
| 55 | + | return context.callback("onKeyPress", callback) | |
| 56 | + | end | |
| 57 | + | ||
| 58 | + | -- onKeyUp(callback) -- callback = function(keys) | |
| 59 | + | context.onKeyUp = function(callback) | |
| 60 | + | return context.callback("onKeyUp", callback) | |
| 61 | + | end | |
| 62 | + | ||
| 63 | + | -- onTalk(callback) -- callback = function(name, level, mode, text, channelId, pos) | |
| 64 | + | context.onTalk = function(callback) | |
| 65 | + | return context.callback("onTalk", callback) | |
| 66 | + | end | |
| 67 | + | ||
| 68 | + | -- onTextMessage(callback) -- callback = function(mode, text) | |
| 69 | + | context.onTextMessage = function(callback) | |
| 70 | + | return context.callback("onTextMessage", callback) | |
| 71 | + | end | |
| 72 | + | ||
| 73 | + | -- onAddThing(callback) -- callback = function(tile, thing) | |
| 74 | + | context.onAddThing = function(callback) | |
| 75 | + | return context.callback("onAddThing", callback) | |
| 76 | + | end | |
| 77 | + | ||
| 78 | + | -- onRemoveThing(callback) -- callback = function(tile, thing) | |
| 79 | + | context.onRemoveThing = function(callback) | |
| 80 | + | return context.callback("onRemoveThing", callback) | |
| 81 | + | end | |
| 82 | + | ||
| 83 | + | -- onCreatureAppear(callback) -- callback = function(creature) | |
| 84 | + | context.onCreatureAppear = function(callback) | |
| 85 | + | return context.callback("onCreatureAppear", callback) | |
| 86 | + | end | |
| 87 | + | ||
| 88 | + | -- onCreatureDisappear(callback) -- callback = function(creature) | |
| 89 | + | context.onCreatureDisappear = function(callback) | |
| 90 | + | return context.callback("onCreatureDisappear", callback) | |
| 91 | + | end | |
| 92 | + | ||
| 93 | + | -- onCreaturePositionChange(callback) -- callback = function(creature, newPos, oldPos) | |
| 94 | + | context.onCreaturePositionChange = function(callback) | |
| 95 | + | return context.callback("onCreaturePositionChange", callback) | |
| 96 | + | end | |
| 97 | + | ||
| 98 | + | -- onCreatureHealthPercentChange(callback) -- callback = function(creature, healthPercent) | |
| 99 | + | context.onCreatureHealthPercentChange = function(callback) | |
| 100 | + | return context.callback("onCreatureHealthPercentChange", callback) | |
| 101 | + | end | |
| 102 | + | ||
| 103 | + | -- onUse(callback) -- callback = function(pos, itemId, stackPos, subType) | |
| 104 | + | context.onUse = function(callback) | |
| 105 | + | return context.callback("onUse", callback) | |
| 106 | + | end | |
| 107 | + | ||
| 108 | + | -- onUseWith(callback) -- callback = function(pos, itemId, target, subType) | |
| 109 | + | context.onUseWith = function(callback) | |
| 110 | + | return context.callback("onUseWith", callback) | |
| 111 | + | end | |
| 112 | + | ||
| 113 | + | -- onContainerOpen -- callback = function(container, previousContainer) | |
| 114 | + | context.onContainerOpen = function(callback) | |
| 115 | + | return context.callback("onContainerOpen", callback) | |
| 116 | + | end | |
| 117 | + | ||
| 118 | + | -- onContainerClose -- callback = function(container) | |
| 119 | + | context.onContainerClose = function(callback) | |
| 120 | + | return context.callback("onContainerClose", callback) | |
| 121 | + | end | |
| 122 | + | ||
| 123 | + | -- onContainerUpdateItem -- callback = function(container, slot, item) | |
| 124 | + | context.onContainerUpdateItem = function(callback) | |
| 125 | + | return context.callback("onContainerUpdateItem", callback) | |
| 126 | + | end | |
| 127 | + | ||
| 128 | + | -- onMissle -- callback = function(missle) | |
| 129 | + | context.onMissle = function(callback) | |
| 130 | + | return context.callback("onMissle", callback) | |
| 131 | + | end | |
| 132 | + | ||
| 133 | + | -- onAnimatedText -- callback = function(thing, text) | |
| 134 | + | context.onAnimatedText = function(callback) | |
| 135 | + | return context.callback("onAnimatedText", callback) | |
| 136 | + | end | |
| 137 | + | ||
| 138 | + | -- onStaticText -- callback = function(thing, text) | |
| 139 | + | context.onStaticText = function(callback) | |
| 140 | + | return context.callback("onStaticText", callback) | |
| 141 | + | end | |
| 142 | + | ||
| 143 | + | -- onChannelList -- callback = function(channels) | |
| 144 | + | context.onChannelList = function(callback) | |
| 145 | + | return context.callback("onChannelList", callback) | |
| 146 | + | end | |
| 147 | + | ||
| 148 | + | -- onOpenChannel -- callback = function(channelId, name) | |
| 149 | + | context.onOpenChannel = function(callback) | |
| 150 | + | return context.callback("onOpenChannel", callback) | |
| 151 | + | end | |
| 152 | + | ||
| 153 | + | -- onCloseChannel -- callback = function(channelId) | |
| 154 | + | context.onCloseChannel = function(callback) | |
| 155 | + | return context.callback("onCloseChannel", callback) | |
| 156 | + | end | |
| 157 | + | ||
| 158 | + | -- onChannelEvent -- callback = function(channelId, name, event) | |
| 159 | + | context.onChannelEvent = function(callback) | |
| 160 | + | return context.callback("onChannelEvent", callback) | |
| 161 | + | end | |
| 162 | + | ||
| 163 | + | -- onTurn -- callback = function(creature, direction) | |
| 164 | + | context.onTurn = function(callback) | |
| 165 | + | return context.callback("onTurn", callback) | |
| 166 | + | end | |
| 167 | + | ||
| 168 | + | ||
| 169 | + | -- CUSTOM CALLBACKS | |
| 170 | + | ||
| 171 | + | -- listen(name, callback) -- callback = function(text, channelId, pos) | |
| 172 | + | context.listen = function(name, callback) | |
| 173 | + | if not name then return context.error("listen: invalid name") end | |
| 174 | + | name = name:lower() | |
| 175 | + | return context.onTalk(function(name2, level, mode, text, channelId, pos) | |
| 176 | + | if name == name2:lower() then | |
| 177 | + | callback(text, channelId, pos) | |
| 178 | + | end | |
| 179 | + | end) | |
| 180 | + | end | |
| 181 | + | ||
| 182 | + | -- onPlayerPositionChange(callback) -- callback = function(newPos, oldPos) | |
| 183 | + | context.onPlayerPositionChange = function(callback) | |
| 184 | + | return context.onCreaturePositionChange(function(creature, newPos, oldPos) | |
| 185 | + | if creature == context.player then | |
| 186 | + | callback(newPos, oldPos) | |
| 187 | + | end | |
| 188 | + | end) | |
| 189 | + | end | |
| 190 | + | ||
| 191 | + | -- onPlayerHealthChange(callback) -- callback = function(healthPercent) | |
| 192 | + | context.onPlayerHealthChange = function(callback) | |
| 193 | + | return context.onCreatureHealthPercentChange(function(creature, healthPercent) | |
| 194 | + | if creature == context.player then | |
| 195 | + | callback(healthPercent) | |
| 196 | + | end | |
| 197 | + | end) | |
| 198 | + | end |
| @@ -0,0 +1,25 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.North = 0 | |
| 4 | + | context.East = 1 | |
| 5 | + | context.South = 2 | |
| 6 | + | context.West = 3 | |
| 7 | + | context.NorthEast = 4 | |
| 8 | + | context.SouthEast = 5 | |
| 9 | + | context.SouthWest = 6 | |
| 10 | + | context.NorthWest = 7 | |
| 11 | + | ||
| 12 | + | context.InventorySlotOther = 0 | |
| 13 | + | context.InventorySlotHead = 1 | |
| 14 | + | context.InventorySlotNeck = 2 | |
| 15 | + | context.InventorySlotBack = 3 | |
| 16 | + | context.InventorySlotBody = 4 | |
| 17 | + | context.InventorySlotRight = 5 | |
| 18 | + | context.InventorySlotLeft = 6 | |
| 19 | + | context.InventorySlotLeg = 7 | |
| 20 | + | context.InventorySlotFeet = 8 | |
| 21 | + | context.InventorySlotFinger = 9 | |
| 22 | + | context.InventorySlotAmmo = 10 | |
| 23 | + | context.InventorySlotPurse = 11 | |
| 24 | + | context.InventorySlotFirst = 1 | |
| 25 | + | context.InventorySlotLast = 10 |
| @@ -0,0 +1,176 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | local iconsWithoutPosition = 0 | |
| 4 | + | ||
| 5 | + | context.addIcon = function(id, options, callback) | |
| 6 | + | --[[ | |
| 7 | + | Available options: | |
| 8 | + | item: {id=2160, count=100} | |
| 9 | + | outfit: outfit table ({}) | |
| 10 | + | text: string | |
| 11 | + | x: float (0.0 - 1.0) | |
| 12 | + | y: float (0.0 - 1.0) | |
| 13 | + | hotkey: string | |
| 14 | + | switchable: true / false [default: true] | |
| 15 | + | movable: true / false [default: true] | |
| 16 | + | phantom: true / false [defaule: false] | |
| 17 | + | ]]-- | |
| 18 | + | local panel = modules.game_interface.gameMapPanel | |
| 19 | + | if type(id) ~= "string" or id:len() < 1 then | |
| 20 | + | return context.error("Invalid id for addIcon") | |
| 21 | + | end | |
| 22 | + | if options.switchable == false and type(callback) ~= 'function' then | |
| 23 | + | return context.error("Invalid callback for addIcon") | |
| 24 | + | end | |
| 25 | + | if type(context.storage._icons) ~= "table" then | |
| 26 | + | context.storage._icons = {} | |
| 27 | + | end | |
| 28 | + | if type(context.storage._icons[id]) ~= "table" then | |
| 29 | + | context.storage._icons[id] = {} | |
| 30 | + | end | |
| 31 | + | local config = context.storage._icons[id] | |
| 32 | + | local widget = g_ui.createWidget("BotIcon", panel) | |
| 33 | + | widget.botWidget = true | |
| 34 | + | widget.botIcon = true | |
| 35 | + | ||
| 36 | + | if type(config.x) ~= 'number' and type(config.y) ~= 'number' then | |
| 37 | + | if type(options.x) == 'number' and type(options.y) == 'number' then | |
| 38 | + | config.x = math.min(1.0, math.max(0.0, options.x)) | |
| 39 | + | config.y = math.min(1.0, math.max(0.0, options.y)) | |
| 40 | + | else | |
| 41 | + | config.x = 0.01 + math.floor(iconsWithoutPosition / 5) / 10 | |
| 42 | + | config.y = 0.05 + (iconsWithoutPosition % 5) / 5 | |
| 43 | + | iconsWithoutPosition = iconsWithoutPosition + 1 | |
| 44 | + | end | |
| 45 | + | end | |
| 46 | + | ||
| 47 | + | if options.item then | |
| 48 | + | if type(options.item) == 'number' then | |
| 49 | + | widget.item:setItemId(options.item) | |
| 50 | + | else | |
| 51 | + | widget.item:setItemId(options.item.id) | |
| 52 | + | widget.item:setItemCount(options.item.count or 1) | |
| 53 | + | widget.item:setShowCount(false) | |
| 54 | + | end | |
| 55 | + | end | |
| 56 | + | ||
| 57 | + | if options.outfit then | |
| 58 | + | widget.creature:setOutfit(options.outfit) | |
| 59 | + | end | |
| 60 | + | ||
| 61 | + | if options.switchable == false then | |
| 62 | + | widget.status:hide() | |
| 63 | + | widget.status:setOn(true) | |
| 64 | + | else | |
| 65 | + | if config.enabled ~= true then | |
| 66 | + | config.enabled = false | |
| 67 | + | end | |
| 68 | + | widget.status:setOn(config.enabled) | |
| 69 | + | end | |
| 70 | + | ||
| 71 | + | if options.text then | |
| 72 | + | if options.switchable ~= false then | |
| 73 | + | widget.status:hide() | |
| 74 | + | if widget.status:isOn() then | |
| 75 | + | widget.text:setColor('green') | |
| 76 | + | else | |
| 77 | + | widget.text:setColor('red') | |
| 78 | + | end | |
| 79 | + | end | |
| 80 | + | widget.text:setText(options.text) | |
| 81 | + | end | |
| 82 | + | ||
| 83 | + | widget.setOn = function(val) | |
| 84 | + | widget.status:setOn(val) | |
| 85 | + | if widget.status:isOn() then | |
| 86 | + | widget.text:setColor('green') | |
| 87 | + | else | |
| 88 | + | widget.text:setColor('red') | |
| 89 | + | end | |
| 90 | + | config.enabled = widget.status:isOn() | |
| 91 | + | end | |
| 92 | + | ||
| 93 | + | widget.onClick = function(widget) | |
| 94 | + | if options.switchable ~= false then | |
| 95 | + | widget.setOn(not widget.status:isOn()) | |
| 96 | + | if type(callback) == 'table' then | |
| 97 | + | callback.setOn(config.enabled) | |
| 98 | + | return | |
| 99 | + | end | |
| 100 | + | end | |
| 101 | + | ||
| 102 | + | callback(widget, widget.status:isOn()) | |
| 103 | + | end | |
| 104 | + | ||
| 105 | + | if options.hotkey then | |
| 106 | + | widget.hotkey:setText(options.hotkey) | |
| 107 | + | context.hotkey(options.hotkey, "", function() | |
| 108 | + | widget:onClick() | |
| 109 | + | end, nil, options.switchable ~= false) | |
| 110 | + | else | |
| 111 | + | widget.hotkey:hide() | |
| 112 | + | end | |
| 113 | + | ||
| 114 | + | if options.movable ~= false then | |
| 115 | + | widget.onDragEnter = function(widget, mousePos) | |
| 116 | + | if not g_keyboard.isCtrlPressed() then | |
| 117 | + | return false | |
| 118 | + | end | |
| 119 | + | widget:breakAnchors() | |
| 120 | + | widget.movingReference = { x = mousePos.x - widget:getX(), y = mousePos.y - widget:getY() } | |
| 121 | + | return true | |
| 122 | + | end | |
| 123 | + | ||
| 124 | + | widget.onDragMove = function(widget, mousePos, moved) | |
| 125 | + | local parentRect = widget:getParent():getRect() | |
| 126 | + | local x = math.min(math.max(parentRect.x, mousePos.x - widget.movingReference.x), parentRect.x + parentRect.width - widget:getWidth()) | |
| 127 | + | local y = math.min(math.max(parentRect.y - widget:getParent():getMarginTop(), mousePos.y - widget.movingReference.y), parentRect.y + parentRect.height - widget:getHeight()) | |
| 128 | + | widget:move(x, y) | |
| 129 | + | return true | |
| 130 | + | end | |
| 131 | + | ||
| 132 | + | widget.onDragLeave = function(widget, pos) | |
| 133 | + | local parent = widget:getParent() | |
| 134 | + | local parentRect = parent:getRect() | |
| 135 | + | local x = widget:getX() - parentRect.x | |
| 136 | + | local y = widget:getY() - parentRect.y | |
| 137 | + | local width = parentRect.width - widget:getWidth() | |
| 138 | + | local height = parentRect.height - widget:getHeight() | |
| 139 | + | ||
| 140 | + | config.x = math.min(1, math.max(0, x / width)) | |
| 141 | + | config.y = math.min(1, math.max(0, y / height)) | |
| 142 | + | ||
| 143 | + | widget:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) | |
| 144 | + | widget:addAnchor(AnchorVerticalCenter, 'parent', AnchorVerticalCenter) | |
| 145 | + | widget:setMarginTop(math.max(height * (-0.5) - parent:getMarginTop(), height * (-0.5 + config.y))) | |
| 146 | + | widget:setMarginLeft(width * (-0.5 + config.x)) | |
| 147 | + | return true | |
| 148 | + | end | |
| 149 | + | end | |
| 150 | + | ||
| 151 | + | widget.onGeometryChange = function(widget) | |
| 152 | + | if widget:isDragging() then return end | |
| 153 | + | local parent = widget:getParent() | |
| 154 | + | local parentRect = parent:getRect() | |
| 155 | + | local width = parentRect.width - widget:getWidth() | |
| 156 | + | local height = parentRect.height - widget:getHeight() | |
| 157 | + | widget:setMarginTop(math.max(height * (-0.5) - parent:getMarginTop(), height * (-0.5 + config.y))) | |
| 158 | + | widget:setMarginLeft(width * (-0.5 + config.x)) | |
| 159 | + | end | |
| 160 | + | ||
| 161 | + | if options.phantom ~= true then | |
| 162 | + | widget.onMouseRelease = function() | |
| 163 | + | return true | |
| 164 | + | end | |
| 165 | + | end | |
| 166 | + | ||
| 167 | + | if options.switchable ~= false then | |
| 168 | + | if type(callback) == 'table' then | |
| 169 | + | callback.setOn(config.enabled) | |
| 170 | + | callback.icon = widget | |
| 171 | + | else | |
| 172 | + | callback(widget, widget.status:isOn()) | |
| 173 | + | end | |
| 174 | + | end | |
| 175 | + | return widget | |
| 176 | + | end |
| @@ -0,0 +1,211 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | -- MAIN BOT FUNCTION | |
| 4 | + | -- macro(timeout, callback) | |
| 5 | + | -- macro(timeout, name, callback) | |
| 6 | + | -- macro(timeout, name, callback, parent) | |
| 7 | + | -- macro(timeout, name, hotkey, callback) | |
| 8 | + | -- macro(timeout, name, hotkey, callback, parent) | |
| 9 | + | context.macro = function(timeout, name, hotkey, callback, parent) | |
| 10 | + | if type(timeout) ~= 'number' or timeout < 1 then | |
| 11 | + | error("Invalid timeout for macro: " .. tostring(timeout)) | |
| 12 | + | end | |
| 13 | + | if type(name) == 'function' then | |
| 14 | + | callback = name | |
| 15 | + | name = "" | |
| 16 | + | hotkey = "" | |
| 17 | + | elseif type(hotkey) == 'function' then | |
| 18 | + | parent = callback | |
| 19 | + | callback = hotkey | |
| 20 | + | hotkey = "" | |
| 21 | + | elseif type(callback) ~= 'function' then | |
| 22 | + | error("Invalid callback for macro: " .. tostring(callback)) | |
| 23 | + | end | |
| 24 | + | if hotkey == nil then | |
| 25 | + | hotkey = "" | |
| 26 | + | end | |
| 27 | + | if type(name) ~= 'string' or type(hotkey) ~= 'string' then | |
| 28 | + | error("Invalid name or hotkey for macro") | |
| 29 | + | end | |
| 30 | + | if not parent then | |
| 31 | + | parent = context.panel | |
| 32 | + | end | |
| 33 | + | if hotkey:len() > 0 then | |
| 34 | + | hotkey = retranslateKeyComboDesc(hotkey) | |
| 35 | + | end | |
| 36 | + | ||
| 37 | + | -- min timeout is 50, to avoid lags | |
| 38 | + | if timeout < 50 then | |
| 39 | + | timeout = 50 | |
| 40 | + | end | |
| 41 | + | ||
| 42 | + | table.insert(context._macros, { | |
| 43 | + | enabled = false, | |
| 44 | + | name = name, | |
| 45 | + | timeout = timeout, | |
| 46 | + | lastExecution = context.now + math.random(0, 100), | |
| 47 | + | hotkey = hotkey, | |
| 48 | + | }) | |
| 49 | + | local macro = context._macros[#context._macros] | |
| 50 | + | ||
| 51 | + | macro.isOn = function() | |
| 52 | + | return macro.enabled | |
| 53 | + | end | |
| 54 | + | macro.isOff = function() | |
| 55 | + | return not macro.enabled | |
| 56 | + | end | |
| 57 | + | macro.toggle = function(widget) | |
| 58 | + | if macro.isOn() then | |
| 59 | + | macro.setOff() | |
| 60 | + | else | |
| 61 | + | macro.setOn() | |
| 62 | + | end | |
| 63 | + | end | |
| 64 | + | macro.setOn = function(val) | |
| 65 | + | if val == false then | |
| 66 | + | return macro.setOff() | |
| 67 | + | end | |
| 68 | + | macro.enabled = true | |
| 69 | + | context.storage._macros[name] = true | |
| 70 | + | if macro.switch then | |
| 71 | + | macro.switch:setOn(true) | |
| 72 | + | end | |
| 73 | + | if macro.icon then | |
| 74 | + | macro.icon.setOn(true) | |
| 75 | + | end | |
| 76 | + | end | |
| 77 | + | macro.setOff = function(val) | |
| 78 | + | if val == false then | |
| 79 | + | return macro.setOn() | |
| 80 | + | end | |
| 81 | + | macro.enabled = false | |
| 82 | + | context.storage._macros[name] = false | |
| 83 | + | if macro.switch then | |
| 84 | + | macro.switch:setOn(false) | |
| 85 | + | end | |
| 86 | + | if macro.icon then | |
| 87 | + | macro.icon.setOn(false) | |
| 88 | + | end | |
| 89 | + | end | |
| 90 | + | ||
| 91 | + | if name:len() > 0 then | |
| 92 | + | -- creature switch | |
| 93 | + | local text = name | |
| 94 | + | if hotkey:len() > 0 then | |
| 95 | + | text = name .. " [" .. hotkey .. "]" | |
| 96 | + | end | |
| 97 | + | macro.switch = context.addSwitch("macro_" .. (#context._macros + 1), text, macro.toggle, parent) | |
| 98 | + | ||
| 99 | + | -- load state | |
| 100 | + | if context.storage._macros[name] == true then | |
| 101 | + | macro.setOn() | |
| 102 | + | end | |
| 103 | + | else | |
| 104 | + | macro.enabled = true -- unnamed macros are enabled by default | |
| 105 | + | end | |
| 106 | + | ||
| 107 | + | local desc = "lua" | |
| 108 | + | local info = debug.getinfo(2, "Sl") | |
| 109 | + | if info then | |
| 110 | + | desc = info.short_src .. ":" .. info.currentline | |
| 111 | + | end | |
| 112 | + | ||
| 113 | + | macro.callback = function(macro) | |
| 114 | + | if not macro.delay or macro.delay < context.now then | |
| 115 | + | context._currentExecution = macro | |
| 116 | + | local start = g_clock.realMillis() | |
| 117 | + | callback(macro) | |
| 118 | + | local executionTime = g_clock.realMillis() - start | |
| 119 | + | if executionTime > 100 then | |
| 120 | + | context.warning("Slow macro (" .. executionTime .. "ms): " .. macro.name .. " - " .. desc) | |
| 121 | + | end | |
| 122 | + | context._currentExecution = nil | |
| 123 | + | return true | |
| 124 | + | end | |
| 125 | + | end | |
| 126 | + | return macro | |
| 127 | + | end | |
| 128 | + | ||
| 129 | + | -- hotkey(keys, callback) | |
| 130 | + | -- hotkey(keys, name, callback) | |
| 131 | + | -- hotkey(keys, name, callback, parent) | |
| 132 | + | context.hotkey = function(keys, name, callback, parent, single) | |
| 133 | + | if type(name) == 'function' then | |
| 134 | + | callback = name | |
| 135 | + | name = "" | |
| 136 | + | end | |
| 137 | + | if not parent then | |
| 138 | + | parent = context.panel | |
| 139 | + | end | |
| 140 | + | keys = retranslateKeyComboDesc(keys) | |
| 141 | + | if not keys or #keys == 0 then | |
| 142 | + | return context.error("Invalid hotkey keys " .. tostring(name)) | |
| 143 | + | end | |
| 144 | + | if context._hotkeys[keys] then | |
| 145 | + | return context.error("Duplicated hotkey: " .. keys) | |
| 146 | + | end | |
| 147 | + | ||
| 148 | + | local switch = nil | |
| 149 | + | if name:len() > 0 then | |
| 150 | + | switch = context._addHotkeySwitch(name, keys, parent) | |
| 151 | + | end | |
| 152 | + | ||
| 153 | + | context._hotkeys[keys] = { | |
| 154 | + | name = name, | |
| 155 | + | lastExecution = context.now, | |
| 156 | + | switch = switch, | |
| 157 | + | single = single | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | local desc = "lua" | |
| 161 | + | local info = debug.getinfo(2, "Sl") | |
| 162 | + | if info then | |
| 163 | + | desc = info.short_src .. ":" .. info.currentline | |
| 164 | + | end | |
| 165 | + | ||
| 166 | + | local hotkeyData = context._hotkeys[keys] | |
| 167 | + | hotkeyData.callback = function() | |
| 168 | + | if not hotkeyData.delay or hotkeyData.delay < context.now then | |
| 169 | + | context._currentExecution = hotkeyData | |
| 170 | + | local start = g_clock.realMillis() | |
| 171 | + | callback() | |
| 172 | + | local executionTime = g_clock.realMillis() - start | |
| 173 | + | if executionTime > 100 then | |
| 174 | + | context.warning("Slow hotkey (" .. executionTime .. "ms): " .. hotkeyData.name .. " - " .. desc) | |
| 175 | + | end | |
| 176 | + | context._currentExecution = nil | |
| 177 | + | return true | |
| 178 | + | end | |
| 179 | + | end | |
| 180 | + | ||
| 181 | + | return hotkeyData | |
| 182 | + | end | |
| 183 | + | ||
| 184 | + | -- singlehotkey(keys, callback) | |
| 185 | + | -- singlehotkey(keys, name, callback) | |
| 186 | + | -- singlehotkey(keys, name, callback, parent) | |
| 187 | + | context.singlehotkey = function(keys, name, callback, parent) | |
| 188 | + | if type(name) == 'function' then | |
| 189 | + | callback = name | |
| 190 | + | name = "" | |
| 191 | + | end | |
| 192 | + | return context.hotkey(keys, name, callback, parent, true) | |
| 193 | + | end | |
| 194 | + | ||
| 195 | + | -- schedule(timeout, callback) | |
| 196 | + | context.schedule = function(timeout, callback) | |
| 197 | + | local extecute_time = g_clock.millis() + timeout | |
| 198 | + | table.insert(context._scheduler, { | |
| 199 | + | execution = extecute_time, | |
| 200 | + | callback = callback | |
| 201 | + | }) | |
| 202 | + | table.sort(context._scheduler, function(a, b) return a.execution < b.execution end) | |
| 203 | + | end | |
| 204 | + | ||
| 205 | + | -- delay(duration) -- block execution of current macro/hotkey/callback for x milliseconds | |
| 206 | + | context.delay = function(duration) | |
| 207 | + | if not context._currentExecution then | |
| 208 | + | return context.error("Invalid usage of delay function, it should be used inside callbacks") | |
| 209 | + | end | |
| 210 | + | context._currentExecution.delay = context.now + duration | |
| 211 | + | end |
| @@ -0,0 +1,241 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.getMapView = function() return modules.game_interface.getMapPanel() end | |
| 4 | + | context.getMapPanel = context.getMapView | |
| 5 | + | context.zoomIn = function() modules.game_interface.getMapPanel():zoomIn() end | |
| 6 | + | context.zoomOut = function() modules.game_interface.getMapPanel():zoomOut() end | |
| 7 | + | ||
| 8 | + | context.getSpectators = function(param1, param2) | |
| 9 | + | --[[ | |
| 10 | + | if param1 is table (position) then it's used for central position, then param2 is used as param1 | |
| 11 | + | if param1 is creature, then creature position and direction of creature is used, then param2 is used as param1 | |
| 12 | + | if param1 is true/false then it's used for multifloor, example: getSpectators(true) | |
| 13 | + | if param1 is string then it's used for getSpectatorsByPattern | |
| 14 | + | ]]-- | |
| 15 | + | local pos = context.player:getPosition() | |
| 16 | + | local direction = context.player:getDirection() | |
| 17 | + | if type(param1) == 'table' then | |
| 18 | + | pos = param1 | |
| 19 | + | direction = 8 -- invalid direction | |
| 20 | + | param1 = param2 | |
| 21 | + | end | |
| 22 | + | if type(param1) == 'userdata' then | |
| 23 | + | pos = param1:getPosition() | |
| 24 | + | direction = param1:getDirection() | |
| 25 | + | param1 = param2 | |
| 26 | + | end | |
| 27 | + | ||
| 28 | + | if type(param1) == 'string' then | |
| 29 | + | return g_map.getSpectatorsByPattern(pos, param1, direction) | |
| 30 | + | end | |
| 31 | + | ||
| 32 | + | local multifloor = false | |
| 33 | + | if type(param1) == 'boolean' and param1 == true then | |
| 34 | + | multifloor = true | |
| 35 | + | end | |
| 36 | + | return g_map.getSpectators(pos, multifloor) | |
| 37 | + | end | |
| 38 | + | ||
| 39 | + | context.getCreatureById = function(id, multifloor) | |
| 40 | + | if type(id) ~= 'number' then return nil end | |
| 41 | + | if multifloor ~= true then | |
| 42 | + | multifloor = false | |
| 43 | + | end | |
| 44 | + | for i, spec in ipairs(g_map.getSpectators(context.player:getPosition(), multifloor)) do | |
| 45 | + | if spec:getId() == id then | |
| 46 | + | return spec | |
| 47 | + | end | |
| 48 | + | end | |
| 49 | + | return nil | |
| 50 | + | end | |
| 51 | + | ||
| 52 | + | context.getCreatureByName = function(name, multifloor) | |
| 53 | + | if not name then return nil end | |
| 54 | + | name = name:lower() | |
| 55 | + | if multifloor ~= true then | |
| 56 | + | multifloor = false | |
| 57 | + | end | |
| 58 | + | for i, spec in ipairs(g_map.getSpectators(context.player:getPosition(), multifloor)) do | |
| 59 | + | if spec:getName():lower() == name then | |
| 60 | + | return spec | |
| 61 | + | end | |
| 62 | + | end | |
| 63 | + | return nil | |
| 64 | + | end | |
| 65 | + | ||
| 66 | + | context.getPlayerByName = function(name, multifloor) | |
| 67 | + | if not name then return nil end | |
| 68 | + | name = name:lower() | |
| 69 | + | if multifloor ~= true then | |
| 70 | + | multifloor = false | |
| 71 | + | end | |
| 72 | + | for i, spec in ipairs(g_map.getSpectators(context.player:getPosition(), multifloor)) do | |
| 73 | + | if spec:isPlayer() and spec:getName():lower() == name then | |
| 74 | + | return spec | |
| 75 | + | end | |
| 76 | + | end | |
| 77 | + | return nil | |
| 78 | + | end | |
| 79 | + | ||
| 80 | + | context.findAllPaths = function(start, maxDist, params) | |
| 81 | + | --[[ | |
| 82 | + | Available params: | |
| 83 | + | ignoreLastCreature | |
| 84 | + | ignoreCreatures | |
| 85 | + | ignoreNonPathable | |
| 86 | + | ignoreNonWalkable | |
| 87 | + | ignoreStairs | |
| 88 | + | ignoreCost | |
| 89 | + | allowUnseen | |
| 90 | + | allowOnlyVisibleTiles | |
| 91 | + | ]]-- | |
| 92 | + | if type(params) ~= 'table' then | |
| 93 | + | params = {} | |
| 94 | + | end | |
| 95 | + | for key, value in pairs(params) do | |
| 96 | + | if value == nil or value == false then | |
| 97 | + | params[key] = 0 | |
| 98 | + | elseif value == true then | |
| 99 | + | params[key] = 1 | |
| 100 | + | end | |
| 101 | + | end | |
| 102 | + | return g_map.findEveryPath(start, maxDist, params) | |
| 103 | + | end | |
| 104 | + | context.findEveryPath = context.findAllPaths | |
| 105 | + | ||
| 106 | + | context.translateAllPathsToPath = function(paths, destPos) | |
| 107 | + | local predirections = {} | |
| 108 | + | local directions = {} | |
| 109 | + | local destPosStr = destPos | |
| 110 | + | if type(destPos) ~= 'string' then | |
| 111 | + | destPosStr = destPos.x .. "," .. destPos.y .. "," .. destPos.z | |
| 112 | + | end | |
| 113 | + | ||
| 114 | + | while destPosStr:len() > 0 do | |
| 115 | + | local node = paths[destPosStr] | |
| 116 | + | if not node then | |
| 117 | + | break | |
| 118 | + | end | |
| 119 | + | if node[3] < 0 then | |
| 120 | + | break | |
| 121 | + | end | |
| 122 | + | table.insert(predirections, node[3]) | |
| 123 | + | destPosStr = node[4] | |
| 124 | + | end | |
| 125 | + | -- reverse | |
| 126 | + | for i=#predirections,1,-1 do | |
| 127 | + | table.insert(directions, predirections[i]) | |
| 128 | + | end | |
| 129 | + | return directions | |
| 130 | + | end | |
| 131 | + | context.translateEveryPathToPath = context.translateAllPathsToPath | |
| 132 | + | ||
| 133 | + | ||
| 134 | + | context.findPath = function(startPos, destPos, maxDist, params) | |
| 135 | + | --[[ | |
| 136 | + | Available params: | |
| 137 | + | ignoreLastCreature | |
| 138 | + | ignoreCreatures | |
| 139 | + | ignoreNonPathable | |
| 140 | + | ignoreNonWalkable | |
| 141 | + | ignoreStairs | |
| 142 | + | ignoreCost | |
| 143 | + | allowUnseen | |
| 144 | + | allowOnlyVisibleTiles | |
| 145 | + | precision | |
| 146 | + | marginMin | |
| 147 | + | marginMax | |
| 148 | + | ]]-- | |
| 149 | + | if not destPos or startPos.z ~= destPos.z then | |
| 150 | + | return | |
| 151 | + | end | |
| 152 | + | if type(maxDist) ~= 'number' then | |
| 153 | + | maxDist = 100 | |
| 154 | + | end | |
| 155 | + | if type(params) ~= 'table' then | |
| 156 | + | params = {} | |
| 157 | + | end | |
| 158 | + | local destPosStr = destPos.x .. "," .. destPos.y .. "," .. destPos.z | |
| 159 | + | params["destination"] = destPosStr | |
| 160 | + | local paths = context.findAllPaths(startPos, maxDist, params) | |
| 161 | + | local marginMin = params.marginMin or params.minMargin | |
| 162 | + | local marginMax = params.marginMax or params.maxMargin | |
| 163 | + | if type(marginMin) == 'number' and type(marginMax) == 'number' then | |
| 164 | + | local bestCandidate = nil | |
| 165 | + | local bestCandidatePos = nil | |
| 166 | + | for x = -marginMax, marginMax do | |
| 167 | + | for y = -marginMax, marginMax do | |
| 168 | + | if math.abs(x) >= marginMin or math.abs(y) >= marginMin then | |
| 169 | + | local dest = (destPos.x + x) .. "," .. (destPos.y + y) .. "," .. destPos.z | |
| 170 | + | local node = paths[dest] | |
| 171 | + | if node and (not bestCandidate or bestCandidate[1] > node[1]) then | |
| 172 | + | bestCandidate = node | |
| 173 | + | bestCandidatePos = dest | |
| 174 | + | end | |
| 175 | + | end | |
| 176 | + | end | |
| 177 | + | end | |
| 178 | + | if bestCandidate then | |
| 179 | + | return context.translateAllPathsToPath(paths, bestCandidatePos) | |
| 180 | + | end | |
| 181 | + | return | |
| 182 | + | end | |
| 183 | + | ||
| 184 | + | if not paths[destPosStr] then | |
| 185 | + | local precision = params.precision | |
| 186 | + | if type(precision) == 'number' then | |
| 187 | + | for p = 1, precision do | |
| 188 | + | local bestCandidate = nil | |
| 189 | + | local bestCandidatePos = nil | |
| 190 | + | for x = -p, p do | |
| 191 | + | for y = -p, p do | |
| 192 | + | local dest = (destPos.x + x) .. "," .. (destPos.y + y) .. "," .. destPos.z | |
| 193 | + | local node = paths[dest] | |
| 194 | + | if node and (not bestCandidate or bestCandidate[1] > node[1]) then | |
| 195 | + | bestCandidate = node | |
| 196 | + | bestCandidatePos = dest | |
| 197 | + | end | |
| 198 | + | end | |
| 199 | + | end | |
| 200 | + | if bestCandidate then | |
| 201 | + | return context.translateAllPathsToPath(paths, bestCandidatePos) | |
| 202 | + | end | |
| 203 | + | end | |
| 204 | + | end | |
| 205 | + | return nil | |
| 206 | + | end | |
| 207 | + | ||
| 208 | + | return context.translateAllPathsToPath(paths, destPos) | |
| 209 | + | end | |
| 210 | + | context.getPath = context.findPath | |
| 211 | + | ||
| 212 | + | -- also works as autoWalk(dirs) where dirs is a list eg.: {1,2,3,0,1,1,2,} | |
| 213 | + | context.autoWalk = function(destination, maxDist, params) | |
| 214 | + | if type(destination) == "table" and table.isList(destination) and not maxDist and not params then | |
| 215 | + | g_game.autoWalk(destination, {x=0,y=0,z=0}) | |
| 216 | + | return true | |
| 217 | + | end | |
| 218 | + | ||
| 219 | + | -- Available params same as for findPath | |
| 220 | + | local path = context.findPath(context.player:getPosition(), destination, maxDist, params) | |
| 221 | + | if not path then | |
| 222 | + | return false | |
| 223 | + | end | |
| 224 | + | -- autowalk without prewalk animation | |
| 225 | + | g_game.autoWalk(path, {x=0,y=0,z=0}) | |
| 226 | + | return true | |
| 227 | + | end | |
| 228 | + | ||
| 229 | + | context.getTileUnderCursor = function() | |
| 230 | + | if not modules.game_interface.gameMapPanel.mousePos then return end | |
| 231 | + | return modules.game_interface.gameMapPanel:getTile(modules.game_interface.gameMapPanel.mousePos) | |
| 232 | + | end | |
| 233 | + | ||
| 234 | + | context.canShoot = function(pos, distance) | |
| 235 | + | if not distance then distance = 5 end | |
| 236 | + | local tile = g_map.getTile(pos, distance) | |
| 237 | + | if tile then | |
| 238 | + | return tile:canShoot(distance) | |
| 239 | + | end | |
| 240 | + | return false | |
| 241 | + | end |
| @@ -0,0 +1,130 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.NPC = {} | |
| 4 | + | ||
| 5 | + | context.NPC.talk = function(text) | |
| 6 | + | if g_game.getClientVersion() >= 810 then | |
| 7 | + | g_game.talkChannel(11, 0, text) | |
| 8 | + | else | |
| 9 | + | return context.say(text) | |
| 10 | + | end | |
| 11 | + | end | |
| 12 | + | context.NPC.say = context.NPC.talk | |
| 13 | + | ||
| 14 | + | context.NPC.isTrading = function() | |
| 15 | + | return modules.game_npctrade.npcWindow and modules.game_npctrade.npcWindow:isVisible() | |
| 16 | + | end | |
| 17 | + | context.NPC.hasTrade = context.NPC.isTrading | |
| 18 | + | context.NPC.hasTradeWindow = context.NPC.isTrading | |
| 19 | + | context.NPC.isTradeOpen = context.NPC.isTrading | |
| 20 | + | ||
| 21 | + | context.NPC.getSellItems = function() | |
| 22 | + | if not context.NPC.isTrading() then return {} end | |
| 23 | + | local items = {} | |
| 24 | + | for i, item in ipairs(modules.game_npctrade.tradeItems[modules.game_npctrade.SELL]) do | |
| 25 | + | table.insert(items, { | |
| 26 | + | item = item.ptr, | |
| 27 | + | id = item.ptr:getId(), | |
| 28 | + | count = item.ptr:getCount(), | |
| 29 | + | name = item.name, | |
| 30 | + | subType = item.ptr:getSubType(), | |
| 31 | + | weight = item.weight / 100, | |
| 32 | + | price = item.price | |
| 33 | + | }) | |
| 34 | + | end | |
| 35 | + | return items | |
| 36 | + | end | |
| 37 | + | ||
| 38 | + | context.NPC.getBuyItems = function() | |
| 39 | + | if not context.NPC.isTrading() then return {} end | |
| 40 | + | local items = {} | |
| 41 | + | for i, item in ipairs(modules.game_npctrade.tradeItems[modules.game_npctrade.BUY]) do | |
| 42 | + | table.insert(items, { | |
| 43 | + | item = item.ptr, | |
| 44 | + | id = item.ptr:getId(), | |
| 45 | + | count = item.ptr:getCount(), | |
| 46 | + | name = item.name, | |
| 47 | + | subType = item.ptr:getSubType(), | |
| 48 | + | weight = item.weight / 100, | |
| 49 | + | price = item.price | |
| 50 | + | }) | |
| 51 | + | end | |
| 52 | + | return items | |
| 53 | + | end | |
| 54 | + | ||
| 55 | + | context.NPC.getSellQuantity = function(item) | |
| 56 | + | if not context.NPC.isTrading() then return 0 end | |
| 57 | + | if type(item) == 'number' then | |
| 58 | + | item = Item.create(item) | |
| 59 | + | end | |
| 60 | + | return modules.game_npctrade.getSellQuantity(item) | |
| 61 | + | end | |
| 62 | + | ||
| 63 | + | context.NPC.canTradeItem = function(item) | |
| 64 | + | if not context.NPC.isTrading() then return false end | |
| 65 | + | if type(item) == 'number' then | |
| 66 | + | item = Item.create(item) | |
| 67 | + | end | |
| 68 | + | return modules.game_npctrade.canTradeItem(item) | |
| 69 | + | end | |
| 70 | + | ||
| 71 | + | context.NPC.sell = function(item, count, ignoreEquipped) | |
| 72 | + | if type(item) == 'number' then | |
| 73 | + | for i, entry in ipairs(context.NPC.getSellItems()) do | |
| 74 | + | if entry.id == item then | |
| 75 | + | item = entry.item | |
| 76 | + | break | |
| 77 | + | end | |
| 78 | + | end | |
| 79 | + | if type(item) == 'number' then | |
| 80 | + | item = Item.create(item) | |
| 81 | + | end | |
| 82 | + | end | |
| 83 | + | if count == 0 then | |
| 84 | + | count = 1 | |
| 85 | + | end | |
| 86 | + | if count == nil or count == -1 then | |
| 87 | + | count = context.NPC.getSellQuantity(item) | |
| 88 | + | end | |
| 89 | + | if ignoreEquipped == nil then | |
| 90 | + | ignoreEquipped = true | |
| 91 | + | end | |
| 92 | + | g_game.sellItem(item, count, ignoreEquipped) | |
| 93 | + | end | |
| 94 | + | ||
| 95 | + | context.NPC.buy = function(item, count, ignoreCapacity, withBackpack) | |
| 96 | + | if type(item) == 'number' then | |
| 97 | + | for i, entry in ipairs(context.NPC.getBuyItems()) do | |
| 98 | + | if entry.id == item then | |
| 99 | + | item = entry.item | |
| 100 | + | break | |
| 101 | + | end | |
| 102 | + | end | |
| 103 | + | if type(item) == 'number' then | |
| 104 | + | item = Item.create(item) | |
| 105 | + | end | |
| 106 | + | end | |
| 107 | + | if count == nil or count <= 0 then | |
| 108 | + | count = 1 | |
| 109 | + | end | |
| 110 | + | if ignoreCapacity == nil then | |
| 111 | + | ignoreCapacity = false | |
| 112 | + | end | |
| 113 | + | if withBackpack == nil then | |
| 114 | + | withBackpack = false | |
| 115 | + | end | |
| 116 | + | g_game.buyItem(item, count, ignoreCapacity, withBackpack) | |
| 117 | + | end | |
| 118 | + | ||
| 119 | + | context.NPC.sellAll = function() | |
| 120 | + | if not context.NPC.isTrading() then return false end | |
| 121 | + | modules.game_npctrade.sellAll() | |
| 122 | + | end | |
| 123 | + | ||
| 124 | + | context.NPC.closeTrade = function() | |
| 125 | + | modules.game_npctrade.closeNpcTrade() | |
| 126 | + | end | |
| 127 | + | context.NPC.close = context.NPC.closeTrade | |
| 128 | + | context.NPC.finish = context.NPC.closeTrade | |
| 129 | + | context.NPC.endTrade = context.NPC.closeTrade | |
| 130 | + | context.NPC.finishTrade = context.NPC.closeTrade |
| @@ -0,0 +1,166 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.name = function() return context.player:getName() end | |
| 4 | + | ||
| 5 | + | context.hp = function() return context.player:getHealth() end | |
| 6 | + | context.mana = function() return context.player:getMana() end | |
| 7 | + | context.hppercent = function() return context.player:getHealthPercent() end | |
| 8 | + | context.manapercent = function() if context.player:getMaxMana() <= 1 then return 100 else return math.floor(context.player:getMana() * 100 / context.player:getMaxMana()) end end | |
| 9 | + | context.maxhp = function() return context.player:getMaxHealth() end | |
| 10 | + | context.maxmana = function() return context.player:getMaxMana() end | |
| 11 | + | context.hpmax = function() return context.player:getMaxHealth() end | |
| 12 | + | context.manamax = function() return context.player:getMaxMana() end | |
| 13 | + | ||
| 14 | + | context.cap = function() return context.player:getCapacity() end | |
| 15 | + | context.freecap = function() return context.player:getFreeCapacity() end | |
| 16 | + | context.maxcap = function() return context.player:getTotalCapacity() end | |
| 17 | + | context.capmax = function() return context.player:getTotalCapacity() end | |
| 18 | + | ||
| 19 | + | context.exp = function() return context.player:getExperience() end | |
| 20 | + | context.lvl = function() return context.player:getLevel() end | |
| 21 | + | context.level = function() return context.player:getLevel() end | |
| 22 | + | ||
| 23 | + | context.mlev = function() return context.player:getMagicLevel() end | |
| 24 | + | context.magic = function() return context.player:getMagicLevel() end | |
| 25 | + | context.mlevel = function() return context.player:getMagicLevel() end | |
| 26 | + | ||
| 27 | + | context.soul = function() return context.player:getSoul() end | |
| 28 | + | context.stamina = function() return context.player:getStamina() end | |
| 29 | + | context.voc = function() return context.player:getVocation() end | |
| 30 | + | context.vocation = function() return context.player:getVocation() end | |
| 31 | + | ||
| 32 | + | context.bless = function() return context.player:getBlessings() end | |
| 33 | + | context.blesses = function() return context.player:getBlessings() end | |
| 34 | + | context.blessings = function() return context.player:getBlessings() end | |
| 35 | + | ||
| 36 | + | ||
| 37 | + | context.pos = function() return context.player:getPosition() end | |
| 38 | + | context.posx = function() return context.player:getPosition().x end | |
| 39 | + | context.posy = function() return context.player:getPosition().y end | |
| 40 | + | context.posz = function() return context.player:getPosition().z end | |
| 41 | + | ||
| 42 | + | context.direction = function() return context.player:getDirection() end | |
| 43 | + | context.speed = function() return context.player:getSpeed() end | |
| 44 | + | context.skull = function() return context.player:getSkull() end | |
| 45 | + | context.outfit = function() return context.player:getOutfit() end | |
| 46 | + | ||
| 47 | + | context.setOutfit = function(outfit) | |
| 48 | + | modules.game_outfit.ignoreNextOutfitWindow = g_clock.millis() | |
| 49 | + | g_game.requestOutfit() | |
| 50 | + | context.schedule(100, function() | |
| 51 | + | g_game.changeOutfit(outfit) | |
| 52 | + | end) | |
| 53 | + | end | |
| 54 | + | context.changeOutfit = context.setOutfit | |
| 55 | + | context.setSpeed = function(value) context.player:setSpeed(value) end | |
| 56 | + | ||
| 57 | + | context.walk = function(dir) return modules.game_walking.walk(dir) end | |
| 58 | + | context.turn = function(dir) return g_game.turn(dir) end | |
| 59 | + | ||
| 60 | + | -- game releated | |
| 61 | + | context.getChannels = function() | |
| 62 | + | -- return { channelId = channelName } | |
| 63 | + | return modules.game_console.channels | |
| 64 | + | end | |
| 65 | + | context.getChannelId = function(name) | |
| 66 | + | for id, channel in pairs(context.getChannels()) do | |
| 67 | + | if name:lower() == channel:lower() then | |
| 68 | + | return id | |
| 69 | + | end | |
| 70 | + | end | |
| 71 | + | return nil | |
| 72 | + | end | |
| 73 | + | context.getChannel = context.getChannelId | |
| 74 | + | ||
| 75 | + | context.say = g_game.talk | |
| 76 | + | context.talk = g_game.talk | |
| 77 | + | context.yell = function(text) g_game.talkChannel(3, 0, text) end | |
| 78 | + | context.talkChannel = function(channel, text) g_game.talkChannel(7, channel, text) end | |
| 79 | + | context.sayChannel = context.talkChannel | |
| 80 | + | context.talkPrivate = function(receiver, text) g_game.talkPrivate(5, receiver, text) end | |
| 81 | + | context.sayPrivate = context.talkPrivate | |
| 82 | + | ||
| 83 | + | context.talkNpc = function(text) | |
| 84 | + | if g_game.getClientVersion() >= 810 then | |
| 85 | + | g_game.talkChannel(11, 0, text) | |
| 86 | + | else | |
| 87 | + | return context.say(text) | |
| 88 | + | end | |
| 89 | + | end | |
| 90 | + | context.sayNpc = context.talkNpc | |
| 91 | + | context.sayNPC = context.talkNpc | |
| 92 | + | context.talkNPC = context.talkNpc | |
| 93 | + | ||
| 94 | + | context.saySpell = function(text, lastSpellTimeout) | |
| 95 | + | if not text or text:len() < 1 then | |
| 96 | + | return | |
| 97 | + | end | |
| 98 | + | if context.lastSpell == nil then | |
| 99 | + | context.lastSpell = 0 | |
| 100 | + | end | |
| 101 | + | if not lastSpellTimeout then | |
| 102 | + | lastSpellTimeout = 1000 | |
| 103 | + | end | |
| 104 | + | if context.lastSpell + lastSpellTimeout > context.now then | |
| 105 | + | return false | |
| 106 | + | end | |
| 107 | + | context.say(text) | |
| 108 | + | context.lastSpell = context.now | |
| 109 | + | return true | |
| 110 | + | end | |
| 111 | + | ||
| 112 | + | context.setSpellTimeout = function() | |
| 113 | + | context.lastSpell = context.now | |
| 114 | + | end | |
| 115 | + | ||
| 116 | + | context.use = function(thing, subtype) | |
| 117 | + | if type(thing) == 'number' then | |
| 118 | + | return g_game.useInventoryItem(thing, subtype) | |
| 119 | + | else | |
| 120 | + | return g_game.use(thing) | |
| 121 | + | end | |
| 122 | + | end | |
| 123 | + | context.usewith = function(thing, target, subtype) | |
| 124 | + | if type(thing) == 'number' then | |
| 125 | + | return g_game.useInventoryItemWith(thing, target, subtype) | |
| 126 | + | else | |
| 127 | + | return g_game.useWith(item, target, subtype) | |
| 128 | + | end | |
| 129 | + | end | |
| 130 | + | context.useWith = context.usewith | |
| 131 | + | ||
| 132 | + | context.useRune = function(itemid, target, lastSpellTimeout) | |
| 133 | + | if context.lastRuneUse == nil then | |
| 134 | + | context.lastRuneUse = 0 | |
| 135 | + | end | |
| 136 | + | if not lastRuneTimeout then | |
| 137 | + | lastRuneTimeout = 1000 | |
| 138 | + | end | |
| 139 | + | if context.lastRuneUse + lastRuneTimeout > context.now then | |
| 140 | + | return false | |
| 141 | + | end | |
| 142 | + | context.usewith(itemid, target) | |
| 143 | + | context.lastRuneUse = context.now | |
| 144 | + | return true | |
| 145 | + | end | |
| 146 | + | context.userune = context.useRune | |
| 147 | + | ||
| 148 | + | context.findItem = function(itemId, subType) | |
| 149 | + | if subType == nil then | |
| 150 | + | subType = -1 | |
| 151 | + | end | |
| 152 | + | return g_game.findItemInContainers(itemId, subType) | |
| 153 | + | end | |
| 154 | + | ||
| 155 | + | context.attack = g_game.attack | |
| 156 | + | context.cancelAttack = g_game.cancelAttack | |
| 157 | + | context.follow = g_game.follow | |
| 158 | + | context.cancelFollow = g_game.cancelFollow | |
| 159 | + | context.cancelAttackAndFollow = g_game.cancelAttackAndFollow | |
| 160 | + | ||
| 161 | + | context.logout = g_game.forceLogout | |
| 162 | + | context.ping = g_game.getPing | |
| 163 | + | ||
| 164 | + | modules.game_cooldown.isGroupCooldownIconActive(id) | |
| 165 | + | modules.game_cooldown.isCooldownIconActive(id) | |
| 166 | + |
| @@ -0,0 +1,32 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | for i, state in ipairs(PlayerStates) do | |
| 4 | + | context[state] = state | |
| 5 | + | end | |
| 6 | + | ||
| 7 | + | context.hasCondition = function(condition) return bit.band(context.player:getStates(), condition) > 0 end | |
| 8 | + | ||
| 9 | + | context.isPoisioned = function() return context.hasCondition(PlayerStates.Poison) end | |
| 10 | + | context.isBurnining = function() return context.hasCondition(PlayerStates.Burn) end | |
| 11 | + | context.isEnergized = function() return context.hasCondition(PlayerStates.Energy) end | |
| 12 | + | context.isDrunk = function() return context.hasCondition(PlayerStates.Drunk) end | |
| 13 | + | context.hasManaShield = function() return context.hasCondition(PlayerStates.ManaShield) end | |
| 14 | + | context.isParalyzed = function() return context.hasCondition(PlayerStates.Paralyze) end | |
| 15 | + | context.hasHaste = function() return context.hasCondition(PlayerStates.Haste) end | |
| 16 | + | context.hasSwords = function() return context.hasCondition(PlayerStates.Swords) end | |
| 17 | + | context.isInFight = function() return context.hasCondition(PlayerStates.Swords) end | |
| 18 | + | context.canLogout = function() return not context.hasCondition(PlayerStates.Swords) end | |
| 19 | + | context.isDrowning = function() return context.hasCondition(PlayerStates.Drowning) end | |
| 20 | + | context.isFreezing = function() return context.hasCondition(PlayerStates.Freezing) end | |
| 21 | + | context.isDazzled = function() return context.hasCondition(PlayerStates.Dazzled) end | |
| 22 | + | context.isCursed = function() return context.hasCondition(PlayerStates.Cursed) end | |
| 23 | + | context.hasPartyBuff = function() return context.hasCondition(PlayerStates.PartyBuff) end | |
| 24 | + | context.hasPzLock = function() return context.hasCondition(PlayerStates.PzBlock) end | |
| 25 | + | context.hasPzBlock = function() return context.hasCondition(PlayerStates.PzBlock) end | |
| 26 | + | context.isPzLocked = function() return context.hasCondition(PlayerStates.PzBlock) end | |
| 27 | + | context.isPzBlocked = function() return context.hasCondition(PlayerStates.PzBlock) end | |
| 28 | + | context.isInProtectionZone = function() return context.hasCondition(PlayerStates.Pz) end | |
| 29 | + | context.hasPz = function() return context.hasCondition(PlayerStates.Pz) end | |
| 30 | + | context.isInPz = function() return context.hasCondition(PlayerStates.Pz) end | |
| 31 | + | context.isBleeding = function() return context.hasCondition(PlayerStates.Bleeding) end | |
| 32 | + | context.isHungry = function() return context.hasCondition(PlayerStates.Hungry) end |
| @@ -0,0 +1,45 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.SlotOther = InventorySlotOther | |
| 4 | + | context.SlotHead = InventorySlotHead | |
| 5 | + | context.SlotNeck = InventorySlotNeck | |
| 6 | + | context.SlotBack = InventorySlotBack | |
| 7 | + | context.SlotBody = InventorySlotBody | |
| 8 | + | context.SlotRight = InventorySlotRight | |
| 9 | + | context.SlotLeft = InventorySlotLeft | |
| 10 | + | context.SlotLeg = InventorySlotLeg | |
| 11 | + | context.SlotFeet = InventorySlotFeet | |
| 12 | + | context.SlotFinger = InventorySlotFinger | |
| 13 | + | context.SlotAmmo = InventorySlotAmmo | |
| 14 | + | context.SlotPurse = InventorySlotPurse | |
| 15 | + | ||
| 16 | + | context.getInventoryItem = function(slot) return context.player:getInventoryItem(slot) end | |
| 17 | + | context.getSlot = context.getInventoryItem | |
| 18 | + | ||
| 19 | + | context.getHead = function() return context.getInventoryItem(context.SlotHead) end | |
| 20 | + | context.getNeck = function() return context.getInventoryItem(context.SlotNeck) end | |
| 21 | + | context.getBack = function() return context.getInventoryItem(context.SlotBack) end | |
| 22 | + | context.getBody = function() return context.getInventoryItem(context.SlotBody) end | |
| 23 | + | context.getRight = function() return context.getInventoryItem(context.SlotRight) end | |
| 24 | + | context.getLeft = function() return context.getInventoryItem(context.SlotLeft) end | |
| 25 | + | context.getLeg = function() return context.getInventoryItem(context.SlotLeg) end | |
| 26 | + | context.getFeet = function() return context.getInventoryItem(context.SlotFeet) end | |
| 27 | + | context.getFinger = function() return context.getInventoryItem(context.SlotFinger) end | |
| 28 | + | context.getAmmo = function() return context.getInventoryItem(context.SlotAmmo) end | |
| 29 | + | context.getPurse = function() return context.getInventoryItem(context.SlotPurse) end | |
| 30 | + | ||
| 31 | + | context.getContainers = function() return g_game.getContainers() end | |
| 32 | + | context.getContainer = function(index) return g_game.getContainer(index) end | |
| 33 | + | ||
| 34 | + | context.moveToSlot = function(item, slot, count) | |
| 35 | + | if type(item) == 'number' then | |
| 36 | + | item = context.findItem(item) | |
| 37 | + | end | |
| 38 | + | if not item then | |
| 39 | + | return | |
| 40 | + | end | |
| 41 | + | if count == nil then | |
| 42 | + | count = item:getCount() | |
| 43 | + | end | |
| 44 | + | return g_game.move(item, {x=65535, y=slot, z=0}, count) | |
| 45 | + | end |
| @@ -0,0 +1,59 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.loadScript = function(path, onLoadCallback) | |
| 4 | + | if type(path) ~= 'string' then | |
| 5 | + | return context.error("Invalid path for loadScript: " .. tostring(path)) | |
| 6 | + | end | |
| 7 | + | if path:lower():find("http") == 1 then | |
| 8 | + | return context.loadRemoteScript(path) | |
| 9 | + | end | |
| 10 | + | if not g_resources.fileExists(path) then | |
| 11 | + | return context.error("File " .. path .. " doesn't exist") | |
| 12 | + | end | |
| 13 | + | ||
| 14 | + | local status, result = pcall(function() | |
| 15 | + | assert(load(g_resources.readFileContents(path), path, nil, context))() | |
| 16 | + | end) | |
| 17 | + | if not status then | |
| 18 | + | return context.error("Error while loading script from: " .. path .. ":\n" .. result) | |
| 19 | + | end | |
| 20 | + | if onLoadCallback then | |
| 21 | + | onLoadCallback() | |
| 22 | + | end | |
| 23 | + | end | |
| 24 | + | ||
| 25 | + | context.loadRemoteScript = function(url, onLoadCallback) | |
| 26 | + | if type(url) ~= 'string' or url:lower():find("http") ~= 1 then | |
| 27 | + | return context.error("Invalid url for loadRemoteScript: " .. tostring(url)) | |
| 28 | + | end | |
| 29 | + | ||
| 30 | + | HTTP.get(url, function(data, err) | |
| 31 | + | if err or data:len() == 0 then | |
| 32 | + | -- try to load from cache | |
| 33 | + | if type(context.storage.scriptsCache) ~= 'table' then | |
| 34 | + | context.storage.scriptsCache = {} | |
| 35 | + | end | |
| 36 | + | local cache = context.storage.scriptsCache[url] | |
| 37 | + | if cache and type(cache) == 'string' and cache:len() > 0 then | |
| 38 | + | data = cache | |
| 39 | + | else | |
| 40 | + | return context.error("Can't load script from: " .. url .. ", error: " .. err) | |
| 41 | + | end | |
| 42 | + | end | |
| 43 | + | ||
| 44 | + | local status, result = pcall(function() | |
| 45 | + | assert(load(data, url, nil, context))() | |
| 46 | + | end) | |
| 47 | + | if not status then | |
| 48 | + | return context.error("Error while loading script from: " .. url .. ":\n" .. result) | |
| 49 | + | end | |
| 50 | + | -- cache script | |
| 51 | + | if type(context.storage.scriptsCache) ~= 'table' then | |
| 52 | + | context.storage.scriptsCache = {} | |
| 53 | + | end | |
| 54 | + | context.storage.scriptsCache[url] = data | |
| 55 | + | if onLoadCallback then | |
| 56 | + | onLoadCallback() | |
| 57 | + | end | |
| 58 | + | end) | |
| 59 | + | end |
| @@ -0,0 +1,91 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.BotServer = {} | |
| 4 | + | context.BotServer.url = "ws://bot.otclient.ovh:8000/" | |
| 5 | + | context.BotServer.timeout = 3 | |
| 6 | + | context.BotServer.ping = 0 | |
| 7 | + | context.BotServer._callbacks = {} | |
| 8 | + | context.BotServer._lastMessageId = 0 | |
| 9 | + | context.BotServer._wasConnected = true -- show first warning | |
| 10 | + | ||
| 11 | + | context.BotServer.init = function(name, channel) | |
| 12 | + | if not channel or not name or channel:len() < 1 or name:len() < 1 then | |
| 13 | + | return context.error("Invalid params for BotServer.init") | |
| 14 | + | end | |
| 15 | + | if context.BotServer._websocket then | |
| 16 | + | return context.error("BotServer is already initialized") | |
| 17 | + | end | |
| 18 | + | context.BotServer._websocket = HTTP.WebSocketJSON(context.BotServer.url, { | |
| 19 | + | onMessage = function(message, socketId) | |
| 20 | + | if not context._websockets[socketId] then | |
| 21 | + | return g_http.cancel(socketId) | |
| 22 | + | end | |
| 23 | + | if not context.BotServer._websocket or context.BotServer._websocket.id ~= socketId then | |
| 24 | + | return g_http.cancel(socketId) | |
| 25 | + | end | |
| 26 | + | context.BotServer._wasConnected = true | |
| 27 | + | if message["type"] == "ping" then | |
| 28 | + | context.BotServer.ping = message["ping"] | |
| 29 | + | return context.BotServer._websocket.send({type="ping"}) | |
| 30 | + | end | |
| 31 | + | if message["type"] == "message" then | |
| 32 | + | context.BotServer._lastMessageId = message["id"] | |
| 33 | + | local topics = context.BotServer._callbacks[message["topic"]] | |
| 34 | + | if topics then | |
| 35 | + | for i=1,#topics do | |
| 36 | + | topics[i](message["name"], message["message"], message["topic"]) | |
| 37 | + | end | |
| 38 | + | end | |
| 39 | + | topics = context.BotServer._callbacks["*"] | |
| 40 | + | if topics then | |
| 41 | + | for i=1,#topics do | |
| 42 | + | topics[i](message["name"], message["message"], message["topic"]) | |
| 43 | + | end | |
| 44 | + | end | |
| 45 | + | return | |
| 46 | + | end | |
| 47 | + | end, | |
| 48 | + | onClose = function(message, socketId) | |
| 49 | + | if not context._websockets[socketId] then | |
| 50 | + | return | |
| 51 | + | end | |
| 52 | + | context._websockets[socketId] = nil | |
| 53 | + | if not context.BotServer._websocket or context.BotServer._websocket.id ~= socketId then | |
| 54 | + | return | |
| 55 | + | end | |
| 56 | + | if context.BotServer._wasConnected then | |
| 57 | + | context.warn("BotServer disconnected") | |
| 58 | + | end | |
| 59 | + | context.BotServer._wasConnected = false | |
| 60 | + | context.BotServer._websocket = nil | |
| 61 | + | context.BotServer.ping = 0 | |
| 62 | + | context.BotServer.init(name, channel) | |
| 63 | + | end | |
| 64 | + | }, context.BotServer.timeout) | |
| 65 | + | context._websockets[context.BotServer._websocket.id] = 1 | |
| 66 | + | context.BotServer._websocket.send({type="init", name=name, channel=channel, lastMessage=context.BotServer._lastMessageId}) | |
| 67 | + | end | |
| 68 | + | ||
| 69 | + | context.BotServer.terminate = function() | |
| 70 | + | if context.BotServer._websocket then | |
| 71 | + | context.BotServer._websocket:close() | |
| 72 | + | context.BotServer._websocket = nil | |
| 73 | + | end | |
| 74 | + | end | |
| 75 | + | ||
| 76 | + | context.BotServer.listen = function(topic, callback) -- callback = function(name, message, topic) -- message is parsed json = table | |
| 77 | + | if not context.BotServer._websocket then | |
| 78 | + | return context.error("BotServer is not initialized") | |
| 79 | + | end | |
| 80 | + | if not context.BotServer._callbacks[topic] then | |
| 81 | + | context.BotServer._callbacks[topic] = {} | |
| 82 | + | end | |
| 83 | + | table.insert(context.BotServer._callbacks[topic], callback) | |
| 84 | + | end | |
| 85 | + | ||
| 86 | + | context.BotServer.send = function(topic, message) | |
| 87 | + | if not context.BotServer._websocket then | |
| 88 | + | return context.error("BotServer is not initialized") | |
| 89 | + | end | |
| 90 | + | context.BotServer._websocket.send({type="message", topic=topic, message=message}) | |
| 91 | + | end |
| @@ -0,0 +1,31 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.getSoundChannel = function() | |
| 4 | + | if not g_sounds then | |
| 5 | + | return | |
| 6 | + | end | |
| 7 | + | return g_sounds.getChannel(SoundChannels.Bot) | |
| 8 | + | end | |
| 9 | + | ||
| 10 | + | context.playSound = function(file) | |
| 11 | + | local botSoundChannel = context.getSoundChannel() | |
| 12 | + | if not botSoundChannel then | |
| 13 | + | return | |
| 14 | + | end | |
| 15 | + | botSoundChannel:setEnabled(true) | |
| 16 | + | botSoundChannel:stop(0) | |
| 17 | + | botSoundChannel:play(file, 0, 1.0) | |
| 18 | + | return botSoundChannel | |
| 19 | + | end | |
| 20 | + | ||
| 21 | + | context.stopSound = function() | |
| 22 | + | local botSoundChannel = context.getSoundChannel() | |
| 23 | + | if not botSoundChannel then | |
| 24 | + | return | |
| 25 | + | end | |
| 26 | + | botSoundChannel:stop() | |
| 27 | + | end | |
| 28 | + | ||
| 29 | + | context.playAlarm = function() | |
| 30 | + | return context.playSound("/sounds/alarm.ogg") | |
| 31 | + | end |
| @@ -0,0 +1,3 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.test = function() return context.info("test") end |
| @@ -0,0 +1,19 @@ | |||
| 1 | + | local context = G.botContext | |
| 2 | + | ||
| 3 | + | context.encode = function(data, indent) return json.encode(data, indent or 2) end | |
| 4 | + | context.decode = function(text) local status, result = pcall(function() return json.decode(text) end) if status then return result end return {} end | |
| 5 | + | ||
| 6 | + | context.displayGeneralBox = function(title, message, buttons, onEnterCallback, onEscapeCallback) | |
| 7 | + | local box = displayGeneralBox(title, message, buttons, onEnterCallback, onEscapeCallback) | |
| 8 | + | box.botWidget = true | |
| 9 | + | return box | |
| 10 | + | end | |
| 11 | + | ||
| 12 | + | context.doScreenshot = function(filename) | |
| 13 | + | g_app.doScreenshot(filename) | |
| 14 | + | end | |
| 15 | + | context.screenshot = context.doScreenshot | |
| 16 | + | ||
| 17 | + | context.getVersion = function() | |
| 18 | + | return g_app.getVersion() | |
| 19 | + | end |