Version 2.3 - cooldowns in action bar, more advanced bot, new cavebot, bug fixes

This commit is contained in:
OTCv8 2020-04-15 01:22:06 +02:00
parent ed8162a9d5
commit 9a4ab2ae3b
68 changed files with 3261 additions and 428 deletions

View File

@ -1,7 +1,8 @@
<?php
// set chmod 777 to dir with this file to create checksum files
$data = file_get_contents("php://input");
$data = json_decode($data);
// set write permission or chmod 777 to dir with this file to let it create checksum files
$data = json_decode(file_get_contents("php://input"));
$platform = "";
$version = 0;
if(!empty($data)) {
$platform = $data->platform;
$version = $data->version; // currently not used
@ -19,7 +20,6 @@ if($platform == "WIN32-WGL") { // opengl
}
$data_dir = "/var/www/otclient/files";
$things_dir = "/data/things"; // files from that dir won't be downloaded automaticly, you can set it to null to download everything automaticly (useful if you have only 1 version of data/sprites)
$files_url = "http://otclient.ovh/files";
$update_checksum_interval = 60; // caling updater 100x/s would lag disc, we need to cache it
$main_files_and_dirs = array("data", "modules", "layouts", "init.lua"); // used to ignore other files/dirs in data_dir
@ -48,23 +48,18 @@ function updateChecksums() {
global $binary_path;
global $checksums_file;
global $data;
global $things_dir;
$ret = array();
$data_dir_realpath = realpath($data_dir);
$files = getDirFiles($data_dir);
foreach($files as $file) {
$relative_path = str_replace($data_dir_realpath, "", $file);
$ps = explode("/", $relative_path);
$ps = explode(DIRECTORY_SEPARATOR, $relative_path);
if($relative_path == $binary_path || (count($ps) >= 2 && in_array($ps[1], $main_files_and_dirs)))
$ret[$relative_path] = md5_file($file);
}
foreach($ret as $file => $checksum) {
if($things_dir != null && !empty($things_dir) && strpos($file, $things_dir) === 0) {
$data["things"][$file] = $checksum;
} else {
$data["files"][$file] = $checksum;
}
$data["files"][$file] = $checksum;
}
$ret = json_encode($data);
if(file_put_contents($checksums_file, $ret) === FALSE) {
@ -84,7 +79,8 @@ if (function_exists('sem_get')) {
sem_acquire($semaphore);
}
$ft = filemtime($checksums_file);
$ft = file_exists($checksums_file) ? filemtime($checksums_file) : false;
if($ft === false || $ft + $update_checksum_interval < time()) {
echo updateChecksums();
} else {

2
data/things/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -23,6 +23,8 @@ Servers = {
USE_NEW_ENERGAME = false -- not done yet
ALLOW_CUSTOM_SERVERS = true -- if true it shows option ANOTHER on server list
g_app.setName("OTCv8")
-- CONFIG END
-- print first terminal message

View File

@ -54,6 +54,8 @@ end
function init()
connect(g_app, { onRun = startup,
onExit = exit })
connect(g_game, { onGameStart = onGameStart,
onGameEnd = onGameEnd })
g_window.setMinimumSize({ width = 800, height = 600 })
if g_sounds ~= nil then
@ -102,6 +104,8 @@ end
function terminate()
disconnect(g_app, { onRun = startup,
onExit = exit })
disconnect(g_game, { onGameStart = onGameStart,
onGameEnd = onGameEnd })
-- save window configs
g_settings.set('window-size', g_window.getUnmaximizedSize())
g_settings.set('window-pos', g_window.getUnmaximizedPos())
@ -111,3 +115,13 @@ end
function exit()
g_logger.info("Exiting application..")
end
function onGameStart()
local player = g_game.getLocalPlayer()
if not player then return end
g_window.setTitle(g_app.getName() .. " - " .. player:getName())
end
function onGameEnd()
g_window.setTitle(g_app.getName())
end

View File

@ -112,7 +112,7 @@ EnterGameWindow
MenuLabel
id: serverLabel
!text: tr('IP:PORT or url')
!text: tr('IP:PORT')
anchors.left: prev.left
anchors.top: prev.bottom
margin-top: 8

View File

@ -154,7 +154,7 @@ function sendStats()
g_stats.clearSlow(i - 1)
end
data.widgets = g_stats.getWidgetsInfo(10, false)
data = json.encode(data)
data = json.encode(data, 1)
if Services.stats ~= nil and Services.stats:len() > 3 then
g_http.post(Services.stats, data)
end

View File

@ -337,7 +337,7 @@ function executeCommand(command)
-- detect and convert commands with simple syntax
local realCommand
if string.sub(command, 1, 1) == '=' then
realCommand = 'print(' .. string.sub(command,2) .. ')'
realCommand = 'print(tostring(' .. string.sub(command,2) .. '))'
else
realCommand = command
end
@ -363,6 +363,8 @@ function executeCommand(command)
addLine('ERROR: incorrect lua syntax: ' .. err:sub(5), 'red')
return
end
commandEnv['player'] = g_game.getLocalPlayer()
-- setup func env to commandEnv
setfenv(func, commandEnv)

View File

@ -1,7 +1,7 @@
--
-- json.lua
--
-- Copyright (c) 2018 rxi
-- Copyright (c) 2019 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
@ -46,26 +46,40 @@ for k, v in pairs(escape_char_map) do
end
local function make_indent(state)
return string.rep(" ", state.currentIndentLevel * state.indent)
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
local function encode_nil()
return "null"
end
local function encode_table(val, stack)
local function encode_table(val, state)
local res = {}
stack = stack or {}
local stack = state.stack
local pretty = state.indent > 0
local close_indent = make_indent(state)
local comma = pretty and ",\n" or ","
local colon = pretty and ": " or ":"
local open_brace = pretty and "{\n" or "{"
local close_brace = pretty and ("\n" .. close_indent .. "}") or "}"
local open_bracket = pretty and "[\n" or "["
local close_bracket = pretty and ("\n" .. close_indent .. "]") or "]"
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
@ -78,11 +92,13 @@ local function encode_table(val, stack)
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
for _, v in ipairs(val) do
state.currentIndentLevel = state.currentIndentLevel + 1
table.insert(res, make_indent(state) .. encode(v, state))
state.currentIndentLevel = state.currentIndentLevel - 1
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
return open_bracket .. table.concat(res, comma) .. close_bracket
else
-- Treat as an object
@ -90,10 +106,12 @@ local function encode_table(val, stack)
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
state.currentIndentLevel = state.currentIndentLevel + 1
table.insert(res, make_indent(state) .. encode(k, state) .. colon .. encode(v, state))
state.currentIndentLevel = state.currentIndentLevel - 1
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
return open_brace .. table.concat(res, comma) .. close_brace
end
end
@ -121,18 +139,22 @@ local type_func_map = {
}
encode = function(val, stack)
encode = function(val, state)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
return f(val, state)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
function json.encode(val, indent)
local state = {
indent = indent or 0,
currentIndentLevel = 0,
stack = {}
}
return encode(val, state)
end

View File

@ -155,3 +155,9 @@ end
function UITabBar:getTabsPanel()
return table.collect(self.tabs, function(_,tab) return tab.tabPanel end)
end
function UITabBar:clearTabs()
while #self.tabs > 0 do
self:removeTab(self.tabs[#self.tabs])
end
end

View File

@ -34,8 +34,10 @@ function init()
connect(g_game, {
onGameStart = online,
onGameEnd = offline
})
onGameEnd = offline,
onSpellGroupCooldown = onSpellGroupCooldown,
onSpellCooldown = onSpellCooldown
})
if g_game.isOnline() then
online()
@ -45,14 +47,16 @@ end
function terminate()
disconnect(g_game, {
onGameStart = online,
onGameEnd = offline
onGameEnd = offline,
onSpellGroupCooldown = onSpellGroupCooldown,
onSpellCooldown = onSpellCooldown
})
saveConfig()
-- remove hotkeys
offline()
-- remove hotkeys, also saves config
if actionPanel1.tabBar:getChildCount() > 0 and actionPanel2.tabBar:getChildCount() > 0 then
offline()
end
actionPanel1:destroy()
actionPanel2:destroy()
end
@ -90,16 +94,27 @@ function offline()
hotkeyAssignWindow:destroy()
hotkeyAssignWindow = nil
end
saveConfig()
local gameRootPanel = modules.game_interface.getRootPanel()
for index, panel in ipairs({actionPanel1, actionPanel2}) do
local config = {}
for i, child in ipairs(panel.tabBar:getChildren()) do
local gameRootPanel = modules.game_interface.getRootPanel()
if child.hotkey then
g_keyboard.unbindKeyPress(child.hotkey, child.callback, gameRootPanel)
if child.config then
table.insert(config, child.config)
if type(child.config.hotkey) == 'string' and child.config.hotkey:len() > 0 then
g_keyboard.unbindKeyPress(child.config.hotkey, child.callback, gameRootPanel)
end
else
table.insert(config, {})
end
if child.cooldownEvent then
removeEvent(child.cooldownEvent)
end
end
actionConfig:setNode('actions_' .. index, config)
panel.tabBar:destroyChildren()
end
actionConfig:save()
end
function setupActionPanel(index, panel)
@ -108,10 +123,11 @@ function setupActionPanel(index, panel)
for i, buttonConfig in pairs(rawConfig) do -- sorting, because key in rawConfig is string
config[tonumber(i)] = buttonConfig
end
panel.tabBar:destroyChildren()
for i=1,actionButtonsInPanel do
local action = g_ui.createWidget('ActionButton', panel.tabBar)
setupAction(index, action, config[i])
action.config = config[i] or {}
setupAction(action)
end
panel.nextButton.onClick = function()
@ -122,28 +138,13 @@ function setupActionPanel(index, panel)
end
end
function saveConfig()
for index, panel in ipairs({actionPanel1, actionPanel2}) do
local config = {}
for i, child in ipairs(panel.tabBar:getChildren()) do
table.insert(config, {
text = child.text:getText(),
item = child.item:getItemId(),
count = child.item:getItemCount(),
action = child.actionType,
hotkey = child.hotkey
})
end
actionConfig:setNode('actions_' .. index, config)
end
actionConfig:save()
end
function setupAction(index, action, config)
function setupAction(action)
local config = action.config
action.item:setShowCount(false)
action.onMouseRelease = actionOnMouseRelease
action.callback = function(k, c, ticks) executeAction(action, ticks) end
action.item.onItemChange = nil -- disable callbacks for setup
if config then
if type(config.text) == 'number' then
config.text = tostring(config.text)
@ -151,43 +152,204 @@ function setupAction(index, action, config)
if type(config.hotkey) == 'number' then
config.hotkey = tostring(config.hotkey)
end
action.hotkey = config.hotkey
if type(action.hotkey) == 'string' and action.hotkey:len() > 0 then
if type(config.hotkey) == 'string' and config.hotkey:len() > 0 then
local gameRootPanel = modules.game_interface.getRootPanel()
g_keyboard.bindKeyPress(action.hotkey, action.callback, gameRootPanel)
g_keyboard.bindKeyPress(config.hotkey, action.callback, gameRootPanel)
action.hotkeyLabel:setText(config.hotkey)
else
action.hotkeyLabel:setText("")
end
action.hotkeyLabel:setText(action.hotkey or "")
action.text:setText(config.text)
if action.text:getText():len() > 0 then
action.text:setImageSource("")
action.cooldownTill = 0
action.cooldownStart = 0
if type(config.text) == 'string' and config.text:len() > 0 then
action.text:setText(config.text)
action:setBorderColor(ActionColors.text)
action.item:setOn(true) -- removes background
if Spells then
local spell, profile = Spells.getSpellByWords(config.text:lower())
action.spell = spell
if action.spell and action.spell.icon and profile then
action.text:setImageSource(SpelllistSettings[profile].iconFile)
action.text:setImageClip(Spells.getImageClip(SpellIcons[action.spell.icon][1], profile))
action.text:setText("")
end
end
else
action.text:setText("")
action.spell = nil
if type(config.item) == 'number' and config.item > 100 then
action.item:setOn(true)
action.item:setItemId(config.item)
action.item:setItemCount(config.count or 1)
setupActionType(action, config.actionType)
else
action.item:setItemId(0)
action.item:setOn(false)
action:setBorderColor(ActionColors.empty)
end
end
if config.item > 0 then
setupActionType(action, config.action)
end
action.item:setOn(config.item > 0)
action.item:setItemId(config.item)
action.item:setItemCount(config.count)
end
action.item.onItemChange = actionOnItemChange
end
function setupActionType(action, actionType)
action.actionType = actionType
if action.actionType == ActionTypes.USE then
local item = action.item:getItem()
if action.item:getItem():isMultiUse() then
if not actionType or actionType <= ActionTypes.USE then
actionType = ActionTypes.USE_WITH
end
elseif g_game.getClientVersion() >= 910 then
if actionType ~= ActionTypes.USE and actionType ~= ActionTypes.EQUIP then
actionType = ActionTypes.USE
end
else
actionType = ActionTypes.USE
end
action.config.actionType = actionType
if action.config.actionType == ActionTypes.USE then
action:setBorderColor(ActionColors.itemUse)
elseif action.actionType == ActionTypes.USE_SELF then
elseif action.config.actionType == ActionTypes.USE_SELF then
action:setBorderColor(ActionColors.itemUseSelf)
elseif action.actionType == ActionTypes.USE_TARGET then
elseif action.config.actionType == ActionTypes.USE_TARGET then
action:setBorderColor(ActionColors.itemUseTarget)
elseif action.actionType == ActionTypes.USE_WITH then
elseif action.config.actionType == ActionTypes.USE_WITH then
action:setBorderColor(ActionColors.itemUseWith)
elseif action.actionType == ActionTypes.EQUIP then
elseif action.config.actionType == ActionTypes.EQUIP then
action:setBorderColor(ActionColors.itemEquip)
end
end
function updateAction(action, newConfig)
local config = action.config
if newConfig.hotkey and type(config.hotkey) == 'string' and config.hotkey:len() > 0 then
local gameRootPanel = modules.game_interface.getRootPanel()
g_keyboard.unbindKeyPress(config.hotkey, action.callback, gameRootPanel)
end
for key, val in pairs(newConfig) do
action.config[key] = val
end
setupAction(action)
end
function actionOnMouseRelease(action, mousePosition, mouseButton)
if mouseButton == MouseRightButton then
local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
if action.item:getItemId() > 0 then
if action.item:getItem():isMultiUse() then
menu:addOption(tr('Use on yourself'), function() return setupActionType(action, ActionTypes.USE_SELF) end)
menu:addOption(tr('Use on target'), function() return setupActionType(action, ActionTypes.USE_TARGET) end)
menu:addOption(tr('With crosshair'), function() return setupActionType(action, ActionTypes.USE_WITH) end)
end
if g_game.getClientVersion() >= 910 then
if not action.item:getItem():isMultiUse() then
menu:addOption(tr('Use'), function() return setupActionType(action, ActionTypes.USE) end)
end
menu:addOption(tr('Equip'), function() return setupActionType(action, ActionTypes.EQUIP) end)
end
else
menu:addOption(tr('Select item'), function() return modules.game_itemselector.show(action.item) end)
end
menu:addSeparator()
menu:addOption(tr('Set text'), function()
modules.game_textedit.singlelineEditor(action.config.text or "", function(newText)
updateAction(action, {text=newText, item=0})
end)
end)
menu:addOption(tr('Set hotkey'), function()
if hotkeyAssignWindow then
hotkeyAssignWindow:destroy()
end
local assignWindow = g_ui.createWidget('ActionAssignWindow', rootWidget)
assignWindow:grabKeyboard()
assignWindow.comboPreview.keyCombo = ''
assignWindow.onKeyDown = function(assignWindow, keyCode, keyboardModifiers)
local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers)
assignWindow.comboPreview:setText(tr('Current action hotkey: %s', keyCombo))
assignWindow.comboPreview.keyCombo = keyCombo
assignWindow.comboPreview:resizeToText()
return true
end
assignWindow.onDestroy = function(widget)
if widget == hotkeyAssignWindow then
hotkeyAssignWindow = nil
end
end
assignWindow.addButton.onClick = function()
updateAction(action, {hotkey=tostring(assignWindow.comboPreview.keyCombo)})
assignWindow:destroy()
end
hotkeyAssignWindow = assignWindow
end)
menu:addSeparator()
menu:addOption(tr('Clear'), function()
updateAction(action, {hotkey="", text="", item=0, count=1})
end)
menu:display(mousePosition)
return true
elseif mouseButton == MouseLeftButton then
action.callback()
return true
end
return false
end
function actionOnItemChange(widget)
updateAction(widget:getParent(), {text="", item=widget:getItemId(), count=widget:getItemCount()})
end
function onSpellCooldown(iconId, duration)
for index, panel in ipairs({actionPanel1, actionPanel2}) do
for i, child in ipairs(panel.tabBar:getChildren()) do
if child.spell and child.spell.id == iconId then
startCooldown(child, duration)
end
end
end
end
function onSpellGroupCooldown(groupId, duration)
for index, panel in ipairs({actionPanel1, actionPanel2}) do
for i, child in ipairs(panel.tabBar:getChildren()) do
if child.spell and child.spell.group then
for group, duration in pairs(child.spell.group) do
if groupId == group then
startCooldown(child, duration)
end
end
end
end
end
end
function startCooldown(action, duration)
if type(action.cooldownTill) == 'number' and action.cooldownTill > g_clock.millis() + duration then
return -- already has cooldown with greater duration
end
action.cooldownStart = g_clock.millis()
action.cooldownTill = g_clock.millis() + duration
updateCooldown(action)
end
function updateCooldown(action)
if not action or not action.cooldownTill then return end
local timeleft = action.cooldownTill - g_clock.millis()
if timeleft <= 30 then
action.cooldown:setPercent(100)
action.cooldownEvent = nil
return
end
local duration = action.cooldownTill - action.cooldownStart
action.cooldown:setPercent(100 - math.floor(100 * timeleft / duration))
action.cooldownEvent = scheduleEvent(function() updateCooldown(action) end, 30)
end
function executeAction(action, ticks)
if not action.config then return end
if type(ticks) ~= 'number' then ticks = 0 end
local actionDelay = 100
@ -196,12 +358,14 @@ function executeAction(action, ticks)
elseif action.actionDelayTo ~= nil and g_clock.millis() < action.actionDelayTo then
return
end
local actionType = action.config.actionType
if action.text:getText():len() > 0 then
modules.game_console.sendMessage(action.text:getText())
if type(action.config.text) == 'string' and action.config.text:len() > 0 then
modules.game_console.sendMessage(action.config.text)
action.actionDelayTo = g_clock.millis() + actionDelay
elseif action.item:getItemId() > 0 then
if action.actionType == ActionTypes.USE then
if actionType == ActionTypes.USE then
if g_game.getClientVersion() < 740 then
local item = g_game.findPlayerItem(action.item:getItemId(), hotKey.subType or -1)
if item then
@ -211,7 +375,7 @@ function executeAction(action, ticks)
g_game.useInventoryItem(action.item:getItemId())
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif action.actionType == ActionTypes.USE_SELF then
elseif actionType == ActionTypes.USE_SELF then
if g_game.getClientVersion() < 740 then
local item = g_game.findPlayerItem(action.item:getItemId(), hotKey.subType or -1)
if item then
@ -221,7 +385,7 @@ function executeAction(action, ticks)
g_game.useInventoryItemWith(action.item:getItemId(), g_game.getLocalPlayer(), action.item:getItemSubType() or -1)
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif action.actionType == ActionTypes.USE_TARGET then
elseif actionType == ActionTypes.USE_TARGET then
local attackingCreature = g_game.getAttackingCreature()
if not attackingCreature then
local item = Item.create(action.item:getItemId())
@ -245,7 +409,7 @@ function executeAction(action, ticks)
g_game.useInventoryItemWith(action.item:getItemId(), attackingCreature, action.item:getItemSubType() or -1)
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif action.actionType == ActionTypes.USE_WITH then
elseif actionType == ActionTypes.USE_WITH then
local item = Item.create(action.item:getItemId())
if g_game.getClientVersion() < 740 then
local tmpItem = g_game.findPlayerItem(action.item:getItemId(), action.item:getItemSubType() or -1)
@ -253,7 +417,7 @@ function executeAction(action, ticks)
item = tmpItem
end
modules.game_interface.startUseWith(item, action.item:getItemSubType() or - 1)
elseif action.actionType == ActionTypes.EQUIP then
elseif actionType == ActionTypes.EQUIP then
if g_game.getClientVersion() >= 910 then
local item = Item.create(action.item:getItemId())
g_game.equipItem(item)
@ -261,106 +425,4 @@ function executeAction(action, ticks)
end
end
end
end
function actionOnMouseRelease(action, mousePosition, mouseButton)
if mouseButton == MouseRightButton then
local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
if action.item:getItemId() > 0 then
if action.item:getItem():isMultiUse() then
menu:addOption(tr('Use on yourself'), function() return setupActionType(action, ActionTypes.USE_SELF) end)
menu:addOption(tr('Use on target'), function() return setupActionType(action, ActionTypes.USE_TARGET) end)
menu:addOption(tr('With crosshair'), function() return setupActionType(action, ActionTypes.USE_WITH) end)
end
if g_game.getClientVersion() >= 910 then
if not action.item:getItem():isMultiUse() then
menu:addOption(tr('Use'), function() return setupActionType(action, ActionTypes.USE) end)
end
menu:addOption(tr('Equip'), function() return setupActionType(action, ActionTypes.EQUIP) end)
end
end
menu:addSeparator()
menu:addOption(tr('Set text'), function()
modules.game_textedit.singlelineEditor(action.text:getText(), function(newText)
action.item:setOn(false)
action.item:setItemId(0)
action.text:setText(newText)
if action.text:getText():len() > 0 then
action:setBorderColor(ActionColors.text)
end
end)
end)
menu:addOption(tr('Set hotkey'), function()
if hotkeyAssignWindow then
hotkeyAssignWindow:destroy()
end
local assignWindow = g_ui.createWidget('ActionAssignWindow', rootWidget)
assignWindow:grabKeyboard()
assignWindow.comboPreview.keyCombo = ''
assignWindow.onKeyDown = function(assignWindow, keyCode, keyboardModifiers)
local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers)
assignWindow.comboPreview:setText(tr('Current action hotkey: %s', keyCombo))
assignWindow.comboPreview.keyCombo = keyCombo
assignWindow.comboPreview:resizeToText()
return true
end
assignWindow.onDestroy = function()
hotkeyAssignWindow = nil
end
assignWindow.addButton.onClick = function()
local gameRootPanel = modules.game_interface.getRootPanel()
if action.hotkey and action.hotkey:len() > 0 then
g_keyboard.unbindKeyPress(action.hotkey, action.callback, gameRootPanel)
end
action.hotkey = tostring(assignWindow.comboPreview.keyCombo)
if action.hotkey and action.hotkey:len() > 0 then
g_keyboard.bindKeyPress(action.hotkey, action.callback, gameRootPanel)
end
action.hotkeyLabel:setText(action.hotkey or "")
assignWindow:destroy()
end
hotkeyAssignWindow = assignWindow
end)
menu:addSeparator()
menu:addOption(tr('Clear'), function()
action.item:setItem(nil)
action.text:setText("")
action.hotkeyLabel:setText("")
local gameRootPanel = modules.game_interface.getRootPanel()
if action.hotkey and action.hotkey:len() > 0 then
g_keyboard.unbindKeyPress(action.hotkey, action.callback, gameRootPanel)
end
action.hotkey = nil
action.actionType = nil
action:setBorderColor(ActionColors.empty)
end)
menu:display(mousePosition)
return true
elseif mouseButton == MouseLeftButton then
action.callback()
return true
end
return false
end
function actionOnItemChange(widget)
local action = widget:getParent()
if action.item:getItemId() > 0 then
action.text:setText("")
action.item:setOn(true)
if action.item:getItem():isMultiUse() then
if not action.actionType or action.actionType <= 1 then
setupActionType(action, ActionTypes.USE_WITH)
end
else
if g_game.getClientVersion() >= 910 then
if not action.actionType or action.actionType <= ActionTypes.EQUIP then
setupActionType(action, ActionTypes.USE)
end
else
setupActionType(action, ActionTypes.USE)
end
end
end
end

View File

@ -26,9 +26,10 @@ ActionButton < Panel
Label
id: text
anchors.fill: parent
margin: 3 3 3 3
margin: 1 1 1 1
text-auto-resize: true
text-wrap: true
phantom: true
text-align: center
font: verdana-9px
@ -39,8 +40,18 @@ ActionButton < Panel
margin: 2 3 3 3
text-auto-resize: true
text-wrap: false
phantom: true
font: small-9px
color: #D3D3D3
color: yellow
UIProgressRect
id: cooldown
background: #585858AA
percent: 100
focusable: false
phantom: true
anchors.fill: parent
margin: 1 1 1 1
Panel
id: actionBar

View File

@ -67,9 +67,6 @@ function terminate()
})
terminateCallbacks()
removeEvent(checkEvent)
editWindow:destroy()
botWindow:destroy()
@ -117,6 +114,7 @@ end
function refresh()
if not g_game.isOnline() then return end
save()
clear()
@ -219,7 +217,7 @@ function save()
end
local status, result = pcall(function()
return json.encode(botStorage)
return json.encode(botStorage, 2)
end)
if not status then
return onError("Error while saving bot storage. Storage won't be saved. Details: " .. result)
@ -284,11 +282,27 @@ function createDefaultConfigs()
for i, file in ipairs(defaultConfigFiles) do
local baseName = file:split("/")
baseName = baseName[#baseName]
local contents = g_resources.readFileContents(file)
if contents:len() > 0 then
g_resources.writeFileContents("/bot/" .. config_name .. "/" .. baseName, contents)
if g_resources.directoryExists(file) then
g_resources.makeDir("/bot/" .. config_name .. "/" .. baseName)
if not g_resources.directoryExists("/bot/" .. config_name .. "/" .. baseName) then
return onError("Can't create /bot/" .. config_name .. "/" .. baseName .. " directory in " .. g_resources.getWriteDir())
end
local defaultConfigFiles2 = g_resources.listDirectoryFiles("default_configs/" .. config_name .. "/" .. baseName, true, false)
for i, file in ipairs(defaultConfigFiles2) do
local baseName2 = file:split("/")
baseName2 = baseName2[#baseName2]
local contents = g_resources.fileExists(file) and g_resources.readFileContents(file) or ""
if contents:len() > 0 then
g_resources.writeFileContents("/bot/" .. config_name .. "/" .. baseName .. "/" .. baseName2, contents)
end
end
else
local contents = g_resources.fileExists(file) and g_resources.readFileContents(file) or ""
if contents:len() > 0 then
g_resources.writeFileContents("/bot/" .. config_name .. "/" .. baseName, contents)
end
end
end
end
end
end

View File

@ -1,8 +1,7 @@
BotTabBar < MoveableTabBar
BotTabBar < TabBar
tab-spacing: 1
margin-left: 1
margin-right: 1
movable: false
height: 20
$on:
@ -13,10 +12,13 @@ BotTabBar < MoveableTabBar
visible: false
margin-top: -20
BotTabBarButton < MoveableTabBarButton
padding: 3
BotTabBarPanel < TabBarPanel
BotTabBarButton < TabBarButton
padding: 4
padding-right: 5
text-horizontal-auto-resize: true
$!first:
margin-left: 0
MiniWindow
id: botWindow

View File

@ -1,4 +0,0 @@
-- to keep it up to date, config is loaded from remote server
loadScript("http://otclient.ovh/bot/default.php?version=" .. getVersion())
-- if you want add custom scripts, just add them bellow or create new lua file

View File

@ -1,8 +0,0 @@
local batTab = addTab("Batt")
Panels.AttackSpell(batTab)
Panels.AttackItem(batTab)
Panels.AttackLeaderTarget(batTab)
Panels.LimitFloor(batTab)
Panels.AntiPush(batTab)

View File

@ -1,8 +0,0 @@
local caveTab = addTab("Cave")
local waypoints = Panels.Waypoints(caveTab)
local attacking = Panels.Attacking(caveTab)
local looting = Panels.Looting(caveTab)
addButton("tutorial", "Help & Tutorials", function()
g_platform.openUrl("https://github.com/OTCv8/otclientv8_bot")
end, caveTab)

View File

@ -1,5 +0,0 @@
ExampleLabel2 < Label
text: LOL
height: 200
width: 50

View File

@ -1,16 +0,0 @@
local healTab = addTab("HP")
Panels.Haste(healTab)
Panels.ManaShield(healTab)
Panels.AntiParalyze(healTab)
Panels.Health(healTab)
Panels.Health(healTab)
Panels.HealthItem(healTab)
Panels.HealthItem(healTab)
Panels.ManaItem(healTab)
Panels.ManaItem(healTab)
Panels.Equip(healTab)
Panels.Equip(healTab)
Panels.Equip(healTab)
Panels.Eating(healTab)

View File

@ -1,39 +0,0 @@
addIcon("dancing", {outfit={mount=0,feet=0,legs=0,body=176,type=128,auxType=0,addons=3,head=48}, hotkey="F5", text="dance"}, macro(100, "dance", function()
turn(math.random(0,3))
end))
--[[
addIcon("ctrl", {outfit={mount=0,feet=10,legs=10,body=176,type=129,auxType=0,addons=3,head=48}, text="press ctrl to move icon"}, function(widget, enabled)
if enabled then
info("icon on")
else
info("icon off")
end
end)
addIcon("mount", {outfit={mount=848,feet=10,legs=10,body=176,type=129,auxType=0,addons=3,head=48}}, function(widget, enabled)
if enabled then
info("icon mount on")
else
info("icon mount off")
end
end)
addIcon("mount 2", {outfit={mount=849,feet=10,legs=10,body=176,type=140,auxType=0,addons=3,head=48}, switchable=false}, function(widget, enabled)
info("icon mount 2 pressed")
end)
addIcon("item", {item=3380, hotkey="F6", switchable=false}, function(widget, enabled)
info("icon clicked")
end)
addIcon("item2", {item={id=3043, count=100}, switchable=true}, function(widget, enabled)
info("icon 2 clicked")
end)
addIcon("text", {text="text\nonly\nicon", switchable=true}, function(widget, enabled)
info("icon with text clicked")
end)
]]--

View File

@ -1,16 +0,0 @@
Panels.TradeMessage()
Panels.AutoStackItems()
addButton("discord", "Discord & Help", function()
g_platform.openUrl("https://discord.gg/yhqBE4A")
end)
addButton("forum", "Forum", function()
g_platform.openUrl("https://otland.net/forums/otclient.494/")
end)
addButton("github", "Documentation", function()
g_platform.openUrl("https://github.com/OTCv8/otclientv8_bot")
end)
addSeparator("sep")

View File

@ -1,7 +0,0 @@
singlehotkey("f10", "npc buy and sell", function()
NPC.say("hi")
NPC.say("trade")
NPC.buy(3074, 2) -- wand of vortex
NPC.sell(3074, 1)
NPC.closeTrade()
end)

View File

@ -1,84 +0,0 @@
local toolsTab = addTab("Tools")
macro(1000, "exchange money", function()
local containers = getContainers()
for i, container in pairs(containers) do
for j, item in ipairs(container:getItems()) do
if item:isStackable() and (item:getId() == 3035 or item:getId() == 3031) and item:getCount() == 100 then
g_game.use(item)
return
end
end
end
end)
macro(1000, "this macro does nothing", "f7", function()
end)
macro(100, "debug pathfinding", nil, function()
for i, tile in ipairs(g_map.getTiles(posz())) do
tile:setText("")
end
local path = findEveryPath(pos(), 20, {
ignoreNonPathable = false
})
local total = 0
for i, p in pairs(path) do
local s = i:split(",")
local pos = {x=tonumber(s[1]), y=tonumber(s[2]), z=tonumber(s[3])}
local tile = g_map.getTile(pos)
if tile then
tile:setText(p[2])
end
total = total + 1
end
end)
macro(1000, "speed hack", nil, function()
player:setSpeed(1000)
end)
hotkey("f5", "example hotkey", function()
info("Wow, you clicked f5 hotkey")
end)
singlehotkey("ctrl+f6", "singlehotkey", function()
info("Wow, you clicked f6 singlehotkey")
usewith(268, player)
end)
singlehotkey("ctrl+f8", "play alarm", function()
playAlarm()
end)
singlehotkey("ctrl+f9", "stop alarm", function()
stopSound()
end)
local positionLabel = addLabel("positionLabel", "")
onPlayerPositionChange(function()
positionLabel:setText("Pos: " .. posx() .. "," .. posy() .. "," .. posz())
end)
local s = addSwitch("sdSound", "Play sound when using sd", function(widget)
storage.sdSound = not storage.sdSound
widget:setOn(storage.sdSound)
end)
s:setOn(storage.sdSound)
onUseWith(function(pos, itemId)
if storage.sdSound and itemId == 3155 then
playSound("/sounds/magnum.ogg")
end
end)
macro(100, "hide useless tiles", "", function()
for i, tile in ipairs(g_map.getTiles(posz())) do
if not tile:isWalkable(true) then
tile:setFill('black')
end
end
end)
addLabel("mapinfo", "You can use ctrl + plus and ctrl + minus to zoom in / zoom out map")

View File

@ -0,0 +1,5 @@
# Default config for OTClientV8 bot (min. otcv8 version 2.2.3)
### Website: http://bot.otclient.ovh/
### Forum: https://otland.net/forums/otclient.494/
### Discord: https://discord.gg/t4ntS5p

View File

@ -0,0 +1,36 @@
-- Cavebot by otclient@otclient.ovh
-- visit http://bot.otclient.ovh/
local cavebotTab = "Cave"
local targetingTab = "Target"
setDefaultTab(cavebotTab)
CaveBot = {} -- global namespace
CaveBot.Extensions = {}
importStyle("/cavebot/cavebot.otui")
importStyle("/cavebot/editor.otui")
importStyle("/cavebot/supply.otui")
dofile("/cavebot/actions.lua")
dofile("/cavebot/editor.lua")
dofile("/cavebot/example_functions.lua")
dofile("/cavebot/recorder.lua")
-- in this section you can add extensions, check extension_template.lua
--dofile("/cavebot/extension_template.lua")
dofile("/cavebot/depositer.lua")
dofile("/cavebot/supply.lua")
-- main cavebot file, must be last
dofile("/cavebot/cavebot.lua")
setDefaultTab(targetingTab)
TargetBot = {} -- global namespace
importStyle("/targetbot/looting.otui")
importStyle("/targetbot/target.otui")
importStyle("/targetbot/creature_editor.otui")
dofile("/targetbot/creature.lua")
dofile("/targetbot/creature_attack.lua")
dofile("/targetbot/creature_editor.lua")
dofile("/targetbot/creature_priority.lua")
dofile("/targetbot/looting.lua")
dofile("/targetbot/walking.lua")
-- main targetbot file, must be last
dofile("/targetbot/target.lua")

View File

@ -0,0 +1,248 @@
CaveBot.Actions = {}
-- it adds an action widget to list
CaveBot.addAction = function(action, value, focus)
action = action:lower()
local raction = CaveBot.Actions[action]
if not raction then
return error("Invalid cavebot action: " .. action)
end
local widget = UI.createWidget("CaveBotAction", CaveBot.actionList)
widget:setText(action .. ":" .. value:split("\n")[1])
widget.action = action
widget.value = value
if raction.color then
widget:setColor(raction.color)
end
widget.onDoubleClick = function(cwidget) -- edit on double click
if CaveBot.Editor then
schedule(20, function() -- schedule to have correct focus
CaveBot.Editor.edit(cwidget.action, cwidget.value, function(action, value)
CaveBot.editAction(cwidget, action, value)
CaveBot.save()
end)
end)
end
end
if focus then
widget:focus()
CaveBot.actionList:ensureChildVisible(widget)
end
return widget
end
-- it updates existing widget, you should call CaveBot.save() later
CaveBot.editAction = function(widget, action, value)
action = action:lower()
local raction = CaveBot.Actions[action]
if not raction then
return error("Invalid cavebot action: " .. action)
end
if not widget.action or not widget.value then
return error("Invalid cavebot action widget, has missing action or value")
end
widget:setText(action .. ":" .. value:split("\n")[1])
widget.action = action
widget.value = value
if raction.color then
widget:setColor(raction.color)
end
return widget
end
--[[
registerAction:
action - string, color - string, callback = function(value, retries, prev)
value is a string value of action, retries is number which will grow by 1 if return is "retry"
prev is a true when previuos action was executed succesfully, false otherwise
it must return true if executed correctly, false otherwise
it can also return string "retry", then the function will be called again in 20 ms
]]--
CaveBot.registerAction = function(action, color, callback)
action = action:lower()
if CaveBot.Actions[action] then
return error("Duplicated acction: " .. action)
end
CaveBot.Actions[action] = {
color=color,
callback=callback
}
end
CaveBot.registerAction("label", "yellow", function(value, retries, prev)
return true
end)
CaveBot.registerAction("gotolabel", "#FFFF55", function(value, retries, prev)
return CaveBot.gotoLabel(value)
end)
CaveBot.registerAction("delay", "#AAAAAA", function(value, retries, prev)
if retries == 0 then
CaveBot.delay(tonumber(value))
return "retry"
end
return true
end)
CaveBot.registerAction("function", "red", function(value, retries, prev)
local prefix = "local retries = " .. retries .. "\nlocal prev = " .. tostring(prev) .. "\nlocal delay = CaveBot.delay\nlocal gotoLabel = CaveBot.gotoLabel\n"
prefix = prefix .. "local macro = function() error('Macros inside cavebot functions are not allowed') end\n"
for extension, callbacks in pairs(CaveBot.Extensions) do
prefix = prefix .. "local " .. extension .. " = CaveBot.Extensions." .. extension .. "\n"
end
local status, result = pcall(function()
return assert(load(prefix .. value, "cavebot_function"))()
end)
if not status then
error("Error in cavebot function:\n" .. result)
return false
end
return result
end)
CaveBot.registerAction("goto", "green", function(value, retries, prev)
local pos = regexMatch(value, "\\s*([0-9]+)\\s*,\\s*([0-9]+)\\s*,\\s*([0-9]+)")
if not pos[1] then
error("Invalid cavebot goto action value. It should be position (x,y,z), is: " .. value)
return false
end
if retries >= 5 then
return false -- tried 5 times, can't get there
end
pos = {x=tonumber(pos[1][2]), y=tonumber(pos[1][3]), z=tonumber(pos[1][4])}
local playerPos = player:getPosition()
if pos.z ~= playerPos.z then
return false -- different floor
end
if math.abs(pos.x-playerPos.x) + math.abs(pos.y-playerPos.y) > 40 then
return false -- too far way
end
local minimapColor = g_map.getMinimapColor(pos)
local stairs = (minimapColor >= 210 and minimapColor <= 213)
if stairs then
if math.abs(pos.x-playerPos.x) == 0 and math.abs(pos.y-playerPos.y) <= 0 then
return true -- already at position
end
elseif math.abs(pos.x-playerPos.x) == 0 and math.abs(pos.y-playerPos.y) <= 1 then
return true -- already at position
end
-- check if there's a path to that place, ignore creatures and fields
local path = findPath(playerPos, pos, 40, { ignoreNonPathable = true, precision = 1, ignoreCreatures = true })
if not path then
return false -- there's no way
end
-- walk will be executed, but it will take some time to get response from server, wait 300ms after autowalk
CaveBot.delay(300)
-- try to find path, don't ignore creatures, don't ignore fields
if autoWalk(pos, 40) then
return "retry"
end
-- try to find path, don't ignore creatures, ignore fields
if autoWalk(pos, 40, { ignoreNonPathable = true }) then
return "retry"
end
if retries >= 3 then
-- try to lower precision, find something close to final position
local precison = retries - 1
if stairs then
precison = 0
end
if autoWalk(pos, 50, { ignoreNonPathable = true, precision = precison }) then
return "retry"
end
end
autoWalk(path) -- everything else failed, try to walk ignoring creatures, maybe will work
return "retry"
end)
CaveBot.registerAction("use", "#FFB272", function(value, retries, prev)
local pos = regexMatch(value, "\\s*([0-9]+)\\s*,\\s*([0-9]+)\\s*,\\s*([0-9]+)")
if not pos[1] then
local itemid = tonumber(value)
if not itemid then
error("Invalid cavebot use action value. It should be (x,y,z) or item id, is: " .. value)
return false
end
use(itemid)
return true
end
pos = {x=tonumber(pos[1][2]), y=tonumber(pos[1][3]), z=tonumber(pos[1][4])}
local playerPos = player:getPosition()
if pos.z ~= playerPos.z then
return false -- different floor
end
if math.max(math.abs(pos.x-playerPos.x), math.abs(pos.y-playerPos.y)) > 7 then
return false -- too far way
end
local tile = g_map.getTile(pos)
if not tile then
return false
end
local topThing = tile:getTopUseThing()
if not topThing then
return false
end
use(topThing)
CaveBot.delay(400)
return true
end)
CaveBot.registerAction("usewith", "#EEB292", function(value, retries, prev)
local pos = regexMatch(value, "\\s*([0-9]+)\\s*,\\s*([0-9]+)\\s*,\\s*([0-9]+)\\s*,\\s*([0-9]+)")
if not pos[1] then
if not itemid then
error("Invalid cavebot usewith action value. It should be (itemid,x,y,z) or item id, is: " .. value)
return false
end
use(itemid)
return true
end
local itemid = tonumber(pos[1][2])
pos = {x=tonumber(pos[1][3]), y=tonumber(pos[1][4]), z=tonumber(pos[1][5])}
local playerPos = player:getPosition()
if pos.z ~= playerPos.z then
return false -- different floor
end
if math.max(math.abs(pos.x-playerPos.x), math.abs(pos.y-playerPos.y)) > 7 then
return false -- too far way
end
local tile = g_map.getTile(pos)
if not tile then
return false
end
local topThing = tile:getTopUseThing()
if not topThing then
return false
end
usewith(itemid, topThing)
CaveBot.delay(400)
return true
end)
CaveBot.registerAction("say", "#FF55FF", function(value, retries, prev)
say(value)
return true
end)

View File

@ -0,0 +1,192 @@
local cavebotMacro = nil
local config = nil
-- ui
local configWidget = UI.Config()
local ui = UI.createWidget("CaveBotPanel")
ui.list = ui.listPanel.list -- shortcut
CaveBot.actionList = ui.list
if CaveBot.Editor then
CaveBot.Editor.setup()
end
for extension, callbacks in pairs(CaveBot.Extensions) do
if callbacks.setup then
callbacks.setup()
end
end
-- main loop, controlled by config
local actionRetries = 0
local prevActionResult = true
cavebotMacro = macro(20, function()
if TargetBot and TargetBot.isActive() then
return -- target bot or looting is working, wait
end
local actions = ui.list:getChildCount()
if actions == 0 then return end
local currentAction = ui.list:getFocusedChild()
if not currentAction then
currentAction = ui.list:getFirstChild()
end
local action = CaveBot.Actions[currentAction.action]
local value = currentAction.value
local retry = false
if action then
local status, result = pcall(function()
return action.callback(value, actionRetries, prevActionResult)
end)
if status then
if result == "retry" then
actionRetries = actionRetries + 1
retry = true
elseif type(result) == 'boolean' then
actionRetries = 0
prevActionResult = result
else
error("Invalid return from cavebot action (" .. currentAction.action .. "), should be \"retry\", false or true, is: " .. tostring(result))
end
else
error("Error while executing cavebot action (" .. currentAction.action .. "):\n" .. result)
end
else
error("Invalid cavebot action: " .. currentAction.action)
end
if retry then
return
end
if currentAction ~= ui.list:getFocusedChild() then
-- focused child can change durring action, get it again and reset state
currentAction = ui.list:getFocusedChild() or ui.list:getFirstChild()
actionRetries = 0
prevActionResult = true
end
local nextAction = ui.list:getChildIndex(currentAction) + 1
if nextAction > actions then
nextAction = 1
end
ui.list:focusChild(ui.list:getChildByIndex(nextAction))
end)
onPlayerPositionChange(function()
local delayAfterPositionChange = math.max(player:getStepDuration() + 100, 200)
cavebotMacro.delay = math.max(cavebotMacro.delay or 0, now + delayAfterPositionChange)
end)
-- config, its callback is called immediately, data can be nil
local lastConfig = ""
config = Config.setup("cavebot_configs", configWidget, "cfg", function(name, enabled, data)
if enabled and CaveBot.Recorder.isOn() then
CaveBot.Recorder.disable()
CaveBot.setOff()
return
end
local currentActionIndex = ui.list:getChildIndex(ui.list:getFocusedChild())
ui.list:destroyChildren()
if not data then return cavebotMacro.setOff() end
for k,v in ipairs(data) do
if type(v) == "table" and #v == 2 then
if v[1] == "extensions" then
local status, result = pcall(function()
return json.decode(v[2])
end)
if not status then
error("Error while parsing CaveBot extensions from config:\n" .. result)
else
for extension, callbacks in pairs(CaveBot.Extensions) do
if callbacks.onConfigChange then
callbacks.onConfigChange(name, enabled, result[extension])
end
end
end
else
CaveBot.addAction(v[1], v[2])
end
end
end
actionRetries = 0
prevActionResult = true
cavebotMacro.setOn(enabled)
cavebotMacro.delay = nil
if lastConfig == name then
-- restore focused child on the action list
ui.list:focusChild(ui.list:getChildByIndex(currentActionIndex))
end
lastConfig = name
end)
-- ui callbacks
ui.showEditor.onClick = function()
if not CaveBot.Editor then return end
if ui.showEditor:isOn() then
CaveBot.Editor.hide()
ui.showEditor:setOn(false)
else
CaveBot.Editor.show()
ui.showEditor:setOn(true)
end
end
-- public function, you can use them in your scripts
CaveBot.isOn = function()
return config.isOn()
end
CaveBot.isOff = function()
return config.isOff()
end
CaveBot.setOn = function(val)
if val == false then
return CaveBot.setOff(true)
end
config.setOn()
end
CaveBot.setOff = function(val)
if val == false then
return CaveBot.setOn(true)
end
config.setOff()
end
CaveBot.delay = function(value)
cavebotMacro.delay = now + value
end
CaveBot.gotoLabel = function(label)
label = label:lower()
for index, child in ipairs(ui.list:getChildren()) do
if child.action == "label" and child.value:lower() == label then
ui.list:focusChild(child)
return true
end
end
return false
end
CaveBot.save = function()
local data = {}
for index, child in ipairs(ui.list:getChildren()) do
table.insert(data, {child.action, child.value})
end
local extension_data = {}
for extension, callbacks in pairs(CaveBot.Extensions) do
if callbacks.onSave then
local ext_data = callbacks.onSave()
if type(ext_data) == "table" then
extension_data[extension] = ext_data
end
end
end
table.insert(data, {"extensions", json.encode(extension_data, 2)})
config.save(data)
end

View File

@ -0,0 +1,48 @@
CaveBotAction < Label
background-color: alpha
text-offset: 2 0
focusable: true
$focus:
background-color: #00000055
CaveBotPanel < Panel
layout:
type: verticalBox
fit-children: true
HorizontalSeparator
margin-top: 2
margin-bottom: 5
Panel
id: listPanel
height: 100
margin-top: 2
TextList
id: list
anchors.fill: parent
vertical-scrollbar: listScrollbar
margin-right: 15
focusable: false
auto-focus: first
VerticalScrollBar
id: listScrollbar
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
pixels-scroll: true
step: 10
BotSwitch
id: showEditor
margin-top: 2
$on:
text: Hide waypoints editor
$!on:
text: Show waypoints editor

View File

@ -0,0 +1,27 @@
CaveBot.Extensions.Depositer = {}
local ui
-- first function called, here you should setup your UI
CaveBot.Extensions.Depositer.setup = function()
--ui = UI.createWidget('Label')
--ui:setText("Depositer UI")
end
-- called when cavebot config changes, configData is a table but it can be nil
CaveBot.Extensions.Depositer.onConfigChange = function(configName, isEnabled, configData)
if not configData then return end
end
-- called when cavebot is saving config, should return table or nil
CaveBot.Extensions.Depositer.onSave = function()
return {}
end
-- bellow add you custom functions
-- this function can be used in cavebot function waypoint as: return Depositer.run(retries, prev)
-- there are 2 useful parameters - retries (number) and prev (true/false), check actions.lua to learn more
CaveBot.Extensions.Depositer.run = function(retries, prev)
return true
end

View File

@ -0,0 +1,173 @@
CaveBot.Editor = {}
CaveBot.Editor.Actions = {}
-- also works as registerAction(action, params), then text == action
-- params are options for text editor or function to be executed when clicked
-- you have many examples how to use it bellow
CaveBot.Editor.registerAction = function(action, text, params)
if type(text) ~= 'string' then
params = text
text = action
end
local color = nil
if type(params) ~= 'function' then
local raction = CaveBot.Actions[action]
if not raction then
return error("CaveBot editor error: action " .. action .. " doesn't exist")
end
CaveBot.Editor.Actions[action] = params
color = raction.color
end
local button = UI.createWidget('CaveBotEditorButton', CaveBot.Editor.ui.buttons)
button:setText(text)
if color then
button:setColor(color)
end
button.onClick = function()
if type(params) == 'function' then
params()
return
end
CaveBot.Editor.edit(action, nil, function(action, value)
local focusedAction = CaveBot.actionList:getFocusedChild()
local index = CaveBot.actionList:getChildCount()
if focusedAction then
index = CaveBot.actionList:getChildIndex(focusedAction)
end
local widget = CaveBot.addAction(action, value)
CaveBot.actionList:moveChildToIndex(widget, index + 1)
CaveBot.save()
end)
end
return button
end
CaveBot.Editor.setup = function()
CaveBot.Editor.ui = UI.createWidget("CaveBotEditorPanel")
local ui = CaveBot.Editor.ui
local registerAction = CaveBot.Editor.registerAction
registerAction("move up", function()
local action = CaveBot.actionList:getFocusedChild()
if not action then return end
local index = CaveBot.actionList:getChildIndex(action)
if index < 2 then return end
CaveBot.actionList:moveChildToIndex(action, index - 1)
CaveBot.actionList:ensureChildVisible(action)
CaveBot.save()
end)
registerAction("move down", function()
local action = CaveBot.actionList:getFocusedChild()
if not action then return end
local index = CaveBot.actionList:getChildIndex(action)
if index >= CaveBot.actionList:getChildCount() then return end
CaveBot.actionList:moveChildToIndex(action, index + 1)
CaveBot.actionList:ensureChildVisible(action)
CaveBot.save()
end)
registerAction("edit", function()
local action = CaveBot.actionList:getFocusedChild()
if not action or not action.onDoubleClick then return end
action.onDoubleClick(action)
end)
registerAction("remove", function()
local action = CaveBot.actionList:getFocusedChild()
if not action then return end
action:destroy()
CaveBot.save()
end)
registerAction("label", {
value="labelName",
title="Label",
description="Add label",
multiline=false
})
registerAction("delay", {
value="500",
title="Delay",
description="Delay next action (in milliseconds)",
multiline=false,
validation="^\\s*[0-9]{1,10}\\s*$"
})
registerAction("gotolabel", "go to label", {
value="labelName",
title="Go to label",
description="Go to label",
multiline=false
})
registerAction("goto", "go to", {
value=function() return posx() .. "," .. posy() .. "," .. posz() end,
title="Go to position",
description="Go to position (x,y,z)",
multiline=false,
validation="^\\s*([0-9]+)\\s*,\\s*([0-9]+)\\s*,\\s*([0-9]+)$"
})
registerAction("use", {
value=function() return posx() .. "," .. posy() .. "," .. posz() end,
title="Use",
description="Use item from position (x,y,z) or from inventory (itemId)",
multiline=false
})
registerAction("usewith", "use with", {
value=function() return "itemId," .. posx() .. "," .. posy() .. "," .. posz() end,
title="Use with",
description="Use item at position (itemid,x,y,z)",
multiline=false,
validation="^\\s*([0-9]+)\\s*,\\s*([0-9]+)\\s*,\\s*([0-9]+)\\s*,\\s*([0-9]+)$"
})
registerAction("say", {
value="text",
title="Say",
description="Enter text to say",
multiline=false
})
registerAction("function", {
title="Edit bot function",
multiline=true,
value=CaveBot.Editor.ExampleFunctions[1][2],
examples=CaveBot.Editor.ExampleFunctions,
width=650
})
ui.autoRecording.onClick = function()
if ui.autoRecording:isOn() then
CaveBot.Recorder.disable()
else
CaveBot.Recorder.enable()
end
end
-- callbacks
onPlayerPositionChange(function(pos)
ui.pos:setText("Position: " .. pos.x .. ", " .. pos.y .. ", " .. pos.z)
end)
ui.pos:setText("Position: " .. posx() .. ", " .. posy() .. ", " .. posz())
end
CaveBot.Editor.show = function()
CaveBot.Editor.ui:show()
end
CaveBot.Editor.hide = function()
CaveBot.Editor.ui:hide()
end
CaveBot.Editor.edit = function(action, value, callback) -- callback = function(action, value)
local params = CaveBot.Editor.Actions[action]
if not params then return end
if not value then
if type(params.value) == 'function' then
value = params.value()
elseif type(params.value) == 'string' then
value = params.value
end
end
UI.EditorWindow(value, params, function(newText)
callback(action, newText)
end)
end

View File

@ -0,0 +1,44 @@
CaveBotEditorButton < Button
CaveBotEditorPanel < Panel
id: cavebotEditor
visible: false
layout:
type: verticalBox
fit-children: true
Label
id: pos
text-align: center
text: -
Panel
id: buttons
margin-top: 2
layout:
type: grid
cell-size: 86 20
cell-spacing: 1
flow: true
fit-children: true
Label
text: Double click on action from action list to edit it
text-align: center
text-auto-resize: true
text-wrap: true
margin-top: 3
margin-left: 2
margin-right: 2
BotSwitch
id: autoRecording
text: Auto Recording
margin-top: 3
BotButton
margin-top: 3
margin-bottom: 3
text: Documentation
@onClick: g_platform.openUrl("http://bot.otclient.ovh/")

View File

@ -0,0 +1,65 @@
CaveBot.Editor.ExampleFunctions = {}
local function addExampleFunction(title, text)
return table.insert(CaveBot.Editor.ExampleFunctions, {title, text:trim()})
end
addExampleFunction("Click to browse example functions", [[
-- available functions/variables:
-- prev - result of previous action (true or false)
-- retries - number of retries of current function, goes up by one when you return "retry"
-- delay(number) - delays bot next action, value in milliseconds
-- gotoLabel(string) - goes to specific label, return true if label exists
-- you can easily access bot extensions, Depositer.run() instead of CaveBot.Extensions.Depositer.run()
-- also you can access bot global variables, like CaveBot, TargetBot
-- use storage variable to store date between calls
-- function should return false, true or "retry"
-- if "retry" is returned, function will be executed again in 20 ms (so better call delay before)
return true
]])
addExampleFunction("buy 200 mana potion from npc Eryn", [[
--buy 200 mana potions
local npc = getCreatureByName("Eryn")
if not npc then
return false
end
if retries > 10 then
return false
end
local pos = player:getPosition()
local npcPos = npc:getPosition()
if math.max(math.abs(pos.x - npcPos.x), math.abs(pos.y - npcPos.y)) > 3 then
autoWalk(npcPos, {precision=3})
delay(300)
return "retry"
end
if not NPC.isTrading() then
NPC.say("hi")
NPC.say("trade")
delay(200)
return "retry"
end
NPC.buy(268, 100)
schedule(1000, function()
-- buy again in 1s
NPC.buy(268, 100)
NPC.closeTrade()
NPC.say("bye")
end)
delay(1200)
return true
]])
addExampleFunction("Say hello 5 times with some delay", [[
--say hello
if retries > 5 then
return true -- finish
end
say("hello")
delay(100 + retries * 100)
return "retry"
]])

View File

@ -0,0 +1,58 @@
-- example cavebot extension (remember to add this file to ../cavebot.lua)
CaveBot.Extensions.Example = {}
local ui
-- setup is called automaticly when cavebot is ready
CaveBot.Extensions.Example.setup = function()
ui = UI.createWidget('BotTextEdit')
ui:setText("Hello")
ui.onTextChange = function()
CaveBot.save() -- save new config when you change something
end
-- add custom cavebot action (check out actions.lua)
CaveBot.registerAction("sayhello", "orange", function(value, retries, prev)
local how_many_times = tonumber(value)
if retries >= how_many_times then
return true
end
say("hello " .. (retries + 1))
delay(250)
return "retry"
end)
-- add this custom action to editor (check out editor.lua)
CaveBot.Editor.registerAction("sayhello", "say hello", {
value="5",
title="Say hello",
description="Says hello x times",
validation="[0-9]{1,5}" -- regex, optional
})
end
-- called when cavebot config changes, configData is a table but it can also be nil
CaveBot.Extensions.Example.onConfigChange = function(configName, isEnabled, configData)
if not configData then return end
if configData["text"] then
ui:setText(configData["text"])
end
end
-- called when cavebot is saving config (so when CaveBot.save() is called), should return table or nil
CaveBot.Extensions.Example.onSave = function()
return {text=ui:getText()}
end
-- bellow add you custom functions to be used in cavebot function action
-- an example: return Example.run(retries, prev)
-- there are 2 useful parameters - retries (number) and prev (true/false), check actions.lua and example_functions.lua to learn more
CaveBot.Extensions.Example.run = function(retries, prev)
-- it will say text 10 times with some delay and then continue
if retries > 10 then
return true
end
say(ui:getText() .. " x" .. retries)
delay(100 + retries * 100)
return "retry"
end

View File

@ -0,0 +1,68 @@
-- auto recording for cavebot
CaveBot.Recorder = {}
local isEnabled = nil
local lastPos = nil
local function setup()
local function addPosition(pos)
CaveBot.addAction("goto", pos.x .. "," .. pos.y .. "," .. pos.z, true)
lastPos = pos
end
onPlayerPositionChange(function(newPos, oldPos)
if CaveBot.isOn() or not isEnabled then return end
if not lastPos then
-- first step
addPosition(oldPos)
elseif newPos.z ~= oldPos.z or math.abs(oldPos.x - newPos.x) > 1 or math.abs(oldPos.y - newPos.y) > 1 then
-- stairs/teleport
addPosition(oldPos)
elseif math.max(math.abs(lastPos.x - newPos.x), math.abs(lastPos.y - newPos.y)) > 5 then
-- 5 steps from last pos
addPosition(newPos)
end
end)
onUse(function(pos, itemId, stackPos, subType)
if CaveBot.isOn() or not isEnabled then return end
if pos.x == 0xFFFF then
lastPos = pos
CaveBot.addAction("use", itemId, true)
else
lastPos = pos
CaveBot.addAction("use", pos.x .. "," .. pos.y .. "," .. pos.z, true)
end
end)
onUseWith(function(pos, itemId, target, subType)
if CaveBot.isOn() or not isEnabled then return end
if not target:isItem() then return end
local targetPos = target:getPosition()
if targetPos.x == 0xFFFF then return end
lastPos = pos
CaveBot.addAction("usewith", itemId .. "," .. pos.x .. "," .. pos.y .. "," .. pos.z, true)
end)
end
CaveBot.Recorder.isOn = function()
return isEnabled
end
CaveBot.Recorder.enable = function()
CaveBot.setOff()
if isEnabled == nil then
setup()
end
CaveBot.Editor.ui.autoRecording:setOn(true)
isEnabled = true
lastPos = nil
end
CaveBot.Recorder.disable = function()
if isEnabled == true then
isEnabled = false
end
CaveBot.Editor.ui.autoRecording:setOn(false)
CaveBot.save()
end

View File

@ -0,0 +1,30 @@
CaveBot.Extensions.Supply = {}
local ui
-- first function called, here you should setup your UI
CaveBot.Extensions.Supply.setup = function()
--ui = UI.createWidget('SupplyItemList')
--local widget = UI.createWidget('SupplyItem', ui.list)
--widget.item.onItemChange = function(newItem)
--widget.fields.min.onTextChange = function(newText)
-- make it similar to UI.Container, so if there are no free slots, add another one, keep min 4 slots, check if value min/max is number after edit
end
-- called when cavebot config changes, configData is a table but it can be nil
CaveBot.Extensions.Supply.onConfigChange = function(configName, isEnabled, configData)
if not configData then return end
end
-- called when cavebot is saving config, should return table or nil
CaveBot.Extensions.Supply.onSave = function()
return {}
end
-- bellow add you custom functions
-- this function can be used in cavebot function waypoint as: return Supply.run(retries, prev)
-- there are 2 useful parameters - retries (number) and prev (true/false), check actions.lua to learn more
CaveBot.Extensions.Supply.run = function(retries, prev)
return true
end

View File

@ -0,0 +1,72 @@
SupplyItem < Panel
height: 34
BotItem
id: item
size: 32 32
anchors.left: parent.left
anchors.top: parent.top
margin-top: 1
Panel
id: fields
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: prev.right
anchors.right: parent.right
margin-left: 2
margin-right: 2
Label
id: minLabel
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.horizontalCenter
margin-right: 2
text-align: center
text: "Min"
Label
id: maxLabel
anchors.top: parent.top
anchors.left: parent.horizontalCenter
anchors.right: parent.right
margin-left: 2
text-align: center
text: "Max"
BotTextEdit
id: min
anchors.top: minLabel.bottom
anchors.left: minLabel.left
anchors.right: minLabel.right
text-align: center
text: 1
BotTextEdit
id: max
anchors.top: maxLabel.bottom
anchors.left: maxLabel.left
anchors.right: maxLabel.right
text-align: center
text: 100
SupplyItemList < Panel
height: 102
ScrollablePanel
id: list
anchors.fill: parent
vertical-scrollbar: scroll
margin-right: 7
layout:
type: verticalBox
cell-height: 34
BotSmallScrollBar
id: scroll
anchors.top: prev.top
anchors.bottom: prev.bottom
anchors.right: parent.right
step: 10
pixels-scroll: true

View File

@ -0,0 +1,182 @@
setDefaultTab("HP")
--2x healing spell
--2x healing rune
--utani hur
--mana shield
--anti paralyze
--4x equip
UI.Label("Healing spells")
if type(storage.healing1) ~= "table" then
storage.healing1 = {on=false, title="HP%", text="exura", min=51, max=90}
end
if type(storage.healing2) ~= "table" then
storage.healing2 = {on=false, title="HP%", text="exura vita", min=0, max=50}
end
-- create 2 healing widgets
for _, healingInfo in ipairs({storage.healing1, storage.healing2}) do
local healingmacro = macro(20, function()
local hp = player:getHealthPercent()
if healingInfo.max >= hp and hp >= healingInfo.min then
if TargetBot then
TargetBot.saySpell(healingInfo.text) -- sync spell with targetbot if available
else
say(healingInfo.text)
end
end
end)
healingmacro.setOn(healingInfo.on)
UI.DualScrollPanel(healingInfo, function(widget, newParams)
healingInfo = newParams
healingmacro.setOn(healingInfo.on)
end)
end
UI.Separator()
UI.Label("Mana & health potions/runes")
if type(storage.hpitem1) ~= "table" then
storage.hpitem1 = {on=false, title="HP%", item=266, min=51, max=90}
end
if type(storage.hpitem2) ~= "table" then
storage.hpitem2 = {on=false, title="HP%", item=3160, min=0, max=50}
end
if type(storage.manaitem1) ~= "table" then
storage.manaitem1 = {on=false, title="MP%", item=268, min=51, max=90}
end
if type(storage.manaitem2) ~= "table" then
storage.manaitem2 = {on=false, title="MP%", item=3157, min=0, max=50}
end
for i, healingInfo in ipairs({storage.hpitem1, storage.hpitem2, storage.manaitem1, storage.manaitem2}) do
local healingmacro = macro(20, function()
local hp = i <= 2 and player:getHealthPercent() or math.min(100, math.floor(100 * (player:getMana() / player:getMaxMana())))
if healingInfo.max >= hp and hp >= healingInfo.min then
if TargetBot then
TargetBot.useItem(healingInfo.item, player) -- sync spell with targetbot if available
else
useWith(healingInfo.item, player)
end
end
end)
healingmacro.setOn(healingInfo.on)
UI.DualScrollItemPanel(healingInfo, function(widget, newParams)
healingInfo = newParams
healingmacro.setOn(healingInfo.on)
end)
end
UI.Separator()
UI.Label("Mana shield spell:")
UI.TextEdit(storage.manaShield or "utamo vita", function(widget, newText)
storage.manaShield = newText
end)
local lastManaShield = 0
macro(20, "mana shield", function()
if hasManaShield() or lastManaShield + 90000 > now then return end
if TargetBot then
TargetBot.saySpell(storage.manaShield) -- sync spell with targetbot if available
else
say(storage.manaShield)
end
end)
UI.Label("Haste spell:")
UI.TextEdit(storage.hasteSpell or "utani hur", function(widget, newText)
storage.hasteSpell = newText
end)
macro(500, "haste", function()
if hasHaste() then return end
if TargetBot then
TargetBot.saySpell(storage.hasteSpell) -- sync spell with targetbot if available
else
say(storage.hasteSpell)
end
end)
UI.Label("Anti paralyze spell:")
UI.TextEdit(storage.antiParalyze or "utani hur", function(widget, newText)
storage.antiParalyze = newText
end)
macro(100, "anti paralyze", function()
if not isParalyzed() then return end
if TargetBot then
TargetBot.saySpell(storage.antiParalyze) -- sync spell with targetbot if available
else
say(storage.antiParalyze)
end
end)
UI.Separator()
UI.Label("Eatable items:")
if type(storage.foodItems) ~= "table" then
storage.foodItems = {3582, 3577}
end
local foodContainer = UI.Container(function(widget, items)
storage.foodItems = items
end, true)
foodContainer:setHeight(35)
foodContainer:setItems(storage.foodItems)
macro(10000, "eat food", function()
if not storage.foodItems[1] then return end
-- search for food in containers
for _, container in pairs(g_game.getContainers()) do
for __, item in ipairs(container:getItems()) do
for i, foodItem in ipairs(storage.foodItems) do
if item:getId() == foodItem.id then
return g_game.use(item)
end
end
end
end
-- can't find any food, try to eat random item using hotkey
local toEat = storage.foodItems[math.random(1, #storage.foodItems)]
if toEat then g_game.useInventoryItem(toEat.id) end
end)
UI.Separator()
UI.Label("Auto equip")
if type(storage.autoEquip) ~= "table" then
storage.autoEquip = {}
end
for i=1,4 do -- if you want more auto equip panels you can change 4 to higher value
if not storage.autoEquip[i] then
storage.autoEquip[i] = {on=false, title="Auto Equip", item1=i == 1 and 3052 or 0, item2=i == 1 and 3089 or 0, slot=i == 1 and 9 or 0}
end
UI.TwoItemsAndSlotPanel(storage.autoEquip[i], function(widget, newParams)
storage.autoEquip[i] = newParams
end)
end
macro(250, function()
local containers = g_game.getContainers()
for index, autoEquip in ipairs(storage.autoEquip) do
if autoEquip.on then
local slotItem = getSlot(autoEquip.slot)
if not slotItem or (slotItem:getId() ~= autoEquip.item1 and slotItem:getId() ~= autoEquip.item2) then
for _, container in pairs(containers) do
for __, item in ipairs(container:getItems()) do
if item:getId() == autoEquip.item1 or item:getId() == autoEquip.item2 then
g_game.move(item, {x=65535, y=autoEquip.slot, z=0}, item:getCount())
delay(1000) -- don't call it to often
return
end
end
end
end
end
end
end)

View File

@ -0,0 +1,22 @@
-- main tab
VERSION = "1.0"
UI.Label("Config version: " .. VERSION)
UI.Separator()
UI.Separator()
UI.Button("Discord", function()
g_platform.openUrl("https://discord.gg/yhqBE4A")
end)
UI.Button("Forum", function()
g_platform.openUrl("https://otland.net/forums/otclient.494/")
end)
UI.Button("Help & Tutorials", function()
g_platform.openUrl("http://bot.otclient.ovh/")
end)

View File

@ -0,0 +1,41 @@
-- Magic wall & Wild growth timer
-- config
local magicWallId = 2129
local magicWallTime = 20000
local wildGrowthId = 2130
local wildGrowthTime = 45000
-- script
local activeTimers = {}
onAddThing(function(tile, thing)
if not thing:isItem() then
return
end
local timer = 0
if thing:getId() == magicWallId then
timer = magicWallTime
elseif thing:getId() == wildGrowthId then
timer = wildGrowthTime
else
return
end
local pos = tile:getPosition().x .. "," .. tile:getPosition().y .. "," .. tile:getPosition().z
if not activeTimers[pos] or activeTimers[pos] < now then
activeTimers[pos] = now + timer
end
tile:setTimer(activeTimers[pos] - now)
end)
onRemoveThing(function(tile, thing)
if not thing:isItem() then
return
end
if (thing:getId() == magicWallId or thing:getId() == wildGrowthId) and tile:getGround() then
local pos = tile:getPosition().x .. "," .. tile:getPosition().y .. "," .. tile:getPosition().z
activeTimers[pos] = nil
tile:setTimer(0)
end
end)

View File

@ -0,0 +1,93 @@
TargetBot.Creature = {}
TargetBot.Creature.configsCache = {}
TargetBot.Creature.cached = 0
TargetBot.Creature.resetConfigs = function()
TargetBot.targetList:destroyChildren()
TargetBot.Creature.resetConfigsCache()
end
TargetBot.Creature.resetConfigsCache = function()
TargetBot.Creature.configsCache = {}
TargetBot.Creature.cached = 0
end
TargetBot.Creature.addConfig = function(config, focus)
if type(config) ~= 'table' or type(config.name) ~= 'string' then
return error("Invalid targetbot creature config (missing name)")
end
TargetBot.Creature.resetConfigsCache()
if not config.regex then
config.regex = "^" .. config.name:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$"
end
local widget = UI.createWidget("TargetBotEntry", TargetBot.targetList)
widget:setText(config.name)
widget.value = config
widget.onDoubleClick = function(entry) -- edit on double click
schedule(20, function() -- schedule to have correct focus
TargetBot.Creature.edit(entry.value, function(newConfig)
entry:setText(newConfig.name)
entry.value = newConfig
TargetBot.Creature.resetConfigsCache()
TargetBot.save()
end)
end)
end
if focus then
widget:focus()
TargetBot.targetList:ensureChildVisible(widget)
end
return widget
end
TargetBot.Creature.getConfigs = function(creature)
if not creature then return {} end
local name = creature:getName():trim():lower()
-- this function may be slow, so it will be using cache
if TargetBot.Creature.configsCache[name] then
return TargetBot.Creature.configsCache[name]
end
local configs = {}
for _, config in ipairs(TargetBot.targetList:getChildren()) do
if regexMatch(name, config.value.regex)[1] then
table.insert(configs, config.value)
end
end
if TargetBot.Creature.cached > 1000 then
TargetBot.Creature.resetConfigsCache() -- too big cache size, reset
end
TargetBot.Creature.configsCache[name] = configs -- add to cache
TargetBot.Creature.cached = TargetBot.Creature.cached + 1
return configs
end
TargetBot.Creature.calculateParams = function(creature, path)
local configs = TargetBot.Creature.getConfigs(creature)
local priority = 0
local danger = 0
local selectedConfig = nil
for _, config in ipairs(configs) do
local config_priority = TargetBot.Creature.calculatePriority(creature, config, path)
if config_priority > priority then
priority = config_priority
danger = TargetBot.Creature.calculateDanger(creature, config, path)
selectedConfig = config
end
end
return {
config = selectedConfig,
creature = creature,
danger = danger,
priority = priority
}
end
TargetBot.Creature.calculateDanger = function(creature, config, path)
-- config is based on creature_editor
return config.danger
end

View File

@ -0,0 +1,70 @@
TargetBot.Creature.attack = function(params, targets, isLooting) -- params {config, creature, danger, priority}
if player:isWalking() then
lastWalk = now
end
local config = params.config
local creature = params.creature
if g_game.getAttackingCreature() ~= creature then
g_game.attack(creature)
end
if not isLooting then -- walk only when not looting
TargetBot.Creature.walk(creature, config, targets)
end
-- attacks
local mana = player:getMana()
if config.useGroupAttack and config.groupAttackSpell:len() > 1 and mana > config.minManaGroup then
local creatures = g_map.getSpectatorsInRange(player:getPosition(), false, config.groupAttackRadius, config.groupAttackRadius)
local playersAround = false
local monsters = 0
for _, creature in ipairs(creatures) do
if not creature:isLocalPlayer() and creature:isPlayer() then
playersAround = true
elseif creature:isMonster() then
monsters = monsters + 1
end
end
if monsters >= config.groupAttackTargets and (not playersAround or config.groupAttackIgnorePlayers) then
if TargetBot.sayAttackSpell(config.groupAttackSpell, config.groupAttackDelay) then
return
end
end
end
if config.useSpellAttack and config.attackSpell:len() > 1 and mana > config.minMana then
if TargetBot.sayAttackSpell(config.attackSpell, config.attackSpellDelay) then
return
end
end
if config.useRuneAttack and config.attackRune > 100 then
if TargetBot.useAttackItem(config.attackRune, creature, config.attackRuneDelay) then
return
end
end
end
TargetBot.Creature.walk = function(creature, config, targets)
-- luring
if config.lure and not (config.chase and creature:getHealthPercent() < 30) then
local monsters = 0
if targets < config.lureCount then
local path = findPath(player:getPosition(), creature:getPosition(), 5, {ignoreNonPathable=true, precision=2})
if path then
return TargetBot.walkTo(creature:getPosition(), 10, {marginMin=5, marginMax=6, ignoreNonPathable=true})
end
end
end
local currentDistance = findPath(player:getPosition(), creature:getPosition(), 10, {ignoreCreatures=true, ignoreNonPathable=true, ignoreCost=true})
if config.chase and (creature:getHealthPercent() < 30 or not config.keepDistance) then
if #currentDistance > 1 then
return TargetBot.walkTo(creature:getPosition(), 10, {ignoreNonPathable=true, precision=1})
end
elseif config.keepDistance then
if #currentDistance ~= config.keepDistanceRange and #currentDistance ~= config.keepDistanceRange + 1 then
return TargetBot.walkTo(creature:getPosition(), 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1})
end
end
end

View File

@ -0,0 +1,99 @@
TargetBot.Creature.edit = function(config, callback) -- callback = function(newConfig)
config = config or {}
local editor = UI.createWindow('TargetBotCreatureEditorWindow')
local values = {} -- (key, function returning value of key)
editor.name:setText(config.name or "")
table.insert(values, {"name", function() return editor.name:getText() end})
local addScrollBar = function(id, title, min, max, defaultValue)
local widget = UI.createWidget('TargetBotCreatureEditorScrollBar', editor.left)
widget.scroll.onValueChange = function(scroll, value)
widget.text:setText(title .. ": " .. value)
end
widget.scroll:setRange(min, max)
if max-min > 1000 then
widget.scroll:setStep(100)
elseif max-min > 100 then
widget.scroll:setStep(10)
end
widget.scroll:setValue(config[id] or defaultValue)
widget.scroll.onValueChange(widget.scroll, widget.scroll:getValue())
table.insert(values, {id, function() return widget.scroll:getValue() end})
end
local addTextEdit = function(id, title, defaultValue)
local widget = UI.createWidget('TargetBotCreatureEditorTextEdit', editor.right)
widget.text:setText(title)
widget.textEdit:setText(config[id] or defaultValue or "")
table.insert(values, {id, function() return widget.textEdit:getText() end})
end
local addCheckBox = function(id, title, defaultValue)
local widget = UI.createWidget('TargetBotCreatureEditorCheckBox', editor.right)
widget.onClick = function()
widget:setOn(not widget:isOn())
end
widget:setText(title)
if config[id] == nil then
widget:setOn(defaultValue)
else
widget:setOn(config[id])
end
table.insert(values, {id, function() return widget:isOn() end})
end
local addItem = function(id, title, defaultItem)
local widget = UI.createWidget('TargetBotCreatureEditorItem', editor.right)
widget.text:setText(title)
widget.item:setItemId(config[id] or defaultItem)
table.insert(values, {id, function() return widget.item:getItemId() end})
end
editor.cancel.onClick = function()
editor:destroy()
end
editor.onEscape = editor.cancel.onClick
editor.ok.onClick = function()
local newConfig = {}
for _, value in ipairs(values) do
newConfig[value[1]] = value[2]()
end
if newConfig.name:len() < 1 then return end
newConfig.regex = "^" .. newConfig.name:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$"
editor:destroy()
callback(newConfig)
end
-- values
addScrollBar("priority", "Priority", 0, 10, 1)
addScrollBar("danger", "Danger", 0, 10, 1)
addScrollBar("maxDistance", "Max distance", 1, 10, 10)
addScrollBar("keepDistanceRange", "Keep distance", 1, 5, 1)
addScrollBar("lureCount", "Lure", 0, 5, 1)
addScrollBar("minMana", "Min. mana for attack spell", 0, 3000, 200)
addScrollBar("attackSpellDelay", "Attack spell delay", 200, 5000, 2500)
addScrollBar("minManaGroup", "Min. mana for group attack", 0, 3000, 1500)
addScrollBar("groupAttackTargets", "Min. targets for group attack", 1, 10, 2)
addScrollBar("groupAttackRadius", "Radius of group attack spell", 1, 7, 1)
addScrollBar("groupAttackDelay", "Group attack spell delay", 200, 60000, 5000)
addScrollBar("runeAttackDelay", "Rune attack delay", 200, 5000, 2000)
addCheckBox("chase", "Chase", true)
addCheckBox("keepDistance", "Keep Distance", false)
addCheckBox("lure", "Lure", false)
-- addCheckBox("avoidAttacks", "Avoid attacks", true)
addCheckBox("useSpellAttack", "Use attack spell", false)
addTextEdit("attackSpell", "Attack spell", "")
addCheckBox("useGroupAttack", "Use group attack spell", false)
addCheckBox("groupAttackIgnorePlayers", "Ignore players in group attack", false)
addTextEdit("groupAttackSpell", "Group attack spell", "")
addCheckBox("useRuneAttack", "Use attack rune", false)
addItem("attackRune", "Attack rune:", 0)
end

View File

@ -0,0 +1,134 @@
TargetBotCreatureEditorScrollBar < Panel
height: 35
margin-top: 7
Label
id: text
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
text-align: center
HorizontalScrollBar
id: scroll
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 5
minimum: 0
maximum: 10
step: 1
TargetBotCreatureEditorTextEdit < Panel
height: 40
margin-top: 7
Label
id: text
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
text-align: center
TextEdit
id: textEdit
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 5
minimum: 0
maximum: 10
step: 1
TargetBotCreatureEditorItem < Panel
height: 34
margin-top: 7
margin-left: 25
margin-right: 25
Label
id: text
anchors.left: parent.left
anchors.verticalCenter: next.verticalCenter
BotItem
id: item
anchors.top: parent.top
anchors.right: parent.right
TargetBotCreatureEditorCheckBox < BotSwitch
height: 20
margin-top: 7
TargetBotCreatureEditorWindow < MainWindow
text: TargetBot creature editor
width: 500
height: 650
Label
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
text-align: center
text: You can use * (any characters) and ? (any character) in target name
TextEdit
id: name
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-left: 90
margin-top: 5
Label
anchors.verticalCenter: prev.verticalCenter
anchors.left: parent.left
text: Target name:
Panel
id: left
anchors.top: name.bottom
anchors.left: parent.left
anchors.right: parent.horizontalCenter
margin-top: 5
margin-left: 10
margin-right: 10
layout:
type: verticalBox
fit-children: true
Panel
id: right
anchors.top: name.bottom
anchors.left: parent.horizontalCenter
anchors.right: parent.right
margin-top: 5
margin-left: 10
margin-right: 10
layout:
type: verticalBox
fit-children: true
Button
id: help
!text: tr('Help & Tutorials')
anchors.bottom: parent.bottom
anchors.left: parent.left
width: 150
@onClick: g_platform.openUrl("http://bot.otclient.ovh/")
Button
id: ok
!text: tr('Ok')
anchors.bottom: parent.bottom
anchors.right: next.left
margin-right: 10
width: 60
Button
id: cancel
!text: tr('Cancel')
anchors.bottom: parent.bottom
anchors.right: parent.right
width: 60

View File

@ -0,0 +1,37 @@
TargetBot.Creature.calculatePriority = function(creature, config, path)
-- config is based on creature_editor
local priority = config.priority
-- extra priority if it's current target
if g_game.getAttackingCreature() == creature then
priority = priority + 1
end
-- check if distance is fine, if not then attack only if already attacked
if #path > config.maxDistance then
return priority
end
-- extra priority for close distance
local path_length = #path
if path_length == 1 then
priority = priority + 3
elseif path_length <= 3 then
priority = priority + 1
end
-- extra priority for low health
if config.chase and creature:getHealthPercent() < 30 then
priority = priority + 5
elseif creature:getHealthPercent() < 20 then
priority = priority + 2.5
elseif creature:getHealthPercent() < 40 then
priority = priority + 1.5
elseif creature:getHealthPercent() < 60 then
priority = priority + 0.5
elseif creature:getHealthPercent() < 80 then
priority = priority + 0.2
end
return priority
end

View File

@ -0,0 +1,295 @@
TargetBot.Looting = {}
TargetBot.Looting.list = {} -- list of containers to loot
local ui
local items = {}
local containers = {}
local itemsById = {}
local containersById = {}
local dontSave = false
TargetBot.Looting.setup = function()
ui = UI.createWidget("TargetBotLootingPanel")
UI.Container(TargetBot.Looting.onItemsUpdate, true, nil, ui.items)
UI.Container(TargetBot.Looting.onContainersUpdate, true, nil, ui.containers)
ui.everyItem.onClick = function()
ui.everyItem:setOn(not ui.everyItem:isOn())
TargetBot.save()
end
ui.maxDangerPanel.value.onTextChange = function()
local value = tonumber(ui.maxDangerPanel.value:getText())
if not value then
ui.maxDangerPanel.value:setText(0)
end
if dontSave then return end
TargetBot.save()
end
ui.minCapacityPanel.value.onTextChange = function()
local value = tonumber(ui.minCapacityPanel.value:getText())
if not value then
ui.minCapacityPanel.value:setText(0)
end
if dontSave then return end
TargetBot.save()
end
end
TargetBot.Looting.onItemsUpdate = function()
if dontSave then return end
TargetBot.save()
TargetBot.Looting.updateItemsAndContainers()
end
TargetBot.Looting.onContainersUpdate = function()
if dontSave then return end
TargetBot.save()
TargetBot.Looting.updateItemsAndContainers()
end
TargetBot.Looting.update = function(data)
dontSave = true
TargetBot.Looting.list = {}
ui.items:setItems(data['items'] or {})
ui.containers:setItems(data['containers'] or {})
ui.everyItem:setOn(data['everyItem'])
ui.maxDangerPanel.value:setText(data['maxDanger'] or 10)
ui.minCapacityPanel.value:setText(data['minCapacity'] or 100)
TargetBot.Looting.updateItemsAndContainers()
dontSave = false
end
TargetBot.Looting.save = function(data)
data['items'] = ui.items:getItems()
data['containers'] = ui.containers:getItems()
data['maxDanger'] = tonumber(ui.maxDangerPanel.value:getText())
data['minCapacity'] = tonumber(ui.minCapacityPanel.value:getText())
data['everyItem'] = ui.everyItem:isOn()
end
TargetBot.Looting.updateItemsAndContainers = function()
items = ui.items:getItems()
containers = ui.containers:getItems()
itemsById = {}
containersById = {}
for i, item in ipairs(items) do
itemsById[item.id] = 1
end
for i, container in ipairs(containers) do
containersById[container.id] = 1
end
end
local waitTill = 0
local waitingForContainer = nil
local status = ""
local lastFoodConsumption = 0
TargetBot.Looting.getStatus = function()
return status
end
TargetBot.Looting.process = function(targets, dangerLevel)
if (not items[1] and not ui.everyItem:isOn()) or not containers[1] then
status = ""
return false
end
if dangerLevel > tonumber(ui.maxDangerPanel.value:getText()) then
status = "High danger"
return false
end
if player:getFreeCapacity() < tonumber(ui.minCapacityPanel.value:getText()) then
status = "No cap"
TargetBot.Looting.list = {}
return false
end
local loot = TargetBot.Looting.list[1]
if loot == nil then
status = ""
return false
end
if waitTill > now then
return true
end
local containers = g_game.getContainers()
local lootContainers = TargetBot.Looting.getLootContainers(containers)
-- check if there's container for loot and has empty space for it
if not lootContainers[1] then
-- there's no space, don't loot
status = "No space"
return false
end
status = "Looting"
for index, container in pairs(containers) do
if container.lootContainer then
TargetBot.Looting.lootContainer(lootContainers, container)
return true
end
end
local pos = player:getPosition()
local dist = math.max(math.abs(pos.x-loot.pos.x), math.abs(pos.y-loot.pos.y))
if loot.tries > 30 or loot.pos.z ~= pos.z or dist > 20 then
table.remove(TargetBot.Looting.list, 1)
return true
end
local tile = g_map.getTile(loot.pos)
if dist >= 3 or not tile then
loot.tries = loot.tries + 1
TargetBot.walkTo(loot.pos, 20, { ignoreNonPathable = true, precision = 2 })
return true
end
local container = tile:getTopUseThing()
if not container or container:getId() ~= loot.container then
table.remove(TargetBot.Looting.list, 1)
return true
end
g_game.open(container)
waitTill = now + 1000 -- give it 1s to open
waitingForContainer = loot.container
loot.tries = loot.tries + 10
return true
end
TargetBot.Looting.getLootContainers = function(containers)
local lootContainers = {}
local openedContainersById = {}
local toOpen = nil
for index, container in pairs(containers) do
openedContainersById[container:getContainerItem():getId()] = 1
if containersById[container:getContainerItem():getId()] and not container.lootContainer then
if container:getItemsCount() < container:getCapacity() then
table.insert(lootContainers, container)
else -- it's full, open next container if possible
for slot, item in ipairs(container:getItems()) do
if item:isContainer() and containersById[item:getId()] then
toOpen = {item, container}
break
end
end
end
end
end
if not lootContainers[1] then
if toOpen then
g_game.open(toOpen[1], toOpen[2])
waitTill = now + 500 -- wait 0.5s
return lootContainers
end
-- check containers one more time, maybe there's any loot container
for index, container in pairs(containers) do
if not containersById[container:getContainerItem():getId()] and not container.lootContainer then
for slot, item in ipairs(container:getItems()) do
if item:isContainer() and containersById[item:getId()] then
g_game.open(item)
waitTill = now + 500 -- wait 0.5s
return lootContainers
end
end
end
end
-- can't find any lootContainer, let's check slots, maybe there's one
for slot = InventorySlotFirst, InventorySlotLast do
local item = getInventoryItem(slot)
if item and item:isContainer() and not openedContainersById[item:getId()] then
-- container which is not opened yet, let's open it
g_game.open(item)
waitTill = now + 500 -- wait 0.5s
return lootContainers
end
end
end
return lootContainers
end
TargetBot.Looting.lootContainer = function(lootContainers, container)
-- loot items
local nextContainer = nil
for i, item in ipairs(container:getItems()) do
if item:isContainer() then
nextContainer = item
elseif itemsById[item:getId()] or ui.everyItem:isOn() then
item.lootTries = (item.lootTries or 0) + 1
if item.lootTries < 5 then -- if can't be looted within 0.5s then skip it
return TargetBot.Looting.lootItem(lootContainers, item)
end
elseif storage.foodItems and storage.foodItems[1] and lastFoodConsumption + 5000 < now then
for _, food in ipairs(storage.foodItems) do
if item:getId() == food.id then
g_game.use(item)
lastFoodConsumption = now
return
end
end
end
end
-- no more items to loot, open next container
if nextContainer then
nextContainer.lootTries = (nextContainer.lootTries or 0) + 1
if nextContainer.lootTries < 2 then -- max 0.6s to open it
g_game.open(nextContainer, container)
waitTill = now + 300 -- give it 0.3s to open
waitingForContainer = loot.container
return
end
end
-- looting finished, remove container from list
container.lootContainer = false
g_game.close(container)
table.remove(TargetBot.Looting.list, 1)
end
TargetBot.Looting.lootItem = function(lootContainers, item)
if item:isStackable() then
local count = item:getCount()
for _, container in ipairs(lootContainers) do
for slot, citem in ipairs(container:getItems()) do
if item:getId() == citem:getId() and citem:getCount() < 100 then
g_game.move(item, container:getSlotPosition(slot - 1), count)
waitTill = now + 300 -- give it 0.3s to move item
return
end
end
end
end
local container = lootContainers[1]
g_game.move(item, container:getSlotPosition(container:getItemsCount()), 1)
waitTill = now + 300 -- give it 0.3s to move item
end
onContainerOpen(function(container, previousContainer)
if container:getContainerItem():getId() == waitingForContainer then
container.lootContainer = true
waitingForContainer = nil
end
end)
onCreatureDisappear(function(creature)
if not TargetBot.isOn() then return end
if not creature:isMonster() then return end
local pos = player:getPosition()
local mpos = creature:getPosition()
local name = creature:getName()
if pos.z ~= mpos.z or math.max(math.abs(pos.x-mpos.x), math.abs(pos.y-mpos.y)) > 6 then return end
schedule(20, function() -- check in 20ms if there's container (dead body) on that tile
if not containers[1] then return end
if TargetBot.Looting.list[20] then return end -- too many items to loot
local tile = g_map.getTile(mpos)
if not tile then return end
local container = tile:getTopUseThing()
if not container or not container:isContainer() then return end
if not findPath(player:getPosition(), mpos, 6, {ignoreNonPathable=true, ignoreCreatures=true, ignoreCost=true}) then return end
table.insert(TargetBot.Looting.list, {pos=mpos, creature=name, container=container:getId(), added=now, tries=0})
container:setMarked('#000088')
end)
end)

View File

@ -0,0 +1,83 @@
TargetBotLootingPanel < Panel
layout:
type: verticalBox
fit-children: true
HorizontalSeparator
margin-top: 5
Label
margin-top: 5
text: Items to loot
text-align: center
BotContainer
id: items
margin-top: 3
BotSwitch
id: everyItem
!text: tr("Loot every item")
margin-top: 2
Label
margin-top: 5
text: Containers for loot
text-align: center
BotContainer
id: containers
margin-top: 3
height: 45
Panel
id: maxDangerPanel
height: 20
margin-top: 5
BotTextEdit
id: value
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
margin-right: 6
width: 80
Label
anchors.left: parent.left
anchors.verticalCenter: prev.verticalCenter
text: Max. danger:
margin-left: 5
Panel
id: minCapacityPanel
height: 20
margin-top: 3
BotTextEdit
id: value
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
margin-right: 6
width: 80
Label
anchors.left: parent.left
anchors.verticalCenter: prev.verticalCenter
text: Min. capacity:
margin-left: 5
Label
margin-top: 3
margin-left: 20
margin-right: 20
!text: tr("Drag item or click on any of empty slot")
text-align: center
text-wrap: true
text-auto-resize: true
BotButton
margin-top: 3
text: Help & Tutorials
@onClick: g_platform.openUrl("http://bot.otclient.ovh/")

View File

@ -0,0 +1,231 @@
local targetbotMacro = nil
local config = nil
local lastAction = 0
-- ui
local configWidget = UI.Config()
local ui = UI.createWidget("TargetBotPanel")
ui.list = ui.listPanel.list -- shortcut
TargetBot.targetList = ui.list
TargetBot.Looting.setup()
ui.status.left:setText("Status:")
ui.status.right:setText("Off")
ui.target.left:setText("Target:")
ui.target.right:setText("-")
ui.config.left:setText("Config:")
ui.config.right:setText("-")
ui.danger.left:setText("Danger:")
ui.danger.right:setText("0")
ui.editor.debug.onClick = function()
local on = ui.editor.debug:isOn()
ui.editor.debug:setOn(not on)
if on then
for _, spec in ipairs(getSpectators()) do
spec:clearText()
end
end
end
-- main loop, controlled by config
targetbotMacro = macro(100, function()
local pos = player:getPosition()
local creatures = g_map.getSpectatorsInRange(pos, false, 6, 6) -- 12x12 area
if #creatures > 10 then -- if there are too many monsters around, limit area
creatures = g_map.getSpectatorsInRange(pos, false, 3, 3) -- 6x6 area
end
local highestPriority = 0
local dangerLevel = 0
local targets = 0
local highestPriorityParams = nil
for i, creature in ipairs(creatures) do
local path = findPath(player:getPosition(), creature:getPosition(), 7, {ignoreLastCreature=true, ignoreNonPathable=true, ignoreCost=true})
if creature:isMonster() and path then
local params = TargetBot.Creature.calculateParams(creature, path) -- return {craeture, config, danger, priority}
dangerLevel = dangerLevel + params.danger
if params.priority > 0 then
targets = targets + 1
if params.priority > highestPriority then
highestPriority = params.priority
highestPriorityParams = params
end
if ui.editor.debug:isOn() then
creature:setText(params.config.name .. "\n" .. params.priority)
end
end
end
end
-- reset walking
TargetBot.walkTo(nil)
-- looting
local looting = TargetBot.Looting.process(targets, dangerLevel)
local lootingStatus = TargetBot.Looting.getStatus()
ui.danger.right:setText(dangerLevel)
if highestPriorityParams and not isInPz() then
ui.target.right:setText(highestPriorityParams.creature:getName())
ui.config.right:setText(highestPriorityParams.config.name)
TargetBot.Creature.attack(highestPriorityParams, targets, looting)
if lootingStatus:len() > 0 then
TargetBot.setStatus("Attack & " .. lootingStatus)
else
TargetBot.setStatus("Attacking")
end
TargetBot.walk()
lastAction = now
return
end
ui.target.right:setText("-")
ui.config.right:setText("-")
if looting then
TargetBot.walk()
lastAction = now
end
if lootingStatus:len() > 0 then
TargetBot.setStatus(lootingStatus)
else
TargetBot.setStatus("Waiting")
end
end)
-- config, its callback is called immediately, data can be nil
config = Config.setup("targetbot_configs", configWidget, "json", function(name, enabled, data)
if not data then
ui.status.right:setText("Off")
return targetbotMacro.setOff()
end
TargetBot.Creature.resetConfigs()
for _, value in ipairs(data["targeting"] or {}) do
TargetBot.Creature.addConfig(value)
end
TargetBot.Looting.update(data["looting"] or {})
-- add configs
if enabled then
ui.status.right:setText("On")
else
ui.status.right:setText("Off")
end
targetbotMacro.setOn(enabled)
targetbotMacro.delay = nil
end)
-- setup ui
ui.editor.buttons.add.onClick = function()
TargetBot.Creature.edit(nil, function(newConfig)
TargetBot.Creature.addConfig(newConfig, true)
TargetBot.save()
end)
end
ui.editor.buttons.edit.onClick = function()
local entry = ui.list:getFocusedChild()
if not entry then return end
TargetBot.Creature.edit(entry.value, function(newConfig)
entry:setText(newConfig.name)
entry.value = newConfig
TargetBot.Creature.resetConfigsCache()
TargetBot.save()
end)
end
ui.editor.buttons.remove.onClick = function()
local entry = ui.list:getFocusedChild()
if not entry then return end
entry:destroy()
TargetBot.Creature.resetConfigsCache()
TargetBot.save()
end
-- public function, you can use them in your scripts
TargetBot.isActive = function() -- return true if attacking or looting takes place
return lastAction + 300 > now
end
TargetBot.setStatus = function(text)
return ui.status.right:setText(text)
end
TargetBot.isOn = function()
return config.isOn()
end
TargetBot.isOff = function()
return config.isOff()
end
TargetBot.setOn = function(val)
if val == false then
return TargetBot.setOff(true)
end
config.setOn()
end
TargetBot.setOff = function(val)
if val == false then
return TargetBot.setOn(true)
end
config.setOff()
end
TargetBot.delay = function(value)
targetbotMacro.delay = now + value
end
TargetBot.save = function()
local data = {targeting={}, looting={}}
for _, entry in ipairs(ui.list:getChildren()) do
table.insert(data.targeting, entry.value)
end
TargetBot.Looting.save(data.looting)
config.save(data)
end
-- attacks
local lastSpell = 0
local lastAttackSpell = 0
TargetBot.saySpell = function(text, delay)
if type(text) ~= 'string' or text:len() < 1 then return end
if not delay then delay = 500 end
if g_game.getProtocolVersion() < 1090 then
lastAttackSpell = now -- pause attack spells, healing spells are more important
end
if lastSpell + delay < now then
say(text)
lastSpell = now
return true
end
return false
end
TargetBot.sayAttackSpell = function(text, delay)
if type(text) ~= 'string' or text:len() < 1 then return end
if not delay then delay = 2000 end
if lastAttackSpell + delay < now then
say(text)
lastAttackSpell = now
return true
end
return false
end
local lastRuneAttack = 0
TargetBot.useItem = function(item, target, delay)
useWith(item, target)
end
TargetBot.useAttackItem = function(item, target, delay)
if not delay then delay = 2000 end
if lastRuneAttack + delay < now then
useWith(item, target)
lastRuneAttack = now
end
end

View File

@ -0,0 +1,115 @@
TargetBotEntry < Label
background-color: alpha
text-offset: 2 0
focusable: true
$focus:
background-color: #00000055
TargetBotDualLabel < Panel
height: 18
margin-left: 3
margin-right: 4
Label
id: left
anchors.top: parent.top
anchors.left: parent.left
text-auto-resize: true
Label
id: right
anchors.top: parent.top
anchors.right: parent.right
text-auto-resize: true
TargetBotPanel < Panel
layout:
type: verticalBox
fit-children: true
HorizontalSeparator
margin-top: 2
margin-bottom: 5
TargetBotDualLabel
id: status
TargetBotDualLabel
id: target
TargetBotDualLabel
id: config
TargetBotDualLabel
id: danger
Panel
id: listPanel
height: 40
TextList
id: list
anchors.fill: parent
vertical-scrollbar: listScrollbar
margin-right: 15
focusable: false
auto-focus: first
VerticalScrollBar
id: listScrollbar
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
pixels-scroll: true
step: 10
BotSwitch
id: configButton
@onClick: |
self:setOn(not self:isOn())
self:getParent().listPanel:setHeight(self:isOn() and 100 or 40)
self:getParent().editor:setVisible(self:isOn())
$on:
text: Hide target editor
$!on:
text: Show target editor
Panel
id: editor
visible: false
layout:
type: verticalBox
fit-children: true
Panel
id: buttons
height: 20
margin-top: 2
Button
id: add
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
text: Add
width: 56
Button
id: edit
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
text: Edit
width: 56
Button
id: remove
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
text: Remove
width: 56
BotSwitch
id: debug
text: Show target priority

View File

@ -0,0 +1,28 @@
local dest
local maxDist
local params
TargetBot.walkTo = function(_dest, _maxDist, _params)
dest = _dest
maxDist = _maxDist
params = _params
end
-- called every 100ms if targeting or looting is active
TargetBot.walk = function()
if not dest then return end
if player:isWalking() then return end
local pos = player:getPosition()
if pos.z ~= dest.z then return end
local dist = math.max(math.abs(pos.x-dest.x), math.abs(pos.y-dest.y))
if params.precision and params.precision >= dist then return end
if params.marginMin and params.marginMax then
if dist >= params.marginMin and dist <= params.marginMax then
return
end
end
local path = getPath(pos, dest, maxDist, params)
if path then
walk(path[1])
end
end

View File

@ -0,0 +1,110 @@
-- tools tab
setDefaultTab("Tools")
-- allows to test/edit bot lua scripts ingame, you can have multiple scripts like this, just change storage.ingame_lua
UI.Button("Ingame macro editor", function(newText)
UI.MultilineEditorWindow(storage.ingame_macros or "", {title="Macro editor", description="You can add your custom macros (or any other lua code) here"}, function(text)
storage.ingame_macros = text
reload()
end)
end)
UI.Button("Ingame hotkey editor", function(newText)
UI.MultilineEditorWindow(storage.ingame_hotkeys or "", {title="Hotkeys editor", description="You can add your custom hotkeys/singlehotkeys here"}, function(text)
storage.ingame_hotkeys = text
reload()
end)
end)
UI.Separator()
for _, scripts in ipairs({storage.ingame_macros, storage.ingame_hotkeys}) do
if type(scripts) == "string" and scripts:len() > 3 then
local status, result = pcall(function()
assert(load(scripts, "ingame_editor"))()
end)
if not status then
error("Ingame edior error:\n" .. result)
end
end
end
UI.Separator()
UI.Button("Zoom In map [ctrl + =]", function() zoomIn() end)
UI.Button("Zoom Out map [ctrl + -]", function() zoomOut() end)
UI.Separator()
local moneyIds = {3031, 3035} -- gold coin, platinium coin
macro(1000, "Exchange money", function()
local containers = g_game.getContainers()
for index, container in pairs(containers) do
if not container.lootContainer then -- ignore monster containers
for i, item in ipairs(container:getItems()) do
if item:getCount() == 100 then
for m, moneyId in ipairs(moneyIds) do
if item:getId() == moneyId then
return g_game.use(item)
end
end
end
end
end
end
end)
macro(1000, "Stack items", function()
local containers = g_game.getContainers()
local toStack = {}
for index, container in pairs(containers) do
if not container.lootContainer then -- ignore monster containers
for i, item in ipairs(container:getItems()) do
if item:isStackable() and item:getCount() < 100 then
local stackWith = toStack[item:getId()]
if stackWith then
g_game.move(item, stackWith[1], math.min(stackWith[2], item:getCount()))
return
end
toStack[item:getId()] = {container:getSlotPosition(i - 1), 100 - item:getCount()}
end
end
end
end
end)
UI.Separator()
UI.Label("Mana training")
if type(storage.manaTrain) ~= "table" then
storage.manaTrain = {on=false, title="MP%", text="utevo lux", min=80, max=100}
end
local manatrainmacro = macro(1000, function()
local mana = math.min(100, math.floor(100 * (player:getMana() / player:getMaxMana())))
if storage.manaTrain.max >= mana and mana >= storage.manaTrain.min then
say(storage.manaTrain.text)
end
end)
manatrainmacro.setOn(storage.manaTrain.on)
UI.DualScrollPanel(storage.manaTrain, function(widget, newParams)
storage.manaTrain = newParams
manatrainmacro.setOn(storage.manaTrain.on)
end)
UI.Separator()
macro(60000, "Send message on trade", function()
local trade = getChannelId("advertising")
if not trade then
trade = getChannelId("trade")
end
if trade and storage.autoTradeMessage:len() > 0 then
sayChannel(trade, storage.autoTradeMessage)
end
end)
UI.TextEdit(storage.autoTradeMessage or "I'm using OTClientV8!", function(widget, text)
storage.autoTradeMessage = text
end)
UI.Separator()

View File

@ -29,6 +29,20 @@ context.callback = function(callbackType, callback)
context._currentExecution = prevExecution
end
end)
local cb = context._callbacks[callbackType]
return {
remove = function()
local index = nil
for i, cb2 in ipairs(context._callbacks[callbackType]) do
if cb == cb2 then
index = i
end
end
if index then
table.remove(context._callbacks[callbackType], index)
end
end
}
end
-- onKeyDown(callback) -- callback = function(keys)
@ -158,7 +172,7 @@ end
context.listen = function(name, callback)
if not name then return context.error("listen: invalid name") end
name = name:lower()
context.onTalk(function(name2, level, mode, text, channelId, pos)
return context.onTalk(function(name2, level, mode, text, channelId, pos)
if name == name2:lower() then
callback(text, channelId, pos)
end
@ -167,7 +181,7 @@ end
-- onPlayerPositionChange(callback) -- callback = function(newPos, oldPos)
context.onPlayerPositionChange = function(callback)
context.onCreaturePositionChange(function(creature, newPos, oldPos)
return context.onCreaturePositionChange(function(creature, newPos, oldPos)
if creature == context.player then
callback(newPos, oldPos)
end
@ -176,7 +190,7 @@ end
-- onPlayerHealthChange(callback) -- callback = function(healthPercent)
context.onPlayerHealthChange = function(callback)
context.onCreatureHealthPercentChange(function(creature, healthPercent)
return context.onCreatureHealthPercentChange(function(creature, healthPercent)
if creature == context.player then
callback(healthPercent)
end

View File

@ -33,9 +33,10 @@ Config.list = function(dir)
return correctList
end
-- load config from string insteaf of dile
-- load config from string insteaf of file
Config.parse = function(data)
local status, result = pcall(function()
if data:len() < 2 then return {} end
return json.decode(data)
end)
if status and type(result) == 'table' then
@ -51,13 +52,29 @@ Config.parse = function(data)
end
Config.load = function(dir, name)
local file = context.configDir .. "/" .. dir .. "/" .. name .. ".json"
local file = context.configDir .. "/" .. dir .. "/" .. name .. ".json"
if g_resources.fileExists(file) then -- load json
return json.decode(g_resources.readFileContents(file))
local status, result = pcall(function()
local data = g_resources.readFileContents(file)
if data:len() < 2 then return {} end
return json.decode(data)
end)
if not status then
context.error("Invalid json config (" .. name .. "): " .. result)
return {}
end
return result
end
file = context.configDir .. "/" .. dir .. "/" .. name .. ".cfg"
if g_resources.fileExists(file) then -- load cfg
return table.decodeStringPairList(g_resources.readFileContents(file))
local status, result = pcall(function()
return table.decodeStringPairList(g_resources.readFileContents(file))
end)
if not status then
context.error("Invalid cfg config (" .. name .. "): " .. result)
return {}
end
return result
end
return context.error("Config " .. file .. " doesn't exist")
end
@ -87,7 +104,7 @@ Config.save = function(dir, name, value, forcedExtension)
if (table.isStringPairList(value) and forcedExtension ~= "json") or forcedExtension == "cfg" then -- cfg
g_resources.writeFileContents(file .. ".cfg", table.encodeStringPairList(value))
else
g_resources.writeFileContents(file .. ".json", json.encode(value))
g_resources.writeFileContents(file .. ".json", json.encode(value, 2))
end
return true
end
@ -175,7 +192,11 @@ Config.setup = function(dir, widget, configExtension, callback)
if g_resources.fileExists(file) then
return context.error("Config " .. name .. " already exist")
end
g_resources.writeFileContents(file, "")
if configExtension == "json" then
g_resources.writeFileContents(file, json.encode({}))
else
g_resources.writeFileContents(file, "")
end
context.storage._configs[dir].selected = name
widget.switch:setOn(false)
refresh()

View File

@ -1,6 +1,6 @@
local context = G.botContext
context.encode = function(data) return json.encode(data) end
context.encode = function(data, indent) return json.encode(data, indent or 2) end
context.decode = function(text) local status, result = pcall(function() return json.decode(text) end) if status then return result end return {} end
context.displayGeneralBox = function(title, message, buttons, onEnterCallback, onEscapeCallback)

View File

@ -8,5 +8,17 @@ UI.createWidget = function(name, parent)
if parent == nil then
parent = context.panel
end
return g_ui.createWidget(name, parent)
local widget = g_ui.createWidget(name, parent)
widget.botWidget = true
return widget
end
UI.createWindow = function(name)
local widget = g_ui.createWidget(name, g_ui.getRootWidget())
widget.botWidget = true
widget:show()
widget:raise()
widget:focus()
return widget
end

View File

@ -4,6 +4,14 @@ if type(context.UI) ~= "table" then
end
local UI = context.UI
UI.Button = function(text, callback, parent)
local widget = UI.createWidget("BotButton", parent)
widget:setText(text)
widget.onClick = callback
return widget
end
UI.Config = function(parent)
return UI.createWidget("BotConfig", parent)
end
@ -11,8 +19,11 @@ end
-- call :setItems(table) to set items, call :getItems() to get them
-- unique if true, won't allow duplicates
-- callback (can be nil) gets table with new item list, eg: {{id=2160, count=1}, {id=268, count=100}, {id=269, count=20}}
UI.Container = function(callback, unique, parent)
local widget = UI.createWidget("BotContainer", parent)
UI.Container = function(callback, unique, parent, widget)
if not widget then
widget = UI.createWidget("BotContainer", parent)
end
local oldItems = {}
local updateItems = function()
@ -33,7 +44,7 @@ UI.Container = function(callback, unique, parent)
if somethingNew then
oldItems = items
callback(items)
callback(widget, items)
end
widget:setItems(items)
@ -77,6 +88,183 @@ UI.Container = function(callback, unique, parent)
return items
end
widget:setItems({})
return widget
end
UI.DualScrollPanel = function(params, callback, parent) -- callback = function(widget, newParams)
--[[ params:
on - bool,
text - string,
title - string,
min - number,
max - number,
]]
params.title = params.title or "title"
params.text = params.text or ""
params.min = params.min or 20
params.max = params.max or 80
local widget = UI.createWidget('DualScrollPanel', parent)
widget.title:setOn(params.on)
widget.title.onClick = function()
params.on = not params.on
widget.title:setOn(params.on)
if callback then
callback(widget, params)
end
end
widget.text:setText(params.text or "")
widget.text.onTextChange = function(widget, text)
params.text = text
if callback then
callback(widget, params)
end
end
local update = function(dontSignal)
widget.title:setText("" .. params.min .. "% <= " .. params.title .. " <= " .. params.max .. "%")
if callback and not dontSignal then
callback(widget, params)
end
end
widget.scroll1:setValue(params.min)
widget.scroll2:setValue(params.max)
widget.scroll1.onValueChange = function(scroll, value)
params.min = value
update()
end
widget.scroll2.onValueChange = function(scroll, value)
params.max = value
update()
end
update(true)
end
UI.DualScrollItemPanel = function(params, callback, parent) -- callback = function(widget, newParams)
--[[ params:
on - bool,
item - number,
title - string,
min - number,
max - number,
]]
params.title = params.title or "title"
params.item = params.item or 0
params.min = params.min or 20
params.max = params.max or 80
local widget = UI.createWidget('DualScrollItemPanel', parent)
widget.title:setOn(params.on)
widget.title.onClick = function()
params.on = not params.on
widget.title:setOn(params.on)
if callback then
callback(widget, params)
end
end
widget.item:setItemId(params.item)
widget.item.onItemChange = function()
params.item = widget.item:getItemId()
if callback then
callback(widget, params)
end
end
local update = function(dontSignal)
widget.title:setText("" .. params.min .. "% <= " .. params.title .. " <= " .. params.max .. "%")
if callback and not dontSignal then
callback(widget, params)
end
end
widget.scroll1:setValue(params.min)
widget.scroll2:setValue(params.max)
widget.scroll1.onValueChange = function(scroll, value)
params.min = value
update()
end
widget.scroll2.onValueChange = function(scroll, value)
params.max = value
update()
end
update(true)
end
UI.Label = function(text, parent)
local label = UI.createWidget('BotLabel', parent)
label:setText(text)
return label
end
UI.Separator = function(parent)
local separator = UI.createWidget('BotSeparator', parent)
return separator
end
UI.TextEdit = function(text, callback, parent)
local widget = UI.createWidget('BotTextEdit', parent)
widget.onTextChange = callback
widget:setText(text)
return widget
end
UI.TwoItemsAndSlotPanel = function(params, callback, parent)
--[[ params:
on - bool,
title - string,
item1 - number,
item2 - number,
slot - number,
]]
params.title = params.title or "title"
params.item1 = params.item1 or 0
params.item2 = params.item2 or 0
params.slot = params.slot or 1
local widget = UI.createWidget("TwoItemsAndSlotPanel", parent)
widget.title:setText(params.title)
widget.title:setOn(params.on)
widget.title.onClick = function()
params.on = not params.on
widget.title:setOn(params.on)
if callback then
callback(widget, params)
end
end
widget.slot:setCurrentIndex(params.slot)
widget.slot.onOptionChange = function()
params.slot = widget.slot.currentIndex
if callback then
callback(widget, params)
end
end
widget.item1:setItemId(params.item1)
widget.item1.onItemChange = function()
params.item1 = widget.item1:getItemId()
if callback then
callback(widget, params)
end
end
widget.item2:setItemId(params.item2)
widget.item2.onItemChange = function()
params.item2 = widget.item2:getItemId()
if callback then
callback(widget, params)
end
end
return widget
end

View File

@ -1,5 +1,8 @@
local context = G.botContext
-- DO NOT USE THIS CODE.
-- IT'S ONLY HERE FOR BACKWARD COMPATIBILITY, MAY BE REMOVED IN THE FUTURE
context.createWidget = function(name, parent)
if parent == nil then
parent = context.panel
@ -32,11 +35,11 @@ context.addTab = function(name)
return tab.tabPanel.content
end
context.tabs:setOn(true)
local smallTabs = #(context.tabs.tabs) >= 5
local newTab = context.tabs:addTab(name, g_ui.createWidget('BotPanel')).tabPanel.content
if #(context.tabs.tabs) > 5 then
context.tabs:setOn(true)
if smallTabs then
for k,tab in pairs(context.tabs.tabs) do
tab:setPadding(3)
tab:setFont('small-9px')
end
end

View File

@ -0,0 +1,3 @@
DONT USE PANELS
THEY ONLY HERE FOR BACKWARD COMPATIBILITY
MAY BE REMOVED IN THE FUTURE

View File

@ -581,7 +581,7 @@ Panel
newConfig = "name:" .. configName .. "\n"
end
for monster, config in pairs(monsters) do
newConfig = newConfig .. "\n" .. monster .. ":" .. json.encode(config) .. "\n"
newConfig = newConfig .. "\n" .. monster .. ":" .. json.encode(config, 2) .. "\n"
end
context.storage.attacking.configs[context.storage.attacking.activeConfig] = newConfig

View File

@ -1,6 +1,8 @@
BotConfig < Panel
id: botConfig
height: 45
margin-left: 2
margin-right: 2
ComboBox
id: list
@ -24,26 +26,29 @@ BotConfig < Panel
color: #FF0000
Button
margin-top: 1
margin-top: 2
id: add
anchors.top: prev.bottom
anchors.left: parent.left
text: Add
width: 56
height: 20
height: 18
text-offet: 0 2
Button
id: edit
anchors.top: prev.top
anchors.horizontalCenter: parent.horizontalCenter
text: Edit
width: 56
height: 20
height: 18
text-offet: 0 2
Button
id: remove
anchors.top: prev.top
anchors.right: parent.right
text: Remove
width: 56
height: 20
height: 18
text-offet: 0 2

View File

@ -1,6 +1,6 @@
DualScrollPanel < Panel
height: 55
margin-top: 2
height: 51
margin-top: 3
SmallBotSwitch
id: title
@ -36,6 +36,8 @@ DualScrollPanel < Panel
anchors.right: parent.right
anchors.top: scroll1.bottom
margin-top: 3
margin-left: 2
margin-right: 1
SingleScrollItemPanel < Panel
height: 45
@ -66,8 +68,8 @@ SingleScrollItemPanel < Panel
step: 1
DualScrollItemPanel < Panel
height: 37
margin-top: 2
height: 33
margin-top: 3
BotItem
id: item
@ -197,23 +199,26 @@ ItemAndSlotPanel < Panel
height: 20
TwoItemsAndSlotPanel < Panel
height: 37
height: 35
margin-top: 4
BotItem
id: item1
anchors.left: parent.left
anchors.top: parent.top
margin-top: 1
BotItem
id: item2
anchors.left: prev.right
anchors.top: prev.top
margin-left: 1
SmallBotSwitch
id: title
anchors.left: prev.right
anchors.right: parent.right
anchors.top: prev.top
anchors.top: parent.top
text-align: center
margin-left: 2
margin-top: 0

View File

@ -10,6 +10,7 @@ cooldownPanel = nil
lastPlayer = nil
cooldown = {}
cooldowns = {}
groupCooldown = {}
function init()
@ -43,6 +44,11 @@ function terminate()
disconnect(g_game, { onGameStart = online,
onSpellGroupCooldown = onSpellGroupCooldown,
onSpellCooldown = onSpellCooldown })
for key, val in pairs(cooldowns) do
removeCooldown(key)
end
cooldowns = {}
cooldownWindow:destroy()
cooldownButton:destroy()
@ -110,6 +116,7 @@ function removeCooldown(progressRect)
progressRect.icon:destroy()
progressRect.icon = nil
end
cooldowns[progressRect] = nil
progressRect = nil
end
@ -125,6 +132,7 @@ function turnOffCooldown(progressRect)
particle:fill('parent')
scheduleEvent(function() particle:destroy() end, 1000) -- hack until onEffectEnd]]
cooldowns[progressRect] = nil
progressRect = nil
end
@ -145,6 +153,7 @@ function updateCooldown(progressRect, duration)
removeEvent(progressRect.event)
progressRect.event = scheduleEvent(function()
if not progressRect.callback then return end
progressRect.callback[ProgressCallback.update]()
end, 100)
else
@ -187,6 +196,7 @@ function onSpellCooldown(iconId, duration)
end
initCooldown(progressRect, updateFunc, finishFunc)
cooldown[iconId] = true
cooldowns[progressRect] = true
end
function onSpellGroupCooldown(groupId, duration)

View File

@ -38,7 +38,13 @@ function show(itemWidget)
itemWidget:setItem(Item.create(window.item:getItemId(), window.item:getItemCount()))
destroy()
end
local clearFunc = function()
window.item:setItemId(0)
window.item:setItemCount(0)
doneFunc()
end
window.clearButton.onClick = clearFunc
window.okButton.onClick = doneFunc
window.cancelButton.onClick = destroy
window.onEnter = doneFunc

View File

@ -49,6 +49,13 @@ ItemSelectorWindow < MainWindow
text-align: center
!text: tr("Count / SubType")
Button
id: clearButton
!text: tr('Clear')
anchors.bottom: parent.bottom
anchors.left: parent.left
width: 60
Button
id: okButton
!text: tr('Ok')

View File

@ -1034,6 +1034,9 @@ function Market.decrementAmount()
end
function Market.updateCurrentItems()
if not categoryList or not categoryList:getCurrentOption() then
return
end
local id = getMarketCategoryId(categoryList:getCurrentOption().text)
if id == MarketCategory.MetaWeapons then
id = getMarketCategoryId(subCategoryList:getCurrentOption().text)

View File

@ -28,6 +28,7 @@ function show(text, options, callback) -- callback = function(newText)
multiline = true / false
width = number
validation = text (regex)
range = {number, number}
examples = {{name, text}, {name, text}}
]]--
if type(text) == 'userdata' then
@ -63,10 +64,13 @@ function show(text, options, callback) -- callback = function(newText)
end
-- functions
local validate = function(text)
if type(options.validation) ~= 'string' or options.validation:len() == 0 then
return true
if type(options.range) == 'table' then
local value = tonumber(text)
return value >= options.range[1] and value <= options.range[2]
elseif type(options.validation) == 'string' and options.validation:len() > 0 then
return #regexMatch(text, options.validation) == 1
end
return #regexMatch(text, options.validation) == 1
return true
end
local destroy = function()
window:destroy()
@ -109,7 +113,7 @@ function show(text, options, callback) -- callback = function(newText)
window.text:setCursorPos(-1)
end
end
if type(options.validation) == 'string' and options.validation:len() > 0 then
if type(options.range) == 'table' or (type(options.validation) == 'string' and options.validation:len() > 0) then
window.buttons.ok:disable()
window.text.onTextChange = function(widget, text)
if validate(text) then

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.