From cb1460c7bfede2fd2c4c40814b0608b9cb7c209c Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Mon, 6 Nov 2023 19:09:24 -0300 Subject: [PATCH 01/19] init --- data/images/topbuttons/analyzers.png | Bin 0 -> 1023 bytes data/images/topbuttons/bot.png | Bin 0 -> 370 bytes data/images/topbuttons/buttons.png | Bin 0 -> 331 bytes data/images/ui/graph_background.png | Bin 0 -> 22102 bytes data/images/ui/rarity_blue.png | Bin 0 -> 15568 bytes data/images/ui/rarity_frames.png | Bin 0 -> 511 bytes data/images/ui/rarity_gold.png | Bin 0 -> 15583 bytes data/images/ui/rarity_green.png | Bin 0 -> 15573 bytes data/images/ui/rarity_purple.png | Bin 0 -> 15571 bytes data/images/ui/rarity_white.png | Bin 0 -> 15353 bytes data/sounds/Creature_Detected.ogg | Bin 0 -> 8348 bytes data/sounds/Low_Health.ogg | Bin 0 -> 6433 bytes data/sounds/Low_Mana.ogg | Bin 0 -> 6713 bytes data/sounds/Player_Attack.ogg | Bin 0 -> 7524 bytes data/sounds/Player_Detected.ogg | Bin 0 -> 8461 bytes data/sounds/Private_Message.ogg | Bin 0 -> 8047 bytes data/sounds/alarm.ogg | Bin 0 -> 118635 bytes data/sounds/magnum.ogg | Bin 0 -> 52698 bytes data/styles/20-smallscrollbar.otui | 60 + data/styles/40-gamebuttons.otui | 2 + modules/client/client.otmod | 1 + modules/client_profiles/profiles.lua | 160 ++ modules/client_profiles/profiles.otmod | 11 + modules/client_textedit/textedit.lua | 166 ++ modules/client_textedit/textedit.otmod | 9 + modules/client_textedit/textedit.otui | 75 + modules/corelib/keyboard.lua | 2 +- modules/corelib/table.lua | 100 +- modules/corelib/ui/uicombobox.lua | 4 + modules/corelib/ui/uiminiwindow.lua | 758 ++++--- modules/corelib/ui/uiminiwindowcontainer.lua | 344 ++-- modules/corelib/ui/uimovabletabbar.lua | 8 +- modules/corelib/ui/uitabbar.lua | 6 + modules/game_bot/bot.lua | 799 ++++++++ modules/game_bot/bot.otmod | 8 + modules/game_bot/bot.otui | 130 ++ modules/game_bot/configs.png | Bin 0 -> 20453 bytes .../default_configs/cavebot_1.3/cavebot.lua | 39 + .../cavebot_1.3/cavebot/actions.lua | 264 +++ .../cavebot_1.3/cavebot/cavebot.lua | 224 ++ .../cavebot_1.3/cavebot/cavebot.otui | 58 + .../cavebot_1.3/cavebot/config.lua | 94 + .../cavebot_1.3/cavebot/config.otui | 57 + .../cavebot_1.3/cavebot/depositer.lua | 27 + .../cavebot_1.3/cavebot/editor.lua | 174 ++ .../cavebot_1.3/cavebot/editor.otui | 44 + .../cavebot_1.3/cavebot/example_functions.lua | 90 + .../cavebot/extension_template.lua | 58 + .../cavebot_1.3/cavebot/recorder.lua | 65 + .../cavebot_1.3/cavebot/supply.lua | 30 + .../cavebot_1.3/cavebot/supply.otui | 72 + .../cavebot_1.3/cavebot/walking.lua | 93 + .../cavebot_configs/config_name.cfg | 23 + .../cavebot_configs/fast_walking.cfg | 13 + .../cavebot_1.3/cavebot_configs/test_src.cfg | 104 + .../default_configs/cavebot_1.3/hp.lua | 192 ++ .../default_configs/cavebot_1.3/main.lua | 22 + .../cavebot_1.3/mwall_timer.lua | 41 + .../default_configs/cavebot_1.3/storage.json | 128 ++ .../cavebot_1.3/targetbot/creature.lua | 99 + .../cavebot_1.3/targetbot/creature_attack.lua | 122 ++ .../cavebot_1.3/targetbot/creature_editor.lua | 113 + .../targetbot/creature_editor.otui | 164 ++ .../targetbot/creature_priority.lua | 40 + .../cavebot_1.3/targetbot/looting.lua | 299 +++ .../cavebot_1.3/targetbot/looting.otui | 83 + .../cavebot_1.3/targetbot/target.lua | 285 +++ .../cavebot_1.3/targetbot/target.otui | 115 ++ .../cavebot_1.3/targetbot/walking.lua | 28 + .../targetbot_configs/config_name.json | 53 + .../default_configs/cavebot_1.3/tools.lua | 147 ++ .../default_configs/vBot_4.7/_Loader.lua | 65 + .../vBot_4.7/cavebot/actions.lua | 505 +++++ .../default_configs/vBot_4.7/cavebot/bank.lua | 92 + .../vBot_4.7/cavebot/buy_supplies.lua | 76 + .../vBot_4.7/cavebot/cavebot.lua | 447 ++++ .../vBot_4.7/cavebot/cavebot.otui | 58 + .../vBot_4.7/cavebot/clear_tile.lua | 128 ++ .../vBot_4.7/cavebot/config.lua | 111 + .../vBot_4.7/cavebot/config.otui | 57 + .../vBot_4.7/cavebot/d_withdraw.lua | 105 + .../vBot_4.7/cavebot/depositor.lua | 138 ++ .../vBot_4.7/cavebot/doors.lua | 58 + .../vBot_4.7/cavebot/editor.lua | 186 ++ .../vBot_4.7/cavebot/editor.otui | 44 + .../vBot_4.7/cavebot/example_functions.lua | 114 + .../vBot_4.7/cavebot/extension_template.lua | 58 + .../vBot_4.7/cavebot/imbuing.lua | 119 ++ .../vBot_4.7/cavebot/inbox_withdraw.lua | 91 + .../default_configs/vBot_4.7/cavebot/lure.lua | 29 + .../vBot_4.7/cavebot/minimap.lua | 30 + .../vBot_4.7/cavebot/pos_check.lua | 47 + .../vBot_4.7/cavebot/recorder.lua | 69 + .../vBot_4.7/cavebot/sell_all.lua | 66 + .../vBot_4.7/cavebot/stand_lure.lua | 186 ++ .../vBot_4.7/cavebot/supply_check.lua | 137 ++ .../vBot_4.7/cavebot/tasker.lua | 178 ++ .../vBot_4.7/cavebot/travel.lua | 40 + .../vBot_4.7/cavebot/walking.lua | 93 + .../vBot_4.7/cavebot/withdraw.lua | 56 + .../vBot_4.7/targetbot/creature.lua | 99 + .../vBot_4.7/targetbot/creature_attack.lua | 245 +++ .../vBot_4.7/targetbot/creature_editor.lua | 106 + .../vBot_4.7/targetbot/creature_editor.otui | 164 ++ .../vBot_4.7/targetbot/creature_priority.lua | 61 + .../vBot_4.7/targetbot/looting.lua | 324 +++ .../vBot_4.7/targetbot/looting.otui | 69 + .../vBot_4.7/targetbot/target.lua | 328 +++ .../vBot_4.7/targetbot/target.otui | 115 ++ .../vBot_4.7/targetbot/walking.lua | 28 + .../vBot_4.7/vBot/AttackBot.lua | 1248 +++++++++++ .../vBot_4.7/vBot/AttackBot.otui | 624 ++++++ .../vBot_4.7/vBot/BotServer.lua | 203 ++ .../vBot_4.7/vBot/BotServer.otui | 188 ++ .../vBot_4.7/vBot/Conditions.lua | 262 +++ .../vBot_4.7/vBot/Conditions.otui | 463 +++++ .../vBot_4.7/vBot/Containers.lua | 674 ++++++ .../default_configs/vBot_4.7/vBot/Dropper.lua | 146 ++ .../vBot_4.7/vBot/Equipper.lua | 770 +++++++ .../default_configs/vBot_4.7/vBot/HealBot.lua | 712 +++++++ .../vBot_4.7/vBot/HealBot.otui | 492 +++++ .../default_configs/vBot_4.7/vBot/Sio.lua | 252 +++ .../default_configs/vBot_4.7/vBot/alarms.lua | 272 +++ .../default_configs/vBot_4.7/vBot/alarms.otui | 181 ++ .../vBot_4.7/vBot/analyzer.lua | 1732 ++++++++++++++++ .../vBot_4.7/vBot/analyzer.otui | 443 ++++ .../default_configs/vBot_4.7/vBot/antiRs.lua | 33 + .../vBot_4.7/vBot/cast_food.lua | 22 + .../default_configs/vBot_4.7/vBot/cavebot.lua | 53 + .../vBot_4.7/vBot/cavebot_control_panel.lua | 63 + .../default_configs/vBot_4.7/vBot/combo.lua | 443 ++++ .../default_configs/vBot_4.7/vBot/combo.otui | 391 ++++ .../default_configs/vBot_4.7/vBot/configs.lua | 97 + .../vBot_4.7/vBot/depositer_config.lua | 123 ++ .../vBot_4.7/vBot/depositer_config.otui | 99 + .../vBot_4.7/vBot/depot_withdraw.lua | 76 + .../vBot_4.7/vBot/eat_food.lua | 28 + .../default_configs/vBot_4.7/vBot/equip.lua | 36 + .../vBot_4.7/vBot/equipper.otui | 539 +++++ .../default_configs/vBot_4.7/vBot/exeta.lua | 27 + .../default_configs/vBot_4.7/vBot/extras.lua | 663 ++++++ .../default_configs/vBot_4.7/vBot/extras.otui | 158 ++ .../vBot_4.7/vBot/hold_target.lua | 30 + .../vBot_4.7/vBot/ingame_editor.lua | 23 + .../default_configs/vBot_4.7/vBot/items.lua | 1404 +++++++++++++ .../default_configs/vBot_4.7/vBot/main.lua | 40 + .../vBot_4.7/vBot/new_cavebot_lib.lua | 518 +++++ .../vBot_4.7/vBot/new_healer.lua | 455 ++++ .../vBot_4.7/vBot/new_healer.otui | 413 ++++ .../vBot_4.7/vBot/npc_talk.lua | 5 + .../vBot_4.7/vBot/playerlist.lua | 351 ++++ .../vBot_4.7/vBot/playerlist.otui | 151 ++ .../default_configs/vBot_4.7/vBot/pushmax.lua | 287 +++ .../vBot_4.7/vBot/pushmax.otui | 85 + .../vBot_4.7/vBot/quiver_label.lua | 58 + .../vBot_4.7/vBot/quiver_manager.lua | 91 + .../vBot_4.7/vBot/siolist.otui | 192 ++ .../vBot_4.7/vBot/spy_level.lua | 24 + .../vBot_4.7/vBot/supplies.lua | 473 +++++ .../vBot_4.7/vBot/supplies.otui | 244 +++ .../default_configs/vBot_4.7/vBot/tools.lua | 46 + .../default_configs/vBot_4.7/vBot/version.txt | 1 + .../default_configs/vBot_4.7/vBot/vlib.lua | 1173 +++++++++++ .../vBot_4.7/vBot/xeno_menu.lua | 30 + .../default_configs/vBot_4.8/_Loader.lua | 64 + .../vBot_4.8/cavebot/actions.lua | 509 +++++ .../default_configs/vBot_4.8/cavebot/bank.lua | 92 + .../vBot_4.8/cavebot/buy_supplies.lua | 76 + .../vBot_4.8/cavebot/cavebot.lua | 447 ++++ .../vBot_4.8/cavebot/cavebot.otui | 58 + .../vBot_4.8/cavebot/clear_tile.lua | 128 ++ .../vBot_4.8/cavebot/config.lua | 111 + .../vBot_4.8/cavebot/config.otui | 57 + .../vBot_4.8/cavebot/d_withdraw.lua | 105 + .../vBot_4.8/cavebot/depositor.lua | 138 ++ .../vBot_4.8/cavebot/doors.lua | 58 + .../vBot_4.8/cavebot/editor.lua | 186 ++ .../vBot_4.8/cavebot/editor.otui | 44 + .../vBot_4.8/cavebot/example_functions.lua | 114 + .../vBot_4.8/cavebot/extension_template.lua | 58 + .../vBot_4.8/cavebot/imbuing.lua | 119 ++ .../vBot_4.8/cavebot/inbox_withdraw.lua | 91 + .../default_configs/vBot_4.8/cavebot/lure.lua | 29 + .../vBot_4.8/cavebot/minimap.lua | 26 + .../vBot_4.8/cavebot/pos_check.lua | 47 + .../vBot_4.8/cavebot/recorder.lua | 69 + .../vBot_4.8/cavebot/sell_all.lua | 76 + .../vBot_4.8/cavebot/stand_lure.lua | 186 ++ .../vBot_4.8/cavebot/supply_check.lua | 137 ++ .../vBot_4.8/cavebot/tasker.lua | 178 ++ .../vBot_4.8/cavebot/travel.lua | 40 + .../vBot_4.8/cavebot/walking.lua | 93 + .../vBot_4.8/cavebot/withdraw.lua | 56 + .../vBot_4.8/targetbot/creature.lua | 99 + .../vBot_4.8/targetbot/creature_attack.lua | 245 +++ .../vBot_4.8/targetbot/creature_editor.lua | 106 + .../vBot_4.8/targetbot/creature_editor.otui | 164 ++ .../vBot_4.8/targetbot/creature_priority.lua | 61 + .../vBot_4.8/targetbot/looting.lua | 324 +++ .../vBot_4.8/targetbot/looting.otui | 69 + .../vBot_4.8/targetbot/target.lua | 328 +++ .../vBot_4.8/targetbot/target.otui | 115 ++ .../vBot_4.8/targetbot/walking.lua | 28 + .../vBot_4.8/vBot/AttackBot.lua | 1254 +++++++++++ .../vBot_4.8/vBot/AttackBot.otui | 624 ++++++ .../vBot_4.8/vBot/BotServer.lua | 244 +++ .../vBot_4.8/vBot/BotServer.otui | 202 ++ .../vBot_4.8/vBot/Conditions.lua | 262 +++ .../vBot_4.8/vBot/Conditions.otui | 463 +++++ .../vBot_4.8/vBot/Containers.lua | 674 ++++++ .../default_configs/vBot_4.8/vBot/Dropper.lua | 146 ++ .../vBot_4.8/vBot/Equipper.lua | 775 +++++++ .../default_configs/vBot_4.8/vBot/HealBot.lua | 712 +++++++ .../vBot_4.8/vBot/HealBot.otui | 492 +++++ .../default_configs/vBot_4.8/vBot/Sio.lua | 252 +++ .../default_configs/vBot_4.8/vBot/alarms.lua | 223 ++ .../default_configs/vBot_4.8/vBot/alarms.otui | 135 ++ .../vBot_4.8/vBot/analyzer.lua | 1826 +++++++++++++++++ .../vBot_4.8/vBot/analyzer.otui | 514 +++++ .../default_configs/vBot_4.8/vBot/antiRs.lua | 33 + .../vBot_4.8/vBot/cast_food.lua | 22 + .../default_configs/vBot_4.8/vBot/cavebot.lua | 53 + .../vBot_4.8/vBot/cavebot_control_panel.lua | 63 + .../default_configs/vBot_4.8/vBot/combo.lua | 443 ++++ .../default_configs/vBot_4.8/vBot/combo.otui | 391 ++++ .../default_configs/vBot_4.8/vBot/configs.lua | 97 + .../vBot_4.8/vBot/depositer_config.lua | 136 ++ .../vBot_4.8/vBot/depositer_config.otui | 98 + .../vBot_4.8/vBot/depot_withdraw.lua | 76 + .../vBot_4.8/vBot/eat_food.lua | 48 + .../default_configs/vBot_4.8/vBot/equip.lua | 36 + .../vBot_4.8/vBot/equipper.otui | 539 +++++ .../default_configs/vBot_4.8/vBot/exeta.lua | 27 + .../default_configs/vBot_4.8/vBot/extras.lua | 670 ++++++ .../default_configs/vBot_4.8/vBot/extras.otui | 158 ++ .../vBot_4.8/vBot/hold_target.lua | 30 + .../vBot_4.8/vBot/ingame_editor.lua | 23 + .../default_configs/vBot_4.8/vBot/items.lua | 1404 +++++++++++++ .../default_configs/vBot_4.8/vBot/main.lua | 40 + .../vBot_4.8/vBot/new_cavebot_lib.lua | 518 +++++ .../vBot_4.8/vBot/new_healer.lua | 456 ++++ .../vBot_4.8/vBot/new_healer.otui | 413 ++++ .../vBot_4.8/vBot/npc_talk.lua | 5 + .../vBot_4.8/vBot/playerlist.lua | 351 ++++ .../vBot_4.8/vBot/playerlist.otui | 151 ++ .../default_configs/vBot_4.8/vBot/pushmax.lua | 287 +++ .../vBot_4.8/vBot/pushmax.otui | 85 + .../vBot_4.8/vBot/quiver_label.lua | 58 + .../vBot_4.8/vBot/quiver_manager.lua | 91 + .../vBot_4.8/vBot/siolist.otui | 192 ++ .../vBot_4.8/vBot/spy_level.lua | 24 + .../vBot_4.8/vBot/supplies.lua | 473 +++++ .../vBot_4.8/vBot/supplies.otui | 244 +++ .../default_configs/vBot_4.8/vBot/tools.lua | 46 + .../default_configs/vBot_4.8/vBot/version.txt | 1 + .../default_configs/vBot_4.8/vBot/vlib.lua | 1175 +++++++++++ .../vBot_4.8/vBot/xeno_menu.lua | 30 + modules/game_bot/edit.otui | 254 +++ modules/game_bot/executor.lua | 423 ++++ modules/game_bot/functions/callbacks.lua | 271 +++ modules/game_bot/functions/config.lua | 269 +++ modules/game_bot/functions/const.lua | 25 + modules/game_bot/functions/icon.lua | 176 ++ modules/game_bot/functions/main.lua | 211 ++ modules/game_bot/functions/map.lua | 267 +++ modules/game_bot/functions/npc.lua | 130 ++ modules/game_bot/functions/player.lua | 176 ++ .../game_bot/functions/player_conditions.lua | 32 + .../game_bot/functions/player_inventory.lua | 45 + modules/game_bot/functions/script_loader.lua | 59 + modules/game_bot/functions/server.lua | 91 + modules/game_bot/functions/sound.lua | 31 + modules/game_bot/functions/test.lua | 3 + modules/game_bot/functions/tools.lua | 19 + modules/game_bot/functions/ui.lua | 33 + modules/game_bot/functions/ui_elements.lua | 401 ++++ modules/game_bot/functions/ui_legacy.lua | 135 ++ modules/game_bot/functions/ui_windows.lua | 49 + modules/game_bot/panels/DONT_USE_PANELS.txt | 3 + modules/game_bot/panels/attacking.lua | 1193 +++++++++++ modules/game_bot/panels/basic.lua | 57 + modules/game_bot/panels/healing.lua | 346 ++++ modules/game_bot/panels/looting.lua | 431 ++++ modules/game_bot/panels/tools.lua | 36 + modules/game_bot/panels/war.lua | 127 ++ modules/game_bot/panels/waypoints.lua | 775 +++++++ modules/game_bot/scripts.png | Bin 0 -> 14295 bytes modules/game_bot/ui/basic.otui | 88 + modules/game_bot/ui/config.otui | 58 + modules/game_bot/ui/container.otui | 19 + modules/game_bot/ui/icons.otui | 60 + modules/game_bot/ui/panels.otui | 310 +++ modules/game_buttons/buttons.lua | 49 + modules/game_buttons/buttons.otmod | 8 + modules/game_buttons/buttons.otui | 6 + modules/game_console/console.lua | 27 +- modules/game_containers/containers.lua | 6 +- modules/game_interface/gameinterface.lua | 13 + modules/game_interface/interface.otmod | 3 + modules/game_interface/widgets/uigamemap.lua | 10 +- modules/game_itemselector/itemselector.lua | 74 + modules/game_itemselector/itemselector.otmod | 10 + modules/game_itemselector/itemselector.otui | 72 + modules/game_minimap/minimap.lua | 6 +- modules/gamelib/ui/uiitem.lua | 137 ++ src/CMakeLists.txt | 1 + src/client/animatedtext.h | 3 +- src/client/creature.cpp | 35 +- src/client/creature.h | 15 + src/client/declarations.h | 2 + src/client/effect.cpp | 5 +- src/client/game.cpp | 8 + src/client/game.h | 11 + src/client/luafunctions.cpp | 49 +- src/client/map.cpp | 282 ++- src/client/map.h | 8 + src/client/statictext.h | 4 + src/client/thing.h | 6 +- src/client/tile.cpp | 113 +- src/client/tile.h | 18 + src/client/uicreature.cpp | 2 +- src/client/uigraph.cpp | 85 + src/client/uigraph.h | 63 + src/client/uiitem.h | 1 + src/client/uimap.cpp | 11 +- src/client/uimap.h | 4 +- src/framework/core/clock.cpp | 5 +- src/framework/core/clock.h | 2 + src/framework/core/config.cpp | 8 + src/framework/core/config.h | 1 + src/framework/core/eventdispatcher.cpp | 1 + src/framework/core/eventdispatcher.h | 1 + src/framework/core/graphicalapplication.cpp | 4 +- src/framework/core/logger.cpp | 11 +- src/framework/core/modulemanager.cpp | 6 +- src/framework/core/modulemanager.h | 3 + src/framework/core/resourcemanager.cpp | 122 ++ src/framework/core/resourcemanager.h | 2 + src/framework/graphics/cachedtext.h | 1 + src/framework/luaengine/luavaluecasts.h | 31 + src/framework/luafunctions.cpp | 30 + src/framework/platform/platform.h | 3 +- src/framework/platform/win32platform.cpp | 28 +- src/framework/ui/uimanager.cpp | 33 + src/framework/ui/uimanager.h | 1 + src/framework/ui/uiwidget.cpp | 15 + src/framework/ui/uiwidget.h | 1 + vc17/otclient.vcxproj | 7 +- vc17/otclient.vcxproj.filters | 6 + vc17/settings.props | 55 +- 350 files changed, 58901 insertions(+), 574 deletions(-) create mode 100644 data/images/topbuttons/analyzers.png create mode 100644 data/images/topbuttons/bot.png create mode 100644 data/images/topbuttons/buttons.png create mode 100644 data/images/ui/graph_background.png create mode 100644 data/images/ui/rarity_blue.png create mode 100644 data/images/ui/rarity_frames.png create mode 100644 data/images/ui/rarity_gold.png create mode 100644 data/images/ui/rarity_green.png create mode 100644 data/images/ui/rarity_purple.png create mode 100644 data/images/ui/rarity_white.png create mode 100644 data/sounds/Creature_Detected.ogg create mode 100644 data/sounds/Low_Health.ogg create mode 100644 data/sounds/Low_Mana.ogg create mode 100644 data/sounds/Player_Attack.ogg create mode 100644 data/sounds/Player_Detected.ogg create mode 100644 data/sounds/Private_Message.ogg create mode 100644 data/sounds/alarm.ogg create mode 100644 data/sounds/magnum.ogg create mode 100644 data/styles/20-smallscrollbar.otui create mode 100644 data/styles/40-gamebuttons.otui create mode 100644 modules/client_profiles/profiles.lua create mode 100644 modules/client_profiles/profiles.otmod create mode 100644 modules/client_textedit/textedit.lua create mode 100644 modules/client_textedit/textedit.otmod create mode 100644 modules/client_textedit/textedit.otui create mode 100644 modules/game_bot/bot.lua create mode 100644 modules/game_bot/bot.otmod create mode 100644 modules/game_bot/bot.otui create mode 100644 modules/game_bot/configs.png create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/actions.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.otui create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/config.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/config.otui create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/depositer.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.otui create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/example_functions.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/extension_template.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/recorder.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.otui create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot/walking.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/config_name.cfg create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/fast_walking.cfg create mode 100644 modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/test_src.cfg create mode 100644 modules/game_bot/default_configs/cavebot_1.3/hp.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/main.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/mwall_timer.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/storage.json create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/creature.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_attack.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.otui create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_priority.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.otui create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/target.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/target.otui create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot/walking.lua create mode 100644 modules/game_bot/default_configs/cavebot_1.3/targetbot_configs/config_name.json create mode 100644 modules/game_bot/default_configs/cavebot_1.3/tools.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/_Loader.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/actions.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/bank.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/buy_supplies.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/clear_tile.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/config.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/config.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/d_withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/depositor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/doors.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/editor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/editor.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/example_functions.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/extension_template.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/imbuing.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/inbox_withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/lure.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/minimap.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/pos_check.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/recorder.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/sell_all.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/stand_lure.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/supply_check.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/tasker.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/travel.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/walking.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/cavebot/withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/creature.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/creature_attack.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/creature_priority.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/looting.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/looting.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/target.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/target.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/targetbot/walking.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/Containers.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/Dropper.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/Equipper.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/Sio.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/alarms.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/alarms.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/antiRs.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/cast_food.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/cavebot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/cavebot_control_panel.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/combo.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/combo.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/configs.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/depot_withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/eat_food.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/equip.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/equipper.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/exeta.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/extras.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/extras.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/hold_target.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/ingame_editor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/items.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/main.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/new_cavebot_lib.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/npc_talk.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/quiver_label.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/quiver_manager.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/siolist.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/spy_level.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/supplies.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/supplies.otui create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/tools.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/version.txt create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/vlib.lua create mode 100644 modules/game_bot/default_configs/vBot_4.7/vBot/xeno_menu.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/_Loader.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/actions.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/bank.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/buy_supplies.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/clear_tile.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/config.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/config.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/d_withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/depositor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/doors.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/editor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/editor.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/example_functions.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/extension_template.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/imbuing.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/inbox_withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/lure.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/minimap.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/pos_check.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/recorder.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/sell_all.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/stand_lure.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/supply_check.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/tasker.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/travel.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/walking.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/cavebot/withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/creature.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/creature_attack.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/creature_priority.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/looting.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/looting.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/target.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/target.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/targetbot/walking.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/Containers.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/Dropper.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/Equipper.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/Sio.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/alarms.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/alarms.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/antiRs.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/cast_food.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/cavebot.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/cavebot_control_panel.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/combo.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/combo.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/configs.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/depot_withdraw.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/eat_food.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/equip.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/equipper.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/exeta.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/extras.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/extras.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/hold_target.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/ingame_editor.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/items.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/main.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/new_cavebot_lib.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/npc_talk.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/quiver_label.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/quiver_manager.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/siolist.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/spy_level.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/supplies.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/supplies.otui create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/tools.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/version.txt create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/vlib.lua create mode 100644 modules/game_bot/default_configs/vBot_4.8/vBot/xeno_menu.lua create mode 100644 modules/game_bot/edit.otui create mode 100644 modules/game_bot/executor.lua create mode 100644 modules/game_bot/functions/callbacks.lua create mode 100644 modules/game_bot/functions/config.lua create mode 100644 modules/game_bot/functions/const.lua create mode 100644 modules/game_bot/functions/icon.lua create mode 100644 modules/game_bot/functions/main.lua create mode 100644 modules/game_bot/functions/map.lua create mode 100644 modules/game_bot/functions/npc.lua create mode 100644 modules/game_bot/functions/player.lua create mode 100644 modules/game_bot/functions/player_conditions.lua create mode 100644 modules/game_bot/functions/player_inventory.lua create mode 100644 modules/game_bot/functions/script_loader.lua create mode 100644 modules/game_bot/functions/server.lua create mode 100644 modules/game_bot/functions/sound.lua create mode 100644 modules/game_bot/functions/test.lua create mode 100644 modules/game_bot/functions/tools.lua create mode 100644 modules/game_bot/functions/ui.lua create mode 100644 modules/game_bot/functions/ui_elements.lua create mode 100644 modules/game_bot/functions/ui_legacy.lua create mode 100644 modules/game_bot/functions/ui_windows.lua create mode 100644 modules/game_bot/panels/DONT_USE_PANELS.txt create mode 100644 modules/game_bot/panels/attacking.lua create mode 100644 modules/game_bot/panels/basic.lua create mode 100644 modules/game_bot/panels/healing.lua create mode 100644 modules/game_bot/panels/looting.lua create mode 100644 modules/game_bot/panels/tools.lua create mode 100644 modules/game_bot/panels/war.lua create mode 100644 modules/game_bot/panels/waypoints.lua create mode 100644 modules/game_bot/scripts.png create mode 100644 modules/game_bot/ui/basic.otui create mode 100644 modules/game_bot/ui/config.otui create mode 100644 modules/game_bot/ui/container.otui create mode 100644 modules/game_bot/ui/icons.otui create mode 100644 modules/game_bot/ui/panels.otui create mode 100644 modules/game_buttons/buttons.lua create mode 100644 modules/game_buttons/buttons.otmod create mode 100644 modules/game_buttons/buttons.otui create mode 100644 modules/game_itemselector/itemselector.lua create mode 100644 modules/game_itemselector/itemselector.otmod create mode 100644 modules/game_itemselector/itemselector.otui create mode 100644 modules/gamelib/ui/uiitem.lua create mode 100644 src/client/uigraph.cpp create mode 100644 src/client/uigraph.h diff --git a/data/images/topbuttons/analyzers.png b/data/images/topbuttons/analyzers.png new file mode 100644 index 0000000000000000000000000000000000000000..de8294d72445d29e464a92174189d22a693c4903 GIT binary patch literal 1023 zcmV004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}00009a7bBm000ie000ie0hKEb z8vp13=;VR zBqTn70w_QjAzGkt0-_^9#*f|g?q<#yr^uDB@7~v$GiPS5wY~igPnu8z+bd4jyxW_Keq1|q)2cG9)YZj42Q4^Kc*RJ!(;;m&t}qDMg`J zKvvweWWQ7@VPawe7Z(@0&vQ+YPF?0-UxQS&;hdf9xIS>VTCHJscULv6R;z0CSZHP!9r=H?#>*Y$K%4;gM=V50@+5K>S~xm7!sX>9?(XhzdU^^^ihxNU3IPF8Nkq856$!Qi)76=B8Eb26 zsC=qus!UH$PE+FHu{8%L^ip8R8$#3IlefW|lY`(ZZKR=H~qk*%tGn}8FV`XJU zUCih6+6hi3l&7Z=?x-!9 zrVMrDx|H4#}csR<>mCx>nlQGrD2zY t113y|IF_eg4iCzca{~R}c*nMF`~_7;E~JVWc1ZvL002ovPDHLkV1g6e!aV>0 literal 0 HcmV?d00001 diff --git a/data/images/topbuttons/bot.png b/data/images/topbuttons/bot.png new file mode 100644 index 0000000000000000000000000000000000000000..8dc7d11af7c0a94d9100ada64f6fa5bbb8208f1e GIT binary patch literal 370 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4eXvSjv*HQYbX7UJY*oy9;}|ge3!vY!2AI3 z4TqTwGZ^MN%y5|LF!MkjgHMC-mOc5u8bwW&{4p?0yU`Rl|Lfd~GSe>jPD+uo zWn(bj8Z<3cv)QwM-GyCjDSh9=)lD6QD}FS)S2QY{GVk~)suSk5O#dRAalgbG$F*q< zqC5{dAIW-0F6R94t@}{u3dsXjGafSAcrInVX)kGew)V>(v59Ywc-@*?zrnq10pIzv z{gcgnqjz4*v+-cxV_EH%wq5)6*^R=plNyf}1#bPoZua1GM!dJ;8%CWH@#}LQMjZi$ O6N9I#pUXO@geCxVags>@ literal 0 HcmV?d00001 diff --git a/data/images/topbuttons/buttons.png b/data/images/topbuttons/buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..01cb2800456221063b545f2a9107630f6465cd0b GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgg)ZUI3a)}Z@AAk$8Jx;TbdoL)Nl zHCK~^K_uG5So-WIoe;@lPHBrTF|CRI4@2>n^VWW9|@vqcLOWZzYZC!JP*Fl1< zHtZtHjcv9(1^eyWB5&ket8Y~~ZNlVpNQpg)qiLt)hKLs3x!qT8B{Zt&oL==)gn`AO zLtsfBd&b&{%lsF!UN1c>{9|8uWW?pRM5g02%c^XYf>mdKI;Vst0N%)c3;+NC literal 0 HcmV?d00001 diff --git a/data/images/ui/graph_background.png b/data/images/ui/graph_background.png new file mode 100644 index 0000000000000000000000000000000000000000..71f0c12ddee4273705263a2023a9841fa1e7deae GIT binary patch literal 22102 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 E0A}w>F#rGn literal 0 HcmV?d00001 diff --git a/data/images/ui/rarity_blue.png b/data/images/ui/rarity_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..13220bd287a963e950bf3d17650ae39f643d7909 GIT binary patch literal 15568 zcmeI3e^3WX!M)x41>ENDEO01HI70yAp=n@m_l4VXcYEG0 zaFA%xbP|(HY)DHQ4V{Ko)0xIrl18nmV_M0N&a@>Y?WAC0&`Bo={%EQLHCh|{?%i>B zyue#KNvHqp&fMKT@AE$2XZQ2I&vX0FK3uzgT~20xCP5H6)m5%K_)DsvjCA-dcXv;! z247X@CW!7^2%_f!g1CGQ{!S1?J3|m>nhAn^iXif&Cz@Ve3+LV#s%n%8V$mz=N7q-h zsDL1BZ-@;Z#pAAFc`0Dv1j!2wkw6H}CJ6hQNQmRxfI@mfs~B|XKmPU0dQub|`i)jM z;|^5-pIFr$2KC+R8~E-v-X`eRI5X`L777G_!jX}HKPa;ihd!#y!gF<))|1gGN}EGp zrY0mk?pm@!3Ioz=uu?o@w2&n>gVAcVlp0r)CdO!{88dA(Q$`cZ*jS^Lj30VuCOor; zg%-BXwKkp(S`NKWQ9>+DcXoCfI?V zfGo9#c~IE_Z)L??cdv*o$;FwWqu;^BZt&knwD_ao;oFauAB+2iL z?Xucwgk(hp`Sp!rP>?$1TkSMDJGsiT^;DlSzLNUdZ$=F~lWz9y`WTO}}%P=V- zv6+a5TN1>UE>y#0W=%$xF{f&z%tSR{=?I*{U6)Gg+!P&wXIrFjfK!}efNKSGDA;PJ zlY=QU6B~-Hko-~@UKVhg?etv3DN)f)##RSqg$wea+U0};22m7Pla(pq0Vt)4xnd&) zfQ6^LJS@c)uh~*!wKA4cV2eU@yHmqwNxLMzT`iQjv;d`rVjE+F@)TF%<)~7|Vxw#= zW*Y@cEeu~=;x&8C#%bx4@LAG#hDF%qIDg6<_3HoI6m!VTN>wGwaBFs@Y+1N*qiw7n zY?(Pa?eL4yb`;{mGEj@np`Wg{ME^>(Cp9Kov@FM~jm*ibwEzS=J!3R^S&7Jam^ga@ zua9y}KP7zr9GyN8#1&LbDmrU^5m{5B0}tA$e#b*o-DqedJ~Y*hh9)C71VzPZif2QHQ=-&A z$ygh&htG4sS!}hMt#%q6Mk8v)2=IGcBJ8)a+C|JTQ#1y3%o*4?Uto$O9vEAylqauf z`Nc-za~*u7q!Z7Svnn&uyFSm8!J8T`JRiiX;X;7mO$`^G58~BuAwck^h6~RJ z@oKmbAb3;5h3A8KHCzY~ys6>B^Fh2CE(8eP)NtYXAYKg@0t9brxbS=suZ9Z&f;Tl> zcs__%!-W9Bn;I@WAH=KSLV(~+4Hupd;?-~=K=39OS7!1pO%Q~yW_H4NFdr}Y+g^f5 zyRX_+*5Fxpe&g?h?=smpRUh(8hpBMJjx=57VcTW`f9J+=@2`Q0ClQBB+XAFuq)g2g{O(U~_iRF|`O(>nviCto_&{D-W2HZ}h# zr|`F>?;Yql@v3L2KU=^0g@PqDcTPq62GSy)<-OOg?)WA>cpbq=p2~Rm$ICu^XPM!ik)JN>ux;6TT=4YuolP$UhC`3G)_6`&tQn6O zii-MQT=j(?+;9|Z_iX#3M|be-mrq~K%$seOKqcd&OrKABd0OZJxXOFktKX})= zaoy@EQxmxb)Rwip@b+I%*F|2j)yc?US@ zUY>h7{OC&;Jm>b@Zu-ftE$OR16u0hvFstX4g1yr>*>!;o&z7^PWX^Ff1($h8Dw?2FHcYVIri6SEJ*vaMp8c#c_JG1bk Vi(c}ju=*Zxb>({3FUp&D{2QlNIPw4h literal 0 HcmV?d00001 diff --git a/data/images/ui/rarity_frames.png b/data/images/ui/rarity_frames.png new file mode 100644 index 0000000000000000000000000000000000000000..cebc6d963be7ba84bfb63d14facaa3dd31002f83 GIT binary patch literal 511 zcmV001Be1^@s6m49>f0005bNklqlU}d)NI?39{wAcq{M@gF^xp=3_u=aL<}Rc^vY!vY#I7l@0Fn+P2b6#+ z(6FmzYcGNPO*Ij^9D}MsK*#=e#Z=m4avjF&?z2wiP ze;?q&9=kKUK7eJy8z_46xjB(P+kO||#{TE8tQU74L z{ndYp5CKtuLIDZ|D1-tO3Q!0IC={R&3Q#COArzocfI=uhp#X(YfIHaC4h!9?fuRPUO$Df9D4X@o@hQ#!$CnIXmU6xCs|l=0057b B=yd=9 literal 0 HcmV?d00001 diff --git a/data/images/ui/rarity_gold.png b/data/images/ui/rarity_gold.png new file mode 100644 index 0000000000000000000000000000000000000000..e38503eee7bc230ec1cd067c7d73e2826e95ddf2 GIT binary patch literal 15583 zcmeI3e{dA#8OJw)AtabM{zOYsk0XE#-0klDNOH^NY{-S=(1Q?yT$HhFcHbmR?rt}? zd&wnCX|N+5vDO%&5&0!)8X7^8f~k(E(2gKKw6xYz5HUjrhav((5eue6ckeEDmp92P z9qsg=cW3TypZ9s6@3Z@P-{-mgXP+*eU!0!ygEWF5(p@FaGWA!dea4SdzvZT;tD3=; zl(`9_X(~Z9|BN86-&B9k5k!z8h|iZ11oI+6OcYyxynn7bH&rUBkO?B=UG0;!Zf3?L zg0LMG$~}t5{RqQ}J|oMEmB1MCN$PBZus;-%SgsZ*x=K(j`1AFb_UzT`1U_HC#OkKp zQX!}jN}2*-QPcc#uBn!@@%o1xY4#AK3iyD+>OwxRUuHu2`mipeo@>LTUKgIC)aL68 zw1hg3yHr;w27u0Lv>G^yw&-$gM%qeS^5|JQ6GfXz%1qK`18rg`8$(-l(L?V@Q_t)H zzKSVx&W)y1t$clrqDTx$HZ(LC8_Y&AP)*V{n~kJQq{(DZXBgy0zruzLetAknWKhQm zWG)~`iXi%Rnl4)@29YBLSvD_nGug^DPYFR0&R}n;!juK@BGz&P~waIi^Ys_*lgu_}FSdO8#m) z86QlT8QV}yq39I@>SY0k*-nl$oDdb>WQ@x%E3BUbE~i5sFbV?Cn5XeHE6wzvbVB$r=>-8nZE~zPVUBk7|80s9WQL_G5oC31HYRLYb>oKH z*dkCjbac?+6~gUEVgoYJiY;G1SZ%TX6>CpgOt@$nmeU%UgVSmO@OE;@X#BEbkg`-e|GKvhf!-+ z-A8P=X4IZBY%HERGT)=spf?zvpKGGjgQT_ZXlSAv4y{0kCc5F!c;sTgpg2s?Y|wB* zl=deXY2%C3=Q-fWv0BYmI|&cN5v4*1cs))*?YFYlMa*iZa187iF|cB^zywD$FtSt$ zPhR2ji;Swzb?PG}8GEK2R++Kh^?jbi@96tfg|u)I2ngE5xX^qcFUAD{L7NyCnh)f~ zxF8^C6XQbjfxH+O1O#njTxdR!7vq9}piPVm%?I*gTo4eniE*L%KwgXs0)jR%E;Jv= zi*Z3f&?d%(<^y>#E(i$P#JJFWATP!R0YRG>7n%>`#ke3KXcOZ?^MSk=7X$=tVq9oG zkQd{EfS^r`3(W`eVq6dqw25({`9NNb3j%^RF)lP8$cu47K+q<}h2{f!F)j!Q+Qhif zd>}8z1pz^u7#Erko2qJln z%UMwFDZbElGVl>~+iEcTPg{@dlR}%Xc&=t0o^tJnora}r*SmLJUbWz`Kjn?=7586w z@RwP|H>TQl@BQT8Z6{w^ozk^9yV}_Q8>((Vsn7pI=UqG6m(KANfBn@(*&x1i$X;Ag zHvPzlCC?w6dmxZC{?l6OsXw<`=!d1dh-t*YokxD;7+Cw>wpoAuX8CR~fo>7+PoXcZ&Z>xWkvclTbucT?58=B%?Rvmbx+(Ne?jHqV*ab8I~?QOxI` z6)R(5EaWn0?W?oj!PDQ&*uEr~PRy>^*iyD;XYzrh?Bl;Z`uoG}T}36mV;}Io zR(St?m%AE0qU2dl>n5IkvvK><%vapKd&j;1_R9VpzOQ>0))TXvv-V{yOX(SNJn24S zO7_{C-f#LG@{C-gtPJmJN^@85alxuCnE?ojuH&TFkXx6ga@b=NQcGWV;6>smH$ znlwK7=dCBW^W|?Hc}o|NYu3+vd~-)Cc%jqsao%mucaB|hb=?)e?+&O32lm7vG?@@~Y literal 0 HcmV?d00001 diff --git a/data/images/ui/rarity_green.png b/data/images/ui/rarity_green.png new file mode 100644 index 0000000000000000000000000000000000000000..b452b0fd168fd4046746884edb55f8b8f70a0c27 GIT binary patch literal 15573 zcmeI3e^3d)_YG zaZOXSw8lgtFvS=rG)dK(%tSHuCzC`?EYWHsG4WSo(ssrs&`PjloryM;bocJKJ6_)U z1Yz#ut7@bg`{Oh#xU~!?IDj_b_R6yf!cq|MGVEF)sT`o5_vEXuzjaKl;<z&eA|1$AhjeNATp1?Ss?Zc^ZN7Sm zl2BD+FIN=`KA-pEL8otn)h&mUtC}z;n}gJjtK!9BLSGEiUj@Fwsl!2zG zIFay7NW(60d|eZ)p*PTa9ZebHHR5K%8gl7yjKtiPO8ne79gd~z1fQFcth}442c*|i zZy{rYaWkVEiY^pff=|9IU^Q6C@rL7~LYs^(@rV-RVL^$_Di3IRo}=|9DwhQyPm{ys z=rjNrS&f60OR>&jFy@*}lrayOLlEuu`0z2(Hi2zW3MC@V$8t0SO34J7)NGIs$yQ0w`Nn^mX$Yd zsEt*Eb)!c|94BeSwfEdb6!jv9?!Rx~ma z#*baVyQ3V|j|-nThu;Z2sDcVhg=S4GA_{eqpYeeu_41zhFJ&GU9325GTS%qnyZ9eq z4Me8#yRE?8mC$!s0k|FgUsfQtcUdRnsRx{u9ND%b19!J*;iVj1Pa|z$Z2ei3{~ube zU3nidp_-9<#+b2K=J5O)r3PIMq4~LbNJ9aiRG@UW^L@f;KTOG#|)|aX~=PCdP&419>qn2ngE5xX^qcFUAD{L7NyC znh)f~xF8^C6XQbjfxH+O1O#njTxdR!7vq9}piPVm%?I*gTo4eniE*L%KwgXs0)jR% zE;Jv=i*Z3f&?d%(<^y>#E(i$P#JJFWATP!R0YRHcTuHIFG=WEcHPbJ@gV~lbw38qv zH<#F!RMjm1s``xY3U%Kq>%1!`OUv%e_}Q9ox_XRfcfBZewD;Dhm%p?#f2%pkn7!#& zY12{;5#sREL+PuU&z4=IJ|E~g{nf47r31MKTMi%a53d^{hY~gho6F{C7v1_YbH~9) zwj@7yFs*gR_Gi;(J$YDtyXEA+iYISgtWUgCz42J^@`d*vytgtrIKMDUy4R+DboI?6 zpFV4CIRCo(>DD9NUuUepm~o?T^8))b=E>DXo2EE6pE4g)-+4LE|KN$hj|?*(SiSE^ z!fs*yX9pK~)9F=CVShp1pFY%l)KOlso_`yZ z_fz6O*EiIj^Hu))$?dbMuXH?|`iqtCpXuTDv~|p#-MMml;%~*rJ#Woyytv?K(c=AG z({2>J@@>N0SD$;zSv95b&4D)h_LdV@)9wND_szJRI5?$o@}l)!&8Z)LU2u)iRJ?X$ z@9?d&bJ}yW(~hoK{nYXF_V)(gQMJD-_SAm4Ys1XWlnrxwUmN&f>*bWx6&)oXAGc2V z+}r+zXep8|y?OD8{^d7U>>W6oHM_Xw<%J3A{tJ(QGn2H&zO(bHxEC^8A4^Oa`fJ-y z=T!WJYr3A)VLz3#V_8uq_0eynn#2bx5B0wI#`ACg`WCTrw?4fq;~ZVRb8)s~`;5c4 zPn}wEs80L(H=oWd>Av{zC$D6`ap~hf@A<^ZEPQBT%Yv=RQ!~iS8OLmv^BY@_i-Gsg z&u>XR!2hGG>%QgPS3%F*{llm8cAED*f8~$PGg~d4yIYU<{w{yF^OvVT=swxkc6e&J fXWR8gdYDK_>ATc_ZSS!1GI7bWQrq68Yc~7`3D8SU literal 0 HcmV?d00001 diff --git a/data/images/ui/rarity_purple.png b/data/images/ui/rarity_purple.png new file mode 100644 index 0000000000000000000000000000000000000000..05faf5b18ec5febdc7e007ffdbd917c3d979a22c GIT binary patch literal 15571 zcmeI3e^3 zTf^vBVq&VIFedFJGd8A^83XzU#*B$d3o;HG(`aT$)J*LpqY^c(!=z)UgznxQcjpVd zwUc!E&$}~sx6k{$&-dB=yzlee{+C?b|Wz-h}ph6T_NG3>)2qVax#xOO-n723Koyll(>13Wm)a&_4-XKcAO| zVYUlmd6in_dWPX7uZiWQdchR*`nA~@X3q=yS*}S?4fR5U=*z{wIdKX%hVcHfl4BN~=$0gGQgSG%PZy;}jH5 z7X7Ly`3$-)TQ4=Mxj3#TiXM^s^7^BRd`g6!Mv(}zeu6TQ#B@PC7me{Z%bw7s@f;y| z1h3#z6)ldMHqPHDsglwt&5$zco-whuCN5Vr_q5mR^-h~wQCGES2qH+Qja158{Q^-Y zC{nY`39DMPTUk2Y-K(NIwmA1G>7E&{T?mRZgz27eSZFJRSIDm2D^n;7tSZUnlH_rO zcUkEqLPKGpA-z)c@lrs^uoK|y*iy%Y1ShKs4s9!1jAWM4OqEkP3{5d+n~|g$l8h4x z&jd7F5-+-2K@Bs_m??&&<2B-Df*M-s@T|%{kV^dAI31p2+>-2NRfp(h8wA4dYp@fs z!MK^x4aF2n9!b_NOK{M3V!GkDsL&>3ihYX8`Z%H3>CgsDqR2C5E1As+LXI(u&7zEg zVBw7QoK}kNdfJk0wUU+`!4`t(a>a*Fk#X$i&DMQANkCs7vTbcz)ge;&Ij-! zTmTTbiEzRB0A7R(00K7=E;t{+i*Nxz;3mQa=L2{VE&vGJM7ZF5058G?0D+qb7n~2^ zMYsSUa1-Hz^8vgF7XSoqB3y7jfEVEcfWS?J3(g1dB3u9vxQTGV`2b#o3jhK)5iU3% zz>9DJK;S0A1?K~J5iS4-+(fwGd;l-P1ptAY2p60W;6=CqAaE1mg7X2q2p0eZZX#T8 zK7beD0)W6xgbU6G@FH9Q5V(nO!TA7QgbM%yH=($aV{d5+KJC@afc6gNp0qn}VOZkU zVrM~l)tcLt)jgNVl%E=Z!##a3{fn|OId@I(rQi4Ezw#t;lxzBW&oAWmKXhz;EwNyD zjtyTPY&`j^@#mlKO?rRX{?2D}v6g}1oAi~|_O9!nJ@w4rYZEq|+5biPzU#aGve9y2RLW>@w+h|1A4@N}zrr6o ze97L`{#Sg%+Y_%pb@=_yPhDK%=6z*J$GWS>pWCqQhUY%gMaK&oEcO{cYanawcw+gzKdPev*(!WJfHUbW7nMRVpq@71#j_Z=Fk6REdAcK+S6AKF3cG=?N~1b za&GPZ8^2dP|3Zz$RDApRl51B>e*4(YRd?Un{6zbEx5~eK=txb*k2)9S;d{Ot*dkAi zGvC&xyz6@7)udj}x~uOv%!UGE&7uA!n{SldIN#ng_LGsplshADf16*orKNEDKNm0f z)1gKE^B?ax=3aN}rL7n8K08}Gvj3dtTw>FpyCQhP(>;qlTcP$19<`<&KmEvsq0|wv zF5!dD-lywMJaG*7lbfrvPdwND;_Pcl VpLQp->Mtu}#j8r3Ju5eD`!^>SM*aW* literal 0 HcmV?d00001 diff --git a/data/images/ui/rarity_white.png b/data/images/ui/rarity_white.png new file mode 100644 index 0000000000000000000000000000000000000000..9984f9c7325a45fa7223441a5a5813d52ef2cb60 GIT binary patch literal 15353 zcmeI3e{2(F7{_lv)VVMeMpQs&?P7w#>)o~MwmZAxSO=S-bBrOwMcwuKwmaAM%H3`4 z#y}K8AQ;(DgHsXzK*R-qpdpAv98nA=#%cU9fBfMOj0lJbA&~`*zSnl`?OpeZ0pmY! zllJa;pZEDbcc1rtp58yVv9V!cQQ@pYilU0@>pV^HXfeJ8SHfSdr{|nuNOetKit4$Q zqSmaXsL{XR@i;|wvJ`cqjiQ97DC&A;%L9iOz`0jP>y~R2HT6B?oAcz{sk14HAC{Y2 z^cL?TK~lmt(XaS`Egp`-*%alP7mtck2hc4(5R@ZT*3$>xv07w*m35iZ%X*_Vpk1!( zQNfa)hGwa!L*o6`dG112Tz~>$po^AxI26%@c$GDwE5LJOn6X+CQ}m81YqgQk(&BBj z)F>*jIBiZ^V!1L)1#jb=Tv;V|yT#6O4u*9woP*}<0?P}W(~|mF-G%VXrTPOxlV?FH z9ki;f?YbTn7$z2r*J?9L@P~-jOloQCaH2%mz9Xc z&=q}3r(R{X8i~@M)P04c=|mAN#ST(r;$oEHY%G&4$Su0U;0u5nY3E zT-LZ~yP_*vyD~}2w0qLTa8114bndLz8xChpt?9L05J3uQ)<~_nI|`U4pedcI1ZunB zt(0cFdtDA>7H6E2;hFT>L0q0B%EJ zsPM9Y+u>re4V$78n@p&WXu24YK)uHe2W+zJ7wk^9LIR+YE*Hx=8h|p1_DQf513pJt zh11EFRRTT%(d#vbPmuN~Qm0WUDQQ2H_LuW459MjG!Y9&|Y#C4U0S8Zm$}(0eukbm1 z4sJ}^6h1-vZdHa&P7Imm7+3#qQ)H2ukg87B;MVLmZCSW+6K!k>STTNd%n_0k?IHd{&Pex3lXa!L+8kt)%Y619N%(&6aWu+riVe-TUyg159eN*`4Ib!V~ zf-9(`RAScTBAOD=W1PFu7EwC$B{LB}d_N9eku@($AC= zDl^@?F6T++jxMJvqD7J*K=3BPh3A8K2`&T(-Xys2d=M|eg#f{u1Q(tU;w88cAb6AD z!t+7A1Q!AXZxUR1K8Tm#LV(~+f(y?F@e*7J5WGon;rSq5f(rqHHwi90AH+*=AwcjZ z!G-68cnK~92;L;P@O%(2!G!?9n* z9634|J8<9Er+$i{YJ!aYT+O(nk;wwsLu6cL=_x2a_mcP zuuHm$Ti@6_(s_0$kLjbDeb0SG9lCBhx8Z~Og=c>HA+L62AOC!~wO`#pebI5ly56Gx z+11yr9eHu4b-noUl;O&TldFRHFX!~XU3ANgxU1xFY0j>Pr!+lY{OALDw%p;pEgx>C lwjJCPIQ&rcNBpXsA1^R3wJtsItntEaeQks1mHBOt{0*E1-v0mq literal 0 HcmV?d00001 diff --git a/data/sounds/Creature_Detected.ogg b/data/sounds/Creature_Detected.ogg new file mode 100644 index 0000000000000000000000000000000000000000..fd369785876da52ee263f3e565095c2df68b00e0 GIT binary patch literal 8348 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¥YYwh?)_crt>{k7twgk)6rTZCPha=I$WwXTG)fSUtF&Q?3%#!ce~kiN zhNs{Oxq_s4+#zbRpm2a{q$_}=s@Mu5QYzbApv#aA2yl-e!c;aji2xA@B1|I(m$zj5 z1rz^$bMm38!AyEk)zkpxJR*RIIw9Nhg5Wa;dWK+!2;t**7$!|i=wwiG-IgqBQWbPd zechW&o8#b$n=16>`8RwZDXsz??uPSA;R~n`Q8+*=7kt+A3$^B{1SM5XK@T?@9;3Su zl>u1y495K)lA6A0pf13|+J(D^qhr;192lZ;Y-YD}I|n9yGIR}+6T);Qk&X6s+dA0v-^7J zF7D(h4iihpXtOC~vUWTLl7ay%CJJB;Q(YA)$k5hb4AD6oy0xWqc-<5o{Qt$!61%U{p|&>KwXkoe*K3r9L9S z1<&Qgv#Ci$sdP$`+4NW(RAqKtxAmZxD1~xpW$+yn2F$U$DDbAq;o^Oa!|8tsffgyR zTaAZfb`htODaiz=!9;=(9#U*uaP)4Xlr|kA@bRf41eP?zfoTz~9VtS@V=bohps;8t zgs5WwPePc(Dza3||NjTbDzwXg9rQq3&0nadpLd07UTNLZj&U*~W5$aEAZ@G9qd{Bf z!h;2plMXF)F^)oh3FDQFfeD0+UK&=iDI{nvXt;j{i+`XFBEm}-A?&-I5s?ETJduI_ znfDL$ufxs%q7X4qtDy<-3c;Mcx-ZMZOlz0?0Uqq~l$a@GG{R1$6lWErkPw7RAxp2I zcRRn*grx{(rw$^T$7PcYw2yS#O6ew~QZU*XDHb#OY~R?hdjvpSy#C&WTJV>FILQ;A1))uH~`GWZg&#$pGQO$mO%K zWG95@3Tqx$iKMTc0L9hirNNlX*g?WT4k-HhkP4JJje-F1Y$;QtE!vB#(Hbtbb3jSG zS^>b5hz&b(h*9Lc8co=GSx*iT;ssp8dsxr3bMKYt$wYkzJ9)wjHcpj{&X1#6t;w+K zaQCRI86YleQK84oR;j{c0a^@5#7h7iUOx*3B&E>5Nr|mQNyvu))+zL5jI4D2UE4Ll z%EsPl890YY>`yR8YoCr>5%|6-L2*-V?RdO=$gjf|7R;eQsykS*qOWVGkN=*)gTY~u z$75KKwy2>&3;;T-ZS3t$naV1vI5l+*&6QfK@YSYK@X1$HKBiW4-NDHKTlU=;16jxUlijRx^7F&m{+kJh!cKQ1|>7r0T zedcSzSprtHh1SSdLwj)6#(f&MtoLM0>y=K=zPZJMr*dbX5o6D>w*OK1QKqsn^-Zne zS=X3GtCKzQS~Fkc&|pPYuva9QlP60zR2CO`-dd|=>nRemni%iWlHqyE5UHp~e8b`8a{@}E6!^|p3GW2Hda_v=I9`7K`Yz)BS; z1CRG_%vvif!YwqO8GD!nckUP;Wh6xEans+-!r$l>P&-Gc5Ml@LjLXT!ADV?D1)%wmBiAL9>s3c5lq7A z9S+lxBlora3iV1(mtQ^8_;p9~cVpQo&C0N0)mdp~D=jqX_NZ`o-%XhK@~WO63n(cu zd^Kk&Yd(L>?k=$}Fv@8|c6wJ5DsP?^SK1nW`D6%xkK5h8_(L1sTQd{a?`j^d_SK~+ zzBu?cfMjP=R%h^jxFBV4uaCt@dR&bM2JE>$@m;HCyudrg(r^5_@_M#dKz3rmqsdKEdQ9?##72f2~}xj(9_HxZ@dkRe_ZR zr;X2dhQ2TQ3&Rrofg;|0du^TR^y`(DU~O&L^wcE(?}9FeLGyW1Hp=%`I z)p={$55-``xh4+cD-LoW8Eii6YB&(ocS;~xzlKzgXT3UZ2F^T-JGp%rbMSH8w`j|S z(oyx_={I+dkpb}2uRJj!(|Q3jlWEp9n0=H5qb;)GXY3Ol213;}ZP zxOmh2F_THsr0t&?X7_R1?UUnBs0Po|@;+Tb_6QzdVRt9Hsa97;omS6$F-JNhG-?kx@s0foF; z4v_Ysp656$yHXJ$0ZbPUjCD>LeVCjcJwCg<>Fu>QjhZr>(&4|E${Ti9A7oDY;i}yk zpDRO?P8w8C8lBSG-`w#1P58a>u;OYItI%z#1%&12D_p7RY|xT^QNX#s%|THXoj>Qg zC<)#;etiH>D-(zI&JrkSNg3d4DZOdo_2wp3g|4+J8*lpTQAYhBiUEdr802sHmYmBQ zZxzd4wXesqj(ECVx_WG7U;s`v`PtE#QEU3`tVN~*E&TQKFWXi3fARA2zjZsPIDA`Z z$c=n5bp@FFVBD>Gp@oDNytWt%Iq?JqHj_#H=NdZ1%1*5M-5ceu?d;sF!J0E9Pgkm@ z+izM|QStrm`bVeAUYVPQyctqo^|=rF?=izVC4$t*cHgtUTg<+%1X6D3FsE`EUpr-(UH51=-r1-AaDZohB$;&Z`0km+`Psf=Qz>86u+#B5OeA|=4*Al`fZcZI(h#)>VD$_nLq$E4LGA_TX`w|5@D z?C&1(R6F^}J}0)qx9*;rL4ZTKm(R~j z@wXOHqs5YvvM+WbFh4=%(Z(kAJW2t|X7M@ep@C8NVdBBpZM0dVY(+;8 ze|(k@AL-!};~Z9d3bjAud-bh%ewGUI{T6k0)g}za@V)!+r&nzKY&g(Lw6Nb-=^b4ey}9hhFhV^*akPQ$82vclF%HWEQV^p6}%O2P0*9nU+8Ud;F^Ue2rtx zkm{iLj+&dHJjDY<8u@_|>IdT%T8GnUygSfuw`)~x!Ez85LwJL^R=H-&VxaMl-xF^3 zmakiudsj>O>kf@w5@F$gtoYRYS!Dp!f7p}#!t$PK*JJR5@o;rn!0s-Zj&RvsI?r&@ng-3A$>8WK4N0wzc8T%#o*FOTS)1InatrJM!k*!7n*uTTyG9Oy%0RO+5>QRSK!<)8ET(F-sQ~lF8 zC)essl2Sn-f%Wd&;w0*ep4CZ{Q4N+}#=u80C5i9nB1fsiNYP%)z>>i9m$w*?dR^)R ze55uXAN5(bwR2PPmg$G|OEUV*)0bF{mFzSN|5^2L{pVw`;h%0W>rpx{v;FPUDRNO3 zRhJ!?Ju#RG_|vcQ_JD+<>BO1iNw*h1hJN~Zczr`WzPf?zf3Efc_;HM}^HlN4jD!1I zQCp{E$H3eT7xBg~gL#;7kNQrR&odhe!G|{^p7q>|HlM{mbrH%@ogvBKt}jZ~Im4rb{25$h~u+ z59J$t+xzK+g2I&w37~b><3WMcm!vZ(BerYh3@`^^kjejaE4yv7xJqJPcj%H}YgnD7 zNvZr=%M05q-!7I7hZ~={-(y`To~2dtFnpc2d8hrc#t5(c-rLvKeo6LhUjAg}Zp58i zs`sS8*a7Xa3;4GMi6nS7)Lhf^*R}Dv{;9v8Srup7YMdFDsP@_H!umLS zz$|YrvvZs-4%|zsJZZi*xym|Q1bQ5lk$k%Y($AxSla z6qRvEj-i87M|5(iM|AS8(eL@a@9(?5>%G3ezICm=_S*Yi>t6TyzW3g`Z=VMc0sm^# z>As!9>Gma0hEN))gZrcXgK5GSsD_=w8vq))f9@ryt-_uEZo-`?_#}gVGbm@h@IP9U z=pQt)5V~d0!M*Fvjn}R>#+j@WE?)(&zzFL8fWT-g2&6)wnHdC{Xody*|JjEU|8v(S z*m|LWH2^9EjDdJLHPxbJnO4e*%lU*hq*xwD+XjRi zxN6(oqMeM!a%hDr+uNhYHR0^7=DzkQDP1^A#gC;&r{exV<)RVcy5mZYtf*oYIOD3p z*%jd|99Nwl-GPU*cbccj(bv`sp(^|2qGg0w1VVftgqTy-S3mi)ZgKJAkRn9CIE%oI zDbubAvA{>QKkqcx1uO$;0OS{7S+B5i7}XonpalSob(E6hIVI;xwMX4LvS?W!ai|A? zhU|*~z5oEweppta zdFmVQbe>P1B=h7|5kLn(46a+^SgGVPt>{O4NwvNM+Imt)fKk6 zWCdSoWrCW#tvaE@wuHwRMG|$XJswwW&{{_(t-cM%PySH>27o!7um{!03G zQAL_?_EyKc?eMq`dWeA`+|G%AR0DS`iURW3pvlA#;2Jm}TsE1A6lM9dkY+VnJFZ!i zvBY#dKf~3cm7DHr(WXmDt<%ylkvTc3a!UtxU_>IcX?>0rWz1)@vfv zN2;z*x?XVRzQdWthqJFFX)G>*ih)vtjedDiZ8|=gMZtN+LS>hhvWiMdOGC1Yg8%m} zQlM1^fD{1g%~5KOQEJUmLar7itSJgK0kCyfvyV1xPG9E?dF-|8{Oap&cEDqM_&-X3 zON6bNwh{JLC<-{q*yz-|&gK8H3k53zpz&cc(nzkA+Z_;r&aGVrT&0XB@mI3^w^)2C zAxI#SHK#~gy!_r_Q(hW*Nqlh@p+)6=0V55UczxhGt+W1{22`Fru!HeJpeevmTX1^1 z3$r1Em$fE;E zc*zuWr7c5Q=fLMQ8ViS(r(y_FNTM6LG$*6S#;JsnPIfA3N++jqXk-g14l){h60+1lkVorid^*U$ zl)udh`DoSjH2yPQr44y)vtQwF&^Db`HTpLJ(BAO@-AMlm(-#>f~XiQ+*~%z#A3|fv%C>9n8sHwh+jh* z(`1+OB`<`!nGQfyP2>5=YXIFMUr=;+Uo=}W*`4CklTS16Q+1Uk}49Nfo&j`i6%cdmbAOJxG>7=mo zrtBSI_f+==Xkv71(tFx&Q-nm&yYbDn^x;16`ECrgb>A=D@&Df~G+rA~aV5$tbYH zhFHRPBazCRlu1q{d}SE`u#qrzUAOV@kD@OIlSKz6;Uo%h4#=41IsvvxsBA$_h8qIS z+07K1iyP@(4h=(+(qfZIBrPTxqJj=9B6Q%D>vWYRAwl+k2%_e!?a-3W<#mvCN(%ek zL$N$n=agK5SNw;XUe);Unhk8ynUen0&_^|W!@(|A)oPF+OGuFAb&RyEj}&+V139E1 zi`KCsQAL=`mylD0shtJWJ>*RYfT;!opfRSybkD?)#)b4Dych^5V3RGzG$DORE+iM> zv0+A*7RIVDrDr*QI#;^VIKWy1n)nhzhf{F*o_0LisPCzXK#k4zPljGWPOfoiw;>{e zd`m3CR|%WSWwI&Bc*zWM^7^r11{7sBqr}?wcn%DUXss9_fQhjf z%ZJROArr!k{eNS^9A=TlT>k%mfTTjb{O6z#TB`4(8WtJ_E4

q#YSDLS-^V0g$%A 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 DRz9i8 literal 0 HcmV?d00001 diff --git a/data/sounds/Player_Detected.ogg b/data/sounds/Player_Detected.ogg new file mode 100644 index 0000000000000000000000000000000000000000..34de63e432f73516bda8da43983576baa085d426 GIT binary patch literal 8461 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 literal 0 HcmV?d00001 diff --git a/data/sounds/alarm.ogg b/data/sounds/alarm.ogg new file mode 100644 index 0000000000000000000000000000000000000000..a45d4c8f6088688f0fafea5f4ab1b895711a54e2 GIT binary patch literal 118635 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�oyi25;cS6LGQSLK)Eg-bUXy>j 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 literal 0 HcmV?d00001 diff --git a/data/sounds/magnum.ogg b/data/sounds/magnum.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d910ca1ace9ef1ef4b838de324101f64eff6cd76 GIT binary patch literal 52698 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 KG=|NG4*&og+)FtC literal 0 HcmV?d00001 diff --git a/data/styles/20-smallscrollbar.otui b/data/styles/20-smallscrollbar.otui new file mode 100644 index 0000000000..4396a0b5cd --- /dev/null +++ b/data/styles/20-smallscrollbar.otui @@ -0,0 +1,60 @@ +SmallScrollBar < UIScrollBar + orientation: vertical + margin-bottom: 1 + step: 20 + width: 8 + image-source: /images/ui/scrollbar + image-clip: 39 0 13 65 + image-border: 1 + pixels-scroll: true + + UIButton + id: decrementButton + anchors.top: parent.top + anchors.left: parent.left + image-source: /images/ui/scrollbar + image-clip: 0 0 13 13 + image-color: #ffffffff + size: 8 8 + $hover: + image-clip: 13 0 13 13 + $pressed: + image-clip: 26 0 13 13 + $disabled: + image-color: #ffffff66 + + UIButton + id: incrementButton + anchors.bottom: parent.bottom + anchors.right: parent.right + size: 8 8 + image-source: /images/ui/scrollbar + image-clip: 0 13 13 13 + image-color: #ffffffff + $hover: + image-clip: 13 13 13 13 + $pressed: + image-clip: 26 13 13 13 + $disabled: + image-color: #ffffff66 + + UIButton + id: sliderButton + anchors.centerIn: parent + size: 8 11 + image-source: /images/ui/scrollbar + image-clip: 0 26 13 13 + image-border: 2 + image-color: #ffffffff + $hover: + image-clip: 13 26 13 13 + $pressed: + image-clip: 26 26 13 13 + $disabled: + image-color: #ffffff66 + + Label + id: valueLabel + anchors.fill: parent + color: white + text-align: center \ No newline at end of file diff --git a/data/styles/40-gamebuttons.otui b/data/styles/40-gamebuttons.otui new file mode 100644 index 0000000000..76c3b9e2dc --- /dev/null +++ b/data/styles/40-gamebuttons.otui @@ -0,0 +1,2 @@ +GameButtonsWindow < MiniWindow + height: 26 diff --git a/modules/client/client.otmod b/modules/client/client.otmod index d0fa54a4e1..282d7f842a 100644 --- a/modules/client/client.otmod +++ b/modules/client/client.otmod @@ -14,6 +14,7 @@ Module - client_locales - client_topmenu - client_background + - client_textedit - client_options - client_entergame - client_terminal diff --git a/modules/client_profiles/profiles.lua b/modules/client_profiles/profiles.lua new file mode 100644 index 0000000000..eb38948fff --- /dev/null +++ b/modules/client_profiles/profiles.lua @@ -0,0 +1,160 @@ +local settings = {} +ChangedProfile = false + +function init() + connect(g_game, { + onGameStart = online, + onGameEnd = offline + }) + +end + +function terminate() + disconnect(g_game, { + onGameStart = online, + onGameEnd = offline + }) +end + +-- loads settings on character login +function online() + ChangedProfile = false + + -- startup arguments has higher priority than settings + local index = getProfileFromStartupArgument() + if index then + setProfileOption(index) + end + + load() + + if not index then + setProfileOption(getProfileFromSettings() or 1) + end + + -- create main settings dir + if not g_resources.directoryExists("/settings/") then + g_resources.makeDir("/settings/") + end + + -- create profiles dirs + for i=1,10 do + local path = "/settings/profile_"..i + + if not g_resources.directoryExists(path) then + g_resources.makeDir(path) + end + end +end + +function setProfileOption(index) + local currentProfile = g_settings.getNumber('profile') + currentProfile = tostring(currentProfile) + index = tostring(index) + + if currentProfile ~= index then + ChangedProfile = true + return modules.client_options.setOption('profile', index) + end + +end + +-- load profile number from settings +function getProfileFromSettings() + -- settings should save per character, return if not online + if not g_game.isOnline() then return end + + local index = g_game.getCharacterName() + local savedData = settings[index] + + return savedData +end + +-- option to launch client with hardcoded profile +function getProfileFromStartupArgument() + local startupOptions = string.split(g_app.getStartupOptions(), " ") + if #startupOptions < 2 then + return false + end + + for index, option in ipairs(startupOptions) do + if option == "--profile" then + local profileIndex = startupOptions[index + 1] + if profileIndex == nil then + return g_logger.info("Startup arguments incomplete: missing profile index.") + end + + g_logger.info("Startup options: Forced profile: "..profileIndex) + -- set value in options + return profileIndex + end + end + + return false +end + +-- returns string path ie. "/settings/1/actionbar.json" +function getSettingsFilePath(fileNameWithFormat) + local currentProfile = g_settings.getNumber('profile') + + return "/settings/profile_"..currentProfile.."/"..fileNameWithFormat +end + +function offline() + onProfileChange(true) +end + +-- profile change callback (called in options), saves settings & reloads given module configs +function onProfileChange(offline) + if not offline then + if not g_game.isOnline() then return end + -- had to apply some delay + scheduleEvent(collectiveReload, 100) + end + + local currentProfile = g_settings.getNumber('profile') + local index = g_game.getCharacterName() + + if index then + settings[index] = currentProfile + save() + end +end + +-- collection of refresh functions from different modules +function collectiveReload() + modules.game_topbar.refresh(true) + modules.game_actionbar.refresh(true) + modules.game_bot.refresh() +end + +-- json handlers +function load() + local file = "/settings/profiles.json" + if g_resources.fileExists(file) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(file)) + end) + if not status then + return onError( + "Error while reading profiles file. To fix this problem you can delete storage.json. Details: " .. + result) + end + settings = result + end +end + +function save() + local file = "/settings/profiles.json" + local status, result = pcall(function() return json.encode(settings, 2) end) + if not status then + return onError( + "Error while saving profile settings. Data won't be saved. Details: " .. + result) + end + if result:len() > 100 * 1024 * 1024 then + return onError( + "Something went wrong, file is above 100MB, won't be saved") + end + g_resources.writeFileContents(file, result) +end \ No newline at end of file diff --git a/modules/client_profiles/profiles.otmod b/modules/client_profiles/profiles.otmod new file mode 100644 index 0000000000..a43d72eec6 --- /dev/null +++ b/modules/client_profiles/profiles.otmod @@ -0,0 +1,11 @@ +Module + name: client_profiles + description: Client profiles + author: Vithrax + website: discord_Vithrax#5814 + autoload: true + reloadable: false + scripts: [ profiles ] + sandboxed: true + @onLoad: init() + @onUnload: terminate() \ No newline at end of file diff --git a/modules/client_textedit/textedit.lua b/modules/client_textedit/textedit.lua new file mode 100644 index 0000000000..3a55d7ef71 --- /dev/null +++ b/modules/client_textedit/textedit.lua @@ -0,0 +1,166 @@ +local activeWindow + +function init() + g_ui.importStyle('textedit') + + connect(g_game, { onGameEnd = destroyWindow }) +end + +function terminate() + disconnect(g_game, { onGameEnd = destroyWindow }) + + destroyWindow() +end + +function destroyWindow() + if activeWindow then + activeWindow:destroy() + activeWindow = nil + end +end + +-- also works as show(text, callback) +function show(text, options, callback) -- callback = function(newText) + --[[ + Available options: + title = text + description = text + multiline = true / false + width = number + validation = text (regex) + range = {number, number} + examples = {{name, text}, {name, text}} + ]] + -- + if type(text) == 'userdata' then + local widget = text + callback = function(newText) + widget:setText(newText) + end + text = widget:getText() + elseif type(text) == 'number' then + text = tostring(text) + elseif type(text) == 'nil' then + text = '' + elseif type(text) ~= 'string' then + return error("Invalid text type for client_textedit: " .. type(text)) + end + if type(options) == 'function' then + local tmp = callback + callback = options + options = callback + end + options = options or {} + + if activeWindow then + destroyWindow() + end + + local window + if options.multiline then + window = g_ui.createWidget('MultilineTextEditWindow', rootWidget) + window.text = window.textPanel.text + else + window = g_ui.createWidget('SinglelineTextEditWindow', rootWidget) + end + -- functions + local validate = function(text) + 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 true + end + local destroy = function() + window:destroy() + end + local doneFunc = function() + local text = window.text:getText() + if not validate(text) then return end + destroy() + if callback then + callback(text) + end + end + + window.buttons.ok.onClick = doneFunc + window.buttons.cancel.onClick = destroy + if not options.multiline then + window.onEnter = doneFunc + end + window.onEscape = destroy + window.onDestroy = function() + if window == activeWindow then + activeWindow = nil + end + end + + if options.title then + window:setText(options.title) + end + if options.description then + window.description:show() + window.description:setText(options.description) + end + if type(options.examples) == 'table' and #options.examples > 0 then + window.examples:show() + for i, title_text in ipairs(options.examples) do + window.examples:addOption(title_text[1], title_text[2]) + end + window.examples.onOptionChange = function(widget, option, data) + window.text:setText(data) + window.text:setCursorPos(-1) + end + end + + window.text:setText(text) + window.text:setCursorPos(-1) + + window.text.onTextChange = function(widget, text) + if validate(text) then + window.buttons.ok:enable() + if g_platform.isMobile() then + doneFunc() + end + else + window.buttons.ok:disable() + end + end + + if type(options.width) == 'number' then + window:setWidth(options.width) + end + + activeWindow = window + activeWindow:raise() + activeWindow:focus() + if g_platform.isMobile() then + window.text:focus() + local flags = 0 + if options.multiline then + flags = 1 + end + g_window.showTextEditor(window:getText(), window.description:getText(), window.text:getText(), flags) + end + return activeWindow +end + +function hide() + destroyWindow() +end + +function edit(...) + return show(...) +end + +-- legacy +function singlelineEditor(text, callback) + return show(text, {}, callback) +end + +-- legacy +function multilineEditor(description, text, callback) + return show(text, { description = description, multiline = true }, callback) +end diff --git a/modules/client_textedit/textedit.otmod b/modules/client_textedit/textedit.otmod new file mode 100644 index 0000000000..e8ba2c6261 --- /dev/null +++ b/modules/client_textedit/textedit.otmod @@ -0,0 +1,9 @@ +Module + name: client_textedit + description: Shows window which allows to edit text + author: OTClientV8 + website: https://github.com/OTCv8/otclientv8 + sandboxed: true + scripts: [ textedit ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_textedit/textedit.otui b/modules/client_textedit/textedit.otui new file mode 100644 index 0000000000..86d77aa199 --- /dev/null +++ b/modules/client_textedit/textedit.otui @@ -0,0 +1,75 @@ +TextEditButtons < Panel + id: buttons + height: 30 + + 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 + +TextEditWindow < MainWindow + id: textedit + !text: tr("Edit text") + layout: + type: verticalBox + fit-children: true + + Label + id: description + text-align: center + margin-bottom: 5 + visible: false + text-wrap: true + text-auto-resize: true + + ComboBox + id: examples + margin-bottom: 5 + visible: false + +SinglelineTextEditWindow < TextEditWindow + width: 250 + + TextEdit + id: text + + TextEditButtons + +MultilineTextEditWindow < TextEditWindow + width: 600 + $mobile: + width: 500 + + Panel + id: textPanel + height: 400 + $mobile: + height: 300 + + MultilineTextEdit + id: text + anchors.fill: parent + margin-right: 12 + text-wrap: true + vertical-scrollbar: textScroll + + VerticalScrollBar + id: textScroll + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + pixels-scroll: true + step: 10 + + TextEditButtons + diff --git a/modules/corelib/keyboard.lua b/modules/corelib/keyboard.lua index 13c37b2eec..01ca97edd2 100644 --- a/modules/corelib/keyboard.lua +++ b/modules/corelib/keyboard.lua @@ -26,7 +26,7 @@ local function getKeyCode(key) end end -local function retranslateKeyComboDesc(keyComboDesc) +function retranslateKeyComboDesc(keyComboDesc) if keyComboDesc == nil then error('Unable to translate key combo \'' .. keyComboDesc .. '\'') end diff --git a/modules/corelib/table.lua b/modules/corelib/table.lua index 21fd6cab9f..fcf096d518 100644 --- a/modules/corelib/table.lua +++ b/modules/corelib/table.lua @@ -198,8 +198,8 @@ function table.collect(t, func) end function table.insertall(t, s) - for k,v in pairs(s) do - table.insert(t,v) + for k, v in pairs(s) do + table.insert(t, v) end return res end @@ -214,3 +214,99 @@ function table.equals(t, comp) end return true end + +function table.equal(t1, t2, ignore_mt) + local ty1 = type(t1) + local ty2 = type(t2) + if ty1 ~= ty2 then return false end + -- non-table types can be directly compared + if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end + -- as well as tables which have the metamethod __eq + local mt = getmetatable(t1) + if not ignore_mt and mt and mt.__eq then return t1 == t2 end + for k1, v1 in pairs(t1) do + local v2 = t2[k1] + if v2 == nil or not table.equal(v1, v2) then return false end + end + for k2, v2 in pairs(t2) do + local v1 = t1[k2] + if v1 == nil or not table.equal(v1, v2) then return false end + end + return true +end + +function table.isList(t) + local size = #t + return table.size(t) == size and size > 0 +end + +function table.isStringList(t) + if not table.isList(t) then return false end + for k, v in ipairs(t) do + if type(v) ~= 'string' then + return false + end + end + return true +end + +function table.isStringPairList(t) + if not table.isList(t) then return false end + for k, v in ipairs(t) do + if type(v) ~= 'table' or #v ~= 2 or type(v[1]) ~= 'string' or type(v[2]) ~= 'string' then + return false + end + end + return true +end + +function table.encodeStringPairList(t) + local ret = "" + for k, v in ipairs(t) do + if v[2]:find("\n") then + ret = ret .. v[1] .. ":[[\n" .. v[2] .. "\n]]\n" + else + ret = ret .. v[1] .. ":" .. v[2] .. "\n" + end + end + return ret +end + +function table.decodeStringPairList(l) + local ret = {} + local r = regexMatch(l, "(?:^|\\n)([^:^\n]{1,20}):?(.*)(?:$|\\n)") + local multiline = "" + local multilineKey = "" + local multilineActive = false + for k, v in ipairs(r) do + if multilineActive then + local endPos = v[1]:find("%]%]") + if endPos then + if endPos > 1 then + table.insert(ret, { multilineKey, multiline .. "\n" .. v[1]:sub(1, endPos - 1) }) + else + table.insert(ret, { multilineKey, multiline }) + end + multilineActive = false + multiline = "" + multilineKey = "" + else + if multiline:len() == 0 then + multiline = v[1] + else + multiline = multiline .. "\n" .. v[1] + end + end + else + local bracketPos = v[3]:find("%[%[") + if bracketPos == 1 then -- multiline begin + multiline = v[3]:sub(bracketPos + 2) + multilineActive = true + multilineKey = v[2] + elseif v[2]:len() > 0 and v[3]:len() > 0 then + table.insert(ret, { v[2], v[3] }) + end + end + end + return ret +end diff --git a/modules/corelib/ui/uicombobox.lua b/modules/corelib/ui/uicombobox.lua index fb8143ec31..9672637a3f 100644 --- a/modules/corelib/ui/uicombobox.lua +++ b/modules/corelib/ui/uicombobox.lua @@ -13,6 +13,10 @@ function UIComboBox.create() return combobox end +function UIComboBox:clear() + return self:clearOptions() +end + function UIComboBox:clearOptions() self.options = {} self.currentIndex = -1 diff --git a/modules/corelib/ui/uiminiwindow.lua b/modules/corelib/ui/uiminiwindow.lua index 6c943f7d20..6251b1f126 100644 --- a/modules/corelib/ui/uiminiwindow.lua +++ b/modules/corelib/ui/uiminiwindow.lua @@ -1,483 +1,561 @@ -- @docclass -UIMiniWindow = extends(UIWindow, 'UIMiniWindow') +UIMiniWindow = extends(UIWindow, "UIMiniWindow") function UIMiniWindow.create() - local miniwindow = UIMiniWindow.internalCreate() - miniwindow.UIMiniWindowContainer = true - return miniwindow + local miniwindow = UIMiniWindow.internalCreate() + miniwindow.UIMiniWindowContainer = true + return miniwindow end function UIMiniWindow:open(dontSave) - self:setVisible(true) + self:setVisible(true) - if not dontSave then - self:setSettings({ - closed = false - }) - end + if not dontSave then + self:setSettings({ closed = false }) + end - signalcall(self.onOpen, self) + addEvent(function() + self:raise() + self:getParent():saveChildren() + end) + + signalcall(self.onOpen, self) end function UIMiniWindow:close(dontSave) - if not self:isExplicitlyVisible() then - return - end - self:setVisible(false) + if not self:isExplicitlyVisible() then return end + if self.forceOpen then return end + self:setVisible(false) - if not dontSave then - self:setSettings({ - closed = true - }) - end + if not dontSave then + self:setSettings({ closed = true }) + end - signalcall(self.onClose, self) + signalcall(self.onClose, self) end function UIMiniWindow:minimize(dontSave) - self:setOn(true) - self:getChildById('contentsPanel'):hide() - self:getChildById('miniwindowScrollBar'):hide() - self:getChildById('bottomResizeBorder'):hide() - self:getChildById('minimizeButton'):setOn(true) - self.maximizedHeight = self:getHeight() - self:setHeight(self.minimizedHeight) - - if not dontSave then - self:setSettings({ - minimized = true - }) - end + self:setOn(true) + self:getChildById('contentsPanel'):hide() + self:getChildById('miniwindowScrollBar'):hide() + self:getChildById('bottomResizeBorder'):hide() + if self.minimizeButton then + self.minimizeButton:setOn(true) + end + self.maximizedHeight = self:getHeight() + self:setHeight(self.minimizedHeight) + + if not dontSave then + self:setSettings({ minimized = true }) + end - signalcall(self.onMinimize, self) + signalcall(self.onMinimize, self) end function UIMiniWindow:maximize(dontSave) - self:setOn(false) - self:getChildById('contentsPanel'):show() - self:getChildById('miniwindowScrollBar'):show() - self:getChildById('bottomResizeBorder'):show() - self:getChildById('minimizeButton'):setOn(false) - self:setHeight(self:getSettings('height') or self.maximizedHeight) - - if not dontSave then - self:setSettings({ - minimized = false - }) - end - - local parent = self:getParent() - if parent and parent:getClassName() == 'UIMiniWindowContainer' then - parent:fitAll(self) - end - - signalcall(self.onMaximize, self) + self:setOn(false) + self:getChildById('contentsPanel'):show() + self:getChildById('miniwindowScrollBar'):show() + self:getChildById('bottomResizeBorder'):show() + if self.minimizeButton then + self.minimizeButton:setOn(false) + end + self:setHeight(self:getSettings('height') or self.maximizedHeight) + + if not dontSave then + self:setSettings({ minimized = false }) + end + + local parent = self:getParent() + if parent and parent:getClassName() == 'UIMiniWindowContainer' then + parent:fitAll(self) + end + + signalcall(self.onMaximize, self) +end + +function UIMiniWindow:lock(dontSave) + local lockButton = self:getChildById('lockButton') + if lockButton then + lockButton:setOn(true) + end + self:setDraggable(false) + if not dontsave then + self:setSettings({ locked = true }) + end + + signalcall(self.onLockChange, self) +end + +function UIMiniWindow:unlock(dontSave) + local lockButton = self:getChildById('lockButton') + if lockButton then + lockButton:setOn(false) + end + self:setDraggable(true) + if not dontsave then + self:setSettings({ locked = false }) + end + signalcall(self.onLockChange, self) end function UIMiniWindow:setup() - self:getChildById('closeButton').onClick = function() + self:getChildById('closeButton').onClick = + function() self:close() + end + if self.forceOpen then + if self.closeButton then + self.closeButton:hide() end + end - self:getChildById('minimizeButton').onClick = function() - if self:isOn() then + if (self.minimizeButton) then + self.minimizeButton.onClick = + function() + if self:isOn() then self:maximize() - else + else self:minimize() + end end - end + end + + local lockButton = self:getChildById('lockButton') + if lockButton then + lockButton.onClick = + function() + if self:isDraggable() then + self:lock() + else + self:unlock() + end + end + end - self:getChildById('miniwindowTopBar').onDoubleClick = function() + self:getChildById('miniwindowTopBar').onDoubleClick = + function() if self:isOn() then - self:maximize() + self:maximize() else - self:minimize() + self:minimize() end - end -end - -function UIMiniWindow:setupOnStart() - local char = g_game.getCharacterName() - if not char or #char == 0 then - return - end + end + self:getChildById('bottomResizeBorder').onDoubleClick = function() + local resizeBorder = self:getChildById('bottomResizeBorder') + self:setHeight(resizeBorder:getMinimum()) + end - local oldParent = self:getParent() - local newParentSet = false + local oldParent = self:getParent() - local settings = g_settings.getNode('CharMiniWindows') - if not settings then - settings = { - [char] = {} - } + local settings = {} + if g_settings.getNodeSize('MiniWindows') < 50 then + settings = g_settings.getNode('MiniWindows') + end - elseif not settings[char] then - -- if there are no settings for this character, we'll copy the settings from - -- another one, so we'll have something better than all the windows randomly positioned - for k, v in pairs(settings) do - settings[char] = v - g_settings.setNode('CharMiniWindows', settings) - break + if settings then + local selfSettings = settings[self:getId()] + if selfSettings then + if selfSettings.parentId then + local parent = rootWidget:recursiveGetChildById(selfSettings.parentId) + if parent then + if parent:getClassName() == 'UIMiniWindowContainer' and selfSettings.index and parent:isOn() then + self:setParent(parent, true) + self.miniIndex = selfSettings.index + parent:scheduleInsert(self, selfSettings.index) + elseif selfSettings.position then + self:setParent(parent, true) + self:setPosition(topoint(selfSettings.position)) + end + end + end + + if selfSettings.minimized then + self:minimize(true) + else + if selfSettings.height and self:isResizeable() then + self:setHeight(selfSettings.height) + elseif selfSettings.height and not self:isResizeable() then + self:eraseSettings({ height = true }) end + end + if selfSettings.closed and not self.forceOpen and not self.containerWindow then + self:close(true) + end + + if selfSettings.locked then + self:lock(true) + end + else + if not self.forceOpen and self.autoOpen ~= nil and (self.autoOpen == 0 or self.autoOpen == false) and not self.containerWindow then + self:close(true) + end end + end - local selfSettings = settings[char][self:getId()] - if selfSettings then - if selfSettings.parentId then - local parent = rootWidget:recursiveGetChildById(selfSettings.parentId) - if parent and parent:isVisible() then - if parent:getClassName() == 'UIMiniWindowContainer' and selfSettings.index and parent:isOn() then - self.miniIndex = selfSettings.index - parent:scheduleInsert(self, selfSettings.index) - newParentSet = true - elseif selfSettings.position then - self:setParent(parent, true) - self:setPosition(topoint(selfSettings.position)) - newParentSet = true - end - end - end + local newParent = self:getParent() - if selfSettings.minimized then - self:minimize(true) - elseif selfSettings.height then - if self:isResizeable() then - self:setHeight(selfSettings.height) - else - self:eraseSettings({ - height = true - }) - end - end + self.miniLoaded = true - if selfSettings.closed then - self:close(true) - else - self:open(true) + if self.save then + if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' and not self.containerWindow then + addEvent(function() oldParent:order() end) + end + if newParent and newParent:getClassName() == 'UIMiniWindowContainer' and newParent ~= oldParent then + addEvent(function() newParent:order() end) + end + end + + self:fitOnParent() +end + +function UIMiniWindow:setupOnStart() + local char = g_game.getCharacterName() + if not char or #char == 0 then + return + end + + local oldParent = self:getParent() + local newParentSet = false + + local settings = g_settings.getNode('CharMiniWindows') + + if not settings then + settings = { + [char] = {} + } + elseif not settings[char] then + -- if there are no settings for this character, we'll copy the settings from + -- another one, so we'll have something better than all the windows randomly positioned + for k, v in pairs(settings) do + settings[char] = v + g_settings.setNode('CharMiniWindows', settings) + break + end + end + + local selfSettings = settings[char][self:getId()] + if selfSettings then + if selfSettings.parentId then + local parent = rootWidget:recursiveGetChildById(selfSettings.parentId) + if parent and parent:isVisible() then + if parent:getClassName() == 'UIMiniWindowContainer' and selfSettings.index and parent:isOn() then + self.miniIndex = selfSettings.index + parent:scheduleInsert(self, selfSettings.index) + newParentSet = true + elseif selfSettings.position then + self:setParent(parent, true) + self:setPosition(topoint(selfSettings.position)) + newParentSet = true end + end end - local newParent = self:getParent() + if selfSettings.minimized then + self:minimize(true) + elseif selfSettings.height then + if self:isResizeable() then + self:setHeight(selfSettings.height) + else + self:eraseSettings({ + height = true + }) + end + end - if not oldParent and not newParentSet then - oldParent = modules.game_interface.getRightPanel() - self:setParent(oldParent) + if selfSettings.closed then + self:close(true) + else + self:open(true) end + end - self.miniLoaded = true + local newParent = self:getParent() - if self.save then - if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' then - addEvent(function() - oldParent:order() - end) - end - if newParent and newParent:getClassName() == 'UIMiniWindowContainer' and newParent ~= oldParent then - addEvent(function() - newParent:order() - end) - end + if not oldParent and not newParentSet then + oldParent = modules.game_interface.getRightPanel() + self:setParent(oldParent) + end + + self.miniLoaded = true + + if self.save then + if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' then + addEvent(function() + oldParent:order() + end) end + if newParent and newParent:getClassName() == 'UIMiniWindowContainer' and newParent ~= oldParent then + addEvent(function() + newParent:order() + end) + end + end - self:fitOnParent() + self:fitOnParent() end function UIMiniWindow:onVisibilityChange(visible) - self:fitOnParent() + self:fitOnParent() end function UIMiniWindow:onDragEnter(mousePos) - local parent = self:getParent() - if not parent then - return false - end + local parent = self:getParent() + if not parent then return false end - if parent:getClassName() == 'UIMiniWindowContainer' then - local containerParent = parent:getParent() - parent:removeChild(self) - containerParent:addChild(self) - parent:saveChildren() - end + if parent:getClassName() == 'UIMiniWindowContainer' then + local containerParent = parent:getParent():getParent() + parent:removeChild(self) + containerParent:addChild(self) + parent:saveChildren() + end - local oldPos = self:getPosition() - self.movingReference = { - x = mousePos.x - oldPos.x, - y = mousePos.y - oldPos.y - } - self:setPosition(oldPos) - self.free = true - return true + local oldPos = self:getPosition() + self.movingReference = { x = mousePos.x - oldPos.x, y = mousePos.y - oldPos.y } + self:setPosition(oldPos) + self.free = true + return true end function UIMiniWindow:onDragLeave(droppedWidget, mousePos) - if self.movedWidget then - self.setMovedChildMargin(self.movedOldMargin or 0) - self.movedWidget = nil - self.setMovedChildMargin = nil - self.movedOldMargin = nil - self.movedIndex = nil - end + if self.movedWidget then + self.setMovedChildMargin(self.movedOldMargin or 0) + self.movedWidget = nil + self.setMovedChildMargin = nil + self.movedOldMargin = nil + self.movedIndex = nil + end - self:saveParent(self:getParent()) + UIWindow:onDragLeave(self, droppedWidget, mousePos) + self:saveParent(self:getParent()) end function UIMiniWindow:onDragMove(mousePos, mouseMoved) - local oldMousePosY = mousePos.y - mouseMoved.y - local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos) - local overAnyWidget = false - for i = 1, #children do - local child = children[i] - if child:getParent():getClassName() == 'UIMiniWindowContainer' then - overAnyWidget = true - - local childCenterY = child:getY() + child:getHeight() / 2 - if child == self.movedWidget and mousePos.y < childCenterY and oldMousePosY < childCenterY then - break - end - - if self.movedWidget then - self.setMovedChildMargin(self.movedOldMargin or 0) - self.setMovedChildMargin = nil - end - - if mousePos.y < childCenterY then - self.movedOldMargin = child:getMarginTop() - self.setMovedChildMargin = function(v) - child:setMarginTop(v) - end - self.movedIndex = 0 - else - self.movedOldMargin = child:getMarginBottom() - self.setMovedChildMargin = function(v) - child:setMarginBottom(v) - end - self.movedIndex = 1 - end - - self.movedWidget = child - self.setMovedChildMargin(self:getHeight()) - break - end - end - - if not overAnyWidget and self.movedWidget then + local oldMousePosY = mousePos.y - mouseMoved.y + local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos) + local overAnyWidget = false + for i = 1, #children do + local child = children[i] + if child:getParent():getClassName() == 'UIMiniWindowContainer' then + overAnyWidget = true + + local childCenterY = child:getY() + child:getHeight() / 2 + if child == self.movedWidget and mousePos.y < childCenterY and oldMousePosY < childCenterY then + break + end + + if self.movedWidget then self.setMovedChildMargin(self.movedOldMargin or 0) - self.movedWidget = nil + self.setMovedChildMargin = nil + end + + if mousePos.y < childCenterY then + self.movedOldMargin = child:getMarginTop() + self.setMovedChildMargin = function(v) child:setMarginTop(v) end + self.movedIndex = 0 + else + self.movedOldMargin = child:getMarginBottom() + self.setMovedChildMargin = function(v) child:setMarginBottom(v) end + self.movedIndex = 1 + end + + self.movedWidget = child + self.setMovedChildMargin(self:getHeight()) + break end + end - return UIWindow.onDragMove(self, mousePos, mouseMoved) + if not overAnyWidget and self.movedWidget then + self.setMovedChildMargin(self.movedOldMargin or 0) + self.movedWidget = nil + end + + return UIWindow.onDragMove(self, mousePos, mouseMoved) end function UIMiniWindow:onMousePress() - local parent = self:getParent() - if not parent then - return false - end - if parent:getClassName() ~= 'UIMiniWindowContainer' then - self:raise() - return true - end + local parent = self:getParent() + if not parent then return false end + if parent:getClassName() ~= 'UIMiniWindowContainer' then + self:raise() + return true + end end function UIMiniWindow:onFocusChange(focused) - if not focused then - return - end - local parent = self:getParent() - if parent and parent:getClassName() ~= 'UIMiniWindowContainer' then - self:raise() - end + if not focused then return end + local parent = self:getParent() + if parent and parent:getClassName() ~= 'UIMiniWindowContainer' then + self:raise() + end end function UIMiniWindow:onHeightChange(height) - if not self:isOn() then - self:setSettings({ - height = height - }) - end - self:fitOnParent() + if not self:isOn() then + self:setSettings({ height = height }) + end + self:fitOnParent() end function UIMiniWindow:getSettings(name) - if not self.save then - return nil - end - local char = g_game.getCharacterName() - if not char or #char == 0 then - return nil - end - - local settings = g_settings.getNode('CharMiniWindows') - if settings then - local selfSettings = settings[char][self:getId()] - if selfSettings then - return selfSettings[name] - end + if not self.save then return nil end + local settings = g_settings.getNode('MiniWindows') + if settings then + local selfSettings = settings[self:getId()] + if selfSettings then + return selfSettings[name] end - return nil + end + return nil end function UIMiniWindow:setSettings(data) - if not self.save then - return - end - local char = g_game.getCharacterName() - if not char or #char == 0 then - return - end + if not self.save then return end - local settings = g_settings.getNode('CharMiniWindows') - if not settings then - settings = {} - end - if not settings[char] then - settings[char] = {} - end + local settings = g_settings.getNode('MiniWindows') + if not settings then + settings = {} + end - local id = self:getId() - if not settings[char][id] then - settings[char][id] = {} - end + local id = self:getId() + if not settings[id] then + settings[id] = {} + end - for key, value in pairs(data) do - settings[char][id][key] = value - end + for key, value in pairs(data) do + settings[id][key] = value + end - g_settings.setNode('CharMiniWindows', settings) + g_settings.setNode('MiniWindows', settings) end function UIMiniWindow:eraseSettings(data) - if not self.save then - return - end - local char = g_game.getCharacterName() - if not char or #char == 0 then - return - end + if not self.save then return end - local settings = g_settings.getNode('CharMiniWindows') - if not settings then - settings = {} - end - if not settings[char] then - settings[char] = {} - end + local settings = g_settings.getNode('MiniWindows') + if not settings then + settings = {} + end - local id = self:getId() - if not settings[char][id] then - settings[char][id] = {} - end + local id = self:getId() + if not settings[id] then + settings[id] = {} + end - for key, value in pairs(data) do - settings[char][id][key] = nil - end + for key, value in pairs(data) do + settings[id][key] = nil + end - g_settings.setNode('CharMiniWindows', settings) + g_settings.setNode('MiniWindows', settings) +end + +function UIMiniWindow:clearSettings() + if not self.save then return end + + local settings = g_settings.getNode('MiniWindows') + if not settings then + settings = {} + end + + local id = self:getId() + settings[id] = {} + + g_settings.setNode('MiniWindows', settings) end function UIMiniWindow:saveParent(parent) - local parent = self:getParent() - if parent then - if parent:getClassName() == 'UIMiniWindowContainer' then - parent:saveChildren() - else - self:saveParentPosition(parent:getId(), self:getPosition()) - end + local parent = self:getParent() + if parent then + if parent:getClassName() == 'UIMiniWindowContainer' then + parent:saveChildren() + else + self:saveParentPosition(parent:getId(), self:getPosition()) end + end end function UIMiniWindow:saveParentPosition(parentId, position) - local selfSettings = {} - selfSettings.parentId = parentId - selfSettings.position = pointtostring(position) - self:setSettings(selfSettings) + local selfSettings = {} + selfSettings.parentId = parentId + selfSettings.position = pointtostring(position) + self:setSettings(selfSettings) end function UIMiniWindow:saveParentIndex(parentId, index) - local selfSettings = {} - selfSettings.parentId = parentId - selfSettings.index = index - self:setSettings(selfSettings) - self.miniIndex = index + local selfSettings = {} + selfSettings.parentId = parentId + selfSettings.index = index + self:setSettings(selfSettings) + self.miniIndex = index end function UIMiniWindow:disableResize() - self:getChildById('bottomResizeBorder'):disable() + self:getChildById('bottomResizeBorder'):disable() end function UIMiniWindow:enableResize() - self:getChildById('bottomResizeBorder'):enable() + self:getChildById('bottomResizeBorder'):enable() end function UIMiniWindow:fitOnParent() - local parent = self:getParent() - if self:isVisible() and parent and parent:getClassName() == 'UIMiniWindowContainer' then - parent:fitAll(self) - end + local parent = self:getParent() + if self:isVisible() and parent and parent:getClassName() == 'UIMiniWindowContainer' then + parent:fitAll(self) + end end function UIMiniWindow:setParent(parent, dontsave) - UIWidget.setParent(self, parent) - if not dontsave then - self:saveParent(parent) - end - self:fitOnParent() + UIWidget.setParent(self, parent) + if not dontsave then + self:saveParent(parent) + end + self:fitOnParent() end function UIMiniWindow:setHeight(height) - UIWidget.setHeight(self, height) - signalcall(self.onHeightChange, self, height) + UIWidget.setHeight(self, height) + signalcall(self.onHeightChange, self, height) end function UIMiniWindow:setContentHeight(height) - local contentsPanel = self:getChildById('contentsPanel') - local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + - contentsPanel:getPaddingBottom() + local contentsPanel = self:getChildById('contentsPanel') + local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + + contentsPanel:getPaddingBottom() - local resizeBorder = self:getChildById('bottomResizeBorder') - resizeBorder:setParentSize(minHeight + height) + local resizeBorder = self:getChildById('bottomResizeBorder') + resizeBorder:setParentSize(minHeight + height) end function UIMiniWindow:setContentMinimumHeight(height) - local contentsPanel = self:getChildById('contentsPanel') - local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + - contentsPanel:getPaddingBottom() + local contentsPanel = self:getChildById('contentsPanel') + local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + + contentsPanel:getPaddingBottom() - local resizeBorder = self:getChildById('bottomResizeBorder') - resizeBorder:setMinimum(minHeight + height) + local resizeBorder = self:getChildById('bottomResizeBorder') + resizeBorder:setMinimum(minHeight + height) end function UIMiniWindow:setContentMaximumHeight(height) - local contentsPanel = self:getChildById('contentsPanel') - local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + - contentsPanel:getPaddingBottom() + local contentsPanel = self:getChildById('contentsPanel') + local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + + contentsPanel:getPaddingBottom() - local resizeBorder = self:getChildById('bottomResizeBorder') - resizeBorder:setMaximum(minHeight + height) + local resizeBorder = self:getChildById('bottomResizeBorder') + resizeBorder:setMaximum(minHeight + height) end function UIMiniWindow:getMinimumHeight() - local resizeBorder = self:getChildById('bottomResizeBorder') - return resizeBorder:getMinimum() + local resizeBorder = self:getChildById('bottomResizeBorder') + return resizeBorder:getMinimum() end function UIMiniWindow:getMaximumHeight() - local resizeBorder = self:getChildById('bottomResizeBorder') - return resizeBorder:getMaximum() -end - -function UIMiniWindow:modifyMaximumHeight(height) - local resizeBorder = self:getChildById('bottomResizeBorder') - local newHeight = resizeBorder:getMaximum() + height - local curHeight = self:getHeight() - resizeBorder:setMaximum(newHeight) - if newHeight < curHeight or newHeight - height == curHeight then - self:setHeight(newHeight) - end + local resizeBorder = self:getChildById('bottomResizeBorder') + return resizeBorder:getMaximum() end function UIMiniWindow:isResizeable() - local resizeBorder = self:getChildById('bottomResizeBorder') - return resizeBorder:isExplicitlyVisible() and resizeBorder:isEnabled() + local resizeBorder = self:getChildById('bottomResizeBorder') + return resizeBorder:isExplicitlyVisible() and resizeBorder:isEnabled() end diff --git a/modules/corelib/ui/uiminiwindowcontainer.lua b/modules/corelib/ui/uiminiwindowcontainer.lua index 7cb2d45dc4..8c472c0eb5 100644 --- a/modules/corelib/ui/uiminiwindowcontainer.lua +++ b/modules/corelib/ui/uiminiwindowcontainer.lua @@ -1,207 +1,257 @@ -- @docclass -UIMiniWindowContainer = extends(UIWidget, 'UIMiniWindowContainer') +UIMiniWindowContainer = extends(UIWidget, "UIMiniWindowContainer") function UIMiniWindowContainer.create() - local container = UIMiniWindowContainer.internalCreate() - container.scheduledWidgets = {} - container:setFocusable(false) - container:setPhantom(true) - return container + local container = UIMiniWindowContainer.internalCreate() + container.scheduledWidgets = {} + container:setFocusable(false) + container:setPhantom(true) + return container end -- TODO: connect to window onResize event -- TODO: try to resize another widget? -- TODO: try to find another panel? function UIMiniWindowContainer:fitAll(noRemoveChild) - if not self:isVisible() then - return - end + if not self:isVisible() then + return + end - if not noRemoveChild then - local children = self:getChildren() - if #children > 0 then - noRemoveChild = children[#children] - else - return - end + if not noRemoveChild then + local children = self:getChildren() + if #children > 0 then + noRemoveChild = children[#children] + else + return end + end - local sumHeight = 0 - local children = self:getChildren() - for i = 1, #children do - if children[i]:isVisible() then - sumHeight = sumHeight + children[i]:getHeight() - end + local sumHeight = 0 + local children = self:getChildren() + for i = 1, #children do + if children[i]:isVisible() then + sumHeight = sumHeight + children[i]:getHeight() end + end + + local selfHeight = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom()) + + if sumHeight <= selfHeight then + return + end - local selfHeight = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom()) + local removeChildren = {} + + -- try to resize noRemoveChild + local maximumHeight = selfHeight - (sumHeight - noRemoveChild:getHeight()) + if noRemoveChild:isResizeable() and noRemoveChild:getMinimumHeight() <= maximumHeight then + sumHeight = sumHeight - noRemoveChild:getHeight() + maximumHeight + addEvent(function() noRemoveChild:setHeight(maximumHeight) end) + end + + -- try to remove no-save widget + for i = #children, 1, -1 do if sumHeight <= selfHeight then - return + break end - local removeChildren = {} - - -- try to resize noRemoveChild - local maximumHeight = selfHeight - (sumHeight - noRemoveChild:getHeight()) - if noRemoveChild:isResizeable() and noRemoveChild:getMinimumHeight() <= maximumHeight then - sumHeight = sumHeight - noRemoveChild:getHeight() + maximumHeight - addEvent(function() - noRemoveChild:setHeight(maximumHeight) - end) + local child = children[i] + if child ~= noRemoveChild and child:isVisible() and not child.save then + local childHeight = child:getHeight() + sumHeight = sumHeight - childHeight + table.insert(removeChildren, child) end + end - -- try to remove no-save widget - for i = #children, 1, -1 do - if sumHeight <= selfHeight then - break - end + -- try to remove save widget, not forceOpen + for i = #children, 1, -1 do + if sumHeight <= selfHeight then + break + end - local child = children[i] - if child ~= noRemoveChild and not child.save then - local childHeight = child:getHeight() - sumHeight = sumHeight - childHeight - table.insert(removeChildren, child) - end + local child = children[i] + if child ~= noRemoveChild and child:isVisible() and not child.forceOpen then + local childHeight = child:getHeight() + sumHeight = sumHeight - childHeight + table.insert(removeChildren, child) end + end - -- try to remove save widget - for i = #children, 1, -1 do - if sumHeight <= selfHeight then - break - end + -- try to remove save widget + for i = #children, 1, -1 do + if sumHeight <= selfHeight then + break + end - local child = children[i] - if child ~= noRemoveChild and child:isVisible() then - local childHeight = child:getHeight() - sumHeight = sumHeight - childHeight - table.insert(removeChildren, child) - end + local child = children[i] + if child ~= noRemoveChild and child:isVisible() then + local childHeight = child:getHeight() - 50 + sumHeight = sumHeight - childHeight + table.insert(removeChildren, child) end + end - -- close widgets - for i = 1, #removeChildren do - removeChildren[i]:close() + -- close widgets + for i = 1, #removeChildren do + print(removeChildren[i]:getId()) + if removeChildren[i].forceOpen then + removeChildren[i]:minimize(true) + else + removeChildren[i]:close() end + end end function UIMiniWindowContainer:fits(child, minContentHeight, maxContentHeight) - local containerPanel = child:getChildById('contentsPanel') - local indispensableHeight = containerPanel:getMarginTop() + containerPanel:getMarginBottom() + - containerPanel:getPaddingTop() + containerPanel:getPaddingBottom() + local containerPanel = child:getChildById('contentsPanel') + local indispensableHeight = containerPanel:getMarginTop() + + containerPanel:getMarginBottom() + + containerPanel:getPaddingTop() + + containerPanel:getPaddingBottom() - local totalHeight = 0 - local children = self:getChildren() - for i = 1, #children do - if children[i]:isVisible() then - totalHeight = totalHeight + children[i]:getHeight() - end + local totalHeight = 0 + local children = self:getChildren() + for i = 1, #children do + if children[i]:isVisible() then + totalHeight = totalHeight + children[i]:getHeight() end + end - local available = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom()) - totalHeight + local available = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom()) - totalHeight - if maxContentHeight > 0 and available >= (maxContentHeight + indispensableHeight) then - return maxContentHeight + indispensableHeight - elseif available >= (minContentHeight + indispensableHeight) then - return available - else - return -1 - end + if maxContentHeight > 0 and available >= (maxContentHeight + indispensableHeight) then + return maxContentHeight + indispensableHeight + elseif available >= (minContentHeight + indispensableHeight) + then + return available + else + return -1 + end end function UIMiniWindowContainer:onDrop(widget, mousePos) - if widget.UIMiniWindowContainer then - local oldParent = widget:getParent() - if oldParent == self then - return true - end - - if oldParent then - oldParent:removeChild(widget) - end + if widget.UIMiniWindowContainer then + local oldParent = widget:getParent() + if oldParent == self then + return true + end - if widget.movedWidget then - local index = self:getChildIndex(widget.movedWidget) - self:insertChild(index + widget.movedIndex, widget) - else - self:addChild(widget) - end + if oldParent then + oldParent:removeChild(widget) + end - self:fitAll(widget) - return true + if widget.movedWidget then + local index = self:getChildIndex(widget.movedWidget) + self:insertChild(index + widget.movedIndex, widget) + else + self:addChild(widget) end + + self:fitAll(widget) + return true + end +end + +function UIMiniWindowContainer:moveTo(newPanel) + if not newPanel or newPanel == self then + return + end + local children = self:getChildByIndex(1) + while children do + newPanel:addChild(children) + children = self:getChildByIndex(1) + end + newPanel:fitAll() end function UIMiniWindowContainer:swapInsert(widget, index) - local oldParent = widget:getParent() - local oldIndex = self:getChildIndex(widget) + local oldParent = widget:getParent() + local oldIndex = self:getChildIndex(widget) - if oldParent == self and oldIndex ~= index then - local oldWidget = self:getChildByIndex(index) - if oldWidget then - self:removeChild(oldWidget) - self:insertChild(oldIndex, oldWidget) - end - self:removeChild(widget) - self:insertChild(index, widget) + if oldParent == self and oldIndex ~= index then + local oldWidget = self:getChildByIndex(index) + if oldWidget then + self:removeChild(oldWidget) + self:insertChild(oldIndex, oldWidget) end + self:removeChild(widget) + self:insertChild(index, widget) + end end function UIMiniWindowContainer:scheduleInsert(widget, index) - if index - 1 > self:getChildCount() then - if self.scheduledWidgets[index] then - pdebug('replacing scheduled widget id ' .. widget:getId()) - end - self.scheduledWidgets[index] = widget - else - local oldParent = widget:getParent() - if oldParent ~= self then - if oldParent then - oldParent:removeChild(widget) - end - self:insertChild(index, widget) - - while true do - local placed = false - for nIndex, nWidget in pairs(self.scheduledWidgets) do - if nIndex - 1 <= self:getChildCount() then - self:insertChild(nIndex, nWidget) - self.scheduledWidgets[nIndex] = nil - placed = true - break - end - end - if not placed then - break - end - end + if index - 1 > self:getChildCount() then + if self.scheduledWidgets[index] then + pdebug('replacing scheduled widget id ' .. widget:getId()) + end + self.scheduledWidgets[index] = widget + else + local oldParent = widget:getParent() + if oldParent ~= self then + if oldParent then + oldParent:removeChild(widget) + end + self:insertChild(index, widget) + while true do + local placed = false + for nIndex, nWidget in pairs(self.scheduledWidgets) do + if nIndex - 1 <= self:getChildCount() then + local oldParent = nWidget:getParent() + if oldParent ~= self then + if oldParent then + oldParent:removeChild(nWidget) + end + self:insertChild(nIndex, nWidget) + else + self:moveChildToIndex(nWidget, nIndex) + end + self.scheduledWidgets[nIndex] = nil + placed = true + break + end end + if not placed then break end + end end + end end function UIMiniWindowContainer:order() - local children = self:getChildren() - for i = 1, #children do - if not children[i].miniLoaded then - return - end - end + local children = self:getChildren() + for i = 1, #children do + if not children[i].miniLoaded then return end + end - for i = 1, #children do - if children[i].miniIndex then - self:swapInsert(children[i], children[i].miniIndex) - end + table.sort(children, function(a, b) + local indexA = a.miniIndex or a.autoOpen or 999 + local indexB = b.miniIndex or b.autoOpen or 999 + return indexA < indexB + end) + + self:reorderChildren(children) + local ignoreIndex = 0 + for i = 1, #children do + if children[i].save then + children[i].miniIndex = i - ignoreIndex + else + ignoreIndex = ignoreIndex + 1 end + end end function UIMiniWindowContainer:saveChildren() - local children = self:getChildren() - local ignoreIndex = 0 - for i = 1, #children do - if children[i].save then - children[i]:saveParentIndex(self:getId(), i - ignoreIndex) - else - ignoreIndex = ignoreIndex + 1 - end + local children = self:getChildren() + local ignoreIndex = 0 + for i = 1, #children do + if children[i].save then + children[i]:saveParentIndex(self:getId(), i - ignoreIndex) + else + ignoreIndex = ignoreIndex + 1 end + end +end + +function UIMiniWindowContainer:onGeometryChange() + self:fitAll() end diff --git a/modules/corelib/ui/uimovabletabbar.lua b/modules/corelib/ui/uimovabletabbar.lua index a5070e662d..34449c4f23 100644 --- a/modules/corelib/ui/uimovabletabbar.lua +++ b/modules/corelib/ui/uimovabletabbar.lua @@ -331,8 +331,14 @@ function UIMoveableTabBar:onStyleApply(styleName, styleNode) end end +function UIMoveableTabBar:clearTabs() + while #self.tabs > 0 do + self:removeTab(self.tabs[#self.tabs]) + end +end + function UIMoveableTabBar:removeTab(tab) - local tabTables = {self.tabs, self.preTabs, self.postTabs} + local tabTables = { self.tabs, self.preTabs, self.postTabs } local index = nil local tabTable = nil for i = 1, #tabTables do diff --git a/modules/corelib/ui/uitabbar.lua b/modules/corelib/ui/uitabbar.lua index ef5ff0d0c7..5a9722321b 100644 --- a/modules/corelib/ui/uitabbar.lua +++ b/modules/corelib/ui/uitabbar.lua @@ -181,3 +181,9 @@ function UITabBar:getTabsPanel() return tab.tabPanel end) end + +function UITabBar:clearTabs() + while #self.tabs > 0 do + self:removeTab(self.tabs[#self.tabs]) + end +end diff --git a/modules/game_bot/bot.lua b/modules/game_bot/bot.lua new file mode 100644 index 0000000000..ad7339b0fc --- /dev/null +++ b/modules/game_bot/bot.lua @@ -0,0 +1,799 @@ +botWindow = nil +botButton = nil +contentsPanel = nil +editWindow = nil + +local checkEvent = nil + +local botStorage = {} +local botStorageFile = nil +local botWebSockets = {} +local botMessages = nil +local botTabs = nil +local botExecutor = nil + +local configList = nil +local enableButton = nil +local executeEvent = nil +local statusLabel = nil + +local configManagerUrl = "http://otclient.ovh/configs.php" + +function init() + dofile("executor") + + g_ui.importStyle("ui/basic.otui") + g_ui.importStyle("ui/panels.otui") + g_ui.importStyle("ui/config.otui") + g_ui.importStyle("ui/icons.otui") + g_ui.importStyle("ui/container.otui") + + connect(g_game, { + onGameStart = online, + onGameEnd = offline, + }) + + initCallbacks() + + botButton = modules.client_topmenu.addRightGameToggleButton('botButton', tr('Bot'), '/images/topbuttons/bot', toggle, false, 99999) + botButton:setOn(false) + botButton:hide() + + botWindow = g_ui.loadUI('bot', modules.game_interface.getLeftPanel()) + botWindow:setup() + + contentsPanel = botWindow.contentsPanel + configList = contentsPanel.config + enableButton = contentsPanel.enableButton + statusLabel = contentsPanel.statusLabel + botMessages = contentsPanel.messages + botTabs = contentsPanel.botTabs + botTabs:setContentWidget(contentsPanel.botPanel) + + editWindow = g_ui.displayUI('edit') + editWindow:hide() + + if g_game.isOnline() then + clear() + online() + end +end + +function terminate() + save() + clear() + + disconnect(g_game, { + onGameStart = online, + onGameEnd = offline, + }) + + terminateCallbacks() + editWindow:destroy() + + botWindow:destroy() + botButton:destroy() +end + +function clear() + botExecutor = nil + removeEvent(checkEvent) + + -- optimization, callback is not used when not needed + g_game.enableTileThingLuaCallback(false) + + botTabs:clearTabs() + botTabs:setOn(false) + + botMessages:destroyChildren() + botMessages:updateLayout() + + for i, socket in pairs(botWebSockets) do + g_http.cancel(socket) + botWebSockets[i] = nil + end + + for i, widget in pairs(g_ui.getRootWidget():getChildren()) do + if widget.botWidget then + widget:destroy() + end + end + for i, widget in pairs(modules.game_interface.gameMapPanel:getChildren()) do + if widget.botWidget then + widget:destroy() + end + end + for _, widget in pairs({modules.game_interface.getRightPanel(), modules.game_interface.getLeftPanel()}) do + for i, child in pairs(widget:getChildren()) do + if child.botWidget then + child:destroy() + end + end + end + + local gameMapPanel = modules.game_interface.getMapPanel() + if gameMapPanel then + gameMapPanel:unlockVisibleFloor() + end + + if g_sounds then + g_sounds.getChannel(SoundChannels.Bot):stop() + end +end + + +function refresh() + if not g_game.isOnline() then return end + save() + clear() + + -- create bot dir + if not g_resources.directoryExists("/bot") then + g_resources.makeDir("/bot") + if not g_resources.directoryExists("/bot") then + return onError("Can't create bot directory in " .. g_resources.getWriteDir()) + end + end + + -- get list of configs + createDefaultConfigs() + local configs = g_resources.listDirectoryFiles("/bot", false, false) + + -- clean + configList.onOptionChange = nil + enableButton.onClick = nil + configList:clearOptions() + + -- select active config based on settings + local settings = g_settings.getNode('bot') or {} + local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion() + if settings[index] == nil then + settings[index] = { + enabled=false, + config="" + } + end + + -- init list and buttons + for i=1,#configs do + configList:addOption(configs[i]) + end + configList:setCurrentOption(settings[index].config) + if configList:getCurrentOption().text ~= settings[index].config then + settings[index].config = configList:getCurrentOption().text + settings[index].enabled = false + end + + enableButton:setOn(settings[index].enabled) + + configList.onOptionChange = function(widget) + settings[index].config = widget:getCurrentOption().text + g_settings.setNode('bot', settings) + g_settings.save() + refresh() + end + + enableButton.onClick = function(widget) + settings[index].enabled = not settings[index].enabled + g_settings.setNode('bot', settings) + g_settings.save() + refresh() + end + + if not g_game.isOnline() or not settings[index].enabled then + statusLabel:setOn(true) + statusLabel:setText("Status: disabled\nPress off button to enable") + return + end + + local configName = settings[index].config + + -- storage + botStorage = {} + + local path = "/bot/" .. configName .. "/storage/" + if not g_resources.directoryExists(path) then + g_resources.makeDir(path) + end + + botStorageFile = path.."profile_" .. g_settings.getNumber('profile') .. ".json" + if g_resources.fileExists(botStorageFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(botStorageFile)) + end) + if not status then + return onError("Error while reading storage (" .. botStorageFile .. "). To fix this problem you can delete storage.json. Details: " .. result) + end + botStorage = result + end + + -- run script + local status, result = pcall(function() + return executeBot(configName, botStorage, botTabs, message, save, refresh, botWebSockets) end + ) + if not status then + return onError(result) + end + + statusLabel:setOn(false) + botExecutor = result + check() +end + +function save() + if not botExecutor then + return + end + + local settings = g_settings.getNode('bot') or {} + local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion() + if settings[index] == nil then + return + end + + local status, result = pcall(function() + return json.encode(botStorage, 2) + end) + if not status then + return onError("Error while saving bot storage. Storage won't be saved. Details: " .. result) + end + + if result:len() > 100 * 1024 * 1024 then + return onError("Storage file is too big, above 100MB, it won't be saved") + end + + g_resources.writeFileContents(botStorageFile, result) +end + +function onMiniWindowClose() + botButton:setOn(false) +end + +function toggle() + if botButton:isOn() then + botWindow:close() + botButton:setOn(false) + else + botWindow:open() + botButton:setOn(true) + end +end + +function online() + botButton:show() + if not modules.client_profiles.ChangedProfile then + scheduleEvent(refresh, 20) + end +end + +function offline() + save() + clear() + botButton:hide() + editWindow:hide() +end + +function onError(message) + statusLabel:setOn(true) + statusLabel:setText("Error:\n" .. message) + g_logger.error("[BOT] " .. message) +end + +function edit() + local configs = g_resources.listDirectoryFiles("/bot", false, false) + editWindow.manager.upload.config:clearOptions() + for i=1,#configs do + editWindow.manager.upload.config:addOption(configs[i]) + end + editWindow.manager.download.config:setText("") + + editWindow:show() + editWindow:focus() + editWindow:raise() +end + +function createDefaultConfigs() + local defaultConfigFiles = g_resources.listDirectoryFiles("default_configs", false, false) + for i, config_name in ipairs(defaultConfigFiles) do + if not g_resources.directoryExists("/bot/" .. config_name) then + g_resources.makeDir("/bot/" .. config_name) + if not g_resources.directoryExists("/bot/" .. config_name) then + return onError("Can't create /bot/" .. config_name .. " directory in " .. g_resources.getWriteDir()) + end + + local defaultConfigFiles = g_resources.listDirectoryFiles("default_configs/" .. config_name, true, false) + for i, file in ipairs(defaultConfigFiles) do + local baseName = file:split("/") + baseName = baseName[#baseName] + 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 + +function uploadConfig() + local config = editWindow.manager.upload.config:getCurrentOption().text + local archive = compressConfig(config) + if not archive then + return displayErrorBox(tr("Config upload failed"), tr("Config %s is invalid (can't be compressed)", config)) + end + if archive:len() > 1024 * 1024 then + return displayErrorBox(tr("Config upload failed"), tr("Config %s is too big, maximum size is 1024KB. Now it has %s KB.", config, math.floor(archive:len() / 1024))) + end + + local infoBox = displayInfoBox(tr("Uploading config"), tr("Uploading config %s. Please wait.", config)) + + HTTP.postJSON(configManagerUrl .. "?config=" .. config:gsub("%s+", "_"), archive, function(data, err) + if infoBox then + infoBox:destroy() + end + if err or data["error"] then + return displayErrorBox(tr("Config upload failed"), tr("Error while upload config %s:\n%s", config, err or data["error"])) + end + displayInfoBox(tr("Succesful config upload"), tr("Config %s has been uploaded.\n%s", config, data["message"])) + end) +end + +function downloadConfig() + local hash = editWindow.manager.download.config:getText() + if hash:len() == 0 then + return displayErrorBox(tr("Config download error"), tr("Enter correct config hash")) + end + local infoBox = displayInfoBox(tr("Downloading config"), tr("Downloading config with hash %s. Please wait.", hash)) + HTTP.download(configManagerUrl .. "?hash=" .. hash, hash .. ".zip", function(path, checksum, err) + if infoBox then + infoBox:destroy() + end + if err then + return displayErrorBox(tr("Config download error"), tr("Config with hash %s cannot be downloaded", hash)) + end + modules.client_textedit.show("", { + title="Enter name for downloaded config", + description="Config with hash " .. hash .. " has been downloaded. Enter name for new config.\nWarning: if config with same name already exist, it will be overwritten!", + width=500 + }, function(configName) + decompressConfig(configName, "/downloads/" .. path) + refresh() + edit() + end) + end) +end + +function compressConfig(configName) + if not g_resources.directoryExists("/bot/" .. configName) then + return onError("Config " .. configName .. " doesn't exist") + end + local forArchive = {} + for _, file in ipairs(g_resources.listDirectoryFiles("/bot/" .. configName)) do + local fullPath = "/bot/" .. configName .. "/" .. file + if g_resources.fileExists(fullPath) then -- regular file + forArchive[file] = g_resources.readFileContents(fullPath) + else -- dir + for __, file2 in ipairs(g_resources.listDirectoryFiles(fullPath)) do + local fullPath2 = fullPath .. "/" .. file2 + if g_resources.fileExists(fullPath2) then -- regular file + forArchive[file .. "/" .. file2] = g_resources.readFileContents(fullPath2) + end + end + end + end + return g_resources.createArchive(forArchive) +end + +function decompressConfig(configName, archive) + if g_resources.directoryExists("/bot/" .. configName) then + g_resources.deleteFile("/bot/" .. configName) -- also delete dirs + end + local files = g_resources.decompressArchive(archive) + g_resources.makeDir("/bot/" .. configName) + if not g_resources.directoryExists("/bot/" .. configName) then + return onError("Can't create /bot/" .. configName .. " directory in " .. g_resources.getWriteDir()) + end + + for file, contents in pairs(files) do + local split = file:split("/") + split[#split] = nil -- remove file name + local dirPath = "/bot/" .. configName + for _, s in ipairs(split) do + dirPath = dirPath .. "/" .. s + if not g_resources.directoryExists(dirPath) then + g_resources.makeDir(dirPath) + if not g_resources.directoryExists(dirPath) then + return onError("Can't create " .. dirPath .. " directory in " .. g_resources.getWriteDir()) + end + end + end + g_resources.writeFileContents("/bot/" .. configName .. file, contents) + end +end + +-- Executor +function message(category, msg) + local widget = g_ui.createWidget('BotLabel', botMessages) + widget.added = g_clock.millis() + if category == 'error' then + widget:setText(msg) + widget:setColor("red") + g_logger.error("[BOT] " .. msg) + elseif category == 'warn' then + widget:setText(msg) + widget:setColor("yellow") + g_logger.warning("[BOT] " .. msg) + elseif category == 'info' then + widget:setText(msg) + widget:setColor("white") + g_logger.info("[BOT] " .. msg) + end + + if botMessages:getChildCount() > 5 then + botMessages:getFirstChild():destroy() + end +end + +function check() + removeEvent(checkEvent) + if not botExecutor then + return + end + + checkEvent = scheduleEvent(check, 10) + + local status, result = pcall(function() + return botExecutor.script() + end) + if not status then + botExecutor = nil -- critical + return onError(result) + end + + -- remove old messages + local widget = botMessages:getFirstChild() + if widget and widget.added + 5000 < g_clock.millis() then + widget:destroy() + end +end + +-- Callbacks +function initCallbacks() + connect(rootWidget, { + onKeyDown = botKeyDown, + onKeyUp = botKeyUp, + onKeyPress = botKeyPress + }) + + connect(g_game, { + onTalk = botOnTalk, + onTextMessage = botOnTextMessage, + onLoginAdvice = botOnLoginAdvice, + onUse = botOnUse, + onUseWith = botOnUseWith, + onChannelList = botChannelList, + onOpenChannel = botOpenChannel, + onCloseChannel = botCloseChannel, + onChannelEvent = botChannelEvent, + onImbuementWindow = botImbuementWindow, + onModalDialog = botModalDialog, + onAttackingCreatureChange = botAttackingCreatureChange, + onAddItem = botContainerAddItem, + onRemoveItem = botContainerRemoveItem, + onGameEditText = botGameEditText, + onSpellCooldown = botSpellCooldown, + onSpellGroupCooldown = botGroupSpellCooldown + }) + + connect(Tile, { + onAddThing = botAddThing, + onRemoveThing = botRemoveThing + }) + + connect(Creature, { + onAppear = botCreatureAppear, + onDisappear = botCreatureDisappear, + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange, + onTurn = botCreatureTurn, + onWalk = botCreatureWalk, + }) + + connect(LocalPlayer, { + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange, + onTurn = botCreatureTurn, + onWalk = botCreatureWalk, + onManaChange = botManaChange, + onStatesChange = botStatesChange, + onInventoryChange = botInventoryChange + }) + + connect(Container, { + onOpen = botContainerOpen, + onClose = botContainerClose, + onUpdateItem = botContainerUpdateItem, + onAddItem = botContainerAddItem, + onRemoveItem = botContainerRemoveItem, + }) + + connect(g_map, { + onMissle = botOnMissle, + onAnimatedText = botOnAnimatedText, + onStaticText = botOnStaticText + }) +end + +function terminateCallbacks() + disconnect(rootWidget, { + onKeyDown = botKeyDown, + onKeyUp = botKeyUp, + onKeyPress = botKeyPress + }) + + disconnect(g_game, { + onTalk = botOnTalk, + onTextMessage = botOnTextMessage, + onLoginAdvice = botOnLoginAdvice, + onUse = botOnUse, + onUseWith = botOnUseWith, + onChannelList = botChannelList, + onOpenChannel = botOpenChannel, + onCloseChannel = botCloseChannel, + onChannelEvent = botChannelEvent, + onImbuementWindow = botImbuementWindow, + onModalDialog = botModalDialog, + onAttackingCreatureChange = botAttackingCreatureChange, + onGameEditText = botGameEditText, + onSpellCooldown = botSpellCooldown, + onSpellGroupCooldown = botGroupSpellCooldown + }) + + disconnect(Tile, { + onAddThing = botAddThing, + onRemoveThing = botRemoveThing + }) + + disconnect(Creature, { + onAppear = botCreatureAppear, + onDisappear = botCreatureDisappear, + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange, + onTurn = botCreatureTurn, + onWalk = botCreatureWalk, + }) + + disconnect(LocalPlayer, { + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange, + onTurn = botCreatureTurn, + onWalk = botCreatureWalk, + onManaChange = botManaChange, + onStatesChange = botStatesChange, + onInventoryChange = botInventoryChange + }) + + disconnect(Container, { + onOpen = botContainerOpen, + onClose = botContainerClose, + onUpdateItem = botContainerUpdateItem, + onAddItem = botContainerAddItem, + onRemoveItem = botContainerRemoveItem + }) + + disconnect(g_map, { + onMissle = botOnMissle, + onAnimatedText = botOnAnimatedText, + onStaticText = botOnStaticText + }) +end + +function safeBotCall(func) + local status, result = pcall(func) + if not status then + onError(result) + end +end + +function botKeyDown(widget, keyCode, keyboardModifiers) + if botExecutor == nil then return false end + if keyCode == KeyUnknown then return end + safeBotCall(function() botExecutor.callbacks.onKeyDown(keyCode, keyboardModifiers) end) +end + +function botKeyUp(widget, keyCode, keyboardModifiers) + if botExecutor == nil then return false end + if keyCode == KeyUnknown then return end + safeBotCall(function() botExecutor.callbacks.onKeyUp(keyCode, keyboardModifiers) end) +end + +function botKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks) + if botExecutor == nil then return false end + if keyCode == KeyUnknown then return end + safeBotCall(function() botExecutor.callbacks.onKeyPress(keyCode, keyboardModifiers, autoRepeatTicks) end) +end + +function botOnTalk(name, level, mode, text, channelId, pos) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onTalk(name, level, mode, text, channelId, pos) end) +end + +function botOnTextMessage(mode, text) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onTextMessage(mode, text) end) +end + +function botOnLoginAdvice(message) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onLoginAdvice(message) end) +end + +function botAddThing(tile, thing) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onAddThing(tile, thing) end) +end + +function botRemoveThing(tile, thing) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onRemoveThing(tile, thing) end) +end + +function botCreatureAppear(creature) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreatureAppear(creature) end) +end + +function botCreatureDisappear(creature) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreatureDisappear(creature) end) +end + +function botCreaturePositionChange(creature, newPos, oldPos) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreaturePositionChange(creature, newPos, oldPos) end) +end + +function botCraetureHealthPercentChange(creature, healthPercent) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreatureHealthPercentChange(creature, healthPercent) end) +end + +function botOnUse(pos, itemId, stackPos, subType) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onUse(pos, itemId, stackPos, subType) end) +end + +function botOnUseWith(pos, itemId, target, subType) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onUseWith(pos, itemId, target, subType) end) +end + +function botContainerOpen(container, previousContainer) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onContainerOpen(container, previousContainer) end) +end + +function botContainerClose(container) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onContainerClose(container) end) +end + +function botContainerUpdateItem(container, slot, item, oldItem) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onContainerUpdateItem(container, slot, item, oldItem) end) +end + +function botOnMissle(missle) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onMissle(missle) end) +end + +function botOnAnimatedText(thing, text) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onAnimatedText(thing, text) end) +end + +function botOnStaticText(thing, text) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onStaticText(thing, text) end) +end + +function botChannelList(channels) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onChannelList(channels) end) +end + +function botOpenChannel(channelId, name) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onOpenChannel(channelId, name) end) +end + +function botCloseChannel(channelId) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCloseChannel(channelId) end) +end + +function botChannelEvent(channelId, name, event) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onChannelEvent(channelId, name, event) end) +end + +function botCreatureTurn(creature, direction) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onTurn(creature, direction) end) +end + +function botCreatureWalk(creature, oldPos, newPos) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onWalk(creature, oldPos, newPos) end) +end + +function botImbuementWindow(itemId, slots, activeSlots, imbuements, needItems) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onImbuementWindow(itemId, slots, activeSlots, imbuements, needItems) end) +end + +function botModalDialog(id, title, message, buttons, enterButton, escapeButton, choices, priority) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onModalDialog(id, title, message, buttons, enterButton, escapeButton, choices, priority) end) +end + +function botGameEditText(id, itemId, maxLength, text, writer, time) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onGameEditText(id, itemId, maxLength, text, writer, time) end) +end + +function botAttackingCreatureChange(creature, oldCreature) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onAttackingCreatureChange(creature,oldCreature) end) +end + +function botManaChange(player, mana, maxMana, oldMana, oldMaxMana) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onManaChange(player, mana, maxMana, oldMana, oldMaxMana) end) +end + +function botStatesChange(player, states, oldStates) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onStatesChange(player, states, oldStates) end) +end + +function botContainerAddItem(container, slot, item, oldItem) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onAddItem(container, slot, item, oldItem) end) +end + +function botContainerRemoveItem(container, slot, item) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onRemoveItem(container, slot, item) end) +end + +function botSpellCooldown(iconId, duration) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onSpellCooldown(iconId, duration) end) +end + +function botGroupSpellCooldown(iconId, duration) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onGroupSpellCooldown(iconId, duration) end) +end + +function botInventoryChange(player, slot, item, oldItem) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onInventoryChange(player, slot, item, oldItem) end) +end \ No newline at end of file diff --git a/modules/game_bot/bot.otmod b/modules/game_bot/bot.otmod new file mode 100644 index 0000000000..adaebf2778 --- /dev/null +++ b/modules/game_bot/bot.otmod @@ -0,0 +1,8 @@ +Module + name: game_bot + description: Advanced OTClientV8 Bot + author: otclient@otclient.ovh + sandboxed: true + scripts: [ bot ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_bot/bot.otui b/modules/game_bot/bot.otui new file mode 100644 index 0000000000..50bc0a594f --- /dev/null +++ b/modules/game_bot/bot.otui @@ -0,0 +1,130 @@ +BotTabBar < TabBar + tab-spacing: 1 + margin-left: 1 + margin-right: 1 + height: 20 + + $on: + visible: true + margin-top: 2 + + $!on: + visible: false + margin-top: -20 + +BotTabBarPanel < TabBarPanel +BotTabBarButton < TabBarButton + padding: 4 + padding-right: 5 + text-horizontal-auto-resize: true + $!first: + margin-left: 0 + +MiniWindow + id: botWindow + !text: tr('Bot') + height: 600 + icon: /images/topbuttons/bot + @onClose: modules.game_bot.onMiniWindowClose() + &save: true + &autoOpen: 10 + + MiniWindowContents + ComboBox + id: config + &menuScroll: true + &menuHeight: 450 + &menuScrollStep: 100 + &parentWidth: true + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + margin-left: 2 + margin-right: 75 + text-offset: 3 0 + + Button + id: editConfig + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + !text: tr('Edit') + @onClick: modules.game_bot.edit() + margin-left: 3 + margin-right: 37 + + Button + id: enableButton + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + margin-right: 2 + + $on: + text: On + color: #00AA00 + + $!on: + text: Off + color: #FF0000 + + Label + id: statusLabel + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-wrap: true + text-auto-resize: true + text-align: center + !text: tr('Status: waiting') + margin-left: 3 + margin-right: 3 + + $on: + margin-top: 3 + + $!on: + text: + margin-top: -13 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + margin-left: 2 + margin-right: 2 + + Panel + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + id: messages + layout: + type: verticalBox + fit-children: true + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-left: 2 + margin-right: 2 + + BotTabBar + id: botTabs + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-right: -20 + + Panel + id: botPanel + margin-top: 2 + anchors.top: prev.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right diff --git a/modules/game_bot/configs.png b/modules/game_bot/configs.png new file mode 100644 index 0000000000000000000000000000000000000000..1e7ff71e1212f255bd8ea4bddf7ec37a84a3c4a9 GIT binary patch literal 20453 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 literal 0 HcmV?d00001 diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot.lua new file mode 100644 index 0000000000..a32569308b --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot.lua @@ -0,0 +1,39 @@ +-- 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/config.otui") +importStyle("/cavebot/editor.otui") +importStyle("/cavebot/supply.otui") +dofile("/cavebot/actions.lua") +dofile("/cavebot/config.lua") +dofile("/cavebot/editor.lua") +dofile("/cavebot/example_functions.lua") +dofile("/cavebot/recorder.lua") +dofile("/cavebot/walking.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") diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/actions.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/actions.lua new file mode 100644 index 0000000000..64d728819f --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/actions.lua @@ -0,0 +1,264 @@ +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 + if type(value) == 'number' then + value = tostring(value) + 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]+),?\\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 CaveBot.Config.get("mapClick") then + if retries >= 5 then + return false -- tried 5 times, can't get there + end + else + if retries >= 100 then + return false -- tried 100 times, can't get there + end + end + + local precision = tonumber(pos[1][5]) + 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) <= (precision or 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 + + -- try to find path, don't ignore creatures, don't ignore fields + if not CaveBot.Config.get("ignoreFields") and CaveBot.walkTo(pos, 40) then + return "retry" + end + + -- try to find path, don't ignore creatures, ignore fields + if CaveBot.walkTo(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 CaveBot.walkTo(pos, 50, { ignoreNonPathable = true, precision = precison }) then + return "retry" + end + end + + if not CaveBot.Config.get("mapClick") and retries >= 5 then + return false + end + + if CaveBot.Config.get("skipBlocked") then + return false + end + + -- everything else failed, try to walk ignoring creatures, maybe will work + CaveBot.walkTo(pos, 40, { ignoreNonPathable = true, precision = 1, ignoreCreatures = true }) + 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(CaveBot.Config.get("useDelay") + CaveBot.Config.get("ping")) + 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(CaveBot.Config.get("useDelay") + CaveBot.Config.get("ping")) + return true +end) + +CaveBot.registerAction("say", "#FF55FF", function(value, retries, prev) + say(value) + return true +end) diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.lua new file mode 100644 index 0000000000..a00ee716fb --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.lua @@ -0,0 +1,224 @@ +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 +if CaveBot.Config then + CaveBot.Config.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() and not TargetBot.isCaveBotActionAllowed() then + CaveBot.resetWalking() + return -- target bot or looting is working, wait + end + + if CaveBot.doWalking() then + return -- executing walking + 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() + CaveBot.resetWalking() + 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) + +-- 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 + + local cavebotConfig = nil + for k,v in ipairs(data) do + if type(v) == "table" and #v == 2 then + if v[1] == "config" 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 + cavebotConfig = result + end + elseif 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 + + CaveBot.Config.onConfigChange(name, enabled, cavebotConfig) + + actionRetries = 0 + CaveBot.resetWalking() + 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 + +ui.showConfig.onClick = function() + if not CaveBot.Config then return end + if ui.showConfig:isOn() then + CaveBot.Config.hide() + ui.showConfig:setOn(false) + else + CaveBot.Config.show() + ui.showConfig: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 = math.max(cavebotMacro.delay or 0, 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 + + if CaveBot.Config then + table.insert(data, {"config", json.encode(CaveBot.Config.save())}) + 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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.otui b/modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.otui new file mode 100644 index 0000000000..b92ed05fb9 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/cavebot.otui @@ -0,0 +1,58 @@ +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 + + BotSwitch + id: showConfig + margin-top: 2 + + $on: + text: Hide config + + $!on: + text: Show config \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/config.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/config.lua new file mode 100644 index 0000000000..549f663c2d --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/config.lua @@ -0,0 +1,94 @@ +-- config for bot +CaveBot.Config = {} +CaveBot.Config.values = {} +CaveBot.Config.default_values = {} +CaveBot.Config.value_setters = {} + +CaveBot.Config.setup = function() + CaveBot.Config.ui = UI.createWidget("CaveBotConfigPanel") + local ui = CaveBot.Config.ui + local add = CaveBot.Config.add + + add("ping", "Server ping", 100) + add("walkDelay", "Walk delay", 10) + add("mapClick", "Use map click", false) + add("mapClickDelay", "Map click delay", 100) + add("ignoreFields", "Ignore fields", false) + add("skipBlocked", "Skip blocked path", false) + add("useDelay", "Delay after use", 400) +end + +CaveBot.Config.show = function() + CaveBot.Config.ui:show() +end + +CaveBot.Config.hide = function() + CaveBot.Config.ui:hide() +end + +CaveBot.Config.onConfigChange = function(configName, isEnabled, configData) + for k, v in pairs(CaveBot.Config.default_values) do + CaveBot.Config.value_setters[k](v) + end + if not configData then return end + for k, v in pairs(configData) do + if CaveBot.Config.value_setters[k] then + CaveBot.Config.value_setters[k](v) + end + end +end + +CaveBot.Config.save = function() + return CaveBot.Config.values +end + +CaveBot.Config.add = function(id, title, defaultValue) + if CaveBot.Config.values[id] then + return error("Duplicated config key: " .. id) + end + + local panel + local setter -- sets value + if type(defaultValue) == "number" then + panel = UI.createWidget("CaveBotConfigNumberValuePanel", CaveBot.Config.ui) + setter = function(value) + CaveBot.Config.values[id] = value + panel.value:setText(value, true) + end + setter(defaultValue) + panel.value.onTextChange = function(widget, newValue) + newValue = tonumber(newValue) + if newValue then + CaveBot.Config.values[id] = newValue + CaveBot.save() + end + end + elseif type(defaultValue) == "boolean" then + panel = UI.createWidget("CaveBotConfigBooleanValuePanel", CaveBot.Config.ui) + setter = function(value) + CaveBot.Config.values[id] = value + panel.value:setOn(value, true) + end + setter(defaultValue) + panel.value.onClick = function(widget) + widget:setOn(not widget:isOn()) + CaveBot.Config.values[id] = widget:isOn() + CaveBot.save() + end + else + return error("Invalid default value of config for key " .. id .. ", should be number or boolean") + end + + panel.title:setText(tr(title) .. ":") + + CaveBot.Config.value_setters[id] = setter + CaveBot.Config.values[id] = defaultValue + CaveBot.Config.default_values[id] = defaultValue +end + +CaveBot.Config.get = function(id) + if CaveBot.Config.values[id] == nil then + return error("Invalid CaveBot.Config.get, id: " .. id) + end + return CaveBot.Config.values[id] +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/config.otui b/modules/game_bot/default_configs/cavebot_1.3/cavebot/config.otui new file mode 100644 index 0000000000..21d479dd60 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/config.otui @@ -0,0 +1,57 @@ +CaveBotConfigPanel < Panel + id: cavebotEditor + visible: false + + layout: + type: verticalBox + fit-children: true + + HorizontalSeparator + margin-top: 5 + + Label + text-align: center + text: CaveBot Config + margin-top: 5 + +CaveBotConfigNumberValuePanel < Panel + height: 20 + margin-top: 5 + + BotTextEdit + id: value + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + margin-right: 5 + width: 50 + + Label + id: title + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 + +CaveBotConfigBooleanValuePanel < Panel + height: 20 + margin-top: 5 + + BotSwitch + id: value + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + margin-right: 5 + width: 50 + + $on: + text: On + + $!on: + text: Off + + Label + id: title + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/depositer.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/depositer.lua new file mode 100644 index 0000000000..d397c47e7d --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/depositer.lua @@ -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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.lua new file mode 100644 index 0000000000..1fb4e766d8 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.lua @@ -0,0 +1,174 @@ +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.actionList:focusChild(widget) + 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("edit", function() + local action = CaveBot.actionList:getFocusedChild() + if not action or not action.onDoubleClick then return end + action.onDoubleClick(action) + 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("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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.otui b/modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.otui new file mode 100644 index 0000000000..d11288c64d --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/editor.otui @@ -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/") diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/example_functions.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/example_functions.lua new file mode 100644 index 0000000000..556129c11d --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/example_functions.lua @@ -0,0 +1,90 @@ +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" +]]) + +addExampleFunction("Disable TargetBot", [[ +TargetBot.setOff() +return true +]]) + +addExampleFunction("Enable TargetBot", [[ +TargetBot.setOn() +return true +]]) + +addExampleFunction("Enable TargetBot luring", [[ +TargetBot.enableLuring() +return true +]]) + +addExampleFunction("Disable TargetBot luring", [[ +TargetBot.disableLuring() +return true +]]) + +addExampleFunction("Logout", [[ +g_game.safeLogout() +delay(1000) +return "retry" +]]) \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/extension_template.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/extension_template.lua new file mode 100644 index 0000000000..d015f11beb --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/extension_template.lua @@ -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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/recorder.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/recorder.lua new file mode 100644 index 0000000000..27206ba14b --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/recorder.lua @@ -0,0 +1,65 @@ +-- 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", 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 .. "," .. targetPos.x .. "," .. targetPos.y .. "," .. targetPos.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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.lua new file mode 100644 index 0000000000..b3cd4ca207 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.lua @@ -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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.otui b/modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.otui new file mode 100644 index 0000000000..83c76ac794 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/supply.otui @@ -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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot/walking.lua b/modules/game_bot/default_configs/cavebot_1.3/cavebot/walking.lua new file mode 100644 index 0000000000..c8a713366b --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot/walking.lua @@ -0,0 +1,93 @@ +-- walking +local expectedDirs = {} +local isWalking = {} +local walkPath = {} +local walkPathIter = 0 + +CaveBot.resetWalking = function() + expectedDirs = {} + walkPath = {} + isWalking = false +end + +CaveBot.doWalking = function() + if CaveBot.Config.get("mapClick") then + return false + end + if #expectedDirs == 0 then + return false + end + if #expectedDirs >= 3 then + CaveBot.resetWalking() + end + local dir = walkPath[walkPathIter] + if dir then + g_game.walk(dir, false) + table.insert(expectedDirs, dir) + walkPathIter = walkPathIter + 1 + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + return true + end + return false +end + +-- called when player position has been changed (step has been confirmed by server) +onPlayerPositionChange(function(newPos, oldPos) + if not oldPos or not newPos then return end + + local dirs = {{NorthWest, North, NorthEast}, {West, 8, East}, {SouthWest, South, SouthEast}} + local dir = dirs[newPos.y - oldPos.y + 2] + if dir then + dir = dir[newPos.x - oldPos.x + 2] + end + if not dir then + dir = 8 -- 8 is invalid dir, it's fine + end + + if not isWalking or not expectedDirs[1] then + -- some other walk action is taking place (for example use on ladder), wait + walkPath = {} + CaveBot.delay(CaveBot.Config.get("ping") + player:getStepDuration(false, dir) + 150) + return + end + + if expectedDirs[1] ~= dir then + if CaveBot.Config.get("mapClick") then + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + else + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + player:getStepDuration(false, dir)) + end + return + end + + table.remove(expectedDirs, 1) + if CaveBot.Config.get("mapClick") and #expectedDirs > 0 then + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + player:getStepDuration(false, dir)) + end +end) + +CaveBot.walkTo = function(dest, maxDist, params) + local path = getPath(player:getPosition(), dest, maxDist, params) + if not path or not path[1] then + return false + end + local dir = path[1] + + if CaveBot.Config.get("mapClick") then + local ret = autoWalk(path) + if ret then + isWalking = true + expectedDirs = path + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + math.max(CaveBot.Config.get("ping") + player:getStepDuration(false, dir), player:getStepDuration(false, dir) * 2)) + end + return ret + end + + g_game.walk(dir, false) + isWalking = true + walkPath = path + walkPathIter = 2 + expectedDirs = { dir } + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + return true +end diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/config_name.cfg b/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/config_name.cfg new file mode 100644 index 0000000000..abff9d8b0b --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/config_name.cfg @@ -0,0 +1,23 @@ +goto:1033,1044,7 +goto:1031,1038,7 +goto:1030,1038,7 +goto:1157,985,7 +goto:1161,981,7 +goto:1033,1042,7 +goto:1034,1038,7 +goto:1169,985,7 +goto:1175,985,7 +goto:1176,983,7 +goto:756,846,7 +goto:756,846,7 +config:{"walk":100,"walk2":false} +extensions:[[ +{ + "Depositer": [ + + ], + "Supply": [ + + ] +} +]] diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/fast_walking.cfg b/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/fast_walking.cfg new file mode 100644 index 0000000000..a5bb1304cf --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/fast_walking.cfg @@ -0,0 +1,13 @@ +goto:84,112,6 +goto:95,108,6 +config:{"mapClickDelay":100,"walkDelay":10,"ping":250,"ignoreFields":false,"useDelay":400,"mapClick":false} +extensions:[[ +{ + "Depositer": [ + + ], + "Supply": [ + + ] +} +]] diff --git a/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/test_src.cfg b/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/test_src.cfg new file mode 100644 index 0000000000..1049ce7ac2 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/cavebot_configs/test_src.cfg @@ -0,0 +1,104 @@ +goto:93,129,7 +goto:96,123,7 +goto:96,117,7 +goto:101,114,7 +goto:95,111,6 +goto:89,111,6 +goto:83,108,6 +goto:80,102,6 +goto:80,96,6 +goto:85,90,6 +goto:88,92,6 +goto:91,86,7 +goto:97,85,7 +goto:103,84,7 +function:[[ + +TargetBot.enableLuring() + +return true + + +]] +goto:109,79,7 +goto:112,79,7 +goto:112,79,8 +function:[[ + +TargetBot.disableLuring() + +return true + + +]] +goto:112,79,7 +goto:106,84,8 +goto:100,80,8 +goto:100,74,8 +goto:99,80,8 +goto:105,83,8 +function:[[ +TargetBot.setOff() +return true +]] +goto:111,82,8 +goto:112,79,8 +goto:106,82,7 +goto:100,85,7 +goto:94,85,7 +goto:91,91,7 +goto:89,92,7 +goto:83,90,6 +function:[[ +TargetBot.setOff() +return true +]] +goto:77,94,6 +goto:75,95,6 +goto:69,96,7 +goto:63,100,7 +goto:61,102,7 +goto:62,96,8 +use:61,102,8 +goto:62,101,8 +goto:68,99,7 +goto:74,95,7 +goto:75,95,7 +goto:79,101,6 +goto:81,107,6 +goto:87,109,6 +goto:93,112,6 +function:[[ + +TargetBot.disableLuring() + +return true + + +]] +goto:99,116,6 +use:102,114,6 +goto:101,115,6 +use:100,116,5 +goto:101,115,5 +goto:100,116,4 +goto:102,114,5 +goto:101,114,6 +goto:96,120,7 +goto:95,126,7 +function:[[ +g_game.safeLogout() +delay(1000) +return "retry" +]] +config:{"useDelay":400,"mapClickDelay":100,"walkDelay":20,"ping":150,"ignoreFields":false,"skipBlocked":true,"mapClick":false} +extensions:[[ +{ + "Depositer": [ + + ], + "Supply": [ + + ] +} +]] diff --git a/modules/game_bot/default_configs/cavebot_1.3/hp.lua b/modules/game_bot/default_configs/cavebot_1.3/hp.lua new file mode 100644 index 0000000000..88b44d1b46 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/hp.lua @@ -0,0 +1,192 @@ +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, healingInfo.subType, player) -- sync spell with targetbot if available + else + local thing = g_things.getThingType(healingInfo.item) + local subType = g_game.getClientVersion() >= 860 and 0 or 1 + if thing and thing:isFluidContainer() then + subType = healingInfo.subType + end + g_game.useInventoryItemWith(healingInfo.item, player, subType) + end + end + end) + healingmacro.setOn(healingInfo.on) + + UI.DualScrollItemPanel(healingInfo, function(widget, newParams) + healingInfo = newParams + healingmacro.setOn(healingInfo.on and healingInfo.item > 100) + end) +end + +if g_game.getClientVersion() < 780 then + UI.Label("In old tibia potions & runes work only when you have backpack with them opened") +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 + if g_game.getClientVersion() < 780 then return end -- hotkey's dont work on old tibia + 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 too often + return + end + end + end + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/main.lua b/modules/game_bot/default_configs/cavebot_1.3/main.lua new file mode 100644 index 0000000000..57b7ca6ead --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/main.lua @@ -0,0 +1,22 @@ +-- main tab +VERSION = "1.3" + +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("http://otclient.net/") +end) + +UI.Button("Help & Tutorials", function() + g_platform.openUrl("http://bot.otclient.ovh/") +end) diff --git a/modules/game_bot/default_configs/cavebot_1.3/mwall_timer.lua b/modules/game_bot/default_configs/cavebot_1.3/mwall_timer.lua new file mode 100644 index 0000000000..6dc1ec48b0 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/mwall_timer.lua @@ -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) diff --git a/modules/game_bot/default_configs/cavebot_1.3/storage.json b/modules/game_bot/default_configs/cavebot_1.3/storage.json new file mode 100644 index 0000000000..afd37c98ee --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/storage.json @@ -0,0 +1,128 @@ +{ + "hpitem1": { + "max": 90, + "title": "HP%", + "subType": 0, + "item": 266, + "min": 51, + "on": false + }, + "foodItems": [ + { + "id": 3582, + "count": 1 + }, + { + "id": 3577, + "count": 1 + } + ], + "autoEquip": [ + { + "item1": 3052, + "title": "Auto Equip", + "item2": 3089, + "on": false, + "slot": 9 + }, + { + "item1": 0, + "title": "Auto Equip", + "item2": 0, + "on": false, + "slot": 0 + }, + { + "item1": 0, + "title": "Auto Equip", + "item2": 0, + "on": false, + "slot": 0 + }, + { + "item1": 0, + "title": "Auto Equip", + "item2": 0, + "on": false, + "slot": 0 + } + ], + "ingame_hotkeys": "singlehotkey(\"f1\", function()\nlocal shaders = {\"stars\", \"gold\", \"rainbow\", \"sweden\", \"brazil\", \"line\", \"3line\", \"circle\", \"outline\"}\nlocal p = 0\nfor i, c in pairs(getSpectators()) do\n c:setOutfitShader(shaders[1 + p % 10])\n p = p + 1\nend\nend)\n\nsinglehotkey(\"1\", function()\n for _, s in ipairs(getSpectators()) do\n if s:canShoot(3) then\n info(s:getName())\n else\n warn(s:getName())\n end\n end\nend)", + "healing2": { + "max": 50, + "title": "HP%", + "on": false, + "min": 1, + "text": "exura vita" + }, + "ingame_macros": "", + "hasteSpell": "utani hur", + "manaitem2": { + "max": 50, + "title": "MP%", + "subType": 0, + "item": 3157, + "min": 0, + "on": false + }, + "_configs": { + "cavebot_configs": { + "selected": "test_src", + "enabled": false + }, + "targetbot_configs": { + "enabled": false, + "selected": "config_name" + } + }, + "healing1": { + "max": 100, + "title": "HP%", + "on": false, + "min": 51, + "text": "exura" + }, + "dropItems": [ + { + "id": 283, + "count": 1 + }, + { + "id": 284, + "count": 1 + }, + { + "id": 285, + "count": 1 + } + ], + "_macros": { + "": false + }, + "manaitem1": { + "max": 90, + "title": "MP%", + "subType": 0, + "item": 268, + "min": 51, + "on": false + }, + "hpitem2": { + "max": 50, + "title": "HP%", + "subType": 0, + "item": 3160, + "min": 0, + "on": false + }, + "manaShield": "utamo vita", + "autoTradeMessage": "I'm using OTClientV8!", + "antiParalyze": "utani hur", + "manaTrain": { + "max": 100, + "title": "MP%", + "on": false, + "min": 80, + "text": "utevo lux" + } +} \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature.lua b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature.lua new file mode 100644 index 0000000000..d4dd545959 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature.lua @@ -0,0 +1,99 @@ + +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 = "" + for part in string.gmatch(config.name, "[^,]+") do + if config.regex:len() > 0 then + config.regex = config.regex .. "|" + end + config.regex = config.regex .. "^" .. part:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$" + end + 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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_attack.lua b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_attack.lua new file mode 100644 index 0000000000..048c07c65b --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_attack.lua @@ -0,0 +1,122 @@ +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() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) 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.useGroupAttackRune and config.groupAttackRune > 100 then + local creatures = g_map.getSpectatorsInRange(creature:getPosition(), false, config.groupRuneAttackRadius, config.groupRuneAttackRadius) + local playersAround = false + local monsters = 0 + for _, creature in ipairs(creatures) do + if not creature:isLocalPlayer() and creature:isPlayer() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) then + playersAround = true + elseif creature:isMonster() then + monsters = monsters + 1 + end + end + if monsters >= config.groupRuneAttackTargets and (not playersAround or config.groupAttackIgnorePlayers) then + if TargetBot.useAttackItem(config.groupAttackRune, 0, creature, config.groupRuneAttackDelay) 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, 0, creature, config.attackRuneDelay) then + return + end + end +end + +TargetBot.Creature.walk = function(creature, config, targets) + local cpos = creature:getPosition() + local pos = player:getPosition() + + local isTrapped = true + local pos = player:getPosition() + local dirs = {{-1,1}, {0,1}, {1,1}, {-1, 0}, {1, 0}, {-1, -1}, {0, -1}, {1, -1}} + for i=1,#dirs do + local tile = g_map.getTile({x=pos.x-dirs[i][1],y=pos.y-dirs[i][2],z=pos.z}) + if tile and tile:isWalkable(false) then + isTrapped = false + end + end + + -- luring + if TargetBot.canLure() and (config.lure or config.lureCavebot) and not (config.chase and creature:getHealthPercent() < 30) and not isTrapped then + local monsters = 0 + if targets < config.lureCount then + if config.lureCavebot then + return TargetBot.allowCaveBot(200) + else + local path = findPath(pos, cpos, 5, {ignoreNonPathable=true, precision=2}) + if path then + return TargetBot.walkTo(cpos, 10, {marginMin=5, marginMax=6, ignoreNonPathable=true}) + end + end + end + end + + local currentDistance = findPath(pos, cpos, 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(cpos, 10, {ignoreNonPathable=true, precision=1}) + end + elseif config.keepDistance then + if #currentDistance ~= config.keepDistanceRange and #currentDistance ~= config.keepDistanceRange + 1 then + return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1}) + end + end + + if config.avoidAttacks then + local diffx = cpos.x - pos.x + local diffy = cpos.y - pos.y + local candidates = {} + if math.abs(diffx) == 1 and diffy == 0 then + candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}} + elseif diffx == 0 and math.abs(diffy) == 1 then + candidates = {{x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x+1, y=pos.y, z=pos.z}} + end + for _, candidate in ipairs(candidates) do + local tile = g_map.getTile(candidate) + if tile and tile:isWalkable() then + return TargetBot.walkTo(candidate, 2, {ignoreNonPathable=true}) + end + end + end +end diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.lua b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.lua new file mode 100644 index 0000000000..8d92db0b9b --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.lua @@ -0,0 +1,113 @@ +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.content.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.content.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.content.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.content.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 = "" + for part in string.gmatch(newConfig.name, "[^,]+") do + if newConfig.regex:len() > 0 then + newConfig.regex = newConfig.regex .. "|" + end + newConfig.regex = newConfig.regex .. "^" .. part:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$" + end + + 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) + addScrollBar("groupRuneAttackTargets", "Min. targets for group rune attack", 1, 10, 2) + addScrollBar("groupRuneAttackRadius", "Radius of group rune attack", 1, 7, 1) + addScrollBar("groupRuneAttackDelay", "Group rune attack delay", 200, 60000, 5000) + + addCheckBox("chase", "Chase", true) + addCheckBox("keepDistance", "Keep Distance", false) + addCheckBox("dontLoot", "Don't loot", false) + addCheckBox("lure", "Lure", false) + addCheckBox("lureCavebot", "Lure using cavebot", false) + addCheckBox("avoidAttacks", "Avoid wave attacks", false) + + addCheckBox("useSpellAttack", "Use attack spell", false) + addTextEdit("attackSpell", "Attack spell", "") + addCheckBox("useRuneAttack", "Use attack rune", false) + addItem("attackRune", "Attack rune:", 0) + addCheckBox("useGroupAttack", "Use group attack spell", false) + addTextEdit("groupAttackSpell", "Group attack spell", "") + addCheckBox("useGroupAttackRune", "Use group attack rune", false) + addItem("groupAttackRune", "Group attack rune:", 0) + addCheckBox("groupAttackIgnorePlayers", "Ignore players in group attack", false) + addCheckBox("groupAttackIgnoreParty", "Ignore party in group attack", false) +end diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.otui b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.otui new file mode 100644 index 0000000000..24f3da6146 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_editor.otui @@ -0,0 +1,164 @@ +TargetBotCreatureEditorScrollBar < Panel + height: 28 + margin-top: 3 + + 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: 3 + 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: 630 + + $mobile: + height: 300 + + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-align: center + !text: tr('You can use * (any characters) and ? (any character) in target name') + + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-align: center + !text: tr('You can also enter multiple targets, separate them by ,') + + 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: + + VerticalScrollBar + id: contentScroll + anchors.top: name.bottom + anchors.right: parent.right + anchors.bottom: help.top + step: 28 + pixels-scroll: true + margin-right: -10 + margin-top: 5 + margin-bottom: 5 + + ScrollablePanel + id: content + anchors.top: name.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: help.top + vertical-scrollbar: contentScroll + margin-bottom: 10 + + Panel + id: left + anchors.top: parent.top + 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: parent.top + 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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_priority.lua b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_priority.lua new file mode 100644 index 0000000000..dcc2f81fed --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/creature_priority.lua @@ -0,0 +1,40 @@ +TargetBot.Creature.calculatePriority = function(creature, config, path) + -- config is based on creature_editor + local priority = 0 + + -- 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 + + -- add config priority + priority = priority + config.priority + + -- 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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.lua b/modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.lua new file mode 100644 index 0000000000..451450fc73 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.lua @@ -0,0 +1,299 @@ +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 not container:isContainer() then + table.remove(TargetBot.Looting.list, 1) + return true + end + + g_game.open(container) + waitTill = now + 1000 -- give it 1s to open + waitingForContainer = container:getId() + 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() and not itemsById[item:getId()] then + nextContainer = item + elseif itemsById[item:getId()] or (ui.everyItem:isOn() and not item:isContainer()) 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 = nextContainer:getId() + 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 config = TargetBot.Creature.calculateParams(creature, {}) -- return {craeture, config, danger, priority} + if not config.config or config.config.dontLoot 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) diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.otui b/modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.otui new file mode 100644 index 0000000000..97cb351d15 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/looting.otui @@ -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/") diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/target.lua b/modules/game_bot/default_configs/cavebot_1.3/targetbot/target.lua new file mode 100644 index 0000000000..adcd20e01d --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/target.lua @@ -0,0 +1,285 @@ +local targetbotMacro = nil +local config = nil +local lastAction = 0 +local cavebotAllowance = 0 +local lureEnabled = true + +-- 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) + elseif cavebotAllowance > now then + TargetBot.setStatus("Luring using CaveBot") + else + TargetBot.setStatus("Attacking") + if not lureEnabled then + TargetBot.setStatus("Attacking (luring off)") + end + 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 + lureEnabled = true +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.isCaveBotActionAllowed = function() + return cavebotAllowance > 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 + +TargetBot.allowCaveBot = function(time) + cavebotAllowance = now + time +end + +TargetBot.disableLuring = function() + lureEnabled = false +end + +TargetBot.enableLuring = function() + lureEnabled = true +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 lastItemUse = 0 +local lastRuneAttack = 0 + +TargetBot.useItem = function(item, subType, target, delay) + if not delay then delay = 200 end + if lastItemUse + delay < now then + local thing = g_things.getThingType(item) + if not thing or not thing:isFluidContainer() then + subType = g_game.getClientVersion() >= 860 and 0 or 1 + end + if g_game.getClientVersion() < 780 then + local tmpItem = g_game.findPlayerItem(item, subType) + if not tmpItem then return end + g_game.useWith(tmpItem, target, subType) -- using item from bp + else + g_game.useInventoryItemWith(item, target, subType) -- hotkey + end + lastItemUse = now + end +end + +TargetBot.useAttackItem = function(item, subType, target, delay) + if not delay then delay = 2000 end + if lastRuneAttack + delay < now then + local thing = g_things.getThingType(item) + if not thing or not thing:isFluidContainer() then + subType = g_game.getClientVersion() >= 860 and 0 or 1 + end + if g_game.getClientVersion() < 780 then + local tmpItem = g_game.findPlayerItem(item, subType) + if not tmpItem then return end + g_game.useWith(tmpItem, target, subType) -- using item from bp + else + g_game.useInventoryItemWith(item, target, subType) -- hotkey + end + lastRuneAttack = now + end +end + +TargetBot.canLure = function() + return lureEnabled +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/target.otui b/modules/game_bot/default_configs/cavebot_1.3/targetbot/target.otui new file mode 100644 index 0000000000..6e0e4eafa5 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/target.otui @@ -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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot/walking.lua b/modules/game_bot/default_configs/cavebot_1.3/targetbot/walking.lua new file mode 100644 index 0000000000..b256d6acf4 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot/walking.lua @@ -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 diff --git a/modules/game_bot/default_configs/cavebot_1.3/targetbot_configs/config_name.json b/modules/game_bot/default_configs/cavebot_1.3/targetbot_configs/config_name.json new file mode 100644 index 0000000000..e445631405 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/targetbot_configs/config_name.json @@ -0,0 +1,53 @@ +{ + "looting": { + "items": [ + + ], + "maxDanger": 10, + "minCapacity": 100, + "containers": [ + { + "count": 1, + "id": 2853 + } + ], + "everyItem": true + }, + "targeting": [ + { + "useSpellAttack": false, + "useRuneAttack": false, + "minMana": 200, + "avoidAttacks": false, + "groupAttackTargets": 2, + "groupAttackSpell": "", + "danger": 1, + "runeAttackDelay": 2000, + "lureCavebot": true, + "dontLoot": false, + "useGroupAttackRune": false, + "groupRuneAttackRadius": 1, + "groupAttackIgnorePlayers": true, + "maxDistance": 10, + "groupAttackIgnoreParty": false, + "lureCount": 5, + "useGroupAttack": false, + "groupRuneAttackTargets": 2, + "attackSpell": "", + "groupAttackRune": 0, + "groupAttackRadius": 1, + "keepDistanceRange": 1, + "groupRuneAttackDelay": 5000, + "priority": 1, + "attackRune": 0, + "groupAttackDelay": 5000, + "minManaGroup": 1500, + "lure": true, + "keepDistance": false, + "attackSpellDelay": 2500, + "chase": true, + "name": "cat, w?lf, snake, troll", + "regex": "^cat$|^w.?lf$|^snake$|^troll$" + } + ] +} \ No newline at end of file diff --git a/modules/game_bot/default_configs/cavebot_1.3/tools.lua b/modules/game_bot/default_configs/cavebot_1.3/tools.lua new file mode 100644 index 0000000000..7621ab0138 --- /dev/null +++ b/modules/game_bot/default_configs/cavebot_1.3/tools.lua @@ -0,0 +1,147 @@ +-- 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) + +macro(10000, "Anti Kick", function() + local dir = player:getDirection() + turn((dir + 1) % 4) + turn(dir) +end) + +UI.Separator() +UI.Label("Drop items:") +if type(storage.dropItems) ~= "table" then + storage.dropItems = {283, 284, 285} +end + +local foodContainer = UI.Container(function(widget, items) + storage.dropItems = items +end, true) +foodContainer:setHeight(35) +foodContainer:setItems(storage.dropItems) + +macro(5000, "drop items", function() + if not storage.dropItems[1] then return end + if TargetBot and TargetBot.isActive() then return end -- pause when attacking + for _, container in pairs(g_game.getContainers()) do + for __, item in ipairs(container:getItems()) do + for i, dropItem in ipairs(storage.dropItems) do + if item:getId() == dropItem.id then + if item:isStackable() then + return g_game.move(item, player:getPosition(), item:getCount()) + else + return g_game.move(item, player:getPosition(), dropItem.count) -- count is also subtype + end + 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() + if TargetBot and TargetBot.isActive() then return end -- pause when attacking + 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() diff --git a/modules/game_bot/default_configs/vBot_4.7/_Loader.lua b/modules/game_bot/default_configs/vBot_4.7/_Loader.lua new file mode 100644 index 0000000000..972b0b7814 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/_Loader.lua @@ -0,0 +1,65 @@ +-- load all otui files, order doesn't matter +local configName = modules.game_bot.contentsPanel.config:getCurrentOption().text + +local configFiles = g_resources.listDirectoryFiles("/bot/" .. configName .. "/vBot", true, false) +for i, file in ipairs(configFiles) do + local ext = file:split(".") + if ext[#ext]:lower() == "ui" or ext[#ext]:lower() == "otui" then + g_ui.importStyle(file) + end +end + +local function loadScript(name) + return dofile("/vBot/" .. name .. ".lua") +end + +-- here you can set manually order of scripts +-- libraries should be loaded first +local luaFiles = { + "main", + "items", + "vlib", + "new_cavebot_lib", + "configs", -- do not change this and above + "extras", + "cavebot", + "playerlist", + "BotServer", + "alarms", + "Conditions", + "Equipper", + "pushmax", + "combo", + "HealBot", + "new_healer", + "AttackBot", -- last of major modules + "ingame_editor", + "Dropper", + "Containers", + "quiver_manager", + "quiver_label", + "tools", + "antiRs", + "depot_withdraw", + "cast_food", + "eat_food", + "equip", + "exeta", + "analyzer", + "spy_level", + "supplies", + "depositer_config", + "npc_talk", + "xeno_menu", + "hold_target", + "cavebot_control_panel" +} + +for i, file in ipairs(luaFiles) do + loadScript(file) +end + +setDefaultTab("Main") +UI.Separator() +UI.Label("Private Scripts:") +UI.Separator() diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/actions.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/actions.lua new file mode 100644 index 0000000000..d080322b21 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/actions.lua @@ -0,0 +1,505 @@ +CaveBot.Actions = {} +vBot.lastLabel = "" +local oldTibia = g_game.getClientVersion() < 960 +local nextTile = nil + +local noPath = 0 + +-- antistuck f() +local nextPos = nil -- creature +local nextPosF = nil -- furniture +local function modPos(dir) + local y = 0 + local x = 0 + + if dir == 0 then + y = -1 + elseif dir == 1 then + x = 1 + elseif dir == 2 then + y = 1 + elseif dir == 3 then + x = -1 + elseif dir == 4 then + y = -1 + x = 1 + elseif dir == 5 then + y = 1 + x = 1 + elseif dir == 6 then + y = 1 + x = -1 + elseif dir == 7 then + y = -1 + x = -1 + end + + return {x, y} +end + +-- stack-covered antystuck, in & out pz +local lastMoved = now - 200 +onTextMessage(function(mode, text) + if text ~= 'There is not enough room.' then return end + if CaveBot.isOff() then return end + + local tiles = getNearTiles(pos()) + + for i, tile in ipairs(tiles) do + if not tile:hasCreature() and tile:isWalkable() and #tile:getItems() > 9 then + local topThing = tile:getTopThing() + if not isInPz() then + return useWith(3197, tile:getTopThing()) -- disintegrate + else + if now < lastMoved + 200 then return end -- delay to prevent clogging + local nearTiles = getNearTiles(tile:getPosition()) + for i, tile in ipairs(nearTiles) do + local tpos = tile:getPosition() + if pos() ~= tpos then + if tile:isWalkable() then + lastMoved = now + return g_game.move(topThing, tpos) -- move item + end + end + end + end + end + end +end) + +local furnitureIgnore = { 2986 } +local function breakFurniture(destPos) + if isInPz() then return false end + local candidate = {thing=nil, dist=100} + for i, tile in ipairs(g_map.getTiles(posz())) do + local walkable = tile:isWalkable() + local topThing = tile:getTopThing() + local isWg = topThing and topThing:getId() == 2130 + if topThing and (isWg or not table.find(furnitureIgnore, topThing:getId()) and topThing:isItem()) then + local moveable = not topThing:isNotMoveable() + local tpos = tile:getPosition() + local path = findPath(player:getPosition(), tpos, 7, { ignoreNonPathable = true, precision = 1 }) + + if path then + if isWg or (not walkable and moveable) then + local distance = getDistanceBetween(destPos, tpos) + + if distance < candidate.dist then + candidate = {thing=topThing, dist=distance} + end + end + end + end + end + + local thing = candidate.thing + if thing then + useWith(3197, thing) + return true + end + + return false +end + +local function pushPlayer(creature) + local cpos = creature:getPosition() + local tiles = getNearTiles(cpos) + + for i, tile in ipairs(tiles) do + local pos = tile:getPosition() + local minimapColor = g_map.getMinimapColor(pos) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + + if not stairs and tile:isWalkable() then + g_game.move(creature, pos) + end + end + +end + +local function pathfinder() + if not storage.extras.pathfinding then return end + if noPath < 10 then return end + + if not CaveBot.gotoNextWaypointInRange() then + if getConfigFromName and getConfigFromName() then + local profile = CaveBot.getCurrentProfile() + local config = getConfigFromName() + local newProfile = profile == '#Unibase' and config or '#Unibase' + + CaveBot.setCurrentProfile(newProfile) + end + end + noPath = 0 + return true +end + +-- 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 warn("Invalid cavebot action: " .. action) + end + if type(value) == 'number' then + value = tostring(value) + 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 warn("Invalid cavebot action: " .. action) + end + + if not widget.action or not widget.value then + return warn("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 warn("Duplicated acction: " .. action) + end + CaveBot.Actions[action] = { + color=color, + callback=callback + } +end + +CaveBot.registerAction("label", "yellow", function(value, retries, prev) + vBot.lastLabel = value + 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 + local data = string.split(value, ",") + local val = tonumber(data[1]:trim()) + local random + local final + + + if #data == 2 then + random = tonumber(data[2]:trim()) + end + + if random then + local diff = (val/100) * random + local min = val - diff + local max = val + diff + final = math.random(min, max) + end + final = final or val + + CaveBot.delay(final) + return "retry" + end + return true +end) + +CaveBot.registerAction("follow", "#FF8400", function(value, retries, prev) + local c = getCreatureByName(value) + if not c then + print("CaveBot[follow]: can't find creature to follow") + return false + end + local cpos = c:getPosition() + local pos = pos() + if getDistanceBetween(cpos, pos) < 2 then + g_game.cancelFollow() + return true + else + follow(c) + delay(200) + return "retry" + end +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() warn('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 + warn("warn 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]+),?\\s*([0-9]?)") + if not pos[1] then + warn("Invalid cavebot goto action value. It should be position (x,y,z), is: " .. value) + return false + end + + -- reset pathfinder + nextPosF = nil + nextPos = nil + + if CaveBot.Config.get("mapClick") then + if retries >= 5 then + noPath = noPath + 1 + pathfinder() + return false -- tried 5 times, can't get there + end + else + if retries >= 100 then + noPath = noPath + 1 + pathfinder() + return false -- tried 100 times, can't get there + end + end + + local precision = tonumber(pos[1][5]) + 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 + noPath = noPath + 1 + pathfinder() + return false -- different floor + end + + local maxDist = storage.extras.gotoMaxDistance or 40 + + if math.abs(pos.x-playerPos.x) + math.abs(pos.y-playerPos.y) > maxDist then + noPath = noPath + 1 + pathfinder() + 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 + noPath = 0 + return true -- already at position + end + elseif math.abs(pos.x-playerPos.x) == 0 and math.abs(pos.y-playerPos.y) <= (precision or 1) then + noPath = 0 + return true -- already at position + end + -- check if there's a path to that place, ignore creatures and fields + local path = findPath(playerPos, pos, maxDist, { ignoreNonPathable = true, precision = 1, ignoreCreatures = true, allowUnseen = true, allowOnlyVisibleTiles = false }) + if not path then + if breakFurniture(pos, storage.extras.machete) then + CaveBot.delay(1000) + retries = 0 + return "retry" + end + noPath = noPath + 1 + pathfinder() + return false -- there's no way + end + + -- check if there's a path to destination but consider Creatures (attack only if trapped) + local path2 = findPath(playerPos, pos, maxDist, { ignoreNonPathable = true, precision = 1 }) + if not path2 then + local foundMonster = false + for i, dir in ipairs(path) do + local dirs = modPos(dir) + nextPos = nextPos or playerPos + nextPos.x = nextPos.x + dirs[1] + nextPos.y = nextPos.y + dirs[2] + + local tile = g_map.getTile(nextPos) + if tile then + if tile:hasCreature() then + local creature = tile:getCreatures()[1] + local hppc = creature:getHealthPercent() + if creature:isMonster() and (hppc and hppc > 0) and (oldTibia or creature:getType() < 3) then + -- real blocking creature can not meet those conditions - ie. it could be player, so just in case check if the next creature is reachable + local path = findPath(playerPos, creature:getPosition(), 7, { ignoreNonPathable = true, precision = 1 }) + if path then + foundMonster = true + if g_game.getAttackingCreature() ~= creature then + attack(creature) + end + g_game.setChaseMode(1) + CaveBot.delay(100) + retries = 0 -- reset retries, we are trying to unclog the cavebot + break + end + end + end + end + end + + if not foundMonster then + foundMonster = false + return false -- no other way + end + end + + -- try to find path, don't ignore creatures, don't ignore fields + if not CaveBot.Config.get("ignoreFields") and CaveBot.walkTo(pos, 40) then + return "retry" + end + + -- try to find path, don't ignore creatures, ignore fields + if CaveBot.walkTo(pos, maxDist, { ignoreNonPathable = true, allowUnseen = true, allowOnlyVisibleTiles = false }) 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 CaveBot.walkTo(pos, 50, { ignoreNonPathable = true, precision = precison, allowUnseen = true, allowOnlyVisibleTiles = false }) then + return "retry" + end + end + + if not CaveBot.Config.get("mapClick") and retries >= 5 then + noPath = noPath + 1 + pathfinder() + return false + end + + if CaveBot.Config.get("skipBlocked") then + noPath = noPath + 1 + pathfinder() + return false + end + + -- everything else failed, try to walk ignoring creatures, maybe will work + CaveBot.walkTo(pos, maxDist, { ignoreNonPathable = true, precision = 1, ignoreCreatures = true, allowUnseen = true, allowOnlyVisibleTiles = false }) + 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 + warn("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(CaveBot.Config.get("useDelay") + CaveBot.Config.get("ping")) + 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 + warn("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(CaveBot.Config.get("useDelay") + CaveBot.Config.get("ping")) + return true +end) + +CaveBot.registerAction("say", "#FF55FF", function(value, retries, prev) + say(value) + return true +end) +CaveBot.registerAction("npcsay", "#FF55FF", function(value, retries, prev) + NPC.say(value) + return true +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/bank.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/bank.lua new file mode 100644 index 0000000000..6bdee75893 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/bank.lua @@ -0,0 +1,92 @@ +CaveBot.Extensions.Bank = {} + +local balance = 0 + +CaveBot.Extensions.Bank.setup = function() + CaveBot.registerAction("bank", "#db5a5a", function(value, retries) + local data = string.split(value, ",") + local waitVal = 300 + local amount = 0 + local actionType + local npcName + local transferName + local balanceLeft + if #data ~= 3 and #data ~= 2 and #data ~= 4 then + warn("CaveBot[Bank]: incorrect value!") + return false + else + actionType = data[1]:trim():lower() + npcName = data[2]:trim() + if #data == 3 then + amount = tonumber(data[3]:trim()) + end + if #data == 4 then + transferName = data[3]:trim() + balanceLeft = tonumber(data[4]:trim()) + end + end + + if actionType ~= "withdraw" and actionType ~= "deposit" and actionType ~= "transfer" then + warn("CaveBot[Bank]: incorrect action type! should be withdraw/deposit/transfer, is: " .. actionType) + return false + elseif actionType == "withdraw" then + local value = tonumber(amount) + if not value then + warn("CaveBot[Bank]: incorrect amount value! should be number, is: " .. amount) + return false + end + end + + if retries > 5 then + print("CaveBot[Bank]: too many tries, skipping") + return false + end + + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[Bank]: NPC not found, skipping") + return false + end + + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + if actionType == "deposit" then + CaveBot.Conversation("hi", "deposit all", "yes") + CaveBot.delay(storage.extras.talkDelay*3) + return true + elseif actionType == "withdraw" then + CaveBot.Conversation("hi", "withdraw", value, "yes") + CaveBot.delay(storage.extras.talkDelay*4) + return true + else + -- first check balance + CaveBot.Conversation("hi", "balance") + schedule(5000, function() + local amountToTransfer = balance - balanceLeft + if amountToTransfer <= 0 then + warn("CaveBot[Bank] Not enough gold to transfer! proceeding") + return false + end + CaveBot.Conversation("hi", "transfer", amountToTransfer, transferName, "yes") + warn("CaveBot[Bank] transferred "..amountToTransfer.." gold to: "..transferName) + end) + CaveBot.delay(storage.extras.talkDelay*11) + return true + end + end) + + CaveBot.Editor.registerAction("bank", "bank", { + value="action, NPC name", + title="Banker", + description="action type(withdraw/deposit/transfer), NPC name, (if withdraw: amount|if transfer: name, balance left)", + }) +end + + +onTalk(function(name, level, mode, text, channelId, pos) + if mode == 51 and text:find("Your account balance is") then + balance = getFirstNumberInText(text) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/buy_supplies.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/buy_supplies.lua new file mode 100644 index 0000000000..1620ccc0e4 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/buy_supplies.lua @@ -0,0 +1,76 @@ +CaveBot.Extensions.BuySupplies = {} + +CaveBot.Extensions.BuySupplies.setup = function() + CaveBot.registerAction("BuySupplies", "#C300FF", function(value, retries) + local possibleItems = {} + + local val = string.split(value, ",") + local waitVal + if #val == 0 or #val > 2 then + warn("CaveBot[BuySupplies]: incorrect BuySupplies value") + return false + elseif #val == 2 then + waitVal = tonumber(val[2]:trim()) + end + + local npcName = val[1]:trim() + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[BuySupplies]: NPC not found") + return false + end + + if not waitVal and #val == 2 then + warn("CaveBot[BuySupplies]: incorrect delay values!") + elseif waitVal and #val == 2 then + delay(waitVal) + end + + if retries > 50 then + print("CaveBot[BuySupplies]: Too many tries, can't buy") + return false + end + + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + if not NPC.isTrading() then + CaveBot.OpenNpcTrade() + CaveBot.delay(storage.extras.talkDelay*2) + return "retry" + end + + -- get items from npc + local npcItems = NPC.getBuyItems() + for i,v in pairs(npcItems) do + table.insert(possibleItems, v.id) + end + + for id, values in pairs(Supplies.getItemsData()) do + id = tonumber(id) + if table.find(possibleItems, id) then + local max = values.max + local current = player:getItemsCount(id) + local toBuy = max - current + + if toBuy > 0 then + toBuy = math.min(100, toBuy) + + NPC.buy(id, math.min(100, toBuy)) + print("CaveBot[BuySupplies]: bought " .. toBuy .. "x " .. id) + return "retry" + end + end + end + + print("CaveBot[BuySupplies]: bought everything, proceeding") + return true + end) + + CaveBot.Editor.registerAction("buysupplies", "buy supplies", { + value="NPC name", + title="Buy Supplies", + description="NPC Name, delay(in ms, optional)", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.lua new file mode 100644 index 0000000000..6da44598c7 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.lua @@ -0,0 +1,447 @@ +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 +if CaveBot.Config then + CaveBot.Config.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() and not TargetBot.isCaveBotActionAllowed() then + CaveBot.resetWalking() + return -- target bot or looting is working, wait + end + + if CaveBot.doWalking() then + return -- executing walking3 + 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() + CaveBot.resetWalking() + 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 + warn("Invalid return from cavebot action (" .. currentAction.action .. "), should be \"retry\", false or true, is: " .. tostring(result)) + end + else + warn("warn while executing cavebot action (" .. currentAction.action .. "):\n" .. result) + end + else + warn("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) + +-- 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 + + local cavebotConfig = nil + for k,v in ipairs(data) do + if type(v) == "table" and #v == 2 then + if v[1] == "config" then + local status, result = pcall(function() + return json.decode(v[2]) + end) + if not status then + warn("warn while parsing CaveBot extensions from config:\n" .. result) + else + cavebotConfig = result + end + elseif v[1] == "extensions" then + local status, result = pcall(function() + return json.decode(v[2]) + end) + if not status then + warn("warn 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 + + CaveBot.Config.onConfigChange(name, enabled, cavebotConfig) + + actionRetries = 0 + CaveBot.resetWalking() + 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 + +ui.showConfig.onClick = function() + if not CaveBot.Config then return end + if ui.showConfig:isOn() then + CaveBot.Config.hide() + ui.showConfig:setOn(false) + else + CaveBot.Config.show() + ui.showConfig: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.getCurrentProfile = function() + return storage._configs.cavebot_configs.selected +end + +CaveBot.lastReachedLabel = function() + return vBot.lastLabel +end + +CaveBot.gotoNextWaypointInRange = function() + local currentAction = ui.list:getFocusedChild() + local index = ui.list:getChildIndex(currentAction) + local actions = ui.list:getChildren() + + -- start searching from current index + for i, child in ipairs(actions) do + if i > index then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + local maxDist = storage.extras.gotoMaxDistance + if distanceFromPlayer(pos) <= maxDist then + if findPath(player:getPosition(), pos, maxDist, { ignoreNonPathable = true }) then + ui.list:focusChild(ui.list:getChildByIndex(i-1)) + return true + end + end + end + end + end + end + + -- if not found then damn go from start + for i, child in ipairs(actions) do + if i <= index then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + local maxDist = storage.extras.gotoMaxDistance + if distanceFromPlayer(pos) <= maxDist then + if findPath(player:getPosition(), pos, maxDist, { ignoreNonPathable = true }) then + ui.list:focusChild(ui.list:getChildByIndex(i-1)) + return true + end + end + end + end + end + end + + -- not found + return false +end + +local function reverseTable(t, max) + local reversedTable = {} + local itemCount = max or #t + for i, v in ipairs(t) do + reversedTable[itemCount + 1 - i] = v + end + return reversedTable +end + +function rpairs(t) + test() + return function(t, i) + i = i - 1 + if i ~= 0 then + return i, t[i] + end + end, t, #t + 1 +end + +CaveBot.gotoFirstPreviousReachableWaypoint = function() + local currentAction = ui.list:getFocusedChild() + local currentIndex = ui.list:getChildIndex(currentAction) + local index = ui.list:getChildIndex(currentAction) + + -- check up to 100 childs + for i=0,100 do + index = index - i + if index <= 0 or index > currentIndex or math.abs(index-currentIndex) > 100 then + break + end + + local child = ui.list:getChildByIndex(index) + + if child then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + if distanceFromPlayer(pos) <= storage.extras.gotoMaxDistance/2 then + print("found pos, going back "..currentIndex-index.. " waypoints.") + return ui.list:focusChild(child) + end + end + end + end + end + + -- not found + print("previous pos not found, proceeding") + return false +end + +CaveBot.getFirstWaypointBeforeLabel = function(label) + label = "label:"..label + label = label:lower() + local actions = ui.list:getChildren() + local index + + -- find index of label + for i, child in pairs(actions) do + local name = child:getText():lower() + if name == label then + index = i + break + end + end + + -- if there's no index then label was not found + if not index then return false end + + for i=1,#actions do + if index - 1 < 1 then + -- did not found any waypoint in range before label + return false + end + + local child = ui.list:getChildByIndex(index-i) + if child then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + if distanceFromPlayer(pos) <= storage.extras.gotoMaxDistance/2 then + return ui.list:focusChild(child) + end + end + end + end + end +end + +CaveBot.getPreviousLabel = function() + local actions = ui.list:getChildren() + -- check if config is empty + if #actions == 0 then return false end + + local currentAction = ui.list:getFocusedChild() + --check we made any progress in waypoints, if no focused or first then no point checking + if not currentAction or currentAction == ui.list:getFirstChild() then return false end + + local index = ui.list:getChildIndex(currentAction) + + -- if not index then something went wrong and there's no selected child + if not index then return false end + + for i=1,#actions do + if index - i < 1 then + -- did not found any waypoint in range before label + return false + end + + local child = ui.list:getChildByIndex(index-i) + if child then + if child.action == "label" then + return child.value + end + end + end +end + +CaveBot.getNextLabel = function() + local actions = ui.list:getChildren() + -- check if config is empty + if #actions == 0 then return false end + + local currentAction = ui.list:getFocusedChild() or ui.list:getFirstChild() + local index = ui.list:getChildIndex(currentAction) + + -- if not index then something went wrong + if not index then return false end + + for i=1,#actions do + if index + i > #actions then + -- did not found any waypoint in range before label + return false + end + + local child = ui.list:getChildByIndex(index+i) + if child then + if child.action == "label" then + return child.value + end + end + end +end + +local botConfigName = modules.game_bot.contentsPanel.config:getCurrentOption().text +CaveBot.setCurrentProfile = function(name) + if not g_resources.fileExists("/bot/"..botConfigName.."/cavebot_configs/"..name..".cfg") then + return warn("there is no cavebot profile with that name!") + end + CaveBot.setOff() + storage._configs.cavebot_configs.selected = name + CaveBot.setOn() +end + +CaveBot.delay = function(value) + cavebotMacro.delay = math.max(cavebotMacro.delay or 0, 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 + + if CaveBot.Config then + table.insert(data, {"config", json.encode(CaveBot.Config.save())}) + 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 + +CaveBotList = function() + return ui.list +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.otui b/modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.otui new file mode 100644 index 0000000000..b92ed05fb9 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/cavebot.otui @@ -0,0 +1,58 @@ +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 + + BotSwitch + id: showConfig + margin-top: 2 + + $on: + text: Hide config + + $!on: + text: Show config \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/clear_tile.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/clear_tile.lua new file mode 100644 index 0000000000..73a252fb10 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/clear_tile.lua @@ -0,0 +1,128 @@ +CaveBot.Extensions.ClearTile = {} + +CaveBot.Extensions.ClearTile.setup = function() + CaveBot.registerAction("ClearTile", "#00FFFF", function(value, retries) + local data = string.split(value, ",") + local pos = {x=tonumber(data[1]), y=tonumber(data[2]), z=tonumber(data[3])} + local doors = false + local stand = false + local pPos = player:getPosition() + + + for i, value in ipairs(data) do + value = value:lower():trim() + if value == "stand" then + stand = true + elseif value == "doors" then + doors = true + end + end + + + if not #pos == 3 then + warn("CaveBot[ClearTile]: invalid value. It should be position (x,y,z), is: " .. value) + return false + end + + if retries >= 20 then + print("CaveBot[ClearTile]: too many tries, can't clear it") + return false -- tried 20 times, can't clear it + end + + if getDistanceBetween(player:getPosition(), pos) == 0 then + print("CaveBot[ClearTile]: tile reached, proceeding") + return true + end + local tile = g_map.getTile(pos) + if not tile then + print("CaveBot[ClearTile]: can't find tile or tile is unreachable, skipping") + return false + end + local tPos = tile:getPosition() + + -- no items on tile and walkability means we are done + if tile:isWalkable() and tile:getTopUseThing():isNotMoveable() and not tile:hasCreature() and not doors then + if stand then + if not CaveBot.MatchPosition(tPos, 0) then + CaveBot.GoTo(tPos, 0) + return "retry" + end + end + print("CaveBot[ClearTile]: tile clear, proceeding") + return true + end + + if not CaveBot.MatchPosition(tPos, 3) then + CaveBot.GoTo(tPos, 3) + return "retry" + end + + if retries > 0 then + delay(1100) + end + + -- monster + if tile:hasCreature() then + local c = tile:getCreatures()[1] + if c:isMonster() then + attack(c) + return "retry" + end + end + + -- moveable item + local item = tile:getTopMoveThing() + if item:isItem() then + if item and not item:isNotMoveable() then + print("CaveBot[ClearTile]: moving item... " .. item:getId().. " from tile") + g_game.move(item, pPos, item:getCount()) + return "retry" + end + end + + -- player + + -- push creature + if tile:hasCreature() then + local c = tile:getCreatures()[1] + if c and c:isPlayer() then + + local candidates = {} + for _, tile in ipairs(g_map.getTiles(posz())) do + local tPos = tile:getPosition() + if getDistanceBetween(c:getPosition(), tPos) == 1 and tPos ~= pPos and tile:isWalkable() then + table.insert(candidates, tPos) + end + end + + if #candidates == 0 then + print("CaveBot[ClearTile]: can't find tile to push, cannot clear way, skipping") + return false + else + print("CaveBot[ClearTile]: pushing player... " .. c:getName() .. " out of the way") + local pos = candidates[math.random(1,#candidates)] + local tile = g_map.getTile(pos) + tile:setText("here") + schedule(500, function() tile:setText("") end) + g_game.move(c, pos, 1) + return "retry" + end + end + end + + -- doors + if doors then + use(tile:getTopUseThing()) + return "retry" + end + + return "retry" + end) + + CaveBot.Editor.registerAction("cleartile", "clear tile", { + value=function() return posx() .. "," .. posy() .. "," .. posz() end, + title="position of tile to clear", + description="tile position (x,y,z), doors/stand - optional", + multiline=false +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/config.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/config.lua new file mode 100644 index 0000000000..e398111d6b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/config.lua @@ -0,0 +1,111 @@ +-- config for bot +CaveBot.Config = {} +CaveBot.Config.values = {} +CaveBot.Config.default_values = {} +CaveBot.Config.value_setters = {} + +CaveBot.Config.setup = function() + CaveBot.Config.ui = UI.createWidget("CaveBotConfigPanel") + local ui = CaveBot.Config.ui + local add = CaveBot.Config.add + + add("ping", "Server ping", 100) + add("walkDelay", "Walk delay", 10) + add("mapClick", "Use map click", false) + add("mapClickDelay", "Map click delay", 100) + add("ignoreFields", "Ignore fields", false) + add("skipBlocked", "Skip blocked path", false) + add("useDelay", "Delay after use", 400) +end + +CaveBot.Config.show = function() + CaveBot.Config.ui:show() +end + +CaveBot.Config.hide = function() + CaveBot.Config.ui:hide() +end + +CaveBot.Config.onConfigChange = function(configName, isEnabled, configData) + for k, v in pairs(CaveBot.Config.default_values) do + CaveBot.Config.value_setters[k](v) + end + if not configData then return end + for k, v in pairs(configData) do + if CaveBot.Config.value_setters[k] then + CaveBot.Config.value_setters[k](v) + end + end +end + +CaveBot.Config.save = function() + return CaveBot.Config.values +end + +CaveBot.Config.add = function(id, title, defaultValue) + if CaveBot.Config.values[id] then + return warn("Duplicated config key: " .. id) + end + + local panel + local setter -- sets value + if type(defaultValue) == "number" then + panel = UI.createWidget("CaveBotConfigNumberValuePanel", CaveBot.Config.ui) + panel:setId(id) + setter = function(value) + CaveBot.Config.values[id] = value + panel.value:setText(value, true) + end + setter(defaultValue) + panel.value.onTextChange = function(widget, newValue) + newValue = tonumber(newValue) + if newValue then + CaveBot.Config.values[id] = newValue + CaveBot.save() + end + end + elseif type(defaultValue) == "boolean" then + panel = UI.createWidget("CaveBotConfigBooleanValuePanel", CaveBot.Config.ui) + panel:setId(id) + setter = function(value) + CaveBot.Config.values[id] = value + panel.value:setOn(value, true) + end + setter(defaultValue) + panel.value.onClick = function(widget) + widget:setOn(not widget:isOn()) + CaveBot.Config.values[id] = widget:isOn() + CaveBot.save() + end + else + return warn("Invalid default value of config for key " .. id .. ", should be number or boolean") + end + + panel.title:setText(tr(title) .. ":") + + CaveBot.Config.value_setters[id] = setter + CaveBot.Config.values[id] = defaultValue + CaveBot.Config.default_values[id] = defaultValue +end + +CaveBot.Config.get = function(id) + if CaveBot.Config.values[id] == nil then + return warn("Invalid CaveBot.Config.get, id: " .. id) + end + return CaveBot.Config.values[id] +end + +CaveBot.Config.set = function(id, value) + local valueType = CaveBot.Config.get(id) + local panel = CaveBot.Config.ui[id] + + if valueType == 'boolean' then + CaveBot.Config.values[id] = value + panel.value:setOn(value, true) + CaveBot.save() + else + CaveBot.Config.values[id] = value + panel.value:setText(value, true) + CaveBot.save() + end +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/config.otui b/modules/game_bot/default_configs/vBot_4.7/cavebot/config.otui new file mode 100644 index 0000000000..21d479dd60 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/config.otui @@ -0,0 +1,57 @@ +CaveBotConfigPanel < Panel + id: cavebotEditor + visible: false + + layout: + type: verticalBox + fit-children: true + + HorizontalSeparator + margin-top: 5 + + Label + text-align: center + text: CaveBot Config + margin-top: 5 + +CaveBotConfigNumberValuePanel < Panel + height: 20 + margin-top: 5 + + BotTextEdit + id: value + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + margin-right: 5 + width: 50 + + Label + id: title + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 + +CaveBotConfigBooleanValuePanel < Panel + height: 20 + margin-top: 5 + + BotSwitch + id: value + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + margin-right: 5 + width: 50 + + $on: + text: On + + $!on: + text: Off + + Label + id: title + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/d_withdraw.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/d_withdraw.lua new file mode 100644 index 0000000000..888ed1b074 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/d_withdraw.lua @@ -0,0 +1,105 @@ +CaveBot.Extensions.DWithdraw = {} + +CaveBot.Extensions.DWithdraw.setup = function() + CaveBot.registerAction("dpwithdraw", "#002FFF", function(value, retries) + local capLimit + local data = string.split(value, ",") + if retries > 600 then + print("CaveBot[DepotWithdraw]: actions limit reached, proceeding") + return false + end + local destContainer + local depotContainer + delay(70) + + -- input validation + if not value or #data ~= 3 and #data ~= 4 then + warn("CaveBot[DepotWithdraw]: incorrect value!") + return false + end + local indexDp = tonumber(data[1]:trim()) + local destName = data[2]:trim():lower() + local destId = tonumber(data[3]:trim()) + if #data == 4 then + capLimit = tonumber(data[4]:trim()) + end + + + -- cap check + if freecap() < (capLimit or 200) then + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + print("CaveBot[DepotWithdraw]: cap limit reached, proceeding") + return false + end + + -- containers + for i, container in ipairs(getContainers()) do + local cName = container:getName():lower() + if destName == cName then + destContainer = container + elseif cName:find("depot box") then + depotContainer = container + end + end + + if not destContainer then + print("CaveBot[DepotWithdraw]: container not found!") + return false + end + + if containerIsFull(destContainer) then + for i, item in pairs(destContainer:getItems()) do + if item:getId() == destId then + g_game.open(item, destContainer) + return "retry" + end + end + end + + -- stash validation + if depotContainer and #depotContainer:getItems() == 0 then + print("CaveBot[DepotWithdraw]: all items withdrawn") + g_game.close(depotContainer) + return true + end + + if containerIsFull(destContainer) then + for i, item in pairs(destContainer:getItems()) do + if item:getId() == destId then + g_game.open(foundNextContainer, destContainer) + return "retry" + end + end + print("CaveBot[DepotWithdraw]: loot containers full!") + return false + end + + if not CaveBot.OpenDepotBox(indexDp) then + return "retry" + end + + CaveBot.PingDelay(2) + + for i, container in pairs(g_game.getContainers()) do + if string.find(container:getName():lower(), "depot box") then + for j, item in ipairs(container:getItems()) do + statusMessage("[D_Withdraw] witdhrawing item: "..item:getId()) + g_game.move(item, destContainer:getSlotPosition(destContainer:getItemsCount()), item:getCount()) + return "retry" + end + end + end + + return "retry" + end) + + CaveBot.Editor.registerAction("dpwithdraw", "dpwithdraw", { + value="1, shopping bag, 21411", + title="Loot Withdraw", + description="insert index, destination container name and it's ID", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/depositor.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/depositor.lua new file mode 100644 index 0000000000..eb2d03801f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/depositor.lua @@ -0,0 +1,138 @@ +CaveBot.Extensions.Depositor = {} + +--local variables +local destination = nil +local lootTable = nil +local reopenedContainers = false + +local function resetCache() + reopenedContainers = false + destination = nil + lootTable = nil + + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + + if storage.caveBot.backStop then + storage.caveBot.backStop = false + CaveBot.setOff() + elseif storage.caveBot.backTrainers then + storage.caveBot.backTrainers = false + CaveBot.gotoLabel('toTrainers') + elseif storage.caveBot.backOffline then + storage.caveBot.backOffline = false + CaveBot.gotoLabel('toOfflineTraining') + end +end + +local description = g_game.getClientVersion() > 960 and "No - just deposit \n Yes - also reopen loot containers" or "currently not supported, will be added in near future" + +CaveBot.Extensions.Depositor.setup = function() + CaveBot.registerAction("depositor", "#002FFF", function(value, retries) + -- version check, TODO old tibia + if g_game.getClientVersion() < 960 then + resetCache() + warn("CaveBot[Depositor]: unsupported Tibia version, will be added in near future") + return false + end + + -- loot list check + lootTable = lootTable or CaveBot.GetLootItems() + if #lootTable == 0 then + print("CaveBot[Depositor]: no items in loot list. Wrong TargetBot Config? Proceeding") + resetCache() + return true + end + + delay(70) + + -- backpacks etc + if value:lower() == "yes" then + if not reopenedContainers then + CaveBot.CloseAllLootContainers() + delay(3000) + reopenedContainers = true + return "retry" + end + -- open next backpacks if no more loot + if not CaveBot.HasLootItems() then + local lootContainers = CaveBot.GetLootContainers() + for _, container in ipairs(getContainers()) do + local cId = container:getContainerItem():getId() + if table.find(lootContainers, cId) then + for i, item in ipairs(container:getItems()) do + if item:getId() == cId then + g_game.open(item, container) + delay(100) + return "retry" + end + end + end + end + -- couldn't find next container, so we done + print("CaveBot[Depositor]: all items stashed, no backpack to open next, proceeding") + CaveBot.CloseAllLootContainers() + delay(3000) + resetCache() + return true + end + end + + -- first check items + if retries == 0 then + if not CaveBot.HasLootItems() then -- resource consuming function + print("CaveBot[Depositor]: no items to stash, proceeding") + resetCache() + return true + end + end + + -- next check retries + if retries > 400 then + print("CaveBot[Depositor]: Depositor actions limit reached, proceeding") + resetCache() + return true + end + + -- reaching and opening depot + if not CaveBot.ReachAndOpenDepot() then + return "retry" + end + + -- add delay to prevent bugging + CaveBot.PingDelay(2) + + -- prep time and stashing + destination = destination or getContainerByName("Depot chest") + if not destination then return "retry" end + + for _, container in pairs(getContainers()) do + local name = container:getName():lower() + if not name:find("depot") and not name:find("your inbox") then + for _, item in pairs(container:getItems()) do + local id = item:getId() + if table.find(lootTable, id) then + local index = getStashingIndex(id) or item:isStackable() and 1 or 0 + statusMessage("[Depositer] stashing item: " ..id.. " to depot: "..index+1) + CaveBot.StashItem(item, index, destination) + return "retry" + end + end + end + end + + -- we gucci + resetCache() + return true + end) + + CaveBot.Editor.registerAction("depositor", "depositor", { + value="no", + title="Depositor", + description=description, + validation="(yes|Yes|YES|no|No|NO)" + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/doors.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/doors.lua new file mode 100644 index 0000000000..f53992b1b5 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/doors.lua @@ -0,0 +1,58 @@ +CaveBot.Extensions.OpenDoors = {} + +CaveBot.Extensions.OpenDoors.setup = function() + CaveBot.registerAction("OpenDoors", "#00FFFF", function(value, retries) + local pos = string.split(value, ",") + local key = nil + if #pos == 4 then + key = tonumber(pos[4]) + end + if not pos[1] then + warn("CaveBot[OpenDoors]: invalid value. It should be position (x,y,z), is: " .. value) + return false + end + + if retries >= 5 then + print("CaveBot[OpenDoors]: too many tries, can't open doors") + return false -- tried 5 times, can't open + end + + pos = {x=tonumber(pos[1]), y=tonumber(pos[2]), z=tonumber(pos[3])} + + local doorTile + if not doorTile then + for i, tile in ipairs(g_map.getTiles(posz())) do + if tile:getPosition().x == pos.x and tile:getPosition().y == pos.y and tile:getPosition().z == pos.z then + doorTile = tile + end + end + end + + if not doorTile then + return false + end + + if not doorTile:isWalkable() then + if not key then + use(doorTile:getTopUseThing()) + delay(200) + return "retry" + else + useWith(key, doorTile:getTopUseThing()) + delay(200) + return "retry" + end + else + print("CaveBot[OpenDoors]: possible to cross, proceeding") + return true + end + end) + + CaveBot.Editor.registerAction("opendoors", "open doors", { + value=function() return posx() .. "," .. posy() .. "," .. posz() end, + title="Door position", + description="doors position (x,y,z) and key id (optional)", + multiline=false, + validation=[[\d{1,5},\d{1,5},\d{1,2}(?:,\d{1,5}$|$)]] +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/editor.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/editor.lua new file mode 100644 index 0000000000..cefaf2604a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/editor.lua @@ -0,0 +1,186 @@ +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 warn("CaveBot editor warn: 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.actionList:focusChild(widget) + 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("edit", function() + local action = CaveBot.actionList:getFocusedChild() + if not action or not action.onDoubleClick then return end + action.onDoubleClick(action) + 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("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),randomness (in percent-optional)", + multiline=false, + validation="^[0-9]{1,10}$|^[0-9]{1,10},[0-9]{1,4}$" + }) + 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]+),?\\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("follow", { + value="NPC name", + title="Follow Creature", + description="insert creature name to follow", + multiline=false + }) + registerAction("npcsay", { + value="text", + title="NPC Say", + description="Enter text to NPC 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 diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/editor.otui b/modules/game_bot/default_configs/vBot_4.7/cavebot/editor.otui new file mode 100644 index 0000000000..d11288c64d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/editor.otui @@ -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/") diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/example_functions.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/example_functions.lua new file mode 100644 index 0000000000..99252e805c --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/example_functions.lua @@ -0,0 +1,114 @@ +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("Check for PZ and wait until dropped", [[ +if retries > 25 or not isPzLocked() then + return true +else + if isPoisioned() then + say("exana pox") + end + if isPzLocked() then + delay(8000) + end + return "retry" +end +]]) + +addExampleFunction("Check for stamina and imbues", [[ + if stamina() < 900 or player:getSkillLevel(11) == 0 then CaveBot.setOff() return false else return true end +]]) + +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" +]]) + +addExampleFunction("Disable TargetBot", [[ +TargetBot.setOff() +return true +]]) + +addExampleFunction("Enable TargetBot", [[ +TargetBot.setOn() +return true +]]) + +addExampleFunction("Enable TargetBot luring", [[ +TargetBot.enableLuring() +return true +]]) + +addExampleFunction("Disable TargetBot luring", [[ +TargetBot.disableLuring() +return true +]]) + +addExampleFunction("Logout", [[ +g_game.safeLogout() +delay(1000) +return "retry" +]]) + +addExampleFunction("Close Loot Containers", [[ +CaveBot.CloseAllLootContainers() +delay(3000) +return true +]]) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/extension_template.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/extension_template.lua new file mode 100644 index 0000000000..d015f11beb --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/extension_template.lua @@ -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 diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/imbuing.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/imbuing.lua new file mode 100644 index 0000000000..64012a7f97 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/imbuing.lua @@ -0,0 +1,119 @@ +-- imbuing window should be handled separatly +-- reequiping should be handled separatly (ie. equipment manager) + +CaveBot.Extensions.Imbuing = {} + +local SHRINES = {25060, 25061, 25182, 25183} +local currentIndex = 1 +local shrine = nil +local item = nil +local currentId = 0 +local triedToTakeOff = false +local destination = nil + +local function reset() + EquipManager.setOn() + shrine = nil + currentIndex = 1 + item = nil + currentId = 0 + triedToTakeOff = false + destination = nil +end + +CaveBot.Extensions.Imbuing.setup = function() + CaveBot.registerAction("imbuing", "red", function(value, retries) + local data = string.split(value, ",") + local ids = {} + + if #data == 0 and value ~= 'name' then + warn("CaveBot[Imbuing] no items added, proceeding") + reset() + return false + end + + -- setting of equipment manager so it wont disturb imbuing process + EquipManager.setOff() + + if value == 'name' then + local imbuData = AutoImbueTable[player:getName()] + for id, imbues in pairs(imbuData) do + table.insert(ids, id) + end + else + -- convert to number + for i, id in ipairs(data) do + id = tonumber(id) + if not table.find(ids, id) then + table.insert(ids, id) + end + end + end + + -- all items imbued, can proceed + if currentIndex > #ids then + warn("CaveBot[Imbuing] used shrine on all items, proceeding") + reset() + return true + end + + for _, tile in ipairs(g_map.getTiles(posz())) do + for _, item in ipairs(tile:getItems()) do + local id = item:getId() + if table.find(SHRINES, id) then + shrine = item + break + end + end + end + + -- if not shrine + if not shrine then + warn("CaveBot[Imbuing] shrine not found! proceeding") + reset() + return false + end + + destination = shrine:getPosition() + + currentId = ids[currentIndex] + item = findItem(currentId) + + -- maybe equipped? try to take off + if not item then + -- did try before, still not found so item is unavailable + if triedToTakeOff then + warn("CaveBot[Imbuing] item not found! skipping: "..currentId) + triedToTakeOff = false + currentIndex = currentIndex + 1 + return "retry" + end + triedToTakeOff = true + g_game.equipItemId(currentId) + delay(1000) + return "retry" + end + + -- we are past unequiping so just in case we were forced before, reset var + triedToTakeOff = false + + -- reaching shrine + if not CaveBot.MatchPosition(destination, 1) then + CaveBot.GoTo(destination, 1) + delay(200) + return "retry" + end + + useWith(shrine, item) + currentIndex = currentIndex + 1 + warn("CaveBot[Imbuing] Using shrine on item: "..currentId) + delay(4000) + return "retry" + end) + + CaveBot.Editor.registerAction("imbuing", "imbuing", { + value="name", + title="Auto Imbuing", + description="insert below item ids to be imbued, separated by comma\nor 'name' to load from file", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/inbox_withdraw.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/inbox_withdraw.lua new file mode 100644 index 0000000000..d5fc02b483 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/inbox_withdraw.lua @@ -0,0 +1,91 @@ +CaveBot.Extensions.InWithdraw = {} + +CaveBot.Extensions.InWithdraw.setup = function() + CaveBot.registerAction("inwithdraw", "#002FFF", function(value, retries) + local data = string.split(value, ",") + local withdrawId + local amount + + -- validation + if #data ~= 2 then + warn("CaveBot[InboxWithdraw]: incorrect withdraw value") + return false + else + withdrawId = tonumber(data[1]) + amount = tonumber(data[2]) + end + + local currentAmount = itemAmount(withdrawId) + + if currentAmount >= amount then + print("CaveBot[InboxWithdraw]: enough items, proceeding") + return true + end + + if retries > 400 then + print("CaveBot[InboxWithdraw]: actions limit reached, proceeding") + return true + end + + -- actions + local inboxContainer = getContainerByName("your inbox") + delay(100) + if not inboxContainer then + if not CaveBot.ReachAndOpenInbox() then + return "retry" + end + end + local inboxAmount = 0 + if not inboxContainer then + return "retry" + end + for i, item in pairs(inboxContainer:getItems()) do + if item:getId() == withdrawId then + inboxAmount = inboxAmount + item:getCount() + end + end + if inboxAmount == 0 then + warn("CaveBot[InboxWithdraw]: not enough items in inbox container, proceeding") + g_game.close(inboxContainer) + return true + end + + local destination + for i, container in pairs(getContainers()) do + if container:getCapacity() > #container:getItems() and not string.find(container:getName():lower(), "quiver") and not string.find(container:getName():lower(), "depot") and not string.find(container:getName():lower(), "loot") and not string.find(container:getName():lower(), "inbox") then + destination = container + end + end + + if not destination then + print("CaveBot[InboxWithdraw]: couldn't find proper destination container, skipping") + g_game.close(inboxContainer) + return false + end + + CaveBot.PingDelay(2) + + for i, container in pairs(getContainers()) do + if string.find(container:getName():lower(), "your inbox") then + for j, item in pairs(container:getItems()) do + if item:getId() == withdrawId then + if item:isStackable() then + g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), math.min(item:getCount(), (amount - currentAmount))) + return "retry" + else + g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), 1) + return "retry" + end + return "retry" + end + end + end + end + end) + + CaveBot.Editor.registerAction("inwithdraw", "in withdraw", { + value="id,amount", + title="Withdraw Items", + description="insert item id and amount", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/lure.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/lure.lua new file mode 100644 index 0000000000..0cb5c54053 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/lure.lua @@ -0,0 +1,29 @@ +CaveBot.Extensions.Lure = {} + +CaveBot.Extensions.Lure.setup = function() + CaveBot.registerAction("lure", "#FF0090", function(value, retries) + value = value:lower() + if value == "start" then + TargetBot.setOff() + elseif value == "stop" then + TargetBot.setOn() + elseif value == "toggle" then + if TargetBot.isOn() then + TargetBot.setOff() + else + TargetBot.setOn() + end + else + warn("incorrect lure value!") + end + return true + end) + + CaveBot.Editor.registerAction("lure", "lure", { + value="toggle", + title="Lure", + description="TargetBot: start, stop, toggle", + multiline=false, + validation=[[(start|stop|toggle)$]] +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/minimap.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/minimap.lua new file mode 100644 index 0000000000..cd7fabb09f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/minimap.lua @@ -0,0 +1,30 @@ +local minimap = modules.game_minimap.minimapWidget + +minimap.onMouseRelease = function(widget, pos, button) + if not minimap.allowNextRelease then return true end + minimap.allowNextRelease = false + + local mapPos = minimap:getTilePosition(pos) + if not mapPos then return end + + if button == 1 then + local player = g_game.getLocalPlayer() + if minimap.autowalk then + player:autoWalk(mapPos) + end + return true + elseif button == 2 then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("minimapMenu") + menu:setGameMenu(true) + menu:addOption(tr('Create mark'), function() minimap:createFlagWindow(mapPos) end) + menu:addOption(tr('Add CaveBot GoTo'), + function() + CaveBot.addAction("goto", mapPos.x .. "," .. mapPos.y .. "," .. mapPos.z, true) + CaveBot.save() + end) + menu:display(pos) + return true + end + return false +end diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/pos_check.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/pos_check.lua new file mode 100644 index 0000000000..361ddb6da2 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/pos_check.lua @@ -0,0 +1,47 @@ +CaveBot.Extensions.PosCheck = {} + +local posCheckRetries = 0 +CaveBot.Extensions.PosCheck.setup = function() + CaveBot.registerAction("PosCheck", "#00FFFF", function(value, retries) + local tilePos + local data = string.split(value, ",") + if #data ~= 5 then + warn("wrong travel format, should be: label, distance, x, y, z") + return false + end + + local tilePos = player:getPosition() + + tilePos.x = tonumber(data[3]) + tilePos.y = tonumber(data[4]) + tilePos.z = tonumber(data[5]) + + if posCheckRetries > 10 then + posCheckRetries = 0 + print("CaveBot[CheckPos]: waypoints locked, too many tries, unclogging cavebot and proceeding") + return false + elseif (tilePos.z == player:getPosition().z) and (getDistanceBetween(player:getPosition(), tilePos) <= tonumber(data[2])) then + posCheckRetries = 0 + print("CaveBot[CheckPos]: position reached, proceeding") + return true + else + posCheckRetries = posCheckRetries + 1 + if data[1] == "last" then + CaveBot.gotoFirstPreviousReachableWaypoint() + print("CaveBot[CheckPos]: position not-reached, going back to first reachable waypoint.") + return false + else + CaveBot.gotoLabel(data[1]) + print("CaveBot[CheckPos]: position not-reached, going back to label: " .. data[1]) + return false + end + end + end) + + CaveBot.Editor.registerAction("poscheck", "pos check", { + value=function() return "last" .. "," .. "10" .. "," .. posx() .. "," .. posy() .. "," .. posz() end, + title="Location Check", + description="label name, accepted dist from coordinates, x, y, z", + multiline=false, +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/recorder.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/recorder.lua new file mode 100644 index 0000000000..14248f378b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/recorder.lua @@ -0,0 +1,69 @@ +-- 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 + local function addStairs(pos) + CaveBot.addAction("goto", pos.x .. "," .. pos.y .. "," .. pos.z .. ",0", 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 + addStairs(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", 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 .. "," .. targetPos.x .. "," .. targetPos.y .. "," .. targetPos.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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/sell_all.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/sell_all.lua new file mode 100644 index 0000000000..913396559b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/sell_all.lua @@ -0,0 +1,66 @@ +CaveBot.Extensions.SellAll = {} + +local sellAllCap = 0 +CaveBot.Extensions.SellAll.setup = function() + CaveBot.registerAction("SellAll", "#C300FF", function(value, retries) + local val = string.split(value, ",") + local wait + + -- table formatting + for i, v in ipairs(val) do + v = v:trim() + v = tonumber(v) or v + val[i] = v + end + + if table.find(val, "yes", true) then + wait = true + end + + local npcName = val[1] + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[SellAll]: NPC not found! skipping") + return false + end + + if retries > 10 then + print("CaveBot[SellAll]: can't sell, skipping") + return false + end + + if freecap() == sellAllCap then + sellAllCap = 0 + print("CaveBot[SellAll]: Sold everything, proceeding") + return true + end + + delay(800) + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + if not NPC.isTrading() then + CaveBot.OpenNpcTrade() + delay(storage.extras.talkDelay*2) + return "retry" + else + sellAllCap = freecap() + end + + modules.game_npctrade.sellAll(wait, val) + if wait then + print("CaveBot[SellAll]: Sold All with delay") + else + print("CaveBot[SellAll]: Sold All without delay") + end + + return "retry" + end) + + CaveBot.Editor.registerAction("sellall", "sell all", { + value="NPC", + title="Sell All", + description="NPC Name, 'yes' if sell with delay, exceptions: id separated by comma", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/stand_lure.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/stand_lure.lua new file mode 100644 index 0000000000..7230bdd999 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/stand_lure.lua @@ -0,0 +1,186 @@ +CaveBot.Extensions.StandLure = {} +local enable = nil + +local function modPos(dir) + local y = 0 + local x = 0 + + if dir == 0 then + y = -1 + elseif dir == 1 then + x = 1 + elseif dir == 2 then + y = 1 + elseif dir == 3 then + x = -1 + elseif dir == 4 then + y = -1 + x = 1 + elseif dir == 5 then + y = 1 + x = 1 + elseif dir == 6 then + y = 1 + x = -1 + elseif dir == 7 then + y = -1 + x = -1 + end + + return {x, y} +end +local function reset(delay) + if type(Supplies.hasEnough()) == 'table' then + return + end + delay = delay or 0 + CaveBot.delay(delay) + if delay == nil then + enable = nil + end +end + +local resetRetries = false +CaveBot.Extensions.StandLure.setup = function() + CaveBot.registerAction( + "rushlure", + "#FF0090", + function(value, retries) + local nextPos = nil + local data = string.split(value, ",") + if not data[1] then + warn("Invalid cavebot lure action value. It should be position (x,y,z), delay(ms) is: " .. value) + return false + end + + if type(Supplies.hasEnough()) == 'table' then -- do not execute if no supplies + return false + end + + local pos = {x = tonumber(data[1]), y = tonumber(data[2]), z = tonumber(data[3])} + + local delayTime = data[4] and tonumber(data[4]) or 1000 + if not data[5] then + enable = nil + elseif data[5] == "yes" then + enable = true + else + enable = false + end + + delay(100) + + if retries > 50 and not resetRetries then + reset() + warn("[Rush Lure] Too many tries, can't reach position") + return false -- can't stand on tile + end + + if resetRetries then + resetRetries = false + end + + if distanceFromPlayer(pos) > 30 then + reset() + return false -- not reachable + end + + local playerPos = player:getPosition() + local pathWithoutMonsters = findPath(playerPos, pos, 30, { ignoreFields = true, ignoreNonPathable = true, ignoreCreatures = true, precision = 0}) + local pathWithMonsters = findPath(playerPos, pos, maxDist, { ignoreFields = true, ignoreNonPathable = true, ignoreCreatures = false, precision = 0 }) + + if not pathWithoutMonsters then + reset() + warn("[Rush Lure] No possible path to reach position, skipping.") + return false -- spot is unreachable + elseif pathWithoutMonsters and not pathWithMonsters then + local foundMonster = false + for i, dir in ipairs(pathWithoutMonsters) do + local dirs = modPos(dir) + nextPos = nextPos or playerPos + nextPos.x = nextPos.x + dirs[1] + nextPos.y = nextPos.y + dirs[2] + + + local tile = g_map.getTile(nextPos) + if tile then + if tile:hasCreature() then + local creature = tile:getCreatures()[1] + local hppc = creature:getHealthPercent() + if creature:isMonster() and (hppc and hppc > 0) and (oldTibia or creature:getType() < 3) then + -- real blocking creature can not meet those conditions - ie. it could be player, so just in case check if the next creature is reachable + local path = findPath(playerPos, creature:getPosition(), 7, { ignoreNonPathable = true, precision = 1 }) + if path then + creature:setMarked('#00FF00') + if g_game.getAttackingCreature() ~= creature then + attack(creature) + end + g_game.setChaseMode(1) + resetRetries = true -- reset retries, we are trying to unclog the cavebot + delay(100) + return "retry" + end + end + end + end + end + + if not g_game.getAttackingCreature() then + reset() + warn("[Rush Lure] No path, no blocking monster, skipping.") + return false -- no other way + end + end + + -- reaching position, delay targetbot in process + if not CaveBot.MatchPosition(pos, 0) then + TargetBot.delay(300) + CaveBot.walkTo(pos, 30, { ignoreCreatures = false, ignoreFields = true, ignoreNonPathable = true, precision = 0}) + delay(100) + resetRetries = true + return "retry" + end + + TargetBot.setOn() + reset(delayTime) + return true + end + ) + + CaveBot.Editor.registerAction( + "rushlure", + "rush lure", + { + value = function() + return posx() .. "," .. posy() .. "," .. posz() .. ",1000" + end, + title = "Stand Lure", + description = "Run to position(x,y,z), delay(ms), targetbot on/off (yes/no)", + multiline = false, + validation = [[\d{1,5},\d{1,5},\d{1,2},\d{1,5}(?:,(yes|no)$|$)]] + } + ) +end + +local next = false +schedule(5, function() -- delay because cavebot.lua is loaded after this file + modules.game_bot.connect(CaveBotList(), { + onChildFocusChange = function(widget, newChild, oldChild) + + if oldChild and oldChild.action == "rushlure" then + next = true + return + end + + if next then + if enable then + TargetBot.setOn() + elseif enable == false then + TargetBot.setOff() + end + + enable = nil -- reset + next = false + end + end}) +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/supply_check.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/supply_check.lua new file mode 100644 index 0000000000..c694bdeb6e --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/supply_check.lua @@ -0,0 +1,137 @@ +CaveBot.Extensions.SupplyCheck = {} + +local supplyRetries = 0 +local missedChecks = 0 +local rawRound = 0 +local time = now +vBot.CaveBotData = + vBot.CaveBotData or + { + refills = 0, + rounds = 0, + time = {}, + lastRefill = os.time(), + refillTime = {} + } + +local function setCaveBotData(hunting) + if hunting then + supplyRetries = supplyRetries + 1 + else + supplyRetries = 0 + table.insert(vBot.CaveBotData.refillTime, os.difftime(os.time() - vBot.CaveBotData.lastRefill)) + vBot.CaveBotData.lastRefill = os.time() + vBot.CaveBotData.refills = vBot.CaveBotData.refills + 1 + end + + table.insert(vBot.CaveBotData.time, rawRound) + vBot.CaveBotData.rounds = vBot.CaveBotData.rounds + 1 + missedChecks = 0 +end + +CaveBot.Extensions.SupplyCheck.setup = function() + CaveBot.registerAction( + "supplyCheck", + "#db5a5a", + function(value) + local data = string.split(value, ",") + local round = 0 + rawRound = 0 + local label = data[1]:trim() + local pos = nil + if #data == 4 then + pos = {x = tonumber(data[2]), y = tonumber(data[3]), z = tonumber(data[4])} + end + + if pos then + if missedChecks >= 4 then + missedChecks = 0 + supplyRetries = 0 + print("CaveBot[SupplyCheck]: Missed 5 supply checks, proceeding with waypoints") + return true + end + if getDistanceBetween(player:getPosition(), pos) > 10 then + missedChecks = missedChecks + 1 + print("CaveBot[SupplyCheck]: Missed supply check! " .. 5 - missedChecks .. " tries left before skipping.") + return CaveBot.gotoLabel(label) + end + end + + if time then + rawRound = math.ceil((now - time) / 1000) + round = rawRound .. "s" + else + round = "" + end + time = now + + local softCount = itemAmount(6529) + itemAmount(3549) + local supplyData = Supplies.hasEnough() + local supplyInfo = Supplies.getAdditionalData() + + if storage.caveBot.forceRefill then + print("CaveBot[SupplyCheck]: User forced, going back on refill. Last round took: " .. round) + storage.caveBot.forceRefill = false + supplyRetries = 0 + missedChecks = 0 + return false + elseif storage.caveBot.backStop then + print("CaveBot[SupplyCheck]: User forced, going back to city and turning off CaveBot. Last round took: " .. round) + supplyRetries = 0 + missedChecks = 0 + return false + elseif storage.caveBot.backTrainers then + print("CaveBot[SupplyCheck]: User forced, going back to city, then on trainers. Last round took: " .. round) + supplyRetries = 0 + missedChecks = 0 + return false + elseif storage.caveBot.backOffline then + print("CaveBot[SupplyCheck]: User forced, going back to city, then on offline training. Last round took: " .. round) + supplyRetries = 0 + missedChecks = 0 + return false + elseif supplyRetries > (storage.extras.huntRoutes or 50) then + print("CaveBot[SupplyCheck]: Round limit reached, going back on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.imbues.enabled and player:getSkillLevel(11) == 0) then + print("CaveBot[SupplyCheck]: Imbues ran out. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.stamina.enabled and stamina() < tonumber(supplyInfo.stamina.value)) then + print("CaveBot[SupplyCheck]: Stamina ran out. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.softBoots.enabled and softCount < 1) then + print("CaveBot[SupplyCheck]: No soft boots left. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif type(supplyData) == "table" then + print("CaveBot[SupplyCheck]: Not enough item: " .. supplyData.id .. "(only " .. supplyData.amount .. " left). Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.capacity.enabled and freecap() < tonumber(supplyInfo.capacity.value)) then + print("CaveBot[SupplyCheck]: Not enough capacity. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + else + print("CaveBot[SupplyCheck]: Enough supplies. Hunting. Round (" .. supplyRetries .. "/" .. (storage.extras.huntRoutes or 50) .. "). Last round took: " .. round) + setCaveBotData(true) + return CaveBot.gotoLabel(label) + end + end + ) + + CaveBot.Editor.registerAction( + "supplycheck", + "supply check", + { + value = function() + return "startHunt," .. posx() .. "," .. posy() .. "," .. posz() + end, + title = "Supply check label", + description = "Insert here hunting start label", + validation = [[[^,]+,\d{1,5},\d{1,5},\d{1,2}$]] + } + ) +end diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/tasker.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/tasker.lua new file mode 100644 index 0000000000..959515e62c --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/tasker.lua @@ -0,0 +1,178 @@ +CaveBot.Extensions.Tasker = {} + +local dataValidationFailed = function() + print("CaveBot[Tasker]: data validation failed! incorrect data, check cavebot/tasker for more info") + return false +end + +-- miniconfig +local talkDelay = storage.extras.talkDelay +if not storage.caveBotTasker then + storage.caveBotTasker = { + inProgress = false, + monster = "", + taskName = "", + count = 0, + max = 0 + } +end + +local resetTaskData = function() + storage.caveBotTasker.inProgress = false + storage.caveBotTasker.monster = "" + storage.caveBotTasker.monster2 = "" + storage.caveBotTasker.taskName = "" + storage.caveBotTasker.count = 0 + storage.caveBotTasker.max = 0 +end + +CaveBot.Extensions.Tasker.setup = function() + CaveBot.registerAction("Tasker", "#FF0090", function(value, retries) + local taskName = "" + local monster = "" + local monster2 = "" + local count = 0 + local label1 = "" + local label2 = "" + local task + + local data = string.split(value, ",") + if not data or #data < 1 then + dataValidationFailed() + end + local marker = tonumber(data[1]) + + if not marker then + dataValidationFailed() + resetTaskData() + elseif marker == 1 then + if getNpcs(3) == 0 then + print("CaveBot[Tasker]: no NPC found in range! skipping") + return false + end + if #data ~= 4 and #data ~= 5 then + dataValidationFailed() + resetTaskData() + else + taskName = data[2]:lower():trim() + count = tonumber(data[3]:trim()) + monster = data[4]:lower():trim() + if #data == 5 then + monster2 = data[5]:lower():trim() + end + end + elseif marker == 2 then + if #data ~= 3 then + dataValidationFailed() + else + label1 = data[2]:lower():trim() + label2 = data[3]:lower():trim() + end + elseif marker == 3 then + if getNpcs(3) == 0 then + print("CaveBot[Tasker]: no NPC found in range! skipping") + return false + end + if #data ~= 1 then + dataValidationFailed() + end + end + + -- let's cover markers now + if marker == 1 then -- starting task + CaveBot.Conversation("hi", "task", taskName, "yes") + delay(talkDelay*4) + + storage.caveBotTasker.monster = monster + if monster2 then storage.caveBotTasker.monster2 = monster2 end + storage.caveBotTasker.taskName = taskName + storage.caveBotTasker.inProgress = true + storage.caveBotTasker.max = count + storage.caveBotTasker.count = 0 + + print("CaveBot[Tasker]: taken task for: " .. monster .. " x" .. count) + return true + elseif marker == 2 then -- only checking + if not storage.caveBotTasker.inProgress then + CaveBot.gotoLabel(label2) + print("CaveBot[Tasker]: there is no task in progress so going to take one.") + return true + end + + local max = storage.caveBotTasker.max + local count = storage.caveBotTasker.count + + if count >= max then + CaveBot.gotoLabel(label2) + print("CaveBot[Tasker]: task completed: " .. storage.caveBotTasker.taskName) + return true + else + CaveBot.gotoLabel(label1) + print("CaveBot[Tasker]: task in progress, left: " .. max - count .. " " .. storage.caveBotTasker.taskName) + return true + end + + + elseif marker == 3 then -- reporting task + CaveBot.Conversation("hi", "report", "task") + delay(talkDelay*3) + + resetTaskData() + print("CaveBot[Tasker]: task reported, done") + return true + end + + end) + + CaveBot.Editor.registerAction("tasker", "tasker", { + value=[[ There is 3 scenarios for this extension, as example we will use medusa: + + 1. start task, + parameters: + - scenario for extension: 1 + - task name in gryzzly adams: medusae + - monster count: 500 + - monster name to track: medusa + - optional, monster name 2: + 2. check status, + to be used on refill to decide whether to go back or spawn or go give task back + parameters: + - scenario for extension: 2 + - label if task in progress: skipTask + - label if task done: taskDone + 3. report task, + parameters: + - scenario for extension: 3 + + Strong suggestion, almost mandatory - USE POS CHECK to verify position! this module will only check if there is ANY npc in range! + + when begin remove all the text and leave just a single string of parameters + some examples: + + 2, skipReport, goReport + 3 + 1, drakens, 500, draken warmaster, draken spellweaver + 1, medusae, 500, medusa]], + title="Tasker", + multiline = true + }) +end + +local regex = "Loot of ([a-z])* ([a-z A-Z]*):" +local regex2 = "Loot of ([a-z A-Z]*):" +onTextMessage(function(mode, text) + -- if CaveBot.isOff() then return end + if not text:lower():find("loot of") then return end + if #regexMatch(text, regex) == 1 and #regexMatch(text, regex)[1] == 3 then + monster = regexMatch(text, regex)[1][3] + elseif #regexMatch(text, regex2) == 1 and #regexMatch(text, regex2)[1] == 2 then + monster = regexMatch(text, regex2)[1][2] + end + + local m1 = storage.caveBotTasker.monster + local m2 = storage.caveBotTasker.monster2 + + if monster == m1 or monster == m2 and storage.caveBotTasker.count then + storage.caveBotTasker.count = storage.caveBotTasker.count + 1 + end +end) diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/travel.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/travel.lua new file mode 100644 index 0000000000..8e9d21e5b6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/travel.lua @@ -0,0 +1,40 @@ +CaveBot.Extensions.Travel = {} + +CaveBot.Extensions.Travel.setup = function() + CaveBot.registerAction("Travel", "#db5a5a", function(value, retries) + local data = string.split(value, ",") + if #data < 2 then + warn("CaveBot[Travel]: incorrect travel value!") + return false + end + + local npcName = data[1]:trim() + local dest = data[2]:trim() + + if retries > 5 then + print("CaveBot[Travel]: too many tries, can't travel") + return false + end + + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[Travel]: NPC not found, can't travel") + return false + end + + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + CaveBot.Travel(dest) + delay(storage.extras.talkDelay*3) + print("CaveBot[Travel]: travel action finished") + return true + end) + + CaveBot.Editor.registerAction("travel", "travel", { + value="NPC name, city", + title="Travel", + description="NPC name, City name, delay in ms(default is 200ms)", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/walking.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/walking.lua new file mode 100644 index 0000000000..c8a713366b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/walking.lua @@ -0,0 +1,93 @@ +-- walking +local expectedDirs = {} +local isWalking = {} +local walkPath = {} +local walkPathIter = 0 + +CaveBot.resetWalking = function() + expectedDirs = {} + walkPath = {} + isWalking = false +end + +CaveBot.doWalking = function() + if CaveBot.Config.get("mapClick") then + return false + end + if #expectedDirs == 0 then + return false + end + if #expectedDirs >= 3 then + CaveBot.resetWalking() + end + local dir = walkPath[walkPathIter] + if dir then + g_game.walk(dir, false) + table.insert(expectedDirs, dir) + walkPathIter = walkPathIter + 1 + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + return true + end + return false +end + +-- called when player position has been changed (step has been confirmed by server) +onPlayerPositionChange(function(newPos, oldPos) + if not oldPos or not newPos then return end + + local dirs = {{NorthWest, North, NorthEast}, {West, 8, East}, {SouthWest, South, SouthEast}} + local dir = dirs[newPos.y - oldPos.y + 2] + if dir then + dir = dir[newPos.x - oldPos.x + 2] + end + if not dir then + dir = 8 -- 8 is invalid dir, it's fine + end + + if not isWalking or not expectedDirs[1] then + -- some other walk action is taking place (for example use on ladder), wait + walkPath = {} + CaveBot.delay(CaveBot.Config.get("ping") + player:getStepDuration(false, dir) + 150) + return + end + + if expectedDirs[1] ~= dir then + if CaveBot.Config.get("mapClick") then + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + else + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + player:getStepDuration(false, dir)) + end + return + end + + table.remove(expectedDirs, 1) + if CaveBot.Config.get("mapClick") and #expectedDirs > 0 then + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + player:getStepDuration(false, dir)) + end +end) + +CaveBot.walkTo = function(dest, maxDist, params) + local path = getPath(player:getPosition(), dest, maxDist, params) + if not path or not path[1] then + return false + end + local dir = path[1] + + if CaveBot.Config.get("mapClick") then + local ret = autoWalk(path) + if ret then + isWalking = true + expectedDirs = path + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + math.max(CaveBot.Config.get("ping") + player:getStepDuration(false, dir), player:getStepDuration(false, dir) * 2)) + end + return ret + end + + g_game.walk(dir, false) + isWalking = true + walkPath = path + walkPathIter = 2 + expectedDirs = { dir } + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + return true +end diff --git a/modules/game_bot/default_configs/vBot_4.7/cavebot/withdraw.lua b/modules/game_bot/default_configs/vBot_4.7/cavebot/withdraw.lua new file mode 100644 index 0000000000..da29053277 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/cavebot/withdraw.lua @@ -0,0 +1,56 @@ +CaveBot.Extensions.Withdraw = {} + +CaveBot.Extensions.Withdraw.setup = function() + CaveBot.registerAction("withdraw", "#002FFF", function(value, retries) + -- validation + local data = string.split(value, ",") + if #data ~= 3 then + print("CaveBot[Withdraw]: incorrect data! skipping") + return false + end + + -- variables declaration + local source = tonumber(data[1]) + local id = tonumber(data[2]) + local amount = tonumber(data[3]) + + -- validation for correct values + if not id or not amount then + print("CaveBot[Withdraw]: incorrect id or amount! skipping") + return false + end + + -- check for retries + if retries > 100 then + print("CaveBot[Withdraw]: actions limit reached, proceeding") + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + return true + end + + -- check for items + if itemAmount(id) >= amount then + print("CaveBot[Withdraw]: enough items, proceeding") + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + return true + end + + statusMessage("[Withdraw] withdrawing item: " ..id.. " x"..amount) + CaveBot.WithdrawItem(id, amount, source) + CaveBot.PingDelay() + return "retry" + end) + + CaveBot.Editor.registerAction("withdraw", "withdraw", { + value="source,id,amount", + title="Withdraw Items", + description="index/inbox, item id and amount", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/creature.lua b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature.lua new file mode 100644 index 0000000000..225bcec141 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature.lua @@ -0,0 +1,99 @@ + +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 = "" + for part in string.gmatch(config.name, "[^,]+") do + if config.regex:len() > 0 then + config.regex = config.regex .. "|" + end + config.regex = config.regex .. "^" .. part:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$" + end + 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 diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_attack.lua b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_attack.lua new file mode 100644 index 0000000000..4c31644c8d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_attack.lua @@ -0,0 +1,245 @@ +local targetBotLure = false +local targetCount = 0 +local delayValue = 0 +local lureMax = 0 +local anchorPosition = nil +local lastCall = now +local delayFrom = nil +local dynamicLureDelay = false + +function getWalkableTilesCount(position) + local count = 0 + + for i, tile in pairs(getNearTiles(position)) do + if tile:isWalkable() or tile:hasCreature() then + count = count + 1 + end + end + + return count +end + +function rePosition(minTiles) + minTiles = minTiles or 8 + if now - lastCall < 500 then return end + local pPos = player:getPosition() + local tiles = getNearTiles(pPos) + local playerTilesCount = getWalkableTilesCount(pPos) + local tilesTable = {} + + if playerTilesCount > minTiles then return end + for i, tile in ipairs(tiles) do + tilesTable[tile] = not tile:hasCreature() and tile:isWalkable() and getWalkableTilesCount(tile:getPosition()) or nil + end + + local best = 0 + local target = nil + for k,v in pairs(tilesTable) do + if v > best and v > playerTilesCount then + best = v + target = k:getPosition() + end + end + + if target then + lastCall = now + return CaveBot.GoTo(target, 0) + end +end + +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() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) 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.useGroupAttackRune and config.groupAttackRune > 100 then + local creatures = g_map.getSpectatorsInRange(creature:getPosition(), false, config.groupRuneAttackRadius, config.groupRuneAttackRadius) + local playersAround = false + local monsters = 0 + for _, creature in ipairs(creatures) do + if not creature:isLocalPlayer() and creature:isPlayer() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) then + playersAround = true + elseif creature:isMonster() then + monsters = monsters + 1 + end + end + if monsters >= config.groupRuneAttackTargets and (not playersAround or config.groupAttackIgnorePlayers) then + if TargetBot.useAttackItem(config.groupAttackRune, 0, creature, config.groupRuneAttackDelay) 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, 0, creature, config.attackRuneDelay) then + return + end + end +end + +TargetBot.Creature.walk = function(creature, config, targets) + local cpos = creature:getPosition() + local pos = player:getPosition() + + local isTrapped = true + local pos = player:getPosition() + local dirs = {{-1,1}, {0,1}, {1,1}, {-1, 0}, {1, 0}, {-1, -1}, {0, -1}, {1, -1}} + for i=1,#dirs do + local tile = g_map.getTile({x=pos.x-dirs[i][1],y=pos.y-dirs[i][2],z=pos.z}) + if tile and tile:isWalkable(false) then + isTrapped = false + end + end + + -- data for external dynamic lure + if config.lureMin and config.lureMax and config.dynamicLure then + if config.lureMin >= targets then + targetBotLure = true + elseif targets >= config.lureMax then + targetBotLure = false + end + end + targetCount = targets + delayValue = config.lureDelay + + if config.lureMax then + lureMax = config.lureMax + end + + dynamicLureDelay = config.dynamicLureDelay + delayFrom = config.delayFrom + + -- luring + if config.closeLure and config.closeLureAmount <= getMonsters(1) then + return TargetBot.allowCaveBot(150) + end + if TargetBot.canLure() and (config.lure or config.lureCavebot or config.dynamicLure) and not (creature:getHealthPercent() < (storage.extras.killUnder or 30)) and not isTrapped then + if targetBotLure then + anchorPosition = nil + return TargetBot.allowCaveBot(150) + else + if targets < config.lureCount then + if config.lureCavebot then + anchorPosition = nil + return TargetBot.allowCaveBot(150) + else + local path = findPath(pos, cpos, 5, {ignoreNonPathable=true, precision=2}) + if path then + return TargetBot.walkTo(cpos, 10, {marginMin=5, marginMax=6, ignoreNonPathable=true}) + end + end + end + end + end + + local currentDistance = findPath(pos, cpos, 10, {ignoreCreatures=true, ignoreNonPathable=true, ignoreCost=true}) + if (not config.chase or #currentDistance == 1) and not config.avoidAttacks and not config.keepDistance and config.rePosition and (creature:getHealthPercent() >= storage.extras.killUnder) then + return rePosition(config.rePositionAmount or 6) + end + if ((storage.extras.killUnder > 1 and (creature:getHealthPercent() < storage.extras.killUnder)) or config.chase) and not config.keepDistance then + if #currentDistance > 1 then + return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, precision=1}) + end + elseif config.keepDistance then + if not anchorPosition or distanceFromPlayer(anchorPosition) > config.anchorRange then + anchorPosition = pos + end + if #currentDistance ~= config.keepDistanceRange and #currentDistance ~= config.keepDistanceRange + 1 then + if config.anchor and anchorPosition and getDistanceBetween(pos, anchorPosition) <= config.anchorRange*2 then + return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1, maxDistanceFrom={anchorPosition, config.anchorRange}}) + else + return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1}) + end + end + end + + --target only movement + if config.avoidAttacks then + local diffx = cpos.x - pos.x + local diffy = cpos.y - pos.y + local candidates = {} + if math.abs(diffx) == 1 and diffy == 0 then + candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}} + elseif diffx == 0 and math.abs(diffy) == 1 then + candidates = {{x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x+1, y=pos.y, z=pos.z}} + end + for _, candidate in ipairs(candidates) do + local tile = g_map.getTile(candidate) + if tile and tile:isWalkable() then + return TargetBot.walkTo(candidate, 2, {ignoreNonPathable=true}) + end + end + elseif config.faceMonster then + local diffx = cpos.x - pos.x + local diffy = cpos.y - pos.y + local candidates = {} + if diffx == 1 and diffy == 1 then + candidates = {{x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}} + elseif diffx == -1 and diffy == 1 then + candidates = {{x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}} + elseif diffx == -1 and diffy == -1 then + candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x-1, y=pos.y, z=pos.z}} + elseif diffx == 1 and diffy == -1 then + candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x+1, y=pos.y, z=pos.z}} + else + local dir = player:getDirection() + if diffx == 1 and dir ~= 1 then turn(1) + elseif diffx == -1 and dir ~= 3 then turn(3) + elseif diffy == 1 and dir ~= 2 then turn(2) + elseif diffy == -1 and dir ~= 0 then turn(0) + end + end + for _, candidate in ipairs(candidates) do + local tile = g_map.getTile(candidate) + if tile and tile:isWalkable() then + return TargetBot.walkTo(candidate, 2, {ignoreNonPathable=true}) + end + end + end +end + +onPlayerPositionChange(function(newPos, oldPos) + if CaveBot.isOff() then return end + if TargetBot.isOff() then return end + if not lureMax then return end + if storage.TargetBotDelayWhenPlayer then return end + if not dynamicLureDelay then return end + + if targetCount < (delayFrom or lureMax/2) or not target() then return end + CaveBot.delay(delayValue or 0) +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.lua b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.lua new file mode 100644 index 0000000000..37d6d0014b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.lua @@ -0,0 +1,106 @@ +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.content.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.content.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.content.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.content.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 = "" + for part in string.gmatch(newConfig.name, "[^,]+") do + if newConfig.regex:len() > 0 then + newConfig.regex = newConfig.regex .. "|" + end + newConfig.regex = newConfig.regex .. "^" .. part:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$" + end + + 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("anchorRange", "Anchoring Range", 1, 10, 3) + addScrollBar("lureCount", "Classic Lure", 0, 5, 1) + addScrollBar("lureMin", "Dynamic lure min", 0, 29, 1) + addScrollBar("lureMax", "Dynamic lure max", 1, 30, 3) + addScrollBar("lureDelay", "Dynamic lure delay", 100, 1000, 250) + addScrollBar("delayFrom", "Start delay when monsters", 1, 29, 2) + addScrollBar("rePositionAmount", "Min tiles to rePosition", 0, 7, 5) + addScrollBar("closeLureAmount", "Close Pull Until", 0, 8, 3) + + addCheckBox("chase", "Chase", true) + addCheckBox("keepDistance", "Keep Distance", false) + addCheckBox("anchor", "Anchoring", false) + addCheckBox("dontLoot", "Don't loot", false) + addCheckBox("lure", "Lure", false) + addCheckBox("lureCavebot", "Lure using cavebot", false) + addCheckBox("faceMonster", "Face monsters", false) + addCheckBox("avoidAttacks", "Avoid wave attacks", false) + addCheckBox("dynamicLure", "Dynamic lure", false) + addCheckBox("dynamicLureDelay", "Dynamic lure delay", false) + addCheckBox("diamondArrows", "D-Arrows priority", false) + addCheckBox("rePosition", "rePosition to better tile", false) + addCheckBox("closeLure", "Close Pulling Monsters", false) + addCheckBox("rpSafe", "RP PVP SAFE - (DA)", false) +end diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.otui b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.otui new file mode 100644 index 0000000000..9570f8774f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_editor.otui @@ -0,0 +1,164 @@ +TargetBotCreatureEditorScrollBar < Panel + height: 28 + margin-top: 3 + + 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: 3 + 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: 425 + + $mobile: + height: 300 + + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-align: center + !text: tr('You can use * (any characters) and ? (any character) in target name') + + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-align: center + !text: tr('You can also enter multiple targets, separate them by ,') + + 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: + + VerticalScrollBar + id: contentScroll + anchors.top: name.bottom + anchors.right: parent.right + anchors.bottom: help.top + step: 28 + pixels-scroll: true + margin-right: -10 + margin-top: 5 + margin-bottom: 5 + + ScrollablePanel + id: content + anchors.top: name.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: help.top + vertical-scrollbar: contentScroll + margin-bottom: 10 + + Panel + id: left + anchors.top: parent.top + 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: parent.top + 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 diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_priority.lua b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_priority.lua new file mode 100644 index 0000000000..813d3a620b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/creature_priority.lua @@ -0,0 +1,61 @@ +TargetBot.Creature.calculatePriority = function(creature, config, path) + -- config is based on creature_editor + local priority = 0 + local currentTarget = g_game.getAttackingCreature() + + -- extra priority if it's current target + if currentTarget == creature then + priority = priority + 1 + end + + -- check if distance is ok + if #path > config.maxDistance then + if config.rpSafe then + if currentTarget == creature then + g_game.cancelAttackAndFollow() -- if not, stop attack (pvp safe) + end + end + return priority + end + + -- add config priority + priority = priority + config.priority + + -- extra priority for close distance + local path_length = #path + if path_length == 1 then + priority = priority + 10 + elseif path_length <= 3 then + priority = priority + 5 + end + + -- extra priority for paladin diamond arrows + if config.diamondArrows then + local mobCount = getCreaturesInArea(creature:getPosition(), diamondArrowArea, 2) + priority = priority + (mobCount * 4) + + if config.rpSafe then + if getCreaturesInArea(creature:getPosition(), largeRuneArea, 3) > 0 then + if currentTarget == creature then + g_game.cancelAttackAndFollow() + end + return 0 -- pvp safe + end + end + 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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/looting.lua b/modules/game_bot/default_configs/vBot_4.7/targetbot/looting.lua new file mode 100644 index 0000000000..5f8aa8752f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/looting.lua @@ -0,0 +1,324 @@ +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 + --vBot + vBot.lootConainers = {} + vBot.lootItems = {} + for i, item in ipairs(ui.containers:getItems()) do + table.insert(vBot.lootConainers, item['id']) + end + for i, item in ipairs(ui.items:getItems()) do + table.insert(vBot.lootItems, item['id']) + end +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 = storage.extras.lootLast and TargetBot.Looting.list[#TargetBot.Looting.list] or 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)) + local maxRange = storage.extras.looting or 40 + if loot.tries > 30 or loot.pos.z ~= pos.z or dist > maxRange then + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 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 not container:isContainer() then + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 1) + return true + end + + g_game.open(container) + waitTill = now + math.min(g_game.getPing(),100) + waitingForContainer = container:getId() + + 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() or container:hasPages() 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() and not itemsById[item:getId()] then + nextContainer = item + elseif itemsById[item:getId()] or (ui.everyItem:isOn() and not item:isContainer()) 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 = nextContainer:getId() + return + end + end + + -- looting finished, remove container from list + container.lootContainer = false + g_game.close(container) + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 1) +end + +onTextMessage(function(mode, text) + if TargetBot.isOff() then return end + if #TargetBot.Looting.list == 0 then return end + if string.find(text:lower(), "you are not the owner") then -- if we are not the owners of corpse then its a waste of time to try to loot it + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 1) + end +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 isInPz() then return end + if not TargetBot.isOn() then return end + if not creature:isMonster() then return end + local config = TargetBot.Creature.calculateParams(creature, {}) -- return {craeture, config, danger, priority} + if not config.config or config.config.dontLoot 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}) + + table.sort(TargetBot.Looting.list, function(a,b) + a.dist = distanceFromPlayer(a.pos) + b.dist = distanceFromPlayer(b.pos) + + return a.dist > b.dist + end) + container:setMarked('#000088') + end) +end) diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/looting.otui b/modules/game_bot/default_configs/vBot_4.7/targetbot/looting.otui new file mode 100644 index 0000000000..aa973e3805 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/looting.otui @@ -0,0 +1,69 @@ +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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/target.lua b/modules/game_bot/default_configs/vBot_4.7/targetbot/target.lua new file mode 100644 index 0000000000..8bfb499702 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/target.lua @@ -0,0 +1,328 @@ +local targetbotMacro = nil +local config = nil +local lastAction = 0 +local cavebotAllowance = 0 +local lureEnabled = true +local dangerValue = 0 +local looterStatus = "" + +-- 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 + +local oldTibia = g_game.getClientVersion() < 960 + +-- main loop, controlled by config +targetbotMacro = macro(100, function() + local pos = player:getPosition() + local specs = g_map.getSpectatorsInRange(pos, false, 6, 6) -- 12x12 area + local creatures = 0 + for i, spec in ipairs(specs) do + if spec:isMonster() then + creatures = creatures + 1 + end + end + if creatures > 10 then -- if there are too many monsters around, limit area + creatures = g_map.getSpectatorsInRange(pos, false, 3, 3) -- 6x6 area + else + creatures = specs + end + local highestPriority = 0 + local dangerLevel = 0 + local targets = 0 + local highestPriorityParams = nil + for i, creature in ipairs(creatures) do + local hppc = creature:getHealthPercent() + if hppc and hppc > 0 then + local path = findPath(player:getPosition(), creature:getPosition(), 7, {ignoreLastCreature=true, ignoreNonPathable=true, ignoreCost=true, ignoreCreatures=true}) + if creature:isMonster() and (oldTibia or creature:getType() < 3) 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 + end + + -- reset walking + TargetBot.walkTo(nil) + + -- looting + local looting = TargetBot.Looting.process(targets, dangerLevel) + local lootingStatus = TargetBot.Looting.getStatus() + looterStatus = TargetBot.Looting.getStatus() + dangerValue = dangerLevel + + 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) + elseif cavebotAllowance > now then + TargetBot.setStatus("Luring using CaveBot") + else + TargetBot.setStatus("Attacking") + if not lureEnabled then + TargetBot.setStatus("Attacking (luring off)") + end + 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 + lureEnabled = true +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.isCaveBotActionAllowed = function() + return cavebotAllowance > now +end + +TargetBot.setStatus = function(text) + return ui.status.right:setText(text) +end + +TargetBot.getStatus = function() + return ui.status.right:getText() +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.getCurrentProfile = function() + return storage._configs.targetbot_configs.selected +end + +local botConfigName = modules.game_bot.contentsPanel.config:getCurrentOption().text +TargetBot.setCurrentProfile = function(name) + if not g_resources.fileExists("/bot/"..botConfigName.."/targetbot_configs/"..name..".json") then + return warn("there is no targetbot profile with that name!") + end + TargetBot.setOff() + storage._configs.targetbot_configs.selected = name + TargetBot.setOn() +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 + +TargetBot.allowCaveBot = function(time) + cavebotAllowance = now + time +end + +TargetBot.disableLuring = function() + lureEnabled = false +end + +TargetBot.enableLuring = function() + lureEnabled = true +end + +TargetBot.Danger = function() + return dangerValue +end + +TargetBot.lootStatus = function() + return looterStatus +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 lastItemUse = 0 +local lastRuneAttack = 0 + +TargetBot.useItem = function(item, subType, target, delay) + if not delay then delay = 200 end + if lastItemUse + delay < now then + local thing = g_things.getThingType(item) + if not thing or not thing:isFluidContainer() then + subType = g_game.getClientVersion() >= 860 and 0 or 1 + end + if g_game.getClientVersion() < 780 then + local tmpItem = g_game.findPlayerItem(item, subType) + if not tmpItem then return end + g_game.useWith(tmpItem, target, subType) -- using item from bp + else + g_game.useInventoryItemWith(item, target, subType) -- hotkey + end + lastItemUse = now + end +end + +TargetBot.useAttackItem = function(item, subType, target, delay) + if not delay then delay = 2000 end + if lastRuneAttack + delay < now then + local thing = g_things.getThingType(item) + if not thing or not thing:isFluidContainer() then + subType = g_game.getClientVersion() >= 860 and 0 or 1 + end + if g_game.getClientVersion() < 780 then + local tmpItem = g_game.findPlayerItem(item, subType) + if not tmpItem then return end + g_game.useWith(tmpItem, target, subType) -- using item from bp + else + g_game.useInventoryItemWith(item, target, subType) -- hotkey + end + lastRuneAttack = now + end +end + +TargetBot.canLure = function() + return lureEnabled +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/target.otui b/modules/game_bot/default_configs/vBot_4.7/targetbot/target.otui new file mode 100644 index 0000000000..6e0e4eafa5 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/target.otui @@ -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 diff --git a/modules/game_bot/default_configs/vBot_4.7/targetbot/walking.lua b/modules/game_bot/default_configs/vBot_4.7/targetbot/walking.lua new file mode 100644 index 0000000000..b256d6acf4 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/targetbot/walking.lua @@ -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 diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.lua new file mode 100644 index 0000000000..18a98315bc --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.lua @@ -0,0 +1,1248 @@ +setDefaultTab('main') +-- locales +local panelName = "AttackBot" +local currentSettings +local showSettings = false +local showItem = false +local category = 1 +local patternCategory = 1 +local pattern = 1 +local mainWindow + +-- label library + +local categories = { + "Targeted Spell (exori hur, exori flam, etc)", + "Area Rune (avalanche, great fireball, etc)", + "Targeted Rune (sudden death, icycle, etc)", + "Empowerment (utito tempo, etc)", + "Absolute Spell (exori, hells core, etc)", +} + +local patterns = { + -- targeted spells + { + "1 Sqm Range (exori ico)", + "2 Sqm Range", + "3 Sqm Range (strike spells)", + "4 Sqm Range (exori san)", + "5 Sqm Range (exori hur)", + "6 Sqm Range", + "7 Sqm Range (exori con)", + "8 Sqm Range", + "9 Sqm Range", + "10 Sqm Range" + }, + -- area runes + { + "Cross (explosion)", + "Bomb (fire bomb)", + "Ball (gfb, avalanche)" + }, + -- empowerment/targeted rune + { + "1 Sqm Range", + "2 Sqm Range", + "3 Sqm Range", + "4 Sqm Range", + "5 Sqm Range", + "6 Sqm Range", + "7 Sqm Range", + "8 Sqm Range", + "9 Sqm Range", + "10 Sqm Range", + }, + -- absolute + { + "Adjacent (exori, exori gran)", + "3x3 Wave (vis hur, tera hur)", + "Small Area (mas san, exori mas)", + "Medium Area (mas flam, mas frigo)", + "Large Area (mas vis, mas tera)", + "Short Beam (vis lux)", + "Large Beam (gran vis lux)", + "Sweep (exori min)", -- 8 + "Small Wave (gran frigo hur)", + "Big Wave (flam hur, frigo hur)", + "Huge Wave (gran flam hur)", + } +} + + -- spellPatterns[category][pattern][1 - normal, 2 - safe] +local spellPatterns = { + {}, -- blank, wont be used + -- Area Runes, + { + { -- cross + [[ + 010 + 111 + 010 + ]], + -- cross SAFE + [[ + 01110 + 01110 + 11111 + 11111 + 11111 + 01110 + 01110 + ]] + }, + { -- bomb + [[ + 111 + 111 + 111 + ]], + -- bomb SAFE + [[ + 11111 + 11111 + 11111 + 11111 + 11111 + ]] + }, + { -- ball + [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 + ]], + -- ball SAFE + [[ + 000111000 + 001111100 + 011111110 + 111111111 + 111111111 + 111111111 + 011111110 + 001111100 + 000111000 + ]] + }, + }, + {}, -- blank, wont be used + -- Absolute + { + {-- adjacent + [[ + 111 + 111 + 111 + ]], + -- adjacent SAFE + [[ + 11111 + 11111 + 11111 + 11111 + 11111 + ]] + }, + { -- 3x3 Wave + [[ + 0000NNN0000 + 0000NNN0000 + 0000NNN0000 + 00000N00000 + WWW00N00EEE + WWWWW0EEEEE + WWW00S00EEE + 00000S00000 + 0000SSS0000 + 0000SSS0000 + 0000SSS0000 + ]], + -- 3x3 Wave SAFE + [[ + 0000NNNNN0000 + 0000NNNNN0000 + 0000NNNNN0000 + 0000NNNNN0000 + WWWW0NNN0EEEE + WWWWWNNNEEEEE + WWWWWW0EEEEEE + WWWWWSSSEEEEE + WWWW0SSS0EEEE + 0000SSSSS0000 + 0000SSSSS0000 + 0000SSSSS0000 + 0000SSSSS0000 + ]] + }, + { -- small area + [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 + ]], + -- small area SAFE + [[ + 000111000 + 001111100 + 011111110 + 111111111 + 111111111 + 111111111 + 011111110 + 001111100 + 000111000 + ]] + }, + { -- medium area + [[ + 00000100000 + 00011111000 + 00111111100 + 01111111110 + 01111111110 + 11111111111 + 01111111110 + 01111111110 + 00111111100 + 00001110000 + 00000100000 + ]], + -- medium area SAFE + [[ + 0000011100000 + 0000111110000 + 0001111111000 + 0011111111100 + 0111111111110 + 0111111111110 + 1111111111111 + 0111111111110 + 0111111111110 + 0011111111100 + 0001111111000 + 0000111110000 + 0000011100000 + ]] + }, + { -- large area + [[ + 0000001000000 + 0000011100000 + 0000111110000 + 0001111111000 + 0011111111100 + 0111111111110 + 1111111111111 + 0111111111110 + 0011111111100 + 0001111111000 + 0000111110000 + 0000011100000 + 0000001000000 + ]], + -- large area SAFE + [[ + 000000010000000 + 000000111000000 + 000001111100000 + 000011111110000 + 000111111111000 + 001111111111100 + 011111111111110 + 111111111111111 + 011111111111110 + 001111111111100 + 000111111111000 + 000011111110000 + 000001111100000 + 000000111000000 + 000000010000000 + ]] + }, + { -- short beam + [[ + 00000N00000 + 00000N00000 + 00000N00000 + 00000N00000 + 00000N00000 + WWWWW0EEEEE + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 + ]], + -- short beam SAFE + [[ + 00000NNN00000 + 00000NNN00000 + 00000NNN00000 + 00000NNN00000 + 00000NNN00000 + WWWWWNNNEEEEE + WWWWWW0EEEEEE + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + ]] + }, + { -- large beam + [[ + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + WWWWWWW0EEEEEEE + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + ]], + -- large beam SAFE + [[ + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + WWWWWWWNNNEEEEEEE + WWWWWWWW0EEEEEEEE + WWWWWWWSSSEEEEEEE + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + ]], + }, + {}, -- sweep, wont be used + { -- small wave + [[ + 00NNN00 + 00NNN00 + WW0N0EE + WWW0EEE + WW0S0EE + 00SSS00 + 00SSS00 + ]], + -- small wave SAFE + [[ + 00NNNNN00 + 00NNNNN00 + WWNNNNNEE + WWWWNEEEE + WWWW0EEEE + WWWWSEEEE + WWSSSSSEE + 00SSSSS00 + 00SSSSS00 + ]] + }, + { -- large wave + [[ + 000NNNNN000 + 000NNNNN000 + 0000NNN0000 + WW00NNN00EE + WWWW0N0EEEE + WWWWW0EEEEE + WWWW0S0EEEE + WW00SSS00EE + 0000SSS0000 + 000SSSSS000 + 000SSSSS000 + ]], + [[ + 000NNNNNNN000 + 000NNNNNNN000 + 000NNNNNNN000 + WWWWNNNNNEEEE + WWWWNNNNNEEEE + WWWWWNNNEEEEE + WWWWWW0EEEEEE + WWWWWSSSEEEEE + WWWWSSSSSEEEE + WWWWSSSSSEEEE + 000SSSSSSS000 + 000SSSSSSS000 + 000SSSSSSS000 + ]] + }, + { -- huge wave + [[ + 0000NNNNN0000 + 0000NNNNN0000 + 00000NNN00000 + 00000NNN00000 + WW0000N0000EE + WWWW00N00EEEE + WWWWWW0EEEEEE + WWWW00S00EEEE + WW0000S0000EE + 00000SSS00000 + 00000SSS00000 + 0000SSSSS0000 + 0000SSSSS0000 + ]], + [[ + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + WWWWWWWNNNEEEEEEE + WWWWWWWW0EEEEEEEE + WWWWWWWSSSEEEEEEE + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + ]] + } + } +} + +-- direction patterns +local ek = (voc() == 1 or voc() == 11) and true + +local posN = ek and [[ + 111 + 000 + 000 +]] or [[ + 00011111000 + 00011111000 + 00011111000 + 00011111000 + 00000100000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 +]] + +local posE = ek and [[ + 001 + 001 + 001 +]] or [[ + 00000000000 + 00000000000 + 00000000000 + 00000001111 + 00000001111 + 00000011111 + 00000001111 + 00000001111 + 00000000000 + 00000000000 + 00000000000 +]] +local posS = ek and [[ + 000 + 000 + 111 +]] or [[ + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000100000 + 00011111000 + 00011111000 + 00011111000 + 00011111000 +]] +local posW = ek and [[ + 100 + 100 + 100 +]] or [[ + 00000000000 + 00000000000 + 00000000000 + 11110000000 + 11110000000 + 11111000000 + 11110000000 + 11110000000 + 00000000000 + 00000000000 + 00000000000 +]] + +-- AttackBotConfig +-- create blank profiles +if not AttackBotConfig[panelName] or not AttackBotConfig[panelName][1] or #AttackBotConfig[panelName] ~= 5 then + AttackBotConfig[panelName] = { + [1] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #1", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [2] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #2", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [3] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #3", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [4] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #4", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [5] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #5", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + } +end + +if not AttackBotConfig.currentBotProfile or AttackBotConfig.currentBotProfile == 0 or AttackBotConfig.currentBotProfile > 5 then + AttackBotConfig.currentBotProfile = 1 +end + +-- create panel UI +ui = UI.createWidget("AttackBotBotPanel") + +-- finding correct table, manual unfortunately +local setActiveProfile = function() + local n = AttackBotConfig.currentBotProfile + currentSettings = AttackBotConfig[panelName][n] +end +setActiveProfile() + +if not currentSettings.AntiRsRange then + currentSettings.AntiRsRange = 5 +end + +local setProfileName = function() + ui.name:setText(currentSettings.name) +end + +-- small UI elements +ui.title.onClick = function(widget) + currentSettings.enabled = not currentSettings.enabled + widget:setOn(currentSettings.enabled) + vBotConfigSave("atk") +end + +ui.settings.onClick = function(widget) + mainWindow:show() + mainWindow:raise() + mainWindow:focus() +end + + mainWindow = UI.createWindow("AttackBotWindow") + mainWindow:hide() + + local panel = mainWindow.mainPanel + local settingsUI = mainWindow.settingsPanel + + mainWindow.onVisibilityChange = function(widget, visible) + if not visible then + currentSettings.attackTable = {} + for i, child in ipairs(panel.entryList:getChildren()) do + table.insert(currentSettings.attackTable, child.params) + end + vBotConfigSave("atk") + end + end + + -- main panel + + -- functions + function toggleSettings() + panel:setVisible(not showSettings) + mainWindow.shooterLabel:setVisible(not showSettings) + settingsUI:setVisible(showSettings) + mainWindow.settingsLabel:setVisible(showSettings) + mainWindow.settings:setText(showSettings and "Back" or "Settings") + end + toggleSettings() + + mainWindow.settings.onClick = function() + showSettings = not showSettings + toggleSettings() + end + + function toggleItem() + panel.monsters:setWidth(showItem and 405 or 341) + panel.itemId:setVisible(showItem) + panel.spellName:setVisible(not showItem) + end + toggleItem() + + function setCategoryText() + panel.category.description:setText(categories[category]) + end + setCategoryText() + + function setPatternText() + panel.range.description:setText(patterns[patternCategory][pattern]) + end + setPatternText() + + -- in/de/crementation buttons + panel.previousCategory.onClick = function() + if category == 1 then + category = #categories + else + category = category - 1 + end + + showItem = (category == 2 or category == 3) and true or false + patternCategory = category == 4 and 3 or category == 5 and 4 or category + pattern = 1 + toggleItem() + setPatternText() + setCategoryText() + end + panel.nextCategory.onClick = function() + if category == #categories then + category = 1 + else + category = category + 1 + end + + showItem = (category == 2 or category == 3) and true or false + patternCategory = category == 4 and 3 or category == 5 and 4 or category + pattern = 1 + toggleItem() + setPatternText() + setCategoryText() + end + panel.previousSource.onClick = function() + warn("[AttackBot] TODO, reserved for future use.") + end + panel.nextSource.onClick = function() + warn("[AttackBot] TODO, reserved for future use.") + end + panel.previousRange.onClick = function() + local t = patterns[patternCategory] + if pattern == 1 then + pattern = #t + else + pattern = pattern - 1 + end + setPatternText() + end + panel.nextRange.onClick = function() + local t = patterns[patternCategory] + if pattern == #t then + pattern = 1 + else + pattern = pattern + 1 + end + setPatternText() + end + -- eo in/de/crementation + + ------- [[core table function]] ------- + function setupWidget(widget) + local params = widget.params + + widget:setText(params.description) + if params.itemId > 0 then + widget.spell:setVisible(false) + widget.id:setVisible(true) + widget.id:setItemId(params.itemId) + end + widget:setTooltip(params.tooltip) + widget.remove.onClick = function() + panel.up:setEnabled(false) + panel.down:setEnabled(false) + widget:destroy() + end + widget.enabled:setChecked(params.enabled) + widget.enabled.onClick = function() + params.enabled = not params.enabled + widget.enabled:setChecked(params.enabled) + end + -- will serve as edit + widget.onDoubleClick = function(widget) + panel.manaPercent:setValue(params.mana) + panel.creatures:setValue(params.count) + panel.minHp:setValue(params.minHp) + panel.maxHp:setValue(params.maxHp) + panel.cooldown:setValue(params.cooldown) + showItem = params.itemId > 100 and true or false + panel.itemId:setItemId(params.itemId) + panel.spellName:setText(params.spell or "") + panel.orMore:setChecked(params.orMore) + toggleItem() + category = params.category + patternCategory = params.patternCategory + pattern = params.pattern + setPatternText() + setCategoryText() + widget:destroy() + end + widget.onClick = function(widget) + if #panel.entryList:getChildren() == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(false) + elseif panel.entryList:getChildIndex(widget) == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(true) + elseif panel.entryList:getChildIndex(widget) == panel.entryList:getChildCount() then + panel.up:setEnabled(true) + panel.down:setEnabled(false) + else + panel.up:setEnabled(true) + panel.down:setEnabled(true) + end + end + end + + + -- refreshing values + function refreshAttacks() + if not currentSettings.attackTable then return end + + panel.entryList:destroyChildren() + for i, entry in pairs(currentSettings.attackTable) do + local label = UI.createWidget("AttackEntry", panel.entryList) + label.params = entry + setupWidget(label) + end + end + refreshAttacks() + panel.up:setEnabled(false) + panel.down:setEnabled(false) + + -- adding values + panel.addEntry.onClick = function(wdiget) + -- first variables + local creatures = panel.monsters:getText():lower() + local monsters = (creatures:len() == 0 or creatures == "*" or creatures == "monster names") and true or string.split(creatures, ",") + local mana = panel.manaPercent:getValue() + local count = panel.creatures:getValue() + local minHp = panel.minHp:getValue() + local maxHp = panel.maxHp:getValue() + local cooldown = panel.cooldown:getValue() + local itemId = panel.itemId:getItemId() + local spell = panel.spellName:getText() + local tooltip = monsters ~= true and creatures + local orMore = panel.orMore:isChecked() + + -- validation + if showItem and itemId < 100 then + return warn("[AttackBot]: please fill item ID!") + elseif not showItem and (spell:lower() == "spell name" or spell:len() == 0) then + return warn("[AttackBot]: please fill spell name!") + end + + local regex = patternCategory ~= 1 and [[^[^\(]+]] or [[^[^R]+]] + local type = regexMatch(patterns[patternCategory][pattern], regex)[1][1]:trim() + regex = [[^[^ ]+]] + local categoryName = regexMatch(categories[category], regex)[1][1]:trim():lower() + local specificMonsters = monsters == true and "Any Creatures" or "Creatures" + local attackType = showItem and "rune "..itemId or spell + + local countDescription = orMore and count.."+" or count + + local params = { + creatures = creatures, + monsters = monsters, + mana = mana, + count = count, + minHp = minHp, + maxHp = maxHp, + cooldown = cooldown, + itemId = itemId, + spell = spell, + enabled = true, + category = category, + patternCategory = patternCategory, + pattern = pattern, + tooltip = tooltip, + orMore = orMore, + description = '['..type..'] '..countDescription.. ' '..specificMonsters..': '..attackType..', '..categoryName..' ('..minHp..'%-'..maxHp..'%)' + } + + local label = UI.createWidget("AttackEntry", panel.entryList) + label.params = params + setupWidget(label) + resetFields() + end + + -- moving values + -- up + panel.up.onClick = function(widget) + local focused = panel.entryList:getFocusedChild() + local n = panel.entryList:getChildIndex(focused) + + if n-1 == 1 then + widget:setEnabled(false) + end + panel.down:setEnabled(true) + panel.entryList:moveChildToIndex(focused, n-1) + panel.entryList:ensureChildVisible(focused) + end + -- down + panel.down.onClick = function(widget) + local focused = panel.entryList:getFocusedChild() + local n = panel.entryList:getChildIndex(focused) + + if n + 1 == panel.entryList:getChildCount() then + widget:setEnabled(false) + end + panel.up:setEnabled(true) + panel.entryList:moveChildToIndex(focused, n+1) + panel.entryList:ensureChildVisible(focused) + end + + -- [[settings panel]] -- + settingsUI.profileName.onTextChange = function(widget, text) + currentSettings.name = text + setProfileName() + end + settingsUI.IgnoreMana.onClick = function(widget) + currentSettings.ignoreMana = not currentSettings.ignoreMana + settingsUI.IgnoreMana:setChecked(currentSettings.ignoreMana) + end + settingsUI.Rotate.onClick = function(widget) + currentSettings.Rotate = not currentSettings.Rotate + settingsUI.Rotate:setChecked(currentSettings.Rotate) + end + settingsUI.Kills.onClick = function(widget) + currentSettings.Kills = not currentSettings.Kills + settingsUI.Kills:setChecked(currentSettings.Kills) + end + settingsUI.Cooldown.onClick = function(widget) + currentSettings.Cooldown = not currentSettings.Cooldown + settingsUI.Cooldown:setChecked(currentSettings.Cooldown) + end + settingsUI.Visible.onClick = function(widget) + currentSettings.Visible = not currentSettings.Visible + settingsUI.Visible:setChecked(currentSettings.Visible) + end + settingsUI.PvpMode.onClick = function(widget) + currentSettings.pvpMode = not currentSettings.pvpMode + settingsUI.PvpMode:setChecked(currentSettings.pvpMode) + end + settingsUI.PvpSafe.onClick = function(widget) + currentSettings.PvpSafe = not currentSettings.PvpSafe + settingsUI.PvpSafe:setChecked(currentSettings.PvpSafe) + end + settingsUI.Training.onClick = function(widget) + currentSettings.Training = not currentSettings.Training + settingsUI.Training:setChecked(currentSettings.Training) + end + settingsUI.BlackListSafe.onClick = function(widget) + currentSettings.BlackListSafe = not currentSettings.BlackListSafe + settingsUI.BlackListSafe:setChecked(currentSettings.BlackListSafe) + end + settingsUI.KillsAmount.onValueChange = function(widget, value) + currentSettings.KillsAmount = value + end + settingsUI.AntiRsRange.onValueChange = function(widget, value) + currentSettings.AntiRsRange = value + end + + + -- window elements + mainWindow.closeButton.onClick = function() + showSettings = false + toggleSettings() + resetFields() + mainWindow:hide() + end + + -- core functions + function resetFields() + showItem = false + toggleItem() + pattern = 1 + patternCategory = 1 + category = 1 + setPatternText() + setCategoryText() + panel.manaPercent:setText(1) + panel.creatures:setText(1) + panel.minHp:setValue(0) + panel.maxHp:setValue(100) + panel.cooldown:setText(1) + panel.monsters:setText("monster names") + panel.itemId:setItemId(0) + panel.spellName:setText("spell name") + panel.orMore:setChecked(false) + end + resetFields() + + function loadSettings() + -- BOT panel + ui.title:setOn(currentSettings.enabled) + setProfileName() + -- main panel + refreshAttacks() + -- settings + settingsUI.profileName:setText(currentSettings.name) + settingsUI.Visible:setChecked(currentSettings.Visible) + settingsUI.Cooldown:setChecked(currentSettings.Cooldown) + settingsUI.PvpMode:setChecked(currentSettings.pvpMode) + settingsUI.PvpSafe:setChecked(currentSettings.PvpSafe) + settingsUI.BlackListSafe:setChecked(currentSettings.BlackListSafe) + settingsUI.AntiRsRange:setValue(currentSettings.AntiRsRange) + settingsUI.IgnoreMana:setChecked(currentSettings.ignoreMana) + settingsUI.Rotate:setChecked(currentSettings.Rotate) + settingsUI.Kills:setChecked(currentSettings.Kills) + settingsUI.KillsAmount:setValue(currentSettings.KillsAmount) + settingsUI.Training:setChecked(currentSettings.Training) + end + loadSettings() + + local activeProfileColor = function() + for i=1,5 do + if i == AttackBotConfig.currentBotProfile then + ui[i]:setColor("green") + else + ui[i]:setColor("white") + end + end + end + activeProfileColor() + + local profileChange = function() + setActiveProfile() + activeProfileColor() + loadSettings() + resetFields() + vBotConfigSave("atk") + end + + for i=1,5 do + local button = ui[i] + button.onClick = function() + AttackBotConfig.currentBotProfile = i + profileChange() + end + end + + -- public functions + AttackBot = {} -- global table + + AttackBot.isOn = function() + return currentSettings.enabled + end + + AttackBot.isOff = function() + return not currentSettings.enabled + end + + AttackBot.setOff = function() + currentSettings.enabled = false + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + AttackBot.setOn = function() + currentSettings.enabled = true + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + AttackBot.getActiveProfile = function() + return AttackBotConfig.currentBotProfile -- returns number 1-5 + end + + AttackBot.setActiveProfile = function(n) + if not n or not tonumber(n) or n < 1 or n > 5 then + return error("[AttackBot] wrong profile parameter! should be 1 to 5 is " .. n) + else + AttackBotConfig.currentBotProfile = n + profileChange() + end + end + + AttackBot.show = function() + mainWindow:show() + mainWindow:raise() + mainWindow:focus() + end + + +-- otui covered, now support functions +function getPattern(category, pattern, safe) + safe = safe and 2 or 1 + + return spellPatterns[category][pattern][safe] +end + + +function getMonstersInArea(category, posOrCreature, pattern, minHp, maxHp, safePattern, monsterNamesTable) + -- monsterNamesTable can be nil + local monsters = 0 + local t = {} + if monsterNamesTable == true or not monsterNamesTable then + t = {} + else + t = monsterNamesTable + end + + if safePattern then + for i, spec in pairs(getSpectators(posOrCreature, safePattern)) do + if spec ~= player and (spec:isPlayer() and not spec:isPartyMember()) then + return 0 + end + end + end + + if category == 1 or category == 3 or category == 4 then + for i, spec in pairs(getSpectators()) do + local specHp = spec:getHealthPercent() + local name = spec:getName():lower() + monsters = spec:isMonster() and specHp >= minHp and specHp <= maxHp and (#t == 0 or table.find(t, name)) and + (g_game.getClientVersion() < 960 or spec:getType() < 3) and monsters + 1 or monsters + end + return monsters + end + + for i, spec in pairs(getSpectators(posOrCreature, pattern)) do + if spec ~= player then + local specHp = spec:getHealthPercent() + local name = spec:getName():lower() + monsters = spec:isMonster() and specHp >= minHp and specHp <= maxHp and (#t == 0 or table.find(t, name)) and + (g_game.getClientVersion() < 960 or spec:getType() < 3) and monsters + 1 or monsters + end + end + + return monsters +end + +-- for area runes only +-- should return valid targets number (int) and position +function getBestTileByPattern(pattern, minHp, maxHp, safePattern, monsterNamesTable) + local tiles = g_map.getTiles(posz()) + local targetTile = {amount=0,pos=false} + + for i, tile in pairs(tiles) do + local tPos = tile:getPosition() + local distance = distanceFromPlayer(tPos) + if tile:canShoot() and tile:isWalkable() and distance < 4 then + local amount = getMonstersInArea(2, tPos, pattern, minHp, maxHp, safePattern, monsterNamesTable) + if amount > targetTile.amount then + targetTile = {amount=amount,pos=tPos} + end + end + end + + return targetTile.amount > 0 and targetTile or false +end + +function executeAttackBotAction(categoryOrPos, idOrFormula, cooldown) + cooldown = cooldown or 0 + if categoryOrPos == 4 or categoryOrPos == 5 or categoryOrPos == 1 then + cast(idOrFormula, cooldown) + elseif categoryOrPos == 3 then + useWith(idOrFormula, target()) + end +end + +-- support function covered, now the main loop +macro(100, function() + if not currentSettings.enabled then return end + if #currentSettings.attackTable == 0 or isInPz() or not target() or modules.game_cooldown.isGroupCooldownIconActive(1) then return end + + if currentSettings.Training and target() and target():getName():lower():find("training") then return end + + if g_game.getClientVersion() < 960 or not currentSettings.Cooldown then + delay(400) + end + + local monstersN = 0 + local monstersE = 0 + local monstersS = 0 + local monstersW = 0 + monstersN = getCreaturesInArea(pos(), posN, 2) + monstersE = getCreaturesInArea(pos(), posE, 2) + monstersS = getCreaturesInArea(pos(), posS, 2) + monstersW = getCreaturesInArea(pos(), posW, 2) + local posTable = {monstersE, monstersN, monstersS, monstersW} + local bestSide = 0 + local bestDir + -- pulling out the biggest number + for i, v in pairs(posTable) do + if v > bestSide then + bestSide = v + end + end + -- associate biggest number with turn direction + if monstersN == bestSide then bestDir = 0 + elseif monstersE == bestSide then bestDir = 1 + elseif monstersS == bestSide then bestDir = 2 + elseif monstersW == bestSide then bestDir = 3 + end + + if currentSettings.Rotate then + if player:getDirection() ~= bestDir and bestSide > 0 then + turn(bestDir) + return + end + end + + -- support functions done, main spells now + --[[ + entry = { + creatures = creatures, + monsters = monsters, (formatted creatures) + mana = mana, + count = count, + minHp = minHp, + maxHp = maxHp, + cooldown = cooldown, + itemId = itemId, + spell = spell, + enabled = true, + category = category, + patternCategory = patternCategory, + pattern = pattern, + tooltip = tooltip, + description = '['..type..'] '..count.. 'x '..specificMonsters..': '..attackType..', '..categoryName..' ('..minHp..'%-'..maxHp..'%)' + } + ]] + + for i, child in ipairs(panel.entryList:getChildren()) do + local entry = child.params + local attackData = entry.itemId > 100 and entry.itemId or entry.spell + if entry.enabled and manapercent() >= entry.mana then + if (type(attackData) == "string" and canCast(entry.spell, not currentSettings.ignoreMana, not currentSettings.Cooldown)) or (entry.itemId > 100 and (not currentSettings.Visible or findItem(entry.itemId))) then + -- first PVP scenario + if currentSettings.pvpMode and target():getHealthPercent() >= entry.minHp and target():getHealthPercent() <= entry.maxHp and target():canShoot() then + if entry.category == 2 then + return warn("[AttackBot] Area Runes cannot be used in PVP situation!") + else + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + end + -- empowerment + if entry.category == 4 and not isBuffed() then + local monsterAmount = getMonstersInArea(entry.category, nil, nil, entry.minHp, entry.maxHp, false, entry.monsters) + if (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count) and distanceFromPlayer(target():getPosition()) <= entry.pattern then + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + -- + elseif entry.category == 1 or entry.category == 3 then + local monsterAmount = getMonstersInArea(entry.category, nil, nil, entry.minHp, entry.maxHp, false, entry.monsters) + if (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count) and distanceFromPlayer(target():getPosition()) <= entry.pattern then + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + elseif entry.category == 5 then + local pCat = entry.patternCategory + local pattern = entry.pattern + local anchorParam = (pattern == 2 or pattern == 6 or pattern == 7 or pattern > 9) and player or pos() + local safe = currentSettings.PvpSafe and spellPatterns[pCat][entry.pattern][2] or false + local monsterAmount = pCat ~= 8 and getMonstersInArea(entry.category, anchorParam, spellPatterns[pCat][entry.pattern][1], entry.minHp, entry.maxHp, safe, entry.monsters) + if (pattern ~= 8 and (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count)) or (pattern == 8 and bestSide >= entry.count and (not currentSettings.PvpSafe or getPlayers(2) == 0)) then + if (not currentSettings.BlackListSafe or not isBlackListedPlayerInRange(currentSettings.AntiRsRange)) and (not currentSettings.Kills or killsToRs() > currentSettings.KillsAmount) then + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + end + elseif entry.category == 2 then + local pCat = entry.patternCategory + local safe = currentSettings.PvpSafe and spellPatterns[pCat][entry.pattern][2] or false + local data = getBestTileByPattern(spellPatterns[pCat][entry.pattern][1], entry.minHp, entry.maxHp, safe, entry.monsters) + local monsterAmount + local pos + if data then + monsterAmount = data.amount + pos = data.pos + end + if monsterAmount and (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count) then + if (not currentSettings.BlackListSafe or not isBlackListedPlayerInRange(currentSettings.AntiRsRange)) and (not currentSettings.Kills or killsToRs() > currentSettings.KillsAmount) then + return useWith(attackData, g_map.getTile(pos):getTopUseThing()) + end + end + end + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.otui new file mode 100644 index 0000000000..f6329f474c --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/AttackBot.otui @@ -0,0 +1,624 @@ +AttackEntry < UIWidget + background-color: alpha + text-offset: 35 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + + UIItem + id: id + anchors.left: prev.right + anchors.verticalCenter: parent.verticalCenter + size: 16 16 + focusable: false + visible: false + + UIWidget + id: spell + anchors.left: enabled.right + anchors.verticalCenter: parent.verticalCenter + size: 12 12 + margin-left: 1 + image-source: /images/game/dangerous + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + width: 15 + height: 15 + +AttackBotBotPanel < Panel + height: 38 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('AttackBot') + + Button + id: settings + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + Button + id: 1 + anchors.top: prev.bottom + anchors.left: parent.left + text: 1 + margin-right: 2 + margin-top: 4 + size: 17 17 + + Button + id: 2 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 2 + margin-left: 4 + size: 17 17 + + Button + id: 3 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 3 + margin-left: 4 + size: 17 17 + + Button + id: 4 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 4 + margin-left: 4 + size: 17 17 + + Button + id: 5 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 5 + margin-left: 4 + size: 17 17 + + Label + id: name + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + text-align: center + margin-left: 4 + height: 17 + text: Profile #1 + background: #292A2A + +CategoryLabel < Panel + size: 315 15 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 1 + + Label + id: description + anchors.fill: parent + text-align: center + text: Area Rune (avalanche, great fireball, etc) + font: verdana-11px-rounded + background: #363636 + +SourceLabel < Panel + size: 105 15 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 1 + + Label + id: description + anchors.fill: parent + text-align: center + text: Monster Name + font: verdana-11px-rounded + background: #363636 + +RangeLabel < Panel + size: 323 15 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 1 + + Label + id: description + anchors.fill: parent + text-align: center + text: 5 Sqm + font: verdana-11px-rounded + background: #363636 + +PreButton < PreviousButton + background: #363636 + height: 15 + +NexButton < NextButton + background: #363636 + height: 15 + +AttackBotPanel < Panel + size: 500 200 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 5 + + TextList + id: entryList + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 3 + size: 430 100 + vertical-scrollbar: entryListScrollBar + + VerticalScrollBar + id: entryListScrollBar + anchors.top: entryList.top + anchors.bottom: entryList.bottom + anchors.right: entryList.right + step: 14 + pixels-scroll: true + + PreButton + id: previousCategory + anchors.left: entryList.left + anchors.top: entryList.bottom + margin-top: 8 + + NexButton + id: nextCategory + anchors.left: category.right + anchors.top: entryList.bottom + margin-top: 8 + margin-left: 2 + + CategoryLabel + id: category + anchors.top: entryList.bottom + anchors.left: previousCategory.right + anchors.verticalCenter: previousCategory.verticalCenter + margin-left: 3 + + PreButton + id: previousSource + anchors.left: entryList.left + anchors.top: category.bottom + margin-top: 8 + + NexButton + id: nextSource + anchors.left: source.right + anchors.top: category.bottom + margin-top: 8 + margin-left: 2 + + SourceLabel + id: source + anchors.top: category.bottom + anchors.left: previousSource.right + anchors.verticalCenter: previousSource.verticalCenter + margin-left: 3 + + PreButton + id: previousRange + anchors.left: nextSource.right + anchors.verticalCenter: nextSource.verticalCenter + margin-left: 8 + + NexButton + id: nextRange + anchors.left: range.right + anchors.verticalCenter: range.verticalCenter + margin-left: 2 + + RangeLabel + id: range + anchors.left: previousRange.right + anchors.verticalCenter: previousRange.verticalCenter + margin-left: 3 + + TextEdit + id: monsters + anchors.left: entryList.left + anchors.top: range.bottom + margin-top: 5 + size: 405 15 + text: monster names + font: cipsoftFont + background: #363636 + + Label + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 6 + margin-left: 3 + text-align: center + text: Mana%: + font: verdana-11px-rounded + + SpinBox + id: manaPercent + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 30 20 + minimum: 0 + maximum: 99 + step: 1 + editable: true + focusable: true + + Label + anchors.left: prev.right + margin-left: 7 + anchors.verticalCenter: prev.verticalCenter + text: Creatures: + font: verdana-11px-rounded + + SpinBox + id: creatures + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 30 20 + minimum: 1 + maximum: 99 + step: 1 + editable: true + focusable: true + + CheckBox + id: orMore + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + tooltip: or more creatures + + Label + anchors.left: prev.right + margin-left: 7 + anchors.verticalCenter: prev.verticalCenter + text: HP: + font: verdana-11px-rounded + + SpinBox + id: minHp + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 40 20 + minimum: 0 + maximum: 99 + value: 0 + editable: true + focusable: true + + Label + anchors.left: prev.right + margin-left: 4 + anchors.verticalCenter: prev.verticalCenter + text: - + font: verdana-11px-rounded + + SpinBox + id: maxHp + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 40 20 + minimum: 1 + maximum: 100 + value: 100 + editable: true + focusable: true + + Label + anchors.left: prev.right + margin-left: 7 + anchors.verticalCenter: prev.verticalCenter + text: CD: + font: verdana-11px-rounded + + SpinBox + id: cooldown + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 60 20 + minimum: 0 + maximum: 999999 + step: 100 + value: 0 + editable: true + focusable: true + + Button + id: up + anchors.right: parent.right + anchors.top: entryList.bottom + size: 60 17 + text: Move Up + text-align: center + font: cipsoftFont + margin-top: 7 + margin-right: 8 + + Button + id: down + anchors.right: prev.left + anchors.verticalCenter: prev.verticalCenter + size: 60 17 + margin-right: 5 + text: Move Down + text-align: center + font: cipsoftFont + + Button + id: addEntry + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 40 19 + text-align: center + text: New + font: cipsoftFont + + BotItem + id: itemId + anchors.right: addEntry.left + margin-right: 5 + anchors.bottom: parent.bottom + margin-bottom: 2 + tooltip: drag item here on press to open window + + TextEdit + id: spellName + anchors.top: monsters.top + anchors.left: monsters.right + anchors.right: parent.right + margin-left: 5 + height: 15 + text: spell name + background: #363636 + font: cipsoftFont + visible: false + +SettingsPanel < Panel + size: 500 200 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 10 + + VerticalSeparator + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: Visible.right + margin-left: 10 + margin-top: 5 + margin-bottom: 5 + + Label + anchors.top: parent.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 10 + text-align: center + font: verdana-11px-rounded + text: Profile: + + TextEdit + id: profileName + anchors.top: prev.bottom + margin-top: 3 + anchors.left: prev.left + anchors.right: prev.right + margin-left: 20 + margin-right: 20 + + Button + id: resetSettings + anchors.right: parent.right + anchors.bottom: parent.bottom + text-align: center + text: Reset Settings + + CheckBox + id: IgnoreMana + anchors.top: parent.top + anchors.left: parent.left + margin-top: 5 + width: 200 + text: Check RL Tibia conditions + + CheckBox + id: Kills + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 200 + height: 22 + text: Don't use area attacks if less than kills to red skull + text-wrap: true + text-align: left + + SpinBox + id: KillsAmount + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.left: prev.right + text-align: left + width: 30 + minimum: 1 + maximum: 10 + focusable: true + margin-left: 5 + + CheckBox + id: Rotate + anchors.top: Kills.bottom + anchors.left: Kills.left + margin-top: 8 + width: 220 + text: Turn to side with most monsters + + CheckBox + id: Cooldown + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 220 + text: Check spell cooldowns + + CheckBox + id: Visible + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: Items must be visible (recommended) + + CheckBox + id: PvpMode + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: PVP mode + + CheckBox + id: PvpSafe + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: PVP safe + + CheckBox + id: Training + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: Stop when attacking trainers + + CheckBox + id: BlackListSafe + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 200 + height: 18 + text: Stop if Anti-RS player in range + + SpinBox + id: AntiRsRange + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.left: prev.right + text-align: center + width: 50 + minimum: 1 + maximum: 10 + focusable: true + margin-left: 5 + +AttackBotWindow < MainWindow + size: 535 300 + padding: 15 + text: AttackBot v2 + @onEscape: self:hide() + + Label + id: mainLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 10 + margin-left: 2 + !text: tr('More important methods come first (Example: Exori gran above Exori)') + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + SettingsPanel + id: settingsPanel + anchors.top: prev.bottom + margin-top: 10 + anchors.left: parent.left + margin-left: 2 + + Label + id: settingsLabel + anchors.verticalCenter: prev.top + anchors.left: prev.left + margin-left: 3 + text: Settings + color: #fe4400 + font: verdana-11px-rounded + + AttackBotPanel + id: mainPanel + anchors.top: mainLabel.bottom + margin-top: 10 + anchors.left: parent.left + margin-left: 2 + visible: false + + Label + id: shooterLabel + anchors.verticalCenter: prev.top + anchors.left: prev.left + margin-left: 3 + text: Spell Shooter + color: #fe4400 + font: verdana-11px-rounded + visible: false + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: closeButton.top + margin-bottom: 10 + + Button + id: closeButton + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + text: Close + font: cipsoftFont + + Button + id: settings + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + size: 50 21 + font: cipsoftFont + text: Settings \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.lua new file mode 100644 index 0000000000..1be51714e0 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.lua @@ -0,0 +1,203 @@ +setDefaultTab("Main") + +local panelName = "BOTserver" +local ui = setupUI([[ +Panel + height: 18 + Button + id: botServer + anchors.left: parent.left + anchors.right: parent.right + text-align: center + height: 18 + !text: tr('BotServer') +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = { + manaInfo = true, + mwallInfo = true, + vocation = true, + outfit = false, + broadcasts = true +} +end + +local config = storage[panelName] + +if not storage.BotServerChannel then + math.randomseed(os.time()) + storage.BotServerChannel = tostring(math.random(1000000000000,9999999999999)) +end + +local channel = tostring(storage.BotServerChannel) +BotServer.init(name(), channel) + +vBot.BotServerMembers = {} + +rootWidget = g_ui.getRootWidget() +if rootWidget then + botServerWindow = g_ui.createWidget('BotServerWindow', rootWidget) + botServerWindow:hide() + + + botServerWindow.Data.Channel:setText(storage.BotServerChannel) + botServerWindow.Data.Channel.onTextChange = function(widget, text) + storage.BotServerChannel = text + end + botServerWindow.Data.Random.onClick = function(widget) + storage.BotServerChannel = tostring(math.random(1000000000000,9999999999999)) + botServerWindow.Data.Channel:setText(storage.BotServerChannel) + end + botServerWindow.Features.Feature1:setOn(config.manaInfo) + botServerWindow.Features.Feature1.onClick = function(widget) + config.manaInfo = not config.manaInfo + widget:setOn(config.manaInfo) + end + botServerWindow.Features.Feature2:setOn(config.mwallInfo) + botServerWindow.Features.Feature2.onClick = function(widget) + config.mwallInfo = not config.mwallInfo + widget:setOn(config.mwallInfo) + end + botServerWindow.Features.Feature3:setOn(config.vocation) + botServerWindow.Features.Feature3.onClick = function(widget) + config.vocation = not config.vocation + if config.vocation then + BotServer.send("voc", player:getVocation()) + end + widget:setOn(config.vocation) + end + botServerWindow.Features.Feature4:setOn(config.outfit) + botServerWindow.Features.Feature4.onClick = function(widget) + config.outfit = not config.outfit + widget:setOn(config.outfit) + end + botServerWindow.Features.Feature5:setOn(config.broadcasts) + botServerWindow.Features.Feature5.onClick = function(widget) + config.broadcasts = not config.broadcasts + widget:setOn(config.broadcasts) + end + botServerWindow.Features.Broadcast.onClick = function(widget) + if BotServer._websocket then + BotServer.send("broadcast", botServerWindow.Features.broadcastText:getText()) + end + botServerWindow.Features.broadcastText:setText('') + end +end + +function updateStatusText() + if BotServer._websocket then + botServerWindow.Data.ServerStatus:setText("CONNECTED") + if serverCount then + botServerWindow.Data.Members:setText("Members: "..#serverCount) + if ServerMembers then + local text = "" + local regex = [["([a-z 'A-z-]*)"*]] + local re = regexMatch(ServerMembers, regex) + --re[name][2] + for i=1,#re do + if i == 1 then + text = re[i][2] + else + text = text .. "\n" .. re[i][2] + end + end + botServerWindow.Data.Members:setTooltip(text) + end + end + else + botServerWindow.Data.ServerStatus:setText("DISCONNECTED") + botServerWindow.Data.Participants:setText("-") + end +end + +macro(2000, function() + if BotServer._websocket then + BotServer.send("list") + end + updateStatusText() +end) + +local regex = [["(.*?)"]] +BotServer.listen("list", function(name, data) + serverCount = regexMatch(json.encode(data), regex) + ServerMembers = json.encode(data) +end) + +ui.botServer.onClick = function(widget) + botServerWindow:show() + botServerWindow:raise() + botServerWindow:focus() +end + +botServerWindow.closeButton.onClick = function(widget) + botServerWindow:hide() +end + +-- scripts + +-- mwalls +config.mwalls = {} +BotServer.listen("mwall", function(name, message) + if config.mwallInfo then + if not config.mwalls[message["pos"]] or config.mwalls[message["pos"]] < now then + config.mwalls[message["pos"]] = now + message["duration"] - 150 -- 150 is latency correction + end + end +end) + +onAddThing(function(tile, thing) + if config.mwallInfo then + if thing:isItem() and thing:getId() == 2129 then + local pos = tile:getPosition().x .. "," .. tile:getPosition().y .. "," .. tile:getPosition().z + if not config.mwalls[pos] or config.mwalls[pos] < now then + config.mwalls[pos] = now + 20000 + BotServer.send("mwall", {pos=pos, duration=20000}) + end + end + end +end) + +-- mana +local lastMana = 0 +macro(500, function() + if config.manaInfo then + if manapercent() ~= lastMana then + lastMana = manapercent() + BotServer.send("mana", {mana=lastMana}) + end + end +end) + +BotServer.listen("mana", function(name, message) + if config.manaInfo then + local creature = getPlayerByName(name) + if creature then + creature:setManaPercent(message["mana"]) + end + end +end) + +-- vocation +if config.vocation then + BotServer.send("voc", player:getVocation()) + BotServer.send("voc", "yes") +end + +BotServer.listen("voc", function(name, message) + if message == "yes" and config.vocation then + BotServer.send("voc", player:getVocation()) + else + vBot.BotServerMembers[name] = message + end +end) + +-- broadcast +BotServer.listen("broadcast", function(name, message) + if config.broadcasts then + broadcastMessage(name..": "..message) + end +end) + +addSeparator() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.otui new file mode 100644 index 0000000000..ff6874b1a8 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/BotServer.otui @@ -0,0 +1,188 @@ +BotServerData < Panel + size: 340 70 + image-source: /images/ui/window + image-border: 6 + padding: 3 + + Label + id: label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-align: center + !text: tr("BotServer Data") + + Label + id: label + anchors.top: parent.top + anchors.left: parent.left + margin-top: 23 + text-align: center + text: Channel Name: + margin-left: 6 + + TextEdit + id: Channel + anchors.top: parent.top + anchors.left: prev.right + margin-top: 20 + width: 150 + margin-left: 5 + text-align: center + + Button + id: Random + anchors.left: prev.right + anchors.top: prev.top + anchors.right: parent.right + text-align: center + text: Randomize + margin-left: 6 + margin-right: 6 + + Label + id: label + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-left: 6 + margin-bottom: 4 + text-align: center + text: Status: + + BotLabel + id: ServerStatus + anchors.left: prev.right + anchors.bottom: parent.bottom + margin-left: 10 + margin-bottom: 4 + text-align: center + text: CONNECTED + + BotLabel + id: Participants + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-right: 8 + margin-bottom: 4 + text-align: center + + UIWidget + id: Members + anchors.right: Participants.left + anchors.bottom: parent.bottom + size: 80 21 + text-align: center + text: Members: + +FeaturePanel < Panel + size: 340 150 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 3 + + Label + id: title + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + text-align: center + text: Features + + HorizontalSeparator + id: sep + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + + BotSwitch + id: Feature1 + anchors.top: prev.bottom + anchors.left: parent.left + margin-left: 3 + margin-top: 5 + text: Mana info + + BotSwitch + id: Feature2 + anchors.top: sep.bottom + anchors.left: prev.right + margin-top: 5 + margin-left: 5 + text: MWall info + + BotSwitch + id: Feature3 + anchors.top: sep.bottom + anchors.left: prev.right + margin-top: 5 + margin-left: 5 + text: Send Vocation + + BotSwitch + id: Feature4 + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 3 + margin-left: 3 + text: Outfit Vocation + + BotSwitch + id: Feature5 + anchors.bottom: prev.bottom + anchors.left: prev.right + margin-top: 3 + margin-left: 5 + text: Broadcasts + + + TextEdit + id: broadcastText + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-left: 3 + margin-bottom: 3 + margin-right: 80 + + Button + id: Broadcast + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-right: 3 + margin-left: 3 + height: 22 + text: Broadcast + +BotServerWindow < MainWindow + !text: tr('BotServer') + size: 370 310 + @onEscape: self:hide() + + BotServerData + id: Data + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + + FeaturePanel + id: Features + anchors.top: prev.bottom + anchors.horizontalCenter: parent.horizontalCenter + margin-top: 10 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.lua new file mode 100644 index 0000000000..d9846d0bce --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.lua @@ -0,0 +1,262 @@ +setDefaultTab("HP") +local panelName = "ConditionPanel" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Conditions') + + Button + id: conditionList + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + ]]) + ui:setId(panelName) + + if not HealBotConfig[panelName] then + HealBotConfig[panelName] = { + enabled = false, + curePosion = false, + poisonCost = 20, + cureCurse = false, + curseCost = 80, + cureBleed = false, + bleedCost = 45, + cureBurn = false, + burnCost = 30, + cureElectrify = false, + electrifyCost = 22, + cureParalyse = false, + paralyseCost = 40, + paralyseSpell = "utani hur", + holdHaste = false, + hasteCost = 40, + hasteSpell = "utani hur", + holdUtamo = false, + utamoCost = 40, + holdUtana = false, + utanaCost = 440, + holdUtura = false, + uturaType = "", + uturaCost = 100, + ignoreInPz = true, + stopHaste = false + } + end + + local config = HealBotConfig[panelName] + + ui.title:setOn(config.enabled) + ui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) + vBotConfigSave("heal") + end + + ui.conditionList.onClick = function(widget) + conditionsWindow:show() + conditionsWindow:raise() + conditionsWindow:focus() + end + + + + local rootWidget = g_ui.getRootWidget() + if rootWidget then + conditionsWindow = UI.createWindow('ConditionsWindow', rootWidget) + conditionsWindow:hide() + + + conditionsWindow.onVisibilityChange = function(widget, visible) + if not visible then + vBotConfigSave("heal") + end + end + + -- text edits + conditionsWindow.Cure.PoisonCost:setText(config.poisonCost) + conditionsWindow.Cure.PoisonCost.onTextChange = function(widget, text) + config.poisonCost = tonumber(text) + end + + conditionsWindow.Cure.CurseCost:setText(config.curseCost) + conditionsWindow.Cure.CurseCost.onTextChange = function(widget, text) + config.curseCost = tonumber(text) + end + + conditionsWindow.Cure.BleedCost:setText(config.bleedCost) + conditionsWindow.Cure.BleedCost.onTextChange = function(widget, text) + config.bleedCost = tonumber(text) + end + + conditionsWindow.Cure.BurnCost:setText(config.burnCost) + conditionsWindow.Cure.BurnCost.onTextChange = function(widget, text) + config.burnCost = tonumber(text) + end + + conditionsWindow.Cure.ElectrifyCost:setText(config.electrifyCost) + conditionsWindow.Cure.ElectrifyCost.onTextChange = function(widget, text) + config.electrifyCost = tonumber(text) + end + + conditionsWindow.Cure.ParalyseCost:setText(config.paralyseCost) + conditionsWindow.Cure.ParalyseCost.onTextChange = function(widget, text) + config.paralyseCost = tonumber(text) + end + + conditionsWindow.Cure.ParalyseSpell:setText(config.paralyseSpell) + conditionsWindow.Cure.ParalyseSpell.onTextChange = function(widget, text) + config.paralyseSpell = text + end + + conditionsWindow.Hold.HasteSpell:setText(config.hasteSpell) + conditionsWindow.Hold.HasteSpell.onTextChange = function(widget, text) + config.hasteSpell = text + end + + conditionsWindow.Hold.HasteCost:setText(config.hasteCost) + conditionsWindow.Hold.HasteCost.onTextChange = function(widget, text) + config.hasteCost = tonumber(text) + end + + conditionsWindow.Hold.UtamoCost:setText(config.utamoCost) + conditionsWindow.Hold.UtamoCost.onTextChange = function(widget, text) + config.utamoCost = tonumber(text) + end + + conditionsWindow.Hold.UtanaCost:setText(config.utanaCost) + conditionsWindow.Hold.UtanaCost.onTextChange = function(widget, text) + config.utanaCost = tonumber(text) + end + + conditionsWindow.Hold.UturaCost:setText(config.uturaCost) + conditionsWindow.Hold.UturaCost.onTextChange = function(widget, text) + config.uturaCost = tonumber(text) + end + + -- combo box + conditionsWindow.Hold.UturaType:setOption(config.uturaType) + conditionsWindow.Hold.UturaType.onOptionChange = function(widget) + config.uturaType = widget:getCurrentOption().text + end + + -- checkboxes + conditionsWindow.Cure.CurePoison:setChecked(config.curePoison) + conditionsWindow.Cure.CurePoison.onClick = function(widget) + config.curePoison = not config.curePoison + widget:setChecked(config.curePoison) + end + + conditionsWindow.Cure.CureCurse:setChecked(config.cureCurse) + conditionsWindow.Cure.CureCurse.onClick = function(widget) + config.cureCurse = not config.cureCurse + widget:setChecked(config.cureCurse) + end + + conditionsWindow.Cure.CureBleed:setChecked(config.cureBleed) + conditionsWindow.Cure.CureBleed.onClick = function(widget) + config.cureBleed = not config.cureBleed + widget:setChecked(config.cureBleed) + end + + conditionsWindow.Cure.CureBurn:setChecked(config.cureBurn) + conditionsWindow.Cure.CureBurn.onClick = function(widget) + config.cureBurn = not config.cureBurn + widget:setChecked(config.cureBurn) + end + + conditionsWindow.Cure.CureElectrify:setChecked(config.cureElectrify) + conditionsWindow.Cure.CureElectrify.onClick = function(widget) + config.cureElectrify = not config.cureElectrify + widget:setChecked(config.cureElectrify) + end + + conditionsWindow.Cure.CureParalyse:setChecked(config.cureParalyse) + conditionsWindow.Cure.CureParalyse.onClick = function(widget) + config.cureParalyse = not config.cureParalyse + widget:setChecked(config.cureParalyse) + end + + conditionsWindow.Hold.HoldHaste:setChecked(config.holdHaste) + conditionsWindow.Hold.HoldHaste.onClick = function(widget) + config.holdHaste = not config.holdHaste + widget:setChecked(config.holdHaste) + end + + conditionsWindow.Hold.HoldUtamo:setChecked(config.holdUtamo) + conditionsWindow.Hold.HoldUtamo.onClick = function(widget) + config.holdUtamo = not config.holdUtamo + widget:setChecked(config.holdUtamo) + end + + conditionsWindow.Hold.HoldUtana:setChecked(config.holdUtana) + conditionsWindow.Hold.HoldUtana.onClick = function(widget) + config.holdUtana = not config.holdUtana + widget:setChecked(config.holdUtana) + end + + conditionsWindow.Hold.HoldUtura:setChecked(config.holdUtura) + conditionsWindow.Hold.HoldUtura.onClick = function(widget) + config.holdUtura = not config.holdUtura + widget:setChecked(config.holdUtura) + end + + conditionsWindow.Hold.IgnoreInPz:setChecked(config.ignoreInPz) + conditionsWindow.Hold.IgnoreInPz.onClick = function(widget) + config.ignoreInPz = not config.ignoreInPz + widget:setChecked(config.ignoreInPz) + end + + conditionsWindow.Hold.StopHaste:setChecked(config.stopHaste) + conditionsWindow.Hold.StopHaste.onClick = function(widget) + config.stopHaste = not config.stopHaste + widget:setChecked(config.stopHaste) + end + + -- buttons + conditionsWindow.closeButton.onClick = function(widget) + conditionsWindow:hide() + end + + Conditions = {} + Conditions.show = function() + conditionsWindow:show() + conditionsWindow:raise() + conditionsWindow:focus() + end + end + + local utanaCast = nil + macro(500, function() + if not config.enabled or modules.game_cooldown.isGroupCooldownIconActive(2) then return end + if hppercent() > 95 then + if config.curePoison and mana() >= config.poisonCost and isPoisioned() then say("exana pox") + elseif config.cureCurse and mana() >= config.curseCost and isCursed() then say("exana mort") + elseif config.cureBleed and mana() >= config.bleedCost and isBleeding() then say("exana kor") + elseif config.cureBurn and mana() >= config.burnCost and isBurning() then say("exana flam") + elseif config.cureElectrify and mana() >= config.electrifyCost and isEnergized() then say("exana vis") + end + end + if (not config.ignoreInPz or not isInPz()) and config.holdUtura and mana() >= config.uturaCost and canCast(config.uturaType) and hppercent() < 90 then say(config.uturaType) + elseif (not config.ignoreInPz or not isInPz()) and config.holdUtana and mana() >= config.utanaCost and (not utanaCast or (now - utanaCast > 120000)) then say("utana vid") utanaCast = now + end + end) + + macro(50, function() + if not config.enabled then return end + if (not config.ignoreInPz or not isInPz()) and config.holdUtamo and mana() >= config.utamoCost and not hasManaShield() then say("utamo vita") + elseif ((not config.ignoreInPz or not isInPz()) and standTime() < 5000 and config.holdHaste and mana() >= config.hasteCost and not hasHaste() and not getSpellCoolDown(config.hasteSpell) and (not target() or not config.stopHaste or TargetBot.isCaveBotActionAllowed())) and standTime() < 3000 then say(config.hasteSpell) + elseif config.cureParalyse and mana() >= config.paralyseCost and isParalyzed() and not getSpellCoolDown(config.paralyseSpell) then say(config.paralyseSpell) + end + end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.otui new file mode 100644 index 0000000000..ee8d43bec6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/Conditions.otui @@ -0,0 +1,463 @@ +UturaComboBoxPopupMenu < ComboBoxPopupMenu +UturaComboBoxPopupMenuButton < ComboBoxPopupMenuButton +UturaComboBox < ComboBox + @onSetup: | + self:addOption("Utura") + self:addOption("Utura Gran") + +CureConditions < Panel + id: Cure + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 200 190 + + Label + id: label1 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 10 + margin-left: 5 + text: Poison + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label11 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 40 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: PoisonCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CurePoison + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label2 + anchors.left: label1.left + anchors.top: label1.bottom + margin-top: 10 + text: Curse + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label22 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 44 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: CurseCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureCurse + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label3 + anchors.left: label2.left + anchors.top: label2.bottom + margin-top: 10 + text: Bleed + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label33 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 46 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: BleedCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureBleed + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label4 + anchors.left: label3.left + anchors.top: label3.bottom + margin-top: 10 + text: Burn + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label44 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 50 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: BurnCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureBurn + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label5 + anchors.left: label4.left + anchors.top: label4.bottom + margin-top: 10 + text: Electify + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label55 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 33 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: ElectrifyCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureElectrify + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label6 + anchors.left: label5.left + anchors.top: label5.bottom + margin-top: 10 + text: Paralyse + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label66 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 26 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: ParalyseCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureParalyse + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label7 + anchors.left: label6.left + anchors.top: label6.bottom + margin-top: 10 + margin-left: 12 + text: Spell: + font: verdana-11px-rounded + + TextEdit + id: ParalyseSpell + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 10 + width: 100 + font: verdana-11px-rounded + +HoldConditions < Panel + id: Hold + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 200 190 + + Label + id: label1 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 10 + margin-left: 5 + text: Haste + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label11 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 44 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: HasteCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldHaste + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label2 + anchors.left: label1.left + anchors.top: label1.bottom + margin-top: 10 + margin-left: 12 + text: Spell: + font: verdana-11px-rounded + + TextEdit + id: HasteSpell + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 10 + width: 100 + font: verdana-11px-rounded + + Label + id: label3 + anchors.left: label1.left + anchors.top: label2.bottom + margin-top: 10 + text: Utana Vid + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label33 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 21 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: UtanaCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldUtana + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label4 + anchors.left: label3.left + anchors.top: label3.bottom + margin-top: 10 + text: Utamo Vita + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label44 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 12 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: UtamoCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldUtamo + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label5 + anchors.left: label4.left + anchors.top: label4.bottom + margin-top: 10 + text: Recovery + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label55 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 20 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: UturaCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldUtura + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label6 + anchors.left: label5.left + anchors.top: label5.bottom + margin-top: 10 + margin-left: 12 + text: Spell: + font: verdana-11px-rounded + + UturaComboBox + id: UturaType + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 10 + width: 100 + font: verdana-11px-rounded + + CheckBox + id: IgnoreInPz + anchors.left: label5.left + anchors.top: label6.bottom + margin-top: 12 + + Label + anchors.verticalCenter: IgnoreInPz.verticalCenter + anchors.left: prev.right + margin-top: 3 + margin-left: 5 + text: Don't Cast in Protection Zones + font: cipsoftFont + + CheckBox + id: StopHaste + anchors.horizontalCenter: IgnoreInPz.horizontalCenter + anchors.top: IgnoreInPz.bottom + margin-top: 8 + + Label + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-top: 3 + margin-left: 5 + text: Stop Haste if TargetBot Is Active + font: cipsoftFont + +ConditionsWindow < MainWindow + !text: tr('Condition Manager') + size: 445 280 + @onEscape: self:hide() + + CureConditions + id: Cure + anchors.top: parent.top + anchors.left: parent.left + margin-top: 7 + + Label + id: label + anchors.top: parent.top + anchors.left: parent.left + text: Cure Conditions + color: #88e3dd + margin-left: 10 + font: verdana-11px-rounded + + HoldConditions + id: Hold + anchors.top: parent.top + anchors.right: parent.right + margin-top: 7 + + Label + id: label + anchors.top: parent.top + anchors.right: parent.right + text: Hold Conditions + color: #88e3dd + margin-right: 100 + font: verdana-11px-rounded + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/Containers.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/Containers.lua new file mode 100644 index 0000000000..827d0a585d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/Containers.lua @@ -0,0 +1,674 @@ +setDefaultTab("Tools") +local panelName = "renameContainers" +if type(storage[panelName]) ~= "table" then + storage[panelName] = { + enabled = false; + height = 170, + purse = true; + list = { + { + value = "Main Backpack", + enabled = true, + item = 9601, + min = false, + items = { 3081, 3048 } + }, + { + value = "Runes", + enabled = true, + item = 2866, + min = true, + items = { 3161, 3180 } + }, + { + value = "Money", + enabled = true, + item = 2871, + min = true, + items = { 3031, 3035, 3043 } + }, + { + value = "Purse", + enabled = true, + item = 23396, + min = true, + items = {} + }, + } + } +end + +local config = storage[panelName] + +UI.Separator() +local renameContui = setupUI([[ +Panel + height: 50 + + Label + text-align: center + text: Container Panel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + font: verdana-11px-rounded + + BotSwitch + id: title + anchors.top: prev.bottom + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Open Minimised') + font: verdana-11px-rounded + + Button + id: editContList + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + font: verdana-11px-rounded + + Button + id: reopenCont + !text: tr('Reopen All') + anchors.left: parent.left + anchors.top: prev.bottom + anchors.right: parent.horizontalCenter + margin-right: 2 + height: 17 + margin-top: 3 + font: verdana-11px-rounded + + Button + id: minimiseCont + !text: tr('Minimise All') + anchors.top: prev.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + margin-right: 2 + height: 17 + font: verdana-11px-rounded + ]]) +renameContui:setId(panelName) + +g_ui.loadUIFromString([[ +BackpackName < Label + background-color: alpha + text-offset: 18 2 + focusable: true + height: 17 + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 1 + margin-left: 3 + + $focus: + background-color: #00000055 + + Button + id: state + !text: tr('M') + anchors.right: remove.left + anchors.verticalCenter: parent.verticalCenter + margin-right: 1 + width: 15 + height: 15 + + Button + id: remove + !text: tr('X') + !tooltip: tr('Remove') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + margin-right: 15 + width: 15 + height: 15 + + Button + id: openNext + !text: tr('N') + anchors.right: state.left + anchors.verticalCenter: parent.verticalCenter + margin-right: 1 + width: 15 + height: 15 + tooltip: Open container inside with the same ID. + +ContListsWindow < MainWindow + !text: tr('Container Names') + size: 465 170 + @onEscape: self:hide() + + TextList + id: itemList + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: separator.top + width: 200 + margin-bottom: 6 + margin-top: 3 + margin-left: 3 + vertical-scrollbar: itemListScrollBar + + VerticalScrollBar + id: itemListScrollBar + anchors.top: itemList.top + anchors.bottom: itemList.bottom + anchors.right: itemList.right + step: 14 + pixels-scroll: true + + VerticalSeparator + id: sep + anchors.top: parent.top + anchors.left: itemList.right + anchors.bottom: separator.top + margin-top: 3 + margin-bottom: 6 + margin-left: 10 + + Label + id: lblName + anchors.left: sep.right + anchors.top: sep.top + width: 70 + text: Name: + margin-left: 10 + margin-top: 3 + font: verdana-11px-rounded + + TextEdit + id: contName + anchors.left: lblName.right + anchors.top: sep.top + anchors.right: parent.right + font: verdana-11px-rounded + + Label + id: lblCont + anchors.left: lblName.left + anchors.verticalCenter: contId.verticalCenter + width: 70 + text: Container: + font: verdana-11px-rounded + + BotItem + id: contId + anchors.left: contName.left + anchors.top: contName.bottom + margin-top: 3 + + BotContainer + id: sortList + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + anchors.bottom: separator.top + margin-bottom: 6 + margin-top: 3 + + Label + anchors.left: lblCont.left + anchors.verticalCenter: sortList.verticalCenter + width: 70 + text: Items: + font: verdana-11px-rounded + + Button + id: addItem + anchors.right: contName.right + anchors.top: contName.bottom + margin-top: 5 + text: Add + width: 40 + font: cipsoftFont + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + CheckBox + id: purse + anchors.left: parent.left + anchors.bottom: parent.bottom + text: Open Purse + tooltip: Opens Store/Charm Purse + width: 85 + height: 15 + margin-top: 2 + margin-left: 3 + font: verdana-11px-rounded + + CheckBox + id: sort + anchors.left: prev.right + anchors.bottom: parent.bottom + text: Sort Items + tooltip: Sort items based on items widget + width: 85 + height: 15 + margin-top: 2 + margin-left: 15 + font: verdana-11px-rounded + + CheckBox + id: forceOpen + anchors.left: prev.right + anchors.bottom: parent.bottom + text: Keep Open + tooltip: Will keep open containers all the time + width: 85 + height: 15 + margin-top: 2 + margin-left: 15 + font: verdana-11px-rounded + + CheckBox + id: lootBag + anchors.left: prev.right + anchors.bottom: parent.bottom + text: Loot Bag + tooltip: Open Loot Bag (gunzodus franchaise) + width: 85 + height: 15 + margin-top: 2 + margin-left: 15 + font: verdana-11px-rounded + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + + ResizeBorder + id: bottomResizeBorder + anchors.fill: separator + height: 3 + minimum: 170 + maximum: 245 + margin-left: 3 + margin-right: 3 + background: #ffffff88 +]]) + +function findItemsInArray(t, tfind) + local tArray = {} + for x,v in pairs(t) do + if type(v) == "table" then + local aItem = t[x].item + local aEnabled = t[x].enabled + if aItem then + if tfind and aItem == tfind then + return x + elseif not tfind then + if aEnabled then + table.insert(tArray, aItem) + end + end + end + end + end + if not tfind then return tArray end +end + +local lstBPs + + +local openContainer = function(id) + local t = {getRight(), getLeft(), getAmmo()} -- if more slots needed then add them here + for i=1,#t do + local slotItem = t[i] + if slotItem and slotItem:getId() == id then + return g_game.open(slotItem, nil) + end + end + + for i, container in pairs(g_game.getContainers()) do + for i, item in ipairs(container:getItems()) do + if item:isContainer() and item:getId() == id then + return g_game.open(item, nil) + end + end + end +end + +function reopenBackpacks() + lstBPs = findItemsInArray(config.list) + + for _, container in pairs(g_game.getContainers()) do g_game.close(container) end + bpItem = getBack() + if bpItem ~= nil then + g_game.open(bpItem) + end + + schedule(500, function() + local delay = 200 + + if config.purse then + local item = getPurse() + if item then + use(item) + end + end + for i=1,#lstBPs do + schedule(delay, function() + openContainer(lstBPs[i]) + end) + delay = delay + 250 + end + end) + +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + contListWindow = UI.createWindow('ContListsWindow', rootWidget) + contListWindow:hide() + + contListWindow.onGeometryChange = function(widget, old, new) + if old.height == 0 then return end + + config.height = new.height + end + + contListWindow:setHeight(config.height or 170) + + renameContui.editContList.onClick = function(widget) + contListWindow:show() + contListWindow:raise() + contListWindow:focus() + end + + renameContui.reopenCont.onClick = function(widget) + reopenBackpacks() + end + + renameContui.minimiseCont.onClick = function(widget) + for i, container in ipairs(getContainers()) do + local containerWindow = container.window + containerWindow:setContentHeight(34) + end + end + + renameContui.title:setOn(config.enabled) + renameContui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) + end + + contListWindow.closeButton.onClick = function(widget) + contListWindow:hide() + end + + contListWindow.purse.onClick = function(widget) + config.purse = not config.purse + contListWindow.purse:setChecked(config.purse) + end + contListWindow.purse:setChecked(config.purse) + + contListWindow.sort.onClick = function(widget) + config.sort = not config.sort + contListWindow.sort:setChecked(config.sort) + end + contListWindow.sort:setChecked(config.sort) + + contListWindow.forceOpen.onClick = function(widget) + config.forceOpen = not config.forceOpen + contListWindow.forceOpen:setChecked(config.forceOpen) + end + contListWindow.forceOpen:setChecked(config.forceOpen) + + contListWindow.lootBag.onClick = function(widget) + config.lootBag = not config.lootBag + contListWindow.lootBag:setChecked(config.lootBag) + end + contListWindow.lootBag:setChecked(config.lootBag) + + local function refreshSortList(k, t) + t = t or {} + UI.Container(function() + t = contListWindow.sortList:getItems() + config.list[k].items = t + end, true, nil, contListWindow.sortList) + contListWindow.sortList:setItems(t) + end + refreshSortList(t) + + local refreshContNames = function(tFocus) + local storageVal = config.list + if storageVal and #storageVal > 0 then + for i, child in pairs(contListWindow.itemList:getChildren()) do + child:destroy() + end + for k, entry in pairs(storageVal) do + local label = g_ui.createWidget("BackpackName", contListWindow.itemList) + label.onMouseRelease = function() + contListWindow.contId:setItemId(entry.item) + contListWindow.contName:setText(entry.value) + if not entry.items then + entry.items = {} + end + contListWindow.sortList:setItems(entry.items) + refreshSortList(k, entry.items) + end + label.enabled.onClick = function(widget) + entry.enabled = not entry.enabled + label.enabled:setChecked(entry.enabled) + label.enabled:setTooltip(entry.enabled and 'Disable' or 'Enable') + label.enabled:setImageColor(entry.enabled and '#00FF00' or '#FF0000') + end + label.remove.onClick = function(widget) + table.removevalue(config.list, entry) + label:destroy() + end + label.state:setChecked(entry.min) + label.state.onClick = function(widget) + entry.min = not entry.min + label.state:setChecked(entry.min) + label.state:setColor(entry.min and '#00FF00' or '#FF0000') + label.state:setTooltip(entry.min and 'Open Minimised' or 'Do not minimise') + end + label.openNext.onClick = function(widget) + entry.openNext = not entry.openNext + label.openNext:setChecked(entry.openNext) + label.openNext:setColor(entry.openNext and '#00FF00' or '#FF0000') + end + label:setText(entry.value) + label.enabled:setChecked(entry.enabled) + label.enabled:setTooltip(entry.enabled and 'Disable' or 'Enable') + label.enabled:setImageColor(entry.enabled and '#00FF00' or '#FF0000') + label.state:setColor(entry.min and '#00FF00' or '#FF0000') + label.state:setTooltip(entry.min and 'Open Minimised' or 'Do not minimise') + label.openNext:setColor(entry.openNext and '#00FF00' or '#FF0000') + + if tFocus and entry.item == tFocus then + tFocus = label + end + end + if tFocus then contListWindow.itemList:focusChild(tFocus) end + end + end + contListWindow.addItem.onClick = function(widget) + local id = contListWindow.contId:getItemId() + local trigger = contListWindow.contName:getText() + + if id > 100 and trigger:len() > 0 then + local ifind = findItemsInArray(config.list, id) + if ifind then + config.list[ifind] = { item = id, value = trigger, enabled = config.list[ifind].enabled, min = config.list[ifind].min, items = config.list[ifind].items} + else + table.insert(config.list, { item = id, value = trigger, enabled = true, min = false, items = {} }) + end + contListWindow.contId:setItemId(0) + contListWindow.contName:setText('') + contListWindow.contName:setColor('white') + contListWindow.contName:setImageColor('#ffffff') + contListWindow.contId:setImageColor('#ffffff') + refreshContNames(id) + else + contListWindow.contId:setImageColor('red') + contListWindow.contName:setImageColor('red') + contListWindow.contName:setColor('red') + end + end + refreshContNames() +end + +onContainerOpen(function(container, previousContainer) + if not container.window then return end + local containerWindow = container.window + if not previousContainer then + containerWindow:setContentHeight(34) + end + + local storageVal = config.list + if storageVal and #storageVal > 0 then + for _, entry in pairs(storageVal) do + if entry.enabled and string.find(container:getContainerItem():getId(), entry.item) then + if entry.min then + containerWindow:minimize() + end + if renameContui.title:isOn() then + containerWindow:setText(entry.value) + end + if entry.openNext then + for i, item in ipairs(container:getItems()) do + if item:getId() == entry.item then + local time = #storageVal * 250 + schedule(time, function() + time = time + 250 + g_game.open(item) + end) + end + end + end + end + end + end +end) + +local function nameContainersOnLogin() + for i, container in ipairs(getContainers()) do + if renameContui.title:isOn() then + if not container.window then return end + local containerWindow = container.window + local storageVal = config.list + if storageVal and #storageVal > 0 then + for _, entry in pairs(storageVal) do + if entry.enabled and string.find(container:getContainerItem():getId(), entry.item) then + containerWindow:setText(entry.value) + end + end + end + end + end +end +nameContainersOnLogin() + +local function moveItem(item, destination) + return g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), item:getCount()) +end + +local function properTable(t) + local r = {} + for _, entry in pairs(t) do + if type(entry) == "number" then + table.insert(r, entry) + else + table.insert(r, entry.id) + end + end + return r +end + +local mainLoop = macro(150, function(macro) + if not config.sort and not config.purse then return end + + local storageVal = config.list + for _, entry in pairs(storageVal) do + local dId = entry.item + local items = properTable(entry.items) + -- sorting + if config.sort then + for _, container in pairs(getContainers()) do + local cName = container:getName():lower() + if not cName:find("depot") and not cName:find("depot") and not cName:find("quiver") then + local cId = container:getContainerItem():getId() + for __, item in ipairs(container:getItems()) do + local id = item:getId() + if table.find(items, id) and cId ~= dId then + local destination = getContainerByItem(dId, true) + if destination and not containerIsFull(destination) then + return moveItem(item, destination) + end + end + end + end + end + end + -- keep open / purse 23396 + if config.forceOpen then + local container = getContainerByItem(dId) + if not container then + local t = {getBack(), getAmmo(), getFinger(), getNeck(), getLeft(), getRight()} + for i=1,#t do + local slot = t[i] + if slot and slot:getId() == dId then + return g_game.open(slot) + end + end + local cItem = findItem(dId) + if cItem then + return g_game.open(cItem) + end + end + end + end + if config.purse and config.forceOpen and not getContainerByItem(23396) then + return use(getPurse()) + end + if config.lootBag and config.forceOpen and not getContainerByItem(23721) then + if findItem(23721) then + g_game.open(findItem(23721), getContainerByItem(23396)) + else + return use(getPurse()) + end + end + macro:setOff() +end) + + +onContainerOpen(function(container, previousContainer) + mainLoop:setOn() +end) + +onAddItem(function(container, slot, item, oldItem) + mainLoop:setOn() +end) + +onPlayerInventoryChange(function(slot, item, oldItem) + mainLoop:setOn() +end) + +onContainerClose(function(container) + if not container.lootContainer then + mainLoop:setOn() + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/Dropper.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/Dropper.lua new file mode 100644 index 0000000000..96674b9823 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/Dropper.lua @@ -0,0 +1,146 @@ +setDefaultTab("Tools") + +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Dropper') + + Button + id: edit + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Edit +]]) + +local edit = setupUI([[ +Panel + height: 150 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 5 + text-align: center + text: Trash: + + BotContainer + id: TrashItems + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 32 + + Label + anchors.top: prev.bottom + margin-top: 5 + anchors.left: parent.left + anchors.right: parent.right + text-align: center + text: Use: + + BotContainer + id: UseItems + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 32 + + Label + anchors.top: prev.bottom + margin-top: 5 + anchors.left: parent.left + anchors.right: parent.right + text-align: center + text: Drop if below 150 cap: + + BotContainer + id: CapItems + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 32 +]]) +edit:hide() + +if not storage.dropper then + storage.dropper = { + enabled = false, + trashItems = { 283, 284, 285 }, + useItems = { 21203, 14758 }, + capItems = { 21175 } + } +end + +local config = storage.dropper + +local showEdit = false +ui.edit.onClick = function(widget) + showEdit = not showEdit + if showEdit then + edit:show() + else + edit:hide() + end +end + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) + config.enabled = not config.enabled + ui.title:setOn(config.enabled) +end + +UI.Container(function() + config.trashItems = edit.TrashItems:getItems() + end, true, nil, edit.TrashItems) +edit.TrashItems:setItems(config.trashItems) + +UI.Container(function() + config.useItems = edit.UseItems:getItems() + end, true, nil, edit.UseItems) +edit.UseItems:setItems(config.useItems) + +UI.Container(function() + config.capItems = edit.CapItems:getItems() + end, true, nil, edit.CapItems) +edit.CapItems:setItems(config.capItems) + +local function properTable(t) + local r = {} + + for _, entry in pairs(t) do + table.insert(r, entry.id) + end + return r +end + +macro(200, function() + if not config.enabled then return end + local tables = {properTable(config.capItems), properTable(config.useItems), properTable(config.trashItems)} + + local containers = getContainers() + for i=1,3 do + for _, container in pairs(containers) do + for __, item in ipairs(container:getItems()) do + for ___, userItem in ipairs(tables[i]) do + if item:getId() == userItem then + return i == 1 and freecap() < 150 and dropItem(item) or + i == 2 and use(item) or + i == 3 and dropItem(item) + end + end + end + end + end + +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/Equipper.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/Equipper.lua new file mode 100644 index 0000000000..bb0d42a2d8 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/Equipper.lua @@ -0,0 +1,770 @@ +local panelName = "EquipperPanel" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: switch + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('EQ Manager') + + Button + id: setup + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup +]]) +ui:setId(panelName) + +if not storage[panelName] or not storage[panelName].bosses then -- no bosses - old ver + storage[panelName] = { + enabled = false, + rules = {}, + bosses = {} + } +end + +local config = storage[panelName] + +ui.switch:setOn(config.enabled) +ui.switch.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) +end + +local conditions = { -- always add new conditions at the bottom + "Item is available and not worn.", -- nothing 1 + "Monsters around is more than: ", -- spinbox 2 + "Monsters around is less than: ", -- spinbox 3 + "Health precent is below:", -- spinbox 4 + "Health precent is above:", -- spinbox 5 + "Mana precent is below:", -- spinbox 6 + "Mana precent is above:", -- spinbox 7 + "Target name is:", -- BotTextEdit 8 + "Hotkey is being pressed:", -- BotTextEdit 9 + "Player is paralyzed", -- nothing 10 + "Player is in protection zone", -- nothing 11 + "Players around is more than:", -- spinbox 12 + "Players around is less than:", -- spinbox 13 + "TargetBot Danger is Above:", -- spinbox 14 + "Blacklist player in range (sqm)", -- spinbox 15 + "Target is Boss" -- nothing +} + +local conditionNumber = 1 +local optionalConditionNumber = 2 + +local mainWindow = UI.createWindow("EquipWindow") +mainWindow:hide() + +ui.setup.onClick = function() + mainWindow:show() + mainWindow:raise() + mainWindow:focus() +end + +local inputPanel = mainWindow.inputPanel +local listPanel = mainWindow.listPanel +local namePanel = mainWindow.profileName +local eqPanel = mainWindow.setup +local bossPanel = mainWindow.bossPanel + +local slotWidgets = {eqPanel.head, eqPanel.body, eqPanel.legs, eqPanel.feet, eqPanel.neck, eqPanel["left-hand"], eqPanel["right-hand"], eqPanel.finger, eqPanel.ammo} -- back is disabled + +local function setCondition(first, n) + local widget + local spinBox + local textEdit + + if first then + widget = inputPanel.condition.description.text + spinBox = inputPanel.condition.spinbox + textEdit = inputPanel.condition.text + else + widget = inputPanel.optionalCondition.description.text + spinBox = inputPanel.optionalCondition.spinbox + textEdit = inputPanel.optionalCondition.text + end + + -- reset values after change + spinBox:setValue(0) + textEdit:setText('') + + if n == 1 or n == 10 or n == 11 or n == 16 then + spinBox:hide() + textEdit:hide() + elseif n == 9 or n == 8 then + spinBox:hide() + textEdit:show() + if n == 9 then + textEdit:setWidth(75) + else + textEdit:setWidth(200) + end + else + spinBox:show() + textEdit:hide() + end + widget:setText(conditions[n]) +end + +local function resetFields() + conditionNumber = 1 + optionalConditionNumber = 2 + setCondition(false, optionalConditionNumber) + setCondition(true, conditionNumber) + for i, widget in ipairs(slotWidgets) do + widget:setItemId(0) + widget:setChecked(false) + end + for i, child in ipairs(listPanel.list:getChildren()) do + child.display = false + end + namePanel.profileName:setText("") + inputPanel.condition.text:setText('') + inputPanel.condition.spinbox:setValue(0) + inputPanel.useSecondCondition:setText('-') + inputPanel.optionalCondition.text:setText('') + inputPanel.optionalCondition.spinbox:setValue(0) + inputPanel.optionalCondition:hide() + bossPanel:hide() + listPanel:show() + mainWindow.bossList:setText('Boss List') + bossPanel.name:setText('') +end +resetFields() + +mainWindow.closeButton.onClick = function() + resetFields() + mainWindow:hide() +end + +inputPanel.optionalCondition:hide() +inputPanel.useSecondCondition.onOptionChange = function(widget, option, data) + if option ~= "-" then + inputPanel.optionalCondition:show() + else + inputPanel.optionalCondition:hide() + end +end + +-- add default text & windows +setCondition(true, 1) +setCondition(false, 2) + +-- in/de/crementation buttons +inputPanel.condition.nex.onClick = function() + local max = #conditions + + if inputPanel.optionalCondition:isVisible() then + if conditionNumber == max then + if optionalConditionNumber == 1 then + conditionNumber = 2 + else + conditionNumber = 1 + end + else + local futureNumber = conditionNumber + 1 + local safeFutureNumber = conditionNumber + 2 > max and 1 or conditionNumber + 2 + conditionNumber = futureNumber ~= optionalConditionNumber and futureNumber or safeFutureNumber + end + else + conditionNumber = conditionNumber == max and 1 or conditionNumber + 1 + if optionalConditionNumber == conditionNumber then + optionalConditionNumber = optionalConditionNumber == max and 1 or optionalConditionNumber + 1 + setCondition(false, optionalConditionNumber) + end + end + setCondition(true, conditionNumber) +end + +inputPanel.condition.pre.onClick = function() + local max = #conditions + + if inputPanel.optionalCondition:isVisible() then + if conditionNumber == 1 then + if optionalConditionNumber == max then + conditionNumber = max-1 + else + conditionNumber = max + end + else + local futureNumber = conditionNumber - 1 + local safeFutureNumber = conditionNumber - 2 < 1 and max or conditionNumber - 2 + conditionNumber = futureNumber ~= optionalConditionNumber and futureNumber or safeFutureNumber + end + else + conditionNumber = conditionNumber == 1 and max or conditionNumber - 1 + if optionalConditionNumber == conditionNumber then + optionalConditionNumber = optionalConditionNumber == 1 and max or optionalConditionNumber - 1 + setCondition(false, optionalConditionNumber) + end + end + setCondition(true, conditionNumber) +end + +inputPanel.optionalCondition.nex.onClick = function() + local max = #conditions + + if optionalConditionNumber == max then + if conditionNumber == 1 then + optionalConditionNumber = 2 + else + optionalConditionNumber = 1 + end + else + local futureNumber = optionalConditionNumber + 1 + local safeFutureNumber = optionalConditionNumber + 2 > max and 1 or optionalConditionNumber + 2 + optionalConditionNumber = futureNumber ~= conditionNumber and futureNumber or safeFutureNumber + end + setCondition(false, optionalConditionNumber) +end + +inputPanel.optionalCondition.pre.onClick = function() + local max = #conditions + + if optionalConditionNumber == 1 then + if conditionNumber == max then + optionalConditionNumber = max-1 + else + optionalConditionNumber = max + end + else + local futureNumber = optionalConditionNumber - 1 + local safeFutureNumber = optionalConditionNumber - 2 < 1 and max or optionalConditionNumber - 2 + optionalConditionNumber = futureNumber ~= conditionNumber and futureNumber or safeFutureNumber + end + setCondition(false, optionalConditionNumber) +end + +listPanel.up.onClick = function(widget) + local focused = listPanel.list:getFocusedChild() + local n = listPanel.list:getChildIndex(focused) + local t = config.rules + + t[n], t[n-1] = t[n-1], t[n] + if n-1 == 1 then + widget:setEnabled(false) + end + listPanel.down:setEnabled(true) + listPanel.list:moveChildToIndex(focused, n-1) + listPanel.list:ensureChildVisible(focused) +end + +listPanel.down.onClick = function(widget) + local focused = listPanel.list:getFocusedChild() + local n = listPanel.list:getChildIndex(focused) + local t = config.rules + + t[n], t[n+1] = t[n+1], t[n] + if n + 1 == listPanel.list:getChildCount() then + widget:setEnabled(false) + end + listPanel.up:setEnabled(true) + listPanel.list:moveChildToIndex(focused, n+1) + listPanel.list:ensureChildVisible(focused) +end + +eqPanel.cloneEq.onClick = function(widget) + eqPanel.head:setItemId(getHead() and getHead():getId() or 0) + eqPanel.body:setItemId(getBody() and getBody():getId() or 0) + eqPanel.legs:setItemId(getLeg() and getLeg():getId() or 0) + eqPanel.feet:setItemId(getFeet() and getFeet():getId() or 0) + eqPanel.neck:setItemId(getNeck() and getNeck():getId() or 0) + eqPanel["left-hand"]:setItemId(getLeft() and getLeft():getId() or 0) + eqPanel["right-hand"]:setItemId(getRight() and getRight():getId() or 0) + eqPanel.finger:setItemId(getFinger() and getFinger():getId() or 0) + eqPanel.ammo:setItemId(getAmmo() and getAmmo():getId() or 0) +end + +eqPanel.default.onClick = resetFields + +-- buttons disabled by default +listPanel.up:setEnabled(false) +listPanel.down:setEnabled(false) + +-- correct background image +for i, widget in ipairs(slotWidgets) do + widget:setTooltip("Right click to set as slot to unequip") + widget.onItemChange = function(widget) + local selfId = widget:getItemId() + widget:setOn(selfId > 100) + if widget:isChecked() then + widget:setChecked(selfId < 100) + end + end + widget.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local clearItem = widget:isChecked() == false + widget:setChecked(not widget:isChecked()) + if clearItem then + widget:setItemId(0) + end + end + end +end + +inputPanel.condition.description.onMouseWheel = function(widget, mousePos, scroll) + if scroll == 1 then + inputPanel.condition.nex.onClick() + else + inputPanel.condition.pre.onClick() + end +end + +inputPanel.optionalCondition.description.onMouseWheel = function(widget, mousePos, scroll) + if scroll == 1 then + inputPanel.optionalCondition.nex.onClick() + else + inputPanel.optionalCondition.pre.onClick() + end +end + +namePanel.profileName.onTextChange = function(widget, text) + local button = inputPanel.add + text = text:lower() + + for i, child in ipairs(listPanel.list:getChildren()) do + local name = child:getText():lower() + + button:setText(name == text and "Overwrite" or "Add Rule") + button:setTooltip(name == text and "Overwrite existing rule named: "..name, "Add new rule to the list: "..name) + end +end + +local function setupPreview(display, data) + namePanel.profileName:setText('') + if not display then + resetFields() + else + for i, value in ipairs(data) do + local widget = slotWidgets[i] + if value == false then + widget:setChecked(false) + widget:setItemId(0) + elseif value == true then + widget:setChecked(true) + widget:setItemId(0) + else + widget:setChecked(false) + widget:setItemId(value) + end + end + end +end + +local function refreshRules() + local list = listPanel.list + + list:destroyChildren() + for i,v in ipairs(config.rules) do + local widget = UI.createWidget('Rule', list) + widget:setId(v.name) + widget:setText(v.name) + widget.ruleData = v + widget.remove.onClick = function() + widget:destroy() + table.remove(config.rules, table.find(config.rules, v)) + listPanel.up:setEnabled(false) + listPanel.down:setEnabled(false) + refreshRules() + end + widget.visible:setColor(v.visible and "green" or "red") + widget.visible.onClick = function() + v.visible = not v.visible + widget.visible:setColor(v.visible and "green" or "red") + end + widget.enabled:setChecked(v.enabled) + widget.enabled.onClick = function() + v.enabled = not v.enabled + widget.enabled:setChecked(v.enabled) + end + widget.onHoverChange = function(widget, hover) + for i, child in ipairs(list:getChildren()) do + if child.display then return end + end + setupPreview(hover, widget.ruleData.data) + end + widget.onDoubleClick = function(widget) + local ruleData = widget.ruleData + widget.display = true + setupPreview(true, ruleData.data) + conditionNumber = ruleData.mainCondition + optionalConditionNumber = ruleData.optionalCondition + setCondition(false, optionalConditionNumber) + setCondition(true, conditionNumber) + inputPanel.useSecondCondition:setOption(ruleData.relation) + namePanel.profileName:setText(v.name) + + if type(ruleData.mainValue) == "string" then + inputPanel.condition.text:setText(ruleData.mainValue) + elseif type(ruleData.mainValue) == "number" then + inputPanel.condition.spinbox:setValue(ruleData.mainValue) + end + + if type(ruleData.optValue) == "string" then + inputPanel.optionalCondition.text:setText(ruleData.optValue) + elseif type(ruleData.optValue) == "number" then + inputPanel.optionalCondition.spinbox:setValue(ruleData.optValue) + end + end + widget.onClick = function() + local panel = listPanel + if #panel.list:getChildren() == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(false) + elseif panel.list:getChildIndex(panel.list:getFocusedChild()) == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(true) + elseif panel.list:getChildIndex(panel.list:getFocusedChild()) == #panel.list:getChildren() then + panel.up:setEnabled(true) + panel.down:setEnabled(false) + else + panel.up:setEnabled(true) + panel.down:setEnabled(true) + end + end + end +end +refreshRules() + +inputPanel.add.onClick = function(widget) + local mainVal + local optVal + local t = {} + local relation = inputPanel.useSecondCondition:getText() + local profileName = namePanel.profileName:getText() + if profileName:len() == 0 then + return warn("Please fill profile name!") + end + + for i, widget in ipairs(slotWidgets) do + local checked = widget:isChecked() + local id = widget:getItemId() + + if checked then + table.insert(t, true) -- unequip selected slot + elseif id then + table.insert(t, id) -- equip selected item + else + table.insert(t, false) -- ignore slot + end + end + + if conditionNumber == 1 then + mainVal = nil + elseif conditionNumber == 8 then + mainVal = inputPanel.condition.text:getText() + if mainVal:len() == 0 then + return warn("[vBot Equipper] Please fill the name of the creature.") + end + elseif conditionNumber == 9 then + mainVal = inputPanel.condition.text:getText() + if mainVal:len() == 0 then + return warn("[vBot Equipper] Please set correct hotkey.") + end + else + mainVal = inputPanel.condition.spinbox:getValue() + end + + if relation ~= "-" then + if optionalConditionNumber == 1 then + optVal = nil + elseif optionalConditionNumber == 8 then + optVal = inputPanel.optionalCondition.text:getText() + if optVal:len() == 0 then + return warn("[vBot Equipper] Please fill the name of the creature.") + end + elseif optionalConditionNumber == 9 then + optVal = inputPanel.optionalCondition.text:getText() + if optVal:len() == 0 then + return warn("[vBot Equipper] Please set correct hotkey.") + end + else + optVal = inputPanel.optionalCondition.spinbox:getValue() + end + end + + local index + for i, v in ipairs(config.rules) do + if v.name == profileName then + index = i -- search if there's already rule with this name + end + end + + local ruleData = { + name = profileName, + data = t, + enabled = true, + visible = true, + mainCondition = conditionNumber, + optionalCondition = optionalConditionNumber, + mainValue = mainVal, + optValue = optVal, + relation = relation, + } + + if index then + config.rules[index] = ruleData -- overwrite + else + table.insert(config.rules, ruleData) -- create new one + end + + for i, child in ipairs(listPanel.list:getChildren()) do + child.display = false + end + resetFields() + refreshRules() +end + +mainWindow.bossList.onClick = function(widget) + if bossPanel:isVisible() then + bossPanel:hide() + listPanel:show() + widget:setText('Boss List') + else + bossPanel:show() + listPanel:hide() + widget:setText('Rule List') + + end +end + +-- create boss labels +for i, v in ipairs(config.bosses) do + local widget = UI.createWidget("BossLabel", bossPanel.list) + widget:setText(v) + widget.remove.onClick = function() + table.remove(config.bosses, table.find(config.bosses, v)) + widget:destroy() + end +end + +bossPanel.add.onClick = function() + local name = bossPanel.name:getText() + + if name:len() == 0 then + return warn("[Equipped] Please enter boss name!") + elseif table.find(config.bosses, name:lower(), true) then + return warn("[Equipper] Boss already added!") + end + + local widget = UI.createWidget("BossLabel", bossPanel.list) + widget:setText(name) + widget.remove.onClick = function() + table.remove(config.bosses, table.find(config.bosses, name)) + widget:destroy() + end + + table.insert(config.bosses, name) + bossPanel.name:setText('') +end + +local function interpreteCondition(n, v) + + if n == 1 then + return true + elseif n == 2 then + return getMonsters() > v + elseif n == 3 then + return getMonsters() < v + elseif n == 4 then + return hppercent() < v + elseif n == 5 then + return hppercent() > v + elseif n == 6 then + return manapercent() < v + elseif n == 7 then + return manapercent() > v + elseif n == 8 then + return target() and target():getName():lower() == v:lower() or false + elseif n == 9 then + return g_keyboard.isKeyPressed(v) + elseif n == 10 then + return isParalyzed() + elseif n == 11 then + return isInPz() + elseif n == 12 then + return getPlayers() > v + elseif n == 13 then + return getPlayers() < v + elseif n == 14 then + return TargetBot.Danger() > v and TargetBot.isOn() + elseif n == 15 then + return isBlackListedPlayerInRange(v) + elseif n == 16 then + return target() and table.find(config.bosses, target():getName():lower(), true) and true or false + end + +end + +local function finalCheck(first,relation,second) + if relation == "-" then + return first + elseif relation == "and" then + return first and second + elseif relation == "or" then + return first or second + end +end + +local function isEquipped(id) + local t = {getNeck(), getHead(), getBody(), getRight(), getLeft(), getLeg(), getFeet(), getFinger(), getAmmo()} + local ids = {id, getInactiveItemId(id), getActiveItemId(id)} + + for i, slot in pairs(t) do + if slot and table.find(ids, slot:getId()) then + return true + end + end + return false +end + +local function unequipItem(table) + local slots = {getHead(), getBody(), getLeg(), getFeet(), getNeck(), getLeft(), getRight(), getFinger(), getAmmo()} + + if type(table) ~= "table" then return end + for i, slot in ipairs(table) do + local physicalSlot = slots[i] + + if slot == true and physicalSlot then + local id = physicalSlot:getId() + + if g_game.getClientVersion() >= 910 then + -- new tibia + g_game.equipItemId(id) + else + -- old tibia + local dest + for i, container in ipairs(getContainers()) do + local cname = container:getName() + if not containerIsFull(container) then + if not cname:find("loot") and (cname:find("backpack") or cname:find("bag") or cname:find("chess")) then + dest = container + end + break + end + end + + if not dest then return true end + local pos = dest:getSlotPosition(dest:getItemsCount()) + g_game.move(physicalSlot, pos, physicalSlot:getCount()) + end + return true + end + end + return false +end + +local function equipItem(id, slot) + -- need to correct slots... + if slot == 2 then + slot = 4 + elseif slot == 3 then + slot = 7 + elseif slot == 8 then + slot = 9 + elseif slot == 5 then + slot = 2 + elseif slot == 4 then + slot = 8 + elseif slot == 9 then + slot = 10 + elseif slot == 7 then + slot = 5 + end + + + if g_game.getClientVersion() >= 910 then + -- new tibia + return g_game.equipItemId(id) + else + -- old tibia + local item = findItem(id) + return moveToSlot(item, slot) + end +end + + +local function markChild(child) + if mainWindow:isVisible() then + for i, child in ipairs(listPanel.list:getChildren()) do + if child ~= widget then + child:setColor('white') + end + end + widget:setColor('green') + end +end + + +local missingItem = false +local lastRule = false +local correctEq = false +EquipManager = macro(50, function() + if not config.enabled then return end + if #config.rules == 0 then return end + + for i, widget in ipairs(listPanel.list:getChildren()) do + local rule = widget.ruleData + if rule.enabled then + + -- conditions + local firstCondition = interpreteCondition(rule.mainCondition, rule.mainValue) + local optionalCondition = nil + if rule.relation ~= "-" then + optionalCondition = interpreteCondition(rule.optionalCondition, rule.optValue) + end + + -- checks + if finalCheck(firstCondition, rule.relation, optionalCondition) then + + -- performance edits, loop reset + local resetLoop = not missingItem and correctEq and lastRule == rule + if resetLoop then return end + + -- reset executed rule + + + -- first check unequip + if unequipItem(rule.data) == true then + delay(200) + return + end + + -- equiploop + for slot, item in ipairs(rule.data) do + if type(item) == "number" and item > 100 then + if not isEquipped(item) then + if rule.visible then + if findItem(item) then + missingItem = false + delay(200) + return equipItem(item, slot) + else + missingItem = true + end + else + missingItem = false + delay(200) + return equipItem(item, slot) + end + end + end + end + + correctEq = not missingItem and true or false + -- even if nothing was done, exit function to hold rule + return + end + + + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.lua new file mode 100644 index 0000000000..530f429e33 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.lua @@ -0,0 +1,712 @@ +local standBySpells = false +local standByItems = false + +local red = "#ff0800" -- "#ff0800" / #ea3c53 best +local blue = "#7ef9ff" + +setDefaultTab("HP") +local healPanelName = "healbot" +local ui = setupUI([[ +Panel + height: 38 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('HealBot') + + Button + id: settings + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + Button + id: 1 + anchors.top: prev.bottom + anchors.left: parent.left + text: 1 + margin-right: 2 + margin-top: 4 + size: 17 17 + + Button + id: 2 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 2 + margin-left: 4 + size: 17 17 + + Button + id: 3 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 3 + margin-left: 4 + size: 17 17 + + Button + id: 4 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 4 + margin-left: 4 + size: 17 17 + + Button + id: 5 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 5 + margin-left: 4 + size: 17 17 + + Label + id: name + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + text-align: center + margin-left: 4 + height: 17 + text: Profile #1 + background: #292A2A +]]) +ui:setId(healPanelName) + +if not HealBotConfig[healPanelName] or not HealBotConfig[healPanelName][1] or #HealBotConfig[healPanelName] ~= 5 then + HealBotConfig[healPanelName] = { + [1] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #1", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [2] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #2", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [3] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #3", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [4] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #4", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [5] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #5", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + } +end + +if not HealBotConfig.currentHealBotProfile or HealBotConfig.currentHealBotProfile == 0 or HealBotConfig.currentHealBotProfile > 5 then + HealBotConfig.currentHealBotProfile = 1 +end + +-- finding correct table, manual unfortunately +local currentSettings +local setActiveProfile = function() + local n = HealBotConfig.currentHealBotProfile + currentSettings = HealBotConfig[healPanelName][n] +end +setActiveProfile() + +local activeProfileColor = function() + for i=1,5 do + if i == HealBotConfig.currentHealBotProfile then + ui[i]:setColor("green") + else + ui[i]:setColor("white") + end + end +end +activeProfileColor() + +ui.title:setOn(currentSettings.enabled) +ui.title.onClick = function(widget) + currentSettings.enabled = not currentSettings.enabled + widget:setOn(currentSettings.enabled) + vBotConfigSave("heal") +end + +ui.settings.onClick = function(widget) + healWindow:show() + healWindow:raise() + healWindow:focus() +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + healWindow = UI.createWindow('HealWindow', rootWidget) + healWindow:hide() + + healWindow.onVisibilityChange = function(widget, visible) + if not visible then + vBotConfigSave("heal") + healWindow.healer:show() + healWindow.settings:hide() + healWindow.settingsButton:setText("Settings") + end + end + + healWindow.settingsButton.onClick = function(widget) + if healWindow.healer:isVisible() then + healWindow.healer:hide() + healWindow.settings:show() + widget:setText("Back") + else + healWindow.healer:show() + healWindow.settings:hide() + widget:setText("Settings") + end + end + + local setProfileName = function() + ui.name:setText(currentSettings.name) + end + healWindow.settings.profiles.Name.onTextChange = function(widget, text) + currentSettings.name = text + setProfileName() + end + healWindow.settings.list.Visible.onClick = function(widget) + currentSettings.Visible = not currentSettings.Visible + healWindow.settings.list.Visible:setChecked(currentSettings.Visible) + end + healWindow.settings.list.Cooldown.onClick = function(widget) + currentSettings.Cooldown = not currentSettings.Cooldown + healWindow.settings.list.Cooldown:setChecked(currentSettings.Cooldown) + end + healWindow.settings.list.Interval.onClick = function(widget) + currentSettings.Interval = not currentSettings.Interval + healWindow.settings.list.Interval:setChecked(currentSettings.Interval) + end + healWindow.settings.list.Conditions.onClick = function(widget) + currentSettings.Conditions = not currentSettings.Conditions + healWindow.settings.list.Conditions:setChecked(currentSettings.Conditions) + end + healWindow.settings.list.Delay.onClick = function(widget) + currentSettings.Delay = not currentSettings.Delay + healWindow.settings.list.Delay:setChecked(currentSettings.Delay) + end + healWindow.settings.list.MessageDelay.onClick = function(widget) + currentSettings.MessageDelay = not currentSettings.MessageDelay + healWindow.settings.list.MessageDelay:setChecked(currentSettings.MessageDelay) + end + + local refreshSpells = function() + if currentSettings.spellTable then + healWindow.healer.spells.spellList:destroyChildren() + for _, entry in pairs(currentSettings.spellTable) do + local label = UI.createWidget("SpellEntry", healWindow.healer.spells.spellList) + label.enabled:setChecked(entry.enabled) + label.enabled.onClick = function(widget) + standBySpells = false + standByItems = false + entry.enabled = not entry.enabled + label.enabled:setChecked(entry.enabled) + end + label.remove.onClick = function(widget) + standBySpells = false + standByItems = false + table.removevalue(currentSettings.spellTable, entry) + reindexTable(currentSettings.spellTable) + label:destroy() + end + label:setText("(MP>" .. entry.cost .. ") " .. entry.origin .. entry.sign .. entry.value .. ": " .. entry.spell) + end + end + end + refreshSpells() + + local refreshItems = function() + if currentSettings.itemTable then + healWindow.healer.items.itemList:destroyChildren() + for _, entry in pairs(currentSettings.itemTable) do + local label = UI.createWidget("ItemEntry", healWindow.healer.items.itemList) + label.enabled:setChecked(entry.enabled) + label.enabled.onClick = function(widget) + standBySpells = false + standByItems = false + entry.enabled = not entry.enabled + label.enabled:setChecked(entry.enabled) + end + label.remove.onClick = function(widget) + standBySpells = false + standByItems = false + table.removevalue(currentSettings.itemTable, entry) + reindexTable(currentSettings.itemTable) + label:destroy() + end + label.id:setItemId(entry.item) + label:setText(entry.origin .. entry.sign .. entry.value .. ": " .. entry.item) + end + end + end + refreshItems() + + healWindow.healer.spells.MoveUp.onClick = function(widget) + local input = healWindow.healer.spells.spellList:getFocusedChild() + if not input then return end + local index = healWindow.healer.spells.spellList:getChildIndex(input) + if index < 2 then return end + + local t = currentSettings.spellTable + + t[index],t[index-1] = t[index-1], t[index] + healWindow.healer.spells.spellList:moveChildToIndex(input, index - 1) + healWindow.healer.spells.spellList:ensureChildVisible(input) + end + + healWindow.healer.spells.MoveDown.onClick = function(widget) + local input = healWindow.healer.spells.spellList:getFocusedChild() + if not input then return end + local index = healWindow.healer.spells.spellList:getChildIndex(input) + if index >= healWindow.healer.spells.spellList:getChildCount() then return end + + local t = currentSettings.spellTable + + t[index],t[index+1] = t[index+1],t[index] + healWindow.healer.spells.spellList:moveChildToIndex(input, index + 1) + healWindow.healer.spells.spellList:ensureChildVisible(input) + end + + healWindow.healer.items.MoveUp.onClick = function(widget) + local input = healWindow.healer.items.itemList:getFocusedChild() + if not input then return end + local index = healWindow.healer.items.itemList:getChildIndex(input) + if index < 2 then return end + + local t = currentSettings.itemTable + + t[index],t[index-1] = t[index-1], t[index] + healWindow.healer.items.itemList:moveChildToIndex(input, index - 1) + healWindow.healer.items.itemList:ensureChildVisible(input) + end + + healWindow.healer.items.MoveDown.onClick = function(widget) + local input = healWindow.healer.items.itemList:getFocusedChild() + if not input then return end + local index = healWindow.healer.items.itemList:getChildIndex(input) + if index >= healWindow.healer.items.itemList:getChildCount() then return end + + local t = currentSettings.itemTable + + t[index],t[index+1] = t[index+1],t[index] + healWindow.healer.items.itemList:moveChildToIndex(input, index + 1) + healWindow.healer.items.itemList:ensureChildVisible(input) + end + + healWindow.healer.spells.addSpell.onClick = function(widget) + + local spellFormula = healWindow.healer.spells.spellFormula:getText():trim() + local manaCost = tonumber(healWindow.healer.spells.manaCost:getText()) + local spellTrigger = tonumber(healWindow.healer.spells.spellValue:getText()) + local spellSource = healWindow.healer.spells.spellSource:getCurrentOption().text + local spellEquasion = healWindow.healer.spells.spellCondition:getCurrentOption().text + local source + local equasion + + if not manaCost then + warn("HealBot: incorrect mana cost value!") + healWindow.healer.spells.spellFormula:setText('') + healWindow.healer.spells.spellValue:setText('') + healWindow.healer.spells.manaCost:setText('') + return + end + if not spellTrigger then + warn("HealBot: incorrect condition value!") + healWindow.healer.spells.spellFormula:setText('') + healWindow.healer.spells.spellValue:setText('') + healWindow.healer.spells.manaCost:setText('') + return + end + + if spellSource == "Current Mana" then + source = "MP" + elseif spellSource == "Current Health" then + source = "HP" + elseif spellSource == "Mana Percent" then + source = "MP%" + elseif spellSource == "Health Percent" then + source = "HP%" + else + source = "burst" + end + + if spellEquasion == "Above" then + equasion = ">" + elseif spellEquasion == "Below" then + equasion = "<" + else + equasion = "=" + end + + if spellFormula:len() > 0 then + table.insert(currentSettings.spellTable, {index = #currentSettings.spellTable+1, spell = spellFormula, sign = equasion, origin = source, cost = manaCost, value = spellTrigger, enabled = true}) + healWindow.healer.spells.spellFormula:setText('') + healWindow.healer.spells.spellValue:setText('') + healWindow.healer.spells.manaCost:setText('') + end + standBySpells = false + standByItems = false + refreshSpells() + end + + healWindow.healer.items.addItem.onClick = function(widget) + + local id = healWindow.healer.items.itemId:getItemId() + local trigger = tonumber(healWindow.healer.items.itemValue:getText()) + local src = healWindow.healer.items.itemSource:getCurrentOption().text + local eq = healWindow.healer.items.itemCondition:getCurrentOption().text + local source + local equasion + + if not trigger then + warn("HealBot: incorrect trigger value!") + healWindow.healer.items.itemId:setItemId(0) + healWindow.healer.items.itemValue:setText('') + return + end + + if src == "Current Mana" then + source = "MP" + elseif src == "Current Health" then + source = "HP" + elseif src == "Mana Percent" then + source = "MP%" + elseif src == "Health Percent" then + source = "HP%" + else + source = "burst" + end + + if eq == "Above" then + equasion = ">" + elseif eq == "Below" then + equasion = "<" + else + equasion = "=" + end + + if id > 100 then + table.insert(currentSettings.itemTable, {index = #currentSettings.itemTable+1,item = id, sign = equasion, origin = source, value = trigger, enabled = true}) + standBySpells = false + standByItems = false + refreshItems() + healWindow.healer.items.itemId:setItemId(0) + healWindow.healer.items.itemValue:setText('') + end + end + + healWindow.closeButton.onClick = function(widget) + healWindow:hide() + end + + local loadSettings = function() + ui.title:setOn(currentSettings.enabled) + setProfileName() + healWindow.settings.profiles.Name:setText(currentSettings.name) + refreshSpells() + refreshItems() + healWindow.settings.list.Visible:setChecked(currentSettings.Visible) + healWindow.settings.list.Cooldown:setChecked(currentSettings.Cooldown) + healWindow.settings.list.Delay:setChecked(currentSettings.Delay) + healWindow.settings.list.MessageDelay:setChecked(currentSettings.MessageDelay) + healWindow.settings.list.Interval:setChecked(currentSettings.Interval) + healWindow.settings.list.Conditions:setChecked(currentSettings.Conditions) + end + loadSettings() + + local profileChange = function() + setActiveProfile() + activeProfileColor() + loadSettings() + vBotConfigSave("heal") + end + + local resetSettings = function() + currentSettings.enabled = false + currentSettings.spellTable = {} + currentSettings.itemTable = {} + currentSettings.Visible = true + currentSettings.Cooldown = true + currentSettings.Delay = true + currentSettings.MessageDelay = false + currentSettings.Interval = true + currentSettings.Conditions = true + currentSettings.name = "Profile #" .. HealBotConfig.currentBotProfile + end + + -- profile buttons + for i=1,5 do + local button = ui[i] + button.onClick = function() + HealBotConfig.currentHealBotProfile = i + profileChange() + end + end + + healWindow.settings.profiles.ResetSettings.onClick = function() + resetSettings() + loadSettings() + end + + + -- public functions + HealBot = {} -- global table + + HealBot.isOn = function() + return currentSettings.enabled + end + + HealBot.isOff = function() + return not currentSettings.enabled + end + + HealBot.setOff = function() + currentSettings.enabled = false + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + HealBot.setOn = function() + currentSettings.enabled = true + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + HealBot.getActiveProfile = function() + return HealBotConfig.currentHealBotProfile -- returns number 1-5 + end + + HealBot.setActiveProfile = function(n) + if not n or not tonumber(n) or n < 1 or n > 5 then + return error("[HealBot] wrong profile parameter! should be 1 to 5 is " .. n) + else + HealBotConfig.currentHealBotProfile = n + profileChange() + end + end + + HealBot.show = function() + healWindow:show() + healWindow:raise() + healWindow:focus() + end +end + +-- spells +macro(100, function() + if standBySpells then return end + if not currentSettings.enabled then return end + local somethingIsOnCooldown = false + + for _, entry in pairs(currentSettings.spellTable) do + if entry.enabled and entry.cost < mana() then + if canCast(entry.spell, not currentSettings.Conditions, not currentSettings.Cooldown) then + if entry.origin == "HP%" then + if entry.sign == "=" and hppercent() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and hppercent() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and hppercent() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "HP" then + if entry.sign == "=" and hp() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and hp() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and hp() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "MP%" then + if entry.sign == "=" and manapercent() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and manapercent() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and manapercent() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "MP" then + if entry.sign == "=" and mana() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and mana() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and mana() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "burst" then + if entry.sign == "=" and burstDamageValue() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and burstDamageValue() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and burstDamageValue() <= entry.value then + say(entry.spell) + return + end + end + else + somethingIsOnCooldown = true + end + end + end + if not somethingIsOnCooldown then + standBySpells = true + end +end) + +-- items +macro(100, function() + if standByItems then return end + if not currentSettings.enabled or #currentSettings.itemTable == 0 then return end + if currentSettings.Delay and vBot.isUsing then return end + if currentSettings.MessageDelay and vBot.isUsingPotion then return end + + if not currentSettings.MessageDelay then + delay(400) + end + + if TargetBot.isOn() and TargetBot.Looting.getStatus():len() > 0 and currentSettings.Interval then + if not currentSettings.MessageDelay then + delay(700) + else + delay(200) + end + end + + for _, entry in pairs(currentSettings.itemTable) do + local item = findItem(entry.item) + if (not currentSettings.Visible or item) and entry.enabled then + if entry.origin == "HP%" then + if entry.sign == "=" and hppercent() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and hppercent() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and hppercent() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "HP" then + if entry.sign == "=" and hp() == tonumberentry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and hp() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and hp() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "MP%" then + if entry.sign == "=" and manapercent() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and manapercent() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and manapercent() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "MP" then + if entry.sign == "=" and mana() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and mana() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and mana() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "burst" then + if entry.sign == "=" and burstDamageValue() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and burstDamageValue() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and burstDamageValue() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + end + end + end + standByItems = true +end) +UI.Separator() + +onPlayerHealthChange(function(healthPercent) + standByItems = false + standBySpells = false +end) + +onManaChange(function(player, mana, maxMana, oldMana, oldMaxMana) + standByItems = false + standBySpells = false +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.otui new file mode 100644 index 0000000000..fb8cb03d7e --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/HealBot.otui @@ -0,0 +1,492 @@ +SettingCheckBox < CheckBox + text-wrap: true + text-auto-resize: true + margin-top: 3 + font: verdana-11px-rounded + +SpellSourceBoxPopupMenu < ComboBoxPopupMenu +SpellSourceBoxPopupMenuButton < ComboBoxPopupMenuButton +SpellSourceBox < ComboBox + @onSetup: | + self:addOption("Current Mana") + self:addOption("Current Health") + self:addOption("Mana Percent") + self:addOption("Health Percent") + self:addOption("Burst Damage") + +SpellConditionBoxPopupMenu < ComboBoxPopupMenu +SpellConditionBoxPopupMenuButton < ComboBoxPopupMenuButton +SpellConditionBox < ComboBox + @onSetup: | + self:addOption("Below") + self:addOption("Above") + self:addOption("Equal To") + +SpellEntry < Label + background-color: alpha + text-offset: 18 1 + focusable: true + height: 16 + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + text-offset: 1 0 + width: 15 + height: 15 + +ItemEntry < Label + background-color: alpha + text-offset: 40 1 + focusable: true + height: 16 + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + + UIItem + id: id + anchors.left: prev.right + margin-left: 3 + anchors.verticalCenter: parent.verticalCenter + size: 15 15 + focusable: false + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + text-offset: 1 0 + width: 15 + height: 15 + +SpellHealing < FlatPanel + size: 490 130 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + margin-left: 5 + text: Spell Healing + color: #269e26 + font: verdana-11px-rounded + + SpellSourceBox + id: spellSource + anchors.top: spellList.top + anchors.left: spellList.right + margin-left: 80 + width: 125 + font: verdana-11px-rounded + + Label + id: whenSpell + anchors.left: spellList.right + anchors.verticalCenter: prev.verticalCenter + text: When + margin-left: 7 + font: verdana-11px-rounded + + Label + id: isSpell + anchors.left: spellList.right + anchors.top: whenSpell.bottom + text: Is + margin-top: 9 + margin-left: 7 + font: verdana-11px-rounded + + SpellConditionBox + id: spellCondition + anchors.left: spellSource.left + anchors.top: spellSource.bottom + marin-top: 15 + width: 80 + font: verdana-11px-rounded + + TextEdit + id: spellValue + anchors.left: spellCondition.right + anchors.top: spellCondition.top + anchors.bottom: spellCondition.bottom + anchors.right: spellSource.right + font: verdana-11px-rounded + + Label + id: castSpell + anchors.left: isSpell.left + anchors.top: isSpell.bottom + text: Cast + margin-top: 9 + font: verdana-11px-rounded + + TextEdit + id: spellFormula + anchors.left: spellCondition.left + anchors.top: spellCondition.bottom + anchors.right: spellValue.right + font: verdana-11px-rounded + + Label + id: manaSpell + anchors.left: castSpell.left + anchors.top: castSpell.bottom + text: Mana Cost: + margin-top: 8 + font: verdana-11px-rounded + + TextEdit + id: manaCost + anchors.left: spellFormula.left + anchors.top: spellFormula.bottom + width: 40 + font: verdana-11px-rounded + + TextList + id: spellList + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.top: parent.top + padding: 1 + padding-top: 2 + width: 270 + margin-bottom: 7 + margin-left: 7 + margin-top: 10 + vertical-scrollbar: spellListScrollBar + + VerticalScrollBar + id: spellListScrollBar + anchors.top: spellList.top + anchors.bottom: spellList.bottom + anchors.right: spellList.right + step: 14 + pixels-scroll: true + + Button + id: addSpell + anchors.right: spellFormula.right + anchors.bottom: spellList.bottom + text: Add + size: 40 17 + font: cipsoftFont + + Button + id: MoveUp + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Up + size: 55 17 + font: cipsoftFont + + Button + id: MoveDown + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Down + size: 55 17 + font: cipsoftFont + +ItemHealing < FlatPanel + size: 490 120 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + margin-left: 5 + text: Item Healing + color: #ff4513 + font: verdana-11px-rounded + + SpellSourceBox + id: itemSource + anchors.top: itemList.top + anchors.right: parent.right + margin-right: 10 + width: 128 + font: verdana-11px-rounded + + Label + id: whenItem + anchors.left: itemList.right + anchors.verticalCenter: prev.verticalCenter + text: When + margin-left: 7 + font: verdana-11px-rounded + + Label + id: isItem + anchors.left: itemList.right + anchors.top: whenItem.bottom + text: Is + margin-top: 9 + margin-left: 7 + font: verdana-11px-rounded + + SpellConditionBox + id: itemCondition + anchors.left: itemSource.left + anchors.top: itemSource.bottom + marin-top: 15 + width: 80 + font: verdana-11px-rounded + + TextEdit + id: itemValue + anchors.left: itemCondition.right + anchors.top: itemCondition.top + anchors.bottom: itemCondition.bottom + width: 49 + font: verdana-11px-rounded + + Label + id: useItem + anchors.left: isItem.left + anchors.top: isItem.bottom + text: Use + margin-top: 15 + font: verdana-11px-rounded + + BotItem + id: itemId + anchors.left: itemCondition.left + anchors.top: itemCondition.bottom + + TextList + id: itemList + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.top: parent.top + padding: 1 + padding-top: 2 + width: 270 + margin-top: 10 + margin-bottom: 7 + margin-left: 8 + vertical-scrollbar: itemListScrollBar + + VerticalScrollBar + id: itemListScrollBar + anchors.top: itemList.top + anchors.bottom: itemList.bottom + anchors.right: itemList.right + step: 14 + pixels-scroll: true + + Button + id: addItem + anchors.right: itemValue.right + anchors.bottom: itemList.bottom + text: Add + size: 40 17 + font: cipsoftFont + + Button + id: MoveUp + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Up + size: 55 17 + font: cipsoftFont + + Button + id: MoveDown + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Down + size: 55 17 + font: cipsoftFont + +HealerPanel < Panel + size: 510 275 + + SpellHealing + id: spells + anchors.top: parent.top + margin-top: 8 + anchors.left: parent.left + + ItemHealing + id: items + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 10 + +HealBotSettingsPanel < Panel + size: 500 267 + padding-top: 8 + + FlatPanel + id: list + anchors.fill: parent + margin-right: 240 + padding-left: 6 + padding-right: 6 + padding-top: 6 + layout: + type: verticalBox + + Label + text: Additional Settings + text-align: center + font: verdana-11px-rounded + + HorizontalSeparator + + SettingCheckBox + id: Cooldown + text: Check spell cooldowns + margin-top: 10 + + SettingCheckBox + id: Visible + text: Items must be visible (recommended) + + SettingCheckBox + id: Delay + text: Don't use items when interacting + + SettingCheckBox + id: Interval + text: Additional delay when looting corpses + + SettingCheckBox + id: Conditions + text: Also check conditions from RL Tibia + + SettingCheckBox + id: MessageDelay + text: Cooldown based on "Aaaah..." message + + VerticalSeparator + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.left: prev.right + margin-left: 8 + + FlatPanel + id: profiles + anchors.fill: parent + anchors.left: prev.left + margin-left: 8 + margin-right: 8 + padding: 8 + + Label + text: Profile Settings + text-align: center + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + font: verdana-11px-rounded + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + + Label + anchors.top: prev.bottom + margin-top: 30 + anchors.left: parent.left + anchors.right: parent.right + text-align: center + font: verdana-11px-rounded + text: Profile Name: + + TextEdit + id: Name + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + + Button + id: ResetSettings + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + text: Reset Current Profile + text-auto-resize: true + color: #ff4513 + +HealWindow < MainWindow + !text: tr('Self Healer') + size: 520 360 + @onEscape: self:hide() + + Label + id: title + anchors.left: parent.left + anchors.top: parent.top + margin-left: 2 + !text: tr('More important methods come first (Example: Exura gran above Exura)') + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + HealerPanel + id: healer + anchors.top: prev.bottom + anchors.left: parent.left + + HealBotSettingsPanel + id: settings + anchors.top: title.bottom + anchors.left: parent.left + visible: false + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-right: 5 + + Button + id: settingsButton + !text: tr('Settings') + font: cipsoftFont + anchors.left: parent.left + anchors.bottom: parent.bottom + size: 45 21 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/Sio.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/Sio.lua new file mode 100644 index 0000000000..ade2c0dfdc --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/Sio.lua @@ -0,0 +1,252 @@ +setDefaultTab("Main") + local panelName = "advancedFriendHealer" + local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Friend Healer') + + Button + id: editList + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + ]], parent) + ui:setId(panelName) + + if not storage[panelName] then + storage[panelName] = { + minMana = 60, + minFriendHp = 40, + customSpellName = "exura max sio", + customSpell = false, + distance = 8, + itemHeal = false, + id = 3160, + exuraSio = false, + exuraGranSio = false, + exuraMasRes = false, + healEk = false, + healRp = false, + healEd = false, + healMs = false + } + end + + local config = storage[panelName] + + -- basic elements + ui.title:setOn(config.enabled) + ui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) + end + ui.editList.onClick = function(widget) + sioListWindow:show() + sioListWindow:raise() + sioListWindow:focus() + end + + rootWidget = g_ui.getRootWidget() + if rootWidget then + sioListWindow = UI.createWindow('SioListWindow', rootWidget) + sioListWindow:hide() + + -- TextWindow + sioListWindow.spellName:setText(config.customSpellName) + sioListWindow.spellName.onTextChange = function(widget, text) + config.customSpellName = text + end + + -- botswitches + sioListWindow.spell:setOn(config.customSpell) + sioListWindow.spell.onClick = function(widget) + config.customSpell = not config.customSpell + widget:setOn(config.customSpell) + end + sioListWindow.item:setOn(config.itemHeal) + sioListWindow.item.onClick = function(widget) + config.itemHeal = not config.itemHeal + widget:setOn(config.itemHeal) + end + sioListWindow.exuraSio:setOn(config.exuraSio) + sioListWindow.exuraSio.onClick = function(widget) + config.exuraSio = not config.exuraSio + widget:setOn(config.exuraSio) + end + sioListWindow.exuraGranSio:setOn(config.exuraGranSio) + sioListWindow.exuraGranSio.onClick = function(widget) + config.exuraGranSio = not config.exuraGranSio + widget:setOn(config.exuraGranSio) + end + sioListWindow.exuraMasRes:setOn(config.exuraMasRes) + sioListWindow.exuraMasRes.onClick = function(widget) + config.exuraMasRes = not config.exuraMasRes + widget:setOn(config.exuraMasRes) + end + sioListWindow.vocation.ED:setOn(config.healEd) + sioListWindow.vocation.ED.onClick = function(widget) + config.healEd = not config.healEd + widget:setOn(config.healEd) + end + sioListWindow.vocation.MS:setOn(config.healMs) + sioListWindow.vocation.MS.onClick = function(widget) + config.healMs = not config.healMs + widget:setOn(config.healMs) + end + sioListWindow.vocation.EK:setOn(config.healEk) + sioListWindow.vocation.EK.onClick = function(widget) + config.healEk = not config.healEk + widget:setOn(config.healEk) + end + sioListWindow.vocation.RP:setOn(config.healRp) + sioListWindow.vocation.RP.onClick = function(widget) + config.healRp = not config.healRp + widget:setOn(config.healRp) + end + + -- functions + local updateMinManaText = function() + sioListWindow.manaInfo:setText("Minimum Mana >= " .. config.minMana .. "%") + end + local updateFriendHpText = function() + sioListWindow.friendHp:setText("Heal Friend Below " .. config.minFriendHp .. "% hp") + end + local updateDistanceText = function() + sioListWindow.distText:setText("Max Distance: " .. config.distance) + end + + -- scrollbars and text updates + sioListWindow.Distance:setValue(config.distance) + sioListWindow.Distance.onValueChange = function(scroll, value) + config.distance = value + updateDistanceText() + end + updateDistanceText() + + sioListWindow.minMana:setValue(config.minMana) + sioListWindow.minMana.onValueChange = function(scroll, value) + config.minMana = value + updateMinManaText() + end + updateMinManaText() + + sioListWindow.minFriendHp:setValue(config.minFriendHp) + sioListWindow.minFriendHp.onValueChange = function(scroll, value) + config.minFriendHp = value + updateFriendHpText() + end + updateFriendHpText() + + sioListWindow.itemId:setItemId(config.id) + sioListWindow.itemId.onItemChange = function(widget) + config.id = widget:getItemId() + end + + sioListWindow.closeButton.onClick = function(widget) + sioListWindow:hide() + end + + end + + -- local variables + local newTibia = g_game.getClientVersion() >= 960 + + local function isValid(name) + if not newTibia then return true end + + local voc = vBot.BotServerMembers[name] + if not voc then return true end + + if voc == 11 then voc = 1 + elseif voc == 12 then voc = 2 + elseif voc == 13 then voc = 3 + elseif voc == 14 then voc = 4 + end + + local isOk = false + if voc == 1 and config.healEk then + isOk = true + elseif voc == 2 and config.healRp then + isOk = true + elseif voc == 3 and config.healMs then + isOk = true + elseif voc == 4 and config.healEd then + isOk = true + end + + return isOk + end + + macro(200, function() + if not config.enabled then return end + if modules.game_cooldown.isGroupCooldownIconActive(2) then return end + + --[[ + 1. custom spell + 2. exura gran sio - at 50% of minHpValue + 3. exura gran mas res + 4. exura sio + 5. item healing + --]] + + -- exura gran sio & custom spell + if config.customSpell or config.exuraGranSio then + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and spec ~= player and isValid(spec:getName()) and spec:canShoot() then + if isFriend(spec) then + if config.customSpell and spec:getHealthPercent() <= config.minFriendHp then + return cast(config.customSpellName .. ' "' .. spec:getName() .. '"', 1000) + end + if config.exuraGranSio and spec:getHealthPercent() <= config.minFriendHp/3 then + if canCast('exura gran sio "' .. spec:getName() ..'"') then + return cast('exura gran sio "' .. spec:getName() ..'"', 60000) + end + end + end + end + end + end + + -- exura gran mas res and standard sio + local friends = 0 + if config.exuraMasRes then + for i, spec in ipairs(getSpectators(player, largeRuneArea)) do + if spec:isPlayer() and spec ~= player and isValid(spec:getName()) and spec:canShoot() then + if isFriend(spec) and spec:getHealthPercent() <= config.minFriendHp then + friends = friends + 1 + end + end + end + if friends > 1 then + return cast('exura gran mas res', 2000) + end + end + if config.exuraSio or config.itemHeal then + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and spec ~= player and isValid(spec:getName()) and spec:canShoot() then + if isFriend(spec) then + if spec:getHealthPercent() <= config.minFriendHp then + if config.exuraSio then + return cast('exura sio "' .. spec:getName() .. '"', 1000) + elseif findItem(config.id) and distanceFromPlayer(spec:getPosition()) <= config.distance then + return useWith(config.id, spec) + end + end + end + end + end + end + + end) +addSeparator() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/alarms.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/alarms.lua new file mode 100644 index 0000000000..ad58f03337 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/alarms.lua @@ -0,0 +1,272 @@ +local panelName = "alarms" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Alarms') + + Button + id: alerts + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Edit + +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = { + enabled = false, + playerAttack = false, + playerDetected = false, + playerDetectedLogout = false, + creatureDetected = false, + healthBelow = false, + healthValue = 40, + manaBelow = false, + manaValue = 50, + privateMessage = false, + ignoreFriends = true, + warnBoss = false, + bossName = '[B]' +} +end + + + +local config = storage[panelName] + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) +config.enabled = not config.enabled +widget:setOn(config.enabled) +end + +-- new var's validation +config.messageText = config.messageText or "" +config.bossName = config.bossName or "" + +rootWidget = g_ui.getRootWidget() +if rootWidget then + alarmsWindow = UI.createWindow('AlarmsWindow', rootWidget) + alarmsWindow:hide() + + alarmsWindow.closeButton.onClick = function(widget) + alarmsWindow:hide() + end + + alarmsWindow.playerAttack:setOn(config.playerAttack) + alarmsWindow.playerAttack.onClick = function(widget) + config.playerAttack = not config.playerAttack + widget:setOn(config.playerAttack) + end + + alarmsWindow.playerDetected:setOn(config.playerDetected) + alarmsWindow.playerDetected.onClick = function(widget) + config.playerDetected = not config.playerDetected + widget:setOn(config.playerDetected) + end + + alarmsWindow.playerDetectedLogout:setChecked(config.playerDetectedLogout) + alarmsWindow.playerDetectedLogout.onClick = function(widget) + config.playerDetectedLogout = not config.playerDetectedLogout + widget:setChecked(config.playerDetectedLogout) + end + + alarmsWindow.creatureDetected:setOn(config.creatureDetected) + alarmsWindow.creatureDetected.onClick = function(widget) + config.creatureDetected = not config.creatureDetected + widget:setOn(config.creatureDetected) + end + + alarmsWindow.healthBelow:setOn(config.healthBelow) + alarmsWindow.healthBelow.onClick = function(widget) + config.healthBelow = not config.healthBelow + widget:setOn(config.healthBelow) + end + + alarmsWindow.healthValue.onValueChange = function(scroll, value) + config.healthValue = value + alarmsWindow.healthBelow:setText("Health < " .. config.healthValue .. "%") + end + alarmsWindow.healthValue:setValue(config.healthValue) + + alarmsWindow.manaBelow:setOn(config.manaBelow) + alarmsWindow.manaBelow.onClick = function(widget) + config.manaBelow = not config.manaBelow + widget:setOn(config.manaBelow) + end + + alarmsWindow.manaValue.onValueChange = function(scroll, value) + config.manaValue = value + alarmsWindow.manaBelow:setText("Mana < " .. config.manaValue .. "%") + end + alarmsWindow.manaValue:setValue(config.manaValue) + + alarmsWindow.privateMessage:setOn(config.privateMessage) + alarmsWindow.privateMessage.onClick = function(widget) + config.privateMessage = not config.privateMessage + widget:setOn(config.privateMessage) + end + + alarmsWindow.ignoreFriends:setOn(config.ignoreFriends) + alarmsWindow.ignoreFriends.onClick = function(widget) + config.ignoreFriends = not config.ignoreFriends + widget:setOn(config.ignoreFriends) + end + + alarmsWindow.warnBoss:setOn(config.warnBoss) + alarmsWindow.warnBoss.onClick = function(widget) + config.warnBoss = not config.warnBoss + widget:setOn(config.warnBoss) + end + + alarmsWindow.bossName:setText(config.bossName) + alarmsWindow.bossName.onTextChange = function(widget, text) + config.bossName = text + end + + alarmsWindow.warnMessage:setOn(config.warnMessage) + alarmsWindow.warnMessage.onClick = function(widget) + config.warnMessage = not config.warnMessage + widget:setOn(config.warnMessage) + end + + alarmsWindow.messageText:setText(config.messageText) + alarmsWindow.messageText.onTextChange = function(widget, text) + config.messageText = text + end + + local pName = player:getName() + onTextMessage(function(mode, text) + if config.enabled and config.playerAttack and string.match(text, "hitpoints due to an attack") and not string.match(text, "hitpoints due to an attack by a ") then + playSound("/sounds/Player_Attack.ogg") + g_window.setTitle(pName .. " - Player Attacks!") + return + end + + if config.warnMessage and config.messageText:len() > 0 then + text = text:lower() + local parts = string.split(config.messageText, ",") + for i=1,#parts do + local part = parts[i] + part = part:trim() + part = part:lower() + + if text:find(part) then + delay(1500) + playSound(g_resources.fileExists("/sounds/Special_Message.ogg") and "/sounds/Special_Message.ogg" or "/sounds/Private_Message.ogg") + g_window.setTitle(pName .. " - Special Message Detected: "..part) + return + end + end + end + end) + + macro(100, function() + if not config.enabled then + return + end + local specs = getSpectators() + if config.playerDetected then + for _, spec in ipairs(specs) do + if spec:isPlayer() and spec:getName() ~= name() then + local specPos = spec:getPosition() + if (not config.ignoreFriends or not isFriend(spec)) and math.max(math.abs(posx()-specPos.x), math.abs(posy()-specPos.y)) <= 8 then + playSound("/sounds/Player_Detected.ogg") + delay(1500) + g_window.setTitle(pName .. " - Player Detected! "..spec:getName()) + if config.playerDetectedLogout then + modules.game_interface.tryLogout(false) + end + return + end + end + end + end + + if config.creatureDetected then + for _, spec in ipairs(specs) do + if not spec:isPlayer() then + local specPos = spec:getPosition() + if math.max(math.abs(posx()-specPos.x), math.abs(posy()-specPos.y)) <= 8 then + playSound("/sounds/Creature_Detected.ogg") + delay(1500) + g_window.setTitle(pName .. " - Creature Detected! "..spec:getName()) + return + end + end + end + end + + if config.warnBoss then + -- experimental, but since we check only names i think the best way would be to combine all spec's names into one string and then check it to avoid multiple loops + if config.bossName:len() > 0 then + local names = string.split(config.bossName, ",") + local combinedString = "" + for _, spec in ipairs(specs) do + local specPos = spec:getPosition() + if math.max(math.abs(posx() - specPos.x), math.abs(posy() - specPos.y)) <= 8 then + local name = spec:getName():lower() + -- add special sign between names to avoid unwanted combining mistakes + combinedString = combinedString .."&"..name + end + end + for i=1,#names do + local name = names[i] + name = name:trim() + name = name:lower() + + if combinedString:find(name) then + playSound(g_resources.fileExists("/sounds/Special_Creature.ogg") and "/sounds/Special_Creature.ogg" or "/sounds/Creature_Detected.ogg") + delay(1500) + g_window.setTitle(pName .. " - Special Creature Detected: "..name) + return + end + + end + end + end + + if config.healthBelow then + if hppercent() <= config.healthValue then + playSound("/sounds/Low_Health.ogg") + delay(1500) + g_window.setTitle(pName .. " - Low Health! only: "..hppercent().."%") + return + end + end + + if config.manaBelow then + if manapercent() <= config.manaValue then + playSound("/sounds/Low_Mana.ogg") + delay(1500) + g_window.setTitle(pName .. " - Low Mana! only: "..manapercent().."%") + return + end + end + end) + + onTalk(function(name, level, mode, text, channelId, pos) + if mode == 4 and config.enabled and config.privateMessage then + playSound("/sounds/Private_Message.ogg") + g_window.setTitle(pName .. " - Private Message from: " .. name) + return + end + end) +end + +ui.alerts.onClick = function(widget) + alarmsWindow:show() + alarmsWindow:raise() + alarmsWindow:focus() +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/alarms.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/alarms.otui new file mode 100644 index 0000000000..c530efcd97 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/alarms.otui @@ -0,0 +1,181 @@ +AlarmsWindow < MainWindow + !text: tr('Alarms') + size: 300 280 + @onEscape: self:hide() + + BotSwitch + id: playerAttack + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-align: center + text: Player Attack + !tooltip: tr('Alerts when attacked by player.') + + BotSwitch + id: playerDetected + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 4 + text-align: center + text: Player Detected + !tooltip: tr('Alerts when a player is detected on screen.') + + CheckBox + id: playerDetectedLogout + anchors.top: playerDetected.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + margin-top: 3 + margin-left: 4 + text: Logout + !tooltip: tr('Attempts to logout when a player is detected on screen.') + + BotSwitch + id: ignoreFriends + anchors.left: parent.left + anchors.top: playerDetected.bottom + anchors.right: parent.right + text-align: center + margin-top: 4 + text: Ignore Friends + !tooltip: tr('Player detection alerts will ignore friends.') + + HorizontalSeparator + id: sepPlayer + anchors.right: parent.right + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 4 + + BotSwitch + id: creatureDetected + anchors.left: parent.left + anchors.right: parent.right + anchors.top: sepPlayer.bottom + margin-top: 4 + text-align: center + text: Creature Detected + !tooltip: tr('Alerts when a creature is detected on screen.') + + BotSwitch + id: warnBoss + anchors.left: parent.left + anchors.top: prev.bottom + anchors.right: parent.horizontalCenter + text-align: center + margin-top: 5 + text: Creature Name + !tooltip: tr('Alerts when a creature/npc with name is detected on screen. \n eg: Benjamin or [boss] would detect a creature with [boss] in name. \n You can add many examples, just separate them by comma.') + + BotTextEdit + id: bossName + anchors.left: prev.right + margin-left: 4 + anchors.top: prev.top + anchors.right: parent.right + margin-top: 1 + height: 17 + font: terminus-10px + + HorizontalSeparator + id: sepCreature + anchors.right: parent.right + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 4 + + BotSwitch + id: healthBelow + anchors.left: parent.left + anchors.top: prev.bottom + anchors.right: parent.horizontalCenter + text-align: center + margin-top: 4 + text: Health < 50% + + HorizontalScrollBar + id: healthValue + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: healthBelow.top + margin-left: 3 + margin-top: 2 + minimum: 1 + maximum: 100 + step: 1 + + BotSwitch + id: manaBelow + anchors.left: parent.left + anchors.top: healthBelow.bottom + anchors.right: parent.horizontalCenter + text-align: center + margin-top: 4 + text: Mana < 50% + + HorizontalScrollBar + id: manaValue + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: manaBelow.top + margin-left: 3 + margin-top: 2 + minimum: 1 + maximum: 100 + step: 1 + + HorizontalSeparator + id: sepMessages + anchors.right: parent.right + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 4 + + BotSwitch + id: privateMessage + anchors.left: parent.left + anchors.top: prev.bottom + anchors.right: parent.right + text-align: center + margin-top: 4 + text: Private Message + !tooltip: tr('Alerts when recieving a private message.') + + BotSwitch + id: warnMessage + anchors.left: parent.left + anchors.top: prev.bottom + anchors.right: parent.horizontalCenter + text-align: center + margin-top: 5 + text: Message Alert + !tooltip: tr('Alerts when players receive a message that contains given part. \n Eg. event - will trigger alert whenever a message with word event appears. \n You can give many examples, just separate them by comma.') + + BotTextEdit + id: messageText + anchors.left: prev.right + margin-left: 4 + anchors.top: prev.top + anchors.right: parent.right + margin-top: 1 + height: 17 + font: terminus-10px + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.lua new file mode 100644 index 0000000000..e4a77d7723 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.lua @@ -0,0 +1,1732 @@ +--[[ + Bot-based Tibia 12 features v1.1 + made by Vithrax + + Credits also to: + - Martín#2318 + - Lee#7725 + + Thanks for ideas, graphics, functions, design tips! + + br, Vithrax +]] + +vBot.CaveBotData = vBot.CaveBotData or { + refills = 0, + rounds = 0, + time = {}, + lastRefill = os.time(), + refillTime = {} +} +local lootWorth = 0 +local wasteWorth = 0 +local balance = 0 +local balanceDesc = "" +local hourDesc = "" +local desc = "" +local hour = "" +local launchTime = now +local startExp = exp() +local dmgTable = {} +local healTable = {} +local expTable = {} +local totalDmg = 0 +local totalHeal = 0 +local dmgDistribution = {} +local first = { l = "-", r = "0" } +local second = { l = "-", r = "0" } +local third = { l = "-", r = "0" } +local fourth = { l = "-", r = "0" } +local five = { l = "-", r = "0" } +storage.bestHit = storage.bestHit or 0 +storage.bestHeal = storage.bestHeal or 0 +local lootedItems = {} +local useData = {} +local usedItems = {} +local lastDataSend = { 0, 0 } +local analyzerButton +local killList = {} +local membersData = {} +HuntingSessionStart = os.date('%Y-%m-%d, %H:%M:%S') + +if not storage.analyzers then + storage.analyzers = { + trackedLoot = {}, + customPrices = {}, + lootChannel = true, + rarityFrames = true + } +end + +storage.analyzers = storage.analyzers or {} +storage.analyzers.trackedLoot = storage.analyzers.trackedLoot or {} + +local trackedLoot = storage.analyzers.trackedLoot + +--destroy old windows +local windowsTable = { "MainAnalyzerWindow", + "HuntingAnalyzerWindow", + "LootAnalyzerWindow", + "SupplyAnalyzerWindow", + "ImpactAnalyzerWindow", + "XPAnalyzerWindow", + "PartyAnalyzerWindow", + "DropTracker", + "CaveBotStats" +} + +for i, window in ipairs(windowsTable) do + local element = g_ui.getRootWidget():recursiveGetChildById(window) + + if element then + element:destroy() + end +end + +local mainWindow = UI.createMiniWindow("MainAnalyzerWindow") +mainWindow:hide() +mainWindow:setContentMaximumHeight(240) +local huntingWindow = UI.createMiniWindow("HuntingAnalyzer") +huntingWindow:hide() +local lootWindow = UI.createMiniWindow("LootAnalyzer") +lootWindow:hide() +local supplyWindow = UI.createMiniWindow("SupplyAnalyzer") +supplyWindow:hide() +local impactWindow = UI.createMiniWindow("ImpactAnalyzer") +impactWindow:hide() +impactWindow:setContentMaximumHeight(615) +local xpWindow = UI.createMiniWindow("XPAnalyzer") +xpWindow:hide() +xpWindow:setContentMaximumHeight(230) +local settingsWindow = UI.createWindow("FeaturesWindow") +settingsWindow:hide() +local partyHuntWindow = UI.createMiniWindow("PartyAnalyzerWindow") +partyHuntWindow:hide() +local dropTrackerWindow = UI.createMiniWindow("DropTracker") +dropTrackerWindow:hide() +local statsWindow = UI.createMiniWindow("CaveBotStats") +statsWindow:hide() + +--f +local toggle = function() + if mainWindow:isVisible() then + analyzerButton:setOn(false) + mainWindow:close() + else + analyzerButton:setOn(true) + mainWindow:open() + end +end + +local drawGraph = function(graph, value) + graph:addValue(value) +end + +local toggleAnalyzer = function(window) + if window:isVisible() then + window:hide() + else + window:show() + end +end + +local function getSumStats() + local totalWaste = 0 + local totalLoot = 0 + + for k, v in pairs(membersData) do + totalWaste = totalWaste + v.waste + totalLoot = totalLoot + v.loot + end + + local totalBalance = totalLoot - totalWaste + + return totalWaste, totalLoot, totalBalance +end + +local function clipboardData() + local totalWaste, totalLoot, totalBalance = getSumStats() + local final = "" + + + local first = "Session data: From " .. HuntingSessionStart .. " to " .. os.date('%Y-%m-%d, %H:%M:%S') + local second = "Session: " .. sessionTime() + local third = "Loot Type: Market" + local fourth = "Loot " .. format_thousand(totalLoot, true) + local fifth = "Supplies " .. format_thousand(totalWaste, true) + local six = "Balance " .. format_thousand(totalBalance, true) + + local t = { first, second, third, fourth, fifth, six } + for i, string in ipairs(t) do + final = final .. "\n" .. string + end + + --user data now + for k, v in pairs(membersData) do + final = final .. "\n" .. k + + final = final .. "\n\tLoot " .. v.loot + final = final .. "\n\tSupplies " .. v.waste + final = final .. "\n\tBalance " .. v.balance + final = final .. "\n\tDamage " .. v.damage + final = final .. "\n\tHealing " .. v.heal + end + + g_window.setClipboardText(final) +end + +-- create analyzers button +analyzerButton = modules.game_buttons.buttonsWindow.contentsPanel and +modules.game_buttons.buttonsWindow.contentsPanel.buttons.botAnalyzersButton +analyzerButton = analyzerButton or modules.client_topmenu.getButton("botAnalyzersButton") +if analyzerButton then + analyzerButton:destroy() +end + +--button +analyzerButton = modules.client_topmenu.addRightGameToggleButton('botAnalyzersButton', 'vBot Analyzers', + '/images/topbuttons/analyzers', toggle, false, 999999) +analyzerButton:setOn(false) + +--toggles window +mainWindow.contentsPanel.HuntingAnalyzer.onClick = function() + toggleAnalyzer(huntingWindow) +end +mainWindow.onClose = function() + analyzerButton:setOn(false) +end +mainWindow.contentsPanel.LootAnalyzer.onClick = function() + toggleAnalyzer(lootWindow) +end +mainWindow.contentsPanel.SupplyAnalyzer.onClick = function() + toggleAnalyzer(supplyWindow) +end +mainWindow.contentsPanel.ImpactAnalyzer.onClick = function() + toggleAnalyzer(impactWindow) +end +mainWindow.contentsPanel.XPAnalyzer.onClick = function() + toggleAnalyzer(xpWindow) +end +mainWindow.contentsPanel.PartyHunt.onClick = function() + toggleAnalyzer(partyHuntWindow) +end +mainWindow.contentsPanel.DropTracker.onClick = function() + toggleAnalyzer(dropTrackerWindow) +end +mainWindow.contentsPanel.Stats.onClick = function() + toggleAnalyzer(statsWindow) +end + +--stats window +local totalRounds = UI.DualLabel("Total Rounds:", "0", {}, statsWindow.contentsPanel).right +local avRoundTime = UI.DualLabel("Time by Round:", "00:00h", {}, statsWindow.contentsPanel).right +UI.Separator(statsWindow.contentsPanel) +local totalRefills = UI.DualLabel("Total Refills:", "0", {}, statsWindow.contentsPanel).right +local avRefillTime = UI.DualLabel("Time by Refill:", "00:00h", {}, statsWindow.contentsPanel).right +local lastRefill = UI.DualLabel("Time since Refill:", "00:00h", { maxWidth = 200 }, statsWindow.contentsPanel).right +UI.Separator(statsWindow.contentsPanel) +local label = UI.DualLabel("Supplies by Round:", "", { maxWidth = 200 }, statsWindow.contentsPanel).left +label:setColor('#EC9706') +local suppliesByRound = UI.createWidget("AnalyzerItemsPanel", statsWindow.contentsPanel) +UI.Separator(statsWindow.contentsPanel) +label = UI.DualLabel("Supplies by Refill:", "", { maxWidth = 200 }, statsWindow.contentsPanel).left +label:setColor('#ED7117') +local suppliesByRefill = UI.createWidget("AnalyzerItemsPanel", statsWindow.contentsPanel) +UI.Separator(statsWindow.contentsPanel) + + +--huntig +local sessionTimeLabel = UI.DualLabel("Session:", "00:00h", {}, huntingWindow.contentsPanel).right +local xpGainLabel = UI.DualLabel("XP Gain:", "0", {}, huntingWindow.contentsPanel).right +local xpHourLabel = UI.DualLabel("XP/h:", "0", {}, huntingWindow.contentsPanel).right +local lootLabel = UI.DualLabel("Loot:", "0", {}, huntingWindow.contentsPanel).right +local suppliesLabel = UI.DualLabel("Supplies:", "0", {}, huntingWindow.contentsPanel).right +local balanceLabel = UI.DualLabel("Balance:", "0", {}, huntingWindow.contentsPanel).right +local damageLabel = UI.DualLabel("Damage:", "0", {}, huntingWindow.contentsPanel).right +local damageHourLabel = UI.DualLabel("Damage/h:", "0", {}, huntingWindow.contentsPanel).right +local healingLabel = UI.DualLabel("Healing:", "0", {}, huntingWindow.contentsPanel).right +local healingHourLabel = UI.DualLabel("Healing/h:", "0", {}, huntingWindow.contentsPanel).right +UI.DualLabel("Killed Monsters:", "", { maxWidth = 200 }, huntingWindow.contentsPanel) +local killedList = UI.createWidget("AnalyzerListPanel", huntingWindow.contentsPanel) +UI.DualLabel("Looted items:", "", { maxWidth = 200 }, huntingWindow.contentsPanel) +local lootList = UI.createWidget("AnalyzerListPanel", huntingWindow.contentsPanel) + + +--party +UI.Button("Copy to Clipboard", function() clipboardData() end, partyHuntWindow.contentsPanel) +UI.Button("Reset Sessions", function() + if BotServer._websocket then + BotServer.send("partyHunt", false) + end +end, partyHuntWindow.contentsPanel) + +local switch = addSwitch("sendData", "Send Analyzer Data", function(widget) + widget:setOn(not widget:isOn()) + storage.sendPartyAnalyzerData = widget:isOn() +end, partyHuntWindow.contentsPanel) +switch:setOn(storage.sendPartyAnalyzerData) +UI.Separator(partyHuntWindow.contentsPanel) +local partySessionTimeLabel = UI.DualLabel("Session:", "00:00h", {}, partyHuntWindow.contentsPanel).right +local partyLootLabel = UI.DualLabel("Loot:", "0", {}, partyHuntWindow.contentsPanel).right +local partySuppliesLabel = UI.DualLabel("Supplies:", "0", {}, partyHuntWindow.contentsPanel).right +local partyBalanceLabel = UI.DualLabel("Balance:", "0", {}, partyHuntWindow.contentsPanel).right +UI.Separator(partyHuntWindow.contentsPanel) + +local function maintainDropTable() + local panel = dropTrackerWindow.contentsPanel + + for k, v in pairs(trackedLoot) do + local widget = panel[k] + if not widget then + trackedLoot[k] = nil + end + end +end + +local function createTrackedItems() + local panel = dropTrackerWindow.contentsPanel + + for i, child in ipairs(panel:getChildren()) do + if i > 2 then + child:destroy() + end + end + + for k, v in pairs(trackedLoot) do + local dropLoot = UI.createWidget("TrackerItem", dropTrackerWindow.contentsPanel) + local item = dropLoot.item + local name = dropLoot.name + local drops = dropLoot.drops + local id = tonumber(k) + local itemName = id == 3031 and "gold coin" or id == 3035 and "platinum coin" or id == 3043 and "crystal coin" or + Item.create(id):getMarketData().name + + dropLoot:setId(id) + item:setItemId(id) + if item:getItemCount() > 1 then + item:setItemCount(1) + end + name:setText(itemName) + drops:setText("Loot Drops: " .. v) + + dropLoot.onDoubleClick = function() + local id = dropLoot.item:getItemId() + trackedLoot[tostring(id)] = 0 + drops:setText("Loot Drops: 0") + end + + for i, child in pairs(dropLoot:getChildren()) do + child:setTooltip("Double click to reset or clear item to remove.") + end + + item.onItemChange = function(widget) + local id = widget:getItemId() + if id == 0 then + trackedLoot[widget:getParent():getId()] = nil + if tonumber(widget:getParent():getId()) then + widget:getParent():destroy() + return + end + widget:setImageSource('/images/ui/item') + widget:getParent():setId("blank") + name:setText("Set Item to start track.") + drops:setText("Loot Drops: 0") + return + end + + -- only amount have changed, ignore + if tonumber(widget:getParent():getId()) == id then return end + local itemName = id == 3031 and "gold coin" or id == 3035 and "platinum coin" or id == 3043 and "crystal coin" or + Item.create(id):getMarketData().name + + if trackedLoot[tostring(id)] then + warn("vBot[Drop Tracker]: Item already added!") + name:setText("Set Item to start track.") + widget:setItemId(0) + return + end + + widget:setImageSource('') + drops:setText("Loot Drops: 0") + name:setText(itemName) + trackedLoot[tostring(id)] = trackedLoot[tostring(id)] or 0 + widget:getParent():setId(id) + maintainDropTable() + end + end +end + +--drop tracker +UI.Button("Add item to track drops", function() + local dropLoot = UI.createWidget("TrackerItem", dropTrackerWindow.contentsPanel) + local item = dropLoot.item + local name = dropLoot.name + local drops = dropLoot.drops + + item:setImageSource('/images/ui/item') + + dropLoot.onDoubleClick = function() + local id = dropLoot.item:getItemId() + trackedLoot[tostring(id)] = 0 + drops:setText("Loot Drops: 0") + end + + for i, child in pairs(dropLoot:getChildren()) do + child:setTooltip("Double click to reset or clear item to remove.") + end + + item.onItemChange = function(widget) + local id = widget:getItemId() + + if id == 0 then + trackedLoot[widget:getParent():getId()] = nil + if tonumber(widget:getParent():getId()) then + widget:getParent():destroy() + return + end + widget:setImageSource('/images/ui/item') + widget:getParent():setId("blank") + name:setText("Set Item to start track.") + drops:setText("Loot Drops: 0") + return + end + + -- only amount have changed, ignore + if tonumber(widget:getParent():getId()) == id then return end + local itemName = id == 3031 and "gold coin" or id == 3035 and "platinum coin" or id == 3043 and "crystal coin" or + Item.create(id):getMarketData().name + + if trackedLoot[tostring(id)] then + warn("vBot[Drop Tracker]: Item already added!") + name:setText("Set Item to start track.") + widget:setItemId(0) + return + end + + widget:setImageSource('') + drops:setText("Loot Drops: 0") + name:setText(itemName) + trackedLoot[tostring(id)] = trackedLoot[tostring(id)] or 0 + widget:getParent():setId(id) + maintainDropTable() + end +end, dropTrackerWindow.contentsPanel) + +UI.Separator(dropTrackerWindow.contentsPanel) +createTrackedItems() + + +--loot +local lootInLootAnalyzerLabel = UI.DualLabel("Gold Value:", "0", {}, lootWindow.contentsPanel).right +local lootHourInLootAnalyzerLabel = UI.DualLabel("Per Hour:", "0", {}, lootWindow.contentsPanel).right +UI.Separator(lootWindow.contentsPanel) +--//items panel +local lootItems = UI.createWidget("AnalyzerItemsPanel", lootWindow.contentsPanel) +UI.Separator(lootWindow.contentsPanel) +--//graph +local lootGraph = UI.createWidget("AnalyzerGraph", lootWindow.contentsPanel) +lootGraph:setTitle("Loot/h") +drawGraph(lootGraph, 0) + + + + +--supplies +local suppliesInSuppliesAnalyzerLabel = UI.DualLabel("Gold Value:", "0", {}, supplyWindow.contentsPanel).right +local suppliesHourInSuppliesAnalyzerLabel = UI.DualLabel("Per Hour:", "0", {}, supplyWindow.contentsPanel).right +UI.Separator(supplyWindow.contentsPanel) +--//items panel +local supplyItems = UI.createWidget("AnalyzerItemsPanel", supplyWindow.contentsPanel) +UI.Separator(supplyWindow.contentsPanel) +--//graph +local supplyGraph = UI.createWidget("AnalyzerGraph", supplyWindow.contentsPanel) +supplyGraph:setTitle("Waste/h") +drawGraph(supplyGraph, 0) + + + + +-- impact + +--- damage +local title = UI.DualLabel("Damage", "", {}, impactWindow.contentsPanel).left +title:setColor('#E3242B') +local totalDamageLabel = UI.DualLabel("Total:", "0", {}, impactWindow.contentsPanel).right +local maxDpsLabel = UI.DualLabel("Max-DPS:", "0", {}, impactWindow.contentsPanel).right +local bestHitLabel = UI.DualLabel("All-Time High:", "0", {}, impactWindow.contentsPanel).right +UI.Separator(impactWindow.contentsPanel) +local dmgGraph = UI.createWidget("AnalyzerGraph", impactWindow.contentsPanel) +dmgGraph:setTitle("DPS") +drawGraph(dmgGraph, 0) + + +--- distribution +UI.Separator(impactWindow.contentsPanel) +local title2 = UI.DualLabel("Damage Distribution", "", { maxWidth = 150 }, impactWindow.contentsPanel).left +title2:setColor('#FABD02') +local top1 = UI.DualLabel("-", "0", { maxWidth = 200 }, impactWindow.contentsPanel) +local top2 = UI.DualLabel("-", "0", { maxWidth = 200 }, impactWindow.contentsPanel) +local top3 = UI.DualLabel("-", "0", { maxWidth = 200 }, impactWindow.contentsPanel) +local top4 = UI.DualLabel("-", "0", { maxWidth = 200 }, impactWindow.contentsPanel) +local top5 = UI.DualLabel("-", "0", { maxWidth = 200 }, impactWindow.contentsPanel) + +top1.left:setWidth(135) +top2.left:setWidth(135) +top3.left:setWidth(135) +top4.left:setWidth(135) +top5.left:setWidth(135) + + +--- healing +UI.Separator(impactWindow.contentsPanel) +local title3 = UI.DualLabel("Healing", "", {}, impactWindow.contentsPanel).left +title3:setColor('#03C04A') +local totalHealingLabel = UI.DualLabel("Total:", "0", {}, impactWindow.contentsPanel).right +local maxHpsLabel = UI.DualLabel("Max-HPS:", "0", {}, impactWindow.contentsPanel).right +local bestHealLabel = UI.DualLabel("All-Time High:", "0", {}, impactWindow.contentsPanel).right +UI.Separator(impactWindow.contentsPanel) +--//graph +local healGraph = UI.createWidget("AnalyzerGraph", impactWindow.contentsPanel) +healGraph:setTitle("HPS") +drawGraph(healGraph, 0) + + + + + + + +--xp +local xpGrainInXpLabel = UI.DualLabel("XP Gain:", "0", {}, xpWindow.contentsPanel).right +local xpHourInXpLabel = UI.DualLabel("XP/h:", "0", {}, xpWindow.contentsPanel).right +local nextLevelLabel = UI.DualLabel("Next Level:", "-", {}, xpWindow.contentsPanel).right +local progressBar = UI.createWidget("AnalyzerProgressBar", xpWindow.contentsPanel) +progressBar:setPercent(modules.game_skills.skillsWindow.contentsPanel.level.percent:getPercent()) +UI.Separator(xpWindow.contentsPanel) +--//graph +local xpGraph = UI.createWidget("AnalyzerGraph", xpWindow.contentsPanel) +xpGraph:setTitle("XP/h") +drawGraph(xpGraph, 0) + + + + + +--############################################# +--############################################# UI DONE +--############################################# +--############################################# +--############################################# +--############################################# + +setDefaultTab("Main") +-- first, the variables + +local console = modules.game_console +local regex = [[ ([^,|^.]+)]] +local noData = {} +local data = {} + +local function getColor(v) + if v >= 10000000 then -- 10kk, red + return "#FF0000" + elseif v >= 5000000 then -- 5kk, orange + return "#FFA500" + elseif v >= 1000000 then -- 1kk, yellow + return "#FFFF00" + elseif v >= 100000 then -- 100k, purple + return "#F25AED" + elseif v >= 10000 then -- 10k, blue + return "#5F8DF7" + elseif v >= 1000 then -- 1k, green + return "#00FF00" + elseif v >= 50 then + return "#FFFFFF" -- 50gp, white + else + return "#aaaaaa" -- less than 100gp, grey + end +end + +local function formatStr(str) + if string.starts(str, "a ") then + str = str:sub(2, #str) + elseif string.starts(str, "an ") then + str = str:sub(3, #str) + end + + local n = getFirstNumberInText(str) + if n then + str = string.split(str, tostring(n))[1] + str = str:sub(1, #str - 1) + end + + return str:trim() +end + +local function getPrice(name) + name = formatStr(name) + name = name:lower() + -- first check custom prices + if storage.analyzers.customPrices[name] then + return storage.analyzers.customPrices[name] + end + + -- if already checked and no data skip looping items.lua + if noData[name] then + return 0 + end + + -- maybe was already checked, if so, skip looping items.lua + if data[name] then + return data[name] + end + + -- searching in items.lua - big table, if possible skip + for k, v in pairs(LootItems) do + if name == k then + data[name] = v + return v + end + end + + -- if no data, save it and return 0 + noData[name] = true + return 0 +end + +local expGained = function() + return exp() - startExp +end + +function format_thousand(v, comma) + comma = comma and "," or "." + if not v then return 0 end + local s = string.format("%d", math.floor(v)) + local pos = string.len(s) % 3 + if pos == 0 then pos = 3 end + return string.sub(s, 1, pos) + .. string.gsub(string.sub(s, pos + 1), "(...)", comma .. "%1") +end + +local expLeft = function() + local level = lvl() + 1 + return math.floor((50 * level * level * level) / 3 - 100 * level * level + (850 * level) / 3 - 200) - exp() +end + +niceTimeFormat = function(v, seconds) -- v in seconds + local hours = string.format("%02.f", math.floor(v / 3600)) + local mins = string.format("%02.f", math.floor(v / 60 - (hours * 60))) + local secs = string.format("%02.f", math.floor(math.fmod(v, 60))) + + local final = string.format('%s:%s%s', hours, mins, seconds and ":" .. secs or "") + return final +end +local uptime +sessionTime = function() + uptime = math.floor((now - launchTime) / 1000) + return niceTimeFormat(uptime) +end +sessionTime() + +local expPerHour = function(calculation) + local r = 0 + if #expTable > 0 then + r = exp() - expTable[1] + else + return "-" + end + + if uptime < 15 * 60 then + r = math.ceil((r / uptime) * 60 * 60) + else + r = math.ceil(r * 8) + end + if calculation then + return r + else + return format_thousand(r) + end +end + +local function add(t, text, color, last) + table.insert(t, text) + table.insert(t, color) + if not last then + table.insert(t, ", ") + table.insert(t, "#FFFFFF") + end +end + +-- Bot Server +local function sendData() + if BotServer._websocket then + local totalDmg, totalHeal, lootWorth, wasteWorth, balance = getHuntingData() + local outfit = player:getOutfit() + outfit.mount = 0 + local t = { + totalDmg, + totalHeal, + balance, + hppercent(), + manapercent(), + outfit, + player:isPartyLeader(), + lootWorth, + wasteWorth, + modules.game_skills.skillsWindow.contentsPanel.stamina.value:getText(), + format_thousand(expGained()), + expPerHour(), + balanceDesc .. " (" .. hourDesc .. ")", + sessionTime() + } + + -- validation + if lastDataSend.totalDmg ~= t[1] and lastDataSend.totalHeal ~= t[2] then + BotServer.send("partyHunt", t) + lastDataSend[1] = t[1] + lastDataSend[2] = t[2] + end + end +end + +-- process data +BotServer.listen("partyHunt", function(name, message) + if message == true then + sendData() + elseif message == false then + resetAnalyzerSessionData() + else + membersData[name] = { + damage = message[1], + heal = message[2], + balance = message[3], + hp = message[4], + mana = message[5], + outfit = message[6], + leader = message[7], + loot = message[8], + waste = message[9], + stamina = message[10], + expGained = message[11], + expH = message[12], + balanceH = message[13], + session = message[14] + } + + local widgetName = "Widget" .. name + local widget = partyHuntWindow.contentsPanel[widgetName] or + UI.createWidget("MemberWidget", partyHuntWindow.contentsPanel) + widget:setId(widgetName) + widget.lastUpdate = now + + + local t = membersData[name] + widget.name:setText(name) + widget.name:setColor("white") + if t.leader then + widget.name:setColor('#f8db38') + end + schedule(10 * 1000, function() + if widget and widget.lastUpdate and now - widget.lastUpdate > 10000 then + widget.name:setText(widget.name:getText() .. " [inactive]") + widget.name:setColor("#aeaeae") + widget.health:setBackgroundColor("#aeaeae") + widget.mana:setBackgroundColor("#aeaeae") + widget.balance.value:setText("-") + widget.damage.value:setText("-") + widget.healing.value:setText("-") + widget.creature:disable() + end + end) + widget.creature:setOutfit(t.outfit) + widget.health:setPercent(t.hp) + widget.health:setBackgroundColor("#00c000") + widget.mana:setPercent(t.mana) + widget.mana:setBackgroundColor("#0000FF") + widget.balance.value:setText(format_thousand(t.balance)) + if t.balance < 0 then + widget.balance.value:setColor('#ff9854') + elseif t.balance > 0 then + widget.balance.value:setColor('#45ad25') + else + widget.balance.value:setColor('white') + end + widget.damage.value:setText(format_thousand(t.damage)) + widget.healing.value:setText(format_thousand(t.heal)) + + widget.onDoubleClick = function() + membersData[name] = nil + widget:destroy() + end + + --tooltip + local tooltip = "Session: " .. t.session .. "\n" .. + "Stamina: " .. t.stamina .. "\n" .. + "Exp Gained: " .. t.expGained .. "\n" .. + "Exp per Hour: " .. t.expH .. "\n" .. + "Balance: " .. t.balanceH + + widget.creature:setTooltip(tooltip) + end +end) + + +function hightlightText(widget, color, duration) + for i = 0, duration do + schedule(i * 250, function() + if i == duration or (i > 0 and i % 2 == 0) then + widget:setColor("#FFFFFF") + else + widget:setColor(color) + end + end) + end +end + +local nameRegex = [[Loot of (?:an |a |the |)([^:]+)]] +onTextMessage(function(mode, text) + if not storage.analyzers.lootChannel then return end + if not text:find("Loot of") and not text:find("The following items are available in your reward chest") then return end + local name + + -- adding monster to killed list + if text:find("Loot of") then + name = regexMatch(text, nameRegex)[1][2] + if not killList[name] then + killList[name] = 1 + else + killList[name] = killList[name] + 1 + end + refreshKills() + end + -- variables + local split = string.split(text, ":") + local re = regexMatch(split[2], regex) + local combinedWorth = 0 + local formatted + local div + local t = {} + local messageT = {} + + -- add timestamp, creature part and color it as white + add(t, os.date('%H:%M') .. ' ' .. split[1] .. ": ", "#FFFFFF", true) + add(messageT, split[1] .. ": ", "#FFFFFF", true) + + -- main part + if re ~= 0 then + for i = 1, #re do + local data = re[i][2] -- each looted item + local formattedLoot = regexMatch(data, [[(^[^(]+)]])[1][1] + formattedLoot = formattedLoot:trim() + local amount = getFirstNumberInText(formattedLoot) -- amount found in data + local price = amount and getPrice(formattedLoot) * amount or + getPrice(formattedLoot) -- if amount then multity price, else just take price + local color = getColor(price) -- generate hex string based off price + local messageColor = getColor(getPrice(formattedLoot)) + + combinedWorth = combinedWorth + price -- add all prices to calculate total worth + + add(t, data, color, i == #re) + add(messageT, data, color, i == #re) + + --drop tracker + for i, child in ipairs(dropTrackerWindow.contentsPanel:getChildren()) do + local childName = child.name + childName = childName and childName:getText() + + + if childName and formattedLoot:find(childName) then + trackedLoot[tostring(child.item:getItemId())] = trackedLoot[tostring(child.item:getItemId())] + (amount or 1) + child.drops:setText("Loot Drops: " .. trackedLoot[tostring(child.item:getItemId())]) + + hightlightText(child.name, "#f0b400", 8) + modules.game_textmessage.messagesPanel.statusLabel:setVisible(true) + modules.game_textmessage.messagesPanel.statusLabel:setColoredText({ + "Valuable loot: ", "#f0b400", + childName .. "", messageColor, + " dropped by " .. name .. "!", "#f0b400" + }) + schedule(3000, function() + modules.game_textmessage.messagesPanel.statusLabel:setVisible(false) + end) + end + end + end + end + + -- format total worth so it wont look obnoxious + if combinedWorth >= 1000000 then + div = combinedWorth / 1000000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "kk" + elseif combinedWorth >= 1000 then + div = combinedWorth / 1000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "k" + else + formatted = combinedWorth .. "gp" + end + + if modules.game_textmessage.messagesPanel.centerTextMessagePanel.highCenterLabel:getText() == text then + modules.game_textmessage.messagesPanel.centerTextMessagePanel.highCenterLabel:setColoredText(messageT) + schedule(math.max(#text * 50, 2000), function() + modules.game_textmessage.messagesPanel.centerTextMessagePanel.highCenterLabel:setVisible(false) + end) + end + + -- add total worth to string + add(t, " - (", "#FFFFFF", true) + add(t, formatted, getColor(combinedWorth), true) + add(t, ")", "#FFFFFF", true) + + -- get/create tab and write raw message + local tabName = "vBot Loot" + local tab = console.getTab(tabName) or console.addTab(tabName, true) + console.addText(text, console.SpeakTypesSettings, tabName, "") + + -- find last message in given tab and rewrite it with formatted string + local panel = console.consoleTabBar:getTabPanel(tab) + local consoleBuffer = panel:getChildById('consoleBuffer') + local message = consoleBuffer:getLastChild() + message:setColoredText(t) +end) + +local function niceFormat(v) + local div + local formatted + if v >= 10000000 then + div = v / 10000000 + formatted = math.ceil(div) .. "M" + elseif v >= 1000000 then + div = v / 1000000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "M" + elseif v >= 10000 then + div = v / 1000 + formatted = math.floor(div) .. "k" + elseif v >= 1000 then + div = v / 1000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "k" + else + formatted = v + end + return formatted +end + +resetAnalyzerSessionData = function() + vBot.CaveBotData = vBot.CaveBotData or { + refills = 0, + rounds = 0, + time = {}, + lastRefill = os.time(), + refillTime = {} + } + launchTime = now + startExp = exp() + dmgTable = {} + healTable = {} + expTable = {} + totalDmg = 0 + totalHeal = 0 + dmgDistribution = {} + first = { l = "-", r = "0" } + second = { l = "-", r = "0" } + third = { l = "-", r = "0" } + fourth = { l = "-", r = "0" } + five = { l = "-", r = "0" } + lootedItems = {} + useData = {} + usedItems = {} + refreshLoot() + refreshWaste() + xpGraph:clear() + drawGraph(xpGraph, 0) + lootGraph:clear() + drawGraph(lootGraph, 0) + supplyGraph:clear() + drawGraph(supplyGraph, 0) + dmgGraph:clear() + drawGraph(dmgGraph, 0) + healGraph:clear() + drawGraph(healGraph, 0) + killList = {} + refreshKills() + HuntingSessionStart = os.date('%Y-%m-%d, %H:%M:%S') +end + +mainWindow.contentsPanel.ResetSession.onClick = function() + resetAnalyzerSessionData() +end + +mainWindow.contentsPanel.Settings.onClick = function() + settingsWindow:show() + settingsWindow:raise() + settingsWindow:focus() +end + + +-- extras window +settingsWindow.closeButton.onClick = function() + settingsWindow:hide() +end + +local function getFrame(v) + if v >= 1000000 then + return '/images/ui/rarity_gold' + elseif v >= 100000 then + return '/images/ui/rarity_purple' + elseif v >= 10000 then + return '/images/ui/rarity_blue' + elseif v >= 1000 then + return '/images/ui/rarity_green' + else + return '/images/ui/item' + end +end + + +displayCondition = function(menuPosition, lookThing, useThing, creatureThing) + if lookThing and not lookThing:isCreature() and not lookThing:isNotMoveable() and lookThing:isPickupable() then + return true + end +end +local interface = modules.game_interface + +local function setFrames() + if not storage.analyzers.rarityFrames then return end + for _, container in pairs(getContainers()) do + local window = container.itemsPanel + for i, child in pairs(window:getChildren()) do + local id = child:getItemId() + local price = 0 + + if id ~= 0 then -- there's item + local item = Item.create(id) + local name = item:getMarketData().name:lower() + price = getPrice(name) + + -- set rarity frame + child:setImageSource(getFrame(price)) + else -- empty widget + -- revert any possible changes + child:setImageSource("/images/ui/item") + end + child.onHoverChange = function(widget, hovered) + if id == 0 or not hovered then + return interface.removeMenuHook('analyzer') + end + interface.addMenuHook('analyzer', 'Price:', function() end, displayCondition, price) + end + end + end +end +setFrames() + +onContainerOpen(function(container, previousContainer) + setFrames() +end) + +onAddItem(function(container, slot, item, oldItem) + setFrames() +end) + +onRemoveItem(function(container, slot, item) + setFrames() +end) + +onContainerUpdateItem(function(container, slot, item, oldItem) + setFrames() +end) + +function smallNumbers(n) + if n >= 10 ^ 6 then + return string.format("%.1fkk", n / 10 ^ 6) + elseif n >= 10 ^ 3 then + return string.format("%.1fk", n / 10 ^ 3) + else + return tostring(n) + end +end + +function refreshList() + local list = settingsWindow.CustomPrices + list:destroyChildren() + + for name, price in pairs(storage.analyzers.customPrices) do + local label = UI.createWidget("AnalyzerPriceLabel", list) + label.remove.onClick = function() + storage.analyzers.customPrices[name] = nil + label:destroy() + schedule(5, function() + setFrames() + end) + end + label:setText("[" .. name .. "] = " .. smallNumbers(price) .. " gp") + end +end + +refreshList() + +settingsWindow.addItem.onClick = function() + local newPrices = storage.analyzers.customPrices + local id = settingsWindow.ID:getItemId() + local newPrice = tonumber(settingsWindow.NewPrice:getText()) + + if id < 100 then + return warn("No item added!") + end + + local name = Item.create(id):getMarketData().name + + if newPrices[name] then + return warn("Item already added! Remove it from the list to set a new price!") + end + + newPrices[name] = newPrice + settingsWindow.ID:setItemId(0) + settingsWindow.NewPrice:setText(0) + schedule(5, function() + setFrames() + end) + refreshList() +end + +settingsWindow.LootChannel:setOn(storage.analyzers.lootChannel) +settingsWindow.LootChannel.onClick = function(widget) + storage.analyzers.lootChannel = not storage.analyzers.lootChannel + widget:setOn(storage.analyzers.lootChannel) +end + +settingsWindow.RarityFrames:setOn(storage.analyzers.rarityFrames) +settingsWindow.RarityFrames.onClick = function(widget) + storage.analyzers.rarityFrames = not storage.analyzers.rarityFrames + widget:setOn(storage.analyzers.rarityFrames) + setFrames() +end + +local timeToLevel = function() + local t = 0 + if expPerHour(true) == 0 or expPerHour() == "-" then + return "-" + else + t = expLeft() / expPerHour(true) + return niceTimeFormat(math.ceil(t * 60 * 60)) + end +end + +local sumT = function(t) + local s = 0 + for i, v in pairs(t) do + s = s + v.d + end + return s +end + +local valueInSeconds = function(t) + local d = 0 + local time = 0 + if #t > 0 then + for i, v in ipairs(t) do + if now - v.t <= 3000 then + if time == 0 then + time = v.t + end + d = d + v.d + else + table.remove(t, 1) + end + end + end + return math.ceil(d / ((now - time) / 1000)) +end + +local regex = "You lose ([0-9]*) hitpoints due to an attack by ([a-z]*) ([a-z A-z-]*)" +onTextMessage(function(mode, text) + local value = getFirstNumberInText(text) + if mode == 21 then -- damage dealt + totalDmg = totalDmg + value + table.insert(dmgTable, { d = value, t = now }) + if value > storage.bestHit then + storage.bestHit = value + end + end + if mode == 23 then -- healing + totalHeal = totalHeal + value + table.insert(healTable, { d = value, t = now }) + if value > storage.bestHeal then + storage.bestHeal = value + end + end + + -- damage distribution part + if text:find("You lose") then + local data = regexMatch(text, regex)[1] + if data then + local monster = data[4] + local val = data[2] + table.insert(dmgDistribution, { v = val, m = monster, t = now }) + end + end +end) + +function capitalFistLetter(str) + return (string.gsub(str, "^%l", string.upper)) +end + +-- tables maintance +macro(500, function() + local dmgFinal = {} + local labelTable = {} + local dmgSum = 0 + table.insert(expTable, exp()) + if #expTable > 15 * 60 then + for i, v in pairs(expTable) do + if i == 1 then + table.remove(expTable, i) + end + end + end + + for i, v in pairs(dmgDistribution) do + if now - v.t > 60 * 1000 * 10 then + table.remove(dmgDistribution, i) + else + dmgSum = dmgSum + v.v + if not dmgFinal[v.m] then + dmgFinal[v.m] = v.v + else + dmgFinal[v.m] = dmgFinal[v.m] + v.v + end + end + end + + first = dmgFinal[1] or { l = "-", r = "0" } + second = dmgFinal[2] or { l = "-", r = "0" } + third = dmgFinal[3] or { l = "-", r = "0" } + fourth = dmgFinal[4] or { l = "-", r = "0" } + five = dmgFinal[5] or { l = "-", r = "0" } + + for k, v in pairs(dmgFinal) do + table.insert(labelTable, { m = k, d = tonumber(v) }) + end + + table.sort(labelTable, function(a, b) return a.d > b.d end) + + for i, v in pairs(labelTable) do + local val = math.floor((v.d / dmgSum) * 100) .. "%" + local words = string.split(v.m, " ") + local name = "" + for i, word in ipairs(words) do + name = name .. " " .. capitalFistLetter(word) + end + name = name:len() < 20 and name or name:sub(1, 17) .. "..." + name = name:trim() .. ": " + if i == 1 then + first = { l = name, r = val } + elseif i == 2 then + second = { l = name, r = val } + elseif i == 3 then + third = { l = name, r = val } + elseif i == 4 then + fourth = { l = name, r = val } + elseif i == 5 then + five = { l = name, r = val } + else + break + end + end +end) + +function getPanelHeight(panel) + local elements = panel.List:getChildCount() + if elements == 0 then + return 0 + else + local rows = math.ceil(elements / 5) + local height = rows * 35 + return height + end +end + +function refreshLoot() + lootItems:destroyChildren() + lootList:destroyChildren() + + for k, v in pairs(lootedItems) do + local label1 = UI.createWidget("AnalyzerLootItem", lootItems) + local price = v.count and getPrice(v.name) * v.count or getPrice(v.name) + + label1:setItemId(k) + label1:setItemCount(50) + label1:setShowCount(false) + label1.count:setText(niceFormat(v.count)) + label1.count:setColor(getColor(price)) + local tooltipName = v.count > 1 and v.name .. "s" or v.name + label1:setTooltip(v.count .. + "x " .. tooltipName .. " (Value: " .. format_thousand(getPrice(v.name)) .. "gp, Sum: " .. + format_thousand(price) .. "gp)") + --hunting window loot list + local label2 = UI.createWidget("ListLabel", lootList) + label2:setText(v.count .. "x " .. v.name) + end + + if lootItems:getChildCount() == 0 then + local label = UI.createWidget("ListLabel", lootList) + label:setText("None") + end +end + +refreshLoot() + +function refreshKills() + killedList:destroyChildren() + local kills = 0 + for k, v in pairs(killList) do + kills = kills + 1 + local label = UI.createWidget("ListLabel", killedList) + label:setText(v .. "x " .. k) + end + + if kills == 0 then + local label = UI.createWidget("ListLabel", killedList) + label:setText("None") + end +end + +refreshKills() + +function refreshWaste() + supplyItems:destroyChildren() + suppliesByRefill:destroyChildren() + suppliesByRound:destroyChildren() + + local parents = { supplyItems, suppliesByRound, suppliesByRefill } + + for k, v in pairs(usedItems) do + for i = 1, #parents do + local amount = i == 1 and v.count or + i == 2 and v.count / (vBot.CaveBotData.rounds + 1) or + i == 3 and v.count / (vBot.CaveBotData.refills + 1) + amount = math.floor(amount) + local label1 = UI.createWidget("AnalyzerLootItem", parents[i]) + local price = amount and getPrice(v.name) * amount or getPrice(v.name) + + label1:setItemId(k) + label1:setItemCount(50) + label1:setShowCount(false) + label1.count:setText(niceFormat(amount)) + label1.count:setColor(getColor(price)) + local tooltipName = amount > 1 and v.name .. "s" or v.name + label1:setTooltip(amount .. + "x " .. tooltipName .. " (Value: " .. format_thousand(getPrice(v.name)) .. + "gp, Sum: " .. format_thousand(price) .. "gp)") + end + end +end + +-- loot analyzer +-- adding +local containers = CaveBot.GetLootContainers() +local lastCap = freecap() +onAddItem(function(container, slot, item, oldItem) + if not table.find(containers, container:getContainerItem():getId()) then return end + if isInPz() then return end + if slot > 0 then return end + if freecap() >= lastCap then return end + local name = item:getId() + local tmpname = item:getId() == 3031 and "gold coin" or item:getId() == 3035 and "platinum coin" or + item:getId() == 3043 and "crystal coin" or item:getMarketData().name + if not lootedItems[name] then + lootedItems[name] = { count = item:getCount(), name = tmpname } + else + lootedItems[name].count = lootedItems[name].count + item:getCount() + end + lastCap = freecap() + refreshLoot() + + -- drop tracker +end) + +onContainerUpdateItem(function(container, slot, item, oldItem) + if not table.find(containers, container:getContainerItem():getId()) then return end + if not oldItem then return end + if isInPz() then return end + if freecap() == lastCap then return end + + local tmpname = item:getId() == 3031 and "gold coin" or item:getId() == 3035 and "platinum coin" or + item:getId() == 3043 and "crystal coin" or item:getMarketData().name + local amount = item:getCount() - oldItem:getCount() + if amount < 0 then + return + end + local name = item:getId() + if not lootedItems[name] then + lootedItems[name] = { count = amount, name = tmpname } + else + lootedItems[name].count = lootedItems[name].count + amount + end + lastCap = freecap() + refreshLoot() +end) + +-- ammo +local ammo = { 16143, 763, 761, 7365, 3448, 762, 21470, 7364, 14251, 3447, 3449, 15793, 25757, 774, 35901, 6528, 7363, + 3450, 16141, 25758, 14252, 3446, 16142, 35902 } +onContainerUpdateItem(function(container, slot, item, oldItem) + local id = item:getId() + if not table.find(ammo, id) then return end + local newCount = item:getCount() + local oldCount = oldItem:getCount() + local name = item:getMarketData().name + + if oldCount - newCount == 1 then + if not usedItems[id] then + usedItems[id] = { count = 1, name = name } + else + usedItems[id].count = usedItems[id].count + 1 + end + refreshWaste() + end +end) + +-- waste +local regex3 = [[\d ([a-z A-Z]*)s...]] +local lackOfData = {} +onTextMessage(function(mode, text) + text = text:lower() + if not text:find("using one of") then return end + + local amount = getFirstNumberInText(text) + local re = regexMatch(text, regex3) + local name = re[1][2] + local id = WasteItems[name] + + if not id then + if not lackOfData[name] then + lackOfData[name] = true + print("[Analyzer] no data for item: " .. name .. "inside items.lua -> WasteItems") + end + + return + end + + if not useData[name] then + useData[name] = amount + else + if math.abs(useData[name] - amount) == 1 then + useData[name] = amount + if not usedItems[id] then + usedItems[id] = { count = 1, name = name } + else + usedItems[id].count = usedItems[id].count + 1 + end + else + useData[name] = amount + end + refreshWaste() + end +end) + +function hourVal(v) + v = v or 0 + return (v / uptime) * 3600 +end + +function bottingStats() + lootWorth = 0 + wasteWorth = 0 + for k, v in pairs(lootedItems) do + if LootItems[v.name] then + lootWorth = lootWorth + (LootItems[v.name] * v.count) + end + end + for k, v in pairs(usedItems) do + if LootItems[v.name] then + wasteWorth = wasteWorth + (LootItems[v.name] * v.count) + end + end + balance = lootWorth - wasteWorth + + return lootWorth, wasteWorth, balance +end + +function bottingLabels(lootWorth, wasteWorth, balance) + balanceDesc = nil + hourDesc = nil + desc = nil + + if balance >= 1000000 or balance <= -1000000 then + desc = balance / 1000000 + balanceDesc = math.floor(desc) .. "." .. math.floor(desc * 10) % 10 .. "kk" + elseif balance >= 1000 or balance <= -1000 then + desc = balance / 1000 + balanceDesc = math.floor(desc) .. "." .. math.floor(desc * 10) % 10 .. "k" + else + balanceDesc = balance .. "gp" + end + + hour = hourVal(balance) + if hour >= 1000000 or hour <= -1000000 then + desc = balance / 1000000 + hourDesc = math.floor(hourVal(desc)) .. "." .. math.floor(hourVal(desc) * 10) % 10 .. "kk/h" + elseif hour >= 1000 or hour <= -1000 then + desc = balance / 1000 + hourDesc = math.floor(hourVal(desc)) .. "." .. math.floor(hourVal(desc) * 10) % 10 .. "k/h" + else + hourDesc = math.floor(hourVal(balance)) .. "gp/h" + end + + return balanceDesc, hourDesc +end + +function reportStats() + local lootWorth, wasteWorth, balance = bottingStats() + local balanceDesc, hourDesc = bottingLabels(lootWorth, wasteWorth, balance) + + local a, b, c + + a = "Session Time: " .. + sessionTime() .. ", Exp Gained: " .. format_thousand(expGained()) .. ", Exp/h: " .. expPerHour() + b = " | Balance: " .. balanceDesc .. " (" .. hourDesc .. ")" + c = a .. b + + return c +end + +function damageHour() + if uptime < 5 * 60 then + return totalDmg + else + return hourVal(totalDmg) + end +end + +function healHour() + if uptime < 5 * 60 then + return totalHeal + else + return hourVal(totalHeal) + end +end + +function wasteHour() + local lootWorth, wasteWorth, balance = bottingStats() + if uptime < 5 * 60 then + return wasteWorth + else + return hourVal(wasteWorth) + end +end + +function lootHour() + local lootWorth, wasteWorth, balance = bottingStats() + if uptime < 5 * 60 then + return lootWorth + else + return hourVal(lootWorth) + end +end + +function getHuntingData() + local lootWorth, wasteWorth, balance = bottingStats() + return totalDmg, totalHeal, lootWorth, wasteWorth, balance +end + +function avgTable(t) + if type(t) ~= 'table' then return 0 end + local val = 0 + + for i, v in pairs(t) do + val = val + v + end + + if #t == 0 then + return 0 + else + return val / #t + end +end + +--bestdps/hps +local bestDPS = 0 +local bestHPS = 0 +--main loop +macro(500, function() + local lootWorth, wasteWorth, balance = bottingStats() + local balanceDesc, hourDesc = bottingLabels(lootWorth, wasteWorth, balance) + + -- hps and dps + local curHPS = valueInSeconds(healTable) + local curDPS = valueInSeconds(dmgTable) + + bestHPS = bestHPS > curHPS and bestHPS or curHPS + bestDPS = bestDPS > curDPS and bestDPS or curDPS + + --hunt window + sessionTimeLabel:setText(sessionTime()) + xpGainLabel:setText(format_thousand(expGained())) + xpHourLabel:setText(expPerHour()) + lootLabel:setText(format_thousand(lootWorth)) + suppliesLabel:setText(format_thousand(wasteWorth)) + balanceLabel:setColor(balance >= 0 and "#45ad25" or "#ff9854") + balanceLabel:setText(balanceDesc .. " (" .. hourDesc .. ")") + damageLabel:setText(format_thousand(totalDmg)) + damageHourLabel:setText(format_thousand(damageHour())) + healingLabel:setText(format_thousand(totalHeal)) + healingHourLabel:setText(format_thousand(healHour())) + + --loot window + lootInLootAnalyzerLabel:setText(format_thousand(lootWorth)) + lootHourInLootAnalyzerLabel:setText(format_thousand(lootHour())) + + + --supply window + suppliesInSuppliesAnalyzerLabel:setText(format_thousand(wasteWorth)) + suppliesHourInSuppliesAnalyzerLabel:setText(format_thousand(wasteHour())) + + --impact window + totalDamageLabel:setText(format_thousand(totalDmg)) + maxDpsLabel:setText(format_thousand(bestDPS)) + bestHitLabel:setText(storage.bestHit) + + top1.left:setText(first.l) + top1.right:setText(first.r) + top2.left:setText(second.l) + top2.right:setText(second.r) + top3.left:setText(third.l) + top3.right:setText(third.r) + top4.left:setText(fourth.l) + top4.right:setText(fourth.r) + top5.left:setText(five.l) + top5.right:setText(five.r) + + totalHealingLabel:setText(format_thousand(totalHeal)) + maxHpsLabel:setText(format_thousand(bestHPS)) + bestHealLabel:setText(storage.bestHeal) + + --xp window + xpGrainInXpLabel:setText(format_thousand(expGained())) + xpHourInXpLabel:setText(expPerHour()) + nextLevelLabel:setText(timeToLevel()) + progressBar:setPercent(modules.game_skills.skillsWindow.contentsPanel.level.percent:getPercent()) + + + --stats + totalRounds:setText(vBot.CaveBotData.rounds) + avRoundTime:setText(niceTimeFormat(avgTable(vBot.CaveBotData.time), true)) + totalRefills:setText(vBot.CaveBotData.refills) + avRefillTime:setText(niceTimeFormat(avgTable(vBot.CaveBotData.refillTime), true)) + lastRefill:setText(niceTimeFormat(os.difftime(os.time() - vBot.CaveBotData.lastRefill), true)) +end) + +--graphs, draw each minute +macro(60 * 1000, function() + drawGraph(xpGraph, expPerHour(true) or 0) + drawGraph(lootGraph, lootHour() or 0) + drawGraph(supplyGraph, wasteHour() or 0) + drawGraph(dmgGraph, valueInSeconds(dmgTable) or 0) + drawGraph(healGraph, valueInSeconds(healTable) or 0) +end) + +--party hunt analyzer +macro(2000, function() + if not BotServer._websocket then return end + + -- send data + if storage.sendPartyAnalyzerData then + sendData() + end + + local totalWaste, totalLoot, totalBalance = getSumStats() + + partySessionTimeLabel:setText(sessionTime()) + partyLootLabel:setText(format_thousand(totalLoot)) + partySuppliesLabel:setText(format_thousand(totalWaste)) + partyBalanceLabel:setText(format_thousand(totalBalance)) + + if totalBalance < 0 then + partyBalanceLabel:setColor('#ff9854') + elseif totalBalance > 0 then + partyBalanceLabel:setColor('#45ad25') + else + partyBalanceLabel:setColor('white') + end +end) + +-- public functions +-- global namespace +Analyzer = {} + +Analyzer.getKillsAmount = function(name) + return killList[name] or 0 +end + +Analyzer.getLootedAmount = function(nameOrId) + if type(nameOrId) == "number" then + return lootedItems[nameOrId].count or 0 + else + local nameOrId = nameOrId:lower() + for k, v in pairs(lootedItems) do + if v.name == nameOrId then + return v.count + end + end + end + return 0 +end + +Analyzer.getTotalProfit = function() + local lootWorth, wasteWorth, balance = bottingStats() + + return lootWorth +end + +Analyzer.getTotalWaste = function() + local lootWorth, wasteWorth, balance = bottingStats() + + return wasteWorth +end + +Analyzer.getBalance = function() + local lootWorth, wasteWorth, balance = bottingStats() + + return balance +end + +Analyzer.getXpGained = function() + return expGained() +end + +Analyzer.getXpHour = function() + return expPerHour() +end + +Analyzer.getTimeToNextLevel = function() + return timeToLevel() +end + +Analyzer.getCaveBotStats = function() + local parents = { suppliesByRound, suppliesByRefill } + local round = {} + local refill = {} + for i = 1, 2 do + local data = parents[i] + for j, child in ipairs(data:getChildren()) do + local id = child:getItemId() + local count = child.count + + if i == 1 then + round[id] = count + else + refill[id] = count + end + end + end + + return { + totalRounds = totalRounds:getText(), + avRoundTime = avRoundTime:getText(), + totalRefills = totalRefills:getText(), + avRefillTime = avRefillTime:getText(), + lastRefill = lastRefill:getText(), + roundSupplies = round, -- { [id] = amount, [id2] = amount ...} + refillSupplies = refill -- { [id] = amount, [id2] = amount ...} + } +end diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.otui new file mode 100644 index 0000000000..feb1c4bb19 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/analyzer.otui @@ -0,0 +1,443 @@ +TrackerItem < Panel + height: 40 + + BotItem + id: item + anchors.top: parent.top + margin-top: 2 + anchors.left: parent.left + image-source: + + UIWidget + id: name + anchors.top: prev.top + margin-top: 1 + anchors.bottom: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + text: Set Item to start track. + text-align:left + font: verdana-11px-rounded + color: #FFFFFF + + UIWidget + id: drops + anchors.top: prev.bottom + margin-top: 3 + anchors.bottom: Item.bottom + anchors.left: prev.left + anchors.right: parent.right + font: verdana-11px-rounded + text-align:left + text: Loot Drops: 0 + color: #CCCCCC + + +DualLabel < Label + height: 15 + text-offset: 4 0 + font: verdana-11px-rounded + text-align: left + width: 50 + + Label + id: value + anchors.right: parent.right + margin-right: 4 + anchors.verticalCenter: parent.verticalCenter + width: 200 + font: verdana-11px-rounded + text-align: right + text: 0 + +MemberWidget < Panel + height: 85 + margin-top: 3 + + UICreature + id: creature + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + size: 28 28 + + UIWidget + id: name + anchors.left: prev.right + margin-left: 5 + anchors.top: parent.top + height: 12 + anchors.right: parent.right + text: Player Name + font: verdana-11px-rounded + text-align: left + + ProgressBar + id: health + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 2 + height: 7 + background-color: #00c000 + phantom: false + + ProgressBar + id: mana + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + height: 7 + background-color: #0000FF + phantom: false + + DualLabel + id: balance + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 5 + text: Balance: + + DualLabel + id: damage + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + text: Damage: + + DualLabel + id: healing + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + text: Healing: + +AnalyzerPriceLabel < Label + background-color: alpha + text-offset: 2 0 + focusable: true + height: 16 + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + width: 15 + height: 15 + +AnalyzerListPanel < Panel + padding-left: 4 + padding-right: 4 + layout: + type: verticalBox + fit-children: true + + +ListLabel < Label + height: 15 + font: verdana-11px-rounded + text-offset: 15 0 + +AnalyzerItemsPanel < Panel + id: List + padding: 2 + layout: + type: grid + cell-size: 33 33 + cell-spacing: 1 + num-columns: 5 + fit-children: true + +AnalyzerLootItem < UIItem + opacity: 0.87 + height: 37 + margin-left: 1 + virtual: true + background-color: alpha + + Label + id: count + font: verdana-11px-rounded + color: white + opacity: 0.87 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-right: 2 + text-align: right + text: 0 + +AnalyzerGraph < UIGraph + height: 140 + capacity: 400 + line-width: 1 + color: red + margin-top: 5 + margin-left: 5 + margin-right: 5 + background-color: #383636 + padding: 5 + font: verdana-11px-rounded + image-source: /images/ui/graph_background + +AnalyzerProgressBar < ProgressBar + background-color: green + height: 5 + margin-top: 3 + phantom: false + margin-left: 3 + margin-right: 3 + border: 1 black + +AnalyzerButton < Button + height: 22 + margin-bottom: 2 + font: verdana-11px-rounded + text-offset: 0 4 + +MainAnalyzerWindow < MiniWindow + id: MainAnalyzerWindow + text: Analytics Selector + height: 266 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 5 + padding-right: 5 + padding-top: 5 + layout: verticalBox + + AnalyzerButton + id: HuntingAnalyzer + text: Hunting Analyzer + + AnalyzerButton + id: LootAnalyzer + text: Loot Analyzer + + AnalyzerButton + id: SupplyAnalyzer + text: Supply Analyzer + + AnalyzerButton + id: ImpactAnalyzer + text: Impact Analyzer + + AnalyzerButton + id: XPAnalyzer + text: XP Analyzer + + AnalyzerButton + id: DropTracker + text: Drop Tracker + + AnalyzerButton + id: Stats + text: CaveBot Stats + color: #74B73E + + AnalyzerButton + id: PartyHunt + text: Party Hunt + color: #3895D3 + + AnalyzerButton + id: Settings + text: Features & Settings + color: #FABD02 + + AnalyzerButton + id: ResetSession + text: Reset Session + color: #FF0000 + +HuntingAnalyzer < MiniWindow + id: HuntingAnalyzerWindow + text: Hunt Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +LootAnalyzer < MiniWindow + id: LootAnalyzerWindow + text: Loot Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +SupplyAnalyzer < MiniWindow + id: SupplyAnalyzerWindow + text: Supply Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +ImpactAnalyzer < MiniWindow + id: ImpactAnalyzerWindow + text: Impact Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +XPAnalyzer < MiniWindow + id: XPAnalyzerWindow + text: XP Analyzer + height: 150 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +PartyAnalyzerWindow < MiniWindow + id: PartyAnalyzerWindow + text: Party Hunt + height: 200 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 3 + padding-right: 3 + padding-top: 1 + layout: verticalBox + +DropTracker < MiniWindow + id: DropTracker + text: Drop Tracker + height: 200 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 3 + padding-right: 3 + padding-top: 1 + layout: verticalBox + +CaveBotStats < MiniWindow + id: CaveBotStats + text: CaveBot Stats + height: 200 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 3 + padding-right: 3 + padding-top: 1 + layout: verticalBox + +FeaturesWindow < MainWindow + id: FeaturesWindow + size: 250 370 + padding: 15 + text: Analyzers Features + @onEscape: self:hide() + + TextList + id: CustomPrices + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 10 + padding: 1 + height: 220 + vertical-scrollbar: CustomPricesScrollBar + + VerticalScrollBar + id: CustomPricesScrollBar + anchors.top: CustomPrices.top + anchors.bottom: CustomPrices.bottom + anchors.right: CustomPrices.right + step: 14 + pixels-scroll: true + + BotItem + id: ID + anchors.left: CustomPrices.left + anchors.top: CustomPrices.bottom + margin-top: 5 + + SpinBox + id: NewPrice + anchors.left: prev.right + margin-left: 5 + anchors.verticalCenter: prev.verticalCenter + width: 100 + minimum: 0 + maximum: 1000000000 + step: 1 + text-align: center + focusable: true + + Button + id: addItem + anchors.left: prev.right + margin-left: 5 + anchors.verticalCenter: prev.verticalCenter + anchors.right: CustomPrices.right + text: Add + font: verdana-11px-rounded + + HorizontalSeparator + anchors.left: ID.right + margin-left: 5 + anchors.right: CustomPrices.right + anchors.verticalCenter: ID.top + + HorizontalSeparator + id: secondSeparator + anchors.left: ID.right + margin-left: 5 + anchors.right: CustomPrices.right + anchors.bottom: ID.bottom + + BotSwitch + id: LootChannel + anchors.left: CustomPrices.left + anchors.right: parent.horizontalCenter + margin-right: 2 + anchors.top: prev.top + margin-top: 20 + text: Loot Channel + font: verdana-11px-rounded + + BotSwitch + id: RarityFrames + anchors.left: parent.horizontalCenter + margin-left: 2 + anchors.right: CustomPrices.right + anchors.top: secondSeparator.top + margin-top: 20 + text: Rarity Frames + font: verdana-11px-rounded + + HorizontalSeparator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/antiRs.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/antiRs.lua new file mode 100644 index 0000000000..02160acc9a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/antiRs.lua @@ -0,0 +1,33 @@ +setDefaultTab("Tools") +g_game.cancelAttackAndFollow() + +local frags = 0 +local unequip = false +local m = macro(50, "AntiRS & Msg", function() end) + +function safeExit() + CaveBot.setOff() + TargetBot.setOff() + g_game.cancelAttackAndFollow() + g_game.cancelAttackAndFollow() + g_game.cancelAttackAndFollow() + modules.game_interface.forceExit() +end + +onTextMessage(function(mode, text) + if not m.isOn() then return end + if not text:find("Warning! The murder of") then return end + frags = frags + 1 + if killsToRs() < 6 or frags > 1 then + EquipManager.setOff() + schedule(100, function() + local id = getLeft() and getLeft():getId() + + if id and not unequip then + unequip = true + g_game.equipItemId(id) + end + safeExit() + end) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/cast_food.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/cast_food.lua new file mode 100644 index 0000000000..187738d830 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/cast_food.lua @@ -0,0 +1,22 @@ +setDefaultTab("HP") +if voc() ~= 1 and voc() ~= 11 then + if storage.foodItems then + local t = {} + for i, v in pairs(storage.foodItems) do + if not table.find(t, v.id) then + table.insert(t, v.id) + end + end + local foodItems = { 3607, 3585, 3592, 3600, 3601 } + for i, item in pairs(foodItems) do + if not table.find(t, item) then + table.insert(storage.foodItems, item) + end + end + end + macro(500, "Cast Food", function() + if player:getRegenerationTime() <= 400 then + cast("exevo pan", 5000) + end + end) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/cavebot.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/cavebot.lua new file mode 100644 index 0000000000..416c6c0166 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/cavebot.lua @@ -0,0 +1,53 @@ +-- Cavebot by otclient@otclient.ovh +-- visit http://bot.otclient.ovh/ + +local cavebotTab = "Cave" +local targetingTab = storage.extras.joinBot and "Cave" or "Target" + +setDefaultTab(cavebotTab) +CaveBot.Extensions = {} +importStyle("/cavebot/cavebot.otui") +importStyle("/cavebot/config.otui") +importStyle("/cavebot/editor.otui") +dofile("/cavebot/actions.lua") +dofile("/cavebot/config.lua") +dofile("/cavebot/editor.lua") +dofile("/cavebot/example_functions.lua") +dofile("/cavebot/recorder.lua") +dofile("/cavebot/walking.lua") +dofile("/cavebot/minimap.lua") +-- in this section you can add extensions, check extension_template.lua +--dofile("/cavebot/extension_template.lua") +dofile("/cavebot/sell_all.lua") +dofile("/cavebot/depositor.lua") +dofile("/cavebot/buy_supplies.lua") +dofile("/cavebot/d_withdraw.lua") +dofile("/cavebot/supply_check.lua") +dofile("/cavebot/travel.lua") +dofile("/cavebot/doors.lua") +dofile("/cavebot/pos_check.lua") +dofile("/cavebot/withdraw.lua") +dofile("/cavebot/inbox_withdraw.lua") +dofile("/cavebot/lure.lua") +dofile("/cavebot/bank.lua") +dofile("/cavebot/clear_tile.lua") +dofile("/cavebot/tasker.lua") +dofile("/cavebot/imbuing.lua") +dofile("/cavebot/stand_lure.lua") +-- main cavebot file, must be last +dofile("/cavebot/cavebot.lua") + +setDefaultTab(targetingTab) +if storage.extras.joinBot then UI.Label("-- [[ TargetBot ]] --") end +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") diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/cavebot_control_panel.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/cavebot_control_panel.lua new file mode 100644 index 0000000000..765a640de0 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/cavebot_control_panel.lua @@ -0,0 +1,63 @@ +setDefaultTab("Cave") + +g_ui.loadUIFromString([[ +CaveBotControlPanel < Panel + margin-top: 5 + layout: + type: verticalBox + fit-children: true + + HorizontalSeparator + + Label + text-align: center + text: CaveBot Control Panel + font: verdana-11px-rounded + margin-top: 3 + + HorizontalSeparator + + Panel + id: buttons + margin-top: 2 + layout: + type: grid + cell-size: 86 20 + cell-spacing: 1 + flow: true + fit-children: true + + HorizontalSeparator + margin-top: 3 +]]) + +local panel = UI.createWidget("CaveBotControlPanel") + +storage.caveBot = { + forceRefill = false, + backStop = false, + backTrainers = false, + backOffline = false +} + +-- [[ B U T T O N S ]] -- + +local forceRefill = UI.Button("Force Refill", function(widget) + storage.caveBot.forceRefill = true + print("[CaveBot] Going back on refill on next supply check.") +end, panel.buttons) + +local backStop = UI.Button("Back & Stop", function(widget) + storage.caveBot.backStop = true + print("[CaveBot] Going back to city on next supply check and turning off CaveBot on depositer action.") +end, panel.buttons) + +local backTrainers = UI.Button("To Trainers", function(widget) + storage.caveBot.backTrainers = true + print("[CaveBot] Going back to city on next supply check and going to label 'toTrainers' on depositer action.") +end, panel.buttons) + +local backOffline = UI.Button("Offline", function(widget) + storage.caveBot.backOffline = true + print("[CaveBot] Going back to city on next supply check and going to label 'toOfflineTraining' on depositer action.") +end, panel.buttons) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/combo.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/combo.lua new file mode 100644 index 0000000000..b97c118497 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/combo.lua @@ -0,0 +1,443 @@ +setDefaultTab("Main") +local panelName = "combobot" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('ComboBot') + + Button + id: combos + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = { + enabled = false, + onSayEnabled = false, + onShootEnabled = false, + onCastEnabled = false, + followLeaderEnabled = false, + attackLeaderTargetEnabled = false, + attackSpellEnabled = false, + attackItemToggle = false, + sayLeader = "", + shootLeader = "", + castLeader = "", + sayPhrase = "", + spell = "", + serverLeader = "", + item = 3155, + attack = "", + follow = "", + commandsEnabled = true, + serverEnabled = false, + serverLeaderTarget = false, + serverTriggers = true + } +end + +local config = storage[panelName] + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) +config.enabled = not config.enabled +widget:setOn(config.enabled) +end + +ui.combos.onClick = function(widget) + comboWindow:show() + comboWindow:raise() + comboWindow:focus() +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + comboWindow = UI.createWindow('ComboWindow', rootWidget) + comboWindow:hide() + + -- bot item + + comboWindow.actions.attackItem:setItemId(config.item) + comboWindow.actions.attackItem.onItemChange = function(widget) + config.item = widget:getItemId() + end + + -- switches + + comboWindow.actions.commandsToggle:setOn(config.commandsEnabled) + comboWindow.actions.commandsToggle.onClick = function(widget) + config.commandsEnabled = not config.commandsEnabled + widget:setOn(config.commandsEnabled) + end + + comboWindow.server.botServerToggle:setOn(config.serverEnabled) + comboWindow.server.botServerToggle.onClick = function(widget) + config.serverEnabled = not config.serverEnabled + widget:setOn(config.serverEnabled) + end + + comboWindow.server.Triggers:setOn(config.serverTriggers) + comboWindow.server.Triggers.onClick = function(widget) + config.serverTriggers = not config.serverTriggers + widget:setOn(config.serverTriggers) + end + + comboWindow.server.targetServerLeaderToggle:setOn(config.serverLeaderTarget) + comboWindow.server.targetServerLeaderToggle.onClick = function(widget) + config.serverLeaderTarget = not config.serverLeaderTarget + widget:setOn(config.serverLeaderTarget) + end + + -- buttons + comboWindow.closeButton.onClick = function(widget) + comboWindow:hide() + end + + -- combo boxes + + comboWindow.actions.followLeader:setOption(config.follow) + comboWindow.actions.followLeader.onOptionChange = function(widget) + config.follow = widget:getCurrentOption().text + end + + comboWindow.actions.attackLeaderTarget:setOption(config.attack) + comboWindow.actions.attackLeaderTarget.onOptionChange = function(widget) + config.attack = widget:getCurrentOption().text + end + + -- checkboxes + comboWindow.trigger.onSayToggle:setChecked(config.onSayEnabled) + comboWindow.trigger.onSayToggle.onClick = function(widget) + config.onSayEnabled = not config.onSayEnabled + widget:setChecked(config.onSayEnabled) + end + + comboWindow.trigger.onShootToggle:setChecked(config.onShootEnabled) + comboWindow.trigger.onShootToggle.onClick = function(widget) + config.onShootEnabled = not config.onShootEnabled + widget:setChecked(config.onShootEnabled) + end + + comboWindow.trigger.onCastToggle:setChecked(config.onCastEnabled) + comboWindow.trigger.onCastToggle.onClick = function(widget) + config.onCastEnabled = not config.onCastEnabled + widget:setChecked(config.onCastEnabled) + end + + comboWindow.actions.followLeaderToggle:setChecked(config.followLeaderEnabled) + comboWindow.actions.followLeaderToggle.onClick = function(widget) + config.followLeaderEnabled = not config.followLeaderEnabled + widget:setChecked(config.followLeaderEnabled) + end + + comboWindow.actions.attackLeaderTargetToggle:setChecked(config.attackLeaderTargetEnabled) + comboWindow.actions.attackLeaderTargetToggle.onClick = function(widget) + config.attackLeaderTargetEnabled = not config.attackLeaderTargetEnabled + widget:setChecked(config.attackLeaderTargetEnabled) + end + + comboWindow.actions.attackSpellToggle:setChecked(config.attackSpellEnabled) + comboWindow.actions.attackSpellToggle.onClick = function(widget) + config.attackSpellEnabled = not config.attackSpellEnabled + widget:setChecked(config.attackSpellEnabled) + end + + comboWindow.actions.attackItemToggle:setChecked(config.attackItemEnabled) + comboWindow.actions.attackItemToggle.onClick = function(widget) + config.attackItemEnabled = not config.attackItemEnabled + widget:setChecked(config.attackItemEnabled) + end + + -- text edits + comboWindow.trigger.onSayLeader:setText(config.sayLeader) + comboWindow.trigger.onSayLeader.onTextChange = function(widget, text) + config.sayLeader = text + end + + comboWindow.trigger.onShootLeader:setText(config.shootLeader) + comboWindow.trigger.onShootLeader.onTextChange = function(widget, text) + config.shootLeader = text + end + + comboWindow.trigger.onCastLeader:setText(config.castLeader) + comboWindow.trigger.onCastLeader.onTextChange = function(widget, text) + config.castLeader = text + end + + comboWindow.trigger.onSayPhrase:setText(config.sayPhrase) + comboWindow.trigger.onSayPhrase.onTextChange = function(widget, text) + config.sayPhrase = text + end + + comboWindow.actions.attackSpell:setText(config.spell) + comboWindow.actions.attackSpell.onTextChange = function(widget, text) + config.spell = text + end + + comboWindow.server.botServerLeader:setText(config.serverLeader) + comboWindow.server.botServerLeader.onTextChange = function(widget, text) + config.serverLeader = text + end +end + +-- bot server +-- [[ join party made by Frosty ]] -- + +local shouldCloseWindow = false +local firstInvitee = true +local isInComboTeam = false +macro(10, function() + if shouldCloseWindow and config.serverEnabled and config.enabled then + local channelsWindow = modules.game_console.channelsWindow + if channelsWindow then + local child = channelsWindow:getChildById("buttonCancel") + if child then + child:onClick() + shouldCloseWindow = false + isInComboTeam = true + end + end + end +end) + +comboWindow.server.partyButton.onClick = function(widget) + if config.serverEnabled and config.enabled then + if config.serverLeader:len() > 0 and storage.BotServerChannel:len() > 0 then + talkPrivate(config.serverLeader, "request invite " .. storage.BotServerChannel) + else + error("Request failed. Lack of data.") + end + end +end + +onTextMessage(function(mode, text) + if config.serverEnabled and config.enabled then + if mode == 20 then + if string.find(text, "invited you to") then + local regex = "[a-zA-Z]*" + local regexData = regexMatch(text, regex) + if regexData[1][1]:lower() == config.serverLeader:lower() then + local leader = getCreatureByName(regexData[1][1]) + if leader then + g_game.partyJoin(leader:getId()) + g_game.requestChannels() + g_game.joinChannel(1) + shouldCloseWindow = true + end + end + end + end + end +end) + +onTalk(function(name, level, mode, text, channelId, pos) + if config.serverEnabled and config.enabled then + if mode == 4 then + if string.find(text, "request invite") then + local access = string.match(text, "%d.*") + if access and access == storage.BotServerChannel then + local minion = getCreatureByName(name) + if minion then + g_game.partyInvite(minion:getId()) + if firstInvitee then + g_game.requestChannels() + g_game.joinChannel(1) + shouldCloseWindow = true + firstInvitee = false + end + end + else + talkPrivate(name, "Incorrect access key!") + end + end + end + end + -- [[ End of Frosty's Code ]] -- + if config.enabled and config.enabled then + if name:lower() == config.sayLeader:lower() and string.find(text, config.sayPhrase) and config.onSayEnabled then + startCombo = true + end + if (config.castLeader and name:lower() == config.castLeader:lower()) and isAttSpell(text) and config.onCastEnabled then + startCombo = true + end + end + if config.enabled and config.commandsEnabled and (config.shootLeader and name:lower() == config.shootLeader:lower()) or (config.sayLeader and name:lower() == config.sayLeader:lower()) or (config.castLeader and name:lower() == config.castLeader:lower()) then + if string.find(text, "ue") then + say(config.spell) + elseif string.find(text, "sd") then + local params = string.split(text, ",") + if #params == 2 then + local target = params[2]:trim() + if getCreatureByName(target) then + useWith(3155, getCreatureByName(target)) + end + end + elseif string.find(text, "att") then + local attParams = string.split(text, ",") + if #attParams == 2 then + local atTarget = attParams[2]:trim() + if getCreatureByName(atTarget) and config.attack == "COMMAND TARGET" then + g_game.attack(getCreatureByName(atTarget)) + end + end + end + end + if isAttSpell(text) and config.enabled and config.serverEnabled then + BotServer.send("trigger", "start") + end +end) + +onMissle(function(missle) + if config.enabled and config.onShootEnabled then + if not config.shootLeader or config.shootLeader:len() == 0 then + return + end + local src = missle:getSource() + if src.z ~= posz() then + return + end + local from = g_map.getTile(src) + local to = g_map.getTile(missle:getDestination()) + if not from or not to then + return + end + local fromCreatures = from:getCreatures() + local toCreatures = to:getCreatures() + if #fromCreatures ~= 1 or #toCreatures ~= 1 then + return + end + local c1 = fromCreatures[1] + local t1 = toCreatures[1] + leaderTarget = t1 + if c1:getName():lower() == config.shootLeader:lower() then + if config.attackItemEnabled and config.item and config.item > 100 and findItem(config.item) then + useWith(config.item, t1) + end + if config.attackSpellEnabled and config.spell:len() > 1 then + say(config.spell) + end + end + end +end) + +macro(10, function() + if not config.enabled or not config.attackLeaderTargetEnabled then return end + if leaderTarget and config.attack == "LEADER TARGET" then + if not getTarget() or (getTarget() and getTarget():getName() ~= leaderTarget:getName()) then + g_game.attack(leaderTarget) + end + end + if config.enabled and config.serverEnabled and config.attack == "SERVER LEADER TARGET" and serverTarget then + if serverTarget and not getTarget() or (getTarget() and getTarget():getname() ~= serverTarget) + then + g_game.attack(serverTarget) + end + end +end) + + +local toFollow +local toFollowPos = {} + +macro(100, function() + toFollow = nil + if not config.enabled or not config.followLeaderEnabled then return end + if leaderTarget and config.follow == "LEADER TARGET" and leaderTarget:isPlayer() then + toFollow = leaderTarget:getName() + elseif config.follow == "SERVER LEADER TARGET" and config.serverLeader:len() ~= 0 then + toFollow = serverTarget + elseif config.follow == "SERVER LEADER" and config.serverLeader:len() ~= 0 then + toFollow = config.serverLeader + elseif config.follow == "LEADER" then + if config.onSayEnabled and config.sayLeader:len() ~= 0 then + toFollow = config.sayLeader + elseif config.onCastEnabled and config.castLeader:len() ~= 0 then + toFollow = config.castLeader + elseif config.onShootEnabled and config.shootLeader:len() ~= 0 then + toFollow = config.shootLeader + end + end + if not toFollow then return end + local target = getCreatureByName(toFollow) + if target then + local tpos = target:getPosition() + toFollowPos[tpos.z] = tpos + end + if player:isWalking() then return end + local p = toFollowPos[posz()] + if not p then return end + if CaveBot.walkTo(p, 20, {ignoreNonPathable=true, precision=1, ignoreStairs=false}) then + delay(100) + end +end) + +onCreaturePositionChange(function(creature, oldPos, newPos) + if creature:getName() == toFollow and newPos then + toFollowPos[newPos.z] = newPos + end +end) + +local timeout = now +macro(10, function() + if config.enabled and startCombo then + if config.attackItemEnabled and config.item and config.item > 100 and findItem(config.item) then + useWith(config.item, getTarget()) + end + if config.attackSpellEnabled and config.spell:len() > 1 then + say(config.spell) + end + startCombo = false + end + -- attack part / server + if BotServer._websocket and config.enabled and config.serverEnabled then + if target() and now - timeout > 500 then + targetPos = target():getName() + BotServer.send("target", targetPos) + timeout = now + end + end +end) + +onUseWith(function(pos, itemId, target, subType) + if BotServer._websocket and itemId == 3155 then + BotServer.send("useWith", target:getPosition()) + end +end) + +if BotServer._websocket and config.enabled and config.serverEnabled then + BotServer.listen("trigger", function(name, message) + if message == "start" and name:lower() ~= player:getName():lower() and name:lower() == config.serverLeader:lower() and config.serverTriggers then + startCombo = true + end + end) + BotServer.listen("target", function(name, message) + if name:lower() ~= player:getName():lower() and name:lower() == config.serverLeader:lower() then + if not target() or target():getName() == getCreatureByName(message) then + if config.serverLeaderTarget then + serverTarget = getCreatureByName(message) + g_game.attack(getCreatureByName(message)) + end + end + end + end) + BotServer.listen("useWith", function(name, message) + local tile = g_map.getTile(message) + if config.serverTriggers and name:lower() ~= player:getName():lower() and name:lower() == config.serverLeader:lower() and config.attackItemEnabled and config.item and findItem(config.item) then + useWith(config.item, tile:getTopUseThing()) + end + end) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/combo.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/combo.otui new file mode 100644 index 0000000000..b89013acc8 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/combo.otui @@ -0,0 +1,391 @@ +AttackComboBoxPopupMenu < ComboBoxPopupMenu +AttackComboBoxPopupMenuButton < ComboBoxPopupMenuButton +AttackComboBox < ComboBox + @onSetup: | + self:addOption("LEADER TARGET") + self:addOption("COMMAND TARGET") + +FollowComboBoxPopupMenu < ComboBoxPopupMenu +FollowComboBoxPopupMenuButton < ComboBoxPopupMenuButton +FollowComboBox < ComboBox + @onSetup: | + self:addOption("LEADER TARGET") + self:addOption("SERVER LEADER TARGET") + self:addOption("LEADER") + self:addOption("SERVER LEADER") + +ComboTrigger < Panel + id: trigger + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 450 72 + + Label + id: triggerLabel1 + anchors.left: parent.left + anchors.top: parent.top + text: On Say + margin-top: 8 + margin-left: 5 + color: #ffaa00 + + Label + id: leaderLabel + anchors.left: triggerLabel1.right + anchors.top: triggerLabel1.top + text: Leader: + margin-left: 35 + + TextEdit + id: onSayLeader + anchors.left: leaderLabel.right + anchors.top: leaderLabel.top + anchors.bottom: leaderLabel.bottom + margin-left: 5 + width: 120 + font: cipsoftFont + + Label + id: phrase + anchors.left: onSayLeader.right + anchors.top: onSayLeader.top + text: Phrase: + margin-left: 5 + + TextEdit + id: onSayPhrase + anchors.left: phrase.right + anchors.top: leaderLabel.top + anchors.bottom: leaderLabel.bottom + margin-left: 5 + width: 120 + font: cipsoftFont + + CheckBox + id: onSayToggle + anchors.left: onSayPhrase.right + anchors.top: onSayPhrase.top + margin-top: 1 + margin-left: 5 + + Label + id: triggerLabel2 + anchors.left: triggerLabel1.left + anchors.top: triggerLabel1.bottom + text: On Shoot + margin-top: 5 + color: #ffaa00 + + Label + id: leaderLabel1 + anchors.left: triggerLabel2.right + anchors.top: triggerLabel2.top + text: Leader: + margin-left: 24 + + TextEdit + id: onShootLeader + anchors.left: leaderLabel1.right + anchors.top: leaderLabel1.top + anchors.bottom: leaderLabel1.bottom + anchors.right: onSayPhrase.right + margin-left: 5 + width: 120 + font: cipsoftFont + + CheckBox + id: onShootToggle + anchors.left: onShootLeader.right + anchors.top: onShootLeader.top + margin-top: 1 + margin-left: 5 + + Label + id: triggerLabel3 + anchors.left: triggerLabel2.left + anchors.top: triggerLabel2.bottom + text: On Cast + margin-top: 5 + color: #ffaa00 + + Label + id: leaderLabel2 + anchors.left: triggerLabel3.right + anchors.top: triggerLabel3.top + text: Leader: + margin-left: 32 + + TextEdit + id: onCastLeader + anchors.left: leaderLabel2.right + anchors.top: leaderLabel2.top + anchors.bottom: leaderLabel2.bottom + anchors.right: onSayPhrase.right + margin-left: 5 + width: 120 + font: cipsoftFont + + CheckBox + id: onCastToggle + anchors.left: onCastLeader.right + anchors.top: onCastLeader.top + margin-top: 1 + margin-left: 5 + +ComboActions < Panel + id: actions + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 220 100 + + Label + id: label1 + anchors.left: parent.left + anchors.top: parent.top + text: Follow: + margin-top: 5 + margin-left: 3 + height: 15 + color: #ffaa00 + + FollowComboBox + id: followLeader + anchors.left: prev.right + anchors.top: prev.top + margin-left: 7 + height: 15 + width: 145 + font: cipsoftFont + + CheckBox + id: followLeaderToggle + anchors.left: followLeader.right + anchors.top: followLeader.top + margin-top: 2 + margin-left: 5 + + Label + id: label2 + anchors.left: label1.left + anchors.top: label1.bottom + margin-top: 5 + text: Attack: + color: #ffaa00 + + AttackComboBox + id: attackLeaderTarget + anchors.left: prev.right + anchors.top: prev.top + margin-left: 5 + height: 15 + width: 145 + font: cipsoftFont + + CheckBox + id: attackLeaderTargetToggle + anchors.left: attackLeaderTarget.right + anchors.top: attackLeaderTarget.top + margin-top: 2 + margin-left: 5 + + Label + id: label3 + anchors.left: label2.left + anchors.top: label2.bottom + margin-top: 5 + text: Spell: + color: #ffaa00 + + TextEdit + id: attackSpell + anchors.left: prev.right + anchors.top: prev.top + anchors.right: attackLeaderTarget.right + margin-left: 17 + height: 15 + width: 145 + font: cipsoftFont + + CheckBox + id: attackSpellToggle + anchors.left: attackSpell.right + anchors.top: attackSpell.top + margin-top: 2 + margin-left: 5 + + Label + id: label4 + anchors.left: label3.left + anchors.top: label3.bottom + margin-top: 15 + text: Attack Item: + color: #ffaa00 + + BotItem + id: attackItem + anchors.left: prev.right + anchors.verticalCenter: prev.verticalCenter + margin-left: 10 + + CheckBox + id: attackItemToggle + anchors.left: prev.right + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 + + BotSwitch + id: commandsToggle + anchors.left: prev.right + anchors.top: attackItem.top + anchors.right: attackSpellToggle.right + anchors.bottom: attackItem.bottom + margin-left: 5 + text: Leader Commands + text-wrap: true + multiline: true + +BotServer < Panel + id: server + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 220 100 + + Label + id: labelX + anchors.left: parent.left + anchors.top: parent.top + text: Leader: + height: 15 + color: #ffaa00 + margin-left: 3 + margin-top: 5 + + TextEdit + id: botServerLeader + anchors.left: prev.right + anchors.top: prev.top + anchors.right: parent.right + margin-right: 3 + margin-left: 9 + height: 15 + font: cipsoftFont + + Button + id: partyButton + anchors.left: labelX.left + anchors.top: botServerLeader.bottom + margin-top: 5 + height: 30 + text: Join Party + text-wrap: true + multiline: true + + BotSwitch + id: botServerToggle + anchors.left: prev.right + anchors.top: botServerLeader.bottom + anchors.right: parent.right + height: 30 + margin-left: 3 + margin-right: 3 + margin-top: 5 + text: Server Enabled + + BotSwitch + id: targetServerLeaderToggle + anchors.left: partyButton.left + anchors.top: partyButton.bottom + anchors.right: partyButton.right + margin-top: 3 + height: 30 + text: Leader Targets + + BotSwitch + id: Triggers + anchors.left: prev.right + anchors.top: partyButton.bottom + anchors.right: parent.right + margin-top: 3 + height: 30 + margin-left: 3 + margin-right: 3 + text: Triggers + +ComboWindow < MainWindow + !text: tr('Combo Options') + size: 500 280 + @onEscape: self:hide() + + ComboTrigger + id: trigger + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + margin-top: 7 + + Label + id: title + anchors.top: parent.top + anchors.left: parent.left + margin-left: 10 + text: Combo Trigger + color: #ff7700 + + ComboActions + id: actions + anchors.top: trigger.bottom + anchors.left: trigger.left + margin-top: 15 + + Label + id: title + anchors.top: parent.top + anchors.left: parent.left + margin-left: 10 + margin-top: 85 + text: Combo Actions + color: #ff7700 + + BotServer + id: server + anchors.top: actions.top + anchors.left: actions.right + margin-left: 10 + + Label + id: title + anchors.top: parent.top + anchors.left: server.left + margin-left: 3 + margin-top: 85 + text: BotServer + color: #ff7700 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 + + Button + id: toolsButton + !text: tr('Help') + font: cipsoftFont + anchors.right: closeButton.left + anchors.top: closeButton.top + margin-right: 10 + size: 45 21 + @onClick: g_platform.openUrl("http://bot.otclient.ovh/books/scripts/page/combobot") \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/configs.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/configs.lua new file mode 100644 index 0000000000..45cd29af73 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/configs.lua @@ -0,0 +1,97 @@ +--[[ + Configs for modules + Based on Kondrah storage method +--]] +local configName = modules.game_bot.contentsPanel.config:getCurrentOption().text + +-- make vBot config dir +if not g_resources.directoryExists("/bot/".. configName .."/vBot_configs/") then + g_resources.makeDir("/bot/".. configName .."/vBot_configs/") +end + +-- make profile dirs +for i=1,10 do + local path = "/bot/".. configName .."/vBot_configs/profile_"..i + if not g_resources.directoryExists(path) then + g_resources.makeDir(path) + end +end + +local profile = g_settings.getNumber('profile') + +HealBotConfig = {} +local healBotFile = "/bot/" .. configName .. "/vBot_configs/profile_".. profile .. "/HealBot.json" +AttackBotConfig = {} +local attackBotFile = "/bot/" .. configName .. "/vBot_configs/profile_".. profile .. "/AttackBot.json" +SuppliesConfig = {} +local suppliesFile = "/bot/" .. configName .. "/vBot_configs/profile_".. profile .. "/Supplies.json" + + +--healbot +if g_resources.fileExists(healBotFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(healBotFile)) + end) + if not status then + return onError("Error while reading config file (" .. healBotFile .. "). To fix this problem you can delete HealBot.json. Details: " .. result) + end + HealBotConfig = result +end + +--attackbot +if g_resources.fileExists(attackBotFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(attackBotFile)) + end) + if not status then + return onError("Error while reading config file (" .. attackBotFile .. "). To fix this problem you can delete HealBot.json. Details: " .. result) + end + AttackBotConfig = result +end + +--supplies +if g_resources.fileExists(suppliesFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(suppliesFile)) + end) + if not status then + return onError("Error while reading config file (" .. suppliesFile .. "). To fix this problem you can delete HealBot.json. Details: " .. result) + end + SuppliesConfig = result +end + +function vBotConfigSave(file) + -- file can be either + --- heal + --- atk + --- supply + local configFile + local configTable + if not file then return end + file = file:lower() + if file == "heal" then + configFile = healBotFile + configTable = HealBotConfig + elseif file == "atk" then + configFile = attackBotFile + configTable = AttackBotConfig + elseif file == "supply" then + configFile = suppliesFile + configTable = SuppliesConfig + else + return + end + + local status, result = pcall(function() + return json.encode(configTable, 2) + end) + if not status then + return onError("Error while saving config. it won't be saved. Details: " .. result) + end + + if result:len() > 100 * 1024 * 1024 then + return onError("config file is too big, above 100MB, it won't be saved") + end + + g_resources.writeFileContents(configFile, result) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.lua new file mode 100644 index 0000000000..568c825907 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.lua @@ -0,0 +1,123 @@ +setDefaultTab("Cave") +local panelName = "specialDeposit" +local depositerPanel + +UI.Button("Stashing Settings", function() + depositerPanel:show() + depositerPanel:raise() + depositerPanel:focus() +end) + +if not storage[panelName] then + storage[panelName] = { + items = {}, + height = 380 + } +end + +local config = storage[panelName] + +depositerPanel = UI.createWindow('DepositerPanel', rootWidget) +depositerPanel:hide() +-- basic one +depositerPanel.CloseButton.onClick = function() + depositerPanel:hide() +end + +depositerPanel:setHeight(config.height or 380) +depositerPanel.onGeometryChange = function(widget, old, new) + if old.height == 0 then return end + config.height = new.height +end + +function arabicToRoman(n) + local t = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XI", "XII", "XIV", "XV", "XVI", "XVII"} + return t[n] +end + +local function refreshEntries() + depositerPanel.DepositerList:destroyChildren() + for _, entry in ipairs(config.items) do + local panel = g_ui.createWidget("StashItem", depositerPanel.DepositerList) + panel.name:setText(Item.create(entry.id):getMarketData().name) + for i, child in ipairs(panel:getChildren()) do + if child:getId() ~= "slot" then + child:setTooltip("Clear item or double click to remove entry.") + child.onDoubleClick = function(widget) + table.remove(config.items, table.find(entry)) + panel:destroy() + end + end + end + panel.item:setItemId(entry.id) + if entry.id > 0 then + panel.item:setImageSource('') + end + panel.item.onItemChange = function(widget) + local id = widget:getItemId() + if id < 100 then + table.remove(config.items, table.find(entry)) + panel:destroy() + else + for i, data in ipairs(config.items) do + if data.id == id then + warn("[Depositer Panel] Item already added!") + return + end + end + entry.id = id + panel.item:setImageSource('') + panel.name:setText(Item.create(entry.id):getMarketData().name) + if entry.index == 0 then + local window = modules.client_textedit.show(panel.slot, { + title = "Set depot for "..panel.name:getText(), + description = "Select depot to which item should be stashed, choose between 3 and 17", + validation = [[^([3-9]|1[0-7])$]] + }) + window.text:setText(entry.index) + schedule(50, function() + window:raise() + window:focus() + end) + end + end + end + if entry.id > 0 then + panel.slot:setText("Stash to depot: ".. entry.index) + end + panel.slot:setTooltip("Click to set stashing destination.") + panel.slot.onClick = function(widget) + local window = modules.client_textedit.show(widget, { + title = "Set depot for "..panel.name:getText(), + description = "Select depot to which item should be stashed, choose between 3 and 17", + validation = [[^([3-9]|1[0-7])$]] + }) + window.text:setText(entry.index) + schedule(50, function() + window:raise() + window:focus() + end) + end + panel.slot.onTextChange = function(widget, text) + local n = tonumber(text) + if n then + entry.index = n + widget:setText("Stash to depot: "..entry.index) + end + end + end +end +refreshEntries() + +depositerPanel.title.onDoubleClick = function(widget) + table.insert(config.items, {id=0, index=0}) + refreshEntries() +end + +function getStashingIndex(id) + for _, v in pairs(config.items) do + if v.id == id then + return v.index - 1 + end + end +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.otui new file mode 100644 index 0000000000..33dea645b0 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/depositer_config.otui @@ -0,0 +1,99 @@ +StashItem < Panel + height: 40 + + BotItem + id: item + anchors.top: parent.top + margin-top: 2 + anchors.left: parent.left + + UIWidget + id: name + anchors.top: prev.top + margin-top: 1 + anchors.bottom: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + text-align:left + text: item name + font: verdana-11px-rounded + color: #FFFFFF + + UIWidget + id: slot + anchors.top: prev.bottom + margin-top: 3 + anchors.bottom: Item.bottom + anchors.left: prev.left + anchors.right: parent.right + font: verdana-11px-rounded + text-align:left + text: Add item to select locker. + color: #CCCCCC + +DepositerPanel < MainWindow + size: 230 380 + !text: tr('Depositer Panel') + @onEscape: self:hide() + + UIWidget + id: title + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text: Double click here to add item. + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + ScrollablePanel + id: DepositerList + image-source: /images/ui/panel_flat + image-border: 1 + anchors.top: prev.bottom + margin-top: 5 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: sep.top + margin-bottom: 10 + padding: 2 + padding-left: 4 + vertical-scrollbar: DepositerScrollBar + layout: + type: verticalBox + + VerticalScrollBar + id: DepositerScrollBar + anchors.top: DepositerList.top + anchors.bottom: DepositerList.bottom + anchors.right: DepositerList.right + step: 14 + pixels-scroll: true + visible: false + + ResizeBorder + id: bottomResizeBorder + anchors.fill: next + height: 3 + minimum: 180 + maximum: 800 + margin-left: 3 + margin-right: 3 + background: #ffffff88 + + HorizontalSeparator + id: sep + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: CloseButton.top + margin-bottom: 8 + + Button + id: CloseButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/depot_withdraw.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/depot_withdraw.lua new file mode 100644 index 0000000000..8323f714ec --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/depot_withdraw.lua @@ -0,0 +1,76 @@ +-- config +setDefaultTab("Tools") +local defaultBp = "shopping bag" +local id = 21411 + +-- script + +local playerContainer = nil +local depotContainer = nil +local mailContainer = nil + +function reopenLootContainer() + for _, container in pairs(getContainers()) do + if container:getName():lower() == defaultBp:lower() then + g_game.close(container) + end + end + + local lootItem = findItem(id) + if lootItem then + schedule(500, function() g_game.open(lootItem) end) + end + +end + +macro(50, "Depot Withdraw", function() + + -- set the containers + if not potionsContainer or not runesContainer or not ammoContainer then + for i, container in pairs(getContainers()) do + if container:getName() == defaultBp then + playerContainer = container + elseif string.find(container:getName(), "Depot") then + depotContainer = container + elseif string.find(container:getName(), "your inbox") then + mailContainer = container + end + end + end + + if playerContainer and #playerContainer:getItems() == 20 then + for j, item in pairs(playerContainer:getItems()) do + if item:getId() == id then + g_game.open(item, playerContainer) + return + end + end + end + + +if playerContainer and freecap() >= 200 then + local time = 500 + if depotContainer then + for i, container in pairs(getContainers()) do + if string.find(container:getName(), "Depot") then + for j, item in pairs(container:getItems()) do + g_game.move(item, playerContainer:getSlotPosition(playerContainer:getItemsCount()), item:getCount()) + return + end + end + end + end + + if mailContainer then + for i, container in pairs(getContainers()) do + if string.find(container:getName(), "your inbox") then + for j, item in pairs(container:getItems()) do + g_game.move(item, playerContainer:getSlotPosition(playerContainer:getItemsCount()), item:getCount()) + return + end + end + end + end +end + +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/eat_food.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/eat_food.lua new file mode 100644 index 0000000000..27e9fea2f2 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/eat_food.lua @@ -0,0 +1,28 @@ +setDefaultTab("HP") + +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(500, "Eat Food", function() + if player:getRegenerationTime() > 400 or 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 +end) +UI.Separator() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/equip.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/equip.lua new file mode 100644 index 0000000000..b0c2f80343 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/equip.lua @@ -0,0 +1,36 @@ +-- config +setDefaultTab("HP") +local scripts = 2 -- if you want more auto equip panels you can change 2 to higher value + +-- script by kondrah, don't edit below unless you know what you are doing +UI.Label("Auto equip") +if type(storage.autoEquip) ~= "table" then + storage.autoEquip = {} +end +for i=1,scripts do + 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 too often + return + end + end + end + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/equipper.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/equipper.otui new file mode 100644 index 0000000000..d61db7e6a6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/equipper.otui @@ -0,0 +1,539 @@ +SlotBotItem < BotItem + border-width: 0 + $on: + image-source: /images/ui/item + $checked: + border-width: 1 + border-color: #FF0000 + +BossLabel < UIWidget + background-color: alpha + text-offset: 3 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('X') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + margin-right: 15 + text-align: center + text-offset: 0 1 + tooltip: Remove profile from the list. + +ConditionBoxPopupMenu < ComboBoxPopupMenu +ConditionBoxPopupMenuButton < ComboBoxPopupMenuButton +ConditionBox < ComboBox + @onSetup: | + self:addOption("-") + self:addOption("and") + self:addOption("or") + +PreButton < PreviousButton + background: #363636 + height: 15 + +NexButton < NextButton + background: #363636 + height: 15 + +CondidionLabel < FlatPanel + padding: 1 + height: 15 + + Label + id: text + anchors.fill: parent + text-align: center + font: verdana-11px-rounded + background: #363636 + +Rule < UIWidget + background-color: alpha + text-offset: 18 2 + focusable: true + height: 16 + text-align: left + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + tooltip: Entry enabled/disabled + + $focus: + background-color: #00000055 + + Button + id: remove + text: X + anchors.right: parent.right + margin-right: 15 + width: 14 + height: 14 + text-align: center + tooltip: Remove entry + anchors.verticalCenter: parent.verticalCenter + + Button + id: visible + text: V + anchors.right: prev.left + margin-right: 3 + width: 14 + height: 14 + text-align: center + tooltip: Items must be visible + anchors.verticalCenter: parent.verticalCenter + + +ConditionPanel < Panel + height: 58 + + NexButton + id: nex + anchors.top: parent.top + margin-top: 5 + anchors.right: parent.right + + PreButton + id: pre + anchors.top: parent.top + margin-top: 5 + anchors.left: parent.left + + CondidionLabel + id: description + anchors.top: parent.top + margin-top: 5 + anchors.left: prev.right + anchors.right: nex.left + margin-left: 3 + margin-right: 3 + + SpinBox + id: spinbox + anchors.top: description.bottom + margin-top: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: 100 + text-align: center + minimum: 0 + maximum: 100 + step: 1 + focusable: true + + BotTextEdit + id: text + anchors.top: description.bottom + margin-top: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: 200 + text-align: center + + + +ListPanel < FlatPanel + size: 270 300 + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Rules List + font: verdana-11px-rounded + color: #FABD02 + + Label + id: mainLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 10 + margin-left: 2 + !text: tr('More important methods come first.') + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + TextList + id: list + anchors.fill: parent + margin-top: 25 + margin-bottom: 18 + vertical-scrollbar: listScrollBar + padding: 2 + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + + Button + id: up + anchors.right: parent.right + anchors.top: list.bottom + size: 60 17 + text: Move Up + text-align: center + font: cipsoftFont + margin-top: 5 + tooltip: Increase priority of selected rule. + + Button + id: down + anchors.right: prev.left + anchors.verticalCenter: prev.verticalCenter + size: 60 17 + margin-right: 5 + text: Move Down + text-align: center + font: cipsoftFont + tooltip: Decrease priority of selected rule. + +InputPanel < FlatPanel + size: 270 300 + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Condition Panel + font: verdana-11px-rounded + color: #FF0000 + + Label + id: mainLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + text: Equip selected items when: + text-align: center + font: verdana-11px-rounded + color: #aeaeae + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 4 + + ConditionPanel + id: condition + anchors.left: parent.left + anchors.right: parent.right + anchors.top: mainLabel.bottom + margin-top: 15 + + HorizontalSeparator + anchors.verticalCenter: next.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + + ConditionBox + id: useSecondCondition + anchors.top: condition.bottom + margin-top: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: 50 + + ConditionPanel + id: optionalCondition + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + + BotButton + id: add + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + margin-bottom: 10 + text: Add Rule + +EQPanel < FlatPanel + size: 160 230 + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Equipment Setup + font: verdana-11px-rounded + color: #03C04A + + SlotBotItem + id: head + image-source: /images/game/slots/head + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 15 + $on: + image-source: /images/ui/item + + SlotBotItem + id: body + image-source: /images/game/slots/body + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: legs + image-source: /images/game/slots/legs + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: feet + image-source: /images/game/slots/feet + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: neck + image-source: /images/game/slots/neck + anchors.top: head.top + margin-top: 13 + anchors.right: head.left + margin-right: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: left-hand + image-source: /images/game/slots/left-hand + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: finger + image-source: /images/game/slots/finger + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + Item + id: back + image-source: /images/game/slots/back-blessed + anchors.top: head.top + margin-top: 13 + anchors.left: head.right + margin-left: 5 + tooltip: Main back container modifications are unavailable. + + SlotBotItem + id: right-hand + image-source: /images/game/slots/right-hand + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: ammo + image-source: /images/game/slots/ammo + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + + BotButton + id: cloneEq + anchors.top: feet.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 15 + text: Clone Current EQ + font: verdana-11px-rounded + tooltip: Copy currently equipped and non-equipped items. + + BotButton + id: default + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + text: Reset fields + font: verdana-11px-rounded + tooltip: Reset all fields to the blank state + +Profile < FlatPanel + size: 160 35 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + margin-left: 10 + text: Profile Name + font: verdana-11px-rounded + + BotTextEdit + id: profileName + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin: 5 + +BossList < FlatPanel + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Boss List + font: verdana-11px-rounded + color: #FABD02 + + TextList + id: list + anchors.fill: parent + margin-top: 10 + margin-bottom: 20 + vertical-scrollbar: listScrollBar + padding: 2 + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + + BotTextEdit + id: name + anchors.left: list.left + anchors.top: list.bottom + margin-top: 4 + anchors.right: next.left + + Button + id: add + anchors.right: list.right + anchors.top: list.bottom + margin-top: 3 + height: 21 + text: Add Boss + text-align: center + font: verdana-11px-rounded + tooltip: Creature with given name will be considered as boss. + +EquipWindow < MainWindow + size: 750 350 + text: Equipment Manager + @onEscape: self:hide() + + ListPanel + id: listPanel + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-left: -2 + visible: false + + BossList + id: bossPanel + anchors.fill: prev + visible: true + + VerticalSeparator + anchors.top: parent.top + anchors.bottom: bottomSep.top + margin-bottom: 5 + anchors.left: prev.right + margin-left: 10 + + Profile + id: profileName + anchors.top: parent.top + anchors.left: prev.right + margin-left: 10 + + EQPanel + id: setup + anchors.left: prev.left + anchors.top: prev.bottom + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-top: 10 + + InputPanel + id: inputPanel + anchors.left: prev.right + anchors.top: parent.top + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-left: 5 + + HorizontalSeparator + id: bottomSep + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + + Button + id: bossList + !text: tr('Boss list') + font: cipsoftFont + anchors.left: parent.left + anchors.bottom: parent.bottom + size: 65 21 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/exeta.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/exeta.lua new file mode 100644 index 0000000000..324bad93b4 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/exeta.lua @@ -0,0 +1,27 @@ +local voc = player:getVocation() +if voc == 1 or voc == 11 then + setDefaultTab("Cave") + UI.Separator() + local m = macro(100000, "Exeta when low hp", function() end) + local lastCast = now + onCreatureHealthPercentChange(function(creature, healthPercent) + if m.isOff() then return end + if healthPercent > 15 then return end + if CaveBot.isOff() or TargetBot.isOff() then return end + if modules.game_cooldown.isGroupCooldownIconActive(3) then return end + if creature:getPosition() and getDistanceBetween(pos(),creature:getPosition()) > 1 then return end + if canCast("exeta res") and now - lastCast > 6000 then + say("exeta res") + lastCast = now + end + end) + + macro(500, "ExetaIfPlayer", function() + if CaveBot.isOff() then return end + if getMonsters(1) >= 1 and getPlayers(6) > 0 then + say("exeta res") + delay(6000) + end + end) + UI.Separator() +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/extras.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/extras.lua new file mode 100644 index 0000000000..1632d1d5a3 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/extras.lua @@ -0,0 +1,663 @@ +setDefaultTab("Main") + +-- securing storage namespace +local panelName = "extras" +if not storage[panelName] then + storage[panelName] = {} +end +local settings = storage[panelName] + +-- basic elements +extrasWindow = UI.createWindow('ExtrasWindow', rootWidget) +extrasWindow:hide() +extrasWindow.closeButton.onClick = function(widget) + extrasWindow:hide() +end + +extrasWindow.onGeometryChange = function(widget, old, new) + if old.height == 0 then return end + + settings.height = new.height +end + +extrasWindow:setHeight(settings.height or 360) + +-- available options for dest param +local rightPanel = extrasWindow.content.right +local leftPanel = extrasWindow.content.left + +-- objects made by Kondrah - taken from creature editor, minor changes to adapt +local addCheckBox = function(id, title, defaultValue, dest, tooltip) + local widget = UI.createWidget('ExtrasCheckBox', dest) + widget.onClick = function() + widget:setOn(not widget:isOn()) + settings[id] = widget:isOn() + if id == "checkPlayer" then + local label = rootWidget.newHealer.targetSettings.vocations.title + if not widget:isOn() then + label:setColor("#d9321f") + label:setTooltip("! WARNING ! \nTurn on check players in extras to use this feature!") + else + label:setColor("#dfdfdf") + label:setTooltip("") + end + end + end + widget:setText(title) + widget:setTooltip(tooltip) + if settings[id] == nil then + widget:setOn(defaultValue) + else + widget:setOn(settings[id]) + end + settings[id] = widget:isOn() +end + +local addItem = function(id, title, defaultItem, dest, tooltip) + local widget = UI.createWidget('ExtrasItem', dest) + widget.text:setText(title) + widget.text:setTooltip(tooltip) + widget.item:setTooltip(tooltip) + widget.item:setItemId(settings[id] or defaultItem) + widget.item.onItemChange = function(widget) + settings[id] = widget:getItemId() + end + settings[id] = settings[id] or defaultItem +end + +local addTextEdit = function(id, title, defaultValue, dest, tooltip) + local widget = UI.createWidget('ExtrasTextEdit', dest) + widget.text:setText(title) + widget.textEdit:setText(settings[id] or defaultValue or "") + widget.text:setTooltip(tooltip) + widget.textEdit.onTextChange = function(widget, text) + settings[id] = text + end + settings[id] = settings[id] or defaultValue or "" +end + +local addScrollBar = function(id, title, min, max, defaultValue, dest, tooltip) + local widget = UI.createWidget('ExtrasScrollBar', dest) + widget.text:setTooltip(tooltip) + widget.scroll.onValueChange = function(scroll, value) + widget.text:setText(title .. ": " .. value) + if value == 0 then + value = 1 + end + settings[id] = value + end + widget.scroll:setRange(min, max) + widget.scroll:setTooltip(tooltip) + if max - min > 1000 then + widget.scroll:setStep(100) + elseif max - min > 100 then + widget.scroll:setStep(10) + end + widget.scroll:setValue(settings[id] or defaultValue) + widget.scroll.onValueChange(widget.scroll, widget.scroll:getValue()) +end + +UI.Button("vBot Settings and Scripts", function() + extrasWindow:show() + extrasWindow:raise() + extrasWindow:focus() +end) +UI.Separator() + +---- to maintain order, add options right after another: +--- add object +--- add variables for function (optional) +--- add callback (optional) +--- optionals should be addionaly sandboxed (if true then end) + +addItem("rope", "Rope Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default rope item.") +addItem("shovel", "Shovel Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default shovel item.") +addItem("machete", "Machete Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default machete item.") +addItem("scythe", "Scythe Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default scythe item.") +addCheckBox("pathfinding", "CaveBot Pathfinding", true, leftPanel, + "Cavebot will automatically search for first reachable waypoint after missing 10 goto's.") +addScrollBar("talkDelay", "Global NPC Talk Delay", 0, 2000, 1000, leftPanel, + "Breaks between each talk action in cavebot (time in miliseconds).") +addScrollBar("looting", "Max Loot Distance", 0, 50, 40, leftPanel, + "Every loot corpse futher than set distance (in sqm) will be ignored and forgotten.") +addScrollBar("huntRoutes", "Hunting Rounds Limit", 0, 300, 50, leftPanel, + "Round limit for supply check, if character already made more rounds than set, on next supply check will return to city.") +addScrollBar("killUnder", "Kill monsters below", 0, 100, 1, leftPanel, + "Force TargetBot to kill added creatures when they are below set percentage of health - will ignore all other TargetBot settings.") +addScrollBar("gotoMaxDistance", "Max GoTo Distance", 0, 127, 30, leftPanel, + "Maximum distance to next goto waypoint for the bot to try to reach.") +addCheckBox("lootLast", "Start loot from last corpse", true, leftPanel, + "Looting sequence will be reverted and bot will start looting newest bodies.") +addCheckBox("joinBot", "Join TargetBot and CaveBot", false, leftPanel, "Cave and Target tabs will be joined into one.") +addCheckBox("reachable", "Target only pathable mobs", false, leftPanel, "Ignore monsters that can't be reached.") + +addCheckBox("title", "Custom Window Title", true, rightPanel, + "Personalize OTCv8 window name according to character specific.") +if true then + local vocText = "" + + if voc() == 1 or voc() == 11 then + vocText = "- EK" + elseif voc() == 2 or voc() == 12 then + vocText = "- RP" + elseif voc() == 3 or voc() == 13 then + vocText = "- MS" + elseif voc() == 4 or voc() == 14 then + vocText = "- ED" + end + + macro(5000, function() + if settings.title then + if hppercent() > 0 then + g_window.setTitle("Tibia - " .. name() .. " - " .. lvl() .. "lvl " .. vocText) + else + g_window.setTitle("Tibia - " .. name() .. " - DEAD") + end + else + g_window.setTitle("Tibia - " .. name()) + end + end) +end + +addCheckBox("separatePm", "Open PM's in new Window", false, rightPanel, + "PM's will be automatically opened in new tab after receiving one.") +if true then + onTalk(function(name, level, mode, text, channelId, pos) + if mode == 4 and settings.separatePm then + local g_console = modules.game_console + local privateTab = g_console.getTab(name) + if privateTab == nil then + privateTab = g_console.addTab(name, true) + g_console.addPrivateText(g_console.applyMessagePrefixies(name, level, text), + g_console.SpeakTypesSettings['private'], name, false, name) + end + return + end + end) +end + +addTextEdit("useAll", "Use All Hotkey", "space", rightPanel, + "Set hotkey for universal actions - rope, shovel, scythe, use, open doors") +if true then + local useId = { 34847, 1764, 21051, 30823, 6264, 5282, 20453, 20454, 20474, 11708, 11705, + 6257, 6256, 2772, 27260, 2773, 1632, 1633, 1948, 435, 6252, 6253, 5007, 4911, + 1629, 1630, 5108, 5107, 5281, 1968, 435, 1948, 5542, 31116, 31120, 30742, 31115, + 31118, 20474, 5737, 5736, 5734, 5733, 31202, 31228, 31199, 31200, 33262, 30824, + 5125, 5126, 5116, 5117, 8257, 8258, 8255, 8256, 5120, 30777, 30776, 23873, 23877, + 5736, 6264, 31262, 31130, 31129, 6250, 6249, 5122, 30049, 7131, 7132, 7727 } + local shovelId = { 606, 593, 867, 608 } + local ropeId = { 17238, 12202, 12935, 386, 421, 21966, 14238 } + local macheteId = { 2130, 3696 } + local scytheId = { 3653 } + + setDefaultTab("Tools") + -- script + if settings.useAll and settings.useAll:len() > 0 then + hotkey(settings.useAll, function() + if not modules.game_console.isEnabledWASD() then return end + for _, tile in pairs(g_map.getTiles(posz())) do + if distanceFromPlayer(tile:getPosition()) < 2 then + for _, item in pairs(tile:getItems()) do + -- use + if table.find(useId, item:getId()) then + use(item) + return + elseif table.find(shovelId, item:getId()) then + useWith(settings.shovel, item) + return + elseif table.find(ropeId, item:getId()) then + useWith(settings.rope, item) + return + elseif table.find(macheteId, item:getId()) then + useWith(settings.machete, item) + return + elseif table.find(scytheId, item:getId()) then + useWith(settings.scythe, item) + return + end + end + end + end + end) + end +end + + +addCheckBox("timers", "MW & WG Timers", true, rightPanel, "Show times for Magic Walls and Wild Growths.") +if true then + local activeTimers = {} + + onAddThing(function(tile, thing) + if not settings.timers then return end + if not thing:isItem() then + return + end + local timer = 0 + if thing:getId() == 2129 then -- mwall id + timer = 20000 -- mwall time + elseif thing:getId() == 2130 then -- wg id + timer = 45000 -- wg time + 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 settings.timers then return end + if not thing:isItem() then + return + end + if (thing:getId() == 2129 or thing:getId() == 2130) and tile:getGround() then + local pos = tile:getPosition().x .. "," .. tile:getPosition().y .. "," .. tile:getPosition().z + activeTimers[pos] = nil + tile:setTimer(0) + end + end) +end + + +addCheckBox("antiKick", "Anti - Kick", true, rightPanel, "Turn every 10 minutes to prevent kick.") +if true then + macro(600 * 1000, function() + if not settings.antiKick then return end + local dir = player:getDirection() + turn((dir + 1) % 4) + schedule(50, function() turn(dir) end) + end) +end + + +addCheckBox("stake", "Skin Monsters", false, leftPanel, "Automatically skin & stake corpses when cavebot is enabled") +if true then + local knifeBodies = { 4286, 4272, 4173, 4011, 4025, 4047, 4052, 4057, 4062, 4112, 4212, 4321, 4324, 4327, 10352, 10356, + 10360, 10364 } + local stakeBodies = { 4097, 4137, 8738, 18958 } + local fishingBodies = { 9582 } + macro(500, function() + if not CaveBot.isOn() or not settings.stake then return end + for i, tile in ipairs(g_map.getTiles(posz())) do + local item = tile:getTopThing() + if item and item:isContainer() then + if table.find(knifeBodies, item:getId()) and findItem(5908) then + CaveBot.delay(550) + useWith(5908, item) + return + end + if table.find(stakeBodies, item:getId()) and findItem(5942) then + CaveBot.delay(550) + useWith(5942, item) + return + end + if table.find(fishingBodies, item:getId()) and findItem(3483) then + CaveBot.delay(550) + useWith(3483, item) + return + end + end + end + end) +end + + +addCheckBox("oberon", "Auto Reply Oberon", true, rightPanel, "Auto reply to Grand Master Oberon talk minigame.") +if true then + onTalk(function(name, level, mode, text, channelId, pos) + if not settings.oberon then return end + if mode == 34 then + if string.find(text, "world will suffer for") then + say("Are you ever going to fight or do you prefer talking?") + elseif string.find(text, "feet when they see me") then + say("Even before they smell your breath?") + elseif string.find(text, "from this plane") then + say("Too bad you barely exist at all!") + elseif string.find(text, "ESDO LO") then + say("SEHWO ASIMO, TOLIDO ESD") + elseif string.find(text, "will soon rule this world") then + say("Excuse me but I still do not get the message!") + elseif string.find(text, "honourable and formidable") then + say("Then why are we fighting alone right now?") + elseif string.find(text, "appear like a worm") then + say("How appropriate, you look like something worms already got the better of!") + elseif string.find(text, "will be the end of mortal") then + say("Then let me show you the concept of mortality before it!") + elseif string.find(text, "virtues of chivalry") then + say("Dare strike up a Minnesang and you will receive your last accolade!") + end + end + end) +end + + +addCheckBox("autoOpenDoors", "Auto Open Doors", true, rightPanel, "Open doors when trying to step on them.") +if true then + local doorsIds = { 5007, 8265, 1629, 1632, 5129, 6252, 6249, 7715, 7712, 7714, + 7719, 6256, 1669, 1672, 5125, 5115, 5124, 17701, 17710, 1642, + 6260, 5107, 4912, 6251, 5291, 1683, 1696, 1692, 5006, 2179, 5116, + 1632, 11705, 30772, 30774, 6248, 5735, 5732, 5120, 23873, 5736, + 6264, 5122, 30049, 30042, 7727 } + + function checkForDoors(pos) + local tile = g_map.getTile(pos) + if tile then + local useThing = tile:getTopUseThing() + if useThing and table.find(doorsIds, useThing:getId()) then + g_game.use(useThing) + end + end + end + + onKeyPress(function(keys) + local wsadWalking = modules.game_console.isEnabledWASD() + if not settings.autoOpenDoors then return end + local pos = player:getPosition() + if keys == 'Up' or (wsadWalking and keys == 'W') then + pos.y = pos.y - 1 + elseif keys == 'Down' or (wsadWalking and keys == 'S') then + pos.y = pos.y + 1 + elseif keys == 'Left' or (wsadWalking and keys == 'A') then + pos.x = pos.x - 1 + elseif keys == 'Right' or (wsadWalking and keys == 'D') then + pos.x = pos.x + 1 + elseif wsadWalking and keys == "Q" then + pos.y = pos.y - 1 + pos.x = pos.x - 1 + elseif wsadWalking and keys == "E" then + pos.y = pos.y - 1 + pos.x = pos.x + 1 + elseif wsadWalking and keys == "Z" then + pos.y = pos.y + 1 + pos.x = pos.x - 1 + elseif wsadWalking and keys == "C" then + pos.y = pos.y + 1 + pos.x = pos.x + 1 + end + checkForDoors(pos) + end) +end + + +addCheckBox("bless", "Buy bless at login", true, rightPanel, "Say !bless at login.") +if true then + local blessed = false + onTextMessage(function(mode, text) + if not settings.bless then return end + + text = text:lower() + + if text == "you already have all blessings." then + blessed = true + end + end) + if settings.bless then + if player:getBlessings() == 0 then + say("!bless") + schedule(2000, function() + if g_game.getClientVersion() > 1000 then + if not blessed and player:getBlessings() == 0 then + warn("!! Blessings not bought !!") + end + end + end) + end + end +end + + +addCheckBox("reUse", "Keep Crosshair", false, rightPanel, "Keep crosshair after using with item") +if true then + local excluded = { 268, 237, 238, 23373, 266, 236, 239, 7643, 23375, 7642, 23374, 5908, 5942 } + + onUseWith(function(pos, itemId, target, subType) + if settings.reUse and not table.find(excluded, itemId) then + schedule(50, function() + item = findItem(itemId) + if item then + modules.game_interface.startUseWith(item) + end + end) + end + end) +end + + +addCheckBox("suppliesControl", "TargetBot off if low supply", false, leftPanel, + "Turn off TargetBot if either one of supply amount is below 50% of minimum.") +if true then + macro(500, function() + if not settings.suppliesControl then return end + if TargetBot.isOff() then return end + if CaveBot.isOff() then return end + if type(hasSupplies()) == 'table' then + TargetBot.setOff() + end + end) +end + +addCheckBox("holdMwall", "Hold MW/WG", true, rightPanel, + "Mark tiles with below hotkeys to automatically use Magic Wall or Wild Growth") +addTextEdit("holdMwHot", "Magic Wall Hotkey: ", "F5", rightPanel) +addTextEdit("holdWgHot", "Wild Growth Hotkey: ", "F6", rightPanel) +if true then + local hold = 0 + local mwHot + local wgHot + + local candidates = {} + local m = macro(20, function() + mwHot = settings.holdMwHot + wgHot = settings.holdWgHot + + if not settings.holdMwall then return end + if #candidates == 0 then return end + + for i, pos in pairs(candidates) do + local tile = g_map.getTile(pos) + if tile then + if tile:getText():len() == 0 then + table.remove(candidates, i) + end + local rune = tile:getText() == "HOLD MW" and 3180 or tile:getText() == "HOLD WG" and 3156 + if tile:canShoot() and not isInPz() and tile:isWalkable() and tile:getTopUseThing():getId() ~= 2130 then + if math.abs(player:getPosition().x - tile:getPosition().x) < 8 and math.abs(player:getPosition().y - tile:getPosition().y) < 6 then + return useWith(rune, tile:getTopUseThing()) + end + end + end + end + end) + + onRemoveThing(function(tile, thing) + if not settings.holdMwall then return end + if thing:getId() ~= 2129 then return end + if tile:getText():find("HOLD") then + table.insert(candidates, tile:getPosition()) + local rune = tile:getText() == "HOLD MW" and 3180 or tile:getText() == "HOLD WG" and 3156 + if math.abs(player:getPosition().x - tile:getPosition().x) < 8 and math.abs(player:getPosition().y - tile:getPosition().y) < 6 then + return useWith(rune, tile:getTopUseThing()) + end + end + end) + + onAddThing(function(tile, thing) + if not settings.holdMwall then return end + if m.isOff() then return end + if thing:getId() ~= 2129 then return end + if tile:getText():len() > 0 then + table.remove(candidates, table.find(candidates, tile)) + end + end) + + onKeyDown(function(keys) + local wsadWalking = modules.game_console.isEnabledWASD() + if not wsadWalking then return end + if not settings.holdMwall then return end + if m.isOff() then return end + if keys ~= mwHot and keys ~= wgHot then return end + hold = now + + local tile = getTileUnderCursor() + if not tile then return end + + if tile:getText():len() > 0 then + tile:setText("") + else + if keys == mwHot then + tile:setText("HOLD MW") + else + tile:setText("HOLD WG") + end + table.insert(candidates, tile:getPosition()) + end + end) + + onKeyPress(function(keys) + local wsadWalking = modules.game_console.isEnabledWASD() + if not wsadWalking then return end + if not settings.holdMwall then return end + if m.isOff() then return end + if keys ~= mwHot and keys ~= wgHot then return end + + if (hold - now) < -1000 then + candidates = {} + for i, tile in ipairs(g_map.getTiles(posz())) do + local text = tile:getText() + if text:find("HOLD") then + tile:setText("") + end + end + end + end) +end + +addCheckBox("checkPlayer", "Check Players", true, rightPanel, + "Auto look on players and mark level and vocation on character model") +if true then + local found + local function checkPlayers() + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and spec:getText() == "" and spec:getPosition().z == posz() and spec ~= player then + g_game.look(spec) + found = now + end + end + end + if settings.checkPlayer then + schedule(500, function() + checkPlayers() + end) + end + + onPlayerPositionChange(function(x, y) + if not settings.checkPlayer then return end + if x.z ~= y.z then + schedule(20, function() checkPlayers() end) + end + end) + + onCreatureAppear(function(creature) + if not settings.checkPlayer then return end + if creature:isPlayer() and creature:getText() == "" and creature:getPosition().z == posz() and creature ~= player then + g_game.look(creature) + found = now + end + end) + + local regex = [[You see ([^\(]*) \(Level ([0-9]*)\)((?:.)* of the ([\w ]*),|)]] + onTextMessage(function(mode, text) + if not settings.checkPlayer then return end + + local re = regexMatch(text, regex) + if #re ~= 0 then + local name = re[1][2] + local level = re[1][3] + local guild = re[1][5] or "" + + if guild:len() > 10 then + guild = guild:sub(1, 10) -- change to proper (last) values + guild = guild .. "..." + end + local voc + if text:lower():find("sorcerer") then + voc = "MS" + elseif text:lower():find("druid") then + voc = "ED" + elseif text:lower():find("knight") then + voc = "EK" + elseif text:lower():find("paladin") then + voc = "RP" + end + local creature = getCreatureByName(name) + if creature then + creature:setText("\n" .. level .. voc .. "\n" .. guild) + end + if found and now - found < 500 then + modules.game_textmessage.clearMessages() + end + end + end) +end + +addCheckBox("nextBackpack", "Open Next Loot Container", true, leftPanel, + "Auto open next loot container if full - has to have the same ID.") +local function openNextLootContainer() + if not settings.nextBackpack then return end + local containers = getContainers() + local lootCotaniersIds = CaveBot.GetLootContainers() + + for i, container in ipairs(containers) do + local cId = container:getContainerItem():getId() + if containerIsFull(container) then + if table.find(lootCotaniersIds, cId) then + for _, item in ipairs(container:getItems()) do + if item:getId() == cId then + return g_game.open(item, container) + end + end + end + end + end +end +if true then + onContainerOpen(function(container, previousContainer) + schedule(100, function() + openNextLootContainer() + end) + end) + + onAddItem(function(container, slot, item, oldItem) + schedule(100, function() + openNextLootContainer() + end) + end) +end + +addCheckBox("highlightTarget", "Highlight Current Target", true, rightPanel, + "Additionaly hightlight current target with red glow") +if true then + local function forceMarked(creature) + if target() == creature then + creature:setMarked("red") + return schedule(333, function() forceMarked(creature) end) + end + end + + onAttackingCreatureChange(function(newCreature, oldCreature) + if not settings.highlightTarget then return end + if oldCreature then + oldCreature:setMarked('') + end + if newCreature then + forceMarked(newCreature) + end + end) +end diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/extras.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/extras.otui new file mode 100644 index 0000000000..de551d9a5d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/extras.otui @@ -0,0 +1,158 @@ +ExtrasScrollBar < Panel + height: 28 + margin-top: 3 + + UIWidget + 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: 3 + minimum: 0 + maximum: 10 + step: 1 + +ExtrasTextEdit < Panel + height: 40 + margin-top: 7 + + UIWidget + 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 + text-align: center + +ExtrasItem < Panel + height: 34 + margin-top: 7 + margin-left: 25 + margin-right: 25 + + UIWidget + id: text + anchors.left: parent.left + anchors.verticalCenter: next.verticalCenter + + BotItem + id: item + anchors.top: parent.top + anchors.right: parent.right + + +ExtrasCheckBox < BotSwitch + height: 20 + margin-top: 7 + +ExtrasWindow < MainWindow + !text: tr('Extras') + size: 440 360 + padding: 25 + + Label + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: parent.top + text-align: center + text: < CaveBot > + + Label + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: parent.top + text-align: center + text: < Miscellaneous > + + VerticalScrollBar + id: contentScroll + anchors.top: prev.bottom + margin-top: 3 + anchors.right: parent.right + anchors.bottom: separator.top + step: 28 + pixels-scroll: true + margin-right: -10 + margin-top: 5 + margin-bottom: 5 + + ScrollablePanel + id: content + anchors.top: prev.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: separator.top + vertical-scrollbar: contentScroll + margin-bottom: 10 + + Panel + id: left + anchors.top: parent.top + 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: parent.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + margin-top: 5 + margin-left: 10 + margin-right: 10 + layout: + type: verticalBox + fit-children: true + + VerticalSeparator + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.horizontalCenter + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + ResizeBorder + id: bottomResizeBorder + anchors.fill: separator + height: 3 + minimum: 260 + maximum: 600 + margin-left: 3 + margin-right: 3 + background: #ffffff88 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/hold_target.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/hold_target.lua new file mode 100644 index 0000000000..484bf85cbd --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/hold_target.lua @@ -0,0 +1,30 @@ +setDefaultTab("Tools") + +local targetID = nil + +-- escape when attacking will reset hold target +onKeyPress(function(keys) + if keys == "Escape" and targetID then + targetID = nil + end +end) + +macro(100, "Hold Target", function() + -- if attacking then save it as target, but check pos z in case of marking by mistake on other floor + if target() and target():getPosition().z == posz() and not target():isNpc() then + targetID = target():getId() + elseif not target() then + -- there is no saved data, do nothing + if not targetID then return end + + -- look for target + for i, spec in ipairs(getSpectators()) do + local sameFloor = spec:getPosition().z == posz() + local oldTarget = spec:getId() == targetID + + if sameFloor and oldTarget then + attack(spec) + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/ingame_editor.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/ingame_editor.lua new file mode 100644 index 0000000000..1217b82b1a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/ingame_editor.lua @@ -0,0 +1,23 @@ +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 script editor", function(newText) + UI.MultilineEditorWindow(storage.ingame_hotkeys or "", {title="Hotkeys editor", description="You can add your custom scrupts here"}, function(text) + storage.ingame_hotkeys = text + reload() + end) + end) + + UI.Separator() + + for _, scripts in pairs({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() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/items.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/items.lua new file mode 100644 index 0000000000..312aa37b0d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/items.lua @@ -0,0 +1,1404 @@ +LootItems = { + ["gold coin"] = 1, + ["platinum coin"] = 100, + ["crystal coin"] = 10000, + ["abyss hammer"] = 20000, + ["acorn"] = 10, + ["albino plate"] = 1500, + ["alloy legs"] = 11000, + ["alptramun's toothbrush"] = 270000, + ["amber"] = 20000, + ["amber staff"] = 8000, + ["amber with a bug"] = 41000, + ["amber with a dragonfly"] = 56000, + ["ancient amulet"] = 200, + ["ancient belt buckle"] = 260, + ["ancient coin"] = 350, + ["ancient liche bone"] = 28000, + ["ancient shield"] = 900, + ["ancient stone"] = 200, + ["angel figurine"] = 36000, + ["angelic axe"] = 5000, + ["ankh"] = 100, + ["antlers"] = 50, + ["ape fur"] = 120, + ["apron"] = 1300, + ["arbalest"] = 42000, + ["arcane staff"] = 42000, + ["assassin dagger"] = 20000, + ["axe"] = 7, + ["axe ring"] = 100, + ["baby seal doll"] = 20000, + ["badger boots"] = 7500, + ["badger fur"] = 15, + ["bamboo stick"] = 30, + ["banana sash"] = 55, + ["banana staff"] = 1000, + ["bandana"] = 150, + ["bar of gold"] = 10000, + ["basalt fetish"] = 210, + ["basalt figurine"] = 160, + ["bast skirt"] = 750, + ["bat decoration"] = 2000, + ["bat wing"] = 50, + ["battle axe"] = 80, + ["battle hammer"] = 120, + ["battle shield"] = 95, + ["battle stone"] = 290, + ["batwing hat"] = 8000, + ["bear paw"] = 100, + ["beast's nightmare-cushion"] = 630000, + ["beastslayer axe"] = 1500, + ["bed of nails"] = 500, + ["beer tap"] = 50, + ["beetle carapace"] = 200, + ["beetle necklace"] = 1500, + ["behemoth claw"] = 2000, + ["behemoth trophy"] = 20000, + ["bejeweled ship's telescope"] = 20000, + ["berserk potion"] = 500, + ["berserker"] = 40000, + ["black hood"] = 190, + ["black pearl"] = 280, + ["black shield"] = 800, + ["black wool"] = 300, + ["blacksteel sword"] = 6000, + ["blade of corruption"] = 60000, + ["blazing bone"] = 610, + ["blessed sceptre"] = 40000, + ["blood preservation"] = 320, + ["blood tincture in a vial"] = 360, + ["bloody dwarven beard"] = 110, + ["bloody edge"] = 30000, + ["bloody pincers"] = 100, + ["bloody tears"] = 70000, + ["blue crystal shard"] = 1500, + ["blue crystal splinter"] = 400, + ["blue gem"] = 5000, + ["blue glass plate"] = 60, + ["blue goanna scale"] = 230, + ["blue legs"] = 15000, + ["blue piece of cloth"] = 200, + ["blue robe"] = 10000, + ["blue rose"] = 250, + ["boggy dreads"] = 200, + ["bola"] = 35, + ["bone club"] = 5, + ["bone fetish"] = 150, + ["bone shield"] = 80, + ["bone shoulderplate"] = 150, + ["bone sword"] = 20, + ["bone toothpick"] = 150, + ["bonebeast trophy3"] = 6000, + ["bonebreaker"] = 10000, + ["bonecarving knife"] = 190, + ["bonelord eye"] = 80, + ["bonelord helmet"] = 2200, + ["bonelord shield"] = 1200, + ["bones of zorvorax"] = 10000, + ["bony tail"] = 210, + ["book of necromantic rituals"] = 180, + ["book of prayers"] = 120, + ["book page"] = 640, + ["boots of haste"] = 30000, + ["bow"] = 100, + ["bowl of terror sweat"] = 500, + ["brain head's giant neuron"] = 100000, + ["brain head's left hemisphere"] = 90000, + ["brain head's right hemisphere"] = 50000, + ["brass armor"] = 150, + ["brass helmet"] = 30, + ["brass legs"] = 49, + ["brass shield"] = 25, + ["bright bell"] = 220, + ["bright sword"] = 6000, + ["brimstone fangs"] = 380, + ["brimstone shell"] = 210, + ["broadsword"] = 500, + ["broken crossbow"] = 30, + ["broken draken mail"] = 340, + ["broken gladiator shield"] = 190, + ["broken halberd"] = 100, + ["broken helmet"] = 20, + ["broken key ring"] = 8000, + ["broken longbow"] = 120, + ["broken ring of ending"] = 4000, + ["broken shamanic staff"] = 35, + ["broken slicer"] = 120, + ["broken throwing axe"] = 230, + ["broken visor"] = 1900, + ["bronze amulet"] = 50, + ["brooch of embracement"] = 14000, + ["brown crystal splinter"] = 400, + ["brown piece of cloth"] = 100, + ["brutetamer's staff"] = 1500, + ["buckle"] = 7000, + ["bullseye potion"] = 500, + ["bunch of ripe rice"] = 75, + ["bunch of troll hair"] = 30, + ["bundle of cursed straw"] = 800, + ["butcher's axe"] = 18000, + ["calopteryx cape"] = 15000, + ["capricious heart"] = 2100, + ["capricious robe"] = 1200, + ["carapace shield"] = 32000, + ["carlin sword"] = 118, + ["carniphila seeds"] = 50, + ["carrion worm fang"] = 35, + ["castle shield"] = 5000, + ["cat's paw"] = 2000, + ["cave devourer eyes"] = 550, + ["cave devourer legs"] = 350, + ["cave devourer maw"] = 600, + ["cavebear skull"] = 550, + ["centipede leg"] = 28, + ["ceremonial ankh"] = 20000, + ["chain armor"] = 70, + ["chain bolter"] = 40000, + ["chain helmet"] = 17, + ["chain legs"] = 25, + ["chaos mace"] = 9000, + ["charmer's tiara"] = 900, + ["chasm spawn abdomen"] = 240, + ["chasm spawn head"] = 850, + ["chasm spawn tail"] = 120, + ["cheese cutter"] = 50, + ["cheesy figurine"] = 150, + ["chicken feather"] = 30, + ["chitinous mouth"] = 10000, + ["claw of 'the noxious spawn'"] = 15000, + ["cliff strider claw"] = 800, + ["closed trap"] = 75, + ["club"] = 1, + ["club ring"] = 100, + ["coal"] = 20, + ["coat"] = 1, + ["cobra crest"] = 650, + ["cobra crown"] = 50000, + ["cobra tongue"] = 15, + ["coconut shoes"] = 500, + ["collar of blue plasma"] = 6000, + ["collar of green plasma"] = 6000, + ["collar of red plasma"] = 6000, + ["colourful feather"] = 110, + ["colourful feathers"] = 400, + ["colourful snail shell"] = 250, + ["compass"] = 45, + ["composite hornbow"] = 25000, + ["compound eye"] = 150, + ["condensed energy"] = 260, + ["copper shield"] = 50, + ["coral brooch"] = 750, + ["corrupted flag"] = 700, + ["countess sorrow's frozen tear"] = 50000, + ["cow bell"] = 120, + ["cowtana"] = 2500, + ["crab pincers"] = 35, + ["cracked alabaster vase"] = 180, + ["cranial basher"] = 30000, + ["crawler head plating"] = 210, + ["crawler's essence"] = 3700, + ["crest of the deep seas"] = 10000, + ["crocodile boots"] = 1000, + ["crossbow"] = 120, + ["crowbar"] = 50, + ["crown"] = 2700, + ["crown armor"] = 12000, + ["crown helmet"] = 2500, + ["crown legs"] = 12000, + ["crown shield"] = 8000, + ["cruelty's chest"] = 720000, + ["cruelty's claw"] = 640000, + ["crunor idol"] = 30000, + ["crusader helmet"] = 6000, + ["crystal bone"] = 250, + ["crystal crossbow"] = 35000, + ["crystal mace"] = 12000, + ["crystal necklace"] = 400, + ["crystal of balance"] = 1000, + ["crystal of focus"] = 2000, + ["crystal of power"] = 3000, + ["crystal pedestal"] = 500, + ["crystal ring"] = 250, + ["crystal sword"] = 600, + ["crystal wand"] = 10000, + ["crystalline armor"] = 16000, + ["crystalline spikes"] = 440, + ["crystallized anger"] = 400, + ["cultish mask"] = 280, + ["cultish robe"] = 150, + ["cultish symbol"] = 500, + ["curious matter"] = 430, + ["cursed bone"] = 6000, + ["cursed shoulder spikes"] = 320, + ["cyan crystal fragment"] = 800, + ["cyclops toe"] = 55, + ["cyclops trophy"] = 500, + ["dagger"] = 2, + ["damaged armor plates"] = 280, + ["damaged worm head"] = 8000, + ["damselfly eye"] = 25, + ["damselfly wing"] = 20, + ["dandelion seeds"] = 200, + ["dangerous proto matter"] = 300, + ["daramian mace"] = 110, + ["daramian waraxe"] = 1000, + ["dark armor"] = 400, + ["dark bell"] = 250, + ["dark helmet"] = 250, + ["dark mushroom"] = 100, + ["dark rosary"] = 48, + ["dark shield"] = 400, + ["dead rat"] = 1, + ["dead weight"] = 450, + ["death ring"] = 1000, + ["deepling axe"] = 40000, + ["deepling breaktime snack"] = 90, + ["deepling claw"] = 430, + ["deepling guard belt buckle"] = 230, + ["deepling ridge"] = 360, + ["deepling scales"] = 80, + ["deepling squelcher"] = 7000, + ["deepling staff"] = 4000, + ["deepling warts"] = 180, + ["deeptags"] = 290, + ["deepworm jaws"] = 500, + ["deepworm spike roots"] = 650, + ["deepworm spikes"] = 800, + ["deer trophy3"] = 3000, + ["demon dust"] = 300, + ["demon helmet"] = 40000, + ["demon horn"] = 1000, + ["demon shield"] = 30000, + ["demon trophy"] = 40000, + ["demonbone amulet"] = 32000, + ["demonic essence"] = 1000, + ["demonic finger"] = 1000, + ["demonic skeletal hand"] = 80, + ["demonrage sword"] = 36000, + ["depth calcei"] = 25000, + ["depth galea"] = 35000, + ["depth lorica"] = 30000, + ["depth ocrea"] = 16000, + ["depth scutum"] = 36000, + ["devil helmet"] = 1000, + ["diabolic skull"] = 19000, + ["diamond"] = 15000, + ["diamond sceptre"] = 3000, + ["diremaw brainpan"] = 350, + ["diremaw legs"] = 270, + ["dirty turban"] = 120, + ["disgusting trophy"] = 3000, + ["distorted heart"] = 2100, + ["distorted robe"] = 1200, + ["divine plate"] = 55000, + ["djinn blade"] = 15000, + ["doll"] = 200, + ["double axe"] = 260, + ["doublet"] = 3, + ["downy feather"] = 20, + ["dowser"] = 35, + ["drachaku"] = 10000, + ["dracola's eye"] = 50000, + ["dracoyle statue"] = 5000, + ["dragon blood"] = 700, + ["dragon claw"] = 8000, + ["dragon figurine"] = 45000, + ["dragon hammer"] = 2000, + ["dragon lance"] = 9000, + ["dragon lord trophy"] = 10000, + ["dragon necklace"] = 100, + ["dragon priest's wandtip"] = 175, + ["dragon robe"] = 50000, + ["dragon scale mail"] = 40000, + ["dragon shield"] = 4000, + ["dragon slayer"] = 15000, + ["dragon tongue"] = 550, + ["dragonbone staff"] = 3000, + ["dragon's tail"] = 100, + ["draken boots"] = 40000, + ["draken sulphur"] = 550, + ["draken trophy"] = 15000, + ["draken wristbands"] = 430, + ["drakinata"] = 10000, + ["draptor scales"] = 800, + ["dreaded cleaver"] = 10000, + ["dream essence egg"] = 205, + ["dung ball"] = 130, + ["dwarven armor"] = 30000, + ["dwarven axe"] = 1500, + ["dwarven legs"] = 40000, + ["dwarven ring"] = 100, + ["dwarven shield"] = 100, + ["earflap"] = 40, + ["earth blacksteel sword"] = 6000, + ["earth cranial basher"] = 30000, + ["earth crystal mace"] = 12000, + ["earth dragon slayer"] = 15000, + ["earth headchopper"] = 6000, + ["earth heroic axe"] = 30000, + ["earth knight axe"] = 2000, + ["earth mystic blade"] = 30000, + ["earth orcish maul"] = 6000, + ["earth relic sword"] = 25000, + ["earth spike sword"] = 1000, + ["earth war axe"] = 12000, + ["earth war hammer"] = 1200, + ["ectoplasmic sushi"] = 300, + ["egg of the many"] = 15000, + ["elder bonelord tentacle"] = 150, + ["elite draken mail"] = 50000, + ["elven amulet"] = 100, + ["elven astral observer"] = 90, + ["elven hoof"] = 115, + ["elven scouting glass"] = 50, + ["elvish bow"] = 2000, + ["elvish talisman"] = 45, + ["emerald bangle"] = 800, + ["empty honey glass"] = 270, + ["empty potion flask"] = 5, + ["enchanted chicken wing"] = 20000, + ["energy ball"] = 300, + ["energy blacksteel sword"] = 6000, + ["energy cranial basher"] = 30000, + ["energy crystal mace"] = 12000, + ["energy dragon slayer"] = 15000, + ["energy headchopper"] = 6000, + ["energy heroic axe"] = 30000, + ["energy knight axe"] = 2000, + ["energy mystic blade"] = 30000, + ["energy orcish maul"] = 6000, + ["energy relic sword"] = 25000, + ["energy ring"] = 100, + ["energy spike sword"] = 1000, + ["energy vein"] = 270, + ["energy war axe"] = 12000, + ["energy war hammer"] = 1200, + ["ensouled essence"] = 820, + ["epee"] = 8000, + ["essence of a bad dream"] = 360, + ["ethno coat"] = 200, + ["execowtioner axe"] = 12000, + ["executioner"] = 55000, + ["explorer brooch"] = 50, + ["eye of a deepling"] = 150, + ["eye of a weeper"] = 650, + ["eye of corruption"] = 390, + ["fafnar symbol"] = 950, + ["fairy wings"] = 200, + ["falcon crest"] = 650, + ["feather headdress"] = 850, + ["fern"] = 20, + ["fiery blacksteel sword"] = 6000, + ["fiery cranial basher"] = 30000, + ["fiery crystal mace"] = 12000, + ["fiery dragon slayer"] = 15000, + ["fiery headchopper"] = 6000, + ["fiery heart"] = 375, + ["fiery heroic axe"] = 30000, + ["fiery knight axe"] = 2000, + ["fiery mystic blade"] = 30000, + ["fiery orcish maul"] = 6000, + ["fiery relic sword"] = 25000, + ["fiery spike sword"] = 1000, + ["fiery war axe"] = 12000, + ["fiery war hammer"] = 1200, + ["fig leaf"] = 200, + ["figurine of cruelty"] = 3100000, + ["figurine of greed"] = 2900000, + ["figurine of hatred"] = 2700000, + ["figurine of malice"] = 2800000, + ["figurine of megalomania"] = 5000000, + ["figurine of spite"] = 3000000, + ["fir cone"] = 25, + ["fire axe"] = 8000, + ["fire mushroom"] = 200, + ["fire sword"] = 1000, + ["fish fin"] = 150, + ["fishing rod"] = 40, + ["flask of embalming fluid"] = 30, + ["flask of warrior's sweat"] = 10000, + ["flintstone"] = 800, + ["flower dress"] = 1000, + ["flower wreath"] = 500, + ["focus cape"] = 6000, + ["fox paw"] = 100, + ["frazzle skin"] = 400, + ["frazzle tongue"] = 700, + ["frost giant pelt"] = 160, + ["frosty ear of a troll"] = 30, + ["frosty heart"] = 280, + ["frozen lightning"] = 270, + ["frozen starlight"] = 20000, + ["fur armor"] = 5000, + ["fur boots"] = 2000, + ["fur shred"] = 200, + ["furry club"] = 1000, + ["garlic necklace"] = 50, + ["gauze bandage"] = 90, + ["gear crystal"] = 200, + ["gear wheel"] = 500, + ["gearwheel chain"] = 5000, + ["gemmed figurine"] = 3500, + ["geomancer's robe"] = 80, + ["geomancer's staff"] = 120, + ["ghastly dragon head"] = 700, + ["ghostly tissue"] = 90, + ["ghoul snack"] = 60, + ["giant amethyst"] = 60000, + ["giant crab pincer"] = 950, + ["giant emerald"] = 90000, + ["giant eye"] = 380, + ["giant ruby"] = 70000, + ["giant sapphire"] = 50000, + ["giant shimmering pearl"] = 3000, + ["giant smithhammer"] = 250, + ["giant sword"] = 17000, + ["giant tentacle"] = 10000, + ["giant topaz"] = 80000, + ["girlish hair decoration"] = 30, + ["glacial rod"] = 6500, + ["glacier amulet"] = 1500, + ["glacier kilt"] = 11000, + ["glacier mask"] = 2500, + ["glacier robe"] = 11000, + ["glacier shoes"] = 2500, + ["gland"] = 500, + ["glistening bone"] = 250, + ["glob of acid slime"] = 25, + ["glob of mercury"] = 20, + ["glob of tar"] = 30, + ["gloom wolf fur"] = 70, + ["glooth amulet"] = 2000, + ["glooth axe"] = 1500, + ["glooth blade"] = 1500, + ["glooth cape"] = 7000, + ["glooth club"] = 1500, + ["glooth whip"] = 2500, + ["glorious axe"] = 3000, + ["glowing rune"] = 350, + ["goanna claw"] = 260, + ["goanna meat"] = 190, + ["goat grass"] = 50, + ["goblet of gloom"] = 12000, + ["goblin ear"] = 20, + ["gold ingot"] = 5000, + ["gold nugget"] = 850, + ["gold ring"] = 8000, + ["golden amulet"] = 2000, + ["golden armor"] = 20000, + ["golden brush"] = 250, + ["golden fafnar trophy"] = 10000, + ["golden figurine"] = 3000, + ["golden legs"] = 30000, + ["golden lotus brooch"] = 270, + ["golden mask"] = 38000, + ["golden mug"] = 250, + ["golden sickle"] = 1000, + ["goo shell"] = 4000, + ["goosebump leather"] = 650, + ["grant of arms"] = 950, + ["grasshopper legs"] = 15000, + ["grave flower"] = 25, + ["greed's arm"] = 950000, + ["green bandage"] = 180, + ["green crystal fragment"] = 800, + ["green crystal shard"] = 1500, + ["green crystal splinter"] = 400, + ["green dragon leather"] = 100, + ["green dragon scale"] = 100, + ["green gem"] = 5000, + ["green glass plate"] = 180, + ["green mushroom"] = 100, + ["green piece of cloth"] = 200, + ["greenwood coat"] = 50000, + ["griffin shield"] = 3000, + ["grimace"] = 120000, + ["gruesome fan"] = 15000, + ["guardian axe"] = 9000, + ["guardian boots"] = 35000, + ["guardian halberd"] = 11000, + ["guardian shield"] = 2000, + ["guidebook"] = 200, + ["hailstorm rod"] = 3000, + ["hair of a banshee"] = 350, + ["halberd"] = 400, + ["half-digested piece of meat"] = 55, + ["half-digested stones"] = 40, + ["half-eaten brain"] = 85, + ["ham"] = 4, + ["hammer of wrath"] = 30000, + ["hand"] = 1450, + ["hand axe"] = 4, + ["hardened bone"] = 70, + ["harpoon of a giant snail"] = 15000, + ["hatched rorc egg"] = 30, + ["hatchet"] = 25, + ["haunted blade"] = 8000, + ["haunted piece of wood"] = 115, + ["hazardous heart"] = 5000, + ["hazardous robe"] = 3000, + ["head"] = 3500, + ["headchopper"] = 6000, + ["heat core"] = 10000, + ["heaven blossom"] = 50, + ["heavy mace"] = 50000, + ["heavy machete"] = 90, + ["heavy trident"] = 2000, + ["hellhound slobber"] = 500, + ["hellspawn tail"] = 475, + ["helmet of the lost"] = 2000, + ["hemp rope"] = 350, + ["heroic axe"] = 30000, + ["hexagonal ruby"] = 30000, + ["hibiscus dress"] = 3000, + ["hideous chunk"] = 510, + ["hieroglyph banner"] = 500, + ["high guard flag"] = 550, + ["high guard shoulderplates"] = 130, + ["hive bow"] = 28000, + ["hive scythe"] = 17000, + ["hollow stampor hoof"] = 400, + ["holy ash"] = 160, + ["holy orchid"] = 90, + ["honeycomb"] = 40, + ["horn"] = 300, + ["horn of kalyassa"] = 10000, + ["horoscope"] = 40, + ["horseman helmet"] = 280, + ["huge chunk of crude iron"] = 15000, + ["huge shell"] = 15000, + ["huge spiky snail shell"] = 8000, + ["humongous chunk"] = 540, + ["hunter's quiver"] = 80, + ["hunting spear"] = 25, + ["hydra egg"] = 500, + ["hydra head"] = 600, + ["ice flower"] = 370, + ["ice rapier"] = 1000, + ["icy blacksteel sword"] = 6000, + ["icy cranial basher"] = 30000, + ["icy crystal mace"] = 12000, + ["icy dragon slayer"] = 15000, + ["icy headchopper"] = 6000, + ["icy heroic axe"] = 30000, + ["icy knight axe"] = 2000, + ["icy mystic blade"] = 30000, + ["icy orcish maul"] = 6000, + ["icy relic sword"] = 25000, + ["icy spike sword"] = 1000, + ["icy war axe"] = 12000, + ["icy war hammer"] = 1200, + ["incantation notes"] = 90, + ["infernal heart"] = 2100, + ["infernal robe"] = 1200, + ["inkwell"] = 8, + ["instable proto matter"] = 300, + ["iron helmet"] = 150, + ["iron ore"] = 500, + ["ivory carving"] = 300, + ["ivory comb"] = 8000, + ["izcandar's snow globe"] = 180000, + ["izcandar's sundial"] = 225000, + ["jacket"] = 1, + ["jade hammer"] = 25000, + ["jade hat"] = 9000, + ["jagged sickle"] = 150000, + ["jaws"] = 3900, + ["jewelled belt"] = 180, + ["katana"] = 35, + ["katex' blood"] = 210, + ["key to the drowned library"] = 330, + ["knight armor"] = 5000, + ["knight axe"] = 2000, + ["knight legs"] = 5000, + ["kollos shell"] = 420, + ["kongra's shoulderpad"] = 100, + ["krimhorn helmet"] = 200, + ["lamassu hoof"] = 330, + ["lamassu horn"] = 240, + ["lancer beetle shell"] = 80, + ["lancet"] = 90, + ["lavos armor"] = 16000, + ["leaf legs"] = 500, + ["leather armor"] = 12, + ["leather boots"] = 2, + ["leather harness"] = 750, + ["leather helmet"] = 4, + ["leather legs"] = 9, + ["legion helmet"] = 22, + ["legionnaire flags"] = 500, + ["leopard armor"] = 300, + ["leviathan's amulet"] = 3000, + ["life crystal"] = 50, + ["life preserver"] = 300, + ["life ring"] = 50, + ["light shovel"] = 300, + ["lightning boots"] = 2500, + ["lightning headband"] = 2500, + ["lightning legs"] = 11000, + ["lightning pendant"] = 1500, + ["lightning robe"] = 11000, + ["lion cloak patch"] = 190, + ["lion crest"] = 270, + ["lion figurine"] = 10000, + ["lion seal"] = 210, + ["lion trophy3"] = 3000, + ["lion's mane"] = 60, + ["little bowl of myrrh"] = 500, + ["lizard essence"] = 300, + ["lizard heart"] = 530, + ["lizard leather"] = 150, + ["lizard scale"] = 120, + ["lizard trophy"] = 8000, + ["longing eyes"] = 8000, + ["longsword"] = 51, + ["lost basher's spike"] = 280, + ["lost bracers"] = 140, + ["lost husher's staff"] = 250, + ["lost soul"] = 120, + ["luminescent crystal pickaxe"] = 50, + ["luminous orb"] = 1000, + ["lump of dirt"] = 10, + ["lump of earth"] = 130, + ["lunar staff"] = 5000, + ["mace"] = 30, + ["machete"] = 6, + ["mad froth"] = 80, + ["magic light wand"] = 35, + ["magic plate armor"] = 90000, + ["magic sulphur"] = 8000, + ["magma amulet"] = 1500, + ["magma boots"] = 2500, + ["magma clump"] = 570, + ["magma coat"] = 11000, + ["magma legs"] = 11000, + ["magma monocle"] = 2500, + ["malice's horn"] = 620000, + ["malice's spine"] = 850000, + ["malofur's lunchbox"] = 240000, + ["mammoth fur cape"] = 6000, + ["mammoth fur shorts"] = 850, + ["mammoth tusk"] = 100, + ["mammoth whopper"] = 300, + ["mantassin tail"] = 280, + ["manticore ear"] = 310, + ["manticore tail"] = 220, + ["marlin trophy"] = 5000, + ["marsh stalker beak"] = 65, + ["marsh stalker feather"] = 50, + ["mastermind potion"] = 500, + ["mastermind shield"] = 50000, + ["maxilla"] = 250, + ["maxxenius head"] = 500000, + ["meat"] = 2, + ["meat hammer"] = 60, + ["medal of valiance"] = 410000, + ["medusa shield"] = 9000, + ["megalomania's essence"] = 1900000, + ["megalomania's skull"] = 1500000, + ["mercenary sword"] = 12000, + ["metal bat"] = 9000, + ["metal spats"] = 2000, + ["metal spike"] = 320, + ["might ring"] = 250, + ["milk churn"] = 100, + ["mind stone"] = 100, + ["mino lance"] = 7000, + ["mino shield"] = 3000, + ["minotaur horn"] = 75, + ["minotaur leather"] = 80, + ["minotaur trophy"] = 500, + ["miraculum"] = 60, + ["mirror"] = 10, + ["model ship"] = 1000, + ["modified crossbow"] = 10000, + ["mooh'tah plate"] = 6000, + ["moohtant cudgel"] = 14000, + ["moonlight rod"] = 200, + ["moonstone"] = 13000, + ["morbid tapestry"] = 30000, + ["morgaroth's heart"] = 15000, + ["morning star"] = 100, + ["mould heart"] = 2100, + ["mould robe"] = 1200, + ["mr. punish's handcuffs"] = 50000, + ["muck rod"] = 6000, + ["mucus plug"] = 500, + ["mutated bat ear"] = 420, + ["mutated flesh"] = 50, + ["mutated rat tail"] = 150, + ["mycological bow"] = 35000, + ["mysterious fetish"] = 50, + ["mystic blade"] = 30000, + ["mystic turban"] = 150, + ["mystical hourglass"] = 700, + ["naginata"] = 2000, + ["necklace of the deep"] = 3000, + ["necromantic robe"] = 250, + ["necrotic rod"] = 1000, + ["nettle blossom"] = 75, + ["nettle spit"] = 25, + ["nightmare blade"] = 35000, + ["noble amulet"] = 430000, + ["noble armor"] = 900, + ["noble axe"] = 10000, + ["noble cape"] = 425000, + ["noble turban"] = 430, + ["norse shield"] = 1500, + ["northwind rod"] = 1500, + ["nose ring"] = 2000, + ["obsidian lance"] = 500, + ["odd organ"] = 410, + ["ogre ear stud"] = 180, + ["ogre nose ring"] = 210, + ["old parchment"] = 500, + ["onyx chip"] = 500, + ["onyx flail"] = 22000, + ["onyx pendant"] = 3500, + ["opal"] = 500, + ["orange mushroom"] = 150, + ["orb"] = 750, + ["orc leather"] = 30, + ["orc tooth"] = 150, + ["orc trophy3"] = 1000, + ["orcish axe"] = 350, + ["orcish gear"] = 85, + ["orcish maul"] = 6000, + ["orichalcum pearl"] = 40, + ["oriental shoes"] = 15000, + ["ornamented axe"] = 20000, + ["ornamented shield"] = 1500, + ["ornate chestplate"] = 60000, + ["ornate crossbow"] = 12000, + ["ornate legs"] = 40000, + ["ornate locket"] = 18000, + ["ornate mace"] = 42000, + ["ornate shield"] = 42000, + ["orshabaal's brain"] = 12000, + ["pair of hellflayer horns"] = 1300, + ["pair of iron fists"] = 4000, + ["pair of old bracers"] = 500, + ["paladin armor"] = 15000, + ["pale worm's scalp"] = 489000, + ["panda teddy"] = 30000, + ["panther head"] = 750, + ["panther paw"] = 300, + ["patch of fine cloth"] = 1350, + ["patched boots"] = 2000, + ["peacock feather fan"] = 350, + ["pelvis bone"] = 30, + ["perfect behemoth fang"] = 250, + ["pet pig"] = 1500, + ["petrified scream"] = 250, + ["phantasmal hair"] = 500, + ["pharaoh banner"] = 1000, + ["pharaoh sword"] = 23000, + ["phoenix shield"] = 16000, + ["pick"] = 15, + ["piece of archer armor"] = 20, + ["piece of crocodile leather"] = 15, + ["piece of dead brain"] = 420, + ["piece of draconian steel"] = 3000, + ["piece of hell steel"] = 500, + ["piece of hellfire armor"] = 550, + ["piece of massacre's shell"] = 50000, + ["piece of royal steel"] = 10000, + ["piece of scarab shell"] = 45, + ["piece of swampling wood"] = 30, + ["piece of warrior armor"] = 50, + ["pieces of magic chalk"] = 210, + ["pig foot"] = 10, + ["pile of grave earth"] = 25, + ["pirate boots"] = 3000, + ["pirate hat"] = 1000, + ["pirate knee breeches"] = 200, + ["pirate shirt"] = 500, + ["pirate voodoo doll"] = 500, + ["plagueroot offshoot"] = 280000, + ["plasma pearls"] = 250, + ["plasmatic lightning"] = 270, + ["plate armor"] = 400, + ["plate legs"] = 115, + ["plate shield"] = 45, + ["platinum amulet"] = 2500, + ["poison dagger"] = 50, + ["poison gland"] = 210, + ["poison spider shell"] = 10, + ["poisonous slime"] = 50, + ["polar bear paw"] = 30, + ["pool of chitinous glue"] = 480, + ["porcelain mask"] = 2000, + ["powder herb"] = 10, + ["power ring"] = 50, + ["prismatic quartz"] = 450, + ["pristine worm head"] = 15000, + ["protection amulet"] = 100, + ["protective charm"] = 60, + ["pulverized ore"] = 400, + ["purified soul"] = 530, + ["purple robe"] = 110, + ["purple tome"] = 2000, + ["quara bone"] = 500, + ["quara eye"] = 350, + ["quara pincers"] = 410, + ["quara tentacle"] = 140, + ["queen's sceptre"] = 20000, + ["quill"] = 1100, + ["rabbit's foot"] = 50, + ["ragnir helmet"] = 400, + ["rainbow quartz"] = 500, + ["rapier"] = 5, + ["rare earth"] = 80, + ["ratana"] = 500, + ["ravenous circlet"] = 220000, + ["red crystal fragment"] = 800, + ["red dragon leather"] = 200, + ["red dragon scale"] = 200, + ["red gem"] = 1000, + ["red goanna scale"] = 270, + ["red hair dye"] = 40, + ["red lantern"] = 250, + ["red piece of cloth"] = 300, + ["red tome"] = 2000, + ["relic sword"] = 25000, + ["rhino hide"] = 175, + ["rhino horn"] = 265, + ["rhino horn carving"] = 300, + ["rift bow"] = 45000, + ["rift crossbow"] = 45000, + ["rift lance"] = 30000, + ["rift shield"] = 50000, + ["ring of blue plasma"] = 8000, + ["ring of green plasma"] = 8000, + ["ring of healing"] = 100, + ["ring of red plasma"] = 8000, + ["ring of the sky"] = 30000, + ["ripper lance"] = 500, + ["rod"] = 2200, + ["roots"] = 1200, + ["rope"] = 15, + ["rope belt"] = 66, + ["rorc egg"] = 120, + ["rorc feather"] = 70, + ["rotten heart"] = 74000, + ["rotten piece of cloth"] = 30, + ["royal axe"] = 40000, + ["royal helmet"] = 30000, + ["royal tapestry"] = 1000, + ["rubber cap"] = 11000, + ["ruby necklace"] = 2000, + ["runed sword"] = 45000, + ["ruthless axe"] = 45000, + ["sabre"] = 12, + ["sabretooth"] = 400, + ["sacred tree amulet"] = 3000, + ["safety pin"] = 120, + ["sai"] = 16500, + ["salamander shield"] = 280, + ["sample of monster blood"] = 250, + ["sandcrawler shell"] = 20, + ["sapphire hammer"] = 7000, + ["scale armor"] = 75, + ["scale of corruption"] = 680, + ["scale of gelidrazah"] = 10000, + ["scarab amulet"] = 200, + ["scarab pincers"] = 280, + ["scarab shield"] = 2000, + ["scimitar"] = 150, + ["scorpion tail"] = 25, + ["scroll of heroic deeds"] = 230, + ["scythe"] = 10, + ["scythe leg"] = 450, + ["sea horse figurine"] = 42000, + ["sea serpent scale"] = 520, + ["sea serpent trophy"] = 10000, + ["seeds"] = 150, + ["sentinel shield"] = 120, + ["serpent sword"] = 900, + ["shadow herb"] = 20, + ["shadow sceptre"] = 10000, + ["shaggy tail"] = 25, + ["shamanic hood"] = 45, + ["shamanic talisman"] = 200, + ["shard"] = 2000, + ["shimmering beetles"] = 150, + ["shiny stone"] = 500, + ["shockwave amulet"] = 3000, + ["short sword"] = 10, + ["shovel"] = 8, + ["sickle"] = 3, + ["sight of surrender's eye"] = 3000, + ["signet ring"] = 480000, + ["silencer claws"] = 390, + ["silencer resonating chamber"] = 600, + ["silken bookmark"] = 1300, + ["silkweaver bow"] = 12000, + ["silky fur"] = 35, + ["silver amulet"] = 50, + ["silver brooch"] = 150, + ["silver dagger"] = 500, + ["silver fafnar trophy"] = 1000, + ["silver hand mirror"] = 10000, + ["simple dress"] = 50, + ["single human eye"] = 1000, + ["skeleton decoration"] = 3000, + ["skull belt"] = 80, + ["skull coin"] = 12000, + ["skull fetish"] = 250, + ["skull helmet"] = 40000, + ["skull of ratha"] = 250, + ["skull shatterer"] = 170, + ["skull staff"] = 6000, + ["skullcracker armor"] = 18000, + ["skunk tail"] = 50, + ["slime mould"] = 175, + ["slimy leg"] = 4500, + ["sling herb"] = 10, + ["small amethyst"] = 200, + ["small axe"] = 5, + ["small diamond"] = 300, + ["small emerald"] = 250, + ["small enchanted amethyst"] = 200, + ["small enchanted emerald"] = 250, + ["small enchanted ruby"] = 250, + ["small enchanted sapphire"] = 250, + ["small energy ball"] = 250, + ["small flask of eyedrops"] = 95, + ["small notebook"] = 480, + ["small oil lamp"] = 150, + ["small pitchfork"] = 70, + ["small ruby"] = 250, + ["small sapphire"] = 250, + ["small topaz"] = 200, + ["snake skin"] = 400, + ["snakebite rod"] = 100, + ["sniper gloves"] = 2000, + ["soldier helmet"] = 16, + ["solid rage"] = 310, + ["some grimeleech wings"] = 1200, + ["soul orb"] = 25, + ["soul stone"] = 6000, + ["souleater trophy"] = 7500, + ["spark sphere"] = 350, + ["sparkion claw"] = 290, + ["sparkion legs"] = 310, + ["sparkion stings"] = 280, + ["sparkion tail"] = 300, + ["spear"] = 3, + ["spectral gold nugget"] = 500, + ["spectral silver nugget"] = 250, + ["spellsinger's seal"] = 280, + ["spellweaver's robe"] = 12000, + ["sphinx feather"] = 470, + ["sphinx tiara"] = 360, + ["spider fangs"] = 10, + ["spider silk"] = 100, + ["spidris mandible"] = 450, + ["spike shield"] = 250, + ["spike sword"] = 240, + ["spiked iron ball"] = 100, + ["spiked squelcher"] = 5000, + ["spiky club"] = 300, + ["spirit cloak"] = 350, + ["spirit container"] = 40000, + ["spite's spirit"] = 840000, + ["spitter nose"] = 340, + ["spooky blue eye"] = 95, + ["spool of yarn"] = 1000, + ["springsprout rod"] = 3600, + ["srezz' eye"] = 300, + ["stampor horn"] = 280, + ["stampor talons"] = 150, + ["star amulet"] = 500, + ["star herb"] = 15, + ["statue of abyssador"] = 4000, + ["statue of deathstrike"] = 3000, + ["statue of devovorga"] = 1500, + ["statue of gnomevil"] = 2000, + ["stealth ring"] = 200, + ["steel boots"] = 30000, + ["steel helmet"] = 293, + ["steel shield"] = 80, + ["stone herb"] = 20, + ["stone nose"] = 590, + ["stone skin amulet"] = 500, + ["stone wing"] = 120, + ["stonerefiner's skull"] = 100, + ["strand of medusa hair"] = 600, + ["strange helmet"] = 500, + ["strange proto matter"] = 300, + ["strange symbol"] = 200, + ["strange talisman"] = 30, + ["striped fur"] = 50, + ["studded armor"] = 25, + ["studded club"] = 10, + ["studded helmet"] = 20, + ["studded legs"] = 15, + ["studded shield"] = 16, + ["stuffed dragon"] = 6000, + ["sulphurous stone"] = 100, + ["swamp grass"] = 20, + ["swamplair armor"] = 16000, + ["swampling club"] = 40, + ["swampling moss"] = 20, + ["swarmer antenna"] = 130, + ["sword"] = 25, + ["sword ring"] = 100, + ["tail of corruption"] = 240, + ["talon"] = 320, + ["tarantula egg"] = 80, + ["tarnished rhino figurine"] = 320, + ["tattered piece of robe"] = 120, + ["taurus mace"] = 500, + ["telescope eye"] = 1600, + ["tempest shield"] = 35000, + ["templar scytheblade"] = 200, + ["tentacle piece"] = 5000, + ["terra amulet"] = 1500, + ["terra boots"] = 2500, + ["terra hood"] = 2500, + ["terra legs"] = 11000, + ["terra mantle"] = 11000, + ["terra rod"] = 2000, + ["terramite eggs"] = 50, + ["terramite legs"] = 60, + ["terramite shell"] = 170, + ["terrorbird beak"] = 95, + ["thaian sword"] = 16000, + ["the avenger"] = 42000, + ["the handmaiden's protector"] = 50000, + ["the imperor's trident"] = 50000, + ["the ironworker"] = 50000, + ["the justice seeker"] = 40000, + ["the plasmother's remains"] = 50000, + ["thick fur"] = 150, + ["thorn"] = 100, + ["throwing knife"] = 2, + ["tiger eye"] = 350, + ["time ring"] = 100, + ["titan axe"] = 4000, + ["token of love"] = 440000, + ["tooth file"] = 60, + ["tooth of tazhadur"] = 10000, + ["torn shirt"] = 250, + ["tortoise shield"] = 150, + ["tower shield"] = 8000, + ["trapped bad dream monster"] = 900, + ["trashed draken boots"] = 40000, + ["tribal mask"] = 250, + ["troll green"] = 25, + ["trollroot"] = 50, + ["trophy of jaul"] = 4000, + ["trophy of obujos"] = 3000, + ["trophy of tanjis"] = 2000, + ["tunnel tyrant head"] = 500, + ["tunnel tyrant shell"] = 700, + ["turtle shell"] = 90, + ["tusk"] = 100, + ["tusk shield"] = 850, + ["twiceslicer"] = 28000, + ["twin hooks"] = 500, + ["two handed sword"] = 450, + ["undead heart"] = 200, + ["underworld rod"] = 4400, + ["unholy bone"] = 480, + ["unholy book"] = 30000, + ["unicorn figurine"] = 50000, + ["urmahlullu's mane"] = 490000, + ["urmahlullu's paw"] = 245000, + ["urmahlullu's tail"] = 210000, + ["utua's poison"] = 230, + ["vampire dust"] = 100, + ["vampire shield"] = 15000, + ["vampire teeth"] = 275, + ["vampire's cape chain"] = 150, + ["veal"] = 40, + ["vein of ore"] = 330, + ["velvet tapestry"] = 800, + ["venison"] = 55, + ["vexclaw talon"] = 1100, + ["vial"] = 5, + ["vial of hatred"] = 737000, + ["vibrant heart"] = 2100, + ["vibrant robe"] = 1200, + ["viking helmet"] = 66, + ["viking shield"] = 85, + ["vile axe"] = 30000, + ["violet crystal shard"] = 1500, + ["violet gem"] = 10000, + ["violet glass plate"] = 2150, + ["volatile proto matter"] = 300, + ["voodoo doll"] = 400, + ["wailing widow's necklace"] = 3000, + ["walnut"] = 80, + ["wand of cosmic energy"] = 2000, + ["wand of decay"] = 1000, + ["wand of defiance"] = 6500, + ["wand of draconia"] = 1500, + ["wand of dragonbreath"] = 200, + ["wand of everblazing"] = 6000, + ["wand of inferno"] = 3000, + ["wand of starstorm"] = 3600, + ["wand of voodoo"] = 4400, + ["wand of vortex"] = 100, + ["war axe"] = 12000, + ["war crystal"] = 460, + ["war hammer"] = 470, + ["war horn"] = 8000, + ["warmaster's wristguards"] = 200, + ["warrior helmet"] = 5000, + ["warrior's axe"] = 11000, + ["warrior's shield"] = 9000, + ["warwolf fur"] = 30, + ["waspoid claw"] = 320, + ["waspoid wing"] = 190, + ["watch"] = 6, + ["watermelon tourmaline"] = 30000, + ["weaver's wandtip"] = 250, + ["wedding ring"] = 100, + ["werebadger claws"] = 160, + ["werebadger skull"] = 185, + ["werebadger trophy"] = 9000, + ["werebear fur"] = 185, + ["werebear skull"] = 195, + ["werebear trophy"] = 11000, + ["wereboar hooves"] = 175, + ["wereboar loincloth"] = 1500, + ["wereboar trophy"] = 10000, + ["wereboar tusks"] = 165, + ["werefox tail"] = 200, + ["werefox trophy"] = 9000, + ["werehyaena nose"] = 220, + ["werehyaena talisman"] = 350, + ["werehyaena trophy"] = 12000, + ["werewolf amulet"] = 3000, + ["werewolf fangs"] = 180, + ["werewolf fur"] = 380, + ["white deer antlers"] = 400, + ["white deer skin"] = 245, + ["white gem"] = 12000, + ["white pearl"] = 160, + ["white piece of cloth"] = 100, + ["white silk flower"] = 9000, + ["widow's mandibles"] = 110, + ["wild flowers"] = 120, + ["wimp tooth chain"] = 120, + ["windborn colossus armor"] = 50000, + ["winged tail"] = 800, + ["winter wolf fur"] = 20, + ["witch broom"] = 60, + ["witch hat"] = 5000, + ["withered pauldrons"] = 850, + ["withered scalp"] = 900, + ["wolf paw"] = 70, + ["wolf tooth chain"] = 100, + ["wolf trophy"] = 3000, + ["wood"] = 5, + ["wood mushroom"] = 15, + ["wooden hammer"] = 15, + ["wooden shield"] = 5, + ["wool"] = 15, + ["writhing brain"] = 370000, + ["writhing heart"] = 185000, + ["wyrm scale"] = 400, + ["wyvern fang"] = 1500, + ["wyvern talisman"] = 265, + ["yellow gem"] = 1000, + ["yellow piece of cloth"] = 150, + ["yielocks"] = 600, + ["yielowax"] = 600, + ["yirkas' egg"] = 280, + ["young lich worm"] = 25000, + ["zaoan armor"] = 14000, + ["zaoan halberd"] = 500, + ["zaoan helmet"] = 45000, + ["zaoan legs"] = 14000, + ["zaoan robe"] = 12000, + ["zaoan shoes"] = 5000, + ["zaoan sword"] = 30000, + ["zaogun flag"] = 600, + ["zaogun shoulderplates"] = 150, + + -- 12.70 + ["carnisylvan bark"] = 230, + ["carnisylvan finger"] = 250, + ["human teeth"] = 2000, + ["abomination's eye"] = 650000, + ["abomination's tail"] = 700000, + ["abomination's tongue"] = 950000, + ["afflicted strider head"] = 900, + ["afflicted strider worms"] = 500, + ["bashmu fang"] = 600, + ["bashmu feather"] = 350, + ["bashmu tongue"] = 400, + ["blemished spawn abdomen"] = 550, + ["blemished spawn head"] = 800, + ["blemished spawn tail"] = 1000, + ["brainstealer's brain"] = 300000, + ["brainstealer's brainwave"] = 440000, + ["brainstealer's tissue"] = 240000, + ["cave chimera head"] = 1200, + ["cave chimera leg"] = 650, + ["curl of hair"] = 320000, + ["eyeless devourer legs"] = 650, + ["eyeless devourer maw"] = 420, + ["eyeless devourer tongue"] = 900, + ["girtablilu warrior carapace"] = 520, + ["lavafungus head"] = 900, + ["lavafungus ring"] = 390, + ["lavaworm jaws"] = 1100, + ["lavaworm spike roots"] = 600, + ["lavaworm spikes"] = 750, + ["old girtablilu carapace"] = 570, + ["old royal diary"] = 220000, + ["scorpion charm"] = 620, + ["tremendous tyrant head"] = 930, + ["tremendous tyrant shell"] = 740, + ["varnished diremaw brainpan"] = 750, + ["varnished diremaw legs"] = 670, + ["streaked devourer eyes"] = 500, + ["streaked devourer legs"] = 600, + ["streaked devourer maw"] = 400, + ["eldritch crystal"] = 48000, + + -- supplies + ["mana potion"] = 56, + ["strong mana potion"] = 93, + ["great mana potion"] = 144, + ["ultimate mana potion"] = 438, + ["health potion"] = 50, + ["strong health potion"] = 115, + ["great health potion"] = 225, + ["ultimate health potion"] = 379, + ["supreme health potion"] = 625, + ["great spirit potion"] = 228, + ["ultimate spirit potion"] = 438, + -- runes + ["cure poison rune"] = 65, + ["poison field rune"] = 21, + ["fire field rune"] = 28, + ["intense healing rune"] = 95, + ["destroy field rune"] = 15, + ["energy field rune"] = 38, + ["stalagmite rune"] = 12, + ["heavy magic missile rune"] = 12, + ["disintegrate rune"] = 26, + ["ultimate healing rune"] = 175, + ["poison bomb rune"] = 85, + ["animate death rune"] = 375, + ["chameleon rune"] = 210, + ["fireball rune"] = 30, + ["holy missile rune"] = 16, + ["icicle rune"] = 30, + ["stone shower rune"] = 37, + ["thunderstorm rune"] = 47, + ["avalanche rune"] = 57, + ["great fireball rune"] = 57, + ["convince creature rune"] = 80, + ["fire bomb rune"] = 147, + ["poison wall rune"] = 52, + ["explosion rune"] = 31, + ["fire wall rune"] = 61, + ["soulfire rune"] = 46, + ["wild growth rune"] = 160, + ["magic wall rune"] = 116, + ["energy wall rune"] = 85, + ["energy bomb rune"] = 203, + ["sudden death rune"] = 135, + ["paralyse rune"] = 700, + + ["envenomed arrow"] = 12, + ["flaming arrow"] = 5, + ["flash arrow"] = 5, + ["onyx arrow"] = 7, + ["poison arrow"] = 1, + ["shiver arrow"] = 5, + ["simple arrow"] = 2, + ["sniper arrow"] = 5, + ["tarsal arrow"] = 6, + ["arrow"] = 3, + ["burst arrow"] = 0, + ["crystalline arrow"] = 20, + ["diamond arrow"] = 100, + ["earth arrow"] = 5, + ["infernal bolt"] = 12, + ["piercing bolt"] = 5, + ["power bolt"] = 7, + ["prismatic bolt"] = 20, + ["spectral bolt"] = 70, + ["vortex bolt"] = 6, + ["bolt"] = 4, + ["drill bolt"] = 12, +} + +WasteItems = { + -- supplies + ["mana potion"] = 268, + ["strong mana potion"] = 237, + ["great mana potion"] = 238, + ["ultimate mana potion"] = 23373, + ["health potion"] = 266, + ["strong health potion"] = 236, + ["great health potion"] = 239, + ["ultimate health potion"] = 7643, + ["supreme health potion"] = 23375, + ["great spirit potion"] = 7642, + ["ultimate spirit potion"] = 23374, + -- ammo + ["envenomed arrow"] = 16143, + ["flaming arrow"] = 763, + ["flash arrow"] = 761, + ["onyx arrow"] = 7365, + ["poison arrow"] = 3448, + ["shiver arrow"] = 762, + ["simple arrow"] = 21470, + ["sniper arrow"] = 7364, + ["tarsal arrow"] = 14251, + ["arrow"] = 3447, + ["burst arrow"] = 3449, + ["crystalline arrow"] = 15793, + ["diamond arrow"] = 35901, + ["earth arrow"] = 774, + ["infernal bolt"] = 6528, + ["piercing bolt"] = 7363, + ["power bolt"] = 3450, + ["prismatic bolt"] = 16141, + ["spectral bolt"] = 35902, + ["vortex bolt"] = 14252, + ["bolt"] = 3446, + ["drill bolt"] = 16142, + -- runes + ["cure poison rune"] = 3153, + ["poison field rune"] = 3172, + ["fire field rune"] = 3188, + ["intense healing rune"] = 3152, + ["destroy field rune"] = 3148, + ["energy field rune"] = 3164, + ["stalagmite rune"] = 3179, + ["heavy magic missile rune"] = 3198, + ["disintegrate rune"] = 3197, + ["ultimate healing rune"] = 3160, + ["poison bomb rune"] = 3173, + ["animate death rune"] = 3203, + ["chameleon rune"] = 3178, + ["fireball rune"] = 3189, + ["holy missile rune"] = 3182, + ["icicle rune"] = 3158, + ["stone shower rune"] = 3175, + ["thunderstorm rune"] = 3202, + ["avalanche rune"] = 3161, + ["great fireball rune"] = 3191, + ["convince creature rune"] = 3177, + ["fire bomb rune"] = 3192, + ["poison wall rune"] = 3176, + ["explosion rune"] = 3200, + ["fire wall rune"] = 3190, + ["soulfire rune"] = 3195, + ["wild growth rune"] = 3156, + ["magic wall rune"] = 3180, + ["energy wall rune"] = 3166, + ["energy bomb rune"] = 3149, + ["sudden death rune"] = 3155, + ["paralyse rune"] = 3165 +} \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/main.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/main.lua new file mode 100644 index 0000000000..8b3b717d9d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/main.lua @@ -0,0 +1,40 @@ +local version = "4.7" +local currentVersion +local available = false + +storage.checkVersion = storage.checkVersion or 0 + +-- check max once per 12hours +if os.time() > storage.checkVersion + (12 * 60 * 60) then + + storage.checkVersion = os.time() + + HTTP.get("https://raw.githubusercontent.com/Vithrax/vBot/main/vBot/version.txt", function(data, err) + if err then + warn("[vBot updater]: Unable to check version:\n" .. err) + return + end + + currentVersion = data + available = true + end) + +end + +UI.Label("vBot v".. version .." \n Vithrax#5814") +UI.Button("Official OTCv8 Discord!", function() g_platform.openUrl("https://discord.gg/yhqBE4A") end) +UI.Separator() + +schedule(5000, function() + + if not available then return end + if currentVersion ~= version then + + UI.Separator() + UI.Label("New vBot is available for download! v"..currentVersion) + UI.Button("Go to vBot GitHub Page", function() g_platform.openUrl("https://github.com/Vithrax/vBot") end) + UI.Separator() + + end + +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/new_cavebot_lib.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/new_cavebot_lib.lua new file mode 100644 index 0000000000..3434a999f9 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/new_cavebot_lib.lua @@ -0,0 +1,518 @@ +CaveBot = {} -- global namespace + +------------------------------------------------------------------- +-- CaveBot lib 1.0 +-- Contains a universal set of functions to be used in CaveBot + +----------------------[[ basic assumption ]]----------------------- +-- in general, functions cannot be slowed from within, only externally, by event calls, delays etc. +-- considering that and the fact that there is no while loop, every function return action +-- thus, functions will need to be verified outside themselfs or by another function +-- overall tips to creating extension: +-- - functions return action(nil) or true(done) +-- - extensions are controlled by retries var +------------------------------------------------------------------- + +-- local variables, constants and functions, used by global functions +local LOCKERS_LIST = {3497, 3498, 3499, 3500} +local LOCKER_ACCESSTILE_MODIFIERS = { + [3497] = {0,-1}, + [3498] = {1,0}, + [3499] = {0,1}, + [3500] = {-1,0} +} + +local function CaveBotConfigParse() + local name = storage["_configs"]["targetbot_configs"]["selected"] + if not name then + return warn("[vBot] Please create a new TargetBot config and reset bot") + end + local file = configDir .. "/targetbot_configs/" .. name .. ".json" + local data = g_resources.readFileContents(file) + return Config.parse(data)['looting'] +end + +local function getNearTiles(pos) + if type(pos) ~= "table" then + pos = pos:getPosition() + end + + local tiles = {} + local dirs = { + {-1, 1}, + {0, 1}, + {1, 1}, + {-1, 0}, + {1, 0}, + {-1, -1}, + {0, -1}, + {1, -1} + } + for i = 1, #dirs do + local tile = + g_map.getTile( + { + x = pos.x - dirs[i][1], + y = pos.y - dirs[i][2], + z = pos.z + } + ) + if tile then + table.insert(tiles, tile) + end + end + + return tiles +end + +-- ##################### -- +-- [[ Information class ]] -- +-- ##################### -- + +--- global variable to reflect current CaveBot status +CaveBot.Status = "waiting" + +--- Parses config and extracts loot list. +-- @return table +function CaveBot.GetLootItems() + local t = CaveBotConfigParse() and CaveBotConfigParse()["items"] or nil + + local returnTable = {} + if type(t) == "table" then + for i, item in pairs(t) do + table.insert(returnTable, item["id"]) + end + end + + return returnTable +end + + +--- Checks whether player has any visible items to be stashed +-- @return boolean +function CaveBot.HasLootItems() + for _, container in pairs(getContainers()) do + local name = container:getName():lower() + if not name:find("depot") and not name:find("your inbox") then + for _, item in pairs(container:getItems()) do + local id = item:getId() + if table.find(CaveBot.GetLootItems(), id) then + return true + end + end + end + end +end + +--- Parses config and extracts loot containers. +-- @return table +function CaveBot.GetLootContainers() + local t = CaveBotConfigParse() and CaveBotConfigParse()["containers"] or nil + + local returnTable = {} + if type(t) == "table" then + for i, container in pairs(t) do + table.insert(returnTable, container["id"]) + end + end + + return returnTable +end + +--- Information about open containers. +-- @param amount is boolean +-- @return table or integer +function CaveBot.GetOpenedLootContainers(containerTable) + local containers = CaveBot.GetLootContainers() + + local t = {} + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + if table.find(containers, containerId) then + table.insert(t, container) + end + end + + return containerTable and t or #t +end + +--- Some actions needs to be additionally slowed down in case of high ping. +-- Maximum at 2000ms in case of lag spike. +-- @param multiplayer is integer +-- @return void +function CaveBot.PingDelay(multiplayer) + multiplayer = multiplayer or 1 + if ping() and ping() > 150 then -- in most cases ping above 150 affects CaveBot + local value = math.min(ping() * multiplayer, 2000) + return delay(value) + end +end + +-- ##################### -- +-- [[ Container class ]] -- +-- ##################### -- + +--- Closes any loot container that is open. +-- @return void or boolean +function CaveBot.CloseLootContainer() + local containers = CaveBot.GetLootContainers() + + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + if table.find(containers, containerId) then + return g_game.close(container) + end + end + + return true +end + +function CaveBot.CloseAllLootContainers() + local containers = CaveBot.GetLootContainers() + + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + if table.find(containers, containerId) then + g_game.close(container) + end + end + + return true +end + +--- Opens any loot container that isn't already opened. +-- @return void or boolean +function CaveBot.OpenLootContainer() + local containers = CaveBot.GetLootContainers() + + local t = {} + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + table.insert(t, containerId) + end + + for _, container in pairs(getContainers()) do + for _, item in pairs(container:getItems()) do + local id = item:getId() + if table.find(containers, id) and not table.find(t, id) then + return g_game.open(item) + end + end + end + + return true +end + +-- ##################### -- +-- [[[ Position class ]] -- +-- ##################### -- + +--- Compares distance between player position and given pos. +-- @param position is table +-- @param distance is integer +-- @return boolean +function CaveBot.MatchPosition(position, distance) + local pPos = player:getPosition() + distance = distance or 1 + return getDistanceBetween(pPos, position) <= distance +end + +--- Stripped down to take less space. +-- Use only to safe position, like pz movement or reaching npc. +-- Needs to be called between 200-500ms to achieve fluid movement. +-- @param position is table +-- @param distance is integer +-- @return void +function CaveBot.GoTo(position, precision) + if not precision then + precision = 3 + end + return CaveBot.walkTo(position, 20, {ignoreCreatures = true, precision = precision}) +end + +--- Finds position of npc by name and reaches its position. +-- @return void(acion) or boolean +function CaveBot.ReachNPC(name) + name = name:lower() + + local npc = nil + for i, spec in pairs(getSpectators()) do + if spec:isNpc() and spec:getName():lower() == name then + npc = spec + end + end + + if not CaveBot.MatchPosition(npc:getPosition(), 3) then + CaveBot.GoTo(npc:getPosition()) + else + return true + end +end + +-- ##################### -- +-- [[[[ Depot class ]]]] -- +-- ##################### -- + +--- Reaches closest locker. +-- @return void(acion) or boolean + +local depositerLockerTarget = nil +local depositerLockerReachRetries = 0 +function CaveBot.ReachDepot() + local pPos = player:getPosition() + local tiles = getNearTiles(player:getPosition()) + + for i, tile in pairs(tiles) do + for i, item in pairs(tile:getItems()) do + if table.find(LOCKERS_LIST, item:getId()) then + depositerLockerTarget = nil + depositerLockerReachRetries = 0 + return true -- if near locker already then return function + end + end + end + + if depositerLockerReachRetries > 20 then + depositerLockerTarget = nil + depositerLockerReachRetries = 0 + end + + local candidates = {} + + if not depositerLockerTarget or distanceFromPlayer(depositerLockerTarget, pPos) > 12 then + for i, tile in pairs(g_map.getTiles(posz())) do + local tPos = tile:getPosition() + for i, item in pairs(tile:getItems()) do + if table.find(LOCKERS_LIST, item:getId()) then + local lockerTilePos = tile:getPosition() + lockerTilePos.x = lockerTilePos.x + LOCKER_ACCESSTILE_MODIFIERS[item:getId()][1] + lockerTilePos.y = lockerTilePos.y + LOCKER_ACCESSTILE_MODIFIERS[item:getId()][2] + local lockerTile = g_map.getTile(lockerTilePos) + if not lockerTile:hasCreature() then + if findPath(pos(), tPos, 20, {ignoreNonPathable = false, precision = 1, ignoreCreatures = true}) then + local distance = getDistanceBetween(tPos, pPos) + table.insert(candidates, {pos=tPos, dist=distance}) + end + end + end + end + end + + if #candidates > 1 then + table.sort(candidates, function(a,b) return a.dist < b.dist end) + end + end + + depositerLockerTarget = depositerLockerTarget or candidates[1].pos + + if depositerLockerTarget then + if not CaveBot.MatchPosition(depositerLockerTarget) then + depositerLockerReachRetries = depositerLockerReachRetries + 1 + return CaveBot.GoTo(depositerLockerTarget, 1) + else + depositerLockerReachRetries = 0 + depositerLockerTarget = nil + return true + end + end +end + +--- Opens locker item. +-- @return void(acion) or boolean +function CaveBot.OpenLocker() + local pPos = player:getPosition() + local tiles = getNearTiles(player:getPosition()) + + local locker = getContainerByName("Locker") + if not locker then + for i, tile in pairs(tiles) do + for i, item in pairs(tile:getItems()) do + if table.find(LOCKERS_LIST, item:getId()) then + local topThing = tile:getTopUseThing() + if not topThing:isNotMoveable() then + g_game.move(topThing, pPos, topThing:getCount()) + else + return g_game.open(item) + end + end + end + end + else + return true + end +end + +--- Opens depot chest. +-- @return void(acion) or boolean +function CaveBot.OpenDepotChest() + local depot = getContainerByName("Depot chest") + if not depot then + local locker = getContainerByName("Locker") + if not locker then + return CaveBot.OpenLocker() + end + for i, item in pairs(locker:getItems()) do + if item:getId() == 3502 then + return g_game.open(item, locker) + end + end + else + return true + end +end + +--- Opens inbox inside locker. +-- @return void(acion) or boolean +function CaveBot.OpenInbox() + local inbox = getContainerByName("Your inbox") + if not inbox then + local locker = getContainerByName("Locker") + if not locker then + return CaveBot.OpenLocker() + end + for i, item in pairs(locker:getItems()) do + if item:getId() == 12902 then + return g_game.open(item) + end + end + else + return true + end +end + +--- Opens depot box of given number. +-- @param index is integer +-- @return void or boolean +function CaveBot.OpenDepotBox(index) + local depot = getContainerByName("Depot chest") + if not depot then + return CaveBot.ReachAndOpenDepot() + end + + local foundParent = false + for i, container in pairs(getContainers()) do + if container:getName():lower():find("depot box") then + foundParent = container + break + end + end + if foundParent then return true end + + for i, container in pairs(depot:getItems()) do + if i == index then + return g_game.open(container) + end + end +end + +--- Reaches and opens depot. +-- Combined for shorthand usage. +-- @return boolean whether succeed to reach and open depot +function CaveBot.ReachAndOpenDepot() + if CaveBot.ReachDepot() and CaveBot.OpenDepotChest() then + return true + end + return false +end + +--- Reaches and opens imbox. +-- Combined for shorthand usage. +-- @return boolean whether succeed to reach and open depot +function CaveBot.ReachAndOpenInbox() + if CaveBot.ReachDepot() and CaveBot.OpenInbox() then + return true + end + return false +end + +--- Stripped down function to stash item. +-- @param item is object +-- @param index is integer +-- @param destination is object +-- @return void +function CaveBot.StashItem(item, index, destination) + destination = destination or getContainerByName("Depot chest") + if not destination then return false end + + return g_game.move(item, destination:getSlotPosition(index), item:getCount()) +end + +--- Withdraws item from depot chest or mail inbox. +-- main function for depositer/withdrawer +-- @param id is integer +-- @param amount is integer +-- @param fromDepot is boolean or integer +-- @param destination is object +-- @return void +function CaveBot.WithdrawItem(id, amount, fromDepot, destination) + if destination and type(destination) == "string" then + destination = getContainerByName(destination) + end + local itemCount = itemAmount(id) + local depot + for i, container in pairs(getContainers()) do + if container:getName():lower():find("depot box") or container:getName():lower():find("your inbox") then + depot = container + break + end + end + if not depot then + if fromDepot then + if not CaveBot.OpenDepotBox(fromDepot) then return end + else + return CaveBot.ReachAndOpenInbox() + end + return + end + if not destination then + for i, container in pairs(getContainers()) do + if container:getCapacity() > #container:getItems() and not string.find(container:getName():lower(), "quiver") and not string.find(container:getName():lower(), "depot") and not string.find(container:getName():lower(), "loot") and not string.find(container:getName():lower(), "inbox") then + destination = container + end + end + end + + if itemCount >= amount then + return true + end + + local toMove = amount - itemCount + for i, item in pairs(depot:getItems()) do + if item:getId() == id then + return g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), math.min(toMove, item:getCount())) + end + end +end + +-- ##################### -- +-- [[[[[ Talk class ]]]] -- +-- ##################### -- + +--- Controlled by event caller. +-- Simple way to build npc conversations instead of multiline overcopied code. +-- @return void +function CaveBot.Conversation(...) + local expressions = {...} + local delay = storage.extras.talkDelay or 1000 + + local talkDelay = 0 + for i, expr in ipairs(expressions) do + schedule(talkDelay, function() NPC.say(expr) end) + talkDelay = talkDelay + delay + end +end + +--- Says hi trade to NPC. +-- Used as shorthand to open NPC trade window. +-- @return void +function CaveBot.OpenNpcTrade() + return CaveBot.Conversation("hi", "trade") +end + +--- Says hi destination yes to NPC. +-- Used as shorthand to travel. +-- @param destination is string +-- @return void +function CaveBot.Travel(destination) + return CaveBot.Conversation("hi", destination, "yes") +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.lua new file mode 100644 index 0000000000..9e85304bc4 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.lua @@ -0,0 +1,455 @@ +setDefaultTab("Main") +local panelName = "newHealer" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Friend Healer') + + Button + id: edit + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + +]]) +ui:setId(panelName) + +-- validate current settings +if not storage[panelName] or not storage[panelName].priorities then + storage[panelName] = nil +end + +if not storage[panelName] then + storage[panelName] = { + enabled = false, + customPlayers = {}, + vocations = {}, + groups = {}, + priorities = { + + {name="Custom Spell", enabled=false, custom=true}, + {name="Exura Gran Sio", enabled=true, strong = true}, + {name="Exura Sio", enabled=true, normal = true}, + {name="Exura Gran Mas Res", enabled=true, area = true}, + {name="Health Item", enabled=true, health=true}, + {name="Mana Item", enabled=true, mana=true} + + }, + settings = { + + {type="HealItem", text="Mana Item ", value=268}, + {type="HealScroll", text="Item Range: ", value=6}, + {type="HealItem", text="Health Item ", value=3160}, + {type="HealScroll", text="Mas Res Players: ", value=2}, + {type="HealScroll", text="Heal Friend at: ", value=80}, + {type="HealScroll", text="Use Gran Sio at: ", value=80}, + {type="HealScroll", text="Min Player HP%: ", value=80}, + {type="HealScroll", text="Min Player MP%: ", value=50}, + + }, + conditions = { + knights = true, + paladins = true, + druids = false, + sorcerers = false, + party = true, + guild = false, + botserver = false, + friends = false + } + } +end + +local config = storage[panelName] +local healerWindow = UI.createWindow('FriendHealer') +healerWindow:hide() +healerWindow:setId(panelName) + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) +end + +ui.edit.onClick = function() + healerWindow:show() + healerWindow:raise() + healerWindow:focus() +end + +local conditions = healerWindow.conditions +local targetSettings = healerWindow.targetSettings +local customList = healerWindow.customList +local priority = healerWindow.priority + +-- customList +-- create entries on the list +for name, health in pairs(config.customPlayers) do + local widget = UI.createWidget("HealerPlayerEntry", customList.playerList.list) + widget.remove.onClick = function() + config.customPlayers[name] = nil + widget:destroy() + end + widget:setText("["..health.."%] "..name) +end + +customList.playerList.onDoubleClick = function() + customList.playerList:hide() +end + +local function clearFields() + customList.addPanel.name:setText("friend name") + customList.addPanel.health:setText("1") + customList.playerList:show() +end + +local function capitalFistLetter(str) + return (string.gsub(str, "^%l", string.upper)) + end + +customList.addPanel.add.onClick = function() + local name = "" + local words = string.split(customList.addPanel.name:getText(), " ") + local health = tonumber(customList.addPanel.health:getText()) + for i, word in ipairs(words) do + name = name .. " " .. capitalFistLetter(word) + end + + if not health then + clearFields() + return warn("[Friend Healer] Please enter health percent value!") + end + + if name:len() == 0 or name:lower() == "friend name" then + clearFields() + return warn("[Friend Healer] Please enter friend name to be added!") + end + + if config.customPlayers[name] or config.customPlayers[name:lower()] then + clearFields() + return warn("[Friend Healer] Player already added to custom list.") + else + config.customPlayers[name] = health + local widget = UI.createWidget("HealerPlayerEntry", customList.playerList.list) + widget.remove.onClick = function() + config.customPlayers[name] = nil + widget:destroy() + end + widget:setText("["..health.."%] "..name) + end + + clearFields() +end + +local function validate(widget, category) + local list = widget:getParent() + local label = list:getParent().title + -- 1 - priorities | 2 - vocation + category = category or 0 + + if category == 2 and not storage.extras.checkPlayer then + label:setColor("#d9321f") + label:setTooltip("! WARNING ! \nTurn on check players in extras to use this feature!") + return + else + label:setColor("#dfdfdf") + label:setTooltip("") + end + + local checked = false + for i, child in ipairs(list:getChildren()) do + if category == 1 and child.enabled:isChecked() or child:isChecked() then + checked = true + end + end + + if not checked then + label:setColor("#d9321f") + label:setTooltip("! WARNING ! \nNo category selected!") + else + label:setColor("#dfdfdf") + label:setTooltip("") + end +end +-- targetSettings +targetSettings.vocations.box.knights:setChecked(config.conditions.knights) +targetSettings.vocations.box.knights.onClick = function(widget) + config.conditions.knights = not config.conditions.knights + widget:setChecked(config.conditions.knights) + validate(widget, 2) +end + +targetSettings.vocations.box.paladins:setChecked(config.conditions.paladins) +targetSettings.vocations.box.paladins.onClick = function(widget) + config.conditions.paladins = not config.conditions.paladins + widget:setChecked(config.conditions.paladins) + validate(widget, 2) +end + +targetSettings.vocations.box.druids:setChecked(config.conditions.druids) +targetSettings.vocations.box.druids.onClick = function(widget) + config.conditions.druids = not config.conditions.druids + widget:setChecked(config.conditions.druids) + validate(widget, 2) +end + +targetSettings.vocations.box.sorcerers:setChecked(config.conditions.sorcerers) +targetSettings.vocations.box.sorcerers.onClick = function(widget) + config.conditions.sorcerers = not config.conditions.sorcerers + widget:setChecked(config.conditions.sorcerers) + validate(widget, 2) +end + +targetSettings.groups.box.friends:setChecked(config.conditions.friends) +targetSettings.groups.box.friends.onClick = function(widget) + config.conditions.friends = not config.conditions.friends + widget:setChecked(config.conditions.friends) + validate(widget) +end + +targetSettings.groups.box.party:setChecked(config.conditions.party) +targetSettings.groups.box.party.onClick = function(widget) + config.conditions.party = not config.conditions.party + widget:setChecked(config.conditions.party) + validate(widget) +end + +targetSettings.groups.box.guild:setChecked(config.conditions.guild) +targetSettings.groups.box.guild.onClick = function(widget) + config.conditions.guild = not config.conditions.guild + widget:setChecked(config.conditions.guild) + validate(widget) +end + +targetSettings.groups.box.botserver:setChecked(config.conditions.botserver) +targetSettings.groups.box.botserver.onClick = function(widget) + config.conditions.botserver = not config.conditions.botserver + widget:setChecked(config.conditions.botserver) + validate(widget) +end + +validate(targetSettings.vocations.box.knights) +validate(targetSettings.groups.box.friends) +validate(targetSettings.vocations.box.sorcerers, 2) + +-- conditions +for i, setting in ipairs(config.settings) do + local widget = UI.createWidget(setting.type, conditions.box) + local text = setting.text + local val = setting.value + widget.text:setText(text) + + if setting.type == "HealScroll" then + widget.text:setText(widget.text:getText()..val) + if not (text:find("Range") or text:find("Mas Res")) then + widget.text:setText(widget.text:getText().."%") + end + widget.scroll:setValue(val) + widget.scroll.onValueChange = function(scroll, value) + setting.value = value + widget.text:setText(text..value) + if not (text:find("Range") or text:find("Mas Res")) then + widget.text:setText(widget.text:getText().."%") + end + end + if text:find("Range") or text:find("Mas Res") then + widget.scroll:setMaximum(10) + end + else + widget.item:setItemId(val) + widget.item:setShowCount(false) + widget.item.onItemChange = function(widget) + setting.value = widget:getItemId() + end + end +end + + + +-- priority and toggles +local function setCrementalButtons() + for i, child in ipairs(priority.list:getChildren()) do + if i == 1 then + child.increment:disable() + elseif i == 6 then + child.decrement:disable() + else + child.increment:enable() + child.decrement:enable() + end + end +end + +for i, action in ipairs(config.priorities) do + local widget = UI.createWidget("PriorityEntry", priority.list) + + widget:setText(action.name) + widget.increment.onClick = function() + local index = priority.list:getChildIndex(widget) + local table = config.priorities + + priority.list:moveChildToIndex(widget, index-1) + table[index], table[index-1] = table[index-1], table[index] + setCrementalButtons() + end + widget.decrement.onClick = function() + local index = priority.list:getChildIndex(widget) + local table = config.priorities + + priority.list:moveChildToIndex(widget, index+1) + table[index], table[index+1] = table[index+1], table[index] + setCrementalButtons() + end + widget.enabled:setChecked(action.enabled) + widget:setColor(action.enabled and "#98BF64" or "#dfdfdf") + widget.enabled.onClick = function() + action.enabled = not action.enabled + widget:setColor(action.enabled and "#98BF64" or "#dfdfdf") + widget.enabled:setChecked(action.enabled) + validate(widget, 1) + end + if action.custom then + widget.onDoubleClick = function() + local window = modules.client_textedit.show(widget, {title = "Custom Spell", description = "Enter below formula for a custom healing spell"}) + schedule(50, function() + window:raise() + window:focus() + end) + end + widget.onTextChange = function(widget,text) + action.name = text + end + widget:setTooltip("Double click to set spell formula.") + end + + if i == #config.priorities then + validate(widget, 1) + setCrementalButtons() + end +end + +local lastItemUse = now +local function friendHealerAction(spec, targetsInRange) + local name = spec:getName() + local health = spec:getHealthPercent() + local mana = spec:getManaPercent() + local dist = distanceFromPlayer(spec:getPosition()) + targetsInRange = targetsInRange or 0 + + local masResAmount = config.settings[4].value + local itemRange = config.settings[2].value + local healItem = config.settings[3].value + local manaItem = config.settings[1].value + local normalHeal = config.customPlayers[name] or config.settings[5].value + local strongHeal = config.customPlayers[name] and normalHeal/2 or config.settings[6].value + + for i, action in ipairs(config.priorities) do + if action.enabled then + if action.area and masResAmount <= targetsInRange and canCast("exura gran mas res") then + return say("exura gran mas res") + end + if action.mana and findItem(manaItem) and mana <= normalHeal and dist <= itemRange and now - lastItemUse > 1000 then + lastItemUse = now + return useWith(manaItem, spec) + end + if action.health and findItem(healItem) and health <= normalHeal and dist <= itemRange and now - lastItemUse > 1000 then + lastItemUse = now + return useWith(healItem, spec) + end + if action.strong and health <= strongHeal and not modules.game_cooldown.isCooldownIconActive(101) then + return say('exura gran sio "'..name) + end + if (action.normal or action.custom) and health <= normalHeal and canCast('exura sio "'..name) then + return say('exura sio "'..name) + end + end + end +end + +macro(100, function() + if not config.enabled then return end + if modules.game_cooldown.isGroupCooldownIconActive(2) then return end + + local minHp = config.settings[7].value + local minMp = config.settings[8].value + + -- first index will be heal target + local finalTable = {} + local inMasResRange = 0 + + -- check basic + if hppercent() <= minHp or manapercent() <= minMp then return end + + -- get all spectators + local spectators = getSpectators() + + -- clear table from irrelevant spectators + for i, spec in ipairs(getSpectators()) do + if spec:isLocalPlayer() or not spec:isPlayer() or not spec:canShoot() then + if not config.customPlayers[name] then + table.remove(spectators, table.find(spectators, spec)) + end + else + local specText = spec:getText() + -- check players is enabled and spectator already verified + if storage.extras.checkPlayer and specText:len() > 0 then + if specText:find("EK") and not config.conditions.knights or + specText:find("RP") and not config.conditions.paladins or + specText:find("ED") and not config.conditions.druids or + specText:find("MS") and not config.conditions.sorcerers then + if not config.customPlayers[name] then + table.remove(spectators, table.find(spectators, spec)) + end + end + end + local okParty = config.conditions.party and spec:isPartyMember() + local okFriend = config.conditions.friends and isFriend(spec) + local okGuild = config.conditions.guild and spec:getEmblem() == 1 + local okBotServer = config.conditions.botserver and vBot.BotServerMembers[spec:getName()] + if not (okParty or okFriend or okGuild or okBotServer) then + if not config.customPlayers[name] then + table.remove(spectators, table.find(spectators, spec)) + end + end + end + end + + -- no targets, return + if #spectators == 0 then return end + + for name, health in pairs(config.customPlayers) do + for i, spec in ipairs(spectators) do + local specHp = spec:getHealthPercent() + if spec:getName() == name and specHp <= health then + if distanceFromPlayer(spec:getPosition()) <= 2 then + inMasResRange = inMasResRange + 1 + end + table.insert(finalTable, spec) + table.remove(spectators, i) + end + end + end + + for i=1,#spectators do + local spec = spectators[i] + if distanceFromPlayer(spec:getPosition()) <= 3 then + inMasResRange = inMasResRange + 1 + end + table.insert(finalTable, spec) + end + + -- no targets, return + if #finalTable == 0 then return end + + friendHealerAction(finalTable[1], inMasResRange) +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.otui new file mode 100644 index 0000000000..2eb55cc4de --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/new_healer.otui @@ -0,0 +1,413 @@ +CategoryCheckBox < CheckBox + font: verdana-11px-rounded + margin-top: 3 + + $checked: + color: #98BF64 + +HealScroll < Panel + + ToolTipLabel + id: text + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-align: center + text: test + + HorizontalScrollBar + id: scroll + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + minimum: 0 + maximum: 100 + step: 1 + +HealItem < Panel + + BotItem + id: item + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + size: 34 34 + + ToolTipLabel + id: text + anchors.fill: parent + anchors.left: prev.right + margin-left: 8 + text-wrap: true + text-align: left + +ToolTipLabel < UIWidget + font: verdana-11px-rounded + color: #dfdfdf + height: 14 + text-align: center + +HealerPlayerEntry < Label + background-color: alpha + text-offset: 5 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + anchors.right: parent.right + margin-right: 2 + anchors.verticalCenter: parent.verticalCenter + size: 15 15 + margin-right: 15 + text: X + tooltip: Remove player from the list + +PriorityEntry < ToolTipLabel + background-color: alpha + text-offset: 18 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + size: 15 15 + margin-top: 2 + margin-left: 3 + + Button + id: increment + anchors.right: parent.right + margin-right: 2 + anchors.verticalCenter: parent.verticalCenter + size: 14 14 + text: + + tooltip: Increase Priority + + Button + id: decrement + anchors.right: prev.left + margin-right: 2 + anchors.verticalCenter: parent.verticalCenter + size: 14 14 + text: - + tooltip: Decrease Priority + +TargetSettings < Panel + size: 280 125 + padding: 3 + image-source: /images/ui/window + image-border: 6 + + Label + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Heal Target Settings + + Groups + id: groups + anchors.top: prev.bottom + margin-top: 8 + anchors.left: parent.left + margin-left: 9 + + Vocations + id: vocations + anchors.left: prev.right + margin-left: 5 + anchors.verticalCenter: prev.verticalCenter + +Groups < FlatPanel + size: 150 90 + padding: 3 + padding-top: 5 + + ToolTipLabel + id: title + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + text: Groups + tooltip: Players added in custom list will always be in scope + + HorizontalSeparator + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + + Panel + id: box + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + padding: 2 + layout: + type: verticalBox + + CategoryCheckBox + id: friends + text: Friends + + CategoryCheckBox + id: party + text: Party Members + + CategoryCheckBox + id: guild + text: Guild Members + + CategoryCheckBox + id: botserver + text: BotServer Members + +Vocations < FlatPanel + size: 100 90 + padding: 3 + padding-top: 5 + + ToolTipLabel + id: title + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Vocations + + HorizontalSeparator + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + + Panel + id: box + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + padding: 2 + + layout: + type: verticalBox + + CategoryCheckBox + id: knights + text: Knights + + CategoryCheckBox + id: paladins + text: Paladins + + CategoryCheckBox + id: druids + text: Druids + + CategoryCheckBox + id: sorcerers + text: Sorcerers + +Priority < Panel + size: 190 123 + padding: 6 + padding-top: 3 + image-source: /images/ui/window + image-border: 6 + + ToolTipLabel + id: title + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Priority & Toggles + + TextList + id: list + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + fit-children: true + padding-top: 1 + +AddPlayer < FlatPanel + padding: 5 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + font: verdana-11px-rounded + text: Add Player to Custom List + text-align: center + text-wrap: true + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + + SpinBox + id: health + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 20 + width: 50 + minimum: 1 + maximum: 99 + step: 1 + focusable: true + text-align: center + + Label + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + font: verdana-11px-rounded + text: %HP - heal if below + + TextEdit + id: name + anchors.top: health.bottom + margin-top: 5 + anchors.left: health.left + anchors.right: parent.right + font: verdana-11px-rounded + text-align: center + text: friend name + + Button + id: add + anchors.left: health.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + font: verdana-11px-rounded + text: Add Player + +PlayerList < Panel + + TextList + id: list + anchors.fill: parent + fit-children: true + padding-top: 2 + vertical-scrollbar: listScrollBar + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + +CustomList < Panel + size: 190 172 + padding: 6 + padding-top: 3 + image-source: /images/ui/window + image-border: 6 + + ToolTipLabel + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Custom Player List + tooltip: Double click on the list below to add new player. + + AddPlayer + id: addPanel + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + PlayerList + id: playerList + anchors.fill: prev + +Conditions < Panel + size: 280 170 + padding: 3 + image-source: /images/ui/window + image-border: 6 + + Label + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Player Conditions + + Panel + id: box + anchors.fill: parent + margin-top: 16 + padding: 5 + padding-top: 3 + layout: + type: grid + cell-size: 128 31 + cell-spacing: 5 + num-columns: 2 + +FriendHealer < MainWindow + !text: tr('Friend Healer') + size: 512 390 + padding-top: 30 + @onEscape: self:hide() + + Conditions + id: conditions + anchors.top: parent.top + anchors.right: parent.right + + TargetSettings + id: targetSettings + anchors.top: prev.bottom + margin-top: 10 + anchors.left: prev.left + + Priority + id: priority + anchors.top: parent.top + anchors.left: parent.left + + CustomList + id: customList + anchors.top: priority.bottom + margin-top: 10 + anchors.left: priority.left + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + @onClick: self:getParent():hide() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/npc_talk.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/npc_talk.lua new file mode 100644 index 0000000000..4ed5cf4e04 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/npc_talk.lua @@ -0,0 +1,5 @@ +onAttackingCreatureChange(function(creature, OldCreature) + if creature and creature:isNpc() and distanceFromPlayer(creature:getPosition()) <= 3 then + CaveBot.Conversation("hi", "trade") + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.lua new file mode 100644 index 0000000000..948928d69d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.lua @@ -0,0 +1,351 @@ +--[[ + configuration for check players + example made on server Gunzodus + + example link for player overview: + https://www.gunzodus.net/character/show/Sir_Vithrax + + *note that space in character name was replaced with underscore (_) - this character will be important + + in this case: + link = "https://www.gunzodus.net/character/show/" -- everything with all the characters up to the start of the name + spacing = "_" -- space replacement in character name +]] + +local link = "https://www.gunzodus.net/character/show/" +local spacing = "_" + + + +-- do not edit below +setDefaultTab("Main") +local tabs = {"Friends", "Enemies", "BlackList"} +local panelName = "playerList" +local colors = {"#03C04A", "#fc4c4e", "orange"} + +if not storage[panelName] then + storage[panelName] = { + enemyList = {}, + friendList = {}, + blackList = {}, + groupMembers = true, + outfits = false, + marks = false, + highlight = false + } +end + +local config = storage[panelName] +local playerTables = {config.friendList, config.enemyList, config.blackList} + +-- functions +local function clearCachedPlayers() + CachedFriends = {} + CachedEnemies = {} +end + +local refreshStatus = function() + for _, spec in ipairs(getSpectators()) do + if spec:isPlayer() and not spec:isLocalPlayer() then + if config.outfits then + local specOutfit = spec:getOutfit() + if isFriend(spec:getName()) then + if config.highlight then + spec:setMarked('#0000FF') + end + specOutfit.head = 88 + specOutfit.body = 88 + specOutfit.legs = 88 + specOutfit.feet = 88 + if storage.BOTserver.outfit then + local voc = vBot.BotServerMembers[spec:getName()] + specOutfit.addons = 3 + if voc == 1 then + specOutfit.type = 131 + elseif voc == 2 then + specOutfit.type = 129 + elseif voc == 3 then + specOutfit.type = 130 + elseif voc == 4 then + specOutfit.type = 144 + end + end + spec:setOutfit(specOutfit) + elseif isEnemy(spec:getName()) then + if config.highlight then + spec:setMarked('#FF0000') + end + specOutfit.head = 94 + specOutfit.body = 94 + specOutfit.legs = 94 + specOutfit.feet = 94 + spec:setOutfit(specOutfit) + end + end + end + end +end +refreshStatus() + +local checkStatus = function(creature) + if not creature:isPlayer() or creature:isLocalPlayer() then return end + + local specName = creature:getName() + local specOutfit = creature:getOutfit() + + if isFriend(specName) then + if config.highlight then + creature:setMarked('#0000FF') + end + if config.outfits then + specOutfit.head = 88 + specOutfit.body = 88 + specOutfit.legs = 88 + specOutfit.feet = 88 + if storage.BOTserver.outfit then + local voc = vBot.BotServerMembers[creature:getName()] + specOutfit.addons = 3 + if voc == 1 then + specOutfit.type = 131 + elseif voc == 2 then + specOutfit.type = 129 + elseif voc == 3 then + specOutfit.type = 130 + elseif voc == 4 then + specOutfit.type = 144 + end + end + creature:setOutfit(specOutfit) + end + elseif isEnemy(specName) then + if config.highlight then + creature:setMarked('#FF0000') + end + if config.outfits then + specOutfit.head = 94 + specOutfit.body = 94 + specOutfit.legs = 94 + specOutfit.feet = 94 + creature:setOutfit(specOutfit) + end + end +end + + +rootWidget = g_ui.getRootWidget() +if rootWidget then + local ListWindow = UI.createWindow('PlayerListWindow', rootWidget) + ListWindow:hide() + + UI.Button("Player Lists", function() + ListWindow:show() + ListWindow:raise() + ListWindow:focus() + end) + + -- settings + ListWindow.settings.Members:setChecked(config.groupMembers) + ListWindow.settings.Members.onClick = function(widget) + config.groupMembers = not config.groupMembers + if not config.groupMembers then + clearCachedPlayers() + end + refreshStatus() + widget:setChecked(config.groupMembers) + end + ListWindow.settings.Outfit:setChecked(config.outfits) + ListWindow.settings.Outfit.onClick = function(widget) + config.outfits = not config.outfits + widget:setChecked(config.outfits) + refreshStatus() + end + ListWindow.settings.NeutralsAreEnemy:setChecked(config.marks) + ListWindow.settings.NeutralsAreEnemy.onClick = function(widget) + config.marks = not config.marks + widget:setChecked(config.marks) + end + ListWindow.settings.Highlight:setChecked(config.highlight) + ListWindow.settings.Highlight.onClick = function(widget) + config.highlight = not config.highlight + widget:setChecked(config.highlight) + end + + ListWindow.settings.AutoAdd:setChecked(config.autoAdd) + ListWindow.settings.AutoAdd.onClick = function(widget) + config.autoAdd = not config.autoAdd + widget:setChecked(config.autoAdd) + end + + local TabBar = ListWindow.tmpTabBar + TabBar:setContentWidget(ListWindow.tmpTabContent) + local blacklistList + + for v = 1, 3 do + local listPanel = g_ui.createWidget("tPanel") -- Creates Panel + local playerList = playerTables[v] + listPanel:setId(tabs[v].."Tab") + TabBar:addTab(tabs[v], listPanel) + + -- elements + local addButton = listPanel.add + local nameTab = listPanel.name + local list = listPanel.list + if v == 3 then + blacklistList = list + end + + for i, name in ipairs(playerList) do + local label = UI.createWidget("PlayerLabel", list) + label:setText(name) + label.remove.onClick = function() + table.remove(playerList, table.find(playerList, name)) + label:destroy() + clearCachedPlayers() + refreshStatus() + end + label.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('Check Player', function() + local name = widget:getText():gsub(" ", spacing) + g_platform.openUrl(link..name) + end, "") + menu:addOption('Copy Name', function() + g_window.setClipboardText(widget:getText()) + end, "") + menu:display(mousePos) + return true + end + end + end + end + + local tabButton = TabBar.buttonsPanel:getChildren()[v] + + tabButton.onStyleApply = function(widget) + if TabBar:getCurrentTab() == widget then + widget:setColor(colors[v]) + end + end + + -- callbacks + addButton.onClick = function() + local names = string.split(nameTab:getText(), ",") + + if #names == 0 then + warn("vBot[PlayerList]: Name is missing!") + return + end + + for i=1,#names do + local name = names[i]:trim() + if name:len() == 0 then + warn("vBot[PlayerList]: Name is missing!") + else + if not table.find(playerList, name) then + table.insert(playerList, name) + local label = UI.createWidget("PlayerLabel", list) + label:setText(name) + label.remove.onClick = function() + table.remove(playerList, table.find(playerList, name)) + label:destroy() + end + label.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('Check Player', function() + local name = widget:getText():gsub(" ", "_") + local link = "https://www.gunzodus.net/character/show/" + g_platform.openUrl(link..name) + end, "") + menu:addOption('Copy Name', function() + g_window.setClipboardText(widget:getText()) + end, "") + menu:display(mousePos) + return true + end + end + end + nameTab:setText("") + else + warn("vBot[PlayerList]: Player ".. name .." is already added!") + nameTab:setText("") + end + clearCachedPlayers() + refreshStatus() + end + end + end + + nameTab.onKeyPress = function(widget, keyCode, keyboardModifiers) + if keyCode ~= 5 then + return false + end + addButton.onClick() + return true + end + end + + function addBlackListPlayer(name) + if table.find(config.blackList, name) then return end + + table.insert(config.blackList, name) + local label = UI.createWidget("PlayerLabel", blacklistList) + label:setText(name) + label.remove.onClick = function() + table.remove(playerList, table.find(playerList, name)) + label:destroy() + end + label.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('Check Player', function() + local name = widget:getText():gsub(" ", "_") + local link = "https://www.gunzodus.net/character/show/" + g_platform.openUrl(link..name) + end, "") + menu:addOption('Copy Name', function() + g_window.setClipboardText(widget:getText()) + end, "") + menu:display(mousePos) + return true + end + end + end + end +end + +onTextMessage(function(mode,text) + if not config.autoAdd then return end + if CaveBot.isOff() or TargetBot.isOff() then return end + if not text:find("Warning! The murder of") then return end + + text = string.split(text, "Warning! The murder of ")[1] + text = string.split(text, " was not justified.")[1] + + addBlackListPlayer(text) +end) + +onCreatureAppear(function(creature) + checkStatus(creature) + end) + +onPlayerPositionChange(function(x,y) + if x.z ~= y.z then + schedule(20, function() + refreshStatus() + end) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.otui new file mode 100644 index 0000000000..a4f0d92a5b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/playerlist.otui @@ -0,0 +1,151 @@ +PlayerLabel < UIWidget + background-color: alpha + text-offset: 3 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('X') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + margin-right: 15 + text-align: center + text-offset: 0 1 + tooltip: Remove profile from the list. + +SettingCheckBox < CheckBox + text-wrap: true + text-auto-resize: true + margin-top: 3 + font: verdana-11px-rounded + +Settings < FlatPanel + padding: 6 + layout: + type: verticalBox + + Label + text: Additional Settings + text-align: center + font: verdana-11px-rounded + + HorizontalSeparator + + SettingCheckBox + id: Members + margin-top: 6 + text: Consider group members as friends. + + SettingCheckBox + id: Outfit + text: Color listed player outfits to red or blue. + + SettingCheckBox + id: NeutralsAreEnemy + text: Consider every non friend player as enemy. + + SettingCheckBox + id: Highlight + text: Hightlight listed players in red or blue color. + + SettingCheckBox + id: AutoAdd + text: Automatically add killed players while cave botting to blacklist. + +tPanel < Panel + margin: 3 + padding: 3 + + TextList + id: list + height: 200 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + vertical-scrollbar: listScrollBar + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + + TextEdit + id: name + anchors.top: list.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + + Button + id: add + text: Add Player + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + font: verdana-11px-rounded + +PlayerListWindow < MainWindow + !text: tr('Player List') + size: 405 356 + @onEscape: self:hide() + + TabBar + id: tmpTabBar + anchors.top: parent.top + anchors.left: parent.left + width: 180 + + FlatPanel + id: tmpTabContent + anchors.top: tmpTabBar.bottom + anchors.left: parent.left + width: 180 + anchors.bottom: separator.top + margin-bottom: 5 + + VerticalSeparator + id: verticalSep + anchors.top: parent.top + anchors.bottom: separator.top + margin-bottom: 5 + anchors.horizontalCenter: parent.horizontalCenter + + Settings + id: settings + anchors.left: prev.right + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: next.top + margin: 3 + margin-left: 6 + margin-bottom: 4 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 + @onClick: self:getParent():hide() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.lua new file mode 100644 index 0000000000..0f63447005 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.lua @@ -0,0 +1,287 @@ +---@diagnostic disable: undefined-global +setDefaultTab("Main") + +local panelName = "pushmax" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('PUSHMAX') + + Button + id: push + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = { + enabled = true, + pushDelay = 1060, + pushMaxRuneId = 3188, + mwallBlockId = 2128, + pushMaxKey = "PageUp" + } +end + +local config = storage[panelName] + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) +config.enabled = not config.enabled +widget:setOn(config.enabled) +end + +ui.push.onClick = function(widget) + pushWindow:show() + pushWindow:raise() + pushWindow:focus() +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + pushWindow = UI.createWindow('PushMaxWindow', rootWidget) + pushWindow:hide() + + pushWindow.closeButton.onClick = function(widget) + pushWindow:hide() + end + + local updateDelayText = function() + pushWindow.delayText:setText("Push Delay: ".. config.pushDelay) + end + updateDelayText() + pushWindow.delay.onValueChange = function(scroll, value) + config.pushDelay = value + updateDelayText() + end + pushWindow.delay:setValue(config.pushDelay) + + pushWindow.runeId.onItemChange = function(widget) + config.pushMaxRuneId = widget:getItemId() + end + pushWindow.runeId:setItemId(config.pushMaxRuneId) + pushWindow.mwallId.onItemChange = function(widget) + config.mwallBlockId = widget:getItemId() + end + pushWindow.mwallId:setItemId(config.mwallBlockId) + + pushWindow.hotkey.onTextChange = function(widget, text) + config.pushMaxKey = text + end + pushWindow.hotkey:setText(config.pushMaxKey) +end + + +-- variables for config +local fieldTable = {2118, 105, 2122} +local cleanTile = nil + +-- scripts + +local targetTile +local pushTarget + +local resetData = function() + for i, tile in pairs(g_map.getTiles(posz())) do + if tile:getText() == "TARGET" or tile:getText() == "DEST" or tile:getText() == "CLEAR" then + tile:setText('') + end + end + pushTarget = nil + targetTile = nil + cleanTile = nil +end + +local getCreatureById = function(id) + for i, spec in ipairs(getSpectators()) do + if spec:getId() == id then + return spec + end + end + return false +end + +local isNotOk = function(t,tile) + local tileItems = {} + + for i, item in pairs(tile:getItems()) do + table.insert(tileItems, item:getId()) + end + for i, field in ipairs(t) do + if table.find(tileItems, field) then + return true + end + end + return false +end + +local isOk = function(a,b) + return getDistanceBetween(a,b) == 1 +end + +-- to mark +local hold = 0 +onKeyDown(function(keys) + if not config.enabled then return end + if keys ~= config.pushMaxKey then return end + hold = now + local tile = getTileUnderCursor() + if not tile then return end + if pushTarget and targetTile then + resetData() + return + end + local creature = tile:getCreatures()[1] + if not pushTarget and creature then + pushTarget = creature + if pushTarget then + tile:setText('TARGET') + pushTarget:setMarked('#00FF00') + end + elseif not targetTile and pushTarget then + if pushTarget and getDistanceBetween(tile:getPosition(),pushTarget:getPosition()) ~= 1 then + resetData() + return + else + tile:setText('DEST') + targetTile = tile + end + end +end) + +-- mark tile to throw anything from it +onKeyPress(function(keys) + if not config.enabled then return end + if keys ~= config.pushMaxKey then return end + local tile = getTileUnderCursor() + if not tile then return end + + if (hold - now) < -2500 then + if cleanTile and tile ~= cleanTile then + resetData() + elseif not cleanTile then + cleanTile = tile + tile:setText("CLEAR") + end + end + hold = 0 +end) + +onCreaturePositionChange(function(creature, newPos, oldPos) + if not config.enabled then return end + if creature == player then + resetData() + end + if not pushTarget or not targetTile then return end + if creature == pushTarget and newPos == targetTile then + resetData() + end +end) + +macro(50, function() + if not config.enabled then return end + + local pushDelay = tonumber(config.pushDelay) + local rune = tonumber(config.pushMaxRuneId) + local customMwall = config.mwallBlockId + + if cleanTile then + local tilePos = cleanTile:getPosition() + local pPos = player:getPosition() + if not isOk(tilePos, pPos) then + resetData() + return + end + + if not cleanTile:hasCreature() then return end + local tiles = getNearTiles(tilePos) + local destTile + local forbidden = {} + -- unfortunately double loop + for i, tile in pairs(tiles) do + local minimapColor = g_map.getMinimapColor(tile:getPosition()) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + if stairs then + table.insert(forbidden, tile:getPosition()) + end + end + for i, tile in pairs(tiles) do + local minimapColor = g_map.getMinimapColor(tile:getPosition()) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + if tile:isWalkable() and not isNotOk(fieldTable, tile) and not tile:hasCreature() and not stairs then + local tooClose = false + if #forbidden ~= 0 then + for i=1,#forbidden do + local pos = forbidden[i] + if isOk(pos, tile:getPosition()) then + tooClose = true + break + end + end + end + if not tooClose then + destTile = tile + break + end + end + end + + if not destTile then return end + local parcel = cleanTile:getCreatures()[1] + if parcel then + test() + g_game.move(parcel,destTile:getPosition()) + delay(2000) + end + else + if not pushTarget or not targetTile then return end + local tilePos = targetTile:getPosition() + local targetPos = pushTarget:getPosition() + if not isOk(tilePos,targetPos) then return end + + local tileOfTarget = g_map.getTile(targetPos) + + if not targetTile:isWalkable() then + local topThing = targetTile:getTopUseThing():getId() + if topThing == 2129 or topThing == 2130 or topThing == customMwall then + if targetTile:getTimer() < pushDelay+500 then + vBot.isUsing = true + schedule(pushDelay+700, function() + vBot.isUsing = false + end) + end + if targetTile:getTimer() > pushDelay then + return + end + else + return resetData() + end + end + + if not tileOfTarget:getTopUseThing():isNotMoveable() and targetTile:getTimer() < pushDelay+500 then + return useWith(rune, pushTarget) + end + if isNotOk(fieldTable, targetTile) then + if targetTile:canShoot() then + return useWith(3148, targetTile:getTopUseThing()) + else + return + end + end + g_game.move(pushTarget,tilePos) + delay(2000) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.otui new file mode 100644 index 0000000000..875a4f8b4f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/pushmax.otui @@ -0,0 +1,85 @@ +PushMaxWindow < MainWindow + !text: tr('Pushmax Settings') + size: 200 240 + @onEscape: self:hide() + + BotLabel + id: delayText + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-align: center + + HorizontalScrollBar + id: delay + anchors.left: delayText.left + anchors.right: delayText.right + anchors.top: delayText.bottom + margin-top: 5 + minimum: 800 + maximum: 2000 + step: 10 + + Label + id: label2 + anchors.top: delay.bottom + anchors.left: parent.horizontalCenter + anchors.right: parent.right + text-align: center + text: Custom WallID + margin-top: 5 + + Label + id: label3 + anchors.top: delay.bottom + anchors.right: parent.horizontalCenter + anchors.left: parent.left + text-align: center + text: VS AntiPush + margin-top: 5 + + BotItem + id: runeId + anchors.horizontalCenter: label3.horizontalCenter + anchors.top: label3.bottom + margin-top: 5 + + BotItem + id: mwallId + anchors.horizontalCenter: label2.horizontalCenter + anchors.top: label2.bottom + margin-top: 5 + + Label + id: label1 + anchors.top: mwallId.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 10 + text-align: center + text: Hotkey for PUSHMAX + + TextEdit + id: hotkey + anchors.left: parent.left + anchors.right: parent.right + anchors.top: label1.bottom + margin-top: 5 + text-align: center + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/quiver_label.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/quiver_label.lua new file mode 100644 index 0000000000..671d726831 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/quiver_label.lua @@ -0,0 +1,58 @@ +local quiverSlot = modules.game_inventory.inventoryWindow:recursiveGetChildById('slot5') +local label = quiverSlot.count + +label = label or g_ui.loadUIFromString([[ +Label + id: count + color: #bfbfbf + font: verdana-11px-rounded + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + text-align: right + margin-right: 3 + margin-left: 3 + text: +]], quiverSlot) + + +function getQuiverAmount() + -- old tibia + if g_game.getClientVersion() < 1000 then return end + + + local isQuiverEquipped = getRight() and getRight():isContainer() or false + local quiver = isQuiverEquipped and getContainerByItem(getRight():getId()) + local count = 0 + + if quiver then + for i, item in ipairs(quiver:getItems()) do + count = count + item:getCount() + end + else + return label:setText("") + end + + return label:setText(count) +end +getQuiverAmount() + +onContainerOpen(function(container, previousContainer) + getQuiverAmount() +end) + +onContainerClose(function(container) + getQuiverAmount() +end) + +onAddItem(function(container, slot, item, oldItem) + getQuiverAmount() +end) + +onRemoveItem(function(container, slot, item) + getQuiverAmount() +end) + +onContainerUpdateItem(function(container, slot, item, oldItem) + getQuiverAmount() +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/quiver_manager.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/quiver_manager.lua new file mode 100644 index 0000000000..368bd8cda6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/quiver_manager.lua @@ -0,0 +1,91 @@ +if voc() == 2 or voc() == 12 then + local bows = { 3350, 31581, 27455, 8027, 20082, 36664, 7438, 28718, 36665, 14246, 19362, 35518, 34150, 29417, 9378, 16164, 22866, 12733, 8029, 20083, 20084, 8026, 8028, 34088} + local xbows = { 30393, 3349, 27456, 20085, 16163, 5947, 8021, 14247, 22867, 8023, 22711, 19356, 20086, 20087, 34089} + local arrows = { 16143, 763, 761, 7365, 3448, 762, 21470, 7364, 14251, 3447, 3449, 15793, 25757, 774, 35901 } + local bolts = { 6528, 7363, 3450, 16141, 25758, 14252, 3446, 16142, 35902 } + local hold = false + + onContainerOpen(function(container, previousContainer) + hold = false + end) + + onContainerClose(function(container) + hold = false + end) + + onAddItem(function(container, slot, item, oldItem) + hold = false + end) + + onRemoveItem(function(container, slot, item) + hold = false + end) + + onContainerUpdateItem(function(container, slot, item, oldItem) + hold = false + end) + + + + local function manageQuiver(isBowEquipped, quiverContainer) + local ammo = isBowEquipped and arrows or bolts + local dest = nil + local containers = getContainers() + for i, container in ipairs(containers) do + if container ~= quiverContainer and not containerIsFull(container) then + local cname = container:getName():lower() + if not cname:find("loot") and (cname:find("backpack") or cname:find("bag") or cname:find("chess")) then + dest = container + end + end + end + + -- clearing + if dest then + for i, item in ipairs(quiverContainer:getItems()) do + if not table.find(ammo, item:getId()) then + local pos = dest:getSlotPosition(dest:getItemsCount()) + return g_game.move(item, pos, item:getCount()) + end + end + end + + if not containerIsFull(quiverContainer) then + for i, container in ipairs(containers) do + if container ~= quiverContainer then + for j, item in ipairs(container:getItems()) do + if table.find(ammo, item:getId()) then + local pos = quiverContainer:getSlotPosition(quiverContainer:getItemsCount()) + return g_game.move(item, pos, item:getCount()) + end + end + end + end + end + return true + end + + UI.Separator() + macro(100, "Quiver Manager", function() + if hold then return end -- do nothing if nothing to do + local hand = getLeft() and getLeft():getId() + local quiverEquipped = getRight() and getRight():isContainer() + + if not hand then return end + if not quiverEquipped then return end + + local quiverContainer = getContainerByItem(getRight():getId()) + if not quiverContainer then return end + + local isBowEquipped = getLeft() and table.find(bows, hand) and true or false + if not isBowEquipped then + if not table.find(xbows, hand) then + return -- neither bow and xbow is equipped + end + end + + if manageQuiver(isBowEquipped, quiverContainer) then -- if true then it didn't do anything + hold = true + end + end) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/siolist.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/siolist.otui new file mode 100644 index 0000000000..e9920753fa --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/siolist.otui @@ -0,0 +1,192 @@ +VocationPanel < Panel + padding: 3 + image-source: /images/ui/panel_flat + image-border: 6 + size: 190 55 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-align: center + text: for BotServer, Heal only: + + BotSwitch + id: ED + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.horizontalCenter + text: Druids + + BotSwitch + id: MS + anchors.bottom: parent.bottom + anchors.left: parent.horizontalCenter + anchors.right: parent.right + text: Sorcerers + + BotSwitch + id: EK + anchors.bottom: ED.top + anchors.left: parent.left + anchors.right: parent.horizontalCenter + text: Knights + + BotSwitch + id: RP + anchors.bottom: ED.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + text: Paladins + + + +SioListWindow < MainWindow + !text: tr('Healer Options') + size: 220 360 + @onEscape: self:hide() + + BotSwitch + id: exuraSio + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.horizontalCenter + text: Exura Sio + margin-right: 2 + + BotSwitch + id: exuraGranSio + anchors.top: parent.top + anchors.left: prev.right + anchors.right: parent.right + text: Exura Gran Sio + margin-left: 2 + + BotSwitch + id: exuraMasRes + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text: Exura Gran Mas Res + margin-top: 3 + + BotSwitch + id: spell + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text: Custom Spell + margin-top: 3 + text-align: center + + BotTextEdit + id: spellName + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 10 + + BotItem + id: itemId + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 10 + + BotSwitch + id: item + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + anchors.bottom: prev.verticalCenter + text-align: center + text: Item Healing + margin-left: 2 + + BotLabel + id: distText + anchors.top: itemId.verticalCenter + anchors.left: itemId.right + anchors.right: parent.right + anchors.bottom: itemId.bottom + text-align: center + text: Max Distance + + HorizontalScrollBar + id: Distance + anchors.left: parent.left + anchors.top: itemId.bottom + anchors.right: parent.right + margin-top: 3 + minimum: 1 + maximum: 10 + step: 1 + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 8 + + BotLabel + id: manaInfo + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-align: center + margin-top: 5 + + HorizontalScrollBar + id: minMana + anchors.left: spellName.left + anchors.right: spellName.right + anchors.top: manaInfo.bottom + margin-top: 2 + minimum: 1 + maximum: 100 + step: 1 + + BotLabel + id: friendHp + anchors.left: spellName.left + anchors.right: spellName.right + anchors.top: prev.bottom + text-align: center + margin-top: 5 + + HorizontalScrollBar + id: minFriendHp + anchors.left: spellName.left + anchors.right: spellName.right + anchors.top: friendHp.bottom + margin-top: 2 + minimum: 1 + maximum: 100 + step: 1 + + VocationPanel + id: vocation + anchors.top: prev.bottom + margin-top: 6 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/spy_level.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/spy_level.lua new file mode 100644 index 0000000000..f225d7699d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/spy_level.lua @@ -0,0 +1,24 @@ +-- config + +local keyUp = "=" +local keyDown = "-" +setDefaultTab("Tools") + +-- script + +local lockedLevel = pos().z + +onPlayerPositionChange(function(newPos, oldPos) + lockedLevel = pos().z + modules.game_interface.getMapPanel():unlockVisibleFloor() +end) + +onKeyPress(function(keys) + if keys == keyDown then + lockedLevel = lockedLevel + 1 + modules.game_interface.getMapPanel():lockVisibleFloor(lockedLevel) + elseif keys == keyUp then + lockedLevel = lockedLevel - 1 + modules.game_interface.getMapPanel():lockVisibleFloor(lockedLevel) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/supplies.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/supplies.lua new file mode 100644 index 0000000000..d5fbd4d3ef --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/supplies.lua @@ -0,0 +1,473 @@ +setDefaultTab("Cave") +local panelName = "supplies" +if not SuppliesConfig[panelName] or SuppliesConfig[panelName].item1 then + SuppliesConfig[panelName] = { + currentProfile = "Default", + ["Default"] = {} + } +end + +local function convertOldConfig(config) + if config and config.items then + return config + end -- config is new + + local newConfig = { + items = {}, + capSwitch = config.capSwitch, + SoftBoots = config.SoftBoots, + imbues = config.imbues, + staminaSwitch = config.staminaSwitch, + capValue = config.capValue, + staminaValue = config.staminaValue + } + + local items = { + config.item1, + config.item2, + config.item3, + config.item4, + config.item5, + config.item6 + } + local mins = { + config.item1Min, + config.item2Min, + config.item3Min, + config.item4Min, + config.item5Min, + config.item6Min + } + local maxes = { + config.item1Max, + config.item2Max, + config.item3Max, + config.item4Max, + config.item5Max, + config.item6Max + } + + for i, item in ipairs(items) do + if item > 100 then + local min = mins[i] + local max = maxes[i] + newConfig.items[tostring(item)] = { + min = min, + max = max, + avg = 0 + } + end + end + + return newConfig +end + +-- convert old configs +for k, profile in pairs(SuppliesConfig[panelName]) do + if type(profile) == 'table' then + SuppliesConfig[panelName][k] = convertOldConfig(profile) + end +end + +local currentProfile = SuppliesConfig[panelName].currentProfile +local config = SuppliesConfig[panelName][currentProfile] + +vBotConfigSave("supply") + +if not config then + for k, v in pairs(SuppliesConfig[panelName]) do + if type(v) == "table" then + SuppliesConfig[panelName].currentProfile = k + config = SuppliesConfig[panelName][k] + break + end + end +end + +function getEmptyItemPanels() + local panel = SuppliesWindow.items + local count = 0 + + for i, child in ipairs(panel:getChildren()) do + count = child:getId() == "blank" and count + 1 or count + end + + return count +end + +function deleteFirstEmptyPanel() + local panel = SuppliesWindow.items + + for i, child in ipairs(panel:getChildren()) do + if child:getId() == "blank" then + child:destroy() + break + end + end +end + +function clearEmptyPanels() + local panel = SuppliesWindow.items + + if panel:getChildCount() > 1 then + if getEmptyItemPanels() > 1 then + deleteFirstEmptyPanel() + end + end +end + +function addItemPanel() + local parent = SuppliesWindow.items + local childs = parent:getChildCount() + local panel = UI.createWidget("ItemPanel", parent) + local item = panel.id + local min = panel.min + local max = panel.max + local avg = panel.avg + + panel:setId("blank") + item:setShowCount(false) + + item.onItemChange = function(widget) + local id = widget:getItemId() + local panelId = panel:getId() + + -- empty, verify + if id < 100 then + config.items[panelId] = nil + panel:setId("blank") + clearEmptyPanels() -- clear empty panels if any + return + end + + -- itemId was not changed, ignore + if tonumber(panelId) == id then + return + end + + -- check if isnt already added + if config[tostring(id)] then + warn("vBot[Drop Tracker]: Item already added!") + widget:setItemId(0) + return + end + + -- new item id + config.items[tostring(id)] = config.items[tostring(id)] or {} -- min, max, avg + panel:setId(id) + addItemPanel() -- add new panel + end + + return panel +end + +SuppliesWindow = UI.createWindow("SuppliesWindow") +SuppliesWindow:hide() + +UI.Button( + "Supply Settings", + function() + SuppliesWindow:setVisible(not SuppliesWindow:isVisible()) + end +) + +-- load settings +local function loadSettings() + -- panels + SuppliesWindow.items:destroyChildren() + + for id, data in pairs(config.items) do + local widget = addItemPanel() + widget:setId(id) + widget.id:setItemId(tonumber(id)) + widget.min:setText(data.min) + widget.max:setText(data.max) + widget.avg:setText(data.avg) + end + addItemPanel() -- add empty panel + + -- switches and values + SuppliesWindow.capSwitch:setOn(config.capSwitch) + SuppliesWindow.SoftBoots:setOn(config.SoftBoots) + SuppliesWindow.imbues:setOn(config.imbues) + SuppliesWindow.staminaSwitch:setOn(config.staminaSwitch) + SuppliesWindow.capValue:setText(config.capValue or 0) + SuppliesWindow.staminaValue:setText(config.staminaValue or 0) +end +loadSettings() + +-- save settings +SuppliesWindow.onVisibilityChange = function(widget, visible) + if not visible then + local currentProfile = SuppliesConfig[panelName].currentProfile + SuppliesConfig[panelName][currentProfile].items = {} + local parent = SuppliesWindow.items + + -- items + for i, panel in ipairs(parent:getChildren()) do + if panel.id:getItemId() > 100 then + local id = tostring(panel.id:getItemId()) + local min = panel.min:getValue() + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + SuppliesConfig[panelName][currentProfile].items[id] = { + min = min, + max = max, + avg = avg + } + end + end + + vBotConfigSave("supply") + end +end + +local function refreshProfileList() + local profiles = SuppliesConfig[panelName] + + SuppliesWindow.profiles:destroyChildren() + for k, v in pairs(profiles) do + if type(v) == "table" then + local label = UI.createWidget("ProfileLabel", SuppliesWindow.profiles) + label:setText(k) + label:setTooltip("Click to load this profile. \nDouble click to change the name.") + label.remove.onClick = function() + local childs = SuppliesWindow.profiles:getChildCount() + if childs == 1 then + return info("vBot[Supplies] You need at least one profile!") + end + profiles[k] = nil + label:destroy() + vBotConfigSave("supply") + end + label.onDoubleClick = function(widget) + local window = + modules.client_textedit.show( + widget, + {title = "Set Profile Name", description = "Enter a new name for selected profile"} + ) + schedule( + 50, + function() + window:raise() + window:focus() + end + ) + end + label.onClick = function() + SuppliesConfig[panelName].currentProfile = label:getText() + config = SuppliesConfig[panelName][label:getText()] + loadSettings() + vBotConfigSave("supply") + end + label.onTextChange = function(widget, text) + currentProfile = text + SuppliesConfig[panelName].currentProfile = text + profiles[text] = profiles[k] + profiles[k] = nil + vBotConfigSave("supply") + end + end + end +end +refreshProfileList() + +local function setProfileFocus() + for i, v in ipairs(SuppliesWindow.profiles:getChildren()) do + local name = v:getText() + if name == SuppliesConfig[panelName].currentProfile then + return v:focus() + end + end +end +setProfileFocus() + +SuppliesWindow.newProfile.onClick = function() + local n = SuppliesWindow.profiles:getChildCount() + if n > 6 then + return info("vBot[Supplies] - max profile count reached!") + end + local name = "Profile #" .. n + 1 + SuppliesConfig[panelName][name] = {items = {}} + refreshProfileList() + setProfileFocus() + vBotConfigSave("supply") +end + +SuppliesWindow.capSwitch.onClick = function(widget) + config.capSwitch = not config.capSwitch + widget:setOn(config.capSwitch) +end + +SuppliesWindow.SoftBoots.onClick = function(widget) + config.SoftBoots = not config.SoftBoots + widget:setOn(config.SoftBoots) +end + +SuppliesWindow.imbues.onClick = function(widget) + config.imbues = not config.imbues + widget:setOn(config.imbues) +end + +SuppliesWindow.staminaSwitch.onClick = function(widget) + config.staminaSwitch = not config.staminaSwitch + widget:setOn(config.staminaSwitch) +end + +SuppliesWindow.capValue.onTextChange = function(widget, text) + local value = tonumber(SuppliesWindow.capValue:getText()) + if not value then + SuppliesWindow.capValue:setText(0) + config.capValue = 0 + else + text = text:match("0*(%d+)") + config.capValue = text + end +end + +SuppliesWindow.staminaValue.onTextChange = function(widget, text) + local value = tonumber(SuppliesWindow.staminaValue:getText()) + if not value then + SuppliesWindow.staminaValue:setText(0) + config.staminaValue = 0 + else + text = text:match("0*(%d+)") + config.staminaValue = text + end +end + +SuppliesWindow.increment.onClick = function(widget) + for i, panel in ipairs(SuppliesWindow.items:getChildren()) do + if panel.id:getItemId() > 100 then + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + if avg > 0 then + panel.max:setText(max + avg) + end + end + end +end + +SuppliesWindow.decrement.onClick = function(widget) + for i, panel in ipairs(SuppliesWindow.items:getChildren()) do + if panel.id:getItemId() > 100 then + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + if avg > 0 then + panel.max:setText(math.max(0, max - avg)) -- dont go below 0 + end + end + end +end + +SuppliesWindow.increment.onMouseWheel = function(widget, mousePos, dir) + if dir == 1 then + SuppliesWindow.increment.onClick() + elseif dir == 2 then + SuppliesWindow.decrement.onClick() + end +end + +SuppliesWindow.decrement.onMouseWheel = SuppliesWindow.increment.onMouseWheel + +Supplies = {} -- public functions +Supplies.show = function() + SuppliesWindow:show() + SuppliesWindow:raise() + SuppliesWindow:focus() +end + +Supplies.getItemsData = function() + local t = {} + -- items + for i, panel in ipairs(SuppliesWindow.items:getChildren()) do + if panel.id:getItemId() > 100 then + local id = tostring(panel.id:getItemId()) + local min = panel.min:getValue() + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + t[id] = { + min = min, + max = max, + avg = avg + } + end + end + + return t +end + +Supplies.isSupplyItem = function(id) + local data = Supplies.getItemsData() + id = tostring(id) + + if data[id] then + return data[id] + else + return false + end +end + +Supplies.hasEnough = function() + local data = Supplies.getItemsData() + + for id, values in pairs(data) do + id = tonumber(id) + local minimum = values.min + local current = player:getItemsCount(id) or 0 + + if current < minimum then + return {id=id, amount=current} + end + end + + return true +end + +hasSupplies = Supplies.hasEnough + +Supplies.setAverageValues = function(data) + for id, amount in pairs(data) do + local widget = SuppliesWindow.items[id] + + if widget then + widget.avg:setText(amount) + end + end +end + +Supplies.addSupplyItem = function(id, min, max, avg) + if not id then + return + end + + local widget = addItemPanel() + widget:setId(id) + widget.id:setItemId(tonumber(id)) + widget.min:setText(min or 0) + widget.max:setText(max or 0) + widget.avg:setText(avg or 0) +end + +Supplies.getAdditionalData = function() + local data = { + stamina = {enabled = config.staminaSwitch, value = config.staminaValue}, + capacity = {enabled = config.capSwitch, value = config.capValue}, + softBoots = {enabled = config.SoftBoots}, + imbues = {enabled = config.imbues} + } + return data +end + +Supplies.getFullData = function() + local data = { + items = Supplies.getItemsData(), + additional = Supplies.getAdditionalData() + } + + return data +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/supplies.otui b/modules/game_bot/default_configs/vBot_4.7/vBot/supplies.otui new file mode 100644 index 0000000000..9576c88bec --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/supplies.otui @@ -0,0 +1,244 @@ +ProfileLabel < UIWidget + background-color: alpha + text-offset: 3 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('X') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + margin-right: 3 + text-align: center + text-offset: 0 1 + tooltip: Remove profile from the list. + +SupplySpinBox < SpinBox + height: 20 + margin-left: 3 + width: 75 + minimum: 0 + maximum: 9999 + text-align: center + focusable: true + text: 0 + +ItemPanel < Panel + height: 38 + + BotItem + id: id + anchors.left: parent.left + anchors.bottom: parent.bottom + + SupplySpinBox + id: min + anchors.left: prev.right + anchors.bottom: parent.bottom + + SupplySpinBox + id: max + anchors.left: prev.right + anchors.bottom: parent.bottom + + SupplySpinBox + id: avg + anchors.left: prev.right + anchors.bottom: parent.bottom + width: 50 + + UIWidget + anchors.left: min.left + anchors.bottom: min.top + width: 75 + text-align: center + font: verdana-11px-rounded + text: Min + tooltip: Amount of given supplies for bot to leave the spawn. + + UIWidget + anchors.left: max.left + anchors.bottom: max.top + width: 75 + text-align: center + font: verdana-11px-rounded + text: Max + tooltip: Amount of given supplies to purchase + + UIWidget + anchors.left: avg.left + anchors.bottom: avg.top + width: 55 + text-align: center + font: verdana-11px-rounded + text: AVG + !tooltip: ("This is average consumption of supplies by round to help calculate the amount to purchase\n (info provided by CaveBot Stats)") + +SuppliesWindow < MainWindow + !text: tr('Supplies') + size: 430 330 + @onEscape: self:hide() + + VerticalSeparator + id: sep + anchors.top: parent.top + anchors.right: parent.right + margin-right: 140 + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-left: 10 + visible: false + + Label + anchors.left: sep.right + anchors.right: parent.right + anchors.top: parent.top + margin-left: 10 + margin-top: 3 + text-align: center + text: Additional Conditions: + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: prev.right + margin-top: 3 + + BotSwitch + id: SoftBoots + anchors.top: prev.bottom + anchors.left: sep.right + anchors.right: parent.right + margin-top: 5 + margin-left: 10 + text: No Soft + tooltip: Go refill if there's no more active soft boots. + + BotSwitch + id: capSwitch + height: 20 + anchors.left: SoftBoots.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-right: 50 + text-align: center + text: Cap Below: + tooltip: Go refill if capacity is below set value. + + BotTextEdit + id: capValue + size: 40 20 + anchors.left: prev.right + anchors.right: parent.right + anchors.top: prev.top + margin-left: 5 + + BotSwitch + id: staminaSwitch + height: 20 + anchors.left: SoftBoots.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-right: 50 + text-align: center + text: Stamina: + tooltip: Go refill if stamina is below set value. (in minutes) + + BotTextEdit + id: staminaValue + size: 40 20 + anchors.left: prev.right + anchors.right: parent.right + anchors.top: prev.top + margin-left: 5 + + BotSwitch + id: imbues + anchors.top: prev.bottom + anchors.left: sep.right + anchors.right: parent.right + margin-top: 5 + margin-left: 10 + text: No Imbues + tooltip: Go refill when mana leech imbue has worn off. + + TextList + id: profiles + anchors.top: prev.bottom + margin-top: 5 + anchors.left: prev.left + anchors.right: prev.right + anchors.bottom: bottomSep.top + margin-bottom: 25 + + BotButton + id: newProfile + anchors.left: prev.left + anchors.top: prev.bottom + size: 35 15 + text: New + font: cipsoftFont + tooltip: Create new supplies profile. + + VerticalScrollBar + id: itemsScrollBar + anchors.top: items.top + anchors.bottom: items.bottom + anchors.right: items.right + step: 14 + pixels-scroll: true + + ScrollablePanel + id: items + anchors.top: parent.top + anchors.left: parent.left + anchors.right: sep.left + anchors.bottom: bottomSep.top + margin-bottom: 8 + vertical-scrollbar: itemsScrollBar + layout: verticalBox + + HorizontalSeparator + id: bottomSep + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + tooltip: Close supplies window and save settings. + @onClick: self:getParent():hide() + + Button + id: increment + anchors.verticalCenter: prev.verticalCenter + anchors.right: items.right + text: + + width: 50 + tooltip: increase all max supplies amount by average + + Button + id: decrement + anchors.verticalCenter: prev.verticalCenter + anchors.right: prev.left + margin-right: 3 + text: - + width: 50 + tooltip: decrease all max supplies amount by average \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/tools.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/tools.lua new file mode 100644 index 0000000000..6105692f9d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/tools.lua @@ -0,0 +1,46 @@ +-- tools tab +setDefaultTab("Tools") + +if type(storage.moneyItems) ~= "table" then + storage.moneyItems = {3031, 3035} +end +macro(1000, "Exchange money", function() + if not storage.moneyItems[1] then return end + 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(storage.moneyItems) do + if item:getId() == moneyId.id then + return g_game.use(item) + end + end + end + end + end + end +end) + +local moneyContainer = UI.Container(function(widget, items) + storage.moneyItems = items +end, true) +moneyContainer:setHeight(35) +moneyContainer:setItems(storage.moneyItems) + +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() diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/version.txt b/modules/game_bot/default_configs/vBot_4.7/vBot/version.txt new file mode 100644 index 0000000000..4b65d9df80 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/version.txt @@ -0,0 +1 @@ +4.7 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/vlib.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/vlib.lua new file mode 100644 index 0000000000..de5d7c3072 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/vlib.lua @@ -0,0 +1,1173 @@ +-- Author: Vithrax +-- contains mostly basic function shortcuts and code shorteners + +-- initial global variables declaration +vBot = {} -- global namespace for bot variables +vBot.BotServerMembers = {} +vBot.standTime = now +vBot.isUsingPotion = false +vBot.isUsing = false +vBot.customCooldowns = {} + +function logInfo(text) + local timestamp = os.date("%H:%M:%S") + text = tostring(text) + local start = timestamp.." [vBot]" + + return modules.client_terminal.addLine(start..text, "orange") +end + +-- scripts / functions +onPlayerPositionChange(function(x,y) + vBot.standTime = now +end) + +function standTime() + return now - vBot.standTime +end + +function relogOnCharacter(charName) + local characters = g_ui.getRootWidget().charactersWindow.characters + for index, child in ipairs(characters:getChildren()) do + local name = child:getChildren()[1]:getText() + + if name:lower():find(charName:lower()) then + child:focus() + schedule(100, modules.client_entergame.CharacterList.doLogin) + end + end +end + +function castSpell(text) + if canCast(text) then + say(text) + end +end + +local dmgTable = {} +local lastDmgMessage = now +onTextMessage(function(mode, text) + if not text:lower():find("you lose") or not text:lower():find("due to") then + return + end + local dmg = string.match(text, "%d+") + if #dmgTable > 0 then + for k, v in ipairs(dmgTable) do + if now - v.t > 3000 then table.remove(dmgTable, k) end + end + end + lastDmgMessage = now + table.insert(dmgTable, {d = dmg, t = now}) + schedule(3050, function() + if now - lastDmgMessage > 3000 then dmgTable = {} end + end) +end) + +-- based on data collected by callback calculates per second damage +-- returns number +function burstDamageValue() + local d = 0 + local time = 0 + if #dmgTable > 1 then + for i, v in ipairs(dmgTable) do + if i == 1 then time = v.t end + d = d + v.d + end + end + return math.ceil(d / ((now - time) / 1000)) +end + +-- simplified function from modules +-- displays string as white colour message +function whiteInfoMessage(text) + return modules.game_textmessage.displayGameMessage(text) +end + +function statusMessage(text, logInConsole) + return not logInConsole and modules.game_textmessage.displayFailureMessage(text) or modules.game_textmessage.displayStatusMessage(text) +end + +-- same as above but red message +function broadcastMessage(text) + return modules.game_textmessage.displayBroadcastMessage(text) +end + +-- almost every talk action inside cavebot has to be done by using schedule +-- therefore this is simplified function that doesn't require to build a body for schedule function +function scheduleNpcSay(text, delay) + if not text or not delay then return false end + + return schedule(delay, function() NPC.say(text) end) +end + +-- returns first number in string, already formatted as number +-- returns number or nil +function getFirstNumberInText(text) + local n = nil + if string.match(text, "%d+") then n = tonumber(string.match(text, "%d+")) end + return n +end + +-- function to search if item of given ID can be found on certain tile +-- first argument is always ID +-- the rest of aguments can be: +-- - tile +-- - position +-- - or x,y,z coordinates as p1, p2 and p3 +-- returns boolean +function isOnTile(id, p1, p2, p3) + if not id then return end + local tile + if type(p1) == "table" then + tile = g_map.getTile(p1) + elseif type(p1) ~= "number" then + tile = p1 + else + local p = getPos(p1, p2, p3) + tile = g_map.getTile(p) + end + if not tile then return end + + local item = false + if #tile:getItems() ~= 0 then + for i, v in ipairs(tile:getItems()) do + if v:getId() == id then item = true end + end + else + return false + end + + return item +end + +-- position is a special table, impossible to compare with normal one +-- this is translator from x,y,z to proper position value +-- returns position table +function getPos(x, y, z) + if not x or not y or not z then return nil end + local pos = pos() + pos.x = x + pos.y = y + pos.z = z + + return pos +end + +-- opens purse... that's it +function openPurse() + return g_game.use(g_game.getLocalPlayer():getInventoryItem( + InventorySlotPurse)) +end + +-- check's whether container is full +-- c has to be container object +-- returns boolean +function containerIsFull(c) + if not c then return false end + + if c:getCapacity() > #c:getItems() then + return false + else + return true + end + +end + +function dropItem(idOrObject) + if type(idOrObject) == "number" then + idOrObject = findItem(idOrObject) + end + + g_game.move(idOrObject, pos(), idOrObject:getCount()) +end + +-- not perfect function to return whether character has utito tempo buff +-- known to be bugged if received debuff (ie. roshamuul) +-- TODO: simply a better version +-- returns boolean +function isBuffed() + local var = false + if not hasPartyBuff() then return var end + + local skillId = 0 + for i = 1, 4 do + if player:getSkillBaseLevel(i) > player:getSkillBaseLevel(skillId) then + skillId = i + end + end + + local premium = (player:getSkillLevel(skillId) - player:getSkillBaseLevel(skillId)) + local base = player:getSkillBaseLevel(skillId) + if (premium / 100) * 305 > base then + var = true + end + return var +end + +-- if using index as table element, this can be used to properly assign new idex to all values +-- table needs to contain "index" as value +-- if no index in tables, it will create one +function reindexTable(t) + if not t or type(t) ~= "table" then return end + + local i = 0 + for _, e in pairs(t) do + i = i + 1 + e.index = i + end +end + +-- supports only new tibia, ver 10+ +-- returns how many kills left to get next skull - can be red skull, can be black skull! +-- reutrns number +function killsToRs() + return math.min(g_game.getUnjustifiedPoints().killsDayRemaining, + g_game.getUnjustifiedPoints().killsWeekRemaining, + g_game.getUnjustifiedPoints().killsMonthRemaining) +end + +-- calculates exhaust for potions based on "Aaaah..." message +-- changes state of vBot variable, can be used in other scripts +-- already used in pushmax, healbot, etc + +onTalk(function(name, level, mode, text, channelId, pos) + if name ~= player:getName() then return end + if mode ~= 34 then return end + + if text == "Aaaah..." then + vBot.isUsingPotion = true + schedule(950, function() vBot.isUsingPotion = false end) + end +end) + +-- [[ canCast and cast functions ]] -- +-- callback connected to cast and canCast function +-- detects if a given spell was in fact casted based on player's text messages +-- Cast text and message text must match +-- checks only spells inserted in SpellCastTable by function cast +SpellCastTable = {} +onTalk(function(name, level, mode, text, channelId, pos) + if name ~= player:getName() then return end + text = text:lower() + + if SpellCastTable[text] then SpellCastTable[text].t = now end +end) + +-- if delay is nil or delay is lower than 100 then this function will act as a normal say function +-- checks or adds a spell to SpellCastTable and updates cast time if exist +function cast(text, delay) + text = text:lower() + if type(text) ~= "string" then return end + if not delay or delay < 100 then + return say(text) -- if not added delay or delay is really low then just treat it like casual say + end + if not SpellCastTable[text] or SpellCastTable[text].d ~= delay then + SpellCastTable[text] = {t = now - delay, d = delay} + return say(text) + end + local lastCast = SpellCastTable[text].t + local spellDelay = SpellCastTable[text].d + if now - lastCast > spellDelay then return say(text) end +end + +-- canCast is a base for AttackBot and HealBot +-- checks if spell is ready to be casted again +-- ignoreRL - if true, aparat from cooldown will also check conditions inside gamelib SpellInfo table +-- ignoreCd - it true, will ignore cooldown +-- returns boolean +local Spells = modules.gamelib.SpellInfo['Default'] +function canCast(spell, ignoreRL, ignoreCd) + if type(spell) ~= "string" then return end + spell = spell:lower() + if SpellCastTable[spell] then + if now - SpellCastTable[spell].t > SpellCastTable[spell].d or ignoreCd then + return true + else + return false + end + end + if getSpellData(spell) then + if (ignoreCd or not getSpellCoolDown(spell)) and + (ignoreRL or level() >= getSpellData(spell).level and mana() >= + getSpellData(spell).mana) then + return true + else + return false + end + end + -- if no data nor spell table then return true + return true +end + +local lastPhrase = "" +onTalk(function(name, level, mode, text, channelId, pos) + if name == player:getName() then + lastPhrase = text:lower() + end +end) + +if onSpellCooldown and onGroupSpellCooldown then + onSpellCooldown(function(iconId, duration) + schedule(1, function() + if not vBot.customCooldowns[lastPhrase] then + vBot.customCooldowns[lastPhrase] = {id = iconId} + end + end) + end) + + onGroupSpellCooldown(function(iconId, duration) + schedule(2, function() + if vBot.customCooldowns[lastPhrase] then + vBot.customCooldowns[lastPhrase] = {id = vBot.customCooldowns[lastPhrase].id, group = {[iconId] = duration}} + end + end) + end) +else + warn("Outdated OTClient! update to newest version to take benefits from all scripts!") +end + +-- exctracts data about spell from gamelib SpellInfo table +-- returns table +-- ie:['Spell Name'] = {id, words, exhaustion, premium, type, icon, mana, level, soul, group, vocations} +-- cooldown detection module +function getSpellData(spell) + if not spell then return false end + spell = spell:lower() + local t = nil + local c = nil + for k, v in pairs(Spells) do + if v.words == spell then + t = k + break + end + end + if not t then + for k, v in pairs(vBot.customCooldowns) do + if k == spell then + c = {id = v.id, mana = 1, level = 1, group = v.group} + break + end + end + end + if t then + return Spells[t] + elseif c then + return c + else + return false + end +end + +-- based on info extracted by getSpellData checks if spell is on cooldown +-- returns boolean +function getSpellCoolDown(text) + if not text then return nil end + text = text:lower() + local data = getSpellData(text) + if not data then return false end + local icon = modules.game_cooldown.isCooldownIconActive(data.id) + local group = false + for groupId, duration in pairs(data.group) do + if modules.game_cooldown.isGroupCooldownIconActive(groupId) then + group = true + break + end + end + if icon or group then + return true + else + return false + end +end + +-- global var to indicate that player is trying to do something +-- prevents action blocking by scripts +-- below callbacks are triggers to changing the var state +local isUsingTime = now +macro(100, function() + vBot.isUsing = now < isUsingTime and true or false +end) +onUse(function(pos, itemId, stackPos, subType) + if pos.x > 65000 then return end + if getDistanceBetween(player:getPosition(), pos) > 1 then return end + local tile = g_map.getTile(pos) + if not tile then return end + + local topThing = tile:getTopUseThing() + if topThing:isContainer() then return end + + isUsingTime = now + 1000 +end) +onUseWith(function(pos, itemId, target, subType) + if pos.x < 65000 then isUsingTime = now + 1000 end +end) + +-- returns first word in string +function string.starts(String, Start) + return string.sub(String, 1, string.len(Start)) == Start +end + +-- global tables for cached players to prevent unnecesary resource consumption +-- probably still can be improved, TODO in future +-- c can be creature or string +-- if exected then adds name or name and creature to tables +-- returns boolean +CachedFriends = {} +CachedEnemies = {} +function isFriend(c) + local name = c + if type(c) ~= "string" then + if c == player then return true end + name = c:getName() + end + + if CachedFriends[c] then return true end + if CachedEnemies[c] then return false end + + if table.find(storage.playerList.friendList, name) then + CachedFriends[c] = true + return true + elseif vBot.BotServerMembers[name] ~= nil then + CachedFriends[c] = true + return true + elseif storage.playerList.groupMembers then + local p = c + if type(c) == "string" then p = getCreatureByName(c, true) end + if not p then return false end + if p:isLocalPlayer() then return true end + if p:isPlayer() then + if p:isPartyMember() then + CachedFriends[c] = true + CachedFriends[p] = true + return true + end + end + else + return false + end +end + +-- similar to isFriend but lighter version +-- accepts only name string +-- returns boolean +function isEnemy(c) + local name = c + local p + if type(c) ~= "string" then + if c == player then return false end + name = c:getName() + p = c + end + if not name then return false end + if not p then + p = getCreatureByName(name, true) + end + if not p then return end + if p:isLocalPlayer() then return end + + if p:isPlayer() and table.find(storage.playerList.enemyList, name) or + (storage.playerList.marks and not isFriend(name)) or p:getEmblem() == 2 then + return true + else + return false + end +end + +function getPlayerDistribution() + local friends = {} + local neutrals = {} + local enemies = {} + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and not spec:isLocalPlayer() then + if isFriend(spec) then + table.insert(friends, spec) + elseif isEnemy(spec) then + table.insert(enemies, spec) + else + table.insert(neutrals, spec) + end + end + end + + return friends, neutrals, enemies +end + +function getFriends() + local friends, neutrals, enemies = getPlayerDistribution() + + return friends +end + +function getNeutrals() + local friends, neutrals, enemies = getPlayerDistribution() + + return neutrals +end + +function getEnemies() + local friends, neutrals, enemies = getPlayerDistribution() + + return enemies +end + +-- based on first word in string detects if text is a offensive spell +-- returns boolean +function isAttSpell(expr) + if string.starts(expr, "exori") or string.starts(expr, "exevo") then + return true + else + return false + end +end + +-- returns dressed-up item id based on not dressed id +-- returns number +function getActiveItemId(id) + if not id then return false end + + if id == 3049 then + return 3086 + elseif id == 3050 then + return 3087 + elseif id == 3051 then + return 3088 + elseif id == 3052 then + return 3089 + elseif id == 3053 then + return 3090 + elseif id == 3091 then + return 3094 + elseif id == 3092 then + return 3095 + elseif id == 3093 then + return 3096 + elseif id == 3097 then + return 3099 + elseif id == 3098 then + return 3100 + elseif id == 16114 then + return 16264 + elseif id == 23531 then + return 23532 + elseif id == 23533 then + return 23534 + elseif id == 23529 then + return 23530 + elseif id == 30343 then -- Sleep Shawl + return 30342 + elseif id == 30344 then -- Enchanted Pendulet + return 30345 + elseif id == 30403 then -- Enchanted Theurgic Amulet + return 30402 + elseif id == 31621 then -- Blister Ring + return 31616 + elseif id == 32621 then -- Ring of Souls + return 32635 + else + return id + end +end + +-- returns not dressed item id based on dressed-up id +-- returns number +function getInactiveItemId(id) + if not id then return false end + + if id == 3086 then + return 3049 + elseif id == 3087 then + return 3050 + elseif id == 3088 then + return 3051 + elseif id == 3089 then + return 3052 + elseif id == 3090 then + return 3053 + elseif id == 3094 then + return 3091 + elseif id == 3095 then + return 3092 + elseif id == 3096 then + return 3093 + elseif id == 3099 then + return 3097 + elseif id == 3100 then + return 3098 + elseif id == 16264 then + return 16114 + elseif id == 23532 then + return 23531 + elseif id == 23534 then + return 23533 + elseif id == 23530 then + return 23529 + elseif id == 30342 then -- Sleep Shawl + return 30343 + elseif id == 30345 then -- Enchanted Pendulet + return 30344 + elseif id == 30402 then -- Enchanted Theurgic Amulet + return 30403 + elseif id == 31616 then -- Blister Ring + return 31621 + elseif id == 32635 then -- Ring of Souls + return 32621 + else + return id + end +end + +-- returns amount of monsters within the range of position +-- does not include summons (new tibia) +-- returns number +function getMonstersInRange(pos, range) + if not pos or not range then return false end + local monsters = 0 + for i, spec in pairs(getSpectators()) do + if spec:isMonster() and + (g_game.getClientVersion() < 960 or spec:getType() < 3) and + getDistanceBetween(pos, spec:getPosition()) < range then + monsters = monsters + 1 + end + end + return monsters +end + +-- shortcut in calculating distance from local player position +-- needs only one argument +-- returns number +function distanceFromPlayer(coords) + if not coords then return false end + return getDistanceBetween(pos(), coords) +end + +-- returns amount of monsters within the range of local player position +-- does not include summons (new tibia) +-- can also check multiple floors +-- returns number +function getMonsters(range, multifloor) + if not range then range = 10 end + local mobs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + mobs = (g_game.getClientVersion() < 960 or spec:getType() < 3) and + spec:isMonster() and distanceFromPlayer(spec:getPosition()) <= + range and mobs + 1 or mobs; + end + return mobs; +end + +-- returns amount of players within the range of local player position +-- does not include party members +-- can also check multiple floors +-- returns number +function getPlayers(range, multifloor) + if not range then range = 10 end + local specs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + if not spec:isLocalPlayer() and spec:isPlayer() and distanceFromPlayer(spec:getPosition()) <= range and not ((spec:getShield() ~= 1 and spec:isPartyMember()) or spec:getEmblem() == 1) then + specs = specs + 1 + end + end + return specs; +end + +-- this is multifloor function +-- checks if player added in "Anti RS list" in player list is within the given range +-- returns boolean +function isBlackListedPlayerInRange(range) + if #storage.playerList.blackList == 0 then return end + if not range then range = 10 end + local found = false + for _, spec in pairs(getSpectators(true)) do + local specPos = spec:getPosition() + local pPos = player:getPosition() + if spec:isPlayer() then + if math.abs(specPos.z - pPos.z) <= 2 then + if specPos.z ~= pPos.z then specPos.z = pPos.z end + if distanceFromPlayer(specPos) < range then + if table.find(storage.playerList.blackList, spec:getName()) then + found = true + end + end + end + end + end + return found +end + +-- checks if there is non-friend player withing the range +-- padding is only for multifloor +-- returns boolean +function isSafe(range, multifloor, padding) + local onSame = 0 + local onAnother = 0 + if not multifloor and padding then + multifloor = false + padding = false + end + + for _, spec in pairs(getSpectators(multifloor)) do + if spec:isPlayer() and not spec:isLocalPlayer() and + not isFriend(spec:getName()) then + if spec:getPosition().z == posz() and + distanceFromPlayer(spec:getPosition()) <= range then + onSame = onSame + 1 + end + if multifloor and padding and spec:getPosition().z ~= posz() and + distanceFromPlayer(spec:getPosition()) <= (range + padding) then + onAnother = onAnother + 1 + end + end + end + + if onSame + onAnother > 0 then + return false + else + return true + end +end + +-- returns amount of players within the range of local player position +-- can also check multiple floors +-- returns number +function getAllPlayers(range, multifloor) + if not range then range = 10 end + local specs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + specs = not spec:isLocalPlayer() and spec:isPlayer() and + distanceFromPlayer(spec:getPosition()) <= range and specs + + 1 or specs; + end + return specs; +end + +-- returns amount of NPC's within the range of local player position +-- can also check multiple floors +-- returns number +function getNpcs(range, multifloor) + if not range then range = 10 end + local npcs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + npcs = + spec:isNpc() and distanceFromPlayer(spec:getPosition()) <= range and + npcs + 1 or npcs; + end + return npcs; +end + +-- main function for calculatin item amount in all visible containers +-- also considers equipped items +-- returns number +function itemAmount(id) + return player:getItemsCount(id) +end + +-- self explanatory +-- a is item to use on +-- b is item to use a on +function useOnInvertoryItem(a, b) + local item = findItem(b) + if not item then return end + + return useWith(a, item) +end + +-- pos can be tile or position +-- returns table of tiles surrounding given POS/tile +function getNearTiles(pos) + if type(pos) ~= "table" then pos = pos:getPosition() end + + local tiles = {} + local dirs = { + {-1, 1}, {0, 1}, {1, 1}, {-1, 0}, {1, 0}, {-1, -1}, {0, -1}, {1, -1} + } + for i = 1, #dirs do + local tile = g_map.getTile({ + x = pos.x - dirs[i][1], + y = pos.y - dirs[i][2], + z = pos.z + }) + if tile then table.insert(tiles, tile) end + end + + return tiles +end + +-- self explanatory +-- use along with delay, it will only call action +function useGroundItem(id) + if not id then return false end + + local dest = nil + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + if item:getId() == id then + dest = item + break + end + end + end + + if dest then + return use(dest) + else + return false + end +end + +-- self explanatory +-- use along with delay, it will only call action +function reachGroundItem(id) + if not id then return false end + + local dest = nil + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + local iPos = item:getPosition() + local iId = item:getId() + if iId == id then + if findPath(pos(), iPos, 20, + {ignoreNonPathable = true, precision = 1}) then + dest = item + break + end + end + end + end + + if dest then + return autoWalk(iPos, 20, {ignoreNonPathable = true, precision = 1}) + else + return false + end +end + +-- self explanatory +-- returns object +function findItemOnGround(id) + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + if item:getId() == id then return item end + end + end +end + +-- self explanatory +-- use along with delay, it will only call action +function useOnGroundItem(a, b) + if not b then return false end + local item = findItem(a) + if not item then return false end + + local dest = nil + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + if item:getId() == id then + dest = item + break + end + end + end + + if dest then + return useWith(item, dest) + else + return false + end +end + +-- returns target creature +function target() + if not g_game.isAttacking() then + return + else + return g_game.getAttackingCreature() + end +end + +-- returns target creature +function getTarget() return target() end + +-- dist is boolean +-- returns target position/distance from player +function targetPos(dist) + if not g_game.isAttacking() then return end + if dist then + return distanceFromPlayer(target():getPosition()) + else + return target():getPosition() + end +end + +-- for gunzodus/ezodus only +-- it will reopen loot bag, necessary for depositer +function reopenPurse() + for i, c in pairs(getContainers()) do + if c:getName():lower() == "loot bag" or c:getName():lower() == + "store inbox" then g_game.close(c) end + end + schedule(100, function() + g_game.use(g_game.getLocalPlayer():getInventoryItem(InventorySlotPurse)) + end) + schedule(1400, function() + for i, c in pairs(getContainers()) do + if c:getName():lower() == "store inbox" then + for _, i in pairs(c:getItems()) do + if i:getId() == 23721 then + g_game.open(i, c) + end + end + end + end + end) + return CaveBot.delay(1500) +end + +-- getSpectator patterns +-- param1 - pos/creature +-- param2 - pattern +-- param3 - type of return +-- 1 - everyone, 2 - monsters, 3 - players +-- returns number +function getCreaturesInArea(param1, param2, param3) + local specs = 0 + local monsters = 0 + local players = 0 + for i, spec in pairs(getSpectators(param1, param2)) do + if spec ~= player then + specs = specs + 1 + if spec:isMonster() and + (g_game.getClientVersion() < 960 or spec:getType() < 3) then + monsters = monsters + 1 + elseif spec:isPlayer() and not isFriend(spec:getName()) then + players = players + 1 + end + end + end + + if param3 == 1 then + return specs + elseif param3 == 2 then + return monsters + else + return players + end +end + +-- can be improved +-- TODO in future +-- uses getCreaturesInArea, specType +-- returns number +function getBestTileByPatern(pattern, specType, maxDist, safe) + if not pattern or not specType then return end + if not maxDist then maxDist = 4 end + + local bestTile = nil + local best = nil + for _, tile in pairs(g_map.getTiles(posz())) do + if distanceFromPlayer(tile:getPosition()) <= maxDist then + local minimapColor = g_map.getMinimapColor(tile:getPosition()) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + if tile:canShoot() and tile:isWalkable() then + if getCreaturesInArea(tile:getPosition(), pattern, specType) > 0 then + if (not safe or + getCreaturesInArea(tile:getPosition(), pattern, 3) == 0) then + local candidate = + { + pos = tile, + count = getCreaturesInArea(tile:getPosition(), + pattern, specType) + } + if not best or best.count <= candidate.count then + best = candidate + end + end + end + end + end + end + + bestTile = best + + if bestTile then + return bestTile + else + return false + end +end + +-- returns container object based on name +function getContainerByName(name, notFull) + if type(name) ~= "string" then return nil end + + local d = nil + for i, c in pairs(getContainers()) do + if c:getName():lower() == name:lower() and (not notFull or not containerIsFull(c)) then + d = c + break + end + end + return d +end + +-- returns container object based on container ID +function getContainerByItem(id, notFull) + if type(id) ~= "number" then return nil end + + local d = nil + for i, c in pairs(getContainers()) do + if c:getContainerItem():getId() == id and (not notFull or not containerIsFull(c)) then + d = c + break + end + end + return d +end + +-- [[ ready to use getSpectators patterns ]] -- +LargeUeArea = [[ + 0000001000000 + 0000011100000 + 0000111110000 + 0001111111000 + 0011111111100 + 0111111111110 + 1111111111111 + 0111111111110 + 0011111111100 + 0001111111000 + 0000111110000 + 0000011100000 + 0000001000000 +]] + +NormalUeAreaMs = [[ + 00000100000 + 00011111000 + 00111111100 + 01111111110 + 01111111110 + 11111111111 + 01111111110 + 01111111110 + 00111111100 + 00001110000 + 00000100000 +]] + +NormalUeAreaEd = [[ + 00000100000 + 00001110000 + 00011111000 + 00111111100 + 01111111110 + 11111111111 + 01111111110 + 00111111100 + 00011111000 + 00001110000 + 00000100000 +]] + +smallUeArea = [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 +]] + +largeRuneArea = [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 +]] + +adjacentArea = [[ + 111 + 101 + 111 +]] + +longBeamArea = [[ + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + WWWWWWW0EEEEEEE + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 +]] + +shortBeamArea = [[ + 00000100000 + 00000100000 + 00000100000 + 00000100000 + 00000100000 + EEEEE0WWWWW + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 +]] + +newWaveArea = [[ + 000NNNNN000 + 000NNNNN000 + 0000NNN0000 + WW00NNN00EE + WWWW0N0EEEE + WWWWW0EEEEE + WWWW0S0EEEE + WW00SSS00EE + 0000SSS0000 + 000SSSSS000 + 000SSSSS000 +]] + +bigWaveArea = [[ + 0000NNN0000 + 0000NNN0000 + 0000NNN0000 + 00000N00000 + WWW00N00EEE + WWWWW0EEEEE + WWW00S00EEE + 00000S00000 + 0000SSS0000 + 0000SSS0000 + 0000SSS0000 +]] + +smallWaveArea = [[ + 00NNN00 + 00NNN00 + WW0N0EE + WWW0EEE + WW0S0EE + 00SSS00 + 00SSS00 +]] + +diamondArrowArea = [[ + 01110 + 11111 + 11111 + 11111 + 01110 +]] diff --git a/modules/game_bot/default_configs/vBot_4.7/vBot/xeno_menu.lua b/modules/game_bot/default_configs/vBot_4.7/vBot/xeno_menu.lua new file mode 100644 index 0000000000..2583500579 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.7/vBot/xeno_menu.lua @@ -0,0 +1,30 @@ +modules.game_interface.gameRootPanel.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('AttackBot', AttackBot.show, "OTCv8") + menu:addOption('HealBot', HealBot.show, "OTCv8") + menu:addOption('Conditions', Conditions.show, "OTCv8") + menu:addSeparator() + menu:addOption('CaveBot', function() + if CaveBot.isOn() then + CaveBot.setOff() + else + CaveBot.setOn() + end + end, CaveBot.isOn() and "ON " or "OFF ") + menu:addOption('TargetBot', function() + if TargetBot.isOn() then + TargetBot.setOff() + else + TargetBot.setOn() + end + end, TargetBot.isOn() and "ON " or "OFF ") + menu:display(mousePos) + return true + end + end +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/_Loader.lua b/modules/game_bot/default_configs/vBot_4.8/_Loader.lua new file mode 100644 index 0000000000..57a4bde6ff --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/_Loader.lua @@ -0,0 +1,64 @@ +-- load all otui files, order doesn't matter +local configName = modules.game_bot.contentsPanel.config:getCurrentOption().text + +local configFiles = g_resources.listDirectoryFiles("/bot/" .. configName .. "/vBot", true, false) +for i, file in ipairs(configFiles) do + local ext = file:split(".") + if ext[#ext]:lower() == "ui" or ext[#ext]:lower() == "otui" then + g_ui.importStyle(file) + end +end + +local function loadScript(name) + return dofile("/vBot/" .. name .. ".lua") +end + +-- here you can set manually order of scripts +-- libraries should be loaded first +local luaFiles = { + "main", + "items", + "vlib", + "new_cavebot_lib", + "configs", -- do not change this and above + "extras", + "cavebot", + "playerlist", + "BotServer", + "alarms", + "Conditions", + "Equipper", + "pushmax", + "combo", + "HealBot", + "new_healer", + "AttackBot", -- last of major modules + "ingame_editor", + "Dropper", + "Containers", + "quiver_manager", + "quiver_label", + "tools", + "antiRs", + "depot_withdraw", + "eat_food", + "equip", + "exeta", + "analyzer", + "spy_level", + "supplies", + "depositer_config", + "npc_talk", + "xeno_menu", + "hold_target", + "cavebot_control_panel" +} + +for i, file in ipairs(luaFiles) do + loadScript(file) +end + +setDefaultTab("Main") +UI.Separator() +UI.Label("Private Scripts:") +UI.Separator() diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/actions.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/actions.lua new file mode 100644 index 0000000000..5e9818b87a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/actions.lua @@ -0,0 +1,509 @@ +CaveBot.Actions = {} +vBot.lastLabel = "" +local oldTibia = g_game.getClientVersion() < 960 +local nextTile = nil + +local noPath = 0 + +-- antistuck f() +local nextPos = nil -- creature +local nextPosF = nil -- furniture +local function modPos(dir) + local y = 0 + local x = 0 + + if dir == 0 then + y = -1 + elseif dir == 1 then + x = 1 + elseif dir == 2 then + y = 1 + elseif dir == 3 then + x = -1 + elseif dir == 4 then + y = -1 + x = 1 + elseif dir == 5 then + y = 1 + x = 1 + elseif dir == 6 then + y = 1 + x = -1 + elseif dir == 7 then + y = -1 + x = -1 + end + + return {x, y} +end + +-- stack-covered antystuck, in & out pz +local lastMoved = now - 200 +onTextMessage(function(mode, text) + if text ~= 'There is not enough room.' then return end + if CaveBot.isOff() then return end + + local tiles = getNearTiles(pos()) + + for i, tile in ipairs(tiles) do + if not tile:hasCreature() and tile:isWalkable() and #tile:getItems() > 9 then + local topThing = tile:getTopThing() + if not isInPz() then + return useWith(3197, tile:getTopThing()) -- disintegrate + else + if now < lastMoved + 200 then return end -- delay to prevent clogging + local nearTiles = getNearTiles(tile:getPosition()) + for i, tile in ipairs(nearTiles) do + local tpos = tile:getPosition() + if pos() ~= tpos then + if tile:isWalkable() then + lastMoved = now + return g_game.move(topThing, tpos) -- move item + end + end + end + end + end + end +end) + +local furnitureIgnore = { 2986 } +local function breakFurniture(destPos) + if isInPz() then return false end + local candidate = {thing=nil, dist=100} + for i, tile in ipairs(g_map.getTiles(posz())) do + local walkable = tile:isWalkable() + local topThing = tile:getTopThing() + local isWg = topThing and topThing:getId() == 2130 + if topThing and (isWg or not table.find(furnitureIgnore, topThing:getId()) and topThing:isItem()) then + local moveable = not topThing:isNotMoveable() + local tpos = tile:getPosition() + local path = findPath(player:getPosition(), tpos, 7, { ignoreNonPathable = true, precision = 1 }) + + if path then + if isWg or (not walkable and moveable) then + local distance = getDistanceBetween(destPos, tpos) + + if distance < candidate.dist then + candidate = {thing=topThing, dist=distance} + end + end + end + end + end + + local thing = candidate.thing + if thing then + useWith(3197, thing) + return true + end + + return false +end + +local function pushPlayer(creature) + local cpos = creature:getPosition() + local tiles = getNearTiles(cpos) + + for i, tile in ipairs(tiles) do + local pos = tile:getPosition() + local minimapColor = g_map.getMinimapColor(pos) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + + if not stairs and tile:isWalkable() then + g_game.move(creature, pos) + end + end + +end + +local function pathfinder() + if not storage.extras.pathfinding then return end + if noPath < 10 then return end + + if not CaveBot.gotoNextWaypointInRange() then + if getConfigFromName and getConfigFromName() then + local profile = CaveBot.getCurrentProfile() + local config = getConfigFromName() + local newProfile = profile == '#Unibase' and config or '#Unibase' + + CaveBot.setCurrentProfile(newProfile) + end + end + noPath = 0 + return true +end + +-- 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 warn("Invalid cavebot action: " .. action) + end + if type(value) == 'number' then + value = tostring(value) + 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 warn("Invalid cavebot action: " .. action) + end + + if not widget.action or not widget.value then + return warn("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 warn("Duplicated acction: " .. action) + end + CaveBot.Actions[action] = { + color=color, + callback=callback + } +end + +CaveBot.registerAction("label", "yellow", function(value, retries, prev) + vBot.lastLabel = value + 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 + local data = string.split(value, ",") + local val = tonumber(data[1]:trim()) + local random + local final + + + if #data == 2 then + random = tonumber(data[2]:trim()) + end + + if random then + local diff = (val/100) * random + local min = val - diff + local max = val + diff + final = math.random(min, max) + end + final = final or val + + CaveBot.delay(final) + return "retry" + end + return true +end) + +CaveBot.registerAction("follow", "#FF8400", function(value, retries, prev) + local c = getCreatureByName(value) + if not c then + print("CaveBot[follow]: can't find creature to follow") + return false + end + local cpos = c:getPosition() + local pos = pos() + if getDistanceBetween(cpos, pos) < 2 then + g_game.cancelFollow() + return true + else + follow(c) + delay(200) + return "retry" + end +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() warn('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 + warn("warn 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]+),?\\s*([0-9]?)") + if not pos[1] then + warn("Invalid cavebot goto action value. It should be position (x,y,z), is: " .. value) + return false + end + + -- reset pathfinder + nextPosF = nil + nextPos = nil + + if CaveBot.Config.get("mapClick") then + if retries >= 5 then + noPath = noPath + 1 + pathfinder() + return false -- tried 5 times, can't get there + end + else + if retries >= 100 then + noPath = noPath + 1 + pathfinder() + return false -- tried 100 times, can't get there + end + end + + local precision = tonumber(pos[1][5]) + 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 + noPath = noPath + 1 + pathfinder() + return false -- different floor + end + + local maxDist = storage.extras.gotoMaxDistance or 40 + + if math.abs(pos.x-playerPos.x) + math.abs(pos.y-playerPos.y) > maxDist then + noPath = noPath + 1 + pathfinder() + 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 + noPath = 0 + return true -- already at position + end + elseif math.abs(pos.x-playerPos.x) == 0 and math.abs(pos.y-playerPos.y) <= (precision or 1) then + noPath = 0 + return true -- already at position + end + -- check if there's a path to that place, ignore creatures and fields + local path = findPath(playerPos, pos, maxDist, { ignoreNonPathable = true, precision = 1, ignoreCreatures = true, allowUnseen = true, allowOnlyVisibleTiles = false }) + if not path then + if breakFurniture(pos, storage.extras.machete) then + CaveBot.delay(1000) + retries = 0 + return "retry" + end + noPath = noPath + 1 + pathfinder() + return false -- there's no way + end + + -- check if there's a path to destination but consider Creatures (attack only if trapped) + local path2 = findPath(playerPos, pos, maxDist, { ignoreNonPathable = true, precision = 1 }) + if not path2 then + local foundMonster = false + for i, dir in ipairs(path) do + local dirs = modPos(dir) + nextPos = nextPos or playerPos + nextPos.x = nextPos.x + dirs[1] + nextPos.y = nextPos.y + dirs[2] + + local tile = g_map.getTile(nextPos) + if tile then + if tile:hasCreature() then + local creature = tile:getCreatures()[1] + local hppc = creature:getHealthPercent() + if creature:isMonster() and (hppc and hppc > 0) and (oldTibia or creature:getType() < 3) then + -- real blocking creature can not meet those conditions - ie. it could be player, so just in case check if the next creature is reachable + local path = findPath(playerPos, creature:getPosition(), 7, { ignoreNonPathable = true, precision = 1 }) + if path then + foundMonster = true + if g_game.getAttackingCreature() ~= creature then + if distanceFromPlayer(creature:getPosition()) > 3 then + CaveBot.walkTo(creature:getPosition(), 7, { ignoreNonPathable = true, precision = 1 }) + else + attack(creature) + end + end + g_game.setChaseMode(1) + CaveBot.delay(100) + retries = 0 -- reset retries, we are trying to unclog the cavebot + break + end + end + end + end + end + + if not foundMonster then + foundMonster = false + return false -- no other way + end + end + + -- try to find path, don't ignore creatures, don't ignore fields + if not CaveBot.Config.get("ignoreFields") and CaveBot.walkTo(pos, 40) then + return "retry" + end + + -- try to find path, don't ignore creatures, ignore fields + if CaveBot.walkTo(pos, maxDist, { ignoreNonPathable = true, allowUnseen = true, allowOnlyVisibleTiles = false }) 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 CaveBot.walkTo(pos, 50, { ignoreNonPathable = true, precision = precison, allowUnseen = true, allowOnlyVisibleTiles = false }) then + return "retry" + end + end + + if not CaveBot.Config.get("mapClick") and retries >= 5 then + noPath = noPath + 1 + pathfinder() + return false + end + + if CaveBot.Config.get("skipBlocked") then + noPath = noPath + 1 + pathfinder() + return false + end + + -- everything else failed, try to walk ignoring creatures, maybe will work + CaveBot.walkTo(pos, maxDist, { ignoreNonPathable = true, precision = 1, ignoreCreatures = true, allowUnseen = true, allowOnlyVisibleTiles = false }) + 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 + warn("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(CaveBot.Config.get("useDelay") + CaveBot.Config.get("ping")) + 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 + warn("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(CaveBot.Config.get("useDelay") + CaveBot.Config.get("ping")) + return true +end) + +CaveBot.registerAction("say", "#FF55FF", function(value, retries, prev) + say(value) + return true +end) +CaveBot.registerAction("npcsay", "#FF55FF", function(value, retries, prev) + NPC.say(value) + return true +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/bank.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/bank.lua new file mode 100644 index 0000000000..6bdee75893 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/bank.lua @@ -0,0 +1,92 @@ +CaveBot.Extensions.Bank = {} + +local balance = 0 + +CaveBot.Extensions.Bank.setup = function() + CaveBot.registerAction("bank", "#db5a5a", function(value, retries) + local data = string.split(value, ",") + local waitVal = 300 + local amount = 0 + local actionType + local npcName + local transferName + local balanceLeft + if #data ~= 3 and #data ~= 2 and #data ~= 4 then + warn("CaveBot[Bank]: incorrect value!") + return false + else + actionType = data[1]:trim():lower() + npcName = data[2]:trim() + if #data == 3 then + amount = tonumber(data[3]:trim()) + end + if #data == 4 then + transferName = data[3]:trim() + balanceLeft = tonumber(data[4]:trim()) + end + end + + if actionType ~= "withdraw" and actionType ~= "deposit" and actionType ~= "transfer" then + warn("CaveBot[Bank]: incorrect action type! should be withdraw/deposit/transfer, is: " .. actionType) + return false + elseif actionType == "withdraw" then + local value = tonumber(amount) + if not value then + warn("CaveBot[Bank]: incorrect amount value! should be number, is: " .. amount) + return false + end + end + + if retries > 5 then + print("CaveBot[Bank]: too many tries, skipping") + return false + end + + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[Bank]: NPC not found, skipping") + return false + end + + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + if actionType == "deposit" then + CaveBot.Conversation("hi", "deposit all", "yes") + CaveBot.delay(storage.extras.talkDelay*3) + return true + elseif actionType == "withdraw" then + CaveBot.Conversation("hi", "withdraw", value, "yes") + CaveBot.delay(storage.extras.talkDelay*4) + return true + else + -- first check balance + CaveBot.Conversation("hi", "balance") + schedule(5000, function() + local amountToTransfer = balance - balanceLeft + if amountToTransfer <= 0 then + warn("CaveBot[Bank] Not enough gold to transfer! proceeding") + return false + end + CaveBot.Conversation("hi", "transfer", amountToTransfer, transferName, "yes") + warn("CaveBot[Bank] transferred "..amountToTransfer.." gold to: "..transferName) + end) + CaveBot.delay(storage.extras.talkDelay*11) + return true + end + end) + + CaveBot.Editor.registerAction("bank", "bank", { + value="action, NPC name", + title="Banker", + description="action type(withdraw/deposit/transfer), NPC name, (if withdraw: amount|if transfer: name, balance left)", + }) +end + + +onTalk(function(name, level, mode, text, channelId, pos) + if mode == 51 and text:find("Your account balance is") then + balance = getFirstNumberInText(text) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/buy_supplies.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/buy_supplies.lua new file mode 100644 index 0000000000..1620ccc0e4 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/buy_supplies.lua @@ -0,0 +1,76 @@ +CaveBot.Extensions.BuySupplies = {} + +CaveBot.Extensions.BuySupplies.setup = function() + CaveBot.registerAction("BuySupplies", "#C300FF", function(value, retries) + local possibleItems = {} + + local val = string.split(value, ",") + local waitVal + if #val == 0 or #val > 2 then + warn("CaveBot[BuySupplies]: incorrect BuySupplies value") + return false + elseif #val == 2 then + waitVal = tonumber(val[2]:trim()) + end + + local npcName = val[1]:trim() + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[BuySupplies]: NPC not found") + return false + end + + if not waitVal and #val == 2 then + warn("CaveBot[BuySupplies]: incorrect delay values!") + elseif waitVal and #val == 2 then + delay(waitVal) + end + + if retries > 50 then + print("CaveBot[BuySupplies]: Too many tries, can't buy") + return false + end + + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + if not NPC.isTrading() then + CaveBot.OpenNpcTrade() + CaveBot.delay(storage.extras.talkDelay*2) + return "retry" + end + + -- get items from npc + local npcItems = NPC.getBuyItems() + for i,v in pairs(npcItems) do + table.insert(possibleItems, v.id) + end + + for id, values in pairs(Supplies.getItemsData()) do + id = tonumber(id) + if table.find(possibleItems, id) then + local max = values.max + local current = player:getItemsCount(id) + local toBuy = max - current + + if toBuy > 0 then + toBuy = math.min(100, toBuy) + + NPC.buy(id, math.min(100, toBuy)) + print("CaveBot[BuySupplies]: bought " .. toBuy .. "x " .. id) + return "retry" + end + end + end + + print("CaveBot[BuySupplies]: bought everything, proceeding") + return true + end) + + CaveBot.Editor.registerAction("buysupplies", "buy supplies", { + value="NPC name", + title="Buy Supplies", + description="NPC Name, delay(in ms, optional)", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.lua new file mode 100644 index 0000000000..6da44598c7 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.lua @@ -0,0 +1,447 @@ +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 +if CaveBot.Config then + CaveBot.Config.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() and not TargetBot.isCaveBotActionAllowed() then + CaveBot.resetWalking() + return -- target bot or looting is working, wait + end + + if CaveBot.doWalking() then + return -- executing walking3 + 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() + CaveBot.resetWalking() + 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 + warn("Invalid return from cavebot action (" .. currentAction.action .. "), should be \"retry\", false or true, is: " .. tostring(result)) + end + else + warn("warn while executing cavebot action (" .. currentAction.action .. "):\n" .. result) + end + else + warn("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) + +-- 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 + + local cavebotConfig = nil + for k,v in ipairs(data) do + if type(v) == "table" and #v == 2 then + if v[1] == "config" then + local status, result = pcall(function() + return json.decode(v[2]) + end) + if not status then + warn("warn while parsing CaveBot extensions from config:\n" .. result) + else + cavebotConfig = result + end + elseif v[1] == "extensions" then + local status, result = pcall(function() + return json.decode(v[2]) + end) + if not status then + warn("warn 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 + + CaveBot.Config.onConfigChange(name, enabled, cavebotConfig) + + actionRetries = 0 + CaveBot.resetWalking() + 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 + +ui.showConfig.onClick = function() + if not CaveBot.Config then return end + if ui.showConfig:isOn() then + CaveBot.Config.hide() + ui.showConfig:setOn(false) + else + CaveBot.Config.show() + ui.showConfig: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.getCurrentProfile = function() + return storage._configs.cavebot_configs.selected +end + +CaveBot.lastReachedLabel = function() + return vBot.lastLabel +end + +CaveBot.gotoNextWaypointInRange = function() + local currentAction = ui.list:getFocusedChild() + local index = ui.list:getChildIndex(currentAction) + local actions = ui.list:getChildren() + + -- start searching from current index + for i, child in ipairs(actions) do + if i > index then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + local maxDist = storage.extras.gotoMaxDistance + if distanceFromPlayer(pos) <= maxDist then + if findPath(player:getPosition(), pos, maxDist, { ignoreNonPathable = true }) then + ui.list:focusChild(ui.list:getChildByIndex(i-1)) + return true + end + end + end + end + end + end + + -- if not found then damn go from start + for i, child in ipairs(actions) do + if i <= index then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + local maxDist = storage.extras.gotoMaxDistance + if distanceFromPlayer(pos) <= maxDist then + if findPath(player:getPosition(), pos, maxDist, { ignoreNonPathable = true }) then + ui.list:focusChild(ui.list:getChildByIndex(i-1)) + return true + end + end + end + end + end + end + + -- not found + return false +end + +local function reverseTable(t, max) + local reversedTable = {} + local itemCount = max or #t + for i, v in ipairs(t) do + reversedTable[itemCount + 1 - i] = v + end + return reversedTable +end + +function rpairs(t) + test() + return function(t, i) + i = i - 1 + if i ~= 0 then + return i, t[i] + end + end, t, #t + 1 +end + +CaveBot.gotoFirstPreviousReachableWaypoint = function() + local currentAction = ui.list:getFocusedChild() + local currentIndex = ui.list:getChildIndex(currentAction) + local index = ui.list:getChildIndex(currentAction) + + -- check up to 100 childs + for i=0,100 do + index = index - i + if index <= 0 or index > currentIndex or math.abs(index-currentIndex) > 100 then + break + end + + local child = ui.list:getChildByIndex(index) + + if child then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + if distanceFromPlayer(pos) <= storage.extras.gotoMaxDistance/2 then + print("found pos, going back "..currentIndex-index.. " waypoints.") + return ui.list:focusChild(child) + end + end + end + end + end + + -- not found + print("previous pos not found, proceeding") + return false +end + +CaveBot.getFirstWaypointBeforeLabel = function(label) + label = "label:"..label + label = label:lower() + local actions = ui.list:getChildren() + local index + + -- find index of label + for i, child in pairs(actions) do + local name = child:getText():lower() + if name == label then + index = i + break + end + end + + -- if there's no index then label was not found + if not index then return false end + + for i=1,#actions do + if index - 1 < 1 then + -- did not found any waypoint in range before label + return false + end + + local child = ui.list:getChildByIndex(index-i) + if child then + local text = child:getText() + if string.starts(text, "goto:") then + local re = regexMatch(text, [[(?:goto:)([^,]+),([^,]+),([^,]+)]]) + local pos = {x = tonumber(re[1][2]), y = tonumber(re[1][3]), z = tonumber(re[1][4])} + + if posz() == pos.z then + if distanceFromPlayer(pos) <= storage.extras.gotoMaxDistance/2 then + return ui.list:focusChild(child) + end + end + end + end + end +end + +CaveBot.getPreviousLabel = function() + local actions = ui.list:getChildren() + -- check if config is empty + if #actions == 0 then return false end + + local currentAction = ui.list:getFocusedChild() + --check we made any progress in waypoints, if no focused or first then no point checking + if not currentAction or currentAction == ui.list:getFirstChild() then return false end + + local index = ui.list:getChildIndex(currentAction) + + -- if not index then something went wrong and there's no selected child + if not index then return false end + + for i=1,#actions do + if index - i < 1 then + -- did not found any waypoint in range before label + return false + end + + local child = ui.list:getChildByIndex(index-i) + if child then + if child.action == "label" then + return child.value + end + end + end +end + +CaveBot.getNextLabel = function() + local actions = ui.list:getChildren() + -- check if config is empty + if #actions == 0 then return false end + + local currentAction = ui.list:getFocusedChild() or ui.list:getFirstChild() + local index = ui.list:getChildIndex(currentAction) + + -- if not index then something went wrong + if not index then return false end + + for i=1,#actions do + if index + i > #actions then + -- did not found any waypoint in range before label + return false + end + + local child = ui.list:getChildByIndex(index+i) + if child then + if child.action == "label" then + return child.value + end + end + end +end + +local botConfigName = modules.game_bot.contentsPanel.config:getCurrentOption().text +CaveBot.setCurrentProfile = function(name) + if not g_resources.fileExists("/bot/"..botConfigName.."/cavebot_configs/"..name..".cfg") then + return warn("there is no cavebot profile with that name!") + end + CaveBot.setOff() + storage._configs.cavebot_configs.selected = name + CaveBot.setOn() +end + +CaveBot.delay = function(value) + cavebotMacro.delay = math.max(cavebotMacro.delay or 0, 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 + + if CaveBot.Config then + table.insert(data, {"config", json.encode(CaveBot.Config.save())}) + 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 + +CaveBotList = function() + return ui.list +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.otui b/modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.otui new file mode 100644 index 0000000000..b92ed05fb9 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/cavebot.otui @@ -0,0 +1,58 @@ +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 + + BotSwitch + id: showConfig + margin-top: 2 + + $on: + text: Hide config + + $!on: + text: Show config \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/clear_tile.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/clear_tile.lua new file mode 100644 index 0000000000..73a252fb10 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/clear_tile.lua @@ -0,0 +1,128 @@ +CaveBot.Extensions.ClearTile = {} + +CaveBot.Extensions.ClearTile.setup = function() + CaveBot.registerAction("ClearTile", "#00FFFF", function(value, retries) + local data = string.split(value, ",") + local pos = {x=tonumber(data[1]), y=tonumber(data[2]), z=tonumber(data[3])} + local doors = false + local stand = false + local pPos = player:getPosition() + + + for i, value in ipairs(data) do + value = value:lower():trim() + if value == "stand" then + stand = true + elseif value == "doors" then + doors = true + end + end + + + if not #pos == 3 then + warn("CaveBot[ClearTile]: invalid value. It should be position (x,y,z), is: " .. value) + return false + end + + if retries >= 20 then + print("CaveBot[ClearTile]: too many tries, can't clear it") + return false -- tried 20 times, can't clear it + end + + if getDistanceBetween(player:getPosition(), pos) == 0 then + print("CaveBot[ClearTile]: tile reached, proceeding") + return true + end + local tile = g_map.getTile(pos) + if not tile then + print("CaveBot[ClearTile]: can't find tile or tile is unreachable, skipping") + return false + end + local tPos = tile:getPosition() + + -- no items on tile and walkability means we are done + if tile:isWalkable() and tile:getTopUseThing():isNotMoveable() and not tile:hasCreature() and not doors then + if stand then + if not CaveBot.MatchPosition(tPos, 0) then + CaveBot.GoTo(tPos, 0) + return "retry" + end + end + print("CaveBot[ClearTile]: tile clear, proceeding") + return true + end + + if not CaveBot.MatchPosition(tPos, 3) then + CaveBot.GoTo(tPos, 3) + return "retry" + end + + if retries > 0 then + delay(1100) + end + + -- monster + if tile:hasCreature() then + local c = tile:getCreatures()[1] + if c:isMonster() then + attack(c) + return "retry" + end + end + + -- moveable item + local item = tile:getTopMoveThing() + if item:isItem() then + if item and not item:isNotMoveable() then + print("CaveBot[ClearTile]: moving item... " .. item:getId().. " from tile") + g_game.move(item, pPos, item:getCount()) + return "retry" + end + end + + -- player + + -- push creature + if tile:hasCreature() then + local c = tile:getCreatures()[1] + if c and c:isPlayer() then + + local candidates = {} + for _, tile in ipairs(g_map.getTiles(posz())) do + local tPos = tile:getPosition() + if getDistanceBetween(c:getPosition(), tPos) == 1 and tPos ~= pPos and tile:isWalkable() then + table.insert(candidates, tPos) + end + end + + if #candidates == 0 then + print("CaveBot[ClearTile]: can't find tile to push, cannot clear way, skipping") + return false + else + print("CaveBot[ClearTile]: pushing player... " .. c:getName() .. " out of the way") + local pos = candidates[math.random(1,#candidates)] + local tile = g_map.getTile(pos) + tile:setText("here") + schedule(500, function() tile:setText("") end) + g_game.move(c, pos, 1) + return "retry" + end + end + end + + -- doors + if doors then + use(tile:getTopUseThing()) + return "retry" + end + + return "retry" + end) + + CaveBot.Editor.registerAction("cleartile", "clear tile", { + value=function() return posx() .. "," .. posy() .. "," .. posz() end, + title="position of tile to clear", + description="tile position (x,y,z), doors/stand - optional", + multiline=false +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/config.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/config.lua new file mode 100644 index 0000000000..e398111d6b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/config.lua @@ -0,0 +1,111 @@ +-- config for bot +CaveBot.Config = {} +CaveBot.Config.values = {} +CaveBot.Config.default_values = {} +CaveBot.Config.value_setters = {} + +CaveBot.Config.setup = function() + CaveBot.Config.ui = UI.createWidget("CaveBotConfigPanel") + local ui = CaveBot.Config.ui + local add = CaveBot.Config.add + + add("ping", "Server ping", 100) + add("walkDelay", "Walk delay", 10) + add("mapClick", "Use map click", false) + add("mapClickDelay", "Map click delay", 100) + add("ignoreFields", "Ignore fields", false) + add("skipBlocked", "Skip blocked path", false) + add("useDelay", "Delay after use", 400) +end + +CaveBot.Config.show = function() + CaveBot.Config.ui:show() +end + +CaveBot.Config.hide = function() + CaveBot.Config.ui:hide() +end + +CaveBot.Config.onConfigChange = function(configName, isEnabled, configData) + for k, v in pairs(CaveBot.Config.default_values) do + CaveBot.Config.value_setters[k](v) + end + if not configData then return end + for k, v in pairs(configData) do + if CaveBot.Config.value_setters[k] then + CaveBot.Config.value_setters[k](v) + end + end +end + +CaveBot.Config.save = function() + return CaveBot.Config.values +end + +CaveBot.Config.add = function(id, title, defaultValue) + if CaveBot.Config.values[id] then + return warn("Duplicated config key: " .. id) + end + + local panel + local setter -- sets value + if type(defaultValue) == "number" then + panel = UI.createWidget("CaveBotConfigNumberValuePanel", CaveBot.Config.ui) + panel:setId(id) + setter = function(value) + CaveBot.Config.values[id] = value + panel.value:setText(value, true) + end + setter(defaultValue) + panel.value.onTextChange = function(widget, newValue) + newValue = tonumber(newValue) + if newValue then + CaveBot.Config.values[id] = newValue + CaveBot.save() + end + end + elseif type(defaultValue) == "boolean" then + panel = UI.createWidget("CaveBotConfigBooleanValuePanel", CaveBot.Config.ui) + panel:setId(id) + setter = function(value) + CaveBot.Config.values[id] = value + panel.value:setOn(value, true) + end + setter(defaultValue) + panel.value.onClick = function(widget) + widget:setOn(not widget:isOn()) + CaveBot.Config.values[id] = widget:isOn() + CaveBot.save() + end + else + return warn("Invalid default value of config for key " .. id .. ", should be number or boolean") + end + + panel.title:setText(tr(title) .. ":") + + CaveBot.Config.value_setters[id] = setter + CaveBot.Config.values[id] = defaultValue + CaveBot.Config.default_values[id] = defaultValue +end + +CaveBot.Config.get = function(id) + if CaveBot.Config.values[id] == nil then + return warn("Invalid CaveBot.Config.get, id: " .. id) + end + return CaveBot.Config.values[id] +end + +CaveBot.Config.set = function(id, value) + local valueType = CaveBot.Config.get(id) + local panel = CaveBot.Config.ui[id] + + if valueType == 'boolean' then + CaveBot.Config.values[id] = value + panel.value:setOn(value, true) + CaveBot.save() + else + CaveBot.Config.values[id] = value + panel.value:setText(value, true) + CaveBot.save() + end +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/config.otui b/modules/game_bot/default_configs/vBot_4.8/cavebot/config.otui new file mode 100644 index 0000000000..21d479dd60 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/config.otui @@ -0,0 +1,57 @@ +CaveBotConfigPanel < Panel + id: cavebotEditor + visible: false + + layout: + type: verticalBox + fit-children: true + + HorizontalSeparator + margin-top: 5 + + Label + text-align: center + text: CaveBot Config + margin-top: 5 + +CaveBotConfigNumberValuePanel < Panel + height: 20 + margin-top: 5 + + BotTextEdit + id: value + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + margin-right: 5 + width: 50 + + Label + id: title + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 + +CaveBotConfigBooleanValuePanel < Panel + height: 20 + margin-top: 5 + + BotSwitch + id: value + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + margin-right: 5 + width: 50 + + $on: + text: On + + $!on: + text: Off + + Label + id: title + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/d_withdraw.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/d_withdraw.lua new file mode 100644 index 0000000000..888ed1b074 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/d_withdraw.lua @@ -0,0 +1,105 @@ +CaveBot.Extensions.DWithdraw = {} + +CaveBot.Extensions.DWithdraw.setup = function() + CaveBot.registerAction("dpwithdraw", "#002FFF", function(value, retries) + local capLimit + local data = string.split(value, ",") + if retries > 600 then + print("CaveBot[DepotWithdraw]: actions limit reached, proceeding") + return false + end + local destContainer + local depotContainer + delay(70) + + -- input validation + if not value or #data ~= 3 and #data ~= 4 then + warn("CaveBot[DepotWithdraw]: incorrect value!") + return false + end + local indexDp = tonumber(data[1]:trim()) + local destName = data[2]:trim():lower() + local destId = tonumber(data[3]:trim()) + if #data == 4 then + capLimit = tonumber(data[4]:trim()) + end + + + -- cap check + if freecap() < (capLimit or 200) then + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + print("CaveBot[DepotWithdraw]: cap limit reached, proceeding") + return false + end + + -- containers + for i, container in ipairs(getContainers()) do + local cName = container:getName():lower() + if destName == cName then + destContainer = container + elseif cName:find("depot box") then + depotContainer = container + end + end + + if not destContainer then + print("CaveBot[DepotWithdraw]: container not found!") + return false + end + + if containerIsFull(destContainer) then + for i, item in pairs(destContainer:getItems()) do + if item:getId() == destId then + g_game.open(item, destContainer) + return "retry" + end + end + end + + -- stash validation + if depotContainer and #depotContainer:getItems() == 0 then + print("CaveBot[DepotWithdraw]: all items withdrawn") + g_game.close(depotContainer) + return true + end + + if containerIsFull(destContainer) then + for i, item in pairs(destContainer:getItems()) do + if item:getId() == destId then + g_game.open(foundNextContainer, destContainer) + return "retry" + end + end + print("CaveBot[DepotWithdraw]: loot containers full!") + return false + end + + if not CaveBot.OpenDepotBox(indexDp) then + return "retry" + end + + CaveBot.PingDelay(2) + + for i, container in pairs(g_game.getContainers()) do + if string.find(container:getName():lower(), "depot box") then + for j, item in ipairs(container:getItems()) do + statusMessage("[D_Withdraw] witdhrawing item: "..item:getId()) + g_game.move(item, destContainer:getSlotPosition(destContainer:getItemsCount()), item:getCount()) + return "retry" + end + end + end + + return "retry" + end) + + CaveBot.Editor.registerAction("dpwithdraw", "dpwithdraw", { + value="1, shopping bag, 21411", + title="Loot Withdraw", + description="insert index, destination container name and it's ID", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/depositor.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/depositor.lua new file mode 100644 index 0000000000..eb2d03801f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/depositor.lua @@ -0,0 +1,138 @@ +CaveBot.Extensions.Depositor = {} + +--local variables +local destination = nil +local lootTable = nil +local reopenedContainers = false + +local function resetCache() + reopenedContainers = false + destination = nil + lootTable = nil + + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + + if storage.caveBot.backStop then + storage.caveBot.backStop = false + CaveBot.setOff() + elseif storage.caveBot.backTrainers then + storage.caveBot.backTrainers = false + CaveBot.gotoLabel('toTrainers') + elseif storage.caveBot.backOffline then + storage.caveBot.backOffline = false + CaveBot.gotoLabel('toOfflineTraining') + end +end + +local description = g_game.getClientVersion() > 960 and "No - just deposit \n Yes - also reopen loot containers" or "currently not supported, will be added in near future" + +CaveBot.Extensions.Depositor.setup = function() + CaveBot.registerAction("depositor", "#002FFF", function(value, retries) + -- version check, TODO old tibia + if g_game.getClientVersion() < 960 then + resetCache() + warn("CaveBot[Depositor]: unsupported Tibia version, will be added in near future") + return false + end + + -- loot list check + lootTable = lootTable or CaveBot.GetLootItems() + if #lootTable == 0 then + print("CaveBot[Depositor]: no items in loot list. Wrong TargetBot Config? Proceeding") + resetCache() + return true + end + + delay(70) + + -- backpacks etc + if value:lower() == "yes" then + if not reopenedContainers then + CaveBot.CloseAllLootContainers() + delay(3000) + reopenedContainers = true + return "retry" + end + -- open next backpacks if no more loot + if not CaveBot.HasLootItems() then + local lootContainers = CaveBot.GetLootContainers() + for _, container in ipairs(getContainers()) do + local cId = container:getContainerItem():getId() + if table.find(lootContainers, cId) then + for i, item in ipairs(container:getItems()) do + if item:getId() == cId then + g_game.open(item, container) + delay(100) + return "retry" + end + end + end + end + -- couldn't find next container, so we done + print("CaveBot[Depositor]: all items stashed, no backpack to open next, proceeding") + CaveBot.CloseAllLootContainers() + delay(3000) + resetCache() + return true + end + end + + -- first check items + if retries == 0 then + if not CaveBot.HasLootItems() then -- resource consuming function + print("CaveBot[Depositor]: no items to stash, proceeding") + resetCache() + return true + end + end + + -- next check retries + if retries > 400 then + print("CaveBot[Depositor]: Depositor actions limit reached, proceeding") + resetCache() + return true + end + + -- reaching and opening depot + if not CaveBot.ReachAndOpenDepot() then + return "retry" + end + + -- add delay to prevent bugging + CaveBot.PingDelay(2) + + -- prep time and stashing + destination = destination or getContainerByName("Depot chest") + if not destination then return "retry" end + + for _, container in pairs(getContainers()) do + local name = container:getName():lower() + if not name:find("depot") and not name:find("your inbox") then + for _, item in pairs(container:getItems()) do + local id = item:getId() + if table.find(lootTable, id) then + local index = getStashingIndex(id) or item:isStackable() and 1 or 0 + statusMessage("[Depositer] stashing item: " ..id.. " to depot: "..index+1) + CaveBot.StashItem(item, index, destination) + return "retry" + end + end + end + end + + -- we gucci + resetCache() + return true + end) + + CaveBot.Editor.registerAction("depositor", "depositor", { + value="no", + title="Depositor", + description=description, + validation="(yes|Yes|YES|no|No|NO)" + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/doors.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/doors.lua new file mode 100644 index 0000000000..f53992b1b5 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/doors.lua @@ -0,0 +1,58 @@ +CaveBot.Extensions.OpenDoors = {} + +CaveBot.Extensions.OpenDoors.setup = function() + CaveBot.registerAction("OpenDoors", "#00FFFF", function(value, retries) + local pos = string.split(value, ",") + local key = nil + if #pos == 4 then + key = tonumber(pos[4]) + end + if not pos[1] then + warn("CaveBot[OpenDoors]: invalid value. It should be position (x,y,z), is: " .. value) + return false + end + + if retries >= 5 then + print("CaveBot[OpenDoors]: too many tries, can't open doors") + return false -- tried 5 times, can't open + end + + pos = {x=tonumber(pos[1]), y=tonumber(pos[2]), z=tonumber(pos[3])} + + local doorTile + if not doorTile then + for i, tile in ipairs(g_map.getTiles(posz())) do + if tile:getPosition().x == pos.x and tile:getPosition().y == pos.y and tile:getPosition().z == pos.z then + doorTile = tile + end + end + end + + if not doorTile then + return false + end + + if not doorTile:isWalkable() then + if not key then + use(doorTile:getTopUseThing()) + delay(200) + return "retry" + else + useWith(key, doorTile:getTopUseThing()) + delay(200) + return "retry" + end + else + print("CaveBot[OpenDoors]: possible to cross, proceeding") + return true + end + end) + + CaveBot.Editor.registerAction("opendoors", "open doors", { + value=function() return posx() .. "," .. posy() .. "," .. posz() end, + title="Door position", + description="doors position (x,y,z) and key id (optional)", + multiline=false, + validation=[[\d{1,5},\d{1,5},\d{1,2}(?:,\d{1,5}$|$)]] +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/editor.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/editor.lua new file mode 100644 index 0000000000..cefaf2604a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/editor.lua @@ -0,0 +1,186 @@ +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 warn("CaveBot editor warn: 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.actionList:focusChild(widget) + 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("edit", function() + local action = CaveBot.actionList:getFocusedChild() + if not action or not action.onDoubleClick then return end + action.onDoubleClick(action) + 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("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),randomness (in percent-optional)", + multiline=false, + validation="^[0-9]{1,10}$|^[0-9]{1,10},[0-9]{1,4}$" + }) + 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]+),?\\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("follow", { + value="NPC name", + title="Follow Creature", + description="insert creature name to follow", + multiline=false + }) + registerAction("npcsay", { + value="text", + title="NPC Say", + description="Enter text to NPC 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 diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/editor.otui b/modules/game_bot/default_configs/vBot_4.8/cavebot/editor.otui new file mode 100644 index 0000000000..d11288c64d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/editor.otui @@ -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/") diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/example_functions.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/example_functions.lua new file mode 100644 index 0000000000..99252e805c --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/example_functions.lua @@ -0,0 +1,114 @@ +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("Check for PZ and wait until dropped", [[ +if retries > 25 or not isPzLocked() then + return true +else + if isPoisioned() then + say("exana pox") + end + if isPzLocked() then + delay(8000) + end + return "retry" +end +]]) + +addExampleFunction("Check for stamina and imbues", [[ + if stamina() < 900 or player:getSkillLevel(11) == 0 then CaveBot.setOff() return false else return true end +]]) + +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" +]]) + +addExampleFunction("Disable TargetBot", [[ +TargetBot.setOff() +return true +]]) + +addExampleFunction("Enable TargetBot", [[ +TargetBot.setOn() +return true +]]) + +addExampleFunction("Enable TargetBot luring", [[ +TargetBot.enableLuring() +return true +]]) + +addExampleFunction("Disable TargetBot luring", [[ +TargetBot.disableLuring() +return true +]]) + +addExampleFunction("Logout", [[ +g_game.safeLogout() +delay(1000) +return "retry" +]]) + +addExampleFunction("Close Loot Containers", [[ +CaveBot.CloseAllLootContainers() +delay(3000) +return true +]]) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/extension_template.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/extension_template.lua new file mode 100644 index 0000000000..d015f11beb --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/extension_template.lua @@ -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 diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/imbuing.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/imbuing.lua new file mode 100644 index 0000000000..64012a7f97 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/imbuing.lua @@ -0,0 +1,119 @@ +-- imbuing window should be handled separatly +-- reequiping should be handled separatly (ie. equipment manager) + +CaveBot.Extensions.Imbuing = {} + +local SHRINES = {25060, 25061, 25182, 25183} +local currentIndex = 1 +local shrine = nil +local item = nil +local currentId = 0 +local triedToTakeOff = false +local destination = nil + +local function reset() + EquipManager.setOn() + shrine = nil + currentIndex = 1 + item = nil + currentId = 0 + triedToTakeOff = false + destination = nil +end + +CaveBot.Extensions.Imbuing.setup = function() + CaveBot.registerAction("imbuing", "red", function(value, retries) + local data = string.split(value, ",") + local ids = {} + + if #data == 0 and value ~= 'name' then + warn("CaveBot[Imbuing] no items added, proceeding") + reset() + return false + end + + -- setting of equipment manager so it wont disturb imbuing process + EquipManager.setOff() + + if value == 'name' then + local imbuData = AutoImbueTable[player:getName()] + for id, imbues in pairs(imbuData) do + table.insert(ids, id) + end + else + -- convert to number + for i, id in ipairs(data) do + id = tonumber(id) + if not table.find(ids, id) then + table.insert(ids, id) + end + end + end + + -- all items imbued, can proceed + if currentIndex > #ids then + warn("CaveBot[Imbuing] used shrine on all items, proceeding") + reset() + return true + end + + for _, tile in ipairs(g_map.getTiles(posz())) do + for _, item in ipairs(tile:getItems()) do + local id = item:getId() + if table.find(SHRINES, id) then + shrine = item + break + end + end + end + + -- if not shrine + if not shrine then + warn("CaveBot[Imbuing] shrine not found! proceeding") + reset() + return false + end + + destination = shrine:getPosition() + + currentId = ids[currentIndex] + item = findItem(currentId) + + -- maybe equipped? try to take off + if not item then + -- did try before, still not found so item is unavailable + if triedToTakeOff then + warn("CaveBot[Imbuing] item not found! skipping: "..currentId) + triedToTakeOff = false + currentIndex = currentIndex + 1 + return "retry" + end + triedToTakeOff = true + g_game.equipItemId(currentId) + delay(1000) + return "retry" + end + + -- we are past unequiping so just in case we were forced before, reset var + triedToTakeOff = false + + -- reaching shrine + if not CaveBot.MatchPosition(destination, 1) then + CaveBot.GoTo(destination, 1) + delay(200) + return "retry" + end + + useWith(shrine, item) + currentIndex = currentIndex + 1 + warn("CaveBot[Imbuing] Using shrine on item: "..currentId) + delay(4000) + return "retry" + end) + + CaveBot.Editor.registerAction("imbuing", "imbuing", { + value="name", + title="Auto Imbuing", + description="insert below item ids to be imbued, separated by comma\nor 'name' to load from file", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/inbox_withdraw.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/inbox_withdraw.lua new file mode 100644 index 0000000000..d5fc02b483 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/inbox_withdraw.lua @@ -0,0 +1,91 @@ +CaveBot.Extensions.InWithdraw = {} + +CaveBot.Extensions.InWithdraw.setup = function() + CaveBot.registerAction("inwithdraw", "#002FFF", function(value, retries) + local data = string.split(value, ",") + local withdrawId + local amount + + -- validation + if #data ~= 2 then + warn("CaveBot[InboxWithdraw]: incorrect withdraw value") + return false + else + withdrawId = tonumber(data[1]) + amount = tonumber(data[2]) + end + + local currentAmount = itemAmount(withdrawId) + + if currentAmount >= amount then + print("CaveBot[InboxWithdraw]: enough items, proceeding") + return true + end + + if retries > 400 then + print("CaveBot[InboxWithdraw]: actions limit reached, proceeding") + return true + end + + -- actions + local inboxContainer = getContainerByName("your inbox") + delay(100) + if not inboxContainer then + if not CaveBot.ReachAndOpenInbox() then + return "retry" + end + end + local inboxAmount = 0 + if not inboxContainer then + return "retry" + end + for i, item in pairs(inboxContainer:getItems()) do + if item:getId() == withdrawId then + inboxAmount = inboxAmount + item:getCount() + end + end + if inboxAmount == 0 then + warn("CaveBot[InboxWithdraw]: not enough items in inbox container, proceeding") + g_game.close(inboxContainer) + return true + end + + local destination + for i, container in pairs(getContainers()) do + if container:getCapacity() > #container:getItems() and not string.find(container:getName():lower(), "quiver") and not string.find(container:getName():lower(), "depot") and not string.find(container:getName():lower(), "loot") and not string.find(container:getName():lower(), "inbox") then + destination = container + end + end + + if not destination then + print("CaveBot[InboxWithdraw]: couldn't find proper destination container, skipping") + g_game.close(inboxContainer) + return false + end + + CaveBot.PingDelay(2) + + for i, container in pairs(getContainers()) do + if string.find(container:getName():lower(), "your inbox") then + for j, item in pairs(container:getItems()) do + if item:getId() == withdrawId then + if item:isStackable() then + g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), math.min(item:getCount(), (amount - currentAmount))) + return "retry" + else + g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), 1) + return "retry" + end + return "retry" + end + end + end + end + end) + + CaveBot.Editor.registerAction("inwithdraw", "in withdraw", { + value="id,amount", + title="Withdraw Items", + description="insert item id and amount", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/lure.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/lure.lua new file mode 100644 index 0000000000..0cb5c54053 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/lure.lua @@ -0,0 +1,29 @@ +CaveBot.Extensions.Lure = {} + +CaveBot.Extensions.Lure.setup = function() + CaveBot.registerAction("lure", "#FF0090", function(value, retries) + value = value:lower() + if value == "start" then + TargetBot.setOff() + elseif value == "stop" then + TargetBot.setOn() + elseif value == "toggle" then + if TargetBot.isOn() then + TargetBot.setOff() + else + TargetBot.setOn() + end + else + warn("incorrect lure value!") + end + return true + end) + + CaveBot.Editor.registerAction("lure", "lure", { + value="toggle", + title="Lure", + description="TargetBot: start, stop, toggle", + multiline=false, + validation=[[(start|stop|toggle)$]] +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/minimap.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/minimap.lua new file mode 100644 index 0000000000..5ace5ee471 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/minimap.lua @@ -0,0 +1,26 @@ +local minimap = modules.game_minimap.minimapWidget + +minimap.onMouseRelease = function(widget,pos,button) + if not minimap.allowNextRelease then return true end + minimap.allowNextRelease = false + + local mapPos = minimap:getTilePosition(pos) + if not mapPos then return end + + if button == 1 then + local player = g_game.getLocalPlayer() + if minimap.autowalk then + player:autoWalk(mapPos) + end + return true + elseif button == 2 then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("minimapMenu") + menu:setGameMenu(true) + menu:addOption(tr('Create mark'), function() minimap:createFlagWindow(mapPos) end) + menu:addOption(tr('Add CaveBot GoTo'), function() CaveBot.addAction("goto", mapPos.x .. "," .. mapPos.y .. "," .. mapPos.z, true) CaveBot.save() end) + menu:display(pos) + return true + end + return false +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/pos_check.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/pos_check.lua new file mode 100644 index 0000000000..361ddb6da2 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/pos_check.lua @@ -0,0 +1,47 @@ +CaveBot.Extensions.PosCheck = {} + +local posCheckRetries = 0 +CaveBot.Extensions.PosCheck.setup = function() + CaveBot.registerAction("PosCheck", "#00FFFF", function(value, retries) + local tilePos + local data = string.split(value, ",") + if #data ~= 5 then + warn("wrong travel format, should be: label, distance, x, y, z") + return false + end + + local tilePos = player:getPosition() + + tilePos.x = tonumber(data[3]) + tilePos.y = tonumber(data[4]) + tilePos.z = tonumber(data[5]) + + if posCheckRetries > 10 then + posCheckRetries = 0 + print("CaveBot[CheckPos]: waypoints locked, too many tries, unclogging cavebot and proceeding") + return false + elseif (tilePos.z == player:getPosition().z) and (getDistanceBetween(player:getPosition(), tilePos) <= tonumber(data[2])) then + posCheckRetries = 0 + print("CaveBot[CheckPos]: position reached, proceeding") + return true + else + posCheckRetries = posCheckRetries + 1 + if data[1] == "last" then + CaveBot.gotoFirstPreviousReachableWaypoint() + print("CaveBot[CheckPos]: position not-reached, going back to first reachable waypoint.") + return false + else + CaveBot.gotoLabel(data[1]) + print("CaveBot[CheckPos]: position not-reached, going back to label: " .. data[1]) + return false + end + end + end) + + CaveBot.Editor.registerAction("poscheck", "pos check", { + value=function() return "last" .. "," .. "10" .. "," .. posx() .. "," .. posy() .. "," .. posz() end, + title="Location Check", + description="label name, accepted dist from coordinates, x, y, z", + multiline=false, +}) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/recorder.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/recorder.lua new file mode 100644 index 0000000000..14248f378b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/recorder.lua @@ -0,0 +1,69 @@ +-- 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 + local function addStairs(pos) + CaveBot.addAction("goto", pos.x .. "," .. pos.y .. "," .. pos.z .. ",0", 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 + addStairs(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", 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 .. "," .. targetPos.x .. "," .. targetPos.y .. "," .. targetPos.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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/sell_all.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/sell_all.lua new file mode 100644 index 0000000000..c3d40f3e11 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/sell_all.lua @@ -0,0 +1,76 @@ +CaveBot.Extensions.SellAll = {} + +local sellAllCap = 0 +CaveBot.Extensions.SellAll.setup = function() + CaveBot.registerAction("SellAll", "#C300FF", function(value, retries) + local val = string.split(value, ",") + local wait + + -- table formatting + for i, v in ipairs(val) do + v = v:trim() + v = tonumber(v) or v + val[i] = v + end + + if table.find(val, "yes", true) then + wait = true + end + + local npcName = val[1] + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[SellAll]: NPC not found! skipping") + return false + end + + if retries > 10 then + print("CaveBot[SellAll]: can't sell, skipping") + return false + end + + if freecap() == sellAllCap then + sellAllCap = 0 + print("CaveBot[SellAll]: Sold everything, proceeding") + return true + end + + delay(800) + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + if not NPC.isTrading() then + CaveBot.OpenNpcTrade() + delay(storage.extras.talkDelay*2) + return "retry" + else + sellAllCap = freecap() + end + + storage.cavebotSell = storage.cavebotSell or {} + for i, item in ipairs(storage.cavebotSell) do + local data = type(item) == 'number' and item or item.id + if not table.find(val, data) then + table.insert(val, data) + end + end + + table.dump(val) + + modules.game_npctrade.sellAll(wait, val) + if wait then + print("CaveBot[SellAll]: Sold All with delay") + else + print("CaveBot[SellAll]: Sold All without delay") + end + + return "retry" + end) + + CaveBot.Editor.registerAction("sellall", "sell all", { + value="NPC", + title="Sell All", + description="NPC Name, 'yes' if sell with delay, exceptions: id separated by comma", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/stand_lure.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/stand_lure.lua new file mode 100644 index 0000000000..7230bdd999 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/stand_lure.lua @@ -0,0 +1,186 @@ +CaveBot.Extensions.StandLure = {} +local enable = nil + +local function modPos(dir) + local y = 0 + local x = 0 + + if dir == 0 then + y = -1 + elseif dir == 1 then + x = 1 + elseif dir == 2 then + y = 1 + elseif dir == 3 then + x = -1 + elseif dir == 4 then + y = -1 + x = 1 + elseif dir == 5 then + y = 1 + x = 1 + elseif dir == 6 then + y = 1 + x = -1 + elseif dir == 7 then + y = -1 + x = -1 + end + + return {x, y} +end +local function reset(delay) + if type(Supplies.hasEnough()) == 'table' then + return + end + delay = delay or 0 + CaveBot.delay(delay) + if delay == nil then + enable = nil + end +end + +local resetRetries = false +CaveBot.Extensions.StandLure.setup = function() + CaveBot.registerAction( + "rushlure", + "#FF0090", + function(value, retries) + local nextPos = nil + local data = string.split(value, ",") + if not data[1] then + warn("Invalid cavebot lure action value. It should be position (x,y,z), delay(ms) is: " .. value) + return false + end + + if type(Supplies.hasEnough()) == 'table' then -- do not execute if no supplies + return false + end + + local pos = {x = tonumber(data[1]), y = tonumber(data[2]), z = tonumber(data[3])} + + local delayTime = data[4] and tonumber(data[4]) or 1000 + if not data[5] then + enable = nil + elseif data[5] == "yes" then + enable = true + else + enable = false + end + + delay(100) + + if retries > 50 and not resetRetries then + reset() + warn("[Rush Lure] Too many tries, can't reach position") + return false -- can't stand on tile + end + + if resetRetries then + resetRetries = false + end + + if distanceFromPlayer(pos) > 30 then + reset() + return false -- not reachable + end + + local playerPos = player:getPosition() + local pathWithoutMonsters = findPath(playerPos, pos, 30, { ignoreFields = true, ignoreNonPathable = true, ignoreCreatures = true, precision = 0}) + local pathWithMonsters = findPath(playerPos, pos, maxDist, { ignoreFields = true, ignoreNonPathable = true, ignoreCreatures = false, precision = 0 }) + + if not pathWithoutMonsters then + reset() + warn("[Rush Lure] No possible path to reach position, skipping.") + return false -- spot is unreachable + elseif pathWithoutMonsters and not pathWithMonsters then + local foundMonster = false + for i, dir in ipairs(pathWithoutMonsters) do + local dirs = modPos(dir) + nextPos = nextPos or playerPos + nextPos.x = nextPos.x + dirs[1] + nextPos.y = nextPos.y + dirs[2] + + + local tile = g_map.getTile(nextPos) + if tile then + if tile:hasCreature() then + local creature = tile:getCreatures()[1] + local hppc = creature:getHealthPercent() + if creature:isMonster() and (hppc and hppc > 0) and (oldTibia or creature:getType() < 3) then + -- real blocking creature can not meet those conditions - ie. it could be player, so just in case check if the next creature is reachable + local path = findPath(playerPos, creature:getPosition(), 7, { ignoreNonPathable = true, precision = 1 }) + if path then + creature:setMarked('#00FF00') + if g_game.getAttackingCreature() ~= creature then + attack(creature) + end + g_game.setChaseMode(1) + resetRetries = true -- reset retries, we are trying to unclog the cavebot + delay(100) + return "retry" + end + end + end + end + end + + if not g_game.getAttackingCreature() then + reset() + warn("[Rush Lure] No path, no blocking monster, skipping.") + return false -- no other way + end + end + + -- reaching position, delay targetbot in process + if not CaveBot.MatchPosition(pos, 0) then + TargetBot.delay(300) + CaveBot.walkTo(pos, 30, { ignoreCreatures = false, ignoreFields = true, ignoreNonPathable = true, precision = 0}) + delay(100) + resetRetries = true + return "retry" + end + + TargetBot.setOn() + reset(delayTime) + return true + end + ) + + CaveBot.Editor.registerAction( + "rushlure", + "rush lure", + { + value = function() + return posx() .. "," .. posy() .. "," .. posz() .. ",1000" + end, + title = "Stand Lure", + description = "Run to position(x,y,z), delay(ms), targetbot on/off (yes/no)", + multiline = false, + validation = [[\d{1,5},\d{1,5},\d{1,2},\d{1,5}(?:,(yes|no)$|$)]] + } + ) +end + +local next = false +schedule(5, function() -- delay because cavebot.lua is loaded after this file + modules.game_bot.connect(CaveBotList(), { + onChildFocusChange = function(widget, newChild, oldChild) + + if oldChild and oldChild.action == "rushlure" then + next = true + return + end + + if next then + if enable then + TargetBot.setOn() + elseif enable == false then + TargetBot.setOff() + end + + enable = nil -- reset + next = false + end + end}) +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/supply_check.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/supply_check.lua new file mode 100644 index 0000000000..c694bdeb6e --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/supply_check.lua @@ -0,0 +1,137 @@ +CaveBot.Extensions.SupplyCheck = {} + +local supplyRetries = 0 +local missedChecks = 0 +local rawRound = 0 +local time = now +vBot.CaveBotData = + vBot.CaveBotData or + { + refills = 0, + rounds = 0, + time = {}, + lastRefill = os.time(), + refillTime = {} + } + +local function setCaveBotData(hunting) + if hunting then + supplyRetries = supplyRetries + 1 + else + supplyRetries = 0 + table.insert(vBot.CaveBotData.refillTime, os.difftime(os.time() - vBot.CaveBotData.lastRefill)) + vBot.CaveBotData.lastRefill = os.time() + vBot.CaveBotData.refills = vBot.CaveBotData.refills + 1 + end + + table.insert(vBot.CaveBotData.time, rawRound) + vBot.CaveBotData.rounds = vBot.CaveBotData.rounds + 1 + missedChecks = 0 +end + +CaveBot.Extensions.SupplyCheck.setup = function() + CaveBot.registerAction( + "supplyCheck", + "#db5a5a", + function(value) + local data = string.split(value, ",") + local round = 0 + rawRound = 0 + local label = data[1]:trim() + local pos = nil + if #data == 4 then + pos = {x = tonumber(data[2]), y = tonumber(data[3]), z = tonumber(data[4])} + end + + if pos then + if missedChecks >= 4 then + missedChecks = 0 + supplyRetries = 0 + print("CaveBot[SupplyCheck]: Missed 5 supply checks, proceeding with waypoints") + return true + end + if getDistanceBetween(player:getPosition(), pos) > 10 then + missedChecks = missedChecks + 1 + print("CaveBot[SupplyCheck]: Missed supply check! " .. 5 - missedChecks .. " tries left before skipping.") + return CaveBot.gotoLabel(label) + end + end + + if time then + rawRound = math.ceil((now - time) / 1000) + round = rawRound .. "s" + else + round = "" + end + time = now + + local softCount = itemAmount(6529) + itemAmount(3549) + local supplyData = Supplies.hasEnough() + local supplyInfo = Supplies.getAdditionalData() + + if storage.caveBot.forceRefill then + print("CaveBot[SupplyCheck]: User forced, going back on refill. Last round took: " .. round) + storage.caveBot.forceRefill = false + supplyRetries = 0 + missedChecks = 0 + return false + elseif storage.caveBot.backStop then + print("CaveBot[SupplyCheck]: User forced, going back to city and turning off CaveBot. Last round took: " .. round) + supplyRetries = 0 + missedChecks = 0 + return false + elseif storage.caveBot.backTrainers then + print("CaveBot[SupplyCheck]: User forced, going back to city, then on trainers. Last round took: " .. round) + supplyRetries = 0 + missedChecks = 0 + return false + elseif storage.caveBot.backOffline then + print("CaveBot[SupplyCheck]: User forced, going back to city, then on offline training. Last round took: " .. round) + supplyRetries = 0 + missedChecks = 0 + return false + elseif supplyRetries > (storage.extras.huntRoutes or 50) then + print("CaveBot[SupplyCheck]: Round limit reached, going back on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.imbues.enabled and player:getSkillLevel(11) == 0) then + print("CaveBot[SupplyCheck]: Imbues ran out. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.stamina.enabled and stamina() < tonumber(supplyInfo.stamina.value)) then + print("CaveBot[SupplyCheck]: Stamina ran out. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.softBoots.enabled and softCount < 1) then + print("CaveBot[SupplyCheck]: No soft boots left. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif type(supplyData) == "table" then + print("CaveBot[SupplyCheck]: Not enough item: " .. supplyData.id .. "(only " .. supplyData.amount .. " left). Going on refill. Last round took: " .. round) + setCaveBotData() + return false + elseif (supplyInfo.capacity.enabled and freecap() < tonumber(supplyInfo.capacity.value)) then + print("CaveBot[SupplyCheck]: Not enough capacity. Going on refill. Last round took: " .. round) + setCaveBotData() + return false + else + print("CaveBot[SupplyCheck]: Enough supplies. Hunting. Round (" .. supplyRetries .. "/" .. (storage.extras.huntRoutes or 50) .. "). Last round took: " .. round) + setCaveBotData(true) + return CaveBot.gotoLabel(label) + end + end + ) + + CaveBot.Editor.registerAction( + "supplycheck", + "supply check", + { + value = function() + return "startHunt," .. posx() .. "," .. posy() .. "," .. posz() + end, + title = "Supply check label", + description = "Insert here hunting start label", + validation = [[[^,]+,\d{1,5},\d{1,5},\d{1,2}$]] + } + ) +end diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/tasker.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/tasker.lua new file mode 100644 index 0000000000..959515e62c --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/tasker.lua @@ -0,0 +1,178 @@ +CaveBot.Extensions.Tasker = {} + +local dataValidationFailed = function() + print("CaveBot[Tasker]: data validation failed! incorrect data, check cavebot/tasker for more info") + return false +end + +-- miniconfig +local talkDelay = storage.extras.talkDelay +if not storage.caveBotTasker then + storage.caveBotTasker = { + inProgress = false, + monster = "", + taskName = "", + count = 0, + max = 0 + } +end + +local resetTaskData = function() + storage.caveBotTasker.inProgress = false + storage.caveBotTasker.monster = "" + storage.caveBotTasker.monster2 = "" + storage.caveBotTasker.taskName = "" + storage.caveBotTasker.count = 0 + storage.caveBotTasker.max = 0 +end + +CaveBot.Extensions.Tasker.setup = function() + CaveBot.registerAction("Tasker", "#FF0090", function(value, retries) + local taskName = "" + local monster = "" + local monster2 = "" + local count = 0 + local label1 = "" + local label2 = "" + local task + + local data = string.split(value, ",") + if not data or #data < 1 then + dataValidationFailed() + end + local marker = tonumber(data[1]) + + if not marker then + dataValidationFailed() + resetTaskData() + elseif marker == 1 then + if getNpcs(3) == 0 then + print("CaveBot[Tasker]: no NPC found in range! skipping") + return false + end + if #data ~= 4 and #data ~= 5 then + dataValidationFailed() + resetTaskData() + else + taskName = data[2]:lower():trim() + count = tonumber(data[3]:trim()) + monster = data[4]:lower():trim() + if #data == 5 then + monster2 = data[5]:lower():trim() + end + end + elseif marker == 2 then + if #data ~= 3 then + dataValidationFailed() + else + label1 = data[2]:lower():trim() + label2 = data[3]:lower():trim() + end + elseif marker == 3 then + if getNpcs(3) == 0 then + print("CaveBot[Tasker]: no NPC found in range! skipping") + return false + end + if #data ~= 1 then + dataValidationFailed() + end + end + + -- let's cover markers now + if marker == 1 then -- starting task + CaveBot.Conversation("hi", "task", taskName, "yes") + delay(talkDelay*4) + + storage.caveBotTasker.monster = monster + if monster2 then storage.caveBotTasker.monster2 = monster2 end + storage.caveBotTasker.taskName = taskName + storage.caveBotTasker.inProgress = true + storage.caveBotTasker.max = count + storage.caveBotTasker.count = 0 + + print("CaveBot[Tasker]: taken task for: " .. monster .. " x" .. count) + return true + elseif marker == 2 then -- only checking + if not storage.caveBotTasker.inProgress then + CaveBot.gotoLabel(label2) + print("CaveBot[Tasker]: there is no task in progress so going to take one.") + return true + end + + local max = storage.caveBotTasker.max + local count = storage.caveBotTasker.count + + if count >= max then + CaveBot.gotoLabel(label2) + print("CaveBot[Tasker]: task completed: " .. storage.caveBotTasker.taskName) + return true + else + CaveBot.gotoLabel(label1) + print("CaveBot[Tasker]: task in progress, left: " .. max - count .. " " .. storage.caveBotTasker.taskName) + return true + end + + + elseif marker == 3 then -- reporting task + CaveBot.Conversation("hi", "report", "task") + delay(talkDelay*3) + + resetTaskData() + print("CaveBot[Tasker]: task reported, done") + return true + end + + end) + + CaveBot.Editor.registerAction("tasker", "tasker", { + value=[[ There is 3 scenarios for this extension, as example we will use medusa: + + 1. start task, + parameters: + - scenario for extension: 1 + - task name in gryzzly adams: medusae + - monster count: 500 + - monster name to track: medusa + - optional, monster name 2: + 2. check status, + to be used on refill to decide whether to go back or spawn or go give task back + parameters: + - scenario for extension: 2 + - label if task in progress: skipTask + - label if task done: taskDone + 3. report task, + parameters: + - scenario for extension: 3 + + Strong suggestion, almost mandatory - USE POS CHECK to verify position! this module will only check if there is ANY npc in range! + + when begin remove all the text and leave just a single string of parameters + some examples: + + 2, skipReport, goReport + 3 + 1, drakens, 500, draken warmaster, draken spellweaver + 1, medusae, 500, medusa]], + title="Tasker", + multiline = true + }) +end + +local regex = "Loot of ([a-z])* ([a-z A-Z]*):" +local regex2 = "Loot of ([a-z A-Z]*):" +onTextMessage(function(mode, text) + -- if CaveBot.isOff() then return end + if not text:lower():find("loot of") then return end + if #regexMatch(text, regex) == 1 and #regexMatch(text, regex)[1] == 3 then + monster = regexMatch(text, regex)[1][3] + elseif #regexMatch(text, regex2) == 1 and #regexMatch(text, regex2)[1] == 2 then + monster = regexMatch(text, regex2)[1][2] + end + + local m1 = storage.caveBotTasker.monster + local m2 = storage.caveBotTasker.monster2 + + if monster == m1 or monster == m2 and storage.caveBotTasker.count then + storage.caveBotTasker.count = storage.caveBotTasker.count + 1 + end +end) diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/travel.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/travel.lua new file mode 100644 index 0000000000..8e9d21e5b6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/travel.lua @@ -0,0 +1,40 @@ +CaveBot.Extensions.Travel = {} + +CaveBot.Extensions.Travel.setup = function() + CaveBot.registerAction("Travel", "#db5a5a", function(value, retries) + local data = string.split(value, ",") + if #data < 2 then + warn("CaveBot[Travel]: incorrect travel value!") + return false + end + + local npcName = data[1]:trim() + local dest = data[2]:trim() + + if retries > 5 then + print("CaveBot[Travel]: too many tries, can't travel") + return false + end + + local npc = getCreatureByName(npcName) + if not npc then + print("CaveBot[Travel]: NPC not found, can't travel") + return false + end + + if not CaveBot.ReachNPC(npcName) then + return "retry" + end + + CaveBot.Travel(dest) + delay(storage.extras.talkDelay*3) + print("CaveBot[Travel]: travel action finished") + return true + end) + + CaveBot.Editor.registerAction("travel", "travel", { + value="NPC name, city", + title="Travel", + description="NPC name, City name, delay in ms(default is 200ms)", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/walking.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/walking.lua new file mode 100644 index 0000000000..c8a713366b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/walking.lua @@ -0,0 +1,93 @@ +-- walking +local expectedDirs = {} +local isWalking = {} +local walkPath = {} +local walkPathIter = 0 + +CaveBot.resetWalking = function() + expectedDirs = {} + walkPath = {} + isWalking = false +end + +CaveBot.doWalking = function() + if CaveBot.Config.get("mapClick") then + return false + end + if #expectedDirs == 0 then + return false + end + if #expectedDirs >= 3 then + CaveBot.resetWalking() + end + local dir = walkPath[walkPathIter] + if dir then + g_game.walk(dir, false) + table.insert(expectedDirs, dir) + walkPathIter = walkPathIter + 1 + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + return true + end + return false +end + +-- called when player position has been changed (step has been confirmed by server) +onPlayerPositionChange(function(newPos, oldPos) + if not oldPos or not newPos then return end + + local dirs = {{NorthWest, North, NorthEast}, {West, 8, East}, {SouthWest, South, SouthEast}} + local dir = dirs[newPos.y - oldPos.y + 2] + if dir then + dir = dir[newPos.x - oldPos.x + 2] + end + if not dir then + dir = 8 -- 8 is invalid dir, it's fine + end + + if not isWalking or not expectedDirs[1] then + -- some other walk action is taking place (for example use on ladder), wait + walkPath = {} + CaveBot.delay(CaveBot.Config.get("ping") + player:getStepDuration(false, dir) + 150) + return + end + + if expectedDirs[1] ~= dir then + if CaveBot.Config.get("mapClick") then + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + else + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + player:getStepDuration(false, dir)) + end + return + end + + table.remove(expectedDirs, 1) + if CaveBot.Config.get("mapClick") and #expectedDirs > 0 then + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + player:getStepDuration(false, dir)) + end +end) + +CaveBot.walkTo = function(dest, maxDist, params) + local path = getPath(player:getPosition(), dest, maxDist, params) + if not path or not path[1] then + return false + end + local dir = path[1] + + if CaveBot.Config.get("mapClick") then + local ret = autoWalk(path) + if ret then + isWalking = true + expectedDirs = path + CaveBot.delay(CaveBot.Config.get("mapClickDelay") + math.max(CaveBot.Config.get("ping") + player:getStepDuration(false, dir), player:getStepDuration(false, dir) * 2)) + end + return ret + end + + g_game.walk(dir, false) + isWalking = true + walkPath = path + walkPathIter = 2 + expectedDirs = { dir } + CaveBot.delay(CaveBot.Config.get("walkDelay") + player:getStepDuration(false, dir)) + return true +end diff --git a/modules/game_bot/default_configs/vBot_4.8/cavebot/withdraw.lua b/modules/game_bot/default_configs/vBot_4.8/cavebot/withdraw.lua new file mode 100644 index 0000000000..da29053277 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/cavebot/withdraw.lua @@ -0,0 +1,56 @@ +CaveBot.Extensions.Withdraw = {} + +CaveBot.Extensions.Withdraw.setup = function() + CaveBot.registerAction("withdraw", "#002FFF", function(value, retries) + -- validation + local data = string.split(value, ",") + if #data ~= 3 then + print("CaveBot[Withdraw]: incorrect data! skipping") + return false + end + + -- variables declaration + local source = tonumber(data[1]) + local id = tonumber(data[2]) + local amount = tonumber(data[3]) + + -- validation for correct values + if not id or not amount then + print("CaveBot[Withdraw]: incorrect id or amount! skipping") + return false + end + + -- check for retries + if retries > 100 then + print("CaveBot[Withdraw]: actions limit reached, proceeding") + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + return true + end + + -- check for items + if itemAmount(id) >= amount then + print("CaveBot[Withdraw]: enough items, proceeding") + for i, container in ipairs(getContainers()) do + if container:getName():lower():find("depot") or container:getName():lower():find("locker") then + g_game.close(container) + end + end + return true + end + + statusMessage("[Withdraw] withdrawing item: " ..id.. " x"..amount) + CaveBot.WithdrawItem(id, amount, source) + CaveBot.PingDelay() + return "retry" + end) + + CaveBot.Editor.registerAction("withdraw", "withdraw", { + value="source,id,amount", + title="Withdraw Items", + description="index/inbox, item id and amount", + }) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/creature.lua b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature.lua new file mode 100644 index 0000000000..225bcec141 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature.lua @@ -0,0 +1,99 @@ + +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 = "" + for part in string.gmatch(config.name, "[^,]+") do + if config.regex:len() > 0 then + config.regex = config.regex .. "|" + end + config.regex = config.regex .. "^" .. part:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$" + end + 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 diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_attack.lua b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_attack.lua new file mode 100644 index 0000000000..4c31644c8d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_attack.lua @@ -0,0 +1,245 @@ +local targetBotLure = false +local targetCount = 0 +local delayValue = 0 +local lureMax = 0 +local anchorPosition = nil +local lastCall = now +local delayFrom = nil +local dynamicLureDelay = false + +function getWalkableTilesCount(position) + local count = 0 + + for i, tile in pairs(getNearTiles(position)) do + if tile:isWalkable() or tile:hasCreature() then + count = count + 1 + end + end + + return count +end + +function rePosition(minTiles) + minTiles = minTiles or 8 + if now - lastCall < 500 then return end + local pPos = player:getPosition() + local tiles = getNearTiles(pPos) + local playerTilesCount = getWalkableTilesCount(pPos) + local tilesTable = {} + + if playerTilesCount > minTiles then return end + for i, tile in ipairs(tiles) do + tilesTable[tile] = not tile:hasCreature() and tile:isWalkable() and getWalkableTilesCount(tile:getPosition()) or nil + end + + local best = 0 + local target = nil + for k,v in pairs(tilesTable) do + if v > best and v > playerTilesCount then + best = v + target = k:getPosition() + end + end + + if target then + lastCall = now + return CaveBot.GoTo(target, 0) + end +end + +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() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) 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.useGroupAttackRune and config.groupAttackRune > 100 then + local creatures = g_map.getSpectatorsInRange(creature:getPosition(), false, config.groupRuneAttackRadius, config.groupRuneAttackRadius) + local playersAround = false + local monsters = 0 + for _, creature in ipairs(creatures) do + if not creature:isLocalPlayer() and creature:isPlayer() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) then + playersAround = true + elseif creature:isMonster() then + monsters = monsters + 1 + end + end + if monsters >= config.groupRuneAttackTargets and (not playersAround or config.groupAttackIgnorePlayers) then + if TargetBot.useAttackItem(config.groupAttackRune, 0, creature, config.groupRuneAttackDelay) 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, 0, creature, config.attackRuneDelay) then + return + end + end +end + +TargetBot.Creature.walk = function(creature, config, targets) + local cpos = creature:getPosition() + local pos = player:getPosition() + + local isTrapped = true + local pos = player:getPosition() + local dirs = {{-1,1}, {0,1}, {1,1}, {-1, 0}, {1, 0}, {-1, -1}, {0, -1}, {1, -1}} + for i=1,#dirs do + local tile = g_map.getTile({x=pos.x-dirs[i][1],y=pos.y-dirs[i][2],z=pos.z}) + if tile and tile:isWalkable(false) then + isTrapped = false + end + end + + -- data for external dynamic lure + if config.lureMin and config.lureMax and config.dynamicLure then + if config.lureMin >= targets then + targetBotLure = true + elseif targets >= config.lureMax then + targetBotLure = false + end + end + targetCount = targets + delayValue = config.lureDelay + + if config.lureMax then + lureMax = config.lureMax + end + + dynamicLureDelay = config.dynamicLureDelay + delayFrom = config.delayFrom + + -- luring + if config.closeLure and config.closeLureAmount <= getMonsters(1) then + return TargetBot.allowCaveBot(150) + end + if TargetBot.canLure() and (config.lure or config.lureCavebot or config.dynamicLure) and not (creature:getHealthPercent() < (storage.extras.killUnder or 30)) and not isTrapped then + if targetBotLure then + anchorPosition = nil + return TargetBot.allowCaveBot(150) + else + if targets < config.lureCount then + if config.lureCavebot then + anchorPosition = nil + return TargetBot.allowCaveBot(150) + else + local path = findPath(pos, cpos, 5, {ignoreNonPathable=true, precision=2}) + if path then + return TargetBot.walkTo(cpos, 10, {marginMin=5, marginMax=6, ignoreNonPathable=true}) + end + end + end + end + end + + local currentDistance = findPath(pos, cpos, 10, {ignoreCreatures=true, ignoreNonPathable=true, ignoreCost=true}) + if (not config.chase or #currentDistance == 1) and not config.avoidAttacks and not config.keepDistance and config.rePosition and (creature:getHealthPercent() >= storage.extras.killUnder) then + return rePosition(config.rePositionAmount or 6) + end + if ((storage.extras.killUnder > 1 and (creature:getHealthPercent() < storage.extras.killUnder)) or config.chase) and not config.keepDistance then + if #currentDistance > 1 then + return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, precision=1}) + end + elseif config.keepDistance then + if not anchorPosition or distanceFromPlayer(anchorPosition) > config.anchorRange then + anchorPosition = pos + end + if #currentDistance ~= config.keepDistanceRange and #currentDistance ~= config.keepDistanceRange + 1 then + if config.anchor and anchorPosition and getDistanceBetween(pos, anchorPosition) <= config.anchorRange*2 then + return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1, maxDistanceFrom={anchorPosition, config.anchorRange}}) + else + return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1}) + end + end + end + + --target only movement + if config.avoidAttacks then + local diffx = cpos.x - pos.x + local diffy = cpos.y - pos.y + local candidates = {} + if math.abs(diffx) == 1 and diffy == 0 then + candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}} + elseif diffx == 0 and math.abs(diffy) == 1 then + candidates = {{x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x+1, y=pos.y, z=pos.z}} + end + for _, candidate in ipairs(candidates) do + local tile = g_map.getTile(candidate) + if tile and tile:isWalkable() then + return TargetBot.walkTo(candidate, 2, {ignoreNonPathable=true}) + end + end + elseif config.faceMonster then + local diffx = cpos.x - pos.x + local diffy = cpos.y - pos.y + local candidates = {} + if diffx == 1 and diffy == 1 then + candidates = {{x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}} + elseif diffx == -1 and diffy == 1 then + candidates = {{x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}} + elseif diffx == -1 and diffy == -1 then + candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x-1, y=pos.y, z=pos.z}} + elseif diffx == 1 and diffy == -1 then + candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x+1, y=pos.y, z=pos.z}} + else + local dir = player:getDirection() + if diffx == 1 and dir ~= 1 then turn(1) + elseif diffx == -1 and dir ~= 3 then turn(3) + elseif diffy == 1 and dir ~= 2 then turn(2) + elseif diffy == -1 and dir ~= 0 then turn(0) + end + end + for _, candidate in ipairs(candidates) do + local tile = g_map.getTile(candidate) + if tile and tile:isWalkable() then + return TargetBot.walkTo(candidate, 2, {ignoreNonPathable=true}) + end + end + end +end + +onPlayerPositionChange(function(newPos, oldPos) + if CaveBot.isOff() then return end + if TargetBot.isOff() then return end + if not lureMax then return end + if storage.TargetBotDelayWhenPlayer then return end + if not dynamicLureDelay then return end + + if targetCount < (delayFrom or lureMax/2) or not target() then return end + CaveBot.delay(delayValue or 0) +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.lua b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.lua new file mode 100644 index 0000000000..37d6d0014b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.lua @@ -0,0 +1,106 @@ +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.content.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.content.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.content.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.content.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 = "" + for part in string.gmatch(newConfig.name, "[^,]+") do + if newConfig.regex:len() > 0 then + newConfig.regex = newConfig.regex .. "|" + end + newConfig.regex = newConfig.regex .. "^" .. part:trim():lower():gsub("%*", ".*"):gsub("%?", ".?") .. "$" + end + + 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("anchorRange", "Anchoring Range", 1, 10, 3) + addScrollBar("lureCount", "Classic Lure", 0, 5, 1) + addScrollBar("lureMin", "Dynamic lure min", 0, 29, 1) + addScrollBar("lureMax", "Dynamic lure max", 1, 30, 3) + addScrollBar("lureDelay", "Dynamic lure delay", 100, 1000, 250) + addScrollBar("delayFrom", "Start delay when monsters", 1, 29, 2) + addScrollBar("rePositionAmount", "Min tiles to rePosition", 0, 7, 5) + addScrollBar("closeLureAmount", "Close Pull Until", 0, 8, 3) + + addCheckBox("chase", "Chase", true) + addCheckBox("keepDistance", "Keep Distance", false) + addCheckBox("anchor", "Anchoring", false) + addCheckBox("dontLoot", "Don't loot", false) + addCheckBox("lure", "Lure", false) + addCheckBox("lureCavebot", "Lure using cavebot", false) + addCheckBox("faceMonster", "Face monsters", false) + addCheckBox("avoidAttacks", "Avoid wave attacks", false) + addCheckBox("dynamicLure", "Dynamic lure", false) + addCheckBox("dynamicLureDelay", "Dynamic lure delay", false) + addCheckBox("diamondArrows", "D-Arrows priority", false) + addCheckBox("rePosition", "rePosition to better tile", false) + addCheckBox("closeLure", "Close Pulling Monsters", false) + addCheckBox("rpSafe", "RP PVP SAFE - (DA)", false) +end diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.otui b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.otui new file mode 100644 index 0000000000..9570f8774f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_editor.otui @@ -0,0 +1,164 @@ +TargetBotCreatureEditorScrollBar < Panel + height: 28 + margin-top: 3 + + 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: 3 + 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: 425 + + $mobile: + height: 300 + + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-align: center + !text: tr('You can use * (any characters) and ? (any character) in target name') + + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-align: center + !text: tr('You can also enter multiple targets, separate them by ,') + + 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: + + VerticalScrollBar + id: contentScroll + anchors.top: name.bottom + anchors.right: parent.right + anchors.bottom: help.top + step: 28 + pixels-scroll: true + margin-right: -10 + margin-top: 5 + margin-bottom: 5 + + ScrollablePanel + id: content + anchors.top: name.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: help.top + vertical-scrollbar: contentScroll + margin-bottom: 10 + + Panel + id: left + anchors.top: parent.top + 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: parent.top + 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 diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_priority.lua b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_priority.lua new file mode 100644 index 0000000000..813d3a620b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/creature_priority.lua @@ -0,0 +1,61 @@ +TargetBot.Creature.calculatePriority = function(creature, config, path) + -- config is based on creature_editor + local priority = 0 + local currentTarget = g_game.getAttackingCreature() + + -- extra priority if it's current target + if currentTarget == creature then + priority = priority + 1 + end + + -- check if distance is ok + if #path > config.maxDistance then + if config.rpSafe then + if currentTarget == creature then + g_game.cancelAttackAndFollow() -- if not, stop attack (pvp safe) + end + end + return priority + end + + -- add config priority + priority = priority + config.priority + + -- extra priority for close distance + local path_length = #path + if path_length == 1 then + priority = priority + 10 + elseif path_length <= 3 then + priority = priority + 5 + end + + -- extra priority for paladin diamond arrows + if config.diamondArrows then + local mobCount = getCreaturesInArea(creature:getPosition(), diamondArrowArea, 2) + priority = priority + (mobCount * 4) + + if config.rpSafe then + if getCreaturesInArea(creature:getPosition(), largeRuneArea, 3) > 0 then + if currentTarget == creature then + g_game.cancelAttackAndFollow() + end + return 0 -- pvp safe + end + end + 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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/looting.lua b/modules/game_bot/default_configs/vBot_4.8/targetbot/looting.lua new file mode 100644 index 0000000000..9e41528b97 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/looting.lua @@ -0,0 +1,324 @@ +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 + --vBot + vBot.lootConainers = {} + vBot.lootItems = {} + for i, item in ipairs(ui.containers:getItems()) do + table.insert(vBot.lootConainers, item['id']) + end + for i, item in ipairs(ui.items:getItems()) do + table.insert(vBot.lootItems, item['id']) + end +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 = storage.extras.lootLast and TargetBot.Looting.list[#TargetBot.Looting.list] or 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)) + local maxRange = storage.extras.looting or 40 + if loot.tries > 30 or loot.pos.z ~= pos.z or dist > maxRange then + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 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 not container:isContainer() then + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 1) + return true + end + + g_game.open(container) + waitTill = now + (storage.extras.lootDelay or 200) + waitingForContainer = container:getId() + + 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() or container:hasPages() 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() and not itemsById[item:getId()] then + nextContainer = item + elseif itemsById[item:getId()] or (ui.everyItem:isOn() and not item:isContainer()) 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 = nextContainer:getId() + return + end + end + + -- looting finished, remove container from list + container.lootContainer = false + g_game.close(container) + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 1) +end + +onTextMessage(function(mode, text) + if TargetBot.isOff() then return end + if #TargetBot.Looting.list == 0 then return end + if string.find(text:lower(), "you are not the owner") then -- if we are not the owners of corpse then its a waste of time to try to loot it + table.remove(TargetBot.Looting.list, storage.extras.lootLast and #TargetBot.Looting.list or 1) + end +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 isInPz() then return end + if not TargetBot.isOn() then return end + if not creature:isMonster() then return end + local config = TargetBot.Creature.calculateParams(creature, {}) -- return {craeture, config, danger, priority} + if not config.config or config.config.dontLoot 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}) + + table.sort(TargetBot.Looting.list, function(a,b) + a.dist = distanceFromPlayer(a.pos) + b.dist = distanceFromPlayer(b.pos) + + return a.dist > b.dist + end) + container:setMarked('#000088') + end) +end) diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/looting.otui b/modules/game_bot/default_configs/vBot_4.8/targetbot/looting.otui new file mode 100644 index 0000000000..aa973e3805 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/looting.otui @@ -0,0 +1,69 @@ +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 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/target.lua b/modules/game_bot/default_configs/vBot_4.8/targetbot/target.lua new file mode 100644 index 0000000000..8bfb499702 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/target.lua @@ -0,0 +1,328 @@ +local targetbotMacro = nil +local config = nil +local lastAction = 0 +local cavebotAllowance = 0 +local lureEnabled = true +local dangerValue = 0 +local looterStatus = "" + +-- 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 + +local oldTibia = g_game.getClientVersion() < 960 + +-- main loop, controlled by config +targetbotMacro = macro(100, function() + local pos = player:getPosition() + local specs = g_map.getSpectatorsInRange(pos, false, 6, 6) -- 12x12 area + local creatures = 0 + for i, spec in ipairs(specs) do + if spec:isMonster() then + creatures = creatures + 1 + end + end + if creatures > 10 then -- if there are too many monsters around, limit area + creatures = g_map.getSpectatorsInRange(pos, false, 3, 3) -- 6x6 area + else + creatures = specs + end + local highestPriority = 0 + local dangerLevel = 0 + local targets = 0 + local highestPriorityParams = nil + for i, creature in ipairs(creatures) do + local hppc = creature:getHealthPercent() + if hppc and hppc > 0 then + local path = findPath(player:getPosition(), creature:getPosition(), 7, {ignoreLastCreature=true, ignoreNonPathable=true, ignoreCost=true, ignoreCreatures=true}) + if creature:isMonster() and (oldTibia or creature:getType() < 3) 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 + end + + -- reset walking + TargetBot.walkTo(nil) + + -- looting + local looting = TargetBot.Looting.process(targets, dangerLevel) + local lootingStatus = TargetBot.Looting.getStatus() + looterStatus = TargetBot.Looting.getStatus() + dangerValue = dangerLevel + + 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) + elseif cavebotAllowance > now then + TargetBot.setStatus("Luring using CaveBot") + else + TargetBot.setStatus("Attacking") + if not lureEnabled then + TargetBot.setStatus("Attacking (luring off)") + end + 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 + lureEnabled = true +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.isCaveBotActionAllowed = function() + return cavebotAllowance > now +end + +TargetBot.setStatus = function(text) + return ui.status.right:setText(text) +end + +TargetBot.getStatus = function() + return ui.status.right:getText() +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.getCurrentProfile = function() + return storage._configs.targetbot_configs.selected +end + +local botConfigName = modules.game_bot.contentsPanel.config:getCurrentOption().text +TargetBot.setCurrentProfile = function(name) + if not g_resources.fileExists("/bot/"..botConfigName.."/targetbot_configs/"..name..".json") then + return warn("there is no targetbot profile with that name!") + end + TargetBot.setOff() + storage._configs.targetbot_configs.selected = name + TargetBot.setOn() +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 + +TargetBot.allowCaveBot = function(time) + cavebotAllowance = now + time +end + +TargetBot.disableLuring = function() + lureEnabled = false +end + +TargetBot.enableLuring = function() + lureEnabled = true +end + +TargetBot.Danger = function() + return dangerValue +end + +TargetBot.lootStatus = function() + return looterStatus +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 lastItemUse = 0 +local lastRuneAttack = 0 + +TargetBot.useItem = function(item, subType, target, delay) + if not delay then delay = 200 end + if lastItemUse + delay < now then + local thing = g_things.getThingType(item) + if not thing or not thing:isFluidContainer() then + subType = g_game.getClientVersion() >= 860 and 0 or 1 + end + if g_game.getClientVersion() < 780 then + local tmpItem = g_game.findPlayerItem(item, subType) + if not tmpItem then return end + g_game.useWith(tmpItem, target, subType) -- using item from bp + else + g_game.useInventoryItemWith(item, target, subType) -- hotkey + end + lastItemUse = now + end +end + +TargetBot.useAttackItem = function(item, subType, target, delay) + if not delay then delay = 2000 end + if lastRuneAttack + delay < now then + local thing = g_things.getThingType(item) + if not thing or not thing:isFluidContainer() then + subType = g_game.getClientVersion() >= 860 and 0 or 1 + end + if g_game.getClientVersion() < 780 then + local tmpItem = g_game.findPlayerItem(item, subType) + if not tmpItem then return end + g_game.useWith(tmpItem, target, subType) -- using item from bp + else + g_game.useInventoryItemWith(item, target, subType) -- hotkey + end + lastRuneAttack = now + end +end + +TargetBot.canLure = function() + return lureEnabled +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/target.otui b/modules/game_bot/default_configs/vBot_4.8/targetbot/target.otui new file mode 100644 index 0000000000..6e0e4eafa5 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/target.otui @@ -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 diff --git a/modules/game_bot/default_configs/vBot_4.8/targetbot/walking.lua b/modules/game_bot/default_configs/vBot_4.8/targetbot/walking.lua new file mode 100644 index 0000000000..b256d6acf4 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/targetbot/walking.lua @@ -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 diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.lua new file mode 100644 index 0000000000..2fe7ee12f0 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.lua @@ -0,0 +1,1254 @@ +setDefaultTab('main') +-- locales +local panelName = "AttackBot" +local currentSettings +local showSettings = false +local showItem = false +local category = 1 +local patternCategory = 1 +local pattern = 1 +local mainWindow + +-- label library + +local categories = { + "Targeted Spell (exori hur, exori flam, etc)", + "Area Rune (avalanche, great fireball, etc)", + "Targeted Rune (sudden death, icycle, etc)", + "Empowerment (utito tempo, etc)", + "Absolute Spell (exori, hells core, etc)", +} + +local patterns = { + -- targeted spells + { + "1 Sqm Range (exori ico)", + "2 Sqm Range", + "3 Sqm Range (strike spells)", + "4 Sqm Range (exori san)", + "5 Sqm Range (exori hur)", + "6 Sqm Range", + "7 Sqm Range (exori con)", + "8 Sqm Range", + "9 Sqm Range", + "10 Sqm Range" + }, + -- area runes + { + "Cross (explosion)", + "Bomb (fire bomb)", + "Ball (gfb, avalanche)" + }, + -- empowerment/targeted rune + { + "1 Sqm Range", + "2 Sqm Range", + "3 Sqm Range", + "4 Sqm Range", + "5 Sqm Range", + "6 Sqm Range", + "7 Sqm Range", + "8 Sqm Range", + "9 Sqm Range", + "10 Sqm Range", + }, + -- absolute + { + "Adjacent (exori, exori gran)", + "3x3 Wave (vis hur, tera hur)", + "Small Area (mas san, exori mas)", + "Medium Area (mas flam, mas frigo)", + "Large Area (mas vis, mas tera)", + "Short Beam (vis lux)", + "Large Beam (gran vis lux)", + "Sweep (exori min)", -- 8 + "Small Wave (gran frigo hur)", + "Big Wave (flam hur, frigo hur)", + "Huge Wave (gran flam hur)", + } +} + + -- spellPatterns[category][pattern][1 - normal, 2 - safe] +local spellPatterns = { + {}, -- blank, wont be used + -- Area Runes, + { + { -- cross + [[ + 010 + 111 + 010 + ]], + -- cross SAFE + [[ + 01110 + 01110 + 11111 + 11111 + 11111 + 01110 + 01110 + ]] + }, + { -- bomb + [[ + 111 + 111 + 111 + ]], + -- bomb SAFE + [[ + 11111 + 11111 + 11111 + 11111 + 11111 + ]] + }, + { -- ball + [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 + ]], + -- ball SAFE + [[ + 000111000 + 001111100 + 011111110 + 111111111 + 111111111 + 111111111 + 011111110 + 001111100 + 000111000 + ]] + }, + }, + {}, -- blank, wont be used + -- Absolute + { + {-- adjacent + [[ + 111 + 111 + 111 + ]], + -- adjacent SAFE + [[ + 11111 + 11111 + 11111 + 11111 + 11111 + ]] + }, + { -- 3x3 Wave + [[ + 0000NNN0000 + 0000NNN0000 + 0000NNN0000 + 00000N00000 + WWW00N00EEE + WWWWW0EEEEE + WWW00S00EEE + 00000S00000 + 0000SSS0000 + 0000SSS0000 + 0000SSS0000 + ]], + -- 3x3 Wave SAFE + [[ + 0000NNNNN0000 + 0000NNNNN0000 + 0000NNNNN0000 + 0000NNNNN0000 + WWWW0NNN0EEEE + WWWWWNNNEEEEE + WWWWWW0EEEEEE + WWWWWSSSEEEEE + WWWW0SSS0EEEE + 0000SSSSS0000 + 0000SSSSS0000 + 0000SSSSS0000 + 0000SSSSS0000 + ]] + }, + { -- small area + [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 + ]], + -- small area SAFE + [[ + 000111000 + 001111100 + 011111110 + 111111111 + 111111111 + 111111111 + 011111110 + 001111100 + 000111000 + ]] + }, + { -- medium area + [[ + 00000100000 + 00011111000 + 00111111100 + 01111111110 + 01111111110 + 11111111111 + 01111111110 + 01111111110 + 00111111100 + 00001110000 + 00000100000 + ]], + -- medium area SAFE + [[ + 0000011100000 + 0000111110000 + 0001111111000 + 0011111111100 + 0111111111110 + 0111111111110 + 1111111111111 + 0111111111110 + 0111111111110 + 0011111111100 + 0001111111000 + 0000111110000 + 0000011100000 + ]] + }, + { -- large area + [[ + 0000001000000 + 0000011100000 + 0000111110000 + 0001111111000 + 0011111111100 + 0111111111110 + 1111111111111 + 0111111111110 + 0011111111100 + 0001111111000 + 0000111110000 + 0000011100000 + 0000001000000 + ]], + -- large area SAFE + [[ + 000000010000000 + 000000111000000 + 000001111100000 + 000011111110000 + 000111111111000 + 001111111111100 + 011111111111110 + 111111111111111 + 011111111111110 + 001111111111100 + 000111111111000 + 000011111110000 + 000001111100000 + 000000111000000 + 000000010000000 + ]] + }, + { -- short beam + [[ + 00000N00000 + 00000N00000 + 00000N00000 + 00000N00000 + 00000N00000 + WWWWW0EEEEE + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 + ]], + -- short beam SAFE + [[ + 00000NNN00000 + 00000NNN00000 + 00000NNN00000 + 00000NNN00000 + 00000NNN00000 + WWWWWNNNEEEEE + WWWWWW0EEEEEE + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + 00000SSS00000 + ]] + }, + { -- large beam + [[ + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + WWWWWWW0EEEEEEE + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + ]], + -- large beam SAFE + [[ + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + WWWWWWWNNNEEEEEEE + WWWWWWWW0EEEEEEEE + WWWWWWWSSSEEEEEEE + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + ]], + }, + {}, -- sweep, wont be used + { -- small wave + [[ + 00NNN00 + 00NNN00 + WW0N0EE + WWW0EEE + WW0S0EE + 00SSS00 + 00SSS00 + ]], + -- small wave SAFE + [[ + 00NNNNN00 + 00NNNNN00 + WWNNNNNEE + WWWWNEEEE + WWWW0EEEE + WWWWSEEEE + WWSSSSSEE + 00SSSSS00 + 00SSSSS00 + ]] + }, + { -- large wave + [[ + 000NNNNN000 + 000NNNNN000 + 0000NNN0000 + WW00NNN00EE + WWWW0N0EEEE + WWWWW0EEEEE + WWWW0S0EEEE + WW00SSS00EE + 0000SSS0000 + 000SSSSS000 + 000SSSSS000 + ]], + [[ + 000NNNNNNN000 + 000NNNNNNN000 + 000NNNNNNN000 + WWWWNNNNNEEEE + WWWWNNNNNEEEE + WWWWWNNNEEEEE + WWWWWW0EEEEEE + WWWWWSSSEEEEE + WWWWSSSSSEEEE + WWWWSSSSSEEEE + 000SSSSSSS000 + 000SSSSSSS000 + 000SSSSSSS000 + ]] + }, + { -- huge wave + [[ + 0000NNNNN0000 + 0000NNNNN0000 + 00000NNN00000 + 00000NNN00000 + WW0000N0000EE + WWWW00N00EEEE + WWWWWW0EEEEEE + WWWW00S00EEEE + WW0000S0000EE + 00000SSS00000 + 00000SSS00000 + 0000SSSSS0000 + 0000SSSSS0000 + ]], + [[ + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + 0000000NNN0000000 + WWWWWWWNNNEEEEEEE + WWWWWWWW0EEEEEEEE + WWWWWWWSSSEEEEEEE + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + 0000000SSS0000000 + ]] + } + } +} + +-- direction patterns +local ek = (voc() == 1 or voc() == 11) and true + +local posN = ek and [[ + 111 + 000 + 000 +]] or [[ + 00011111000 + 00011111000 + 00011111000 + 00011111000 + 00000100000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 +]] + +local posE = ek and [[ + 001 + 001 + 001 +]] or [[ + 00000000000 + 00000000000 + 00000000000 + 00000001111 + 00000001111 + 00000011111 + 00000001111 + 00000001111 + 00000000000 + 00000000000 + 00000000000 +]] +local posS = ek and [[ + 000 + 000 + 111 +]] or [[ + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000000000 + 00000100000 + 00011111000 + 00011111000 + 00011111000 + 00011111000 +]] +local posW = ek and [[ + 100 + 100 + 100 +]] or [[ + 00000000000 + 00000000000 + 00000000000 + 11110000000 + 11110000000 + 11111000000 + 11110000000 + 11110000000 + 00000000000 + 00000000000 + 00000000000 +]] + +-- AttackBotConfig +-- create blank profiles +if not AttackBotConfig[panelName] or not AttackBotConfig[panelName][1] or #AttackBotConfig[panelName] ~= 5 then + AttackBotConfig[panelName] = { + [1] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #1", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [2] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #2", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [3] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #3", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [4] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #4", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + [5] = { + enabled = false, + attackTable = {}, + ignoreMana = true, + Kills = false, + Rotate = false, + name = "Profile #5", + Cooldown = true, + Visible = true, + pvpMode = false, + KillsAmount = 1, + PvpSafe = true, + BlackListSafe = false, + AntiRsRange = 5 + }, + } +end + +if not AttackBotConfig.currentBotProfile or AttackBotConfig.currentBotProfile == 0 or AttackBotConfig.currentBotProfile > 5 then + AttackBotConfig.currentBotProfile = 1 +end + +-- create panel UI +ui = UI.createWidget("AttackBotBotPanel") + +-- finding correct table, manual unfortunately +local setActiveProfile = function() + local n = AttackBotConfig.currentBotProfile + currentSettings = AttackBotConfig[panelName][n] +end +setActiveProfile() + +if not currentSettings.AntiRsRange then + currentSettings.AntiRsRange = 5 +end + +local setProfileName = function() + ui.name:setText(currentSettings.name) +end + +-- small UI elements +ui.title.onClick = function(widget) + currentSettings.enabled = not currentSettings.enabled + widget:setOn(currentSettings.enabled) + vBotConfigSave("atk") +end + +ui.settings.onClick = function(widget) + mainWindow:show() + mainWindow:raise() + mainWindow:focus() +end + + mainWindow = UI.createWindow("AttackBotWindow") + mainWindow:hide() + + local panel = mainWindow.mainPanel + local settingsUI = mainWindow.settingsPanel + + mainWindow.onVisibilityChange = function(widget, visible) + if not visible then + currentSettings.attackTable = {} + for i, child in ipairs(panel.entryList:getChildren()) do + table.insert(currentSettings.attackTable, child.params) + end + vBotConfigSave("atk") + end + end + + -- main panel + + -- functions + function toggleSettings() + panel:setVisible(not showSettings) + mainWindow.shooterLabel:setVisible(not showSettings) + settingsUI:setVisible(showSettings) + mainWindow.settingsLabel:setVisible(showSettings) + mainWindow.settings:setText(showSettings and "Back" or "Settings") + end + toggleSettings() + + mainWindow.settings.onClick = function() + showSettings = not showSettings + toggleSettings() + end + + function toggleItem() + panel.monsters:setWidth(showItem and 405 or 341) + panel.itemId:setVisible(showItem) + panel.spellName:setVisible(not showItem) + end + toggleItem() + + function setCategoryText() + panel.category.description:setText(categories[category]) + end + setCategoryText() + + function setPatternText() + panel.range.description:setText(patterns[patternCategory][pattern]) + end + setPatternText() + + -- in/de/crementation buttons + panel.previousCategory.onClick = function() + if category == 1 then + category = #categories + else + category = category - 1 + end + + showItem = (category == 2 or category == 3) and true or false + patternCategory = category == 4 and 3 or category == 5 and 4 or category + pattern = 1 + toggleItem() + setPatternText() + setCategoryText() + end + panel.nextCategory.onClick = function() + if category == #categories then + category = 1 + else + category = category + 1 + end + + showItem = (category == 2 or category == 3) and true or false + patternCategory = category == 4 and 3 or category == 5 and 4 or category + pattern = 1 + toggleItem() + setPatternText() + setCategoryText() + end + panel.previousSource.onClick = function() + warn("[AttackBot] TODO, reserved for future use.") + end + panel.nextSource.onClick = function() + warn("[AttackBot] TODO, reserved for future use.") + end + panel.previousRange.onClick = function() + local t = patterns[patternCategory] + if pattern == 1 then + pattern = #t + else + pattern = pattern - 1 + end + setPatternText() + end + panel.nextRange.onClick = function() + local t = patterns[patternCategory] + if pattern == #t then + pattern = 1 + else + pattern = pattern + 1 + end + setPatternText() + end + -- eo in/de/crementation + + ------- [[core table function]] ------- + function setupWidget(widget) + local params = widget.params + + widget:setText(params.description) + if params.itemId > 0 then + widget.spell:setVisible(false) + widget.id:setVisible(true) + widget.id:setItemId(params.itemId) + end + widget:setTooltip(params.tooltip) + widget.remove.onClick = function() + panel.up:setEnabled(false) + panel.down:setEnabled(false) + widget:destroy() + end + widget.enabled:setChecked(params.enabled) + widget.enabled.onClick = function() + params.enabled = not params.enabled + widget.enabled:setChecked(params.enabled) + end + -- will serve as edit + widget.onDoubleClick = function(widget) + panel.manaPercent:setValue(params.mana) + panel.creatures:setValue(params.count) + panel.minHp:setValue(params.minHp) + panel.maxHp:setValue(params.maxHp) + panel.cooldown:setValue(params.cooldown) + showItem = params.itemId > 100 and true or false + panel.itemId:setItemId(params.itemId) + panel.spellName:setText(params.spell or "") + panel.orMore:setChecked(params.orMore) + toggleItem() + category = params.category + patternCategory = params.patternCategory + pattern = params.pattern + setPatternText() + setCategoryText() + widget:destroy() + end + widget.onClick = function(widget) + if #panel.entryList:getChildren() == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(false) + elseif panel.entryList:getChildIndex(widget) == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(true) + elseif panel.entryList:getChildIndex(widget) == panel.entryList:getChildCount() then + panel.up:setEnabled(true) + panel.down:setEnabled(false) + else + panel.up:setEnabled(true) + panel.down:setEnabled(true) + end + end + end + + + -- refreshing values + function refreshAttacks() + if not currentSettings.attackTable then return end + + panel.entryList:destroyChildren() + for i, entry in pairs(currentSettings.attackTable) do + local label = UI.createWidget("AttackEntry", panel.entryList) + label.params = entry + setupWidget(label) + end + end + refreshAttacks() + panel.up:setEnabled(false) + panel.down:setEnabled(false) + + -- adding values + panel.addEntry.onClick = function(wdiget) + -- first variables + local creatures = panel.monsters:getText():lower() + local monsters = (creatures:len() == 0 or creatures == "*" or creatures == "monster names") and true or string.split(creatures, ",") + local mana = panel.manaPercent:getValue() + local count = panel.creatures:getValue() + local minHp = panel.minHp:getValue() + local maxHp = panel.maxHp:getValue() + local cooldown = panel.cooldown:getValue() + local itemId = panel.itemId:getItemId() + local spell = panel.spellName:getText() + local tooltip = monsters ~= true and creatures + local orMore = panel.orMore:isChecked() + + -- validation + if showItem and itemId < 100 then + return warn("[AttackBot]: please fill item ID!") + elseif not showItem and (spell:lower() == "spell name" or spell:len() == 0) then + return warn("[AttackBot]: please fill spell name!") + end + + local regex = patternCategory ~= 1 and [[^[^\(]+]] or [[^[^R]+]] + local type = regexMatch(patterns[patternCategory][pattern], regex)[1][1]:trim() + regex = [[^[^ ]+]] + local categoryName = regexMatch(categories[category], regex)[1][1]:trim():lower() + local specificMonsters = monsters == true and "Any Creatures" or "Creatures" + local attackType = showItem and "rune "..itemId or spell + + local countDescription = orMore and count.."+" or count + + local params = { + creatures = creatures, + monsters = monsters, + mana = mana, + count = count, + minHp = minHp, + maxHp = maxHp, + cooldown = cooldown, + itemId = itemId, + spell = spell, + enabled = true, + category = category, + patternCategory = patternCategory, + pattern = pattern, + tooltip = tooltip, + orMore = orMore, + description = '['..type..'] '..countDescription.. ' '..specificMonsters..': '..attackType..', '..categoryName..' ('..minHp..'%-'..maxHp..'%)' + } + + local label = UI.createWidget("AttackEntry", panel.entryList) + label.params = params + setupWidget(label) + resetFields() + end + + -- moving values + -- up + panel.up.onClick = function(widget) + local focused = panel.entryList:getFocusedChild() + local n = panel.entryList:getChildIndex(focused) + + if n-1 == 1 then + widget:setEnabled(false) + end + panel.down:setEnabled(true) + panel.entryList:moveChildToIndex(focused, n-1) + panel.entryList:ensureChildVisible(focused) + end + -- down + panel.down.onClick = function(widget) + local focused = panel.entryList:getFocusedChild() + local n = panel.entryList:getChildIndex(focused) + + if n + 1 == panel.entryList:getChildCount() then + widget:setEnabled(false) + end + panel.up:setEnabled(true) + panel.entryList:moveChildToIndex(focused, n+1) + panel.entryList:ensureChildVisible(focused) + end + + -- [[settings panel]] -- + settingsUI.profileName.onTextChange = function(widget, text) + currentSettings.name = text + setProfileName() + end + settingsUI.IgnoreMana.onClick = function(widget) + currentSettings.ignoreMana = not currentSettings.ignoreMana + settingsUI.IgnoreMana:setChecked(currentSettings.ignoreMana) + end + settingsUI.Rotate.onClick = function(widget) + currentSettings.Rotate = not currentSettings.Rotate + settingsUI.Rotate:setChecked(currentSettings.Rotate) + end + settingsUI.Kills.onClick = function(widget) + currentSettings.Kills = not currentSettings.Kills + settingsUI.Kills:setChecked(currentSettings.Kills) + end + settingsUI.Cooldown.onClick = function(widget) + currentSettings.Cooldown = not currentSettings.Cooldown + settingsUI.Cooldown:setChecked(currentSettings.Cooldown) + end + settingsUI.Visible.onClick = function(widget) + currentSettings.Visible = not currentSettings.Visible + settingsUI.Visible:setChecked(currentSettings.Visible) + end + settingsUI.PvpMode.onClick = function(widget) + currentSettings.pvpMode = not currentSettings.pvpMode + settingsUI.PvpMode:setChecked(currentSettings.pvpMode) + end + settingsUI.PvpSafe.onClick = function(widget) + currentSettings.PvpSafe = not currentSettings.PvpSafe + settingsUI.PvpSafe:setChecked(currentSettings.PvpSafe) + end + settingsUI.Training.onClick = function(widget) + currentSettings.Training = not currentSettings.Training + settingsUI.Training:setChecked(currentSettings.Training) + end + settingsUI.BlackListSafe.onClick = function(widget) + currentSettings.BlackListSafe = not currentSettings.BlackListSafe + settingsUI.BlackListSafe:setChecked(currentSettings.BlackListSafe) + end + settingsUI.KillsAmount.onValueChange = function(widget, value) + currentSettings.KillsAmount = value + end + settingsUI.AntiRsRange.onValueChange = function(widget, value) + currentSettings.AntiRsRange = value + end + + + -- window elements + mainWindow.closeButton.onClick = function() + showSettings = false + toggleSettings() + resetFields() + mainWindow:hide() + end + + -- core functions + function resetFields() + showItem = false + toggleItem() + pattern = 1 + patternCategory = 1 + category = 1 + setPatternText() + setCategoryText() + panel.manaPercent:setText(1) + panel.creatures:setText(1) + panel.minHp:setValue(0) + panel.maxHp:setValue(100) + panel.cooldown:setText(1) + panel.monsters:setText("monster names") + panel.itemId:setItemId(0) + panel.spellName:setText("spell name") + panel.orMore:setChecked(false) + end + resetFields() + + function loadSettings() + -- BOT panel + ui.title:setOn(currentSettings.enabled) + setProfileName() + -- main panel + refreshAttacks() + -- settings + settingsUI.profileName:setText(currentSettings.name) + settingsUI.Visible:setChecked(currentSettings.Visible) + settingsUI.Cooldown:setChecked(currentSettings.Cooldown) + settingsUI.PvpMode:setChecked(currentSettings.pvpMode) + settingsUI.PvpSafe:setChecked(currentSettings.PvpSafe) + settingsUI.BlackListSafe:setChecked(currentSettings.BlackListSafe) + settingsUI.AntiRsRange:setValue(currentSettings.AntiRsRange) + settingsUI.IgnoreMana:setChecked(currentSettings.ignoreMana) + settingsUI.Rotate:setChecked(currentSettings.Rotate) + settingsUI.Kills:setChecked(currentSettings.Kills) + settingsUI.KillsAmount:setValue(currentSettings.KillsAmount) + settingsUI.Training:setChecked(currentSettings.Training) + end + loadSettings() + + local activeProfileColor = function() + for i=1,5 do + if i == AttackBotConfig.currentBotProfile then + ui[i]:setColor("green") + else + ui[i]:setColor("white") + end + end + end + activeProfileColor() + + local profileChange = function() + setActiveProfile() + activeProfileColor() + loadSettings() + resetFields() + vBotConfigSave("atk") + end + + for i=1,5 do + local button = ui[i] + button.onClick = function() + AttackBotConfig.currentBotProfile = i + profileChange() + end + end + + -- public functions + AttackBot = {} -- global table + + AttackBot.isOn = function() + return currentSettings.enabled + end + + AttackBot.isOff = function() + return not currentSettings.enabled + end + + AttackBot.setOff = function() + currentSettings.enabled = false + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + AttackBot.setOn = function() + currentSettings.enabled = true + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + AttackBot.getActiveProfile = function() + return AttackBotConfig.currentBotProfile -- returns number 1-5 + end + + AttackBot.setActiveProfile = function(n) + if not n or not tonumber(n) or n < 1 or n > 5 then + return error("[AttackBot] wrong profile parameter! should be 1 to 5 is " .. n) + else + AttackBotConfig.currentBotProfile = n + profileChange() + end + end + + AttackBot.show = function() + mainWindow:show() + mainWindow:raise() + mainWindow:focus() + end + + +-- otui covered, now support functions +function getPattern(category, pattern, safe) + safe = safe and 2 or 1 + + return spellPatterns[category][pattern][safe] +end + + +function getMonstersInArea(category, posOrCreature, pattern, minHp, maxHp, safePattern, monsterNamesTable) + -- monsterNamesTable can be nil + local monsters = 0 + local t = {} + if monsterNamesTable == true or not monsterNamesTable then + t = {} + else + t = monsterNamesTable + end + + if safePattern then + for i, spec in pairs(getSpectators(posOrCreature, safePattern)) do + if spec ~= player and (spec:isPlayer() and not spec:isPartyMember()) then + return 0 + end + end + end + + if category == 1 or category == 3 or category == 4 then + if category == 1 or category == 3 then + local name = getTarget() and getTarget():getName() + if #t ~= 0 and not table.find(t, name, true) then + return 0 + end + end + for i, spec in pairs(getSpectators()) do + local specHp = spec:getHealthPercent() + local name = spec:getName():lower() + monsters = spec:isMonster() and specHp >= minHp and specHp <= maxHp and (#t == 0 or table.find(t, name, true)) and + (g_game.getClientVersion() < 960 or spec:getType() < 3) and monsters + 1 or monsters + end + return monsters + end + + for i, spec in pairs(getSpectators(posOrCreature, pattern)) do + if spec ~= player then + local specHp = spec:getHealthPercent() + local name = spec:getName():lower() + monsters = spec:isMonster() and specHp >= minHp and specHp <= maxHp and (#t == 0 or table.find(t, name)) and + (g_game.getClientVersion() < 960 or spec:getType() < 3) and monsters + 1 or monsters + end + end + + return monsters +end + +-- for area runes only +-- should return valid targets number (int) and position +function getBestTileByPattern(pattern, minHp, maxHp, safePattern, monsterNamesTable) + local tiles = g_map.getTiles(posz()) + local targetTile = {amount=0,pos=false} + + for i, tile in pairs(tiles) do + local tPos = tile:getPosition() + local distance = distanceFromPlayer(tPos) + if tile:canShoot() and tile:isWalkable() and distance < 4 then + local amount = getMonstersInArea(2, tPos, pattern, minHp, maxHp, safePattern, monsterNamesTable) + if amount > targetTile.amount then + targetTile = {amount=amount,pos=tPos} + end + end + end + + return targetTile.amount > 0 and targetTile or false +end + +function executeAttackBotAction(categoryOrPos, idOrFormula, cooldown) + cooldown = cooldown or 0 + if categoryOrPos == 4 or categoryOrPos == 5 or categoryOrPos == 1 then + cast(idOrFormula, cooldown) + elseif categoryOrPos == 3 then + useWith(idOrFormula, target()) + end +end + +-- support function covered, now the main loop +macro(100, function() + if not currentSettings.enabled then return end + if #currentSettings.attackTable == 0 or isInPz() or not target() or modules.game_cooldown.isGroupCooldownIconActive(1) then return end + + if currentSettings.Training and target() and target():getName():lower():find("training") then return end + + if g_game.getClientVersion() < 960 or not currentSettings.Cooldown then + delay(400) + end + + local monstersN = 0 + local monstersE = 0 + local monstersS = 0 + local monstersW = 0 + monstersN = getCreaturesInArea(pos(), posN, 2) + monstersE = getCreaturesInArea(pos(), posE, 2) + monstersS = getCreaturesInArea(pos(), posS, 2) + monstersW = getCreaturesInArea(pos(), posW, 2) + local posTable = {monstersE, monstersN, monstersS, monstersW} + local bestSide = 0 + local bestDir + -- pulling out the biggest number + for i, v in pairs(posTable) do + if v > bestSide then + bestSide = v + end + end + -- associate biggest number with turn direction + if monstersN == bestSide then bestDir = 0 + elseif monstersE == bestSide then bestDir = 1 + elseif monstersS == bestSide then bestDir = 2 + elseif monstersW == bestSide then bestDir = 3 + end + + if currentSettings.Rotate then + if player:getDirection() ~= bestDir and bestSide > 0 then + turn(bestDir) + return + end + end + + -- support functions done, main spells now + --[[ + entry = { + creatures = creatures, + monsters = monsters, (formatted creatures) + mana = mana, + count = count, + minHp = minHp, + maxHp = maxHp, + cooldown = cooldown, + itemId = itemId, + spell = spell, + enabled = true, + category = category, + patternCategory = patternCategory, + pattern = pattern, + tooltip = tooltip, + description = '['..type..'] '..count.. 'x '..specificMonsters..': '..attackType..', '..categoryName..' ('..minHp..'%-'..maxHp..'%)' + } + ]] + + for i, child in ipairs(panel.entryList:getChildren()) do + local entry = child.params + local attackData = entry.itemId > 100 and entry.itemId or entry.spell + if entry.enabled and manapercent() >= entry.mana then + if (type(attackData) == "string" and canCast(entry.spell, not currentSettings.ignoreMana, not currentSettings.Cooldown)) or (entry.itemId > 100 and (not currentSettings.Visible or findItem(entry.itemId))) then + -- first PVP scenario + if currentSettings.pvpMode and target():getHealthPercent() >= entry.minHp and target():getHealthPercent() <= entry.maxHp and target():canShoot() then + if entry.category == 2 then + return warn("[AttackBot] Area Runes cannot be used in PVP situation!") + else + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + end + -- empowerment + if entry.category == 4 and not isBuffed() then + local monsterAmount = getMonstersInArea(entry.category, nil, nil, entry.minHp, entry.maxHp, false, entry.monsters) + if (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count) and distanceFromPlayer(target():getPosition()) <= entry.pattern then + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + -- + elseif entry.category == 1 or entry.category == 3 then + local monsterAmount = getMonstersInArea(entry.category, nil, nil, entry.minHp, entry.maxHp, false, entry.monsters) + if (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count) and distanceFromPlayer(target():getPosition()) <= entry.pattern then + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + elseif entry.category == 5 then + local pCat = entry.patternCategory + local pattern = entry.pattern + local anchorParam = (pattern == 2 or pattern == 6 or pattern == 7 or pattern > 9) and player or pos() + local safe = currentSettings.PvpSafe and spellPatterns[pCat][entry.pattern][2] or false + local monsterAmount = pCat ~= 8 and getMonstersInArea(entry.category, anchorParam, spellPatterns[pCat][entry.pattern][1], entry.minHp, entry.maxHp, safe, entry.monsters) + if (pattern ~= 8 and (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count)) or (pattern == 8 and bestSide >= entry.count and (not currentSettings.PvpSafe or getPlayers(2) == 0)) then + if (not currentSettings.BlackListSafe or not isBlackListedPlayerInRange(currentSettings.AntiRsRange)) and (not currentSettings.Kills or killsToRs() > currentSettings.KillsAmount) then + return executeAttackBotAction(entry.category, attackData, entry.cooldown) + end + end + elseif entry.category == 2 then + local pCat = entry.patternCategory + local safe = currentSettings.PvpSafe and spellPatterns[pCat][entry.pattern][2] or false + local data = getBestTileByPattern(spellPatterns[pCat][entry.pattern][1], entry.minHp, entry.maxHp, safe, entry.monsters) + local monsterAmount + local pos + if data then + monsterAmount = data.amount + pos = data.pos + end + if monsterAmount and (entry.orMore and monsterAmount >= entry.count or not entry.orMore and monsterAmount == entry.count) then + if (not currentSettings.BlackListSafe or not isBlackListedPlayerInRange(currentSettings.AntiRsRange)) and (not currentSettings.Kills or killsToRs() > currentSettings.KillsAmount) then + return useWith(attackData, g_map.getTile(pos):getTopUseThing()) + end + end + end + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.otui new file mode 100644 index 0000000000..f6329f474c --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/AttackBot.otui @@ -0,0 +1,624 @@ +AttackEntry < UIWidget + background-color: alpha + text-offset: 35 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + + UIItem + id: id + anchors.left: prev.right + anchors.verticalCenter: parent.verticalCenter + size: 16 16 + focusable: false + visible: false + + UIWidget + id: spell + anchors.left: enabled.right + anchors.verticalCenter: parent.verticalCenter + size: 12 12 + margin-left: 1 + image-source: /images/game/dangerous + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + width: 15 + height: 15 + +AttackBotBotPanel < Panel + height: 38 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('AttackBot') + + Button + id: settings + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + Button + id: 1 + anchors.top: prev.bottom + anchors.left: parent.left + text: 1 + margin-right: 2 + margin-top: 4 + size: 17 17 + + Button + id: 2 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 2 + margin-left: 4 + size: 17 17 + + Button + id: 3 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 3 + margin-left: 4 + size: 17 17 + + Button + id: 4 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 4 + margin-left: 4 + size: 17 17 + + Button + id: 5 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 5 + margin-left: 4 + size: 17 17 + + Label + id: name + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + text-align: center + margin-left: 4 + height: 17 + text: Profile #1 + background: #292A2A + +CategoryLabel < Panel + size: 315 15 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 1 + + Label + id: description + anchors.fill: parent + text-align: center + text: Area Rune (avalanche, great fireball, etc) + font: verdana-11px-rounded + background: #363636 + +SourceLabel < Panel + size: 105 15 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 1 + + Label + id: description + anchors.fill: parent + text-align: center + text: Monster Name + font: verdana-11px-rounded + background: #363636 + +RangeLabel < Panel + size: 323 15 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 1 + + Label + id: description + anchors.fill: parent + text-align: center + text: 5 Sqm + font: verdana-11px-rounded + background: #363636 + +PreButton < PreviousButton + background: #363636 + height: 15 + +NexButton < NextButton + background: #363636 + height: 15 + +AttackBotPanel < Panel + size: 500 200 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 5 + + TextList + id: entryList + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 3 + size: 430 100 + vertical-scrollbar: entryListScrollBar + + VerticalScrollBar + id: entryListScrollBar + anchors.top: entryList.top + anchors.bottom: entryList.bottom + anchors.right: entryList.right + step: 14 + pixels-scroll: true + + PreButton + id: previousCategory + anchors.left: entryList.left + anchors.top: entryList.bottom + margin-top: 8 + + NexButton + id: nextCategory + anchors.left: category.right + anchors.top: entryList.bottom + margin-top: 8 + margin-left: 2 + + CategoryLabel + id: category + anchors.top: entryList.bottom + anchors.left: previousCategory.right + anchors.verticalCenter: previousCategory.verticalCenter + margin-left: 3 + + PreButton + id: previousSource + anchors.left: entryList.left + anchors.top: category.bottom + margin-top: 8 + + NexButton + id: nextSource + anchors.left: source.right + anchors.top: category.bottom + margin-top: 8 + margin-left: 2 + + SourceLabel + id: source + anchors.top: category.bottom + anchors.left: previousSource.right + anchors.verticalCenter: previousSource.verticalCenter + margin-left: 3 + + PreButton + id: previousRange + anchors.left: nextSource.right + anchors.verticalCenter: nextSource.verticalCenter + margin-left: 8 + + NexButton + id: nextRange + anchors.left: range.right + anchors.verticalCenter: range.verticalCenter + margin-left: 2 + + RangeLabel + id: range + anchors.left: previousRange.right + anchors.verticalCenter: previousRange.verticalCenter + margin-left: 3 + + TextEdit + id: monsters + anchors.left: entryList.left + anchors.top: range.bottom + margin-top: 5 + size: 405 15 + text: monster names + font: cipsoftFont + background: #363636 + + Label + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 6 + margin-left: 3 + text-align: center + text: Mana%: + font: verdana-11px-rounded + + SpinBox + id: manaPercent + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 30 20 + minimum: 0 + maximum: 99 + step: 1 + editable: true + focusable: true + + Label + anchors.left: prev.right + margin-left: 7 + anchors.verticalCenter: prev.verticalCenter + text: Creatures: + font: verdana-11px-rounded + + SpinBox + id: creatures + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 30 20 + minimum: 1 + maximum: 99 + step: 1 + editable: true + focusable: true + + CheckBox + id: orMore + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + tooltip: or more creatures + + Label + anchors.left: prev.right + margin-left: 7 + anchors.verticalCenter: prev.verticalCenter + text: HP: + font: verdana-11px-rounded + + SpinBox + id: minHp + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 40 20 + minimum: 0 + maximum: 99 + value: 0 + editable: true + focusable: true + + Label + anchors.left: prev.right + margin-left: 4 + anchors.verticalCenter: prev.verticalCenter + text: - + font: verdana-11px-rounded + + SpinBox + id: maxHp + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 40 20 + minimum: 1 + maximum: 100 + value: 100 + editable: true + focusable: true + + Label + anchors.left: prev.right + margin-left: 7 + anchors.verticalCenter: prev.verticalCenter + text: CD: + font: verdana-11px-rounded + + SpinBox + id: cooldown + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 4 + size: 60 20 + minimum: 0 + maximum: 999999 + step: 100 + value: 0 + editable: true + focusable: true + + Button + id: up + anchors.right: parent.right + anchors.top: entryList.bottom + size: 60 17 + text: Move Up + text-align: center + font: cipsoftFont + margin-top: 7 + margin-right: 8 + + Button + id: down + anchors.right: prev.left + anchors.verticalCenter: prev.verticalCenter + size: 60 17 + margin-right: 5 + text: Move Down + text-align: center + font: cipsoftFont + + Button + id: addEntry + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 40 19 + text-align: center + text: New + font: cipsoftFont + + BotItem + id: itemId + anchors.right: addEntry.left + margin-right: 5 + anchors.bottom: parent.bottom + margin-bottom: 2 + tooltip: drag item here on press to open window + + TextEdit + id: spellName + anchors.top: monsters.top + anchors.left: monsters.right + anchors.right: parent.right + margin-left: 5 + height: 15 + text: spell name + background: #363636 + font: cipsoftFont + visible: false + +SettingsPanel < Panel + size: 500 200 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 10 + + VerticalSeparator + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: Visible.right + margin-left: 10 + margin-top: 5 + margin-bottom: 5 + + Label + anchors.top: parent.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 10 + text-align: center + font: verdana-11px-rounded + text: Profile: + + TextEdit + id: profileName + anchors.top: prev.bottom + margin-top: 3 + anchors.left: prev.left + anchors.right: prev.right + margin-left: 20 + margin-right: 20 + + Button + id: resetSettings + anchors.right: parent.right + anchors.bottom: parent.bottom + text-align: center + text: Reset Settings + + CheckBox + id: IgnoreMana + anchors.top: parent.top + anchors.left: parent.left + margin-top: 5 + width: 200 + text: Check RL Tibia conditions + + CheckBox + id: Kills + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 200 + height: 22 + text: Don't use area attacks if less than kills to red skull + text-wrap: true + text-align: left + + SpinBox + id: KillsAmount + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.left: prev.right + text-align: left + width: 30 + minimum: 1 + maximum: 10 + focusable: true + margin-left: 5 + + CheckBox + id: Rotate + anchors.top: Kills.bottom + anchors.left: Kills.left + margin-top: 8 + width: 220 + text: Turn to side with most monsters + + CheckBox + id: Cooldown + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 220 + text: Check spell cooldowns + + CheckBox + id: Visible + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: Items must be visible (recommended) + + CheckBox + id: PvpMode + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: PVP mode + + CheckBox + id: PvpSafe + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: PVP safe + + CheckBox + id: Training + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 245 + text: Stop when attacking trainers + + CheckBox + id: BlackListSafe + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 8 + width: 200 + height: 18 + text: Stop if Anti-RS player in range + + SpinBox + id: AntiRsRange + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.left: prev.right + text-align: center + width: 50 + minimum: 1 + maximum: 10 + focusable: true + margin-left: 5 + +AttackBotWindow < MainWindow + size: 535 300 + padding: 15 + text: AttackBot v2 + @onEscape: self:hide() + + Label + id: mainLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 10 + margin-left: 2 + !text: tr('More important methods come first (Example: Exori gran above Exori)') + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + SettingsPanel + id: settingsPanel + anchors.top: prev.bottom + margin-top: 10 + anchors.left: parent.left + margin-left: 2 + + Label + id: settingsLabel + anchors.verticalCenter: prev.top + anchors.left: prev.left + margin-left: 3 + text: Settings + color: #fe4400 + font: verdana-11px-rounded + + AttackBotPanel + id: mainPanel + anchors.top: mainLabel.bottom + margin-top: 10 + anchors.left: parent.left + margin-left: 2 + visible: false + + Label + id: shooterLabel + anchors.verticalCenter: prev.top + anchors.left: prev.left + margin-left: 3 + text: Spell Shooter + color: #fe4400 + font: verdana-11px-rounded + visible: false + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: closeButton.top + margin-bottom: 10 + + Button + id: closeButton + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + text: Close + font: cipsoftFont + + Button + id: settings + anchors.left: parent.left + anchors.verticalCenter: prev.verticalCenter + size: 50 21 + font: cipsoftFont + text: Settings \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.lua new file mode 100644 index 0000000000..e6feb433cc --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.lua @@ -0,0 +1,244 @@ +setDefaultTab("Main") +local regex = [["(.*?)"]] +local panelName = "BOTserver" +local ui = setupUI([[ +Panel + height: 18 + + Button + id: botServer + anchors.left: parent.left + anchors.right: parent.right + text-align: center + height: 18 + !text: tr('BotServer') +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = { + manaInfo = true, + mwallInfo = true, + vocation = true, + outfit = false, + broadcasts = true +} +end + +local config = storage[panelName] +config.mwalls = {} + +if not storage.BotServerChannel then + math.randomseed(os.time()) + storage.BotServerChannel = tostring(math.random(1000000000000,9999999999999)) +end + +local channel = tostring(storage.BotServerChannel) +if config.enabled then + BotServer.init(name(), channel) +end + +vBot.BotServerMembers = {} + +rootWidget = g_ui.getRootWidget() +if rootWidget then + botServerWindow = UI.createWindow('BotServerWindow') + botServerWindow:hide() + + + botServerWindow.enabled:setOn(config.enabled) + botServerWindow.enabled.onClick = function() + config.enabled = not config.enabled + botServerWindow.enabled:setOn(config.enabled) + if config.enabled then + channel = tostring(storage.BotServerChannel) + BotServer.init(name(), channel) + botServerWindow.Data.ServerStatus:setText("CONNECTING...") + ui.botServer:setColor('#FFF380') + botServerWindow.Data.ServerStatus:setColor('#FFF380') + else + if BotServer._websocket then + BotServer.terminate() + end + botServerWindow.Data.ServerStatus:setText("DISCONNECTED") + ui.botServer:setColor('#E3242B') + botServerWindow.Data.ServerStatus:setColor('#E3242B') + botServerWindow.Data.Participants:setText("-") + botServerWindow.Data.Members:setTooltip('') + ServerMembers = {} + serverCount = {} + end + initBotServerListenFunctions() + schedule(2000, updateStatusText) + end + + botServerWindow.Data.Channel:setText(storage.BotServerChannel) + botServerWindow.Data.Channel.onTextChange = function(widget, text) + storage.BotServerChannel = text + end + botServerWindow.Data.Random.onClick = function(widget) + storage.BotServerChannel = tostring(math.random(1000000000000,9999999999999)) + botServerWindow.Data.Channel:setText(storage.BotServerChannel) + end + botServerWindow.Features.Feature1:setOn(config.manaInfo) + botServerWindow.Features.Feature1.onClick = function(widget) + config.manaInfo = not config.manaInfo + widget:setOn(config.manaInfo) + end + botServerWindow.Features.Feature2:setOn(config.mwallInfo) + botServerWindow.Features.Feature2.onClick = function(widget) + config.mwallInfo = not config.mwallInfo + widget:setOn(config.mwallInfo) + end + botServerWindow.Features.Feature3:setOn(config.vocation) + botServerWindow.Features.Feature3.onClick = function(widget) + config.vocation = not config.vocation + if config.vocation then + BotServer.send("voc", player:getVocation()) + end + widget:setOn(config.vocation) + end + botServerWindow.Features.Feature4:setOn(config.outfit) + botServerWindow.Features.Feature4.onClick = function(widget) + config.outfit = not config.outfit + widget:setOn(config.outfit) + end + botServerWindow.Features.Feature5:setOn(config.broadcasts) + botServerWindow.Features.Feature5.onClick = function(widget) + config.broadcasts = not config.broadcasts + widget:setOn(config.broadcasts) + end + botServerWindow.Features.Broadcast.onClick = function(widget) + if BotServer._websocket then + BotServer.send("broadcast", botServerWindow.Features.broadcastText:getText()) + end + botServerWindow.Features.broadcastText:setText('') + end +end + +function initBotServerListenFunctions() + if not BotServer._websocket then return end + if not config.enabled then return end + + -- list + BotServer.listen("list", function(name, data) + serverCount = regexMatch(json.encode(data), regex) + ServerMembers = json.encode(data) + end) + + -- mwalls + BotServer.listen("mwall", function(name, message) + if config.mwallInfo then + if not config.mwalls[message["pos"]] or config.mwalls[message["pos"]] < now then + config.mwalls[message["pos"]] = now + message["duration"] - 150 -- 150 is latency correction + end + end + end) + + -- mana + BotServer.listen("mana", function(name, message) + if config.manaInfo then + local creature = getPlayerByName(name) + if creature then + creature:setManaPercent(message["mana"]) + end + end + end) + + -- vocation + BotServer.listen("voc", function(name, message) + if message == "yes" and config.vocation then + BotServer.send("voc", player:getVocation()) + else + vBot.BotServerMembers[name] = message + end + end) + + -- broadcast + BotServer.listen("broadcast", function(name, message) + if config.broadcasts then + broadcastMessage(name..": "..message) + end + end) +end +initBotServerListenFunctions() + +function updateStatusText() + if BotServer._websocket then + botServerWindow.Data.ServerStatus:setText("CONNECTED") + botServerWindow.Data.ServerStatus:setColor('#03AC13') + ui.botServer:setColor('#03AC13') + if serverCount then + botServerWindow.Data.Participants:setText(#serverCount) + if ServerMembers then + local text = "" + local regex = [["([a-z 'A-z-]*)"*]] + local re = regexMatch(ServerMembers, regex) + --re[name][2] + for i=1,#re do + if i == 1 then + text = re[i][2] + else + text = text .. "\n" .. re[i][2] + end + end + botServerWindow.Data.Members:setTooltip(text) + end + end + else + botServerWindow.Data.ServerStatus:setText("DISCONNECTED") + ui.botServer:setColor('#E3242B') + botServerWindow.Data.ServerStatus:setColor('#E3242B') + botServerWindow.Data.Participants:setText("-") + end +end + +macro(1000, function() + if BotServer._websocket then + BotServer.send("list") + end + updateStatusText() + delay(9000) +end) + +ui.botServer.onClick = function(widget) + botServerWindow:show() + botServerWindow:raise() + botServerWindow:focus() +end + +botServerWindow.closeButton.onClick = function(widget) + botServerWindow:hide() +end + + +onAddThing(function(tile, thing) + if config.mwallInfo and BotServer._websocket then + if thing:isItem() and thing:getId() == 2129 then + local pos = tile:getPosition().x .. "," .. tile:getPosition().y .. "," .. tile:getPosition().z + if not config.mwalls[pos] or config.mwalls[pos] < now then + config.mwalls[pos] = now + 20000 + BotServer.send("mwall", {pos=pos, duration=20000}) + end + end + end +end) + +-- mana +local lastMana = 0 +macro(500, function() + if config.manaInfo and BotServer._websocket then + if manapercent() ~= lastMana then + lastMana = manapercent() + BotServer.send("mana", {mana=lastMana}) + end + end +end) + +-- vocation +if config.vocation and BotServer._websocket then + BotServer.send("voc", player:getVocation()) + BotServer.send("voc", "yes") +end + +addSeparator() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.otui new file mode 100644 index 0000000000..5ca327e563 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/BotServer.otui @@ -0,0 +1,202 @@ +BotServerData < Panel + size: 340 70 + image-source: /images/ui/window + image-border: 6 + padding: 3 + + Label + id: label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-align: center + !text: tr("BotServer Data") + + Label + id: label + anchors.top: parent.top + anchors.left: parent.left + margin-top: 23 + text-align: center + text: Channel Name: + margin-left: 6 + + TextEdit + id: Channel + anchors.top: parent.top + anchors.left: prev.right + margin-top: 20 + width: 150 + margin-left: 5 + text-align: center + + Button + id: Random + anchors.left: prev.right + anchors.top: prev.top + anchors.right: parent.right + text-align: center + text: Randomize + margin-left: 6 + margin-right: 6 + + Label + id: label + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-left: 6 + margin-bottom: 4 + text-align: center + text: Status: + + Label + id: ServerStatus + anchors.left: prev.right + anchors.bottom: parent.bottom + margin-left: 10 + width: 150 + margin-bottom: 4 + text-align: left + + Label + id: Participants + anchors.right: parent.right + anchors.bottom: parent.bottom + width: 20 + margin-right: 8 + margin-bottom: 4 + text-align: center + + UIWidget + id: Members + anchors.right: Participants.left + anchors.bottom: parent.bottom + size: 80 21 + text-align: center + text: Members: + +FeaturePanel < Panel + size: 340 150 + image-source: /images/ui/panel_flat + image-border: 5 + padding: 3 + + Label + id: title + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + text-align: center + text: Features + + HorizontalSeparator + id: sep + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + + BotSwitch + id: Feature1 + anchors.top: prev.bottom + anchors.left: parent.left + margin-left: 3 + margin-top: 5 + text: Mana info + + BotSwitch + id: Feature2 + anchors.top: sep.bottom + anchors.left: prev.right + margin-top: 5 + margin-left: 5 + text: MWall info + + BotSwitch + id: Feature3 + anchors.top: sep.bottom + anchors.left: prev.right + margin-top: 5 + margin-left: 5 + text: Send Vocation + + BotSwitch + id: Feature4 + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 3 + margin-left: 3 + text: Outfit Vocation + + BotSwitch + id: Feature5 + anchors.bottom: prev.bottom + anchors.left: prev.right + margin-top: 3 + margin-left: 5 + text: Broadcasts + + + TextEdit + id: broadcastText + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-left: 3 + margin-bottom: 3 + margin-right: 80 + + Button + id: Broadcast + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-right: 3 + margin-left: 3 + height: 22 + text: Broadcast + +BotServerWindow < MainWindow + !text: tr('BotServer') + size: 370 310 + @onEscape: self:hide() + + BotServerData + id: Data + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + + FeaturePanel + id: Features + anchors.top: prev.bottom + anchors.horizontalCenter: parent.horizontalCenter + margin-top: 10 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 + + BotSwitch + id: enabled + anchors.verticalCenter: prev.verticalCenter + anchors.left: parent.left + margin-left: 5 + height: 21 + + $!on: + text: BotServer: OFF + + $on: + text: BotServer: ON \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.lua new file mode 100644 index 0000000000..d9846d0bce --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.lua @@ -0,0 +1,262 @@ +setDefaultTab("HP") +local panelName = "ConditionPanel" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Conditions') + + Button + id: conditionList + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + ]]) + ui:setId(panelName) + + if not HealBotConfig[panelName] then + HealBotConfig[panelName] = { + enabled = false, + curePosion = false, + poisonCost = 20, + cureCurse = false, + curseCost = 80, + cureBleed = false, + bleedCost = 45, + cureBurn = false, + burnCost = 30, + cureElectrify = false, + electrifyCost = 22, + cureParalyse = false, + paralyseCost = 40, + paralyseSpell = "utani hur", + holdHaste = false, + hasteCost = 40, + hasteSpell = "utani hur", + holdUtamo = false, + utamoCost = 40, + holdUtana = false, + utanaCost = 440, + holdUtura = false, + uturaType = "", + uturaCost = 100, + ignoreInPz = true, + stopHaste = false + } + end + + local config = HealBotConfig[panelName] + + ui.title:setOn(config.enabled) + ui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) + vBotConfigSave("heal") + end + + ui.conditionList.onClick = function(widget) + conditionsWindow:show() + conditionsWindow:raise() + conditionsWindow:focus() + end + + + + local rootWidget = g_ui.getRootWidget() + if rootWidget then + conditionsWindow = UI.createWindow('ConditionsWindow', rootWidget) + conditionsWindow:hide() + + + conditionsWindow.onVisibilityChange = function(widget, visible) + if not visible then + vBotConfigSave("heal") + end + end + + -- text edits + conditionsWindow.Cure.PoisonCost:setText(config.poisonCost) + conditionsWindow.Cure.PoisonCost.onTextChange = function(widget, text) + config.poisonCost = tonumber(text) + end + + conditionsWindow.Cure.CurseCost:setText(config.curseCost) + conditionsWindow.Cure.CurseCost.onTextChange = function(widget, text) + config.curseCost = tonumber(text) + end + + conditionsWindow.Cure.BleedCost:setText(config.bleedCost) + conditionsWindow.Cure.BleedCost.onTextChange = function(widget, text) + config.bleedCost = tonumber(text) + end + + conditionsWindow.Cure.BurnCost:setText(config.burnCost) + conditionsWindow.Cure.BurnCost.onTextChange = function(widget, text) + config.burnCost = tonumber(text) + end + + conditionsWindow.Cure.ElectrifyCost:setText(config.electrifyCost) + conditionsWindow.Cure.ElectrifyCost.onTextChange = function(widget, text) + config.electrifyCost = tonumber(text) + end + + conditionsWindow.Cure.ParalyseCost:setText(config.paralyseCost) + conditionsWindow.Cure.ParalyseCost.onTextChange = function(widget, text) + config.paralyseCost = tonumber(text) + end + + conditionsWindow.Cure.ParalyseSpell:setText(config.paralyseSpell) + conditionsWindow.Cure.ParalyseSpell.onTextChange = function(widget, text) + config.paralyseSpell = text + end + + conditionsWindow.Hold.HasteSpell:setText(config.hasteSpell) + conditionsWindow.Hold.HasteSpell.onTextChange = function(widget, text) + config.hasteSpell = text + end + + conditionsWindow.Hold.HasteCost:setText(config.hasteCost) + conditionsWindow.Hold.HasteCost.onTextChange = function(widget, text) + config.hasteCost = tonumber(text) + end + + conditionsWindow.Hold.UtamoCost:setText(config.utamoCost) + conditionsWindow.Hold.UtamoCost.onTextChange = function(widget, text) + config.utamoCost = tonumber(text) + end + + conditionsWindow.Hold.UtanaCost:setText(config.utanaCost) + conditionsWindow.Hold.UtanaCost.onTextChange = function(widget, text) + config.utanaCost = tonumber(text) + end + + conditionsWindow.Hold.UturaCost:setText(config.uturaCost) + conditionsWindow.Hold.UturaCost.onTextChange = function(widget, text) + config.uturaCost = tonumber(text) + end + + -- combo box + conditionsWindow.Hold.UturaType:setOption(config.uturaType) + conditionsWindow.Hold.UturaType.onOptionChange = function(widget) + config.uturaType = widget:getCurrentOption().text + end + + -- checkboxes + conditionsWindow.Cure.CurePoison:setChecked(config.curePoison) + conditionsWindow.Cure.CurePoison.onClick = function(widget) + config.curePoison = not config.curePoison + widget:setChecked(config.curePoison) + end + + conditionsWindow.Cure.CureCurse:setChecked(config.cureCurse) + conditionsWindow.Cure.CureCurse.onClick = function(widget) + config.cureCurse = not config.cureCurse + widget:setChecked(config.cureCurse) + end + + conditionsWindow.Cure.CureBleed:setChecked(config.cureBleed) + conditionsWindow.Cure.CureBleed.onClick = function(widget) + config.cureBleed = not config.cureBleed + widget:setChecked(config.cureBleed) + end + + conditionsWindow.Cure.CureBurn:setChecked(config.cureBurn) + conditionsWindow.Cure.CureBurn.onClick = function(widget) + config.cureBurn = not config.cureBurn + widget:setChecked(config.cureBurn) + end + + conditionsWindow.Cure.CureElectrify:setChecked(config.cureElectrify) + conditionsWindow.Cure.CureElectrify.onClick = function(widget) + config.cureElectrify = not config.cureElectrify + widget:setChecked(config.cureElectrify) + end + + conditionsWindow.Cure.CureParalyse:setChecked(config.cureParalyse) + conditionsWindow.Cure.CureParalyse.onClick = function(widget) + config.cureParalyse = not config.cureParalyse + widget:setChecked(config.cureParalyse) + end + + conditionsWindow.Hold.HoldHaste:setChecked(config.holdHaste) + conditionsWindow.Hold.HoldHaste.onClick = function(widget) + config.holdHaste = not config.holdHaste + widget:setChecked(config.holdHaste) + end + + conditionsWindow.Hold.HoldUtamo:setChecked(config.holdUtamo) + conditionsWindow.Hold.HoldUtamo.onClick = function(widget) + config.holdUtamo = not config.holdUtamo + widget:setChecked(config.holdUtamo) + end + + conditionsWindow.Hold.HoldUtana:setChecked(config.holdUtana) + conditionsWindow.Hold.HoldUtana.onClick = function(widget) + config.holdUtana = not config.holdUtana + widget:setChecked(config.holdUtana) + end + + conditionsWindow.Hold.HoldUtura:setChecked(config.holdUtura) + conditionsWindow.Hold.HoldUtura.onClick = function(widget) + config.holdUtura = not config.holdUtura + widget:setChecked(config.holdUtura) + end + + conditionsWindow.Hold.IgnoreInPz:setChecked(config.ignoreInPz) + conditionsWindow.Hold.IgnoreInPz.onClick = function(widget) + config.ignoreInPz = not config.ignoreInPz + widget:setChecked(config.ignoreInPz) + end + + conditionsWindow.Hold.StopHaste:setChecked(config.stopHaste) + conditionsWindow.Hold.StopHaste.onClick = function(widget) + config.stopHaste = not config.stopHaste + widget:setChecked(config.stopHaste) + end + + -- buttons + conditionsWindow.closeButton.onClick = function(widget) + conditionsWindow:hide() + end + + Conditions = {} + Conditions.show = function() + conditionsWindow:show() + conditionsWindow:raise() + conditionsWindow:focus() + end + end + + local utanaCast = nil + macro(500, function() + if not config.enabled or modules.game_cooldown.isGroupCooldownIconActive(2) then return end + if hppercent() > 95 then + if config.curePoison and mana() >= config.poisonCost and isPoisioned() then say("exana pox") + elseif config.cureCurse and mana() >= config.curseCost and isCursed() then say("exana mort") + elseif config.cureBleed and mana() >= config.bleedCost and isBleeding() then say("exana kor") + elseif config.cureBurn and mana() >= config.burnCost and isBurning() then say("exana flam") + elseif config.cureElectrify and mana() >= config.electrifyCost and isEnergized() then say("exana vis") + end + end + if (not config.ignoreInPz or not isInPz()) and config.holdUtura and mana() >= config.uturaCost and canCast(config.uturaType) and hppercent() < 90 then say(config.uturaType) + elseif (not config.ignoreInPz or not isInPz()) and config.holdUtana and mana() >= config.utanaCost and (not utanaCast or (now - utanaCast > 120000)) then say("utana vid") utanaCast = now + end + end) + + macro(50, function() + if not config.enabled then return end + if (not config.ignoreInPz or not isInPz()) and config.holdUtamo and mana() >= config.utamoCost and not hasManaShield() then say("utamo vita") + elseif ((not config.ignoreInPz or not isInPz()) and standTime() < 5000 and config.holdHaste and mana() >= config.hasteCost and not hasHaste() and not getSpellCoolDown(config.hasteSpell) and (not target() or not config.stopHaste or TargetBot.isCaveBotActionAllowed())) and standTime() < 3000 then say(config.hasteSpell) + elseif config.cureParalyse and mana() >= config.paralyseCost and isParalyzed() and not getSpellCoolDown(config.paralyseSpell) then say(config.paralyseSpell) + end + end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.otui new file mode 100644 index 0000000000..ee8d43bec6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/Conditions.otui @@ -0,0 +1,463 @@ +UturaComboBoxPopupMenu < ComboBoxPopupMenu +UturaComboBoxPopupMenuButton < ComboBoxPopupMenuButton +UturaComboBox < ComboBox + @onSetup: | + self:addOption("Utura") + self:addOption("Utura Gran") + +CureConditions < Panel + id: Cure + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 200 190 + + Label + id: label1 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 10 + margin-left: 5 + text: Poison + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label11 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 40 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: PoisonCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CurePoison + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label2 + anchors.left: label1.left + anchors.top: label1.bottom + margin-top: 10 + text: Curse + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label22 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 44 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: CurseCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureCurse + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label3 + anchors.left: label2.left + anchors.top: label2.bottom + margin-top: 10 + text: Bleed + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label33 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 46 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: BleedCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureBleed + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label4 + anchors.left: label3.left + anchors.top: label3.bottom + margin-top: 10 + text: Burn + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label44 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 50 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: BurnCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureBurn + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label5 + anchors.left: label4.left + anchors.top: label4.bottom + margin-top: 10 + text: Electify + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label55 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 33 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: ElectrifyCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureElectrify + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label6 + anchors.left: label5.left + anchors.top: label5.bottom + margin-top: 10 + text: Paralyse + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label66 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 26 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: ParalyseCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: CureParalyse + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label7 + anchors.left: label6.left + anchors.top: label6.bottom + margin-top: 10 + margin-left: 12 + text: Spell: + font: verdana-11px-rounded + + TextEdit + id: ParalyseSpell + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 10 + width: 100 + font: verdana-11px-rounded + +HoldConditions < Panel + id: Hold + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 200 190 + + Label + id: label1 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 10 + margin-left: 5 + text: Haste + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label11 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 44 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: HasteCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldHaste + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label2 + anchors.left: label1.left + anchors.top: label1.bottom + margin-top: 10 + margin-left: 12 + text: Spell: + font: verdana-11px-rounded + + TextEdit + id: HasteSpell + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 10 + width: 100 + font: verdana-11px-rounded + + Label + id: label3 + anchors.left: label1.left + anchors.top: label2.bottom + margin-top: 10 + text: Utana Vid + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label33 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 21 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: UtanaCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldUtana + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label4 + anchors.left: label3.left + anchors.top: label3.bottom + margin-top: 10 + text: Utamo Vita + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label44 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 12 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: UtamoCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldUtamo + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label5 + anchors.left: label4.left + anchors.top: label4.bottom + margin-top: 10 + text: Recovery + color: #ffaa00 + font: verdana-11px-rounded + + Label + id: label55 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 20 + text: Mana: + font: verdana-11px-rounded + + TextEdit + id: UturaCost + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 40 + font: verdana-11px-rounded + + CheckBox + id: HoldUtura + anchors.verticalCenter: prev.verticalCenter + anchors.right: parent.right + margin-right: 10 + + Label + id: label6 + anchors.left: label5.left + anchors.top: label5.bottom + margin-top: 10 + margin-left: 12 + text: Spell: + font: verdana-11px-rounded + + UturaComboBox + id: UturaType + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 10 + width: 100 + font: verdana-11px-rounded + + CheckBox + id: IgnoreInPz + anchors.left: label5.left + anchors.top: label6.bottom + margin-top: 12 + + Label + anchors.verticalCenter: IgnoreInPz.verticalCenter + anchors.left: prev.right + margin-top: 3 + margin-left: 5 + text: Don't Cast in Protection Zones + font: cipsoftFont + + CheckBox + id: StopHaste + anchors.horizontalCenter: IgnoreInPz.horizontalCenter + anchors.top: IgnoreInPz.bottom + margin-top: 8 + + Label + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-top: 3 + margin-left: 5 + text: Stop Haste if TargetBot Is Active + font: cipsoftFont + +ConditionsWindow < MainWindow + !text: tr('Condition Manager') + size: 445 280 + @onEscape: self:hide() + + CureConditions + id: Cure + anchors.top: parent.top + anchors.left: parent.left + margin-top: 7 + + Label + id: label + anchors.top: parent.top + anchors.left: parent.left + text: Cure Conditions + color: #88e3dd + margin-left: 10 + font: verdana-11px-rounded + + HoldConditions + id: Hold + anchors.top: parent.top + anchors.right: parent.right + margin-top: 7 + + Label + id: label + anchors.top: parent.top + anchors.right: parent.right + text: Hold Conditions + color: #88e3dd + margin-right: 100 + font: verdana-11px-rounded + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/Containers.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/Containers.lua new file mode 100644 index 0000000000..827d0a585d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/Containers.lua @@ -0,0 +1,674 @@ +setDefaultTab("Tools") +local panelName = "renameContainers" +if type(storage[panelName]) ~= "table" then + storage[panelName] = { + enabled = false; + height = 170, + purse = true; + list = { + { + value = "Main Backpack", + enabled = true, + item = 9601, + min = false, + items = { 3081, 3048 } + }, + { + value = "Runes", + enabled = true, + item = 2866, + min = true, + items = { 3161, 3180 } + }, + { + value = "Money", + enabled = true, + item = 2871, + min = true, + items = { 3031, 3035, 3043 } + }, + { + value = "Purse", + enabled = true, + item = 23396, + min = true, + items = {} + }, + } + } +end + +local config = storage[panelName] + +UI.Separator() +local renameContui = setupUI([[ +Panel + height: 50 + + Label + text-align: center + text: Container Panel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + font: verdana-11px-rounded + + BotSwitch + id: title + anchors.top: prev.bottom + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Open Minimised') + font: verdana-11px-rounded + + Button + id: editContList + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + font: verdana-11px-rounded + + Button + id: reopenCont + !text: tr('Reopen All') + anchors.left: parent.left + anchors.top: prev.bottom + anchors.right: parent.horizontalCenter + margin-right: 2 + height: 17 + margin-top: 3 + font: verdana-11px-rounded + + Button + id: minimiseCont + !text: tr('Minimise All') + anchors.top: prev.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + margin-right: 2 + height: 17 + font: verdana-11px-rounded + ]]) +renameContui:setId(panelName) + +g_ui.loadUIFromString([[ +BackpackName < Label + background-color: alpha + text-offset: 18 2 + focusable: true + height: 17 + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 1 + margin-left: 3 + + $focus: + background-color: #00000055 + + Button + id: state + !text: tr('M') + anchors.right: remove.left + anchors.verticalCenter: parent.verticalCenter + margin-right: 1 + width: 15 + height: 15 + + Button + id: remove + !text: tr('X') + !tooltip: tr('Remove') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + margin-right: 15 + width: 15 + height: 15 + + Button + id: openNext + !text: tr('N') + anchors.right: state.left + anchors.verticalCenter: parent.verticalCenter + margin-right: 1 + width: 15 + height: 15 + tooltip: Open container inside with the same ID. + +ContListsWindow < MainWindow + !text: tr('Container Names') + size: 465 170 + @onEscape: self:hide() + + TextList + id: itemList + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: separator.top + width: 200 + margin-bottom: 6 + margin-top: 3 + margin-left: 3 + vertical-scrollbar: itemListScrollBar + + VerticalScrollBar + id: itemListScrollBar + anchors.top: itemList.top + anchors.bottom: itemList.bottom + anchors.right: itemList.right + step: 14 + pixels-scroll: true + + VerticalSeparator + id: sep + anchors.top: parent.top + anchors.left: itemList.right + anchors.bottom: separator.top + margin-top: 3 + margin-bottom: 6 + margin-left: 10 + + Label + id: lblName + anchors.left: sep.right + anchors.top: sep.top + width: 70 + text: Name: + margin-left: 10 + margin-top: 3 + font: verdana-11px-rounded + + TextEdit + id: contName + anchors.left: lblName.right + anchors.top: sep.top + anchors.right: parent.right + font: verdana-11px-rounded + + Label + id: lblCont + anchors.left: lblName.left + anchors.verticalCenter: contId.verticalCenter + width: 70 + text: Container: + font: verdana-11px-rounded + + BotItem + id: contId + anchors.left: contName.left + anchors.top: contName.bottom + margin-top: 3 + + BotContainer + id: sortList + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + anchors.bottom: separator.top + margin-bottom: 6 + margin-top: 3 + + Label + anchors.left: lblCont.left + anchors.verticalCenter: sortList.verticalCenter + width: 70 + text: Items: + font: verdana-11px-rounded + + Button + id: addItem + anchors.right: contName.right + anchors.top: contName.bottom + margin-top: 5 + text: Add + width: 40 + font: cipsoftFont + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + CheckBox + id: purse + anchors.left: parent.left + anchors.bottom: parent.bottom + text: Open Purse + tooltip: Opens Store/Charm Purse + width: 85 + height: 15 + margin-top: 2 + margin-left: 3 + font: verdana-11px-rounded + + CheckBox + id: sort + anchors.left: prev.right + anchors.bottom: parent.bottom + text: Sort Items + tooltip: Sort items based on items widget + width: 85 + height: 15 + margin-top: 2 + margin-left: 15 + font: verdana-11px-rounded + + CheckBox + id: forceOpen + anchors.left: prev.right + anchors.bottom: parent.bottom + text: Keep Open + tooltip: Will keep open containers all the time + width: 85 + height: 15 + margin-top: 2 + margin-left: 15 + font: verdana-11px-rounded + + CheckBox + id: lootBag + anchors.left: prev.right + anchors.bottom: parent.bottom + text: Loot Bag + tooltip: Open Loot Bag (gunzodus franchaise) + width: 85 + height: 15 + margin-top: 2 + margin-left: 15 + font: verdana-11px-rounded + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + + ResizeBorder + id: bottomResizeBorder + anchors.fill: separator + height: 3 + minimum: 170 + maximum: 245 + margin-left: 3 + margin-right: 3 + background: #ffffff88 +]]) + +function findItemsInArray(t, tfind) + local tArray = {} + for x,v in pairs(t) do + if type(v) == "table" then + local aItem = t[x].item + local aEnabled = t[x].enabled + if aItem then + if tfind and aItem == tfind then + return x + elseif not tfind then + if aEnabled then + table.insert(tArray, aItem) + end + end + end + end + end + if not tfind then return tArray end +end + +local lstBPs + + +local openContainer = function(id) + local t = {getRight(), getLeft(), getAmmo()} -- if more slots needed then add them here + for i=1,#t do + local slotItem = t[i] + if slotItem and slotItem:getId() == id then + return g_game.open(slotItem, nil) + end + end + + for i, container in pairs(g_game.getContainers()) do + for i, item in ipairs(container:getItems()) do + if item:isContainer() and item:getId() == id then + return g_game.open(item, nil) + end + end + end +end + +function reopenBackpacks() + lstBPs = findItemsInArray(config.list) + + for _, container in pairs(g_game.getContainers()) do g_game.close(container) end + bpItem = getBack() + if bpItem ~= nil then + g_game.open(bpItem) + end + + schedule(500, function() + local delay = 200 + + if config.purse then + local item = getPurse() + if item then + use(item) + end + end + for i=1,#lstBPs do + schedule(delay, function() + openContainer(lstBPs[i]) + end) + delay = delay + 250 + end + end) + +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + contListWindow = UI.createWindow('ContListsWindow', rootWidget) + contListWindow:hide() + + contListWindow.onGeometryChange = function(widget, old, new) + if old.height == 0 then return end + + config.height = new.height + end + + contListWindow:setHeight(config.height or 170) + + renameContui.editContList.onClick = function(widget) + contListWindow:show() + contListWindow:raise() + contListWindow:focus() + end + + renameContui.reopenCont.onClick = function(widget) + reopenBackpacks() + end + + renameContui.minimiseCont.onClick = function(widget) + for i, container in ipairs(getContainers()) do + local containerWindow = container.window + containerWindow:setContentHeight(34) + end + end + + renameContui.title:setOn(config.enabled) + renameContui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) + end + + contListWindow.closeButton.onClick = function(widget) + contListWindow:hide() + end + + contListWindow.purse.onClick = function(widget) + config.purse = not config.purse + contListWindow.purse:setChecked(config.purse) + end + contListWindow.purse:setChecked(config.purse) + + contListWindow.sort.onClick = function(widget) + config.sort = not config.sort + contListWindow.sort:setChecked(config.sort) + end + contListWindow.sort:setChecked(config.sort) + + contListWindow.forceOpen.onClick = function(widget) + config.forceOpen = not config.forceOpen + contListWindow.forceOpen:setChecked(config.forceOpen) + end + contListWindow.forceOpen:setChecked(config.forceOpen) + + contListWindow.lootBag.onClick = function(widget) + config.lootBag = not config.lootBag + contListWindow.lootBag:setChecked(config.lootBag) + end + contListWindow.lootBag:setChecked(config.lootBag) + + local function refreshSortList(k, t) + t = t or {} + UI.Container(function() + t = contListWindow.sortList:getItems() + config.list[k].items = t + end, true, nil, contListWindow.sortList) + contListWindow.sortList:setItems(t) + end + refreshSortList(t) + + local refreshContNames = function(tFocus) + local storageVal = config.list + if storageVal and #storageVal > 0 then + for i, child in pairs(contListWindow.itemList:getChildren()) do + child:destroy() + end + for k, entry in pairs(storageVal) do + local label = g_ui.createWidget("BackpackName", contListWindow.itemList) + label.onMouseRelease = function() + contListWindow.contId:setItemId(entry.item) + contListWindow.contName:setText(entry.value) + if not entry.items then + entry.items = {} + end + contListWindow.sortList:setItems(entry.items) + refreshSortList(k, entry.items) + end + label.enabled.onClick = function(widget) + entry.enabled = not entry.enabled + label.enabled:setChecked(entry.enabled) + label.enabled:setTooltip(entry.enabled and 'Disable' or 'Enable') + label.enabled:setImageColor(entry.enabled and '#00FF00' or '#FF0000') + end + label.remove.onClick = function(widget) + table.removevalue(config.list, entry) + label:destroy() + end + label.state:setChecked(entry.min) + label.state.onClick = function(widget) + entry.min = not entry.min + label.state:setChecked(entry.min) + label.state:setColor(entry.min and '#00FF00' or '#FF0000') + label.state:setTooltip(entry.min and 'Open Minimised' or 'Do not minimise') + end + label.openNext.onClick = function(widget) + entry.openNext = not entry.openNext + label.openNext:setChecked(entry.openNext) + label.openNext:setColor(entry.openNext and '#00FF00' or '#FF0000') + end + label:setText(entry.value) + label.enabled:setChecked(entry.enabled) + label.enabled:setTooltip(entry.enabled and 'Disable' or 'Enable') + label.enabled:setImageColor(entry.enabled and '#00FF00' or '#FF0000') + label.state:setColor(entry.min and '#00FF00' or '#FF0000') + label.state:setTooltip(entry.min and 'Open Minimised' or 'Do not minimise') + label.openNext:setColor(entry.openNext and '#00FF00' or '#FF0000') + + if tFocus and entry.item == tFocus then + tFocus = label + end + end + if tFocus then contListWindow.itemList:focusChild(tFocus) end + end + end + contListWindow.addItem.onClick = function(widget) + local id = contListWindow.contId:getItemId() + local trigger = contListWindow.contName:getText() + + if id > 100 and trigger:len() > 0 then + local ifind = findItemsInArray(config.list, id) + if ifind then + config.list[ifind] = { item = id, value = trigger, enabled = config.list[ifind].enabled, min = config.list[ifind].min, items = config.list[ifind].items} + else + table.insert(config.list, { item = id, value = trigger, enabled = true, min = false, items = {} }) + end + contListWindow.contId:setItemId(0) + contListWindow.contName:setText('') + contListWindow.contName:setColor('white') + contListWindow.contName:setImageColor('#ffffff') + contListWindow.contId:setImageColor('#ffffff') + refreshContNames(id) + else + contListWindow.contId:setImageColor('red') + contListWindow.contName:setImageColor('red') + contListWindow.contName:setColor('red') + end + end + refreshContNames() +end + +onContainerOpen(function(container, previousContainer) + if not container.window then return end + local containerWindow = container.window + if not previousContainer then + containerWindow:setContentHeight(34) + end + + local storageVal = config.list + if storageVal and #storageVal > 0 then + for _, entry in pairs(storageVal) do + if entry.enabled and string.find(container:getContainerItem():getId(), entry.item) then + if entry.min then + containerWindow:minimize() + end + if renameContui.title:isOn() then + containerWindow:setText(entry.value) + end + if entry.openNext then + for i, item in ipairs(container:getItems()) do + if item:getId() == entry.item then + local time = #storageVal * 250 + schedule(time, function() + time = time + 250 + g_game.open(item) + end) + end + end + end + end + end + end +end) + +local function nameContainersOnLogin() + for i, container in ipairs(getContainers()) do + if renameContui.title:isOn() then + if not container.window then return end + local containerWindow = container.window + local storageVal = config.list + if storageVal and #storageVal > 0 then + for _, entry in pairs(storageVal) do + if entry.enabled and string.find(container:getContainerItem():getId(), entry.item) then + containerWindow:setText(entry.value) + end + end + end + end + end +end +nameContainersOnLogin() + +local function moveItem(item, destination) + return g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), item:getCount()) +end + +local function properTable(t) + local r = {} + for _, entry in pairs(t) do + if type(entry) == "number" then + table.insert(r, entry) + else + table.insert(r, entry.id) + end + end + return r +end + +local mainLoop = macro(150, function(macro) + if not config.sort and not config.purse then return end + + local storageVal = config.list + for _, entry in pairs(storageVal) do + local dId = entry.item + local items = properTable(entry.items) + -- sorting + if config.sort then + for _, container in pairs(getContainers()) do + local cName = container:getName():lower() + if not cName:find("depot") and not cName:find("depot") and not cName:find("quiver") then + local cId = container:getContainerItem():getId() + for __, item in ipairs(container:getItems()) do + local id = item:getId() + if table.find(items, id) and cId ~= dId then + local destination = getContainerByItem(dId, true) + if destination and not containerIsFull(destination) then + return moveItem(item, destination) + end + end + end + end + end + end + -- keep open / purse 23396 + if config.forceOpen then + local container = getContainerByItem(dId) + if not container then + local t = {getBack(), getAmmo(), getFinger(), getNeck(), getLeft(), getRight()} + for i=1,#t do + local slot = t[i] + if slot and slot:getId() == dId then + return g_game.open(slot) + end + end + local cItem = findItem(dId) + if cItem then + return g_game.open(cItem) + end + end + end + end + if config.purse and config.forceOpen and not getContainerByItem(23396) then + return use(getPurse()) + end + if config.lootBag and config.forceOpen and not getContainerByItem(23721) then + if findItem(23721) then + g_game.open(findItem(23721), getContainerByItem(23396)) + else + return use(getPurse()) + end + end + macro:setOff() +end) + + +onContainerOpen(function(container, previousContainer) + mainLoop:setOn() +end) + +onAddItem(function(container, slot, item, oldItem) + mainLoop:setOn() +end) + +onPlayerInventoryChange(function(slot, item, oldItem) + mainLoop:setOn() +end) + +onContainerClose(function(container) + if not container.lootContainer then + mainLoop:setOn() + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/Dropper.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/Dropper.lua new file mode 100644 index 0000000000..96674b9823 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/Dropper.lua @@ -0,0 +1,146 @@ +setDefaultTab("Tools") + +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Dropper') + + Button + id: edit + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Edit +]]) + +local edit = setupUI([[ +Panel + height: 150 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 5 + text-align: center + text: Trash: + + BotContainer + id: TrashItems + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 32 + + Label + anchors.top: prev.bottom + margin-top: 5 + anchors.left: parent.left + anchors.right: parent.right + text-align: center + text: Use: + + BotContainer + id: UseItems + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 32 + + Label + anchors.top: prev.bottom + margin-top: 5 + anchors.left: parent.left + anchors.right: parent.right + text-align: center + text: Drop if below 150 cap: + + BotContainer + id: CapItems + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 32 +]]) +edit:hide() + +if not storage.dropper then + storage.dropper = { + enabled = false, + trashItems = { 283, 284, 285 }, + useItems = { 21203, 14758 }, + capItems = { 21175 } + } +end + +local config = storage.dropper + +local showEdit = false +ui.edit.onClick = function(widget) + showEdit = not showEdit + if showEdit then + edit:show() + else + edit:hide() + end +end + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) + config.enabled = not config.enabled + ui.title:setOn(config.enabled) +end + +UI.Container(function() + config.trashItems = edit.TrashItems:getItems() + end, true, nil, edit.TrashItems) +edit.TrashItems:setItems(config.trashItems) + +UI.Container(function() + config.useItems = edit.UseItems:getItems() + end, true, nil, edit.UseItems) +edit.UseItems:setItems(config.useItems) + +UI.Container(function() + config.capItems = edit.CapItems:getItems() + end, true, nil, edit.CapItems) +edit.CapItems:setItems(config.capItems) + +local function properTable(t) + local r = {} + + for _, entry in pairs(t) do + table.insert(r, entry.id) + end + return r +end + +macro(200, function() + if not config.enabled then return end + local tables = {properTable(config.capItems), properTable(config.useItems), properTable(config.trashItems)} + + local containers = getContainers() + for i=1,3 do + for _, container in pairs(containers) do + for __, item in ipairs(container:getItems()) do + for ___, userItem in ipairs(tables[i]) do + if item:getId() == userItem then + return i == 1 and freecap() < 150 and dropItem(item) or + i == 2 and use(item) or + i == 3 and dropItem(item) + end + end + end + end + end + +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/Equipper.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/Equipper.lua new file mode 100644 index 0000000000..cc3c782997 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/Equipper.lua @@ -0,0 +1,775 @@ +local panelName = "EquipperPanel" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: switch + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('EQ Manager') + + Button + id: setup + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup +]]) +ui:setId(panelName) + +if not storage[panelName] or not storage[panelName].bosses then -- no bosses - old ver + storage[panelName] = { + enabled = false, + rules = {}, + bosses = {} + } +end + +local config = storage[panelName] + +ui.switch:setOn(config.enabled) +ui.switch.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) +end + +local conditions = { -- always add new conditions at the bottom + "Item is available and not worn.", -- nothing 1 + "Monsters around is more than: ", -- spinbox 2 + "Monsters around is less than: ", -- spinbox 3 + "Health precent is below:", -- spinbox 4 + "Health precent is above:", -- spinbox 5 + "Mana precent is below:", -- spinbox 6 + "Mana precent is above:", -- spinbox 7 + "Target name is:", -- BotTextEdit 8 + "Hotkey is being pressed:", -- BotTextEdit 9 + "Player is paralyzed", -- nothing 10 + "Player is in protection zone", -- nothing 11 + "Players around is more than:", -- spinbox 12 + "Players around is less than:", -- spinbox 13 + "TargetBot Danger is Above:", -- spinbox 14 + "Blacklist player in range (sqm)", -- spinbox 15 + "Target is Boss", -- nothing 16 + "Player is NOT in protection zone", -- nothing 17 + "CaveBot is ON, TargetBot is OFF" -- nothing 18 +} + +local conditionNumber = 1 +local optionalConditionNumber = 2 + +local mainWindow = UI.createWindow("EquipWindow") +mainWindow:hide() + +ui.setup.onClick = function() + mainWindow:show() + mainWindow:raise() + mainWindow:focus() +end + +local inputPanel = mainWindow.inputPanel +local listPanel = mainWindow.listPanel +local namePanel = mainWindow.profileName +local eqPanel = mainWindow.setup +local bossPanel = mainWindow.bossPanel + +local slotWidgets = {eqPanel.head, eqPanel.body, eqPanel.legs, eqPanel.feet, eqPanel.neck, eqPanel["left-hand"], eqPanel["right-hand"], eqPanel.finger, eqPanel.ammo} -- back is disabled + +local function setCondition(first, n) + local widget + local spinBox + local textEdit + + if first then + widget = inputPanel.condition.description.text + spinBox = inputPanel.condition.spinbox + textEdit = inputPanel.condition.text + else + widget = inputPanel.optionalCondition.description.text + spinBox = inputPanel.optionalCondition.spinbox + textEdit = inputPanel.optionalCondition.text + end + + -- reset values after change + spinBox:setValue(0) + textEdit:setText('') + + if n == 1 or n == 10 or n == 11 or n == 16 or n == 17 or n == 18 then + spinBox:hide() + textEdit:hide() + elseif n == 9 or n == 8 then + spinBox:hide() + textEdit:show() + if n == 9 then + textEdit:setWidth(75) + else + textEdit:setWidth(200) + end + else + spinBox:show() + textEdit:hide() + end + widget:setText(conditions[n]) +end + +local function resetFields() + conditionNumber = 1 + optionalConditionNumber = 2 + setCondition(false, optionalConditionNumber) + setCondition(true, conditionNumber) + for i, widget in ipairs(slotWidgets) do + widget:setItemId(0) + widget:setChecked(false) + end + for i, child in ipairs(listPanel.list:getChildren()) do + child.display = false + end + namePanel.profileName:setText("") + inputPanel.condition.text:setText('') + inputPanel.condition.spinbox:setValue(0) + inputPanel.useSecondCondition:setText('-') + inputPanel.optionalCondition.text:setText('') + inputPanel.optionalCondition.spinbox:setValue(0) + inputPanel.optionalCondition:hide() + bossPanel:hide() + listPanel:show() + mainWindow.bossList:setText('Boss List') + bossPanel.name:setText('') +end +resetFields() + +mainWindow.closeButton.onClick = function() + resetFields() + mainWindow:hide() +end + +inputPanel.optionalCondition:hide() +inputPanel.useSecondCondition.onOptionChange = function(widget, option, data) + if option ~= "-" then + inputPanel.optionalCondition:show() + else + inputPanel.optionalCondition:hide() + end +end + +-- add default text & windows +setCondition(true, 1) +setCondition(false, 2) + +-- in/de/crementation buttons +inputPanel.condition.nex.onClick = function() + local max = #conditions + + if inputPanel.optionalCondition:isVisible() then + if conditionNumber == max then + if optionalConditionNumber == 1 then + conditionNumber = 2 + else + conditionNumber = 1 + end + else + local futureNumber = conditionNumber + 1 + local safeFutureNumber = conditionNumber + 2 > max and 1 or conditionNumber + 2 + conditionNumber = futureNumber ~= optionalConditionNumber and futureNumber or safeFutureNumber + end + else + conditionNumber = conditionNumber == max and 1 or conditionNumber + 1 + if optionalConditionNumber == conditionNumber then + optionalConditionNumber = optionalConditionNumber == max and 1 or optionalConditionNumber + 1 + setCondition(false, optionalConditionNumber) + end + end + setCondition(true, conditionNumber) +end + +inputPanel.condition.pre.onClick = function() + local max = #conditions + + if inputPanel.optionalCondition:isVisible() then + if conditionNumber == 1 then + if optionalConditionNumber == max then + conditionNumber = max-1 + else + conditionNumber = max + end + else + local futureNumber = conditionNumber - 1 + local safeFutureNumber = conditionNumber - 2 < 1 and max or conditionNumber - 2 + conditionNumber = futureNumber ~= optionalConditionNumber and futureNumber or safeFutureNumber + end + else + conditionNumber = conditionNumber == 1 and max or conditionNumber - 1 + if optionalConditionNumber == conditionNumber then + optionalConditionNumber = optionalConditionNumber == 1 and max or optionalConditionNumber - 1 + setCondition(false, optionalConditionNumber) + end + end + setCondition(true, conditionNumber) +end + +inputPanel.optionalCondition.nex.onClick = function() + local max = #conditions + + if optionalConditionNumber == max then + if conditionNumber == 1 then + optionalConditionNumber = 2 + else + optionalConditionNumber = 1 + end + else + local futureNumber = optionalConditionNumber + 1 + local safeFutureNumber = optionalConditionNumber + 2 > max and 1 or optionalConditionNumber + 2 + optionalConditionNumber = futureNumber ~= conditionNumber and futureNumber or safeFutureNumber + end + setCondition(false, optionalConditionNumber) +end + +inputPanel.optionalCondition.pre.onClick = function() + local max = #conditions + + if optionalConditionNumber == 1 then + if conditionNumber == max then + optionalConditionNumber = max-1 + else + optionalConditionNumber = max + end + else + local futureNumber = optionalConditionNumber - 1 + local safeFutureNumber = optionalConditionNumber - 2 < 1 and max or optionalConditionNumber - 2 + optionalConditionNumber = futureNumber ~= conditionNumber and futureNumber or safeFutureNumber + end + setCondition(false, optionalConditionNumber) +end + +listPanel.up.onClick = function(widget) + local focused = listPanel.list:getFocusedChild() + local n = listPanel.list:getChildIndex(focused) + local t = config.rules + + t[n], t[n-1] = t[n-1], t[n] + if n-1 == 1 then + widget:setEnabled(false) + end + listPanel.down:setEnabled(true) + listPanel.list:moveChildToIndex(focused, n-1) + listPanel.list:ensureChildVisible(focused) +end + +listPanel.down.onClick = function(widget) + local focused = listPanel.list:getFocusedChild() + local n = listPanel.list:getChildIndex(focused) + local t = config.rules + + t[n], t[n+1] = t[n+1], t[n] + if n + 1 == listPanel.list:getChildCount() then + widget:setEnabled(false) + end + listPanel.up:setEnabled(true) + listPanel.list:moveChildToIndex(focused, n+1) + listPanel.list:ensureChildVisible(focused) +end + +eqPanel.cloneEq.onClick = function(widget) + eqPanel.head:setItemId(getHead() and getHead():getId() or 0) + eqPanel.body:setItemId(getBody() and getBody():getId() or 0) + eqPanel.legs:setItemId(getLeg() and getLeg():getId() or 0) + eqPanel.feet:setItemId(getFeet() and getFeet():getId() or 0) + eqPanel.neck:setItemId(getNeck() and getNeck():getId() or 0) + eqPanel["left-hand"]:setItemId(getLeft() and getLeft():getId() or 0) + eqPanel["right-hand"]:setItemId(getRight() and getRight():getId() or 0) + eqPanel.finger:setItemId(getFinger() and getFinger():getId() or 0) + eqPanel.ammo:setItemId(getAmmo() and getAmmo():getId() or 0) +end + +eqPanel.default.onClick = resetFields + +-- buttons disabled by default +listPanel.up:setEnabled(false) +listPanel.down:setEnabled(false) + +-- correct background image +for i, widget in ipairs(slotWidgets) do + widget:setTooltip("Right click to set as slot to unequip") + widget.onItemChange = function(widget) + local selfId = widget:getItemId() + widget:setOn(selfId > 100) + if widget:isChecked() then + widget:setChecked(selfId < 100) + end + end + widget.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local clearItem = widget:isChecked() == false + widget:setChecked(not widget:isChecked()) + if clearItem then + widget:setItemId(0) + end + end + end +end + +inputPanel.condition.description.onMouseWheel = function(widget, mousePos, scroll) + if scroll == 1 then + inputPanel.condition.nex.onClick() + else + inputPanel.condition.pre.onClick() + end +end + +inputPanel.optionalCondition.description.onMouseWheel = function(widget, mousePos, scroll) + if scroll == 1 then + inputPanel.optionalCondition.nex.onClick() + else + inputPanel.optionalCondition.pre.onClick() + end +end + +namePanel.profileName.onTextChange = function(widget, text) + local button = inputPanel.add + text = text:lower() + + for i, child in ipairs(listPanel.list:getChildren()) do + local name = child:getText():lower() + + button:setText(name == text and "Overwrite" or "Add Rule") + button:setTooltip(name == text and "Overwrite existing rule named: "..name, "Add new rule to the list: "..name) + end +end + +local function setupPreview(display, data) + namePanel.profileName:setText('') + if not display then + resetFields() + else + for i, value in ipairs(data) do + local widget = slotWidgets[i] + if value == false then + widget:setChecked(false) + widget:setItemId(0) + elseif value == true then + widget:setChecked(true) + widget:setItemId(0) + else + widget:setChecked(false) + widget:setItemId(value) + end + end + end +end + +local function refreshRules() + local list = listPanel.list + + list:destroyChildren() + for i,v in ipairs(config.rules) do + local widget = UI.createWidget('Rule', list) + widget:setId(v.name) + widget:setText(v.name) + widget.ruleData = v + widget.remove.onClick = function() + widget:destroy() + table.remove(config.rules, table.find(config.rules, v)) + listPanel.up:setEnabled(false) + listPanel.down:setEnabled(false) + refreshRules() + end + widget.visible:setColor(v.visible and "green" or "red") + widget.visible.onClick = function() + v.visible = not v.visible + widget.visible:setColor(v.visible and "green" or "red") + end + widget.enabled:setChecked(v.enabled) + widget.enabled.onClick = function() + v.enabled = not v.enabled + widget.enabled:setChecked(v.enabled) + end + widget.onHoverChange = function(widget, hover) + for i, child in ipairs(list:getChildren()) do + if child.display then return end + end + setupPreview(hover, widget.ruleData.data) + end + widget.onDoubleClick = function(widget) + local ruleData = widget.ruleData + widget.display = true + setupPreview(true, ruleData.data) + conditionNumber = ruleData.mainCondition + optionalConditionNumber = ruleData.optionalCondition + setCondition(false, optionalConditionNumber) + setCondition(true, conditionNumber) + inputPanel.useSecondCondition:setOption(ruleData.relation) + namePanel.profileName:setText(v.name) + + if type(ruleData.mainValue) == "string" then + inputPanel.condition.text:setText(ruleData.mainValue) + elseif type(ruleData.mainValue) == "number" then + inputPanel.condition.spinbox:setValue(ruleData.mainValue) + end + + if type(ruleData.optValue) == "string" then + inputPanel.optionalCondition.text:setText(ruleData.optValue) + elseif type(ruleData.optValue) == "number" then + inputPanel.optionalCondition.spinbox:setValue(ruleData.optValue) + end + end + widget.onClick = function() + local panel = listPanel + if #panel.list:getChildren() == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(false) + elseif panel.list:getChildIndex(panel.list:getFocusedChild()) == 1 then + panel.up:setEnabled(false) + panel.down:setEnabled(true) + elseif panel.list:getChildIndex(panel.list:getFocusedChild()) == #panel.list:getChildren() then + panel.up:setEnabled(true) + panel.down:setEnabled(false) + else + panel.up:setEnabled(true) + panel.down:setEnabled(true) + end + end + end +end +refreshRules() + +inputPanel.add.onClick = function(widget) + local mainVal + local optVal + local t = {} + local relation = inputPanel.useSecondCondition:getText() + local profileName = namePanel.profileName:getText() + if profileName:len() == 0 then + return warn("Please fill profile name!") + end + + for i, widget in ipairs(slotWidgets) do + local checked = widget:isChecked() + local id = widget:getItemId() + + if checked then + table.insert(t, true) -- unequip selected slot + elseif id then + table.insert(t, id) -- equip selected item + else + table.insert(t, false) -- ignore slot + end + end + + if conditionNumber == 1 then + mainVal = nil + elseif conditionNumber == 8 then + mainVal = inputPanel.condition.text:getText() + if mainVal:len() == 0 then + return warn("[vBot Equipper] Please fill the name of the creature.") + end + elseif conditionNumber == 9 then + mainVal = inputPanel.condition.text:getText() + if mainVal:len() == 0 then + return warn("[vBot Equipper] Please set correct hotkey.") + end + else + mainVal = inputPanel.condition.spinbox:getValue() + end + + if relation ~= "-" then + if optionalConditionNumber == 1 then + optVal = nil + elseif optionalConditionNumber == 8 then + optVal = inputPanel.optionalCondition.text:getText() + if optVal:len() == 0 then + return warn("[vBot Equipper] Please fill the name of the creature.") + end + elseif optionalConditionNumber == 9 then + optVal = inputPanel.optionalCondition.text:getText() + if optVal:len() == 0 then + return warn("[vBot Equipper] Please set correct hotkey.") + end + else + optVal = inputPanel.optionalCondition.spinbox:getValue() + end + end + + local index + for i, v in ipairs(config.rules) do + if v.name == profileName then + index = i -- search if there's already rule with this name + end + end + + local ruleData = { + name = profileName, + data = t, + enabled = true, + visible = true, + mainCondition = conditionNumber, + optionalCondition = optionalConditionNumber, + mainValue = mainVal, + optValue = optVal, + relation = relation, + } + + if index then + config.rules[index] = ruleData -- overwrite + else + table.insert(config.rules, ruleData) -- create new one + end + + for i, child in ipairs(listPanel.list:getChildren()) do + child.display = false + end + resetFields() + refreshRules() +end + +mainWindow.bossList.onClick = function(widget) + if bossPanel:isVisible() then + bossPanel:hide() + listPanel:show() + widget:setText('Boss List') + else + bossPanel:show() + listPanel:hide() + widget:setText('Rule List') + + end +end + +-- create boss labels +for i, v in ipairs(config.bosses) do + local widget = UI.createWidget("BossLabel", bossPanel.list) + widget:setText(v) + widget.remove.onClick = function() + table.remove(config.bosses, table.find(config.bosses, v)) + widget:destroy() + end +end + +bossPanel.add.onClick = function() + local name = bossPanel.name:getText() + + if name:len() == 0 then + return warn("[Equipped] Please enter boss name!") + elseif table.find(config.bosses, name:lower(), true) then + return warn("[Equipper] Boss already added!") + end + + local widget = UI.createWidget("BossLabel", bossPanel.list) + widget:setText(name) + widget.remove.onClick = function() + table.remove(config.bosses, table.find(config.bosses, name)) + widget:destroy() + end + + table.insert(config.bosses, name) + bossPanel.name:setText('') +end + +local function interpreteCondition(n, v) + + if n == 1 then + return true + elseif n == 2 then + return getMonsters() > v + elseif n == 3 then + return getMonsters() < v + elseif n == 4 then + return hppercent() < v + elseif n == 5 then + return hppercent() > v + elseif n == 6 then + return manapercent() < v + elseif n == 7 then + return manapercent() > v + elseif n == 8 then + return target() and target():getName():lower() == v:lower() or false + elseif n == 9 then + return g_keyboard.isKeyPressed(v) + elseif n == 10 then + return isParalyzed() + elseif n == 11 then + return isInPz() + elseif n == 12 then + return getPlayers() > v + elseif n == 13 then + return getPlayers() < v + elseif n == 14 then + return TargetBot.Danger() > v and TargetBot.isOn() + elseif n == 15 then + return isBlackListedPlayerInRange(v) + elseif n == 16 then + return target() and table.find(config.bosses, target():getName():lower(), true) and true or false + elseif n == 17 then + return not isInPz() + elseif n == 18 then + return CaveBot.isOn() and TargetBot.isOff() + end +end + +local function finalCheck(first,relation,second) + if relation == "-" then + return first + elseif relation == "and" then + return first and second + elseif relation == "or" then + return first or second + end +end + +local function isEquipped(id) + local t = {getNeck(), getHead(), getBody(), getRight(), getLeft(), getLeg(), getFeet(), getFinger(), getAmmo()} + local ids = {id, getInactiveItemId(id), getActiveItemId(id)} + + for i, slot in pairs(t) do + if slot and table.find(ids, slot:getId()) then + return true + end + end + return false +end + +local function unequipItem(table) + local slots = {getHead(), getBody(), getLeg(), getFeet(), getNeck(), getLeft(), getRight(), getFinger(), getAmmo()} + + if type(table) ~= "table" then return end + for i, slot in ipairs(table) do + local physicalSlot = slots[i] + + if slot == true and physicalSlot then + local id = physicalSlot:getId() + + if g_game.getClientVersion() >= 910 then + -- new tibia + g_game.equipItemId(id) + else + -- old tibia + local dest + for i, container in ipairs(getContainers()) do + local cname = container:getName() + if not containerIsFull(container) then + if not cname:find("loot") and (cname:find("backpack") or cname:find("bag") or cname:find("chess")) then + dest = container + end + break + end + end + + if not dest then return true end + local pos = dest:getSlotPosition(dest:getItemsCount()) + g_game.move(physicalSlot, pos, physicalSlot:getCount()) + end + return true + end + end + return false +end + +local function equipItem(id, slot) + -- need to correct slots... + if slot == 2 then + slot = 4 + elseif slot == 3 then + slot = 7 + elseif slot == 8 then + slot = 9 + elseif slot == 5 then + slot = 2 + elseif slot == 4 then + slot = 8 + elseif slot == 9 then + slot = 10 + elseif slot == 7 then + slot = 5 + end + + + if g_game.getClientVersion() >= 910 then + -- new tibia + return g_game.equipItemId(id) + else + -- old tibia + local item = findItem(id) + return moveToSlot(item, slot) + end +end + + +local function markChild(child) + if mainWindow:isVisible() then + for i, child in ipairs(listPanel.list:getChildren()) do + if child ~= widget then + child:setColor('white') + end + end + widget:setColor('green') + end +end + + +local missingItem = false +local lastRule = false +local correctEq = false +EquipManager = macro(50, function() + if not config.enabled then return end + if #config.rules == 0 then return end + + for i, widget in ipairs(listPanel.list:getChildren()) do + local rule = widget.ruleData + if rule.enabled then + + -- conditions + local firstCondition = interpreteCondition(rule.mainCondition, rule.mainValue) + local optionalCondition = nil + if rule.relation ~= "-" then + optionalCondition = interpreteCondition(rule.optionalCondition, rule.optValue) + end + + -- checks + if finalCheck(firstCondition, rule.relation, optionalCondition) then + + -- performance edits, loop reset + local resetLoop = not missingItem and correctEq and lastRule == rule + if resetLoop then return end + + -- reset executed rule + + + -- first check unequip + if unequipItem(rule.data) == true then + delay(200) + return + end + + -- equiploop + for slot, item in ipairs(rule.data) do + if type(item) == "number" and item > 100 then + if not isEquipped(item) then + if rule.visible then + if findItem(item) then + missingItem = false + delay(200) + return equipItem(item, slot) + else + missingItem = true + end + else + missingItem = false + delay(200) + return equipItem(item, slot) + end + end + end + end + + correctEq = not missingItem and true or false + -- even if nothing was done, exit function to hold rule + return + end + + + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.lua new file mode 100644 index 0000000000..530f429e33 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.lua @@ -0,0 +1,712 @@ +local standBySpells = false +local standByItems = false + +local red = "#ff0800" -- "#ff0800" / #ea3c53 best +local blue = "#7ef9ff" + +setDefaultTab("HP") +local healPanelName = "healbot" +local ui = setupUI([[ +Panel + height: 38 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('HealBot') + + Button + id: settings + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + Button + id: 1 + anchors.top: prev.bottom + anchors.left: parent.left + text: 1 + margin-right: 2 + margin-top: 4 + size: 17 17 + + Button + id: 2 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 2 + margin-left: 4 + size: 17 17 + + Button + id: 3 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 3 + margin-left: 4 + size: 17 17 + + Button + id: 4 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 4 + margin-left: 4 + size: 17 17 + + Button + id: 5 + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + text: 5 + margin-left: 4 + size: 17 17 + + Label + id: name + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + text-align: center + margin-left: 4 + height: 17 + text: Profile #1 + background: #292A2A +]]) +ui:setId(healPanelName) + +if not HealBotConfig[healPanelName] or not HealBotConfig[healPanelName][1] or #HealBotConfig[healPanelName] ~= 5 then + HealBotConfig[healPanelName] = { + [1] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #1", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [2] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #2", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [3] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #3", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [4] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #4", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + [5] = { + enabled = false, + spellTable = {}, + itemTable = {}, + name = "Profile #5", + Visible = true, + Cooldown = true, + Interval = true, + Conditions = true, + Delay = true, + MessageDelay = false + }, + } +end + +if not HealBotConfig.currentHealBotProfile or HealBotConfig.currentHealBotProfile == 0 or HealBotConfig.currentHealBotProfile > 5 then + HealBotConfig.currentHealBotProfile = 1 +end + +-- finding correct table, manual unfortunately +local currentSettings +local setActiveProfile = function() + local n = HealBotConfig.currentHealBotProfile + currentSettings = HealBotConfig[healPanelName][n] +end +setActiveProfile() + +local activeProfileColor = function() + for i=1,5 do + if i == HealBotConfig.currentHealBotProfile then + ui[i]:setColor("green") + else + ui[i]:setColor("white") + end + end +end +activeProfileColor() + +ui.title:setOn(currentSettings.enabled) +ui.title.onClick = function(widget) + currentSettings.enabled = not currentSettings.enabled + widget:setOn(currentSettings.enabled) + vBotConfigSave("heal") +end + +ui.settings.onClick = function(widget) + healWindow:show() + healWindow:raise() + healWindow:focus() +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + healWindow = UI.createWindow('HealWindow', rootWidget) + healWindow:hide() + + healWindow.onVisibilityChange = function(widget, visible) + if not visible then + vBotConfigSave("heal") + healWindow.healer:show() + healWindow.settings:hide() + healWindow.settingsButton:setText("Settings") + end + end + + healWindow.settingsButton.onClick = function(widget) + if healWindow.healer:isVisible() then + healWindow.healer:hide() + healWindow.settings:show() + widget:setText("Back") + else + healWindow.healer:show() + healWindow.settings:hide() + widget:setText("Settings") + end + end + + local setProfileName = function() + ui.name:setText(currentSettings.name) + end + healWindow.settings.profiles.Name.onTextChange = function(widget, text) + currentSettings.name = text + setProfileName() + end + healWindow.settings.list.Visible.onClick = function(widget) + currentSettings.Visible = not currentSettings.Visible + healWindow.settings.list.Visible:setChecked(currentSettings.Visible) + end + healWindow.settings.list.Cooldown.onClick = function(widget) + currentSettings.Cooldown = not currentSettings.Cooldown + healWindow.settings.list.Cooldown:setChecked(currentSettings.Cooldown) + end + healWindow.settings.list.Interval.onClick = function(widget) + currentSettings.Interval = not currentSettings.Interval + healWindow.settings.list.Interval:setChecked(currentSettings.Interval) + end + healWindow.settings.list.Conditions.onClick = function(widget) + currentSettings.Conditions = not currentSettings.Conditions + healWindow.settings.list.Conditions:setChecked(currentSettings.Conditions) + end + healWindow.settings.list.Delay.onClick = function(widget) + currentSettings.Delay = not currentSettings.Delay + healWindow.settings.list.Delay:setChecked(currentSettings.Delay) + end + healWindow.settings.list.MessageDelay.onClick = function(widget) + currentSettings.MessageDelay = not currentSettings.MessageDelay + healWindow.settings.list.MessageDelay:setChecked(currentSettings.MessageDelay) + end + + local refreshSpells = function() + if currentSettings.spellTable then + healWindow.healer.spells.spellList:destroyChildren() + for _, entry in pairs(currentSettings.spellTable) do + local label = UI.createWidget("SpellEntry", healWindow.healer.spells.spellList) + label.enabled:setChecked(entry.enabled) + label.enabled.onClick = function(widget) + standBySpells = false + standByItems = false + entry.enabled = not entry.enabled + label.enabled:setChecked(entry.enabled) + end + label.remove.onClick = function(widget) + standBySpells = false + standByItems = false + table.removevalue(currentSettings.spellTable, entry) + reindexTable(currentSettings.spellTable) + label:destroy() + end + label:setText("(MP>" .. entry.cost .. ") " .. entry.origin .. entry.sign .. entry.value .. ": " .. entry.spell) + end + end + end + refreshSpells() + + local refreshItems = function() + if currentSettings.itemTable then + healWindow.healer.items.itemList:destroyChildren() + for _, entry in pairs(currentSettings.itemTable) do + local label = UI.createWidget("ItemEntry", healWindow.healer.items.itemList) + label.enabled:setChecked(entry.enabled) + label.enabled.onClick = function(widget) + standBySpells = false + standByItems = false + entry.enabled = not entry.enabled + label.enabled:setChecked(entry.enabled) + end + label.remove.onClick = function(widget) + standBySpells = false + standByItems = false + table.removevalue(currentSettings.itemTable, entry) + reindexTable(currentSettings.itemTable) + label:destroy() + end + label.id:setItemId(entry.item) + label:setText(entry.origin .. entry.sign .. entry.value .. ": " .. entry.item) + end + end + end + refreshItems() + + healWindow.healer.spells.MoveUp.onClick = function(widget) + local input = healWindow.healer.spells.spellList:getFocusedChild() + if not input then return end + local index = healWindow.healer.spells.spellList:getChildIndex(input) + if index < 2 then return end + + local t = currentSettings.spellTable + + t[index],t[index-1] = t[index-1], t[index] + healWindow.healer.spells.spellList:moveChildToIndex(input, index - 1) + healWindow.healer.spells.spellList:ensureChildVisible(input) + end + + healWindow.healer.spells.MoveDown.onClick = function(widget) + local input = healWindow.healer.spells.spellList:getFocusedChild() + if not input then return end + local index = healWindow.healer.spells.spellList:getChildIndex(input) + if index >= healWindow.healer.spells.spellList:getChildCount() then return end + + local t = currentSettings.spellTable + + t[index],t[index+1] = t[index+1],t[index] + healWindow.healer.spells.spellList:moveChildToIndex(input, index + 1) + healWindow.healer.spells.spellList:ensureChildVisible(input) + end + + healWindow.healer.items.MoveUp.onClick = function(widget) + local input = healWindow.healer.items.itemList:getFocusedChild() + if not input then return end + local index = healWindow.healer.items.itemList:getChildIndex(input) + if index < 2 then return end + + local t = currentSettings.itemTable + + t[index],t[index-1] = t[index-1], t[index] + healWindow.healer.items.itemList:moveChildToIndex(input, index - 1) + healWindow.healer.items.itemList:ensureChildVisible(input) + end + + healWindow.healer.items.MoveDown.onClick = function(widget) + local input = healWindow.healer.items.itemList:getFocusedChild() + if not input then return end + local index = healWindow.healer.items.itemList:getChildIndex(input) + if index >= healWindow.healer.items.itemList:getChildCount() then return end + + local t = currentSettings.itemTable + + t[index],t[index+1] = t[index+1],t[index] + healWindow.healer.items.itemList:moveChildToIndex(input, index + 1) + healWindow.healer.items.itemList:ensureChildVisible(input) + end + + healWindow.healer.spells.addSpell.onClick = function(widget) + + local spellFormula = healWindow.healer.spells.spellFormula:getText():trim() + local manaCost = tonumber(healWindow.healer.spells.manaCost:getText()) + local spellTrigger = tonumber(healWindow.healer.spells.spellValue:getText()) + local spellSource = healWindow.healer.spells.spellSource:getCurrentOption().text + local spellEquasion = healWindow.healer.spells.spellCondition:getCurrentOption().text + local source + local equasion + + if not manaCost then + warn("HealBot: incorrect mana cost value!") + healWindow.healer.spells.spellFormula:setText('') + healWindow.healer.spells.spellValue:setText('') + healWindow.healer.spells.manaCost:setText('') + return + end + if not spellTrigger then + warn("HealBot: incorrect condition value!") + healWindow.healer.spells.spellFormula:setText('') + healWindow.healer.spells.spellValue:setText('') + healWindow.healer.spells.manaCost:setText('') + return + end + + if spellSource == "Current Mana" then + source = "MP" + elseif spellSource == "Current Health" then + source = "HP" + elseif spellSource == "Mana Percent" then + source = "MP%" + elseif spellSource == "Health Percent" then + source = "HP%" + else + source = "burst" + end + + if spellEquasion == "Above" then + equasion = ">" + elseif spellEquasion == "Below" then + equasion = "<" + else + equasion = "=" + end + + if spellFormula:len() > 0 then + table.insert(currentSettings.spellTable, {index = #currentSettings.spellTable+1, spell = spellFormula, sign = equasion, origin = source, cost = manaCost, value = spellTrigger, enabled = true}) + healWindow.healer.spells.spellFormula:setText('') + healWindow.healer.spells.spellValue:setText('') + healWindow.healer.spells.manaCost:setText('') + end + standBySpells = false + standByItems = false + refreshSpells() + end + + healWindow.healer.items.addItem.onClick = function(widget) + + local id = healWindow.healer.items.itemId:getItemId() + local trigger = tonumber(healWindow.healer.items.itemValue:getText()) + local src = healWindow.healer.items.itemSource:getCurrentOption().text + local eq = healWindow.healer.items.itemCondition:getCurrentOption().text + local source + local equasion + + if not trigger then + warn("HealBot: incorrect trigger value!") + healWindow.healer.items.itemId:setItemId(0) + healWindow.healer.items.itemValue:setText('') + return + end + + if src == "Current Mana" then + source = "MP" + elseif src == "Current Health" then + source = "HP" + elseif src == "Mana Percent" then + source = "MP%" + elseif src == "Health Percent" then + source = "HP%" + else + source = "burst" + end + + if eq == "Above" then + equasion = ">" + elseif eq == "Below" then + equasion = "<" + else + equasion = "=" + end + + if id > 100 then + table.insert(currentSettings.itemTable, {index = #currentSettings.itemTable+1,item = id, sign = equasion, origin = source, value = trigger, enabled = true}) + standBySpells = false + standByItems = false + refreshItems() + healWindow.healer.items.itemId:setItemId(0) + healWindow.healer.items.itemValue:setText('') + end + end + + healWindow.closeButton.onClick = function(widget) + healWindow:hide() + end + + local loadSettings = function() + ui.title:setOn(currentSettings.enabled) + setProfileName() + healWindow.settings.profiles.Name:setText(currentSettings.name) + refreshSpells() + refreshItems() + healWindow.settings.list.Visible:setChecked(currentSettings.Visible) + healWindow.settings.list.Cooldown:setChecked(currentSettings.Cooldown) + healWindow.settings.list.Delay:setChecked(currentSettings.Delay) + healWindow.settings.list.MessageDelay:setChecked(currentSettings.MessageDelay) + healWindow.settings.list.Interval:setChecked(currentSettings.Interval) + healWindow.settings.list.Conditions:setChecked(currentSettings.Conditions) + end + loadSettings() + + local profileChange = function() + setActiveProfile() + activeProfileColor() + loadSettings() + vBotConfigSave("heal") + end + + local resetSettings = function() + currentSettings.enabled = false + currentSettings.spellTable = {} + currentSettings.itemTable = {} + currentSettings.Visible = true + currentSettings.Cooldown = true + currentSettings.Delay = true + currentSettings.MessageDelay = false + currentSettings.Interval = true + currentSettings.Conditions = true + currentSettings.name = "Profile #" .. HealBotConfig.currentBotProfile + end + + -- profile buttons + for i=1,5 do + local button = ui[i] + button.onClick = function() + HealBotConfig.currentHealBotProfile = i + profileChange() + end + end + + healWindow.settings.profiles.ResetSettings.onClick = function() + resetSettings() + loadSettings() + end + + + -- public functions + HealBot = {} -- global table + + HealBot.isOn = function() + return currentSettings.enabled + end + + HealBot.isOff = function() + return not currentSettings.enabled + end + + HealBot.setOff = function() + currentSettings.enabled = false + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + HealBot.setOn = function() + currentSettings.enabled = true + ui.title:setOn(currentSettings.enabled) + vBotConfigSave("atk") + end + + HealBot.getActiveProfile = function() + return HealBotConfig.currentHealBotProfile -- returns number 1-5 + end + + HealBot.setActiveProfile = function(n) + if not n or not tonumber(n) or n < 1 or n > 5 then + return error("[HealBot] wrong profile parameter! should be 1 to 5 is " .. n) + else + HealBotConfig.currentHealBotProfile = n + profileChange() + end + end + + HealBot.show = function() + healWindow:show() + healWindow:raise() + healWindow:focus() + end +end + +-- spells +macro(100, function() + if standBySpells then return end + if not currentSettings.enabled then return end + local somethingIsOnCooldown = false + + for _, entry in pairs(currentSettings.spellTable) do + if entry.enabled and entry.cost < mana() then + if canCast(entry.spell, not currentSettings.Conditions, not currentSettings.Cooldown) then + if entry.origin == "HP%" then + if entry.sign == "=" and hppercent() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and hppercent() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and hppercent() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "HP" then + if entry.sign == "=" and hp() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and hp() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and hp() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "MP%" then + if entry.sign == "=" and manapercent() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and manapercent() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and manapercent() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "MP" then + if entry.sign == "=" and mana() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and mana() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and mana() <= entry.value then + say(entry.spell) + return + end + elseif entry.origin == "burst" then + if entry.sign == "=" and burstDamageValue() == entry.value then + say(entry.spell) + return + elseif entry.sign == ">" and burstDamageValue() >= entry.value then + say(entry.spell) + return + elseif entry.sign == "<" and burstDamageValue() <= entry.value then + say(entry.spell) + return + end + end + else + somethingIsOnCooldown = true + end + end + end + if not somethingIsOnCooldown then + standBySpells = true + end +end) + +-- items +macro(100, function() + if standByItems then return end + if not currentSettings.enabled or #currentSettings.itemTable == 0 then return end + if currentSettings.Delay and vBot.isUsing then return end + if currentSettings.MessageDelay and vBot.isUsingPotion then return end + + if not currentSettings.MessageDelay then + delay(400) + end + + if TargetBot.isOn() and TargetBot.Looting.getStatus():len() > 0 and currentSettings.Interval then + if not currentSettings.MessageDelay then + delay(700) + else + delay(200) + end + end + + for _, entry in pairs(currentSettings.itemTable) do + local item = findItem(entry.item) + if (not currentSettings.Visible or item) and entry.enabled then + if entry.origin == "HP%" then + if entry.sign == "=" and hppercent() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and hppercent() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and hppercent() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "HP" then + if entry.sign == "=" and hp() == tonumberentry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and hp() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and hp() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "MP%" then + if entry.sign == "=" and manapercent() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and manapercent() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and manapercent() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "MP" then + if entry.sign == "=" and mana() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and mana() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and mana() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + elseif entry.origin == "burst" then + if entry.sign == "=" and burstDamageValue() == entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == ">" and burstDamageValue() >= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + elseif entry.sign == "<" and burstDamageValue() <= entry.value then + g_game.useInventoryItemWith(entry.item, player) + return + end + end + end + end + standByItems = true +end) +UI.Separator() + +onPlayerHealthChange(function(healthPercent) + standByItems = false + standBySpells = false +end) + +onManaChange(function(player, mana, maxMana, oldMana, oldMaxMana) + standByItems = false + standBySpells = false +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.otui new file mode 100644 index 0000000000..fb8cb03d7e --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/HealBot.otui @@ -0,0 +1,492 @@ +SettingCheckBox < CheckBox + text-wrap: true + text-auto-resize: true + margin-top: 3 + font: verdana-11px-rounded + +SpellSourceBoxPopupMenu < ComboBoxPopupMenu +SpellSourceBoxPopupMenuButton < ComboBoxPopupMenuButton +SpellSourceBox < ComboBox + @onSetup: | + self:addOption("Current Mana") + self:addOption("Current Health") + self:addOption("Mana Percent") + self:addOption("Health Percent") + self:addOption("Burst Damage") + +SpellConditionBoxPopupMenu < ComboBoxPopupMenu +SpellConditionBoxPopupMenuButton < ComboBoxPopupMenuButton +SpellConditionBox < ComboBox + @onSetup: | + self:addOption("Below") + self:addOption("Above") + self:addOption("Equal To") + +SpellEntry < Label + background-color: alpha + text-offset: 18 1 + focusable: true + height: 16 + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + text-offset: 1 0 + width: 15 + height: 15 + +ItemEntry < Label + background-color: alpha + text-offset: 40 1 + focusable: true + height: 16 + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + + UIItem + id: id + anchors.left: prev.right + margin-left: 3 + anchors.verticalCenter: parent.verticalCenter + size: 15 15 + focusable: false + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + text-offset: 1 0 + width: 15 + height: 15 + +SpellHealing < FlatPanel + size: 490 130 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + margin-left: 5 + text: Spell Healing + color: #269e26 + font: verdana-11px-rounded + + SpellSourceBox + id: spellSource + anchors.top: spellList.top + anchors.left: spellList.right + margin-left: 80 + width: 125 + font: verdana-11px-rounded + + Label + id: whenSpell + anchors.left: spellList.right + anchors.verticalCenter: prev.verticalCenter + text: When + margin-left: 7 + font: verdana-11px-rounded + + Label + id: isSpell + anchors.left: spellList.right + anchors.top: whenSpell.bottom + text: Is + margin-top: 9 + margin-left: 7 + font: verdana-11px-rounded + + SpellConditionBox + id: spellCondition + anchors.left: spellSource.left + anchors.top: spellSource.bottom + marin-top: 15 + width: 80 + font: verdana-11px-rounded + + TextEdit + id: spellValue + anchors.left: spellCondition.right + anchors.top: spellCondition.top + anchors.bottom: spellCondition.bottom + anchors.right: spellSource.right + font: verdana-11px-rounded + + Label + id: castSpell + anchors.left: isSpell.left + anchors.top: isSpell.bottom + text: Cast + margin-top: 9 + font: verdana-11px-rounded + + TextEdit + id: spellFormula + anchors.left: spellCondition.left + anchors.top: spellCondition.bottom + anchors.right: spellValue.right + font: verdana-11px-rounded + + Label + id: manaSpell + anchors.left: castSpell.left + anchors.top: castSpell.bottom + text: Mana Cost: + margin-top: 8 + font: verdana-11px-rounded + + TextEdit + id: manaCost + anchors.left: spellFormula.left + anchors.top: spellFormula.bottom + width: 40 + font: verdana-11px-rounded + + TextList + id: spellList + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.top: parent.top + padding: 1 + padding-top: 2 + width: 270 + margin-bottom: 7 + margin-left: 7 + margin-top: 10 + vertical-scrollbar: spellListScrollBar + + VerticalScrollBar + id: spellListScrollBar + anchors.top: spellList.top + anchors.bottom: spellList.bottom + anchors.right: spellList.right + step: 14 + pixels-scroll: true + + Button + id: addSpell + anchors.right: spellFormula.right + anchors.bottom: spellList.bottom + text: Add + size: 40 17 + font: cipsoftFont + + Button + id: MoveUp + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Up + size: 55 17 + font: cipsoftFont + + Button + id: MoveDown + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Down + size: 55 17 + font: cipsoftFont + +ItemHealing < FlatPanel + size: 490 120 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + margin-left: 5 + text: Item Healing + color: #ff4513 + font: verdana-11px-rounded + + SpellSourceBox + id: itemSource + anchors.top: itemList.top + anchors.right: parent.right + margin-right: 10 + width: 128 + font: verdana-11px-rounded + + Label + id: whenItem + anchors.left: itemList.right + anchors.verticalCenter: prev.verticalCenter + text: When + margin-left: 7 + font: verdana-11px-rounded + + Label + id: isItem + anchors.left: itemList.right + anchors.top: whenItem.bottom + text: Is + margin-top: 9 + margin-left: 7 + font: verdana-11px-rounded + + SpellConditionBox + id: itemCondition + anchors.left: itemSource.left + anchors.top: itemSource.bottom + marin-top: 15 + width: 80 + font: verdana-11px-rounded + + TextEdit + id: itemValue + anchors.left: itemCondition.right + anchors.top: itemCondition.top + anchors.bottom: itemCondition.bottom + width: 49 + font: verdana-11px-rounded + + Label + id: useItem + anchors.left: isItem.left + anchors.top: isItem.bottom + text: Use + margin-top: 15 + font: verdana-11px-rounded + + BotItem + id: itemId + anchors.left: itemCondition.left + anchors.top: itemCondition.bottom + + TextList + id: itemList + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.top: parent.top + padding: 1 + padding-top: 2 + width: 270 + margin-top: 10 + margin-bottom: 7 + margin-left: 8 + vertical-scrollbar: itemListScrollBar + + VerticalScrollBar + id: itemListScrollBar + anchors.top: itemList.top + anchors.bottom: itemList.bottom + anchors.right: itemList.right + step: 14 + pixels-scroll: true + + Button + id: addItem + anchors.right: itemValue.right + anchors.bottom: itemList.bottom + text: Add + size: 40 17 + font: cipsoftFont + + Button + id: MoveUp + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Up + size: 55 17 + font: cipsoftFont + + Button + id: MoveDown + anchors.right: prev.left + anchors.bottom: prev.bottom + margin-right: 5 + text: Move Down + size: 55 17 + font: cipsoftFont + +HealerPanel < Panel + size: 510 275 + + SpellHealing + id: spells + anchors.top: parent.top + margin-top: 8 + anchors.left: parent.left + + ItemHealing + id: items + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 10 + +HealBotSettingsPanel < Panel + size: 500 267 + padding-top: 8 + + FlatPanel + id: list + anchors.fill: parent + margin-right: 240 + padding-left: 6 + padding-right: 6 + padding-top: 6 + layout: + type: verticalBox + + Label + text: Additional Settings + text-align: center + font: verdana-11px-rounded + + HorizontalSeparator + + SettingCheckBox + id: Cooldown + text: Check spell cooldowns + margin-top: 10 + + SettingCheckBox + id: Visible + text: Items must be visible (recommended) + + SettingCheckBox + id: Delay + text: Don't use items when interacting + + SettingCheckBox + id: Interval + text: Additional delay when looting corpses + + SettingCheckBox + id: Conditions + text: Also check conditions from RL Tibia + + SettingCheckBox + id: MessageDelay + text: Cooldown based on "Aaaah..." message + + VerticalSeparator + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.left: prev.right + margin-left: 8 + + FlatPanel + id: profiles + anchors.fill: parent + anchors.left: prev.left + margin-left: 8 + margin-right: 8 + padding: 8 + + Label + text: Profile Settings + text-align: center + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + font: verdana-11px-rounded + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + + Label + anchors.top: prev.bottom + margin-top: 30 + anchors.left: parent.left + anchors.right: parent.right + text-align: center + font: verdana-11px-rounded + text: Profile Name: + + TextEdit + id: Name + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + + Button + id: ResetSettings + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + text: Reset Current Profile + text-auto-resize: true + color: #ff4513 + +HealWindow < MainWindow + !text: tr('Self Healer') + size: 520 360 + @onEscape: self:hide() + + Label + id: title + anchors.left: parent.left + anchors.top: parent.top + margin-left: 2 + !text: tr('More important methods come first (Example: Exura gran above Exura)') + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + HealerPanel + id: healer + anchors.top: prev.bottom + anchors.left: parent.left + + HealBotSettingsPanel + id: settings + anchors.top: title.bottom + anchors.left: parent.left + visible: false + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-right: 5 + + Button + id: settingsButton + !text: tr('Settings') + font: cipsoftFont + anchors.left: parent.left + anchors.bottom: parent.bottom + size: 45 21 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/Sio.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/Sio.lua new file mode 100644 index 0000000000..ade2c0dfdc --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/Sio.lua @@ -0,0 +1,252 @@ +setDefaultTab("Main") + local panelName = "advancedFriendHealer" + local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Friend Healer') + + Button + id: editList + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + + ]], parent) + ui:setId(panelName) + + if not storage[panelName] then + storage[panelName] = { + minMana = 60, + minFriendHp = 40, + customSpellName = "exura max sio", + customSpell = false, + distance = 8, + itemHeal = false, + id = 3160, + exuraSio = false, + exuraGranSio = false, + exuraMasRes = false, + healEk = false, + healRp = false, + healEd = false, + healMs = false + } + end + + local config = storage[panelName] + + -- basic elements + ui.title:setOn(config.enabled) + ui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) + end + ui.editList.onClick = function(widget) + sioListWindow:show() + sioListWindow:raise() + sioListWindow:focus() + end + + rootWidget = g_ui.getRootWidget() + if rootWidget then + sioListWindow = UI.createWindow('SioListWindow', rootWidget) + sioListWindow:hide() + + -- TextWindow + sioListWindow.spellName:setText(config.customSpellName) + sioListWindow.spellName.onTextChange = function(widget, text) + config.customSpellName = text + end + + -- botswitches + sioListWindow.spell:setOn(config.customSpell) + sioListWindow.spell.onClick = function(widget) + config.customSpell = not config.customSpell + widget:setOn(config.customSpell) + end + sioListWindow.item:setOn(config.itemHeal) + sioListWindow.item.onClick = function(widget) + config.itemHeal = not config.itemHeal + widget:setOn(config.itemHeal) + end + sioListWindow.exuraSio:setOn(config.exuraSio) + sioListWindow.exuraSio.onClick = function(widget) + config.exuraSio = not config.exuraSio + widget:setOn(config.exuraSio) + end + sioListWindow.exuraGranSio:setOn(config.exuraGranSio) + sioListWindow.exuraGranSio.onClick = function(widget) + config.exuraGranSio = not config.exuraGranSio + widget:setOn(config.exuraGranSio) + end + sioListWindow.exuraMasRes:setOn(config.exuraMasRes) + sioListWindow.exuraMasRes.onClick = function(widget) + config.exuraMasRes = not config.exuraMasRes + widget:setOn(config.exuraMasRes) + end + sioListWindow.vocation.ED:setOn(config.healEd) + sioListWindow.vocation.ED.onClick = function(widget) + config.healEd = not config.healEd + widget:setOn(config.healEd) + end + sioListWindow.vocation.MS:setOn(config.healMs) + sioListWindow.vocation.MS.onClick = function(widget) + config.healMs = not config.healMs + widget:setOn(config.healMs) + end + sioListWindow.vocation.EK:setOn(config.healEk) + sioListWindow.vocation.EK.onClick = function(widget) + config.healEk = not config.healEk + widget:setOn(config.healEk) + end + sioListWindow.vocation.RP:setOn(config.healRp) + sioListWindow.vocation.RP.onClick = function(widget) + config.healRp = not config.healRp + widget:setOn(config.healRp) + end + + -- functions + local updateMinManaText = function() + sioListWindow.manaInfo:setText("Minimum Mana >= " .. config.minMana .. "%") + end + local updateFriendHpText = function() + sioListWindow.friendHp:setText("Heal Friend Below " .. config.minFriendHp .. "% hp") + end + local updateDistanceText = function() + sioListWindow.distText:setText("Max Distance: " .. config.distance) + end + + -- scrollbars and text updates + sioListWindow.Distance:setValue(config.distance) + sioListWindow.Distance.onValueChange = function(scroll, value) + config.distance = value + updateDistanceText() + end + updateDistanceText() + + sioListWindow.minMana:setValue(config.minMana) + sioListWindow.minMana.onValueChange = function(scroll, value) + config.minMana = value + updateMinManaText() + end + updateMinManaText() + + sioListWindow.minFriendHp:setValue(config.minFriendHp) + sioListWindow.minFriendHp.onValueChange = function(scroll, value) + config.minFriendHp = value + updateFriendHpText() + end + updateFriendHpText() + + sioListWindow.itemId:setItemId(config.id) + sioListWindow.itemId.onItemChange = function(widget) + config.id = widget:getItemId() + end + + sioListWindow.closeButton.onClick = function(widget) + sioListWindow:hide() + end + + end + + -- local variables + local newTibia = g_game.getClientVersion() >= 960 + + local function isValid(name) + if not newTibia then return true end + + local voc = vBot.BotServerMembers[name] + if not voc then return true end + + if voc == 11 then voc = 1 + elseif voc == 12 then voc = 2 + elseif voc == 13 then voc = 3 + elseif voc == 14 then voc = 4 + end + + local isOk = false + if voc == 1 and config.healEk then + isOk = true + elseif voc == 2 and config.healRp then + isOk = true + elseif voc == 3 and config.healMs then + isOk = true + elseif voc == 4 and config.healEd then + isOk = true + end + + return isOk + end + + macro(200, function() + if not config.enabled then return end + if modules.game_cooldown.isGroupCooldownIconActive(2) then return end + + --[[ + 1. custom spell + 2. exura gran sio - at 50% of minHpValue + 3. exura gran mas res + 4. exura sio + 5. item healing + --]] + + -- exura gran sio & custom spell + if config.customSpell or config.exuraGranSio then + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and spec ~= player and isValid(spec:getName()) and spec:canShoot() then + if isFriend(spec) then + if config.customSpell and spec:getHealthPercent() <= config.minFriendHp then + return cast(config.customSpellName .. ' "' .. spec:getName() .. '"', 1000) + end + if config.exuraGranSio and spec:getHealthPercent() <= config.minFriendHp/3 then + if canCast('exura gran sio "' .. spec:getName() ..'"') then + return cast('exura gran sio "' .. spec:getName() ..'"', 60000) + end + end + end + end + end + end + + -- exura gran mas res and standard sio + local friends = 0 + if config.exuraMasRes then + for i, spec in ipairs(getSpectators(player, largeRuneArea)) do + if spec:isPlayer() and spec ~= player and isValid(spec:getName()) and spec:canShoot() then + if isFriend(spec) and spec:getHealthPercent() <= config.minFriendHp then + friends = friends + 1 + end + end + end + if friends > 1 then + return cast('exura gran mas res', 2000) + end + end + if config.exuraSio or config.itemHeal then + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and spec ~= player and isValid(spec:getName()) and spec:canShoot() then + if isFriend(spec) then + if spec:getHealthPercent() <= config.minFriendHp then + if config.exuraSio then + return cast('exura sio "' .. spec:getName() .. '"', 1000) + elseif findItem(config.id) and distanceFromPlayer(spec:getPosition()) <= config.distance then + return useWith(config.id, spec) + end + end + end + end + end + end + + end) +addSeparator() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/alarms.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/alarms.lua new file mode 100644 index 0000000000..928beb6aa2 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/alarms.lua @@ -0,0 +1,223 @@ +local panelName = "alarms" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Alarms') + + Button + id: alerts + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Edit + +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = {} +end + +local config = storage[panelName] + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) +end + +local window = UI.createWindow("AlarmsWindow") +window:hide() + +ui.alerts.onClick = function() + window:show() + window:raise() + window:focus() +end + +local widgets = +{ + "AlarmCheckBox", + "AlarmCheckBoxAndSpinBox", + "AlarmCheckBoxAndTextEdit" +} + +local parents = +{ + window.list, + window.settingsList +} + + +-- type +addAlarm = function(id, title, defaultValue, alarmType, parent, tooltip) + local widget = UI.createWidget(widgets[alarmType], parents[parent]) + widget:setId(id) + + if type(config[id]) ~= 'table' then + config[id] = {} + end + + widget.tick:setText(title) + widget.tick:setChecked(config[id].enabled) + widget.tick:setTooltip(tooltip) + widget.tick.onClick = function() + config[id].enabled = not config[id].enabled + widget.tick:setChecked(config[id].enabled) + end + + if alarmType > 1 and type(config[id].value) == 'nil' then + config[id].value = defaultValue + end + + if alarmType == 2 then + widget.value:setValue(config[id].value) + widget.value.onValueChange = function(widget, value) + config[id].value = value + end + elseif alarmType == 3 then + widget.text:setText(config[id].value) + widget.text.onTextChange = function(widget, newText) + config[id].value = newText + end + end + +end + +-- settings +addAlarm("ignoreFriends", "Ignore Friends", true, 1, 2) +addAlarm("flashClient", "Flash Client", true, 1, 2) + +-- alarm list +addAlarm("damageTaken", "Damage Taken", false, 1, 1) +addAlarm("lowHealth", "Low Health", 20, 2, 1) +addAlarm("lowMana", "Low Mana", 20, 2, 1) +addAlarm("playerAttack", "Player Attack", false, 1, 1) + +UI.Separator(window.list) + +addAlarm("privateMsg", "Private Message", false, 1, 1) +addAlarm("defaultMsg", "Default Message", false, 1, 1) +addAlarm("customMessage", "Custom Message:", "", 3, 1, "You can add text, that if found in any incoming message will trigger alert.\n You can add many, just separate them by comma.") + +UI.Separator(window.list) + +addAlarm("creatureDetected", "Creature Detected", false, 1, 1) +addAlarm("playerDetected", "Player Detected", false, 1, 1) +addAlarm("creatureName", "Creature Name:", "", 3, 1, "You can add a name or part of it, that if found in any visible creature name will trigger alert.\nYou can add many, just separate them by comma.") + + +local lastCall = now +local function alarm(file, windowText) + if now - lastCall < 2000 then return end -- 2s delay + lastCall = now + + if not g_resources.fileExists(file) then + file = "/sounds/alarm.ogg" + lastCall = now + 4000 -- alarm.ogg length is 6s + end + + + if modules.game_bot.g_app.getOs() == "windows" and config.flashClient.enabled then + g_window.flash() + end + g_window.setTitle(player:getName() .. " - " .. windowText) + playSound(file) +end + +-- damage taken & custom message +onTextMessage(function(mode, text) + if not config.enabled then return end + if mode == 22 and config.damageTaken.enabled then + return alarm('/sounds/magnum.ogg', "Damage Received!") + end + + if config.customMessage.enabled then + local alertText = config.customMessage.value + if alertText:len() > 0 then + text = text:lower() + local parts = string.split(alertText, ",") + + for i=1,#parts do + local part = parts[i] + part = part:trim() + part = part:lower() + + if text:find(part) then + return alarm('/sounds/magnum.ogg', "Special Message!") + end + end + end + end +end) + +-- default & private message +onTalk(function(name, level, mode, text, channelId, pos) + if not config.enabled then return end + if name == player:getName() then return end -- ignore self messages + if config.ignoreFriends.enabled and isFriend(name) then return end -- ignore friends if enabled + + if mode == 1 and config.defaultMsg.enabled then + return alarm("/sounds/magnum.ogg", "Default Message!") + end + + if mode == 4 and config.privateMsg.enabled then + return alarm("/sounds/Private_Message.ogg", "Private Message!") + end +end) + +-- health & mana +macro(100, function() + if not config.enabled then return end + if config.lowHealth.enabled then + if hppercent() < config.lowHealth.value then + return alarm("/sounds/Low_Health.ogg", "Low Health!") + end + end + + if config.lowMana.enabled then + if hppercent() < config.lowMana.value then + return alarm("/sounds/Low_Mana.ogg", "Low Mana!") + end + end + + for i, spec in ipairs(getSpectators()) do + if not spec:isLocalPlayer() and not (config.ignoreFriends.enabled and isFriend(spec)) then + + if config.creatureDetected.enabled then + return alarm("/sounds/magnum.ogg", "Creature Detected!") + end + + if spec:isPlayer() then + if spec:isTimedSquareVisible() and config.playerAttack.enabled then + return alarm("/sounds/Player_Attack.ogg", "Player Attack!") + end + if config.playerDetected.enabled then + return alarm("/sounds/Player_Detected.ogg", "Player Detected!") + end + end + + if config.creatureName.enabled then + local name = spec:getName():lower() + local fragments = string.split(config.creatureName.value, ",") + + for i=1,#fragments do + local frag = fragments[i]:trim():lower() + + if name:lower():find(frag) then + return alarm("/sounds/alarm.ogg", "Special Creature Detected!") + end + end + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/alarms.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/alarms.otui new file mode 100644 index 0000000000..ea8faa61e0 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/alarms.otui @@ -0,0 +1,135 @@ +AlarmCheckBox < Panel + height: 20 + margin-top: 2 + + CheckBox + id: tick + anchors.fill: parent + margin-top: 4 + font: verdana-11px-rounded + text: Player Attack + text-offset: 17 -3 + +AlarmCheckBoxAndSpinBox < Panel + height: 20 + margin-top: 2 + + CheckBox + id: tick + anchors.fill: parent + anchors.right: next.left + margin-top: 4 + font: verdana-11px-rounded + text: Player Attack + text-offset: 17 -3 + + SpinBox + id: value + anchors.top: parent.top + margin-top: 1 + margin-bottom: 1 + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 40 + minimum: 0 + maximum: 100 + step: 1 + editable: true + focusable: true + +AlarmCheckBoxAndTextEdit < Panel + height: 20 + margin-top: 2 + + CheckBox + id: tick + anchors.fill: parent + anchors.right: next.left + margin-top: 4 + font: verdana-11px-rounded + text: Creature Name + text-offset: 17 -3 + + BotTextEdit + id: text + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 150 + font: terminus-10px + margin-top: 1 + margin-bottom: 1 + +AlarmsWindow < MainWindow + !text: tr('Alarms') + size: 330 400 + padding: 15 + @onEscape: self:hide() + + FlatPanel + id: list + anchors.fill: parent + anchors.bottom: settingsList.top + margin-bottom: 20 + margin-top: 10 + layout: verticalBox + padding: 10 + padding-top: 5 + + FlatPanel + id: settingsList + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: separator.top + margin-bottom: 5 + margin-top: 10 + padding: 5 + padding-left: 10 + layout: + type: verticalBox + fit-children: true + + Label + anchors.verticalCenter: settingsList.top + anchors.left: settingsList.left + margin-left: 5 + width: 200 + text: Alarms Settings + font: verdana-11px-rounded + color: #9f5031 + + Label + anchors.verticalCenter: list.top + anchors.left: list.left + margin-left: 5 + width: 200 + text: Active Alarms + font: verdana-11px-rounded + color: #9f5031 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + ResizeBorder + id: bottomResizeBorder + anchors.fill: separator + height: 3 + minimum: 260 + maximum: 600 + margin-left: 3 + margin-right: 3 + background: #ffffff88 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-right: 5 + @onClick: self:getParent():hide() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.lua new file mode 100644 index 0000000000..74859f161e --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.lua @@ -0,0 +1,1826 @@ +--[[ + Bot-based Tibia 12 features v1.1 + made by Vithrax + + Credits also to: + - Martín#2318 + - Lee#7725 + + Thanks for ideas, graphics, functions, design tips! + + br, Vithrax +]] + +-- here you can fix incorrect bosses names in cooldown messages +local BOSSES = { + -- {in message, correct one} + {"Scarlet Etzel", "Scarlett Etzel"}, + {"Leiden", "Ravenous Hunger"}, + {"Urmahlulu", "Urmahlullu"} +} + +vBot.CaveBotData = vBot.CaveBotData or { + refills = 0, + rounds = 0, + time = {}, + lastRefill = os.time(), + refillTime = {} +} +local lootWorth = 0 +local wasteWorth = 0 +local balance = 0 +local balanceDesc = "" +local hourDesc = "" +local desc = "" +local hour = "" +local launchTime = now +local startExp = exp() +local dmgTable = {} +local healTable = {} +local expTable = {} +local totalDmg = 0 +local totalHeal = 0 +local dmgDistribution = {} +local first = {l="-", r="0"} +local second = {l="-", r="0"} +local third = {l="-", r="0"} +local fourth = {l="-", r="0"} +local five = {l="-", r="0"} +storage.bestHit = storage.bestHit or 0 +storage.bestHeal = storage.bestHeal or 0 +local lootedItems = {} +local useData = {} +local usedItems ={} +local lastDataSend = {0, 0} +local analyzerButton +local killList = {} +local membersData = {} +HuntingSessionStart = os.date('%Y-%m-%d, %H:%M:%S') + +if not storage.analyzers then + storage.analyzers = { + trackedLoot = {}, + trackedBoss = {}, + outfits = {}, + customPrices = {}, + lootChannel = true, + rarityFrames = true, + } +end + +storage.analyzers = storage.analyzers or {} +storage.analyzers.trackedLoot = storage.analyzers.trackedLoot or {} +storage.analyzers.trackedBoss = storage.analyzers.trackedBoss or {} +storage.analyzers.outfits = storage.analyzers.outfits or {} +local trackedLoot = storage.analyzers.trackedLoot + +--destroy old windows +local windowsTable = {"MainAnalyzerWindow", + "HuntingAnalyzerWindow", + "LootAnalyzerWindow", + "SupplyAnalyzerWindow", + "ImpactAnalyzerWindow", + "XPAnalyzerWindow", + "PartyAnalyzerWindow", + "DropTracker", + "CaveBotStats", + "BossTracker" + } + + for i, window in ipairs(windowsTable) do + local element = g_ui.getRootWidget():recursiveGetChildById(window) + + if element then + element:destroy() + end +end + +local mainWindow = UI.createMiniWindow("MainAnalyzerWindow") +mainWindow:hide() +mainWindow:setContentMaximumHeight(267) +local huntingWindow = UI.createMiniWindow("HuntingAnalyzer") +huntingWindow:hide() +local lootWindow = UI.createMiniWindow("LootAnalyzer") +lootWindow:hide() +local supplyWindow = UI.createMiniWindow("SupplyAnalyzer") +supplyWindow:hide() +local impactWindow = UI.createMiniWindow("ImpactAnalyzer") +impactWindow:hide() +impactWindow:setContentMaximumHeight(615) +local xpWindow = UI.createMiniWindow("XPAnalyzer") +xpWindow:hide() +xpWindow:setContentMaximumHeight(230) +local settingsWindow = UI.createWindow("FeaturesWindow") +settingsWindow:hide() +local partyHuntWindow = UI.createMiniWindow("PartyAnalyzerWindow") +partyHuntWindow:hide() +local dropTrackerWindow = UI.createMiniWindow("DropTracker") +dropTrackerWindow:hide() +local statsWindow = UI.createMiniWindow("CaveBotStats") +statsWindow:hide() +local bossWindow = UI.createMiniWindow("BossTracker") +bossWindow:hide() + +--f +local toggle = function() + if mainWindow:isVisible() then + analyzerButton:setOn(false) + mainWindow:close() + else + analyzerButton:setOn(true) + mainWindow:open() + end +end + +local drawGraph = function(graph, value) + graph:addValue(value) +end + +local toggleAnalyzer = function(window) + if window:isVisible() then + window:hide() + else + window:show() + end +end + +local function getSumStats() + local totalWaste = 0 + local totalLoot = 0 + + for k,v in pairs(membersData) do + totalWaste = totalWaste + v.waste + totalLoot = totalLoot + v.loot + end + + local totalBalance = totalLoot - totalWaste + + return totalWaste, totalLoot, totalBalance +end + +local function clipboardData() + local totalWaste, totalLoot, totalBalance = getSumStats() + local final = "" + + + local first = "Session data: From " .. HuntingSessionStart .." to ".. os.date('%Y-%m-%d, %H:%M:%S') + local second = "Session: " .. sessionTime() + local third = "Loot Type: Market" + local fourth = "Loot " .. format_thousand(totalLoot, true) + local fifth = "Supplies " .. format_thousand(totalWaste, true) + local six = "Balance " .. format_thousand(totalBalance, true) + + local t = {first, second, third, fourth, fifth, six} + for i, string in ipairs(t) do + final = final.. "\n"..string + end + + --user data now + for k,v in pairs(membersData) do + final = final.. "\n".. k + + final = final.. "\n\tLoot "..v.loot + final = final.. "\n\tSupplies "..v.waste + final = final.. "\n\tBalance "..v.balance + final = final.. "\n\tDamage "..v.damage + final = final.. "\n\tHealing "..v.heal + end + + g_window.setClipboardText(final) +end + +-- create analyzers button +analyzerButton = modules.game_buttons.buttonsWindow.contentsPanel and modules.game_buttons.buttonsWindow.contentsPanel.buttons.botAnalyzersButton +analyzerButton = analyzerButton or modules.client_topmenu.getButton("botAnalyzersButton") +if analyzerButton then + analyzerButton:destroy() +end + +--button +analyzerButton = modules.client_topmenu.addRightGameToggleButton('botAnalyzersButton', 'vBot Analyzers', '/images/topbuttons/analyzers', toggle, false, 999999) +analyzerButton:setOn(false) + +--toggles window +mainWindow.contentsPanel.HuntingAnalyzer.onClick = function() + toggleAnalyzer(huntingWindow) +end +mainWindow.onClose = function() + analyzerButton:setOn(false) +end +mainWindow.contentsPanel.LootAnalyzer.onClick = function() + toggleAnalyzer(lootWindow) +end +mainWindow.contentsPanel.SupplyAnalyzer.onClick = function() + toggleAnalyzer(supplyWindow) +end +mainWindow.contentsPanel.ImpactAnalyzer.onClick = function() + toggleAnalyzer(impactWindow) +end +mainWindow.contentsPanel.XPAnalyzer.onClick = function() + toggleAnalyzer(xpWindow) +end +mainWindow.contentsPanel.PartyHunt.onClick = function() + toggleAnalyzer(partyHuntWindow) +end +mainWindow.contentsPanel.DropTracker.onClick = function() + toggleAnalyzer(dropTrackerWindow) +end +mainWindow.contentsPanel.Stats.onClick = function() + toggleAnalyzer(statsWindow) +end +mainWindow.contentsPanel.BossTracker.onClick = function() + toggleAnalyzer(bossWindow) +end + +-- boss tracker +bossWindow.contentsPanel.search.onTextChange = function(widget, newText) + newText = newText:lower() + for i, child in ipairs(bossWindow.contentsPanel:getChildren()) do + local text = child:getId():lower() + if child:getId() ~= "search" then + child:setVisible(text:find(newText)) + end + end +end + +-- on login +newTimeFormat = function(v) -- v in seconds + local hours = string.format("%02.f", math.floor(v/3600)) + local mins = string.format("%02.f", math.floor(v/60 - (hours*60))) + + local final = hours.. "h "..mins.."min" + return final +end + +function createBossPanel(bossName, dueTime) + local widget = bossWindow.contentsPanel[bossName] or UI.createWidget("BossCreaturePanel", bossWindow.contentsPanel) + local outfit = storage.analyzers.outfits[bossName] + + widget.time = dueTime + widget:setId(bossName) + if outfit then + widget.creature:setOutfit(outfit) + else + widget.creature:setTooltip("Outfit preview not available.\nTo get one you need to 'attack' ".. bossName.."\nOr you need to correct the boss name inside analyzers.lua file, const BOSSES") + end + widget.name:setText(bossName) + + local timeLeft = os.difftime(dueTime, os.time()) + if timeLeft > 0 then + widget.cooldown:setText(newTimeFormat(timeLeft)) + widget.cooldown:setColor('#f29257') + else + widget.cooldown:setText("No Cooldown") + widget.cooldown:setColor('#b8b8b8') + end +end + +for bossName, dueTime in pairs(storage.analyzers.trackedBoss) do + createBossPanel(bossName, dueTime) +end + +local bossRegex = [[You (?:can|may) challenge ([\w\W]*) again in ([\d]*)]] +onTalk(function(name, level, mode, text, channelId, pos) + if mode == 34 then + local re = regexMatch(text, bossRegex) + local name = re and re[1] and re[1][2] + local cd = re and re[1] and re[1][3] + + for i=1,#BOSSES do + local bad = BOSSES[i][1] + local good = BOSSES[i][2] + + if name == bad then + name = good + end + end + + if not cd then return end + + cd = tonumber(cd) * 60 * 60 -- cd in seconds + + storage.analyzers.trackedBoss[name] = os.time() + cd + createBossPanel(name, os.time() + cd) + end +end) + +-- save outfits +onAttackingCreatureChange(function(newCreature, oldCreature) + local name = newCreature and newCreature:getName() + local outfit = newCreature and newCreature:getOutfit() + + if name then + storage.analyzers.outfits[name] = storage.analyzers.outfits[name] or outfit + end +end) + +--stats window +local totalRounds = UI.DualLabel("Total Rounds:", "0", {}, statsWindow.contentsPanel).right +local avRoundTime = UI.DualLabel("Time by Round:", "00:00h", {}, statsWindow.contentsPanel).right +UI.Separator(statsWindow.contentsPanel) +local totalRefills = UI.DualLabel("Total Refills:", "0", {}, statsWindow.contentsPanel).right +local avRefillTime = UI.DualLabel("Time by Refill:", "00:00h", {}, statsWindow.contentsPanel).right +local lastRefill = UI.DualLabel("Time since Refill:", "00:00h", {maxWidth = 200}, statsWindow.contentsPanel).right +UI.Separator(statsWindow.contentsPanel) +local label = UI.DualLabel("Supplies by Round:", "", {maxWidth = 200}, statsWindow.contentsPanel).left +label:setColor('#EC9706') +local suppliesByRound = UI.createWidget("AnalyzerItemsPanel", statsWindow.contentsPanel) +UI.Separator(statsWindow.contentsPanel) +label = UI.DualLabel("Supplies by Refill:", "", {maxWidth = 200}, statsWindow.contentsPanel).left +label:setColor('#ED7117') +local suppliesByRefill = UI.createWidget("AnalyzerItemsPanel", statsWindow.contentsPanel) +UI.Separator(statsWindow.contentsPanel) + + +--huntig +local sessionTimeLabel = UI.DualLabel("Session:", "00:00h", {}, huntingWindow.contentsPanel).right +local xpGainLabel = UI.DualLabel("XP Gain:", "0", {}, huntingWindow.contentsPanel).right +local xpHourLabel = UI.DualLabel("XP/h:", "0", {}, huntingWindow.contentsPanel).right +local lootLabel = UI.DualLabel("Loot:", "0", {}, huntingWindow.contentsPanel).right +local suppliesLabel = UI.DualLabel("Supplies:", "0", {}, huntingWindow.contentsPanel).right +local balanceLabel = UI.DualLabel("Balance:", "0", {}, huntingWindow.contentsPanel).right +local damageLabel = UI.DualLabel("Damage:", "0", {}, huntingWindow.contentsPanel).right +local damageHourLabel = UI.DualLabel("Damage/h:", "0", {}, huntingWindow.contentsPanel).right +local healingLabel = UI.DualLabel("Healing:", "0", {}, huntingWindow.contentsPanel).right +local healingHourLabel = UI.DualLabel("Healing/h:", "0", {}, huntingWindow.contentsPanel).right +UI.DualLabel("Killed Monsters:", "", {maxWidth = 200}, huntingWindow.contentsPanel) +local killedList = UI.createWidget("AnalyzerListPanel", huntingWindow.contentsPanel) +UI.DualLabel("Looted items:", "", {maxWidth = 200}, huntingWindow.contentsPanel) +local lootList = UI.createWidget("AnalyzerListPanel", huntingWindow.contentsPanel) + + +--party +UI.Button("Copy to Clipboard", function() clipboardData() end, partyHuntWindow.contentsPanel) +UI.Button("Reset Sessions", function() + if BotServer._websocket then + BotServer.send("partyHunt", false) + end +end, partyHuntWindow.contentsPanel) + +local switch = addSwitch("sendData", "Send Analyzer Data", function(widget) + widget:setOn(not widget:isOn()) + storage.sendPartyAnalyzerData = widget:isOn() +end, partyHuntWindow.contentsPanel) +switch:setOn(storage.sendPartyAnalyzerData) +UI.Separator(partyHuntWindow.contentsPanel) +local partySessionTimeLabel = UI.DualLabel("Session:", "00:00h", {}, partyHuntWindow.contentsPanel).right +local partyLootLabel = UI.DualLabel("Loot:", "0", {}, partyHuntWindow.contentsPanel).right +local partySuppliesLabel = UI.DualLabel("Supplies:", "0", {}, partyHuntWindow.contentsPanel).right +local partyBalanceLabel = UI.DualLabel("Balance:", "0", {}, partyHuntWindow.contentsPanel).right +UI.Separator(partyHuntWindow.contentsPanel) + +local function maintainDropTable() + local panel = dropTrackerWindow.contentsPanel + + for k,v in pairs(trackedLoot) do + local widget = panel[k] + if not widget then + trackedLoot[k] = nil + end + end +end + +local function createTrackedItems() + local panel = dropTrackerWindow.contentsPanel + + for i, child in ipairs(panel:getChildren()) do + if i > 2 then + child:destroy() + end + end + + for k,v in pairs(trackedLoot) do + local dropLoot = UI.createWidget("TrackerItem", dropTrackerWindow.contentsPanel) + local item = dropLoot.item + local name = dropLoot.name + local drops = dropLoot.drops + local id = tonumber(k) + local itemName = id == 3031 and "gold coin" or id == 3035 and "platinum coin" or id == 3043 and "crystal coin" or Item.create(id):getMarketData().name + + dropLoot:setId(id) + item:setItemId(id) + if item:getItemCount() > 1 then + item:setItemCount(1) + end + name:setText(itemName) + drops:setText("Loot Drops: "..v) + + dropLoot.onDoubleClick = function() + local id = dropLoot.item:getItemId() + trackedLoot[tostring(id)] = 0 + drops:setText("Loot Drops: 0") + end + + for i, child in pairs(dropLoot:getChildren()) do + child:setTooltip("Double click to reset or clear item to remove.") + end + + item.onItemChange = function(widget) + local id = widget:getItemId() + if id == 0 then + trackedLoot[widget:getParent():getId()] = nil + if tonumber(widget:getParent():getId()) then + widget:getParent():destroy() + return + end + widget:setImageSource('/images/ui/item') + widget:getParent():setId("blank") + name:setText("Set Item to start track.") + drops:setText("Loot Drops: 0") + return + end + + -- only amount have changed, ignore + if tonumber(widget:getParent():getId()) == id then return end + local itemName = id == 3031 and "gold coin" or id == 3035 and "platinum coin" or id == 3043 and "crystal coin" or Item.create(id):getMarketData().name + + if trackedLoot[tostring(id)] then + warn("vBot[Drop Tracker]: Item already added!") + name:setText("Set Item to start track.") + widget:setItemId(0) + return + end + + widget:setImageSource('') + drops:setText("Loot Drops: 0") + name:setText(itemName) + trackedLoot[tostring(id)] = trackedLoot[tostring(id)] or 0 + widget:getParent():setId(id) + maintainDropTable() + end + end +end + +--drop tracker +UI.Button("Add item to track drops", function() + local dropLoot = UI.createWidget("TrackerItem", dropTrackerWindow.contentsPanel) + local item = dropLoot.item + local name = dropLoot.name + local drops = dropLoot.drops + + item:setImageSource('/images/ui/item') + + dropLoot.onDoubleClick = function() + local id = dropLoot.item:getItemId() + trackedLoot[tostring(id)] = 0 + drops:setText("Loot Drops: 0") + end + + for i, child in pairs(dropLoot:getChildren()) do + child:setTooltip("Double click to reset or clear item to remove.") + end + + item.onItemChange = function(widget) + local id = widget:getItemId() + + if id == 0 then + trackedLoot[widget:getParent():getId()] = nil + if tonumber(widget:getParent():getId()) then + widget:getParent():destroy() + return + end + widget:setImageSource('/images/ui/item') + widget:getParent():setId("blank") + name:setText("Set Item to start track.") + drops:setText("Loot Drops: 0") + return + end + + -- only amount have changed, ignore + if tonumber(widget:getParent():getId()) == id then return end + local itemName = id == 3031 and "gold coin" or id == 3035 and "platinum coin" or id == 3043 and "crystal coin" or Item.create(id):getMarketData().name + + if trackedLoot[tostring(id)] then + warn("vBot[Drop Tracker]: Item already added!") + name:setText("Set Item to start track.") + widget:setItemId(0) + return + end + + widget:setImageSource('') + drops:setText("Loot Drops: 0") + name:setText(itemName) + trackedLoot[tostring(id)] = trackedLoot[tostring(id)] or 0 + widget:getParent():setId(id) + maintainDropTable() + end +end, dropTrackerWindow.contentsPanel) + +UI.Separator(dropTrackerWindow.contentsPanel) +createTrackedItems() + + +--loot +local lootInLootAnalyzerLabel = UI.DualLabel("Gold Value:", "0", {}, lootWindow.contentsPanel).right +local lootHourInLootAnalyzerLabel = UI.DualLabel("Per Hour:", "0", {}, lootWindow.contentsPanel).right +UI.Separator(lootWindow.contentsPanel) +--//items panel +local lootItems = UI.createWidget("AnalyzerItemsPanel", lootWindow.contentsPanel) +UI.Separator(lootWindow.contentsPanel) +--//graph +local lootGraph = UI.createWidget("AnalyzerGraph", lootWindow.contentsPanel) + lootGraph:setTitle("Loot/h") + drawGraph(lootGraph, 0) + + + + +--supplies +local suppliesInSuppliesAnalyzerLabel = UI.DualLabel("Gold Value:", "0", {}, supplyWindow.contentsPanel).right +local suppliesHourInSuppliesAnalyzerLabel = UI.DualLabel("Per Hour:", "0", {}, supplyWindow.contentsPanel).right +UI.Separator(supplyWindow.contentsPanel) +--//items panel +local supplyItems = UI.createWidget("AnalyzerItemsPanel", supplyWindow.contentsPanel) +UI.Separator(supplyWindow.contentsPanel) +--//graph +local supplyGraph = UI.createWidget("AnalyzerGraph", supplyWindow.contentsPanel) + supplyGraph:setTitle("Waste/h") + drawGraph(supplyGraph, 0) + + + + +-- impact + +--- damage +local title = UI.DualLabel("Damage", "", {}, impactWindow.contentsPanel).left +title:setColor('#E3242B') +local totalDamageLabel = UI.DualLabel("Total:", "0", {}, impactWindow.contentsPanel).right +local maxDpsLabel = UI.DualLabel("Max-DPS:", "0", {}, impactWindow.contentsPanel).right +local bestHitLabel = UI.DualLabel("All-Time High:", "0", {}, impactWindow.contentsPanel).right +UI.Separator(impactWindow.contentsPanel) +local dmgGraph = UI.createWidget("AnalyzerGraph", impactWindow.contentsPanel) + dmgGraph:setTitle("DPS") + drawGraph(dmgGraph, 0) + + +--- distribution +UI.Separator(impactWindow.contentsPanel) +local title2 = UI.DualLabel("Damage Distribution", "", {maxWidth = 150}, impactWindow.contentsPanel).left +title2:setColor('#FABD02') +local top1 = UI.DualLabel("-", "0", {maxWidth = 200}, impactWindow.contentsPanel) +local top2 = UI.DualLabel("-", "0", {maxWidth = 200}, impactWindow.contentsPanel) +local top3 = UI.DualLabel("-", "0", {maxWidth = 200}, impactWindow.contentsPanel) +local top4 = UI.DualLabel("-", "0", {maxWidth = 200}, impactWindow.contentsPanel) +local top5 = UI.DualLabel("-", "0", {maxWidth = 200}, impactWindow.contentsPanel) + +top1.left:setWidth(135) +top2.left:setWidth(135) +top3.left:setWidth(135) +top4.left:setWidth(135) +top5.left:setWidth(135) + + +--- healing +UI.Separator(impactWindow.contentsPanel) +local title3 = UI.DualLabel("Healing", "", {}, impactWindow.contentsPanel).left +title3:setColor('#03C04A') +local totalHealingLabel = UI.DualLabel("Total:", "0", {}, impactWindow.contentsPanel).right +local maxHpsLabel = UI.DualLabel("Max-HPS:", "0", {}, impactWindow.contentsPanel).right +local bestHealLabel = UI.DualLabel("All-Time High:", "0", {}, impactWindow.contentsPanel).right +UI.Separator(impactWindow.contentsPanel) +--//graph +local healGraph = UI.createWidget("AnalyzerGraph", impactWindow.contentsPanel) + healGraph:setTitle("HPS") + drawGraph(healGraph, 0) + + + + + + + +--xp +local xpGrainInXpLabel = UI.DualLabel("XP Gain:", "0", {}, xpWindow.contentsPanel).right +local xpHourInXpLabel = UI.DualLabel("XP/h:", "0", {}, xpWindow.contentsPanel).right +local nextLevelLabel = UI.DualLabel("Next Level:", "-", {}, xpWindow.contentsPanel).right +local progressBar = UI.createWidget("AnalyzerProgressBar", xpWindow.contentsPanel) +progressBar:setPercent(modules.game_skills.skillsWindow.contentsPanel.level.percent:getPercent()) +UI.Separator(xpWindow.contentsPanel) +--//graph +local xpGraph = UI.createWidget("AnalyzerGraph", xpWindow.contentsPanel) + xpGraph:setTitle("XP/h") + drawGraph(xpGraph, 0) + + + + + +--############################################# +--############################################# UI DONE +--############################################# +--############################################# +--############################################# +--############################################# + +setDefaultTab("Main") +-- first, the variables + +local console = modules.game_console +local regex = [[ ([^,|^.]+)]] +local noData = {} +local data = {} + +local function getColor(v) + if v >= 10000000 then -- 10kk, red + return "#FF0000" + elseif v >= 5000000 then -- 5kk, orange + return "#FFA500" + elseif v >= 1000000 then -- 1kk, yellow + return "#FFFF00" + elseif v >= 100000 then -- 100k, purple + return "#F25AED" + elseif v >= 10000 then -- 10k, blue + return "#5F8DF7" + elseif v >= 1000 then -- 1k, green + return "#00FF00" + elseif v >= 50 then + return "#FFFFFF" -- 50gp, white + else + return "#aaaaaa" -- less than 100gp, grey + end +end + +local function formatStr(str) + if string.starts(str, "a ") then + str = str:sub(2, #str) + elseif string.starts(str, "an ") then + str = str:sub(3, #str) + end + + local n = getFirstNumberInText(str) + if n then + str = string.split(str, tostring(n))[1] + str = str:sub(1,#str-1) + end + + return str:trim() +end + +local function getPrice(name) + name = formatStr(name) + name = name:lower() + -- first check custom prices + if storage.analyzers.customPrices[name] then + return storage.analyzers.customPrices[name] + end + + -- if already checked and no data skip looping items.lua + if noData[name] then + return 0 + end + + -- maybe was already checked, if so, skip looping items.lua + if data[name] then + return data[name] + end + + -- searching in items.lua - big table, if possible skip + for k,v in pairs(LootItems) do + if name == k then + data[name] = v + return v + end + end + + -- if no data, save it and return 0 + noData[name] = true + return 0 +end + +local expGained = function() + return exp() - startExp +end + +function format_thousand(v, comma) + comma = comma and "," or "." + if not v then return 0 end + local s = string.format("%d", math.floor(v)) + local pos = string.len(s) % 3 + if pos == 0 then pos = 3 end + return string.sub(s, 1, pos) + .. string.gsub(string.sub(s, pos+1), "(...)", comma.."%1") +end + +local expLeft = function() + local level = lvl()+1 + return math.floor((50*level*level*level)/3 - 100*level*level + (850*level)/3 - 200) - exp() +end + +niceTimeFormat = function(v, seconds) -- v in seconds + local hours = string.format("%02.f", math.floor(v/3600)) + local mins = string.format("%02.f", math.floor(v/60 - (hours*60))) + local secs = string.format("%02.f", math.floor(math.fmod(v, 60))) + + local final = string.format('%s:%s%s',hours,mins,seconds and ":"..secs or "") + return final +end +local uptime +sessionTime = function() + uptime = math.floor((now - launchTime)/1000) + return niceTimeFormat(uptime) +end +sessionTime() + +local expPerHour = function(calculation) + local r = 0 + if #expTable > 0 then + r = exp() - expTable[1] + else + return "-" + end + + if uptime < 15*60 then + r = math.ceil((r/uptime)*60*60) + else + r = math.ceil(r*8) + end + if calculation then + return r + else + return format_thousand(r) + end +end + +local function add(t, text, color, last) + table.insert(t, text) + table.insert(t, color) + if not last then + table.insert(t, ", ") + table.insert(t, "#FFFFFF") + end +end + +-- Bot Server +local function sendData() + if BotServer._websocket then + local totalDmg, totalHeal, lootWorth, wasteWorth, balance = getHuntingData() + local outfit = player:getOutfit() + outfit.mount = 0 + local t = { + totalDmg, + totalHeal, + balance, + hppercent(), + manapercent(), + outfit, + player:isPartyLeader(), + lootWorth, + wasteWorth, + modules.game_skills.skillsWindow.contentsPanel.stamina.value:getText(), + format_thousand(expGained()), + expPerHour(), + balanceDesc .. " (" .. hourDesc .. ")", + sessionTime() + } + + -- validation + if lastDataSend.totalDmg ~= t[1] and lastDataSend.totalHeal ~= t[2] then + BotServer.send("partyHunt", t) + lastDataSend[1] = t[1] + lastDataSend[2] = t[2] + end + end +end + +-- process data +if BotServer._websocket then + BotServer.listen("partyHunt", function(name, message) + if message == true then + sendData() + elseif message == false then + resetAnalyzerSessionData() + else + membersData[name] = { + damage = message[1], + heal = message[2], + balance = message[3], + hp = message[4], + mana = message[5], + outfit = message[6], + leader = message[7], + loot = message[8], + waste = message[9], + stamina = message[10], + expGained = message[11], + expH = message[12], + balanceH = message[13], + session = message[14] + } + + local widgetName = "Widget"..name + local widget = partyHuntWindow.contentsPanel[widgetName] or UI.createWidget("MemberWidget", partyHuntWindow.contentsPanel) + widget:setId(widgetName) + widget.lastUpdate = now + + + local t = membersData[name] + widget.name:setText(name) + widget.name:setColor("white") + if t.leader then + widget.name:setColor('#f8db38') + end + schedule(10*1000, function() + if widget and widget.lastUpdate and now - widget.lastUpdate > 10000 then + widget.name:setText(widget.name:getText().. " [inactive]") + widget.name:setColor("#aeaeae") + widget.health:setBackgroundColor("#aeaeae") + widget.mana:setBackgroundColor("#aeaeae") + widget.balance.value:setText("-") + widget.damage.value:setText("-") + widget.healing.value:setText("-") + widget.creature:disable() + end + end) + widget.creature:setOutfit(t.outfit) + widget.health:setPercent(t.hp) + widget.health:setBackgroundColor("#00c000") + widget.mana:setPercent(t.mana) + widget.mana:setBackgroundColor("#0000FF") + widget.balance.value:setText(format_thousand(t.balance)) + if t.balance < 0 then + widget.balance.value:setColor('#ff9854') + elseif t.balance > 0 then + widget.balance.value:setColor('#45ad25') + else + widget.balance.value:setColor('white') + end + widget.damage.value:setText(format_thousand(t.damage)) + widget.healing.value:setText(format_thousand(t.heal)) + + widget.onDoubleClick = function() + membersData[name] = nil + widget:destroy() + end + + --tooltip + local tooltip = "Session: "..t.session.."\n".. + "Stamina: "..t.stamina.."\n".. + "Exp Gained: "..t.expGained.."\n".. + "Exp per Hour: "..t.expH.."\n".. + "Balance: "..t.balanceH + + widget.creature:setTooltip(tooltip) + end + end) +end + + +function hightlightText(widget, color, duration) + for i=0,duration do + schedule(i * 250, function() + if i == duration or (i > 0 and i % 2 == 0) then + widget:setColor("#FFFFFF") + else + widget:setColor(color) + end + end) + end +end + +local nameRegex = [[Loot of (?:an |a |the |)([^:]+)]] +onTextMessage(function(mode, text) + if not storage.analyzers.lootChannel then return end + if not text:find("Loot of") and not text:find("The following items are available in your reward chest") then return end + local name + + -- adding monster to killed list + if text:find("Loot of") then + name = regexMatch(text, nameRegex)[1][2] + if not killList[name] then + killList[name] = 1 + else + killList[name] = killList[name] + 1 + end + refreshKills() + end + -- variables + local split = string.split(text, ":") + local re = regexMatch(split[2], regex) + local combinedWorth = 0 + local formatted + local div + local t = {} + local messageT = {} + + -- add timestamp, creature part and color it as white + add(t, os.date('%H:%M') .. ' ' .. split[1]..": ", "#FFFFFF", true) + add(messageT, split[1]..": ", "#FFFFFF", true) + + -- main part + if re ~= 0 then + for i=1,#re do + local data = re[i][2] -- each looted item + local formattedLoot = regexMatch(data, [[(^[^(]+)]])[1][1] + formattedLoot = formattedLoot:trim() + local amount = getFirstNumberInText(formattedLoot) -- amount found in data + local price = amount and getPrice(formattedLoot) * amount or getPrice(formattedLoot) -- if amount then multity price, else just take price + local color = getColor(price) -- generate hex string based off price + local messageColor = getColor(getPrice(formattedLoot)) + + combinedWorth = combinedWorth + price -- add all prices to calculate total worth + + add(t, data, color, i==#re) + add(messageT, data, color, i==#re) + + --drop tracker + for i, child in ipairs(dropTrackerWindow.contentsPanel:getChildren()) do + local childName = child.name + childName = childName and childName:getText() + + + if childName and formattedLoot:find(childName) then + trackedLoot[tostring(child.item:getItemId())] = trackedLoot[tostring(child.item:getItemId())] + (amount or 1) + child.drops:setText("Loot Drops: "..trackedLoot[tostring(child.item:getItemId())]) + + hightlightText(child.name,"#f0b400", 8) + modules.game_textmessage.messagesPanel.statusLabel:setVisible(true) + modules.game_textmessage.messagesPanel.statusLabel:setColoredText({ + "Valuable loot: ", "#f0b400", + childName.."", messageColor, + " dropped by "..name.."!", "#f0b400" + }) + schedule(3000, function() + modules.game_textmessage.messagesPanel.statusLabel:setVisible(false) + end) + end + end + end + end + + -- format total worth so it wont look obnoxious + if combinedWorth >= 1000000 then + div = combinedWorth/1000000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "kk" + elseif combinedWorth >= 1000 then + div = combinedWorth/1000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "k" + else + formatted = combinedWorth .. "gp" + end + + if modules.game_textmessage.messagesPanel.centerTextMessagePanel.highCenterLabel:getText() == text then + modules.game_textmessage.messagesPanel.centerTextMessagePanel.highCenterLabel:setColoredText(messageT) + schedule(math.max(#text * 50, 2000), function() + modules.game_textmessage.messagesPanel.centerTextMessagePanel.highCenterLabel:setVisible(false) + end) + end + + -- add total worth to string + add(t, " - (", "#FFFFFF", true) + add(t, formatted, getColor(combinedWorth), true) + add(t, ")", "#FFFFFF", true) + + -- get/create tab and write raw message + local tabName = "vBot Loot" + local tab = console.getTab(tabName) or console.addTab(tabName, true) + console.addText(text, console.SpeakTypesSettings, tabName, "") + + -- find last message in given tab and rewrite it with formatted string + local panel = console.consoleTabBar:getTabPanel(tab) + local consoleBuffer = panel:getChildById('consoleBuffer') + local message = consoleBuffer:getLastChild() + message:setColoredText(t) +end) + +local function niceFormat(v) + local div + local formatted + if v >= 10000000 then + div = v/10000000 + formatted = math.ceil(div) .. "M" + elseif v >= 1000000 then + div = v/1000000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "M" + elseif v >= 10000 then + div = v/1000 + formatted = math.floor(div) .. "k" + elseif v >= 1000 then + div = v/1000 + formatted = math.floor(div) .. "." .. math.floor(div * 10) % 10 .. "k" + else + formatted = v + end + return formatted +end + +resetAnalyzerSessionData = function() + vBot.CaveBotData = vBot.CaveBotData or { + refills = 0, + rounds = 0, + time = {}, + lastRefill = os.time(), + refillTime = {} + } + launchTime = now + startExp = exp() + dmgTable = {} + healTable = {} + expTable = {} + totalDmg = 0 + totalHeal = 0 + dmgDistribution = {} + first = {l="-", r="0"} + second = {l="-", r="0"} + third = {l="-", r="0"} + fourth = {l="-", r="0"} + five = {l="-", r="0"} + lootedItems = {} + useData = {} + usedItems ={} + refreshLoot() + refreshWaste() + xpGraph:clear() + drawGraph(xpGraph, 0) + lootGraph:clear() + drawGraph(lootGraph, 0) + supplyGraph:clear() + drawGraph(supplyGraph, 0) + dmgGraph:clear() + drawGraph(dmgGraph, 0) + healGraph:clear() + drawGraph(healGraph, 0) + killList = {} + refreshKills() + HuntingSessionStart = os.date('%Y-%m-%d, %H:%M:%S') +end + +mainWindow.contentsPanel.ResetSession.onClick = function() + resetAnalyzerSessionData() +end + +mainWindow.contentsPanel.Settings.onClick = function() + settingsWindow:show() + settingsWindow:raise() + settingsWindow:focus() +end + + +-- extras window +settingsWindow.closeButton.onClick = function() + settingsWindow:hide() +end + +local function getFrame(v) + if v >= 1000000 then + return '/images/ui/rarity_gold' + elseif v >= 100000 then + return '/images/ui/rarity_purple' + elseif v >= 10000 then + return '/images/ui/rarity_blue' + elseif v >= 1000 then + return '/images/ui/rarity_green' + else + return '/images/ui/item' + end +end + + +displayCondition = function(menuPosition, lookThing, useThing, creatureThing) + if lookThing and not lookThing:isCreature() and not lookThing:isNotMoveable() and lookThing:isPickupable() then + return true + end +end +local interface = modules.game_interface + +local function setFrames() + if not storage.analyzers.rarityFrames then return end + for _, container in pairs(getContainers()) do + local window = container.itemsPanel + for i, child in pairs(window:getChildren()) do + local id = child:getItemId() + local price = 0 + + if id ~= 0 then -- there's item + local item = Item.create(id) + local name = item:getMarketData().name:lower() + price = getPrice(name) + + -- set rarity frame + child:setImageSource(getFrame(price)) + else -- empty widget + -- revert any possible changes + child:setImageSource("/images/ui/item") + end + child.onHoverChange = function(widget, hovered) + if id == 0 or not hovered then + return interface.removeMenuHook('analyzer') + end + interface.addMenuHook('analyzer', 'Price:', function() end, displayCondition, price) + end + end + end +end +setFrames() + +onContainerOpen(function(container, previousContainer) + setFrames() +end) + +onAddItem(function(container, slot, item, oldItem) + setFrames() +end) + +onRemoveItem(function(container, slot, item) + setFrames() +end) + +onContainerUpdateItem(function(container, slot, item, oldItem) + setFrames() +end) + +function smallNumbers(n) + if n >= 10 ^ 6 then + return string.format("%.1fkk", n / 10 ^ 6) + elseif n >= 10 ^ 3 then + return string.format("%.1fk", n / 10 ^ 3) + else + return tostring(n) + end +end + +function refreshList() + local list = settingsWindow.CustomPrices + list:destroyChildren() + + for name, price in pairs(storage.analyzers.customPrices) do + local label = UI.createWidget("AnalyzerPriceLabel", list) + label.remove.onClick = function() + storage.analyzers.customPrices[name] = nil + label:destroy() + schedule(5, function() + setFrames() + end) + end + label:setText("["..name.."] = "..smallNumbers(price).." gp") + end +end +refreshList() + +settingsWindow.addItem.onClick = function() + local newPrices = storage.analyzers.customPrices + local id = settingsWindow.ID:getItemId() + local newPrice = tonumber(settingsWindow.NewPrice:getText()) + + if id < 100 then + return warn("No item added!") + end + + local name = Item.create(id):getMarketData().name + + if newPrices[name] then + return warn("Item already added! Remove it from the list to set a new price!") + end + + newPrices[name] = newPrice + settingsWindow.ID:setItemId(0) + settingsWindow.NewPrice:setText(0) + schedule(5, function() + setFrames() + end) + refreshList() +end + +settingsWindow.LootChannel:setOn(storage.analyzers.lootChannel) +settingsWindow.LootChannel.onClick = function(widget) + storage.analyzers.lootChannel = not storage.analyzers.lootChannel + widget:setOn(storage.analyzers.lootChannel) +end + +settingsWindow.RarityFrames:setOn(storage.analyzers.rarityFrames) +settingsWindow.RarityFrames.onClick = function(widget) + storage.analyzers.rarityFrames = not storage.analyzers.rarityFrames + widget:setOn(storage.analyzers.rarityFrames) + setFrames() +end + +local timeToLevel = function() + local t = 0 + if expPerHour(true) == 0 or expPerHour() == "-" then + return "-" + else + t = expLeft()/expPerHour(true) + return niceTimeFormat(math.ceil(t*60*60)) + end +end + +local sumT = function(t) + local s = 0 + for i,v in pairs(t) do + s = s + v.d + end + return s +end + +local valueInSeconds = function(t) + local d = 0 + local time = 0 + if #t > 0 then + for i, v in ipairs(t) do + if now - v.t <= 3000 then + if time == 0 then + time = v.t + end + d = d + v.d + else + table.remove(t, 1) + end + end + end + return math.ceil(d/((now-time)/1000)) +end + +local regex = "You lose ([0-9]*) hitpoints due to an attack by ([a-z]*) ([a-z A-z-]*)" +onTextMessage(function(mode, text) + local value = getFirstNumberInText(text) + if mode == 21 then -- damage dealt + totalDmg = totalDmg + value + table.insert(dmgTable, {d = value, t = now}) + if value > storage.bestHit then + storage.bestHit = value + end + end + if mode == 23 then -- healing + totalHeal = totalHeal + value + table.insert(healTable, {d = value, t = now}) + if value > storage.bestHeal then + storage.bestHeal = value + end + end + + -- damage distribution part + if text:find("You lose") then + local data = regexMatch(text, regex)[1] + if data then + local monster = data[4] + local val = data[2] + table.insert(dmgDistribution, {v=val,m=monster,t=now}) + end + end +end) + +function capitalFistLetter(str) + return (string.gsub(str, "^%l", string.upper)) +end + +-- tables maintance +macro(500, function() + local dmgFinal = {} + local labelTable = {} + local dmgSum = 0 + table.insert(expTable, exp()) + if #expTable > 15*60 then + for i,v in pairs(expTable) do + if i == 1 then + table.remove(expTable, i) + end + end + end + + for i,v in pairs(dmgDistribution) do + if now - v.t > 60*1000*10 then + table.remove(dmgDistribution, i) + else + dmgSum = dmgSum + v.v + if not dmgFinal[v.m] then + dmgFinal[v.m] = v.v + else + dmgFinal[v.m] = dmgFinal[v.m] + v.v + end + end + end + + first = dmgFinal[1] or {l="-", r="0"} + second = dmgFinal[2] or {l="-", r="0"} + third = dmgFinal[3] or {l="-", r="0"} + fourth = dmgFinal[4] or {l="-", r="0"} + five = dmgFinal[5] or {l="-", r="0"} + + for k,v in pairs(dmgFinal) do + table.insert(labelTable, {m=k, d=tonumber(v)}) + end + + table.sort(labelTable, function(a,b) return a.d > b.d end) + + for i,v in pairs(labelTable) do + local val = math.floor((v.d/dmgSum)*100) .. "%" + local words = string.split(v.m, " ") + local name = "" + for i, word in ipairs(words) do + name = name .. " " .. capitalFistLetter(word) + end + name = name:len() < 20 and name or name:sub(1,17).."..." + name = name:trim()..": " + if i == 1 then + first = {l=name, r=val} + elseif i == 2 then + second = {l=name, r=val} + elseif i == 3 then + third = {l=name, r=val} + elseif i == 4 then + fourth = {l=name, r=val} + elseif i == 5 then + five = {l=name, r=val} + else + break + end + end +end) + +function getPanelHeight(panel) + + local elements = panel.List:getChildCount() + if elements == 0 then + return 0 + else + local rows = math.ceil(elements/5) + local height = rows * 35 + return height + end +end + +function refreshLoot() + + lootItems:destroyChildren() + lootList:destroyChildren() + + for k,v in pairs(lootedItems) do + local label1 = UI.createWidget("AnalyzerLootItem", lootItems) + local price = v.count and getPrice(v.name) * v.count or getPrice(v.name) + + label1:setItemId(k) + label1:setItemCount(50) + label1:setShowCount(false) + label1.count:setText(niceFormat(v.count)) + label1.count:setColor(getColor(price)) + local tooltipName = v.count > 1 and v.name.."s" or v.name + label1:setTooltip(v.count .. "x " .. tooltipName .. " (Value: "..format_thousand(getPrice(v.name)).."gp, Sum: "..format_thousand(price).."gp)") + --hunting window loot list + local label2 = UI.createWidget("ListLabel", lootList) + label2:setText(v.count .. "x " .. v.name) + end + + if lootItems:getChildCount() == 0 then + local label = UI.createWidget("ListLabel", lootList) + label:setText("None") + end +end +refreshLoot() + +function refreshKills() + killedList:destroyChildren() + local kills = 0 + for k,v in pairs(killList) do + kills = kills + 1 + local label = UI.createWidget("ListLabel", killedList) + label:setText(v .. "x " .. k) + end + + if kills == 0 then + local label = UI.createWidget("ListLabel", killedList) + label:setText("None") + end +end +refreshKills() + +function refreshWaste() + + supplyItems:destroyChildren() + suppliesByRefill:destroyChildren() + suppliesByRound:destroyChildren() + + local parents = {supplyItems, suppliesByRound, suppliesByRefill} + + for k,v in pairs(usedItems) do + for i=1,#parents do + local amount = i == 1 and v.count or + i == 2 and v.count/(vBot.CaveBotData.rounds + 1) or + i == 3 and v.count/(vBot.CaveBotData.refills + 1) + amount = math.floor(amount) + local label1 = UI.createWidget("AnalyzerLootItem", parents[i]) + local price = amount and getPrice(v.name) * amount or getPrice(v.name) + + label1:setItemId(k) + label1:setItemCount(50) + label1:setShowCount(false) + label1.count:setText(niceFormat(amount)) + label1.count:setColor(getColor(price)) + local tooltipName = amount > 1 and v.name.."s" or v.name + label1:setTooltip(amount .. "x " .. tooltipName .. " (Value: "..format_thousand(getPrice(v.name)).."gp, Sum: "..format_thousand(price).."gp)") + end + end +end + +-- loot analyzer +-- adding +local containers = CaveBot.GetLootContainers() +local lastCap = freecap() +onAddItem(function(container, slot, item, oldItem) + if not table.find(containers, container:getContainerItem():getId()) then return end + if isInPz() then return end + if slot > 0 then return end + if freecap() >= lastCap then return end + local name = item:getId() + local tmpname = item:getId() == 3031 and "gold coin" or item:getId() == 3035 and "platinum coin" or item:getId() == 3043 and "crystal coin" or item:getMarketData().name + if not lootedItems[name] then + lootedItems[name] = { count = item:getCount(), name = tmpname } + else + lootedItems[name].count = lootedItems[name].count + item:getCount() + end + lastCap = freecap() + refreshLoot() + + -- drop tracker +end) + +onContainerUpdateItem(function(container, slot, item, oldItem) + if not table.find(containers, container:getContainerItem():getId()) then return end + if not oldItem then return end + if isInPz() then return end + if freecap() == lastCap then return end + + local tmpname = item:getId() == 3031 and "gold coin" or item:getId() == 3035 and "platinum coin" or item:getId() == 3043 and "crystal coin" or item:getMarketData().name + local amount = item:getCount() - oldItem:getCount() + if amount < 0 then + return + end + local name = item:getId() + if not lootedItems[name] then + lootedItems[name] = { count = amount, name = tmpname } + else + lootedItems[name].count = lootedItems[name].count + amount + end + lastCap = freecap() + refreshLoot() +end) + +-- ammo +local ammo = {16143, 763, 761, 7365, 3448, 762, 21470, 7364, 14251, 3447, 3449, 15793, 25757, 774, 35901, 6528, 7363, 3450, 16141, 25758, 14252, 3446, 16142, 35902} +onContainerUpdateItem(function(container, slot, item, oldItem) + local id = item:getId() + if not table.find(ammo, id) then return end + local newCount = item:getCount() + local oldCount = oldItem:getCount() + local name = item:getMarketData().name + + if oldCount - newCount == 1 then + if not usedItems[id] then + usedItems[id] = { count = 1, name = name} + else + usedItems[id].count = usedItems[id].count + 1 + end + refreshWaste() + end +end) + +-- waste +local regex3 = [[\d ([a-z A-Z]*)s...]] +local lackOfData = {} +onTextMessage(function(mode, text) + text = text:lower() + if not text:find("using one of") then return end + + local amount = getFirstNumberInText(text) + local re = regexMatch(text, regex3) + local name = re[1][2] + local id = WasteItems[name] + + if not id then + + if not lackOfData[name] then + lackOfData[name] = true + print("[Analyzer] no data for item: "..name.. "inside items.lua -> WasteItems") + end + + return + end + + if not useData[name] then + useData[name] = amount + else + if math.abs(useData[name]-amount) == 1 then + useData[name] = amount + if not usedItems[id] then + usedItems[id] = { count = 1, name = name} + else + usedItems[id].count = usedItems[id].count + 1 + end + else + useData[name] = amount + end + refreshWaste() + end +end) + +function hourVal(v) + v = v or 0 + return (v/uptime)*3600 +end + +function bottingStats() + lootWorth = 0 + wasteWorth = 0 + for k, v in pairs(lootedItems) do + if LootItems[v.name] then + lootWorth = lootWorth + (LootItems[v.name]*v.count) + end + end + for k, v in pairs(usedItems) do + if LootItems[v.name] then + wasteWorth = wasteWorth + (LootItems[v.name]*v.count) + end + end + balance = lootWorth - wasteWorth + + return lootWorth, wasteWorth, balance +end + +function bottingLabels(lootWorth, wasteWorth, balance) + balanceDesc = nil + hourDesc = nil + desc = nil + + if balance >= 1000000 or balance <= -1000000 then + desc = balance / 1000000 + balanceDesc = math.floor(desc) .. "." .. math.floor(desc * 10) % 10 .. "kk" + elseif balance >= 1000 or balance <= -1000 then + desc = balance / 1000 + balanceDesc = math.floor(desc) .. "." .. math.floor(desc * 10) % 10 .."k" + else + balanceDesc = balance .. "gp" + end + + hour = hourVal(balance) + if hour >= 1000000 or hour <= -1000000 then + desc = balance / 1000000 + hourDesc = math.floor(hourVal(desc)) .. "." .. math.floor(hourVal(desc) * 10) % 10 .. "kk/h" + elseif hour >= 1000 or hour <= -1000 then + desc = balance / 1000 + hourDesc = math.floor(hourVal(desc)) .. "." .. math.floor(hourVal(desc) * 10) % 10 .. "k/h" + else + hourDesc = math.floor(hourVal(balance)) .. "gp/h" + end + + return balanceDesc, hourDesc +end + +function reportStats() + local lootWorth, wasteWorth, balance = bottingStats() + local balanceDesc, hourDesc = bottingLabels(lootWorth, wasteWorth, balance) + + local a, b, c + + a = "Session Time: " .. sessionTime() .. ", Exp Gained: " .. format_thousand(expGained()) .. ", Exp/h: " .. expPerHour() + b = " | Balance: " .. balanceDesc .. " (" .. hourDesc .. ")" + c = a..b + + return c +end + +function damageHour() + if uptime < 5*60 then + return totalDmg + else + return hourVal(totalDmg) + end +end + +function healHour() + if uptime < 5*60 then + return totalHeal + else + return hourVal(totalHeal) + end +end + +function wasteHour() + local lootWorth, wasteWorth, balance = bottingStats() + if uptime < 5*60 then + return wasteWorth + else + return hourVal(wasteWorth) + end +end + + +function lootHour() + local lootWorth, wasteWorth, balance = bottingStats() + if uptime < 5*60 then + return lootWorth + else + return hourVal(lootWorth) + end +end + +function getHuntingData() + local lootWorth, wasteWorth, balance = bottingStats() + return totalDmg, totalHeal, lootWorth, wasteWorth, balance +end + +function avgTable(t) + if type(t) ~= 'table' then return 0 end + local val = 0 + + for i,v in pairs(t) do + val = val + v + end + + if #t == 0 then + return 0 + else + return val/#t + end +end + +--bestdps/hps +local bestDPS = 0 +local bestHPS = 0 +--main loop +macro(500, function() + local lootWorth, wasteWorth, balance = bottingStats() + local balanceDesc, hourDesc = bottingLabels(lootWorth, wasteWorth, balance) + + -- hps and dps + local curHPS = valueInSeconds(healTable) + local curDPS = valueInSeconds(dmgTable) + + bestHPS = bestHPS > curHPS and bestHPS or curHPS + bestDPS = bestDPS > curDPS and bestDPS or curDPS + + --hunt window + sessionTimeLabel:setText(sessionTime()) + xpGainLabel:setText(format_thousand(expGained())) + xpHourLabel:setText(expPerHour()) + lootLabel:setText(format_thousand(lootWorth)) + suppliesLabel:setText(format_thousand(wasteWorth)) + balanceLabel:setColor(balance >= 0 and "#45ad25" or "#ff9854") + balanceLabel:setText(balanceDesc .. " (" .. hourDesc .. ")") + damageLabel:setText(format_thousand(totalDmg)) + damageHourLabel:setText(format_thousand(damageHour())) + healingLabel:setText(format_thousand(totalHeal)) + healingHourLabel:setText(format_thousand(healHour())) + + --loot window + lootInLootAnalyzerLabel:setText(format_thousand(lootWorth)) + lootHourInLootAnalyzerLabel:setText(format_thousand(lootHour())) + + + --supply window + suppliesInSuppliesAnalyzerLabel:setText(format_thousand(wasteWorth)) + suppliesHourInSuppliesAnalyzerLabel:setText(format_thousand(wasteHour())) + + --impact window + totalDamageLabel:setText(format_thousand(totalDmg)) + maxDpsLabel:setText(format_thousand(bestDPS)) + bestHitLabel:setText(storage.bestHit) + + top1.left:setText(first.l) + top1.right:setText(first.r) + top2.left:setText(second.l) + top2.right:setText(second.r) + top3.left:setText(third.l) + top3.right:setText(third.r) + top4.left:setText(fourth.l) + top4.right:setText(fourth.r) + top5.left:setText(five.l) + top5.right:setText(five.r) + + totalHealingLabel:setText(format_thousand(totalHeal)) + maxHpsLabel:setText(format_thousand(bestHPS)) + bestHealLabel:setText(storage.bestHeal) + + --xp window + xpGrainInXpLabel:setText(format_thousand(expGained())) + xpHourInXpLabel:setText(expPerHour()) + nextLevelLabel:setText(timeToLevel()) + progressBar:setPercent(modules.game_skills.skillsWindow.contentsPanel.level.percent:getPercent()) + + + --stats + totalRounds:setText(vBot.CaveBotData.rounds) + avRoundTime:setText(niceTimeFormat(avgTable(vBot.CaveBotData.time),true)) + totalRefills:setText(vBot.CaveBotData.refills) + avRefillTime:setText(niceTimeFormat(avgTable(vBot.CaveBotData.refillTime),true)) + lastRefill:setText(niceTimeFormat(os.difftime(os.time()-vBot.CaveBotData.lastRefill),true)) + +end) + +--graphs, draw each minute +macro(60*1000, function() + + drawGraph(xpGraph, expPerHour(true) or 0) + drawGraph(lootGraph, lootHour() or 0) + drawGraph(supplyGraph, wasteHour() or 0) + drawGraph(dmgGraph, valueInSeconds(dmgTable) or 0) + drawGraph(healGraph, valueInSeconds(healTable) or 0) +end) + +--party hunt analyzer +macro(2000, function() + if not BotServer._websocket then return end + + -- send data + if storage.sendPartyAnalyzerData then + sendData() + end + + local totalWaste, totalLoot, totalBalance = getSumStats() + + partySessionTimeLabel:setText(sessionTime()) + partyLootLabel:setText(format_thousand(totalLoot)) + partySuppliesLabel:setText(format_thousand(totalWaste)) + partyBalanceLabel:setText(format_thousand(totalBalance)) + + if totalBalance < 0 then + partyBalanceLabel:setColor('#ff9854') + elseif totalBalance > 0 then + partyBalanceLabel:setColor('#45ad25') + else + partyBalanceLabel:setColor('white') + end + + for bossName, dueTime in pairs(storage.analyzers.trackedBoss) do + createBossPanel(bossName, dueTime) + end +end) + +-- public functions +-- global namespace +Analyzer = {} + +Analyzer.getKillsAmount = function(name) + return killList[name] or 0 +end + +Analyzer.getLootedAmount = function(nameOrId) + if type(nameOrId) == "number" then + return lootedItems[nameOrId].count or 0 + else + local nameOrId = nameOrId:lower() + for k,v in pairs(lootedItems) do + if v.name == nameOrId then + return v.count + end + end + end + return 0 +end + +Analyzer.getTotalProfit = function() + local lootWorth, wasteWorth, balance = bottingStats() + + return lootWorth +end + +Analyzer.getTotalWaste = function() + local lootWorth, wasteWorth, balance = bottingStats() + + return wasteWorth +end + +Analyzer.getBalance = function() + local lootWorth, wasteWorth, balance = bottingStats() + + return balance +end + +Analyzer.getXpGained = function() + return expGained() +end + +Analyzer.getXpHour = function() + return expPerHour() +end + +Analyzer.getTimeToNextLevel = function() + return timeToLevel() +end + +Analyzer.getCaveBotStats = function() + local parents = {suppliesByRound, suppliesByRefill} + local round = {} + local refill = {} + for i=1,2 do + local data = parents[i] + for j, child in ipairs(data:getChildren()) do + local id = child:getItemId() + local count = child.count + + if i == 1 then + round[id] = count + else + refill[id] = count + end + end + end + + return { + totalRounds = totalRounds:getText(), + avRoundTime = avRoundTime:getText(), + totalRefills = totalRefills:getText(), + avRefillTime = avRefillTime:getText(), + lastRefill = lastRefill:getText(), + roundSupplies = round, -- { [id] = amount, [id2] = amount ...} + refillSupplies = refill -- { [id] = amount, [id2] = amount ...} + } +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.otui new file mode 100644 index 0000000000..216d9583cb --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/analyzer.otui @@ -0,0 +1,514 @@ +BossCreaturePanel < Panel + height: 38 + + UICreature + id: creature + size: 35 35 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + old-scaling: true + margin-left: 3 + + Label + id: name + anchors.left: creature.right + margin: 1 + margin-left: 5 + margin-top: 4 + anchors.top: parent.top + anchors.bottom: creature.verticalCenter + anchors.right: parent.right + font: verdana-11px-rounded + color: #FFFFFF + text: Duke Krule + + Label + id: cooldown + anchors.left: creature.right + margin: 1 + margin-left: 5 + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.top: creature.verticalCenter + font: verdana-11px-rounded + text: 19h 20min + + +SearchPanel < TextEdit + placeholder: Type to search + margin-top: 1 + @onClick: modules.client_textedit.show(self) + + Button + id: clear + anchors.right: parent.right + margin-right: -2 + anchors.verticalCenter: parent.verticalCenter + size: 18 18 + text: X + @onClick: | + self:getParent():setText("") + +TrackerItem < Panel + height: 40 + + BotItem + id: item + anchors.top: parent.top + margin-top: 2 + anchors.left: parent.left + image-source: + + UIWidget + id: name + anchors.top: prev.top + margin-top: 1 + anchors.bottom: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + text: Set Item to start track. + text-align:left + font: verdana-11px-rounded + color: #FFFFFF + + UIWidget + id: drops + anchors.top: prev.bottom + margin-top: 3 + anchors.bottom: Item.bottom + anchors.left: prev.left + anchors.right: parent.right + font: verdana-11px-rounded + text-align:left + text: Loot Drops: 0 + color: #CCCCCC + + +DualLabel < Label + height: 15 + text-offset: 4 0 + font: verdana-11px-rounded + text-align: left + width: 50 + + Label + id: value + anchors.right: parent.right + margin-right: 4 + anchors.verticalCenter: parent.verticalCenter + width: 200 + font: verdana-11px-rounded + text-align: right + text: 0 + +MemberWidget < Panel + height: 85 + margin-top: 3 + + UICreature + id: creature + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + size: 28 28 + + UIWidget + id: name + anchors.left: prev.right + margin-left: 5 + anchors.top: parent.top + height: 12 + anchors.right: parent.right + text: Player Name + font: verdana-11px-rounded + text-align: left + + ProgressBar + id: health + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 2 + height: 7 + background-color: #00c000 + phantom: false + + ProgressBar + id: mana + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + height: 7 + background-color: #0000FF + phantom: false + + DualLabel + id: balance + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 5 + text: Balance: + + DualLabel + id: damage + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + text: Damage: + + DualLabel + id: healing + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + text: Healing: + +AnalyzerPriceLabel < Label + background-color: alpha + text-offset: 2 0 + focusable: true + height: 16 + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('x') + anchors.right: parent.right + margin-right: 15 + width: 15 + height: 15 + +AnalyzerListPanel < Panel + padding-left: 4 + padding-right: 4 + layout: + type: verticalBox + fit-children: true + + +ListLabel < Label + height: 15 + font: verdana-11px-rounded + text-offset: 15 0 + +AnalyzerItemsPanel < Panel + id: List + padding: 2 + layout: + type: grid + cell-size: 33 33 + cell-spacing: 1 + num-columns: 5 + fit-children: true + +AnalyzerLootItem < UIItem + opacity: 0.87 + height: 37 + margin-left: 1 + virtual: true + background-color: alpha + + Label + id: count + font: verdana-11px-rounded + color: white + opacity: 0.87 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-right: 2 + text-align: right + text: 0 + +AnalyzerGraph < UIGraph + height: 140 + capacity: 400 + line-width: 1 + color: red + margin-top: 5 + margin-left: 5 + margin-right: 5 + background-color: #383636 + padding: 5 + font: verdana-11px-rounded + image-source: /images/ui/graph_background + +AnalyzerProgressBar < ProgressBar + background-color: green + height: 5 + margin-top: 3 + phantom: false + margin-left: 3 + margin-right: 3 + border: 1 black + +AnalyzerButton < Button + height: 22 + margin-bottom: 2 + font: verdana-11px-rounded + text-offset: 0 4 + +MainAnalyzerWindow < MiniWindow + id: MainAnalyzerWindow + text: Analytics Selector + height: 293 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 5 + padding-right: 5 + padding-top: 5 + layout: verticalBox + + AnalyzerButton + id: HuntingAnalyzer + text: Hunting Analyzer + + AnalyzerButton + id: LootAnalyzer + text: Loot Analyzer + + AnalyzerButton + id: SupplyAnalyzer + text: Supply Analyzer + + AnalyzerButton + id: ImpactAnalyzer + text: Impact Analyzer + + AnalyzerButton + id: XPAnalyzer + text: XP Analyzer + + AnalyzerButton + id: DropTracker + text: Drop Tracker + + AnalyzerButton + id: Stats + text: CaveBot Stats + color: #74B73E + + AnalyzerButton + id: PartyHunt + text: Party Hunt + color: #3895D3 + + AnalyzerButton + id: BossTracker + text: Boss Cooldowns + color: #df3afb + + AnalyzerButton + id: Settings + text: Features & Settings + color: #FABD02 + + AnalyzerButton + id: ResetSession + text: Reset Session + color: #FF0000 + +HuntingAnalyzer < MiniWindow + id: HuntingAnalyzerWindow + text: Hunt Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +LootAnalyzer < MiniWindow + id: LootAnalyzerWindow + text: Loot Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +SupplyAnalyzer < MiniWindow + id: SupplyAnalyzerWindow + text: Supply Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +ImpactAnalyzer < MiniWindow + id: ImpactAnalyzerWindow + text: Impact Analyzer + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +XPAnalyzer < MiniWindow + id: XPAnalyzerWindow + text: XP Analyzer + height: 150 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-top: 3 + layout: verticalBox + +PartyAnalyzerWindow < MiniWindow + id: PartyAnalyzerWindow + text: Party Hunt + height: 200 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 3 + padding-right: 3 + padding-top: 1 + layout: verticalBox + +DropTracker < MiniWindow + id: DropTracker + text: Drop Tracker + height: 200 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 3 + padding-right: 3 + padding-top: 1 + layout: verticalBox + +CaveBotStats < MiniWindow + id: CaveBotStats + text: CaveBot Stats + height: 200 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 3 + padding-right: 3 + padding-top: 1 + layout: verticalBox + +BossTracker < MiniWindow + id: BossTracker + text: Boss Cooldowns + height: 200 + icon: /images/topbuttons/analyzers + + MiniWindowContents + padding-left: 3 + padding-right: 3 + padding-top: 1 + layout: verticalBox + + SearchPanel + id: search + +FeaturesWindow < MainWindow + id: FeaturesWindow + size: 250 370 + padding: 15 + text: Analyzers Features + @onEscape: self:hide() + + TextList + id: CustomPrices + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 10 + padding: 1 + height: 220 + vertical-scrollbar: CustomPricesScrollBar + + VerticalScrollBar + id: CustomPricesScrollBar + anchors.top: CustomPrices.top + anchors.bottom: CustomPrices.bottom + anchors.right: CustomPrices.right + step: 14 + pixels-scroll: true + + BotItem + id: ID + anchors.left: CustomPrices.left + anchors.top: CustomPrices.bottom + margin-top: 5 + + SpinBox + id: NewPrice + anchors.left: prev.right + margin-left: 5 + anchors.verticalCenter: prev.verticalCenter + width: 100 + minimum: 0 + maximum: 1000000000 + step: 1 + text-align: center + focusable: true + + Button + id: addItem + anchors.left: prev.right + margin-left: 5 + anchors.verticalCenter: prev.verticalCenter + anchors.right: CustomPrices.right + text: Add + font: verdana-11px-rounded + + HorizontalSeparator + anchors.left: ID.right + margin-left: 5 + anchors.right: CustomPrices.right + anchors.verticalCenter: ID.top + + HorizontalSeparator + id: secondSeparator + anchors.left: ID.right + margin-left: 5 + anchors.right: CustomPrices.right + anchors.bottom: ID.bottom + + BotSwitch + id: LootChannel + anchors.left: CustomPrices.left + anchors.right: parent.horizontalCenter + margin-right: 2 + anchors.top: prev.top + margin-top: 20 + text: Loot Channel + font: verdana-11px-rounded + + BotSwitch + id: RarityFrames + anchors.left: parent.horizontalCenter + margin-left: 2 + anchors.right: CustomPrices.right + anchors.top: secondSeparator.top + margin-top: 20 + text: Rarity Frames + font: verdana-11px-rounded + + HorizontalSeparator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/antiRs.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/antiRs.lua new file mode 100644 index 0000000000..02160acc9a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/antiRs.lua @@ -0,0 +1,33 @@ +setDefaultTab("Tools") +g_game.cancelAttackAndFollow() + +local frags = 0 +local unequip = false +local m = macro(50, "AntiRS & Msg", function() end) + +function safeExit() + CaveBot.setOff() + TargetBot.setOff() + g_game.cancelAttackAndFollow() + g_game.cancelAttackAndFollow() + g_game.cancelAttackAndFollow() + modules.game_interface.forceExit() +end + +onTextMessage(function(mode, text) + if not m.isOn() then return end + if not text:find("Warning! The murder of") then return end + frags = frags + 1 + if killsToRs() < 6 or frags > 1 then + EquipManager.setOff() + schedule(100, function() + local id = getLeft() and getLeft():getId() + + if id and not unequip then + unequip = true + g_game.equipItemId(id) + end + safeExit() + end) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/cast_food.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/cast_food.lua new file mode 100644 index 0000000000..187738d830 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/cast_food.lua @@ -0,0 +1,22 @@ +setDefaultTab("HP") +if voc() ~= 1 and voc() ~= 11 then + if storage.foodItems then + local t = {} + for i, v in pairs(storage.foodItems) do + if not table.find(t, v.id) then + table.insert(t, v.id) + end + end + local foodItems = { 3607, 3585, 3592, 3600, 3601 } + for i, item in pairs(foodItems) do + if not table.find(t, item) then + table.insert(storage.foodItems, item) + end + end + end + macro(500, "Cast Food", function() + if player:getRegenerationTime() <= 400 then + cast("exevo pan", 5000) + end + end) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/cavebot.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/cavebot.lua new file mode 100644 index 0000000000..416c6c0166 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/cavebot.lua @@ -0,0 +1,53 @@ +-- Cavebot by otclient@otclient.ovh +-- visit http://bot.otclient.ovh/ + +local cavebotTab = "Cave" +local targetingTab = storage.extras.joinBot and "Cave" or "Target" + +setDefaultTab(cavebotTab) +CaveBot.Extensions = {} +importStyle("/cavebot/cavebot.otui") +importStyle("/cavebot/config.otui") +importStyle("/cavebot/editor.otui") +dofile("/cavebot/actions.lua") +dofile("/cavebot/config.lua") +dofile("/cavebot/editor.lua") +dofile("/cavebot/example_functions.lua") +dofile("/cavebot/recorder.lua") +dofile("/cavebot/walking.lua") +dofile("/cavebot/minimap.lua") +-- in this section you can add extensions, check extension_template.lua +--dofile("/cavebot/extension_template.lua") +dofile("/cavebot/sell_all.lua") +dofile("/cavebot/depositor.lua") +dofile("/cavebot/buy_supplies.lua") +dofile("/cavebot/d_withdraw.lua") +dofile("/cavebot/supply_check.lua") +dofile("/cavebot/travel.lua") +dofile("/cavebot/doors.lua") +dofile("/cavebot/pos_check.lua") +dofile("/cavebot/withdraw.lua") +dofile("/cavebot/inbox_withdraw.lua") +dofile("/cavebot/lure.lua") +dofile("/cavebot/bank.lua") +dofile("/cavebot/clear_tile.lua") +dofile("/cavebot/tasker.lua") +dofile("/cavebot/imbuing.lua") +dofile("/cavebot/stand_lure.lua") +-- main cavebot file, must be last +dofile("/cavebot/cavebot.lua") + +setDefaultTab(targetingTab) +if storage.extras.joinBot then UI.Label("-- [[ TargetBot ]] --") end +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") diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/cavebot_control_panel.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/cavebot_control_panel.lua new file mode 100644 index 0000000000..765a640de0 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/cavebot_control_panel.lua @@ -0,0 +1,63 @@ +setDefaultTab("Cave") + +g_ui.loadUIFromString([[ +CaveBotControlPanel < Panel + margin-top: 5 + layout: + type: verticalBox + fit-children: true + + HorizontalSeparator + + Label + text-align: center + text: CaveBot Control Panel + font: verdana-11px-rounded + margin-top: 3 + + HorizontalSeparator + + Panel + id: buttons + margin-top: 2 + layout: + type: grid + cell-size: 86 20 + cell-spacing: 1 + flow: true + fit-children: true + + HorizontalSeparator + margin-top: 3 +]]) + +local panel = UI.createWidget("CaveBotControlPanel") + +storage.caveBot = { + forceRefill = false, + backStop = false, + backTrainers = false, + backOffline = false +} + +-- [[ B U T T O N S ]] -- + +local forceRefill = UI.Button("Force Refill", function(widget) + storage.caveBot.forceRefill = true + print("[CaveBot] Going back on refill on next supply check.") +end, panel.buttons) + +local backStop = UI.Button("Back & Stop", function(widget) + storage.caveBot.backStop = true + print("[CaveBot] Going back to city on next supply check and turning off CaveBot on depositer action.") +end, panel.buttons) + +local backTrainers = UI.Button("To Trainers", function(widget) + storage.caveBot.backTrainers = true + print("[CaveBot] Going back to city on next supply check and going to label 'toTrainers' on depositer action.") +end, panel.buttons) + +local backOffline = UI.Button("Offline", function(widget) + storage.caveBot.backOffline = true + print("[CaveBot] Going back to city on next supply check and going to label 'toOfflineTraining' on depositer action.") +end, panel.buttons) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/combo.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/combo.lua new file mode 100644 index 0000000000..b97c118497 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/combo.lua @@ -0,0 +1,443 @@ +setDefaultTab("Main") +local panelName = "combobot" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('ComboBot') + + Button + id: combos + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = { + enabled = false, + onSayEnabled = false, + onShootEnabled = false, + onCastEnabled = false, + followLeaderEnabled = false, + attackLeaderTargetEnabled = false, + attackSpellEnabled = false, + attackItemToggle = false, + sayLeader = "", + shootLeader = "", + castLeader = "", + sayPhrase = "", + spell = "", + serverLeader = "", + item = 3155, + attack = "", + follow = "", + commandsEnabled = true, + serverEnabled = false, + serverLeaderTarget = false, + serverTriggers = true + } +end + +local config = storage[panelName] + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) +config.enabled = not config.enabled +widget:setOn(config.enabled) +end + +ui.combos.onClick = function(widget) + comboWindow:show() + comboWindow:raise() + comboWindow:focus() +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + comboWindow = UI.createWindow('ComboWindow', rootWidget) + comboWindow:hide() + + -- bot item + + comboWindow.actions.attackItem:setItemId(config.item) + comboWindow.actions.attackItem.onItemChange = function(widget) + config.item = widget:getItemId() + end + + -- switches + + comboWindow.actions.commandsToggle:setOn(config.commandsEnabled) + comboWindow.actions.commandsToggle.onClick = function(widget) + config.commandsEnabled = not config.commandsEnabled + widget:setOn(config.commandsEnabled) + end + + comboWindow.server.botServerToggle:setOn(config.serverEnabled) + comboWindow.server.botServerToggle.onClick = function(widget) + config.serverEnabled = not config.serverEnabled + widget:setOn(config.serverEnabled) + end + + comboWindow.server.Triggers:setOn(config.serverTriggers) + comboWindow.server.Triggers.onClick = function(widget) + config.serverTriggers = not config.serverTriggers + widget:setOn(config.serverTriggers) + end + + comboWindow.server.targetServerLeaderToggle:setOn(config.serverLeaderTarget) + comboWindow.server.targetServerLeaderToggle.onClick = function(widget) + config.serverLeaderTarget = not config.serverLeaderTarget + widget:setOn(config.serverLeaderTarget) + end + + -- buttons + comboWindow.closeButton.onClick = function(widget) + comboWindow:hide() + end + + -- combo boxes + + comboWindow.actions.followLeader:setOption(config.follow) + comboWindow.actions.followLeader.onOptionChange = function(widget) + config.follow = widget:getCurrentOption().text + end + + comboWindow.actions.attackLeaderTarget:setOption(config.attack) + comboWindow.actions.attackLeaderTarget.onOptionChange = function(widget) + config.attack = widget:getCurrentOption().text + end + + -- checkboxes + comboWindow.trigger.onSayToggle:setChecked(config.onSayEnabled) + comboWindow.trigger.onSayToggle.onClick = function(widget) + config.onSayEnabled = not config.onSayEnabled + widget:setChecked(config.onSayEnabled) + end + + comboWindow.trigger.onShootToggle:setChecked(config.onShootEnabled) + comboWindow.trigger.onShootToggle.onClick = function(widget) + config.onShootEnabled = not config.onShootEnabled + widget:setChecked(config.onShootEnabled) + end + + comboWindow.trigger.onCastToggle:setChecked(config.onCastEnabled) + comboWindow.trigger.onCastToggle.onClick = function(widget) + config.onCastEnabled = not config.onCastEnabled + widget:setChecked(config.onCastEnabled) + end + + comboWindow.actions.followLeaderToggle:setChecked(config.followLeaderEnabled) + comboWindow.actions.followLeaderToggle.onClick = function(widget) + config.followLeaderEnabled = not config.followLeaderEnabled + widget:setChecked(config.followLeaderEnabled) + end + + comboWindow.actions.attackLeaderTargetToggle:setChecked(config.attackLeaderTargetEnabled) + comboWindow.actions.attackLeaderTargetToggle.onClick = function(widget) + config.attackLeaderTargetEnabled = not config.attackLeaderTargetEnabled + widget:setChecked(config.attackLeaderTargetEnabled) + end + + comboWindow.actions.attackSpellToggle:setChecked(config.attackSpellEnabled) + comboWindow.actions.attackSpellToggle.onClick = function(widget) + config.attackSpellEnabled = not config.attackSpellEnabled + widget:setChecked(config.attackSpellEnabled) + end + + comboWindow.actions.attackItemToggle:setChecked(config.attackItemEnabled) + comboWindow.actions.attackItemToggle.onClick = function(widget) + config.attackItemEnabled = not config.attackItemEnabled + widget:setChecked(config.attackItemEnabled) + end + + -- text edits + comboWindow.trigger.onSayLeader:setText(config.sayLeader) + comboWindow.trigger.onSayLeader.onTextChange = function(widget, text) + config.sayLeader = text + end + + comboWindow.trigger.onShootLeader:setText(config.shootLeader) + comboWindow.trigger.onShootLeader.onTextChange = function(widget, text) + config.shootLeader = text + end + + comboWindow.trigger.onCastLeader:setText(config.castLeader) + comboWindow.trigger.onCastLeader.onTextChange = function(widget, text) + config.castLeader = text + end + + comboWindow.trigger.onSayPhrase:setText(config.sayPhrase) + comboWindow.trigger.onSayPhrase.onTextChange = function(widget, text) + config.sayPhrase = text + end + + comboWindow.actions.attackSpell:setText(config.spell) + comboWindow.actions.attackSpell.onTextChange = function(widget, text) + config.spell = text + end + + comboWindow.server.botServerLeader:setText(config.serverLeader) + comboWindow.server.botServerLeader.onTextChange = function(widget, text) + config.serverLeader = text + end +end + +-- bot server +-- [[ join party made by Frosty ]] -- + +local shouldCloseWindow = false +local firstInvitee = true +local isInComboTeam = false +macro(10, function() + if shouldCloseWindow and config.serverEnabled and config.enabled then + local channelsWindow = modules.game_console.channelsWindow + if channelsWindow then + local child = channelsWindow:getChildById("buttonCancel") + if child then + child:onClick() + shouldCloseWindow = false + isInComboTeam = true + end + end + end +end) + +comboWindow.server.partyButton.onClick = function(widget) + if config.serverEnabled and config.enabled then + if config.serverLeader:len() > 0 and storage.BotServerChannel:len() > 0 then + talkPrivate(config.serverLeader, "request invite " .. storage.BotServerChannel) + else + error("Request failed. Lack of data.") + end + end +end + +onTextMessage(function(mode, text) + if config.serverEnabled and config.enabled then + if mode == 20 then + if string.find(text, "invited you to") then + local regex = "[a-zA-Z]*" + local regexData = regexMatch(text, regex) + if regexData[1][1]:lower() == config.serverLeader:lower() then + local leader = getCreatureByName(regexData[1][1]) + if leader then + g_game.partyJoin(leader:getId()) + g_game.requestChannels() + g_game.joinChannel(1) + shouldCloseWindow = true + end + end + end + end + end +end) + +onTalk(function(name, level, mode, text, channelId, pos) + if config.serverEnabled and config.enabled then + if mode == 4 then + if string.find(text, "request invite") then + local access = string.match(text, "%d.*") + if access and access == storage.BotServerChannel then + local minion = getCreatureByName(name) + if minion then + g_game.partyInvite(minion:getId()) + if firstInvitee then + g_game.requestChannels() + g_game.joinChannel(1) + shouldCloseWindow = true + firstInvitee = false + end + end + else + talkPrivate(name, "Incorrect access key!") + end + end + end + end + -- [[ End of Frosty's Code ]] -- + if config.enabled and config.enabled then + if name:lower() == config.sayLeader:lower() and string.find(text, config.sayPhrase) and config.onSayEnabled then + startCombo = true + end + if (config.castLeader and name:lower() == config.castLeader:lower()) and isAttSpell(text) and config.onCastEnabled then + startCombo = true + end + end + if config.enabled and config.commandsEnabled and (config.shootLeader and name:lower() == config.shootLeader:lower()) or (config.sayLeader and name:lower() == config.sayLeader:lower()) or (config.castLeader and name:lower() == config.castLeader:lower()) then + if string.find(text, "ue") then + say(config.spell) + elseif string.find(text, "sd") then + local params = string.split(text, ",") + if #params == 2 then + local target = params[2]:trim() + if getCreatureByName(target) then + useWith(3155, getCreatureByName(target)) + end + end + elseif string.find(text, "att") then + local attParams = string.split(text, ",") + if #attParams == 2 then + local atTarget = attParams[2]:trim() + if getCreatureByName(atTarget) and config.attack == "COMMAND TARGET" then + g_game.attack(getCreatureByName(atTarget)) + end + end + end + end + if isAttSpell(text) and config.enabled and config.serverEnabled then + BotServer.send("trigger", "start") + end +end) + +onMissle(function(missle) + if config.enabled and config.onShootEnabled then + if not config.shootLeader or config.shootLeader:len() == 0 then + return + end + local src = missle:getSource() + if src.z ~= posz() then + return + end + local from = g_map.getTile(src) + local to = g_map.getTile(missle:getDestination()) + if not from or not to then + return + end + local fromCreatures = from:getCreatures() + local toCreatures = to:getCreatures() + if #fromCreatures ~= 1 or #toCreatures ~= 1 then + return + end + local c1 = fromCreatures[1] + local t1 = toCreatures[1] + leaderTarget = t1 + if c1:getName():lower() == config.shootLeader:lower() then + if config.attackItemEnabled and config.item and config.item > 100 and findItem(config.item) then + useWith(config.item, t1) + end + if config.attackSpellEnabled and config.spell:len() > 1 then + say(config.spell) + end + end + end +end) + +macro(10, function() + if not config.enabled or not config.attackLeaderTargetEnabled then return end + if leaderTarget and config.attack == "LEADER TARGET" then + if not getTarget() or (getTarget() and getTarget():getName() ~= leaderTarget:getName()) then + g_game.attack(leaderTarget) + end + end + if config.enabled and config.serverEnabled and config.attack == "SERVER LEADER TARGET" and serverTarget then + if serverTarget and not getTarget() or (getTarget() and getTarget():getname() ~= serverTarget) + then + g_game.attack(serverTarget) + end + end +end) + + +local toFollow +local toFollowPos = {} + +macro(100, function() + toFollow = nil + if not config.enabled or not config.followLeaderEnabled then return end + if leaderTarget and config.follow == "LEADER TARGET" and leaderTarget:isPlayer() then + toFollow = leaderTarget:getName() + elseif config.follow == "SERVER LEADER TARGET" and config.serverLeader:len() ~= 0 then + toFollow = serverTarget + elseif config.follow == "SERVER LEADER" and config.serverLeader:len() ~= 0 then + toFollow = config.serverLeader + elseif config.follow == "LEADER" then + if config.onSayEnabled and config.sayLeader:len() ~= 0 then + toFollow = config.sayLeader + elseif config.onCastEnabled and config.castLeader:len() ~= 0 then + toFollow = config.castLeader + elseif config.onShootEnabled and config.shootLeader:len() ~= 0 then + toFollow = config.shootLeader + end + end + if not toFollow then return end + local target = getCreatureByName(toFollow) + if target then + local tpos = target:getPosition() + toFollowPos[tpos.z] = tpos + end + if player:isWalking() then return end + local p = toFollowPos[posz()] + if not p then return end + if CaveBot.walkTo(p, 20, {ignoreNonPathable=true, precision=1, ignoreStairs=false}) then + delay(100) + end +end) + +onCreaturePositionChange(function(creature, oldPos, newPos) + if creature:getName() == toFollow and newPos then + toFollowPos[newPos.z] = newPos + end +end) + +local timeout = now +macro(10, function() + if config.enabled and startCombo then + if config.attackItemEnabled and config.item and config.item > 100 and findItem(config.item) then + useWith(config.item, getTarget()) + end + if config.attackSpellEnabled and config.spell:len() > 1 then + say(config.spell) + end + startCombo = false + end + -- attack part / server + if BotServer._websocket and config.enabled and config.serverEnabled then + if target() and now - timeout > 500 then + targetPos = target():getName() + BotServer.send("target", targetPos) + timeout = now + end + end +end) + +onUseWith(function(pos, itemId, target, subType) + if BotServer._websocket and itemId == 3155 then + BotServer.send("useWith", target:getPosition()) + end +end) + +if BotServer._websocket and config.enabled and config.serverEnabled then + BotServer.listen("trigger", function(name, message) + if message == "start" and name:lower() ~= player:getName():lower() and name:lower() == config.serverLeader:lower() and config.serverTriggers then + startCombo = true + end + end) + BotServer.listen("target", function(name, message) + if name:lower() ~= player:getName():lower() and name:lower() == config.serverLeader:lower() then + if not target() or target():getName() == getCreatureByName(message) then + if config.serverLeaderTarget then + serverTarget = getCreatureByName(message) + g_game.attack(getCreatureByName(message)) + end + end + end + end) + BotServer.listen("useWith", function(name, message) + local tile = g_map.getTile(message) + if config.serverTriggers and name:lower() ~= player:getName():lower() and name:lower() == config.serverLeader:lower() and config.attackItemEnabled and config.item and findItem(config.item) then + useWith(config.item, tile:getTopUseThing()) + end + end) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/combo.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/combo.otui new file mode 100644 index 0000000000..b89013acc8 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/combo.otui @@ -0,0 +1,391 @@ +AttackComboBoxPopupMenu < ComboBoxPopupMenu +AttackComboBoxPopupMenuButton < ComboBoxPopupMenuButton +AttackComboBox < ComboBox + @onSetup: | + self:addOption("LEADER TARGET") + self:addOption("COMMAND TARGET") + +FollowComboBoxPopupMenu < ComboBoxPopupMenu +FollowComboBoxPopupMenuButton < ComboBoxPopupMenuButton +FollowComboBox < ComboBox + @onSetup: | + self:addOption("LEADER TARGET") + self:addOption("SERVER LEADER TARGET") + self:addOption("LEADER") + self:addOption("SERVER LEADER") + +ComboTrigger < Panel + id: trigger + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 450 72 + + Label + id: triggerLabel1 + anchors.left: parent.left + anchors.top: parent.top + text: On Say + margin-top: 8 + margin-left: 5 + color: #ffaa00 + + Label + id: leaderLabel + anchors.left: triggerLabel1.right + anchors.top: triggerLabel1.top + text: Leader: + margin-left: 35 + + TextEdit + id: onSayLeader + anchors.left: leaderLabel.right + anchors.top: leaderLabel.top + anchors.bottom: leaderLabel.bottom + margin-left: 5 + width: 120 + font: cipsoftFont + + Label + id: phrase + anchors.left: onSayLeader.right + anchors.top: onSayLeader.top + text: Phrase: + margin-left: 5 + + TextEdit + id: onSayPhrase + anchors.left: phrase.right + anchors.top: leaderLabel.top + anchors.bottom: leaderLabel.bottom + margin-left: 5 + width: 120 + font: cipsoftFont + + CheckBox + id: onSayToggle + anchors.left: onSayPhrase.right + anchors.top: onSayPhrase.top + margin-top: 1 + margin-left: 5 + + Label + id: triggerLabel2 + anchors.left: triggerLabel1.left + anchors.top: triggerLabel1.bottom + text: On Shoot + margin-top: 5 + color: #ffaa00 + + Label + id: leaderLabel1 + anchors.left: triggerLabel2.right + anchors.top: triggerLabel2.top + text: Leader: + margin-left: 24 + + TextEdit + id: onShootLeader + anchors.left: leaderLabel1.right + anchors.top: leaderLabel1.top + anchors.bottom: leaderLabel1.bottom + anchors.right: onSayPhrase.right + margin-left: 5 + width: 120 + font: cipsoftFont + + CheckBox + id: onShootToggle + anchors.left: onShootLeader.right + anchors.top: onShootLeader.top + margin-top: 1 + margin-left: 5 + + Label + id: triggerLabel3 + anchors.left: triggerLabel2.left + anchors.top: triggerLabel2.bottom + text: On Cast + margin-top: 5 + color: #ffaa00 + + Label + id: leaderLabel2 + anchors.left: triggerLabel3.right + anchors.top: triggerLabel3.top + text: Leader: + margin-left: 32 + + TextEdit + id: onCastLeader + anchors.left: leaderLabel2.right + anchors.top: leaderLabel2.top + anchors.bottom: leaderLabel2.bottom + anchors.right: onSayPhrase.right + margin-left: 5 + width: 120 + font: cipsoftFont + + CheckBox + id: onCastToggle + anchors.left: onCastLeader.right + anchors.top: onCastLeader.top + margin-top: 1 + margin-left: 5 + +ComboActions < Panel + id: actions + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 220 100 + + Label + id: label1 + anchors.left: parent.left + anchors.top: parent.top + text: Follow: + margin-top: 5 + margin-left: 3 + height: 15 + color: #ffaa00 + + FollowComboBox + id: followLeader + anchors.left: prev.right + anchors.top: prev.top + margin-left: 7 + height: 15 + width: 145 + font: cipsoftFont + + CheckBox + id: followLeaderToggle + anchors.left: followLeader.right + anchors.top: followLeader.top + margin-top: 2 + margin-left: 5 + + Label + id: label2 + anchors.left: label1.left + anchors.top: label1.bottom + margin-top: 5 + text: Attack: + color: #ffaa00 + + AttackComboBox + id: attackLeaderTarget + anchors.left: prev.right + anchors.top: prev.top + margin-left: 5 + height: 15 + width: 145 + font: cipsoftFont + + CheckBox + id: attackLeaderTargetToggle + anchors.left: attackLeaderTarget.right + anchors.top: attackLeaderTarget.top + margin-top: 2 + margin-left: 5 + + Label + id: label3 + anchors.left: label2.left + anchors.top: label2.bottom + margin-top: 5 + text: Spell: + color: #ffaa00 + + TextEdit + id: attackSpell + anchors.left: prev.right + anchors.top: prev.top + anchors.right: attackLeaderTarget.right + margin-left: 17 + height: 15 + width: 145 + font: cipsoftFont + + CheckBox + id: attackSpellToggle + anchors.left: attackSpell.right + anchors.top: attackSpell.top + margin-top: 2 + margin-left: 5 + + Label + id: label4 + anchors.left: label3.left + anchors.top: label3.bottom + margin-top: 15 + text: Attack Item: + color: #ffaa00 + + BotItem + id: attackItem + anchors.left: prev.right + anchors.verticalCenter: prev.verticalCenter + margin-left: 10 + + CheckBox + id: attackItemToggle + anchors.left: prev.right + anchors.verticalCenter: prev.verticalCenter + margin-left: 5 + + BotSwitch + id: commandsToggle + anchors.left: prev.right + anchors.top: attackItem.top + anchors.right: attackSpellToggle.right + anchors.bottom: attackItem.bottom + margin-left: 5 + text: Leader Commands + text-wrap: true + multiline: true + +BotServer < Panel + id: server + image-source: /images/ui/panel_flat + image-border: 6 + padding: 3 + size: 220 100 + + Label + id: labelX + anchors.left: parent.left + anchors.top: parent.top + text: Leader: + height: 15 + color: #ffaa00 + margin-left: 3 + margin-top: 5 + + TextEdit + id: botServerLeader + anchors.left: prev.right + anchors.top: prev.top + anchors.right: parent.right + margin-right: 3 + margin-left: 9 + height: 15 + font: cipsoftFont + + Button + id: partyButton + anchors.left: labelX.left + anchors.top: botServerLeader.bottom + margin-top: 5 + height: 30 + text: Join Party + text-wrap: true + multiline: true + + BotSwitch + id: botServerToggle + anchors.left: prev.right + anchors.top: botServerLeader.bottom + anchors.right: parent.right + height: 30 + margin-left: 3 + margin-right: 3 + margin-top: 5 + text: Server Enabled + + BotSwitch + id: targetServerLeaderToggle + anchors.left: partyButton.left + anchors.top: partyButton.bottom + anchors.right: partyButton.right + margin-top: 3 + height: 30 + text: Leader Targets + + BotSwitch + id: Triggers + anchors.left: prev.right + anchors.top: partyButton.bottom + anchors.right: parent.right + margin-top: 3 + height: 30 + margin-left: 3 + margin-right: 3 + text: Triggers + +ComboWindow < MainWindow + !text: tr('Combo Options') + size: 500 280 + @onEscape: self:hide() + + ComboTrigger + id: trigger + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + margin-top: 7 + + Label + id: title + anchors.top: parent.top + anchors.left: parent.left + margin-left: 10 + text: Combo Trigger + color: #ff7700 + + ComboActions + id: actions + anchors.top: trigger.bottom + anchors.left: trigger.left + margin-top: 15 + + Label + id: title + anchors.top: parent.top + anchors.left: parent.left + margin-left: 10 + margin-top: 85 + text: Combo Actions + color: #ff7700 + + BotServer + id: server + anchors.top: actions.top + anchors.left: actions.right + margin-left: 10 + + Label + id: title + anchors.top: parent.top + anchors.left: server.left + margin-left: 3 + margin-top: 85 + text: BotServer + color: #ff7700 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 + + Button + id: toolsButton + !text: tr('Help') + font: cipsoftFont + anchors.right: closeButton.left + anchors.top: closeButton.top + margin-right: 10 + size: 45 21 + @onClick: g_platform.openUrl("http://bot.otclient.ovh/books/scripts/page/combobot") \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/configs.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/configs.lua new file mode 100644 index 0000000000..45cd29af73 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/configs.lua @@ -0,0 +1,97 @@ +--[[ + Configs for modules + Based on Kondrah storage method +--]] +local configName = modules.game_bot.contentsPanel.config:getCurrentOption().text + +-- make vBot config dir +if not g_resources.directoryExists("/bot/".. configName .."/vBot_configs/") then + g_resources.makeDir("/bot/".. configName .."/vBot_configs/") +end + +-- make profile dirs +for i=1,10 do + local path = "/bot/".. configName .."/vBot_configs/profile_"..i + if not g_resources.directoryExists(path) then + g_resources.makeDir(path) + end +end + +local profile = g_settings.getNumber('profile') + +HealBotConfig = {} +local healBotFile = "/bot/" .. configName .. "/vBot_configs/profile_".. profile .. "/HealBot.json" +AttackBotConfig = {} +local attackBotFile = "/bot/" .. configName .. "/vBot_configs/profile_".. profile .. "/AttackBot.json" +SuppliesConfig = {} +local suppliesFile = "/bot/" .. configName .. "/vBot_configs/profile_".. profile .. "/Supplies.json" + + +--healbot +if g_resources.fileExists(healBotFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(healBotFile)) + end) + if not status then + return onError("Error while reading config file (" .. healBotFile .. "). To fix this problem you can delete HealBot.json. Details: " .. result) + end + HealBotConfig = result +end + +--attackbot +if g_resources.fileExists(attackBotFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(attackBotFile)) + end) + if not status then + return onError("Error while reading config file (" .. attackBotFile .. "). To fix this problem you can delete HealBot.json. Details: " .. result) + end + AttackBotConfig = result +end + +--supplies +if g_resources.fileExists(suppliesFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(suppliesFile)) + end) + if not status then + return onError("Error while reading config file (" .. suppliesFile .. "). To fix this problem you can delete HealBot.json. Details: " .. result) + end + SuppliesConfig = result +end + +function vBotConfigSave(file) + -- file can be either + --- heal + --- atk + --- supply + local configFile + local configTable + if not file then return end + file = file:lower() + if file == "heal" then + configFile = healBotFile + configTable = HealBotConfig + elseif file == "atk" then + configFile = attackBotFile + configTable = AttackBotConfig + elseif file == "supply" then + configFile = suppliesFile + configTable = SuppliesConfig + else + return + end + + local status, result = pcall(function() + return json.encode(configTable, 2) + end) + if not status then + return onError("Error while saving config. it won't be saved. Details: " .. result) + end + + if result:len() > 100 * 1024 * 1024 then + return onError("config file is too big, above 100MB, it won't be saved") + end + + g_resources.writeFileContents(configFile, result) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.lua new file mode 100644 index 0000000000..a869c771bc --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.lua @@ -0,0 +1,136 @@ +setDefaultTab("Cave") +local panelName = "specialDeposit" +local depositerPanel + +UI.Button("Stashing Settings", function() + depositerPanel:show() + depositerPanel:raise() + depositerPanel:focus() +end) + +if not storage[panelName] then + storage[panelName] = { + items = {}, + height = 380 + } +end + +local config = storage[panelName] + +depositerPanel = UI.createWindow('DepositerPanel', rootWidget) +depositerPanel:hide() +-- basic one +depositerPanel.CloseButton.onClick = function() + depositerPanel:hide() +end + +depositerPanel:setHeight(config.height or 380) +depositerPanel.onGeometryChange = function(widget, old, new) + if old.height == 0 then return end + config.height = new.height +end + +function arabicToRoman(n) + local t = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XI", "XII", "XIV", "XV", "XVI", "XVII"} + return t[n] +end + +local function refreshEntries() + depositerPanel.DepositerList:destroyChildren() + for _, entry in ipairs(config.items) do + local panel = g_ui.createWidget("StashItem", depositerPanel.DepositerList) + panel.name:setText(Item.create(entry.id):getMarketData().name) + for i, child in ipairs(panel:getChildren()) do + if child:getId() ~= "slot" then + child:setTooltip("Clear item or double click to remove entry.") + child.onDoubleClick = function(widget) + table.remove(config.items, table.find(entry)) + panel:destroy() + end + end + end + panel.item:setItemId(entry.id) + if entry.id > 0 then + panel.item:setImageSource('') + end + panel.item.onItemChange = function(widget) + local id = widget:getItemId() + if id < 100 then + table.remove(config.items, table.find(entry)) + panel:destroy() + else + for i, data in ipairs(config.items) do + if data.id == id then + warn("[Depositer Panel] Item already added!") + return + end + end + entry.id = id + panel.item:setImageSource('') + panel.name:setText(Item.create(entry.id):getMarketData().name) + if entry.index == 0 then + local window = modules.client_textedit.show(panel.slot, { + title = "Set depot for "..panel.name:getText(), + description = "Select depot to which item should be stashed, choose between 3 and 17", + validation = [[^([3-9]|1[0-7])$]] + }) + window.text:setText(entry.index) + schedule(50, function() + window:raise() + window:focus() + end) + end + end + end + if entry.id > 0 then + panel.slot:setText("Stash to depot: ".. entry.index) + end + panel.slot:setTooltip("Click to set stashing destination.") + panel.slot.onClick = function(widget) + local window = modules.client_textedit.show(widget, { + title = "Set depot for "..panel.name:getText(), + description = "Select depot to which item should be stashed, choose between 3 and 17", + validation = [[^([3-9]|1[0-7])$]] + }) + window.text:setText(entry.index) + schedule(50, function() + window:raise() + window:focus() + end) + end + panel.slot.onTextChange = function(widget, text) + local n = tonumber(text) + if n then + entry.index = n + widget:setText("Stash to depot: "..entry.index) + end + end + end +end +refreshEntries() + +depositerPanel.title.onDoubleClick = function(widget) + table.insert(config.items, {id=0, index=0}) + refreshEntries() +end + +function getStashingIndex(id) + for _, v in pairs(config.items) do + if v.id == id then + return v.index - 1 + end + end +end + +UI.Separator() +UI.Label("Sell Exeptions") + +if type(storage.cavebotSell) ~= "table" then + storage.cavebotSell = {23544, 3081} +end + +local sellContainer = UI.Container(function(widget, items) + storage.cavebotSell = items +end, true) +sellContainer:setHeight(35) +sellContainer:setItems(storage.cavebotSell) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.otui new file mode 100644 index 0000000000..eb3ab6bec7 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/depositer_config.otui @@ -0,0 +1,98 @@ +StashItem < Panel + height: 40 + + BotItem + id: item + anchors.top: parent.top + margin-top: 2 + anchors.left: parent.left + + UIWidget + id: name + anchors.top: prev.top + margin-top: 1 + anchors.bottom: prev.verticalCenter + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + text-align:left + text: item name + font: verdana-11px-rounded + color: #FFFFFF + + UIWidget + id: slot + anchors.top: prev.bottom + margin-top: 3 + anchors.bottom: Item.bottom + anchors.left: prev.left + anchors.right: parent.right + font: verdana-11px-rounded + text-align:left + text: Add item to select locker. + color: #CCCCCC + +DepositerPanel < MainWindow + size: 230 380 + !text: tr('Depositer Panel') + @onEscape: self:hide() + + UIWidget + id: title + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text: Double click here to add item. + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + ScrollablePanel + id: DepositerList + image-source: /images/ui/panel_flat + image-border: 1 + anchors.top: prev.bottom + margin-top: 5 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: sep.top + margin-bottom: 10 + padding: 2 + padding-left: 4 + vertical-scrollbar: DepositerScrollBar + layout: + type: verticalBox + + VerticalScrollBar + id: DepositerScrollBar + anchors.top: DepositerList.top + anchors.bottom: DepositerList.bottom + anchors.right: DepositerList.right + step: 14 + pixels-scroll: true + + ResizeBorder + id: bottomResizeBorder + anchors.fill: next + height: 3 + minimum: 180 + maximum: 800 + margin-left: 3 + margin-right: 3 + background: #ffffff88 + + HorizontalSeparator + id: sep + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: CloseButton.top + margin-bottom: 8 + + Button + id: CloseButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/depot_withdraw.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/depot_withdraw.lua new file mode 100644 index 0000000000..8323f714ec --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/depot_withdraw.lua @@ -0,0 +1,76 @@ +-- config +setDefaultTab("Tools") +local defaultBp = "shopping bag" +local id = 21411 + +-- script + +local playerContainer = nil +local depotContainer = nil +local mailContainer = nil + +function reopenLootContainer() + for _, container in pairs(getContainers()) do + if container:getName():lower() == defaultBp:lower() then + g_game.close(container) + end + end + + local lootItem = findItem(id) + if lootItem then + schedule(500, function() g_game.open(lootItem) end) + end + +end + +macro(50, "Depot Withdraw", function() + + -- set the containers + if not potionsContainer or not runesContainer or not ammoContainer then + for i, container in pairs(getContainers()) do + if container:getName() == defaultBp then + playerContainer = container + elseif string.find(container:getName(), "Depot") then + depotContainer = container + elseif string.find(container:getName(), "your inbox") then + mailContainer = container + end + end + end + + if playerContainer and #playerContainer:getItems() == 20 then + for j, item in pairs(playerContainer:getItems()) do + if item:getId() == id then + g_game.open(item, playerContainer) + return + end + end + end + + +if playerContainer and freecap() >= 200 then + local time = 500 + if depotContainer then + for i, container in pairs(getContainers()) do + if string.find(container:getName(), "Depot") then + for j, item in pairs(container:getItems()) do + g_game.move(item, playerContainer:getSlotPosition(playerContainer:getItemsCount()), item:getCount()) + return + end + end + end + end + + if mailContainer then + for i, container in pairs(getContainers()) do + if string.find(container:getName(), "your inbox") then + for j, item in pairs(container:getItems()) do + g_game.move(item, playerContainer:getSlotPosition(playerContainer:getItemsCount()), item:getCount()) + return + end + end + end + end +end + +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/eat_food.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/eat_food.lua new file mode 100644 index 0000000000..ad8edb5f12 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/eat_food.lua @@ -0,0 +1,48 @@ +setDefaultTab("HP") +if voc() ~= 1 and voc() ~= 11 then + if storage.foodItems then + local t = {} + for i, v in pairs(storage.foodItems) do + if not table.find(t, v.id) then + table.insert(t, v.id) + end + end + local foodItems = { 3607, 3585, 3592, 3600, 3601 } + for i, item in pairs(foodItems) do + if not table.find(t, item) then + table.insert(storage.foodItems, item) + end + end + end + macro(500, "Cast Food", function() + if player:getRegenerationTime() <= 400 then + cast("exevo pan", 5000) + end + end) +end + +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(500, "Eat Food", function() + if player:getRegenerationTime() > 400 or 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 +end) +UI.Separator() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/equip.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/equip.lua new file mode 100644 index 0000000000..b0c2f80343 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/equip.lua @@ -0,0 +1,36 @@ +-- config +setDefaultTab("HP") +local scripts = 2 -- if you want more auto equip panels you can change 2 to higher value + +-- script by kondrah, don't edit below unless you know what you are doing +UI.Label("Auto equip") +if type(storage.autoEquip) ~= "table" then + storage.autoEquip = {} +end +for i=1,scripts do + 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 too often + return + end + end + end + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/equipper.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/equipper.otui new file mode 100644 index 0000000000..d61db7e6a6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/equipper.otui @@ -0,0 +1,539 @@ +SlotBotItem < BotItem + border-width: 0 + $on: + image-source: /images/ui/item + $checked: + border-width: 1 + border-color: #FF0000 + +BossLabel < UIWidget + background-color: alpha + text-offset: 3 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('X') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + margin-right: 15 + text-align: center + text-offset: 0 1 + tooltip: Remove profile from the list. + +ConditionBoxPopupMenu < ComboBoxPopupMenu +ConditionBoxPopupMenuButton < ComboBoxPopupMenuButton +ConditionBox < ComboBox + @onSetup: | + self:addOption("-") + self:addOption("and") + self:addOption("or") + +PreButton < PreviousButton + background: #363636 + height: 15 + +NexButton < NextButton + background: #363636 + height: 15 + +CondidionLabel < FlatPanel + padding: 1 + height: 15 + + Label + id: text + anchors.fill: parent + text-align: center + font: verdana-11px-rounded + background: #363636 + +Rule < UIWidget + background-color: alpha + text-offset: 18 2 + focusable: true + height: 16 + text-align: left + font: verdana-11px-rounded + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 15 + height: 15 + margin-top: 2 + margin-left: 3 + tooltip: Entry enabled/disabled + + $focus: + background-color: #00000055 + + Button + id: remove + text: X + anchors.right: parent.right + margin-right: 15 + width: 14 + height: 14 + text-align: center + tooltip: Remove entry + anchors.verticalCenter: parent.verticalCenter + + Button + id: visible + text: V + anchors.right: prev.left + margin-right: 3 + width: 14 + height: 14 + text-align: center + tooltip: Items must be visible + anchors.verticalCenter: parent.verticalCenter + + +ConditionPanel < Panel + height: 58 + + NexButton + id: nex + anchors.top: parent.top + margin-top: 5 + anchors.right: parent.right + + PreButton + id: pre + anchors.top: parent.top + margin-top: 5 + anchors.left: parent.left + + CondidionLabel + id: description + anchors.top: parent.top + margin-top: 5 + anchors.left: prev.right + anchors.right: nex.left + margin-left: 3 + margin-right: 3 + + SpinBox + id: spinbox + anchors.top: description.bottom + margin-top: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: 100 + text-align: center + minimum: 0 + maximum: 100 + step: 1 + focusable: true + + BotTextEdit + id: text + anchors.top: description.bottom + margin-top: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: 200 + text-align: center + + + +ListPanel < FlatPanel + size: 270 300 + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Rules List + font: verdana-11px-rounded + color: #FABD02 + + Label + id: mainLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 10 + margin-left: 2 + !text: tr('More important methods come first.') + text-align: left + font: verdana-11px-rounded + color: #aeaeae + + TextList + id: list + anchors.fill: parent + margin-top: 25 + margin-bottom: 18 + vertical-scrollbar: listScrollBar + padding: 2 + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + + Button + id: up + anchors.right: parent.right + anchors.top: list.bottom + size: 60 17 + text: Move Up + text-align: center + font: cipsoftFont + margin-top: 5 + tooltip: Increase priority of selected rule. + + Button + id: down + anchors.right: prev.left + anchors.verticalCenter: prev.verticalCenter + size: 60 17 + margin-right: 5 + text: Move Down + text-align: center + font: cipsoftFont + tooltip: Decrease priority of selected rule. + +InputPanel < FlatPanel + size: 270 300 + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Condition Panel + font: verdana-11px-rounded + color: #FF0000 + + Label + id: mainLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + text: Equip selected items when: + text-align: center + font: verdana-11px-rounded + color: #aeaeae + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 4 + + ConditionPanel + id: condition + anchors.left: parent.left + anchors.right: parent.right + anchors.top: mainLabel.bottom + margin-top: 15 + + HorizontalSeparator + anchors.verticalCenter: next.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + + ConditionBox + id: useSecondCondition + anchors.top: condition.bottom + margin-top: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: 50 + + ConditionPanel + id: optionalCondition + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + + BotButton + id: add + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + margin-bottom: 10 + text: Add Rule + +EQPanel < FlatPanel + size: 160 230 + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Equipment Setup + font: verdana-11px-rounded + color: #03C04A + + SlotBotItem + id: head + image-source: /images/game/slots/head + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 15 + $on: + image-source: /images/ui/item + + SlotBotItem + id: body + image-source: /images/game/slots/body + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: legs + image-source: /images/game/slots/legs + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: feet + image-source: /images/game/slots/feet + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: neck + image-source: /images/game/slots/neck + anchors.top: head.top + margin-top: 13 + anchors.right: head.left + margin-right: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: left-hand + image-source: /images/game/slots/left-hand + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: finger + image-source: /images/game/slots/finger + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + Item + id: back + image-source: /images/game/slots/back-blessed + anchors.top: head.top + margin-top: 13 + anchors.left: head.right + margin-left: 5 + tooltip: Main back container modifications are unavailable. + + SlotBotItem + id: right-hand + image-source: /images/game/slots/right-hand + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + $on: + image-source: /images/ui/item + + SlotBotItem + id: ammo + image-source: /images/game/slots/ammo + anchors.horizontalCenter: prev.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + + BotButton + id: cloneEq + anchors.top: feet.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 15 + text: Clone Current EQ + font: verdana-11px-rounded + tooltip: Copy currently equipped and non-equipped items. + + BotButton + id: default + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + text: Reset fields + font: verdana-11px-rounded + tooltip: Reset all fields to the blank state + +Profile < FlatPanel + size: 160 35 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + margin-left: 10 + text: Profile Name + font: verdana-11px-rounded + + BotTextEdit + id: profileName + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin: 5 + +BossList < FlatPanel + padding-left: 10 + padding-right: 10 + padding-bottom: 10 + + Label + id: title + anchors.verticalCenter: parent.top + anchors.left: parent.left + text: Boss List + font: verdana-11px-rounded + color: #FABD02 + + TextList + id: list + anchors.fill: parent + margin-top: 10 + margin-bottom: 20 + vertical-scrollbar: listScrollBar + padding: 2 + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + + BotTextEdit + id: name + anchors.left: list.left + anchors.top: list.bottom + margin-top: 4 + anchors.right: next.left + + Button + id: add + anchors.right: list.right + anchors.top: list.bottom + margin-top: 3 + height: 21 + text: Add Boss + text-align: center + font: verdana-11px-rounded + tooltip: Creature with given name will be considered as boss. + +EquipWindow < MainWindow + size: 750 350 + text: Equipment Manager + @onEscape: self:hide() + + ListPanel + id: listPanel + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-left: -2 + visible: false + + BossList + id: bossPanel + anchors.fill: prev + visible: true + + VerticalSeparator + anchors.top: parent.top + anchors.bottom: bottomSep.top + margin-bottom: 5 + anchors.left: prev.right + margin-left: 10 + + Profile + id: profileName + anchors.top: parent.top + anchors.left: prev.right + margin-left: 10 + + EQPanel + id: setup + anchors.left: prev.left + anchors.top: prev.bottom + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-top: 10 + + InputPanel + id: inputPanel + anchors.left: prev.right + anchors.top: parent.top + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-left: 5 + + HorizontalSeparator + id: bottomSep + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + + Button + id: bossList + !text: tr('Boss list') + font: cipsoftFont + anchors.left: parent.left + anchors.bottom: parent.bottom + size: 65 21 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/exeta.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/exeta.lua new file mode 100644 index 0000000000..324bad93b4 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/exeta.lua @@ -0,0 +1,27 @@ +local voc = player:getVocation() +if voc == 1 or voc == 11 then + setDefaultTab("Cave") + UI.Separator() + local m = macro(100000, "Exeta when low hp", function() end) + local lastCast = now + onCreatureHealthPercentChange(function(creature, healthPercent) + if m.isOff() then return end + if healthPercent > 15 then return end + if CaveBot.isOff() or TargetBot.isOff() then return end + if modules.game_cooldown.isGroupCooldownIconActive(3) then return end + if creature:getPosition() and getDistanceBetween(pos(),creature:getPosition()) > 1 then return end + if canCast("exeta res") and now - lastCast > 6000 then + say("exeta res") + lastCast = now + end + end) + + macro(500, "ExetaIfPlayer", function() + if CaveBot.isOff() then return end + if getMonsters(1) >= 1 and getPlayers(6) > 0 then + say("exeta res") + delay(6000) + end + end) + UI.Separator() +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/extras.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/extras.lua new file mode 100644 index 0000000000..6eb961396d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/extras.lua @@ -0,0 +1,670 @@ +setDefaultTab("Main") + +-- securing storage namespace +local panelName = "extras" +if not storage[panelName] then + storage[panelName] = {} +end +local settings = storage[panelName] + +-- basic elements +extrasWindow = UI.createWindow('ExtrasWindow', rootWidget) +extrasWindow:hide() +extrasWindow.closeButton.onClick = function(widget) + extrasWindow:hide() +end + +extrasWindow.onGeometryChange = function(widget, old, new) + if old.height == 0 then return end + + settings.height = new.height +end + +local __height = 360 +if settings.height and settings.height > 0 then + __height = settings.height +end + +extrasWindow:setHeight(__height) + +-- available options for dest param +local rightPanel = extrasWindow.content.right +local leftPanel = extrasWindow.content.left + +-- objects made by Kondrah - taken from creature editor, minor changes to adapt +local addCheckBox = function(id, title, defaultValue, dest, tooltip) + local widget = UI.createWidget('ExtrasCheckBox', dest) + widget.onClick = function() + widget:setOn(not widget:isOn()) + settings[id] = widget:isOn() + if id == "checkPlayer" then + local label = rootWidget.newHealer.targetSettings.vocations.title + if not widget:isOn() then + label:setColor("#d9321f") + label:setTooltip("! WARNING ! \nTurn on check players in extras to use this feature!") + else + label:setColor("#dfdfdf") + label:setTooltip("") + end + end + end + widget:setText(title) + widget:setTooltip(tooltip) + if settings[id] == nil then + widget:setOn(defaultValue) + else + widget:setOn(settings[id]) + end + settings[id] = widget:isOn() +end + +local addItem = function(id, title, defaultItem, dest, tooltip) + local widget = UI.createWidget('ExtrasItem', dest) + widget.text:setText(title) + widget.text:setTooltip(tooltip) + widget.item:setTooltip(tooltip) + widget.item:setItemId(settings[id] or defaultItem) + widget.item.onItemChange = function(widget) + settings[id] = widget:getItemId() + end + settings[id] = settings[id] or defaultItem +end + +local addTextEdit = function(id, title, defaultValue, dest, tooltip) + local widget = UI.createWidget('ExtrasTextEdit', dest) + widget.text:setText(title) + widget.textEdit:setText(settings[id] or defaultValue or "") + widget.text:setTooltip(tooltip) + widget.textEdit.onTextChange = function(widget, text) + settings[id] = text + end + settings[id] = settings[id] or defaultValue or "" +end + +local addScrollBar = function(id, title, min, max, defaultValue, dest, tooltip) + local widget = UI.createWidget('ExtrasScrollBar', dest) + widget.text:setTooltip(tooltip) + widget.scroll.onValueChange = function(scroll, value) + widget.text:setText(title .. ": " .. value) + if value == 0 then + value = 1 + end + settings[id] = value + end + widget.scroll:setRange(min, max) + widget.scroll:setTooltip(tooltip) + if max - min > 1000 then + widget.scroll:setStep(100) + elseif max - min > 100 then + widget.scroll:setStep(10) + end + widget.scroll:setValue(settings[id] or defaultValue) + widget.scroll.onValueChange(widget.scroll, widget.scroll:getValue()) +end + +UI.Button("vBot Settings and Scripts", function() + extrasWindow:show() + extrasWindow:raise() + extrasWindow:focus() +end) +UI.Separator() + +---- to maintain order, add options right after another: +--- add object +--- add variables for function (optional) +--- add callback (optional) +--- optionals should be addionaly sandboxed (if true then end) + +addItem("rope", "Rope Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default rope item.") +addItem("shovel", "Shovel Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default shovel item.") +addItem("machete", "Machete Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default machete item.") +addItem("scythe", "Scythe Item", 9596, leftPanel, + "This item will be used in various bot related scripts as default scythe item.") +addCheckBox("pathfinding", "CaveBot Pathfinding", true, leftPanel, + "Cavebot will automatically search for first reachable waypoint after missing 10 goto's.") +addScrollBar("talkDelay", "Global NPC Talk Delay", 0, 2000, 1000, leftPanel, + "Breaks between each talk action in cavebot (time in miliseconds).") +addScrollBar("looting", "Max Loot Distance", 0, 50, 40, leftPanel, + "Every loot corpse futher than set distance (in sqm) will be ignored and forgotten.") +addScrollBar("lootDelay", "Loot Delay", 0, 1000, 200, leftPanel, + "Wait time for loot container to open. Lower value means faster looting. \n WARNING if you are having looting issues(e.g. container is locked in closing/opnening), increase this value.") +addScrollBar("huntRoutes", "Hunting Rounds Limit", 0, 300, 50, leftPanel, + "Round limit for supply check, if character already made more rounds than set, on next supply check will return to city.") +addScrollBar("killUnder", "Kill monsters below", 0, 100, 1, leftPanel, + "Force TargetBot to kill added creatures when they are below set percentage of health - will ignore all other TargetBot settings.") +addScrollBar("gotoMaxDistance", "Max GoTo Distance", 0, 127, 30, leftPanel, + "Maximum distance to next goto waypoint for the bot to try to reach.") +addCheckBox("lootLast", "Start loot from last corpse", true, leftPanel, + "Looting sequence will be reverted and bot will start looting newest bodies.") +addCheckBox("joinBot", "Join TargetBot and CaveBot", false, leftPanel, "Cave and Target tabs will be joined into one.") +addCheckBox("reachable", "Target only pathable mobs", false, leftPanel, "Ignore monsters that can't be reached.") + +addCheckBox("title", "Custom Window Title", true, rightPanel, + "Personalize OTCv8 window name according to character specific.") +if true then + local vocText = "" + + if voc() == 1 or voc() == 11 then + vocText = "- EK" + elseif voc() == 2 or voc() == 12 then + vocText = "- RP" + elseif voc() == 3 or voc() == 13 then + vocText = "- MS" + elseif voc() == 4 or voc() == 14 then + vocText = "- ED" + end + + macro(5000, function() + if settings.title then + if hppercent() > 0 then + g_window.setTitle("Tibia - " .. name() .. " - " .. lvl() .. "lvl " .. vocText) + else + g_window.setTitle("Tibia - " .. name() .. " - DEAD") + end + else + g_window.setTitle("Tibia - " .. name()) + end + end) +end + +addCheckBox("separatePm", "Open PM's in new Window", false, rightPanel, + "PM's will be automatically opened in new tab after receiving one.") +if true then + onTalk(function(name, level, mode, text, channelId, pos) + if mode == 4 and settings.separatePm then + local g_console = modules.game_console + local privateTab = g_console.getTab(name) + if privateTab == nil then + privateTab = g_console.addTab(name, true) + g_console.addPrivateText(g_console.applyMessagePrefixies(name, level, text), + g_console.SpeakTypesSettings['private'], name, false, name) + end + return + end + end) +end + +addTextEdit("useAll", "Use All Hotkey", "space", rightPanel, + "Set hotkey for universal actions - rope, shovel, scythe, use, open doors") +if true then + local useId = { 34847, 1764, 21051, 30823, 6264, 5282, 20453, 20454, 20474, 11708, 11705, + 6257, 6256, 2772, 27260, 2773, 1632, 1633, 1948, 435, 6252, 6253, 5007, 4911, + 1629, 1630, 5108, 5107, 5281, 1968, 435, 1948, 5542, 31116, 31120, 30742, 31115, + 31118, 20474, 5737, 5736, 5734, 5733, 31202, 31228, 31199, 31200, 33262, 30824, + 5125, 5126, 5116, 5117, 8257, 8258, 8255, 8256, 5120, 30777, 30776, 23873, 23877, + 5736, 6264, 31262, 31130, 31129, 6250, 6249, 5122, 30049, 7131, 7132, 7727 } + local shovelId = { 606, 593, 867, 608 } + local ropeId = { 17238, 12202, 12935, 386, 421, 21966, 14238 } + local macheteId = { 2130, 3696 } + local scytheId = { 3653 } + + setDefaultTab("Tools") + -- script + if settings.useAll and settings.useAll:len() > 0 then + hotkey(settings.useAll, function() + if not modules.game_console.isEnabledWASD() then return end + for _, tile in pairs(g_map.getTiles(posz())) do + if distanceFromPlayer(tile:getPosition()) < 2 then + for _, item in pairs(tile:getItems()) do + -- use + if table.find(useId, item:getId()) then + use(item) + return + elseif table.find(shovelId, item:getId()) then + useWith(settings.shovel, item) + return + elseif table.find(ropeId, item:getId()) then + useWith(settings.rope, item) + return + elseif table.find(macheteId, item:getId()) then + useWith(settings.machete, item) + return + elseif table.find(scytheId, item:getId()) then + useWith(settings.scythe, item) + return + end + end + end + end + end) + end +end + + +addCheckBox("timers", "MW & WG Timers", true, rightPanel, "Show times for Magic Walls and Wild Growths.") +if true then + local activeTimers = {} + + onAddThing(function(tile, thing) + if not settings.timers then return end + if not thing:isItem() then + return + end + local timer = 0 + if thing:getId() == 2129 then -- mwall id + timer = 20000 -- mwall time + elseif thing:getId() == 2130 then -- wg id + timer = 45000 -- wg time + 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 settings.timers then return end + if not thing:isItem() then + return + end + if (thing:getId() == 2129 or thing:getId() == 2130) and tile:getGround() then + local pos = tile:getPosition().x .. "," .. tile:getPosition().y .. "," .. tile:getPosition().z + activeTimers[pos] = nil + tile:setTimer(0) + end + end) +end + + +addCheckBox("antiKick", "Anti - Kick", true, rightPanel, "Turn every 10 minutes to prevent kick.") +if true then + macro(600 * 1000, function() + if not settings.antiKick then return end + local dir = player:getDirection() + turn((dir + 1) % 4) + schedule(50, function() turn(dir) end) + end) +end + + +addCheckBox("stake", "Skin Monsters", false, leftPanel, "Automatically skin & stake corpses when cavebot is enabled") +if true then + local knifeBodies = { 4286, 4272, 4173, 4011, 4025, 4047, 4052, 4057, 4062, 4112, 4212, 4321, 4324, 4327, 10352, 10356, + 10360, 10364 } + local stakeBodies = { 4097, 4137, 8738, 18958 } + local fishingBodies = { 9582 } + macro(500, function() + if not CaveBot.isOn() or not settings.stake then return end + for i, tile in ipairs(g_map.getTiles(posz())) do + local item = tile:getTopThing() + if item and item:isContainer() then + if table.find(knifeBodies, item:getId()) and findItem(5908) then + CaveBot.delay(550) + useWith(5908, item) + return + end + if table.find(stakeBodies, item:getId()) and findItem(5942) then + CaveBot.delay(550) + useWith(5942, item) + return + end + if table.find(fishingBodies, item:getId()) and findItem(3483) then + CaveBot.delay(550) + useWith(3483, item) + return + end + end + end + end) +end + + +addCheckBox("oberon", "Auto Reply Oberon", true, rightPanel, "Auto reply to Grand Master Oberon talk minigame.") +if true then + onTalk(function(name, level, mode, text, channelId, pos) + if not settings.oberon then return end + if mode == 34 then + if string.find(text, "world will suffer for") then + say("Are you ever going to fight or do you prefer talking?") + elseif string.find(text, "feet when they see me") then + say("Even before they smell your breath?") + elseif string.find(text, "from this plane") then + say("Too bad you barely exist at all!") + elseif string.find(text, "ESDO LO") then + say("SEHWO ASIMO, TOLIDO ESD") + elseif string.find(text, "will soon rule this world") then + say("Excuse me but I still do not get the message!") + elseif string.find(text, "honourable and formidable") then + say("Then why are we fighting alone right now?") + elseif string.find(text, "appear like a worm") then + say("How appropriate, you look like something worms already got the better of!") + elseif string.find(text, "will be the end of mortal") then + say("Then let me show you the concept of mortality before it!") + elseif string.find(text, "virtues of chivalry") then + say("Dare strike up a Minnesang and you will receive your last accolade!") + end + end + end) +end + + +addCheckBox("autoOpenDoors", "Auto Open Doors", true, rightPanel, "Open doors when trying to step on them.") +if true then + local doorsIds = { 5007, 8265, 1629, 1632, 5129, 6252, 6249, 7715, 7712, 7714, + 7719, 6256, 1669, 1672, 5125, 5115, 5124, 17701, 17710, 1642, + 6260, 5107, 4912, 6251, 5291, 1683, 1696, 1692, 5006, 2179, 5116, + 1632, 11705, 30772, 30774, 6248, 5735, 5732, 5120, 23873, 5736, + 6264, 5122, 30049, 30042, 7727 } + + function checkForDoors(pos) + local tile = g_map.getTile(pos) + if tile then + local useThing = tile:getTopUseThing() + if useThing and table.find(doorsIds, useThing:getId()) then + g_game.use(useThing) + end + end + end + + onKeyPress(function(keys) + local wsadWalking = modules.game_console.isEnabledWASD() + if not settings.autoOpenDoors then return end + local pos = player:getPosition() + if keys == 'Up' or (wsadWalking and keys == 'W') then + pos.y = pos.y - 1 + elseif keys == 'Down' or (wsadWalking and keys == 'S') then + pos.y = pos.y + 1 + elseif keys == 'Left' or (wsadWalking and keys == 'A') then + pos.x = pos.x - 1 + elseif keys == 'Right' or (wsadWalking and keys == 'D') then + pos.x = pos.x + 1 + elseif wsadWalking and keys == "Q" then + pos.y = pos.y - 1 + pos.x = pos.x - 1 + elseif wsadWalking and keys == "E" then + pos.y = pos.y - 1 + pos.x = pos.x + 1 + elseif wsadWalking and keys == "Z" then + pos.y = pos.y + 1 + pos.x = pos.x - 1 + elseif wsadWalking and keys == "C" then + pos.y = pos.y + 1 + pos.x = pos.x + 1 + end + checkForDoors(pos) + end) +end + + +addCheckBox("bless", "Buy bless at login", true, rightPanel, "Say !bless at login.") +if true then + local blessed = false + onTextMessage(function(mode, text) + if not settings.bless then return end + + text = text:lower() + + if text == "you already have all blessings." then + blessed = true + end + end) + if settings.bless then + if player:getBlessings() == 0 then + say("!bless") + schedule(2000, function() + if g_game.getClientVersion() > 1000 then + if not blessed and player:getBlessings() == 0 then + warn("!! Blessings not bought !!") + end + end + end) + end + end +end + + +addCheckBox("reUse", "Keep Crosshair", false, rightPanel, "Keep crosshair after using with item") +if true then + local excluded = { 268, 237, 238, 23373, 266, 236, 239, 7643, 23375, 7642, 23374, 5908, 5942 } + + onUseWith(function(pos, itemId, target, subType) + if settings.reUse and not table.find(excluded, itemId) then + schedule(50, function() + item = findItem(itemId) + if item then + modules.game_interface.startUseWith(item) + end + end) + end + end) +end + + +addCheckBox("suppliesControl", "TargetBot off if low supply", false, leftPanel, + "Turn off TargetBot if either one of supply amount is below 50% of minimum.") +if true then + macro(500, function() + if not settings.suppliesControl then return end + if TargetBot.isOff() then return end + if CaveBot.isOff() then return end + if type(hasSupplies()) == 'table' then + TargetBot.setOff() + end + end) +end + +addCheckBox("holdMwall", "Hold MW/WG", true, rightPanel, + "Mark tiles with below hotkeys to automatically use Magic Wall or Wild Growth") +addTextEdit("holdMwHot", "Magic Wall Hotkey: ", "F5", rightPanel) +addTextEdit("holdWgHot", "Wild Growth Hotkey: ", "F6", rightPanel) +if true then + local hold = 0 + local mwHot + local wgHot + + local candidates = {} + local m = macro(20, function() + mwHot = settings.holdMwHot + wgHot = settings.holdWgHot + + if not settings.holdMwall then return end + if #candidates == 0 then return end + + for i, pos in pairs(candidates) do + local tile = g_map.getTile(pos) + if tile then + if tile:getText():len() == 0 then + table.remove(candidates, i) + end + local rune = tile:getText() == "HOLD MW" and 3180 or tile:getText() == "HOLD WG" and 3156 + if tile:canShoot() and not isInPz() and tile:isWalkable() and tile:getTopUseThing():getId() ~= 2130 then + if math.abs(player:getPosition().x - tile:getPosition().x) < 8 and math.abs(player:getPosition().y - tile:getPosition().y) < 6 then + return useWith(rune, tile:getTopUseThing()) + end + end + end + end + end) + + onRemoveThing(function(tile, thing) + if not settings.holdMwall then return end + if thing:getId() ~= 2129 then return end + if tile:getText():find("HOLD") then + table.insert(candidates, tile:getPosition()) + local rune = tile:getText() == "HOLD MW" and 3180 or tile:getText() == "HOLD WG" and 3156 + if math.abs(player:getPosition().x - tile:getPosition().x) < 8 and math.abs(player:getPosition().y - tile:getPosition().y) < 6 then + return useWith(rune, tile:getTopUseThing()) + end + end + end) + + onAddThing(function(tile, thing) + if not settings.holdMwall then return end + if m.isOff() then return end + if thing:getId() ~= 2129 then return end + if tile:getText():len() > 0 then + table.remove(candidates, table.find(candidates, tile)) + end + end) + + onKeyDown(function(keys) + local wsadWalking = modules.game_console.isEnabledWASD() + if not wsadWalking then return end + if not settings.holdMwall then return end + if m.isOff() then return end + if keys ~= mwHot and keys ~= wgHot then return end + hold = now + + local tile = getTileUnderCursor() + if not tile then return end + + if tile:getText():len() > 0 then + tile:setText("") + else + if keys == mwHot then + tile:setText("HOLD MW") + else + tile:setText("HOLD WG") + end + table.insert(candidates, tile:getPosition()) + end + end) + + onKeyPress(function(keys) + local wsadWalking = modules.game_console.isEnabledWASD() + if not wsadWalking then return end + if not settings.holdMwall then return end + if m.isOff() then return end + if keys ~= mwHot and keys ~= wgHot then return end + + if (hold - now) < -1000 then + candidates = {} + for i, tile in ipairs(g_map.getTiles(posz())) do + local text = tile:getText() + if text:find("HOLD") then + tile:setText("") + end + end + end + end) +end + +addCheckBox("checkPlayer", "Check Players", true, rightPanel, + "Auto look on players and mark level and vocation on character model") +if true then + local found + local function checkPlayers() + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and spec:getText() == "" and spec:getPosition().z == posz() and spec ~= player then + g_game.look(spec) + found = now + end + end + end + if settings.checkPlayer then + schedule(500, function() + checkPlayers() + end) + end + + onPlayerPositionChange(function(x, y) + if not settings.checkPlayer then return end + if x.z ~= y.z then + schedule(20, function() checkPlayers() end) + end + end) + + onCreatureAppear(function(creature) + if not settings.checkPlayer then return end + if creature:isPlayer() and creature:getText() == "" and creature:getPosition().z == posz() and creature ~= player then + g_game.look(creature) + found = now + end + end) + + local regex = [[You see ([^\(]*) \(Level ([0-9]*)\)((?:.)* of the ([\w ]*),|)]] + onTextMessage(function(mode, text) + if not settings.checkPlayer then return end + + local re = regexMatch(text, regex) + if #re ~= 0 then + local name = re[1][2] + local level = re[1][3] + local guild = re[1][5] or "" + + if guild:len() > 10 then + guild = guild:sub(1, 10) -- change to proper (last) values + guild = guild .. "..." + end + local voc + if text:lower():find("sorcerer") then + voc = "MS" + elseif text:lower():find("druid") then + voc = "ED" + elseif text:lower():find("knight") then + voc = "EK" + elseif text:lower():find("paladin") then + voc = "RP" + end + local creature = getCreatureByName(name) + if creature then + creature:setText("\n" .. level .. voc .. "\n" .. guild) + end + if found and now - found < 500 then + modules.game_textmessage.clearMessages() + end + end + end) +end + +addCheckBox("nextBackpack", "Open Next Loot Container", true, leftPanel, + "Auto open next loot container if full - has to have the same ID.") +local function openNextLootContainer() + if not settings.nextBackpack then return end + local containers = getContainers() + local lootCotaniersIds = CaveBot.GetLootContainers() + + for i, container in ipairs(containers) do + local cId = container:getContainerItem():getId() + if containerIsFull(container) then + if table.find(lootCotaniersIds, cId) then + for _, item in ipairs(container:getItems()) do + if item:getId() == cId then + return g_game.open(item, container) + end + end + end + end + end +end +if true then + onContainerOpen(function(container, previousContainer) + schedule(100, function() + openNextLootContainer() + end) + end) + + onAddItem(function(container, slot, item, oldItem) + schedule(100, function() + openNextLootContainer() + end) + end) +end + +addCheckBox("highlightTarget", "Highlight Current Target", true, rightPanel, + "Additionaly hightlight current target with red glow") +if true then + local function forceMarked(creature) + if target() == creature then + creature:setMarked("red") + return schedule(333, function() forceMarked(creature) end) + end + end + + onAttackingCreatureChange(function(newCreature, oldCreature) + if not settings.highlightTarget then return end + if oldCreature then + oldCreature:setMarked('') + end + if newCreature then + forceMarked(newCreature) + end + end) +end diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/extras.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/extras.otui new file mode 100644 index 0000000000..de551d9a5d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/extras.otui @@ -0,0 +1,158 @@ +ExtrasScrollBar < Panel + height: 28 + margin-top: 3 + + UIWidget + 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: 3 + minimum: 0 + maximum: 10 + step: 1 + +ExtrasTextEdit < Panel + height: 40 + margin-top: 7 + + UIWidget + 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 + text-align: center + +ExtrasItem < Panel + height: 34 + margin-top: 7 + margin-left: 25 + margin-right: 25 + + UIWidget + id: text + anchors.left: parent.left + anchors.verticalCenter: next.verticalCenter + + BotItem + id: item + anchors.top: parent.top + anchors.right: parent.right + + +ExtrasCheckBox < BotSwitch + height: 20 + margin-top: 7 + +ExtrasWindow < MainWindow + !text: tr('Extras') + size: 440 360 + padding: 25 + + Label + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: parent.top + text-align: center + text: < CaveBot > + + Label + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: parent.top + text-align: center + text: < Miscellaneous > + + VerticalScrollBar + id: contentScroll + anchors.top: prev.bottom + margin-top: 3 + anchors.right: parent.right + anchors.bottom: separator.top + step: 28 + pixels-scroll: true + margin-right: -10 + margin-top: 5 + margin-bottom: 5 + + ScrollablePanel + id: content + anchors.top: prev.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: separator.top + vertical-scrollbar: contentScroll + margin-bottom: 10 + + Panel + id: left + anchors.top: parent.top + 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: parent.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + margin-top: 5 + margin-left: 10 + margin-right: 10 + layout: + type: verticalBox + fit-children: true + + VerticalSeparator + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.horizontalCenter + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + ResizeBorder + id: bottomResizeBorder + anchors.fill: separator + height: 3 + minimum: 260 + maximum: 600 + margin-left: 3 + margin-right: 3 + background: #ffffff88 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/hold_target.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/hold_target.lua new file mode 100644 index 0000000000..484bf85cbd --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/hold_target.lua @@ -0,0 +1,30 @@ +setDefaultTab("Tools") + +local targetID = nil + +-- escape when attacking will reset hold target +onKeyPress(function(keys) + if keys == "Escape" and targetID then + targetID = nil + end +end) + +macro(100, "Hold Target", function() + -- if attacking then save it as target, but check pos z in case of marking by mistake on other floor + if target() and target():getPosition().z == posz() and not target():isNpc() then + targetID = target():getId() + elseif not target() then + -- there is no saved data, do nothing + if not targetID then return end + + -- look for target + for i, spec in ipairs(getSpectators()) do + local sameFloor = spec:getPosition().z == posz() + local oldTarget = spec:getId() == targetID + + if sameFloor and oldTarget then + attack(spec) + end + end + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/ingame_editor.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/ingame_editor.lua new file mode 100644 index 0000000000..1217b82b1a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/ingame_editor.lua @@ -0,0 +1,23 @@ +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 script editor", function(newText) + UI.MultilineEditorWindow(storage.ingame_hotkeys or "", {title="Hotkeys editor", description="You can add your custom scrupts here"}, function(text) + storage.ingame_hotkeys = text + reload() + end) + end) + + UI.Separator() + + for _, scripts in pairs({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() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/items.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/items.lua new file mode 100644 index 0000000000..312aa37b0d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/items.lua @@ -0,0 +1,1404 @@ +LootItems = { + ["gold coin"] = 1, + ["platinum coin"] = 100, + ["crystal coin"] = 10000, + ["abyss hammer"] = 20000, + ["acorn"] = 10, + ["albino plate"] = 1500, + ["alloy legs"] = 11000, + ["alptramun's toothbrush"] = 270000, + ["amber"] = 20000, + ["amber staff"] = 8000, + ["amber with a bug"] = 41000, + ["amber with a dragonfly"] = 56000, + ["ancient amulet"] = 200, + ["ancient belt buckle"] = 260, + ["ancient coin"] = 350, + ["ancient liche bone"] = 28000, + ["ancient shield"] = 900, + ["ancient stone"] = 200, + ["angel figurine"] = 36000, + ["angelic axe"] = 5000, + ["ankh"] = 100, + ["antlers"] = 50, + ["ape fur"] = 120, + ["apron"] = 1300, + ["arbalest"] = 42000, + ["arcane staff"] = 42000, + ["assassin dagger"] = 20000, + ["axe"] = 7, + ["axe ring"] = 100, + ["baby seal doll"] = 20000, + ["badger boots"] = 7500, + ["badger fur"] = 15, + ["bamboo stick"] = 30, + ["banana sash"] = 55, + ["banana staff"] = 1000, + ["bandana"] = 150, + ["bar of gold"] = 10000, + ["basalt fetish"] = 210, + ["basalt figurine"] = 160, + ["bast skirt"] = 750, + ["bat decoration"] = 2000, + ["bat wing"] = 50, + ["battle axe"] = 80, + ["battle hammer"] = 120, + ["battle shield"] = 95, + ["battle stone"] = 290, + ["batwing hat"] = 8000, + ["bear paw"] = 100, + ["beast's nightmare-cushion"] = 630000, + ["beastslayer axe"] = 1500, + ["bed of nails"] = 500, + ["beer tap"] = 50, + ["beetle carapace"] = 200, + ["beetle necklace"] = 1500, + ["behemoth claw"] = 2000, + ["behemoth trophy"] = 20000, + ["bejeweled ship's telescope"] = 20000, + ["berserk potion"] = 500, + ["berserker"] = 40000, + ["black hood"] = 190, + ["black pearl"] = 280, + ["black shield"] = 800, + ["black wool"] = 300, + ["blacksteel sword"] = 6000, + ["blade of corruption"] = 60000, + ["blazing bone"] = 610, + ["blessed sceptre"] = 40000, + ["blood preservation"] = 320, + ["blood tincture in a vial"] = 360, + ["bloody dwarven beard"] = 110, + ["bloody edge"] = 30000, + ["bloody pincers"] = 100, + ["bloody tears"] = 70000, + ["blue crystal shard"] = 1500, + ["blue crystal splinter"] = 400, + ["blue gem"] = 5000, + ["blue glass plate"] = 60, + ["blue goanna scale"] = 230, + ["blue legs"] = 15000, + ["blue piece of cloth"] = 200, + ["blue robe"] = 10000, + ["blue rose"] = 250, + ["boggy dreads"] = 200, + ["bola"] = 35, + ["bone club"] = 5, + ["bone fetish"] = 150, + ["bone shield"] = 80, + ["bone shoulderplate"] = 150, + ["bone sword"] = 20, + ["bone toothpick"] = 150, + ["bonebeast trophy3"] = 6000, + ["bonebreaker"] = 10000, + ["bonecarving knife"] = 190, + ["bonelord eye"] = 80, + ["bonelord helmet"] = 2200, + ["bonelord shield"] = 1200, + ["bones of zorvorax"] = 10000, + ["bony tail"] = 210, + ["book of necromantic rituals"] = 180, + ["book of prayers"] = 120, + ["book page"] = 640, + ["boots of haste"] = 30000, + ["bow"] = 100, + ["bowl of terror sweat"] = 500, + ["brain head's giant neuron"] = 100000, + ["brain head's left hemisphere"] = 90000, + ["brain head's right hemisphere"] = 50000, + ["brass armor"] = 150, + ["brass helmet"] = 30, + ["brass legs"] = 49, + ["brass shield"] = 25, + ["bright bell"] = 220, + ["bright sword"] = 6000, + ["brimstone fangs"] = 380, + ["brimstone shell"] = 210, + ["broadsword"] = 500, + ["broken crossbow"] = 30, + ["broken draken mail"] = 340, + ["broken gladiator shield"] = 190, + ["broken halberd"] = 100, + ["broken helmet"] = 20, + ["broken key ring"] = 8000, + ["broken longbow"] = 120, + ["broken ring of ending"] = 4000, + ["broken shamanic staff"] = 35, + ["broken slicer"] = 120, + ["broken throwing axe"] = 230, + ["broken visor"] = 1900, + ["bronze amulet"] = 50, + ["brooch of embracement"] = 14000, + ["brown crystal splinter"] = 400, + ["brown piece of cloth"] = 100, + ["brutetamer's staff"] = 1500, + ["buckle"] = 7000, + ["bullseye potion"] = 500, + ["bunch of ripe rice"] = 75, + ["bunch of troll hair"] = 30, + ["bundle of cursed straw"] = 800, + ["butcher's axe"] = 18000, + ["calopteryx cape"] = 15000, + ["capricious heart"] = 2100, + ["capricious robe"] = 1200, + ["carapace shield"] = 32000, + ["carlin sword"] = 118, + ["carniphila seeds"] = 50, + ["carrion worm fang"] = 35, + ["castle shield"] = 5000, + ["cat's paw"] = 2000, + ["cave devourer eyes"] = 550, + ["cave devourer legs"] = 350, + ["cave devourer maw"] = 600, + ["cavebear skull"] = 550, + ["centipede leg"] = 28, + ["ceremonial ankh"] = 20000, + ["chain armor"] = 70, + ["chain bolter"] = 40000, + ["chain helmet"] = 17, + ["chain legs"] = 25, + ["chaos mace"] = 9000, + ["charmer's tiara"] = 900, + ["chasm spawn abdomen"] = 240, + ["chasm spawn head"] = 850, + ["chasm spawn tail"] = 120, + ["cheese cutter"] = 50, + ["cheesy figurine"] = 150, + ["chicken feather"] = 30, + ["chitinous mouth"] = 10000, + ["claw of 'the noxious spawn'"] = 15000, + ["cliff strider claw"] = 800, + ["closed trap"] = 75, + ["club"] = 1, + ["club ring"] = 100, + ["coal"] = 20, + ["coat"] = 1, + ["cobra crest"] = 650, + ["cobra crown"] = 50000, + ["cobra tongue"] = 15, + ["coconut shoes"] = 500, + ["collar of blue plasma"] = 6000, + ["collar of green plasma"] = 6000, + ["collar of red plasma"] = 6000, + ["colourful feather"] = 110, + ["colourful feathers"] = 400, + ["colourful snail shell"] = 250, + ["compass"] = 45, + ["composite hornbow"] = 25000, + ["compound eye"] = 150, + ["condensed energy"] = 260, + ["copper shield"] = 50, + ["coral brooch"] = 750, + ["corrupted flag"] = 700, + ["countess sorrow's frozen tear"] = 50000, + ["cow bell"] = 120, + ["cowtana"] = 2500, + ["crab pincers"] = 35, + ["cracked alabaster vase"] = 180, + ["cranial basher"] = 30000, + ["crawler head plating"] = 210, + ["crawler's essence"] = 3700, + ["crest of the deep seas"] = 10000, + ["crocodile boots"] = 1000, + ["crossbow"] = 120, + ["crowbar"] = 50, + ["crown"] = 2700, + ["crown armor"] = 12000, + ["crown helmet"] = 2500, + ["crown legs"] = 12000, + ["crown shield"] = 8000, + ["cruelty's chest"] = 720000, + ["cruelty's claw"] = 640000, + ["crunor idol"] = 30000, + ["crusader helmet"] = 6000, + ["crystal bone"] = 250, + ["crystal crossbow"] = 35000, + ["crystal mace"] = 12000, + ["crystal necklace"] = 400, + ["crystal of balance"] = 1000, + ["crystal of focus"] = 2000, + ["crystal of power"] = 3000, + ["crystal pedestal"] = 500, + ["crystal ring"] = 250, + ["crystal sword"] = 600, + ["crystal wand"] = 10000, + ["crystalline armor"] = 16000, + ["crystalline spikes"] = 440, + ["crystallized anger"] = 400, + ["cultish mask"] = 280, + ["cultish robe"] = 150, + ["cultish symbol"] = 500, + ["curious matter"] = 430, + ["cursed bone"] = 6000, + ["cursed shoulder spikes"] = 320, + ["cyan crystal fragment"] = 800, + ["cyclops toe"] = 55, + ["cyclops trophy"] = 500, + ["dagger"] = 2, + ["damaged armor plates"] = 280, + ["damaged worm head"] = 8000, + ["damselfly eye"] = 25, + ["damselfly wing"] = 20, + ["dandelion seeds"] = 200, + ["dangerous proto matter"] = 300, + ["daramian mace"] = 110, + ["daramian waraxe"] = 1000, + ["dark armor"] = 400, + ["dark bell"] = 250, + ["dark helmet"] = 250, + ["dark mushroom"] = 100, + ["dark rosary"] = 48, + ["dark shield"] = 400, + ["dead rat"] = 1, + ["dead weight"] = 450, + ["death ring"] = 1000, + ["deepling axe"] = 40000, + ["deepling breaktime snack"] = 90, + ["deepling claw"] = 430, + ["deepling guard belt buckle"] = 230, + ["deepling ridge"] = 360, + ["deepling scales"] = 80, + ["deepling squelcher"] = 7000, + ["deepling staff"] = 4000, + ["deepling warts"] = 180, + ["deeptags"] = 290, + ["deepworm jaws"] = 500, + ["deepworm spike roots"] = 650, + ["deepworm spikes"] = 800, + ["deer trophy3"] = 3000, + ["demon dust"] = 300, + ["demon helmet"] = 40000, + ["demon horn"] = 1000, + ["demon shield"] = 30000, + ["demon trophy"] = 40000, + ["demonbone amulet"] = 32000, + ["demonic essence"] = 1000, + ["demonic finger"] = 1000, + ["demonic skeletal hand"] = 80, + ["demonrage sword"] = 36000, + ["depth calcei"] = 25000, + ["depth galea"] = 35000, + ["depth lorica"] = 30000, + ["depth ocrea"] = 16000, + ["depth scutum"] = 36000, + ["devil helmet"] = 1000, + ["diabolic skull"] = 19000, + ["diamond"] = 15000, + ["diamond sceptre"] = 3000, + ["diremaw brainpan"] = 350, + ["diremaw legs"] = 270, + ["dirty turban"] = 120, + ["disgusting trophy"] = 3000, + ["distorted heart"] = 2100, + ["distorted robe"] = 1200, + ["divine plate"] = 55000, + ["djinn blade"] = 15000, + ["doll"] = 200, + ["double axe"] = 260, + ["doublet"] = 3, + ["downy feather"] = 20, + ["dowser"] = 35, + ["drachaku"] = 10000, + ["dracola's eye"] = 50000, + ["dracoyle statue"] = 5000, + ["dragon blood"] = 700, + ["dragon claw"] = 8000, + ["dragon figurine"] = 45000, + ["dragon hammer"] = 2000, + ["dragon lance"] = 9000, + ["dragon lord trophy"] = 10000, + ["dragon necklace"] = 100, + ["dragon priest's wandtip"] = 175, + ["dragon robe"] = 50000, + ["dragon scale mail"] = 40000, + ["dragon shield"] = 4000, + ["dragon slayer"] = 15000, + ["dragon tongue"] = 550, + ["dragonbone staff"] = 3000, + ["dragon's tail"] = 100, + ["draken boots"] = 40000, + ["draken sulphur"] = 550, + ["draken trophy"] = 15000, + ["draken wristbands"] = 430, + ["drakinata"] = 10000, + ["draptor scales"] = 800, + ["dreaded cleaver"] = 10000, + ["dream essence egg"] = 205, + ["dung ball"] = 130, + ["dwarven armor"] = 30000, + ["dwarven axe"] = 1500, + ["dwarven legs"] = 40000, + ["dwarven ring"] = 100, + ["dwarven shield"] = 100, + ["earflap"] = 40, + ["earth blacksteel sword"] = 6000, + ["earth cranial basher"] = 30000, + ["earth crystal mace"] = 12000, + ["earth dragon slayer"] = 15000, + ["earth headchopper"] = 6000, + ["earth heroic axe"] = 30000, + ["earth knight axe"] = 2000, + ["earth mystic blade"] = 30000, + ["earth orcish maul"] = 6000, + ["earth relic sword"] = 25000, + ["earth spike sword"] = 1000, + ["earth war axe"] = 12000, + ["earth war hammer"] = 1200, + ["ectoplasmic sushi"] = 300, + ["egg of the many"] = 15000, + ["elder bonelord tentacle"] = 150, + ["elite draken mail"] = 50000, + ["elven amulet"] = 100, + ["elven astral observer"] = 90, + ["elven hoof"] = 115, + ["elven scouting glass"] = 50, + ["elvish bow"] = 2000, + ["elvish talisman"] = 45, + ["emerald bangle"] = 800, + ["empty honey glass"] = 270, + ["empty potion flask"] = 5, + ["enchanted chicken wing"] = 20000, + ["energy ball"] = 300, + ["energy blacksteel sword"] = 6000, + ["energy cranial basher"] = 30000, + ["energy crystal mace"] = 12000, + ["energy dragon slayer"] = 15000, + ["energy headchopper"] = 6000, + ["energy heroic axe"] = 30000, + ["energy knight axe"] = 2000, + ["energy mystic blade"] = 30000, + ["energy orcish maul"] = 6000, + ["energy relic sword"] = 25000, + ["energy ring"] = 100, + ["energy spike sword"] = 1000, + ["energy vein"] = 270, + ["energy war axe"] = 12000, + ["energy war hammer"] = 1200, + ["ensouled essence"] = 820, + ["epee"] = 8000, + ["essence of a bad dream"] = 360, + ["ethno coat"] = 200, + ["execowtioner axe"] = 12000, + ["executioner"] = 55000, + ["explorer brooch"] = 50, + ["eye of a deepling"] = 150, + ["eye of a weeper"] = 650, + ["eye of corruption"] = 390, + ["fafnar symbol"] = 950, + ["fairy wings"] = 200, + ["falcon crest"] = 650, + ["feather headdress"] = 850, + ["fern"] = 20, + ["fiery blacksteel sword"] = 6000, + ["fiery cranial basher"] = 30000, + ["fiery crystal mace"] = 12000, + ["fiery dragon slayer"] = 15000, + ["fiery headchopper"] = 6000, + ["fiery heart"] = 375, + ["fiery heroic axe"] = 30000, + ["fiery knight axe"] = 2000, + ["fiery mystic blade"] = 30000, + ["fiery orcish maul"] = 6000, + ["fiery relic sword"] = 25000, + ["fiery spike sword"] = 1000, + ["fiery war axe"] = 12000, + ["fiery war hammer"] = 1200, + ["fig leaf"] = 200, + ["figurine of cruelty"] = 3100000, + ["figurine of greed"] = 2900000, + ["figurine of hatred"] = 2700000, + ["figurine of malice"] = 2800000, + ["figurine of megalomania"] = 5000000, + ["figurine of spite"] = 3000000, + ["fir cone"] = 25, + ["fire axe"] = 8000, + ["fire mushroom"] = 200, + ["fire sword"] = 1000, + ["fish fin"] = 150, + ["fishing rod"] = 40, + ["flask of embalming fluid"] = 30, + ["flask of warrior's sweat"] = 10000, + ["flintstone"] = 800, + ["flower dress"] = 1000, + ["flower wreath"] = 500, + ["focus cape"] = 6000, + ["fox paw"] = 100, + ["frazzle skin"] = 400, + ["frazzle tongue"] = 700, + ["frost giant pelt"] = 160, + ["frosty ear of a troll"] = 30, + ["frosty heart"] = 280, + ["frozen lightning"] = 270, + ["frozen starlight"] = 20000, + ["fur armor"] = 5000, + ["fur boots"] = 2000, + ["fur shred"] = 200, + ["furry club"] = 1000, + ["garlic necklace"] = 50, + ["gauze bandage"] = 90, + ["gear crystal"] = 200, + ["gear wheel"] = 500, + ["gearwheel chain"] = 5000, + ["gemmed figurine"] = 3500, + ["geomancer's robe"] = 80, + ["geomancer's staff"] = 120, + ["ghastly dragon head"] = 700, + ["ghostly tissue"] = 90, + ["ghoul snack"] = 60, + ["giant amethyst"] = 60000, + ["giant crab pincer"] = 950, + ["giant emerald"] = 90000, + ["giant eye"] = 380, + ["giant ruby"] = 70000, + ["giant sapphire"] = 50000, + ["giant shimmering pearl"] = 3000, + ["giant smithhammer"] = 250, + ["giant sword"] = 17000, + ["giant tentacle"] = 10000, + ["giant topaz"] = 80000, + ["girlish hair decoration"] = 30, + ["glacial rod"] = 6500, + ["glacier amulet"] = 1500, + ["glacier kilt"] = 11000, + ["glacier mask"] = 2500, + ["glacier robe"] = 11000, + ["glacier shoes"] = 2500, + ["gland"] = 500, + ["glistening bone"] = 250, + ["glob of acid slime"] = 25, + ["glob of mercury"] = 20, + ["glob of tar"] = 30, + ["gloom wolf fur"] = 70, + ["glooth amulet"] = 2000, + ["glooth axe"] = 1500, + ["glooth blade"] = 1500, + ["glooth cape"] = 7000, + ["glooth club"] = 1500, + ["glooth whip"] = 2500, + ["glorious axe"] = 3000, + ["glowing rune"] = 350, + ["goanna claw"] = 260, + ["goanna meat"] = 190, + ["goat grass"] = 50, + ["goblet of gloom"] = 12000, + ["goblin ear"] = 20, + ["gold ingot"] = 5000, + ["gold nugget"] = 850, + ["gold ring"] = 8000, + ["golden amulet"] = 2000, + ["golden armor"] = 20000, + ["golden brush"] = 250, + ["golden fafnar trophy"] = 10000, + ["golden figurine"] = 3000, + ["golden legs"] = 30000, + ["golden lotus brooch"] = 270, + ["golden mask"] = 38000, + ["golden mug"] = 250, + ["golden sickle"] = 1000, + ["goo shell"] = 4000, + ["goosebump leather"] = 650, + ["grant of arms"] = 950, + ["grasshopper legs"] = 15000, + ["grave flower"] = 25, + ["greed's arm"] = 950000, + ["green bandage"] = 180, + ["green crystal fragment"] = 800, + ["green crystal shard"] = 1500, + ["green crystal splinter"] = 400, + ["green dragon leather"] = 100, + ["green dragon scale"] = 100, + ["green gem"] = 5000, + ["green glass plate"] = 180, + ["green mushroom"] = 100, + ["green piece of cloth"] = 200, + ["greenwood coat"] = 50000, + ["griffin shield"] = 3000, + ["grimace"] = 120000, + ["gruesome fan"] = 15000, + ["guardian axe"] = 9000, + ["guardian boots"] = 35000, + ["guardian halberd"] = 11000, + ["guardian shield"] = 2000, + ["guidebook"] = 200, + ["hailstorm rod"] = 3000, + ["hair of a banshee"] = 350, + ["halberd"] = 400, + ["half-digested piece of meat"] = 55, + ["half-digested stones"] = 40, + ["half-eaten brain"] = 85, + ["ham"] = 4, + ["hammer of wrath"] = 30000, + ["hand"] = 1450, + ["hand axe"] = 4, + ["hardened bone"] = 70, + ["harpoon of a giant snail"] = 15000, + ["hatched rorc egg"] = 30, + ["hatchet"] = 25, + ["haunted blade"] = 8000, + ["haunted piece of wood"] = 115, + ["hazardous heart"] = 5000, + ["hazardous robe"] = 3000, + ["head"] = 3500, + ["headchopper"] = 6000, + ["heat core"] = 10000, + ["heaven blossom"] = 50, + ["heavy mace"] = 50000, + ["heavy machete"] = 90, + ["heavy trident"] = 2000, + ["hellhound slobber"] = 500, + ["hellspawn tail"] = 475, + ["helmet of the lost"] = 2000, + ["hemp rope"] = 350, + ["heroic axe"] = 30000, + ["hexagonal ruby"] = 30000, + ["hibiscus dress"] = 3000, + ["hideous chunk"] = 510, + ["hieroglyph banner"] = 500, + ["high guard flag"] = 550, + ["high guard shoulderplates"] = 130, + ["hive bow"] = 28000, + ["hive scythe"] = 17000, + ["hollow stampor hoof"] = 400, + ["holy ash"] = 160, + ["holy orchid"] = 90, + ["honeycomb"] = 40, + ["horn"] = 300, + ["horn of kalyassa"] = 10000, + ["horoscope"] = 40, + ["horseman helmet"] = 280, + ["huge chunk of crude iron"] = 15000, + ["huge shell"] = 15000, + ["huge spiky snail shell"] = 8000, + ["humongous chunk"] = 540, + ["hunter's quiver"] = 80, + ["hunting spear"] = 25, + ["hydra egg"] = 500, + ["hydra head"] = 600, + ["ice flower"] = 370, + ["ice rapier"] = 1000, + ["icy blacksteel sword"] = 6000, + ["icy cranial basher"] = 30000, + ["icy crystal mace"] = 12000, + ["icy dragon slayer"] = 15000, + ["icy headchopper"] = 6000, + ["icy heroic axe"] = 30000, + ["icy knight axe"] = 2000, + ["icy mystic blade"] = 30000, + ["icy orcish maul"] = 6000, + ["icy relic sword"] = 25000, + ["icy spike sword"] = 1000, + ["icy war axe"] = 12000, + ["icy war hammer"] = 1200, + ["incantation notes"] = 90, + ["infernal heart"] = 2100, + ["infernal robe"] = 1200, + ["inkwell"] = 8, + ["instable proto matter"] = 300, + ["iron helmet"] = 150, + ["iron ore"] = 500, + ["ivory carving"] = 300, + ["ivory comb"] = 8000, + ["izcandar's snow globe"] = 180000, + ["izcandar's sundial"] = 225000, + ["jacket"] = 1, + ["jade hammer"] = 25000, + ["jade hat"] = 9000, + ["jagged sickle"] = 150000, + ["jaws"] = 3900, + ["jewelled belt"] = 180, + ["katana"] = 35, + ["katex' blood"] = 210, + ["key to the drowned library"] = 330, + ["knight armor"] = 5000, + ["knight axe"] = 2000, + ["knight legs"] = 5000, + ["kollos shell"] = 420, + ["kongra's shoulderpad"] = 100, + ["krimhorn helmet"] = 200, + ["lamassu hoof"] = 330, + ["lamassu horn"] = 240, + ["lancer beetle shell"] = 80, + ["lancet"] = 90, + ["lavos armor"] = 16000, + ["leaf legs"] = 500, + ["leather armor"] = 12, + ["leather boots"] = 2, + ["leather harness"] = 750, + ["leather helmet"] = 4, + ["leather legs"] = 9, + ["legion helmet"] = 22, + ["legionnaire flags"] = 500, + ["leopard armor"] = 300, + ["leviathan's amulet"] = 3000, + ["life crystal"] = 50, + ["life preserver"] = 300, + ["life ring"] = 50, + ["light shovel"] = 300, + ["lightning boots"] = 2500, + ["lightning headband"] = 2500, + ["lightning legs"] = 11000, + ["lightning pendant"] = 1500, + ["lightning robe"] = 11000, + ["lion cloak patch"] = 190, + ["lion crest"] = 270, + ["lion figurine"] = 10000, + ["lion seal"] = 210, + ["lion trophy3"] = 3000, + ["lion's mane"] = 60, + ["little bowl of myrrh"] = 500, + ["lizard essence"] = 300, + ["lizard heart"] = 530, + ["lizard leather"] = 150, + ["lizard scale"] = 120, + ["lizard trophy"] = 8000, + ["longing eyes"] = 8000, + ["longsword"] = 51, + ["lost basher's spike"] = 280, + ["lost bracers"] = 140, + ["lost husher's staff"] = 250, + ["lost soul"] = 120, + ["luminescent crystal pickaxe"] = 50, + ["luminous orb"] = 1000, + ["lump of dirt"] = 10, + ["lump of earth"] = 130, + ["lunar staff"] = 5000, + ["mace"] = 30, + ["machete"] = 6, + ["mad froth"] = 80, + ["magic light wand"] = 35, + ["magic plate armor"] = 90000, + ["magic sulphur"] = 8000, + ["magma amulet"] = 1500, + ["magma boots"] = 2500, + ["magma clump"] = 570, + ["magma coat"] = 11000, + ["magma legs"] = 11000, + ["magma monocle"] = 2500, + ["malice's horn"] = 620000, + ["malice's spine"] = 850000, + ["malofur's lunchbox"] = 240000, + ["mammoth fur cape"] = 6000, + ["mammoth fur shorts"] = 850, + ["mammoth tusk"] = 100, + ["mammoth whopper"] = 300, + ["mantassin tail"] = 280, + ["manticore ear"] = 310, + ["manticore tail"] = 220, + ["marlin trophy"] = 5000, + ["marsh stalker beak"] = 65, + ["marsh stalker feather"] = 50, + ["mastermind potion"] = 500, + ["mastermind shield"] = 50000, + ["maxilla"] = 250, + ["maxxenius head"] = 500000, + ["meat"] = 2, + ["meat hammer"] = 60, + ["medal of valiance"] = 410000, + ["medusa shield"] = 9000, + ["megalomania's essence"] = 1900000, + ["megalomania's skull"] = 1500000, + ["mercenary sword"] = 12000, + ["metal bat"] = 9000, + ["metal spats"] = 2000, + ["metal spike"] = 320, + ["might ring"] = 250, + ["milk churn"] = 100, + ["mind stone"] = 100, + ["mino lance"] = 7000, + ["mino shield"] = 3000, + ["minotaur horn"] = 75, + ["minotaur leather"] = 80, + ["minotaur trophy"] = 500, + ["miraculum"] = 60, + ["mirror"] = 10, + ["model ship"] = 1000, + ["modified crossbow"] = 10000, + ["mooh'tah plate"] = 6000, + ["moohtant cudgel"] = 14000, + ["moonlight rod"] = 200, + ["moonstone"] = 13000, + ["morbid tapestry"] = 30000, + ["morgaroth's heart"] = 15000, + ["morning star"] = 100, + ["mould heart"] = 2100, + ["mould robe"] = 1200, + ["mr. punish's handcuffs"] = 50000, + ["muck rod"] = 6000, + ["mucus plug"] = 500, + ["mutated bat ear"] = 420, + ["mutated flesh"] = 50, + ["mutated rat tail"] = 150, + ["mycological bow"] = 35000, + ["mysterious fetish"] = 50, + ["mystic blade"] = 30000, + ["mystic turban"] = 150, + ["mystical hourglass"] = 700, + ["naginata"] = 2000, + ["necklace of the deep"] = 3000, + ["necromantic robe"] = 250, + ["necrotic rod"] = 1000, + ["nettle blossom"] = 75, + ["nettle spit"] = 25, + ["nightmare blade"] = 35000, + ["noble amulet"] = 430000, + ["noble armor"] = 900, + ["noble axe"] = 10000, + ["noble cape"] = 425000, + ["noble turban"] = 430, + ["norse shield"] = 1500, + ["northwind rod"] = 1500, + ["nose ring"] = 2000, + ["obsidian lance"] = 500, + ["odd organ"] = 410, + ["ogre ear stud"] = 180, + ["ogre nose ring"] = 210, + ["old parchment"] = 500, + ["onyx chip"] = 500, + ["onyx flail"] = 22000, + ["onyx pendant"] = 3500, + ["opal"] = 500, + ["orange mushroom"] = 150, + ["orb"] = 750, + ["orc leather"] = 30, + ["orc tooth"] = 150, + ["orc trophy3"] = 1000, + ["orcish axe"] = 350, + ["orcish gear"] = 85, + ["orcish maul"] = 6000, + ["orichalcum pearl"] = 40, + ["oriental shoes"] = 15000, + ["ornamented axe"] = 20000, + ["ornamented shield"] = 1500, + ["ornate chestplate"] = 60000, + ["ornate crossbow"] = 12000, + ["ornate legs"] = 40000, + ["ornate locket"] = 18000, + ["ornate mace"] = 42000, + ["ornate shield"] = 42000, + ["orshabaal's brain"] = 12000, + ["pair of hellflayer horns"] = 1300, + ["pair of iron fists"] = 4000, + ["pair of old bracers"] = 500, + ["paladin armor"] = 15000, + ["pale worm's scalp"] = 489000, + ["panda teddy"] = 30000, + ["panther head"] = 750, + ["panther paw"] = 300, + ["patch of fine cloth"] = 1350, + ["patched boots"] = 2000, + ["peacock feather fan"] = 350, + ["pelvis bone"] = 30, + ["perfect behemoth fang"] = 250, + ["pet pig"] = 1500, + ["petrified scream"] = 250, + ["phantasmal hair"] = 500, + ["pharaoh banner"] = 1000, + ["pharaoh sword"] = 23000, + ["phoenix shield"] = 16000, + ["pick"] = 15, + ["piece of archer armor"] = 20, + ["piece of crocodile leather"] = 15, + ["piece of dead brain"] = 420, + ["piece of draconian steel"] = 3000, + ["piece of hell steel"] = 500, + ["piece of hellfire armor"] = 550, + ["piece of massacre's shell"] = 50000, + ["piece of royal steel"] = 10000, + ["piece of scarab shell"] = 45, + ["piece of swampling wood"] = 30, + ["piece of warrior armor"] = 50, + ["pieces of magic chalk"] = 210, + ["pig foot"] = 10, + ["pile of grave earth"] = 25, + ["pirate boots"] = 3000, + ["pirate hat"] = 1000, + ["pirate knee breeches"] = 200, + ["pirate shirt"] = 500, + ["pirate voodoo doll"] = 500, + ["plagueroot offshoot"] = 280000, + ["plasma pearls"] = 250, + ["plasmatic lightning"] = 270, + ["plate armor"] = 400, + ["plate legs"] = 115, + ["plate shield"] = 45, + ["platinum amulet"] = 2500, + ["poison dagger"] = 50, + ["poison gland"] = 210, + ["poison spider shell"] = 10, + ["poisonous slime"] = 50, + ["polar bear paw"] = 30, + ["pool of chitinous glue"] = 480, + ["porcelain mask"] = 2000, + ["powder herb"] = 10, + ["power ring"] = 50, + ["prismatic quartz"] = 450, + ["pristine worm head"] = 15000, + ["protection amulet"] = 100, + ["protective charm"] = 60, + ["pulverized ore"] = 400, + ["purified soul"] = 530, + ["purple robe"] = 110, + ["purple tome"] = 2000, + ["quara bone"] = 500, + ["quara eye"] = 350, + ["quara pincers"] = 410, + ["quara tentacle"] = 140, + ["queen's sceptre"] = 20000, + ["quill"] = 1100, + ["rabbit's foot"] = 50, + ["ragnir helmet"] = 400, + ["rainbow quartz"] = 500, + ["rapier"] = 5, + ["rare earth"] = 80, + ["ratana"] = 500, + ["ravenous circlet"] = 220000, + ["red crystal fragment"] = 800, + ["red dragon leather"] = 200, + ["red dragon scale"] = 200, + ["red gem"] = 1000, + ["red goanna scale"] = 270, + ["red hair dye"] = 40, + ["red lantern"] = 250, + ["red piece of cloth"] = 300, + ["red tome"] = 2000, + ["relic sword"] = 25000, + ["rhino hide"] = 175, + ["rhino horn"] = 265, + ["rhino horn carving"] = 300, + ["rift bow"] = 45000, + ["rift crossbow"] = 45000, + ["rift lance"] = 30000, + ["rift shield"] = 50000, + ["ring of blue plasma"] = 8000, + ["ring of green plasma"] = 8000, + ["ring of healing"] = 100, + ["ring of red plasma"] = 8000, + ["ring of the sky"] = 30000, + ["ripper lance"] = 500, + ["rod"] = 2200, + ["roots"] = 1200, + ["rope"] = 15, + ["rope belt"] = 66, + ["rorc egg"] = 120, + ["rorc feather"] = 70, + ["rotten heart"] = 74000, + ["rotten piece of cloth"] = 30, + ["royal axe"] = 40000, + ["royal helmet"] = 30000, + ["royal tapestry"] = 1000, + ["rubber cap"] = 11000, + ["ruby necklace"] = 2000, + ["runed sword"] = 45000, + ["ruthless axe"] = 45000, + ["sabre"] = 12, + ["sabretooth"] = 400, + ["sacred tree amulet"] = 3000, + ["safety pin"] = 120, + ["sai"] = 16500, + ["salamander shield"] = 280, + ["sample of monster blood"] = 250, + ["sandcrawler shell"] = 20, + ["sapphire hammer"] = 7000, + ["scale armor"] = 75, + ["scale of corruption"] = 680, + ["scale of gelidrazah"] = 10000, + ["scarab amulet"] = 200, + ["scarab pincers"] = 280, + ["scarab shield"] = 2000, + ["scimitar"] = 150, + ["scorpion tail"] = 25, + ["scroll of heroic deeds"] = 230, + ["scythe"] = 10, + ["scythe leg"] = 450, + ["sea horse figurine"] = 42000, + ["sea serpent scale"] = 520, + ["sea serpent trophy"] = 10000, + ["seeds"] = 150, + ["sentinel shield"] = 120, + ["serpent sword"] = 900, + ["shadow herb"] = 20, + ["shadow sceptre"] = 10000, + ["shaggy tail"] = 25, + ["shamanic hood"] = 45, + ["shamanic talisman"] = 200, + ["shard"] = 2000, + ["shimmering beetles"] = 150, + ["shiny stone"] = 500, + ["shockwave amulet"] = 3000, + ["short sword"] = 10, + ["shovel"] = 8, + ["sickle"] = 3, + ["sight of surrender's eye"] = 3000, + ["signet ring"] = 480000, + ["silencer claws"] = 390, + ["silencer resonating chamber"] = 600, + ["silken bookmark"] = 1300, + ["silkweaver bow"] = 12000, + ["silky fur"] = 35, + ["silver amulet"] = 50, + ["silver brooch"] = 150, + ["silver dagger"] = 500, + ["silver fafnar trophy"] = 1000, + ["silver hand mirror"] = 10000, + ["simple dress"] = 50, + ["single human eye"] = 1000, + ["skeleton decoration"] = 3000, + ["skull belt"] = 80, + ["skull coin"] = 12000, + ["skull fetish"] = 250, + ["skull helmet"] = 40000, + ["skull of ratha"] = 250, + ["skull shatterer"] = 170, + ["skull staff"] = 6000, + ["skullcracker armor"] = 18000, + ["skunk tail"] = 50, + ["slime mould"] = 175, + ["slimy leg"] = 4500, + ["sling herb"] = 10, + ["small amethyst"] = 200, + ["small axe"] = 5, + ["small diamond"] = 300, + ["small emerald"] = 250, + ["small enchanted amethyst"] = 200, + ["small enchanted emerald"] = 250, + ["small enchanted ruby"] = 250, + ["small enchanted sapphire"] = 250, + ["small energy ball"] = 250, + ["small flask of eyedrops"] = 95, + ["small notebook"] = 480, + ["small oil lamp"] = 150, + ["small pitchfork"] = 70, + ["small ruby"] = 250, + ["small sapphire"] = 250, + ["small topaz"] = 200, + ["snake skin"] = 400, + ["snakebite rod"] = 100, + ["sniper gloves"] = 2000, + ["soldier helmet"] = 16, + ["solid rage"] = 310, + ["some grimeleech wings"] = 1200, + ["soul orb"] = 25, + ["soul stone"] = 6000, + ["souleater trophy"] = 7500, + ["spark sphere"] = 350, + ["sparkion claw"] = 290, + ["sparkion legs"] = 310, + ["sparkion stings"] = 280, + ["sparkion tail"] = 300, + ["spear"] = 3, + ["spectral gold nugget"] = 500, + ["spectral silver nugget"] = 250, + ["spellsinger's seal"] = 280, + ["spellweaver's robe"] = 12000, + ["sphinx feather"] = 470, + ["sphinx tiara"] = 360, + ["spider fangs"] = 10, + ["spider silk"] = 100, + ["spidris mandible"] = 450, + ["spike shield"] = 250, + ["spike sword"] = 240, + ["spiked iron ball"] = 100, + ["spiked squelcher"] = 5000, + ["spiky club"] = 300, + ["spirit cloak"] = 350, + ["spirit container"] = 40000, + ["spite's spirit"] = 840000, + ["spitter nose"] = 340, + ["spooky blue eye"] = 95, + ["spool of yarn"] = 1000, + ["springsprout rod"] = 3600, + ["srezz' eye"] = 300, + ["stampor horn"] = 280, + ["stampor talons"] = 150, + ["star amulet"] = 500, + ["star herb"] = 15, + ["statue of abyssador"] = 4000, + ["statue of deathstrike"] = 3000, + ["statue of devovorga"] = 1500, + ["statue of gnomevil"] = 2000, + ["stealth ring"] = 200, + ["steel boots"] = 30000, + ["steel helmet"] = 293, + ["steel shield"] = 80, + ["stone herb"] = 20, + ["stone nose"] = 590, + ["stone skin amulet"] = 500, + ["stone wing"] = 120, + ["stonerefiner's skull"] = 100, + ["strand of medusa hair"] = 600, + ["strange helmet"] = 500, + ["strange proto matter"] = 300, + ["strange symbol"] = 200, + ["strange talisman"] = 30, + ["striped fur"] = 50, + ["studded armor"] = 25, + ["studded club"] = 10, + ["studded helmet"] = 20, + ["studded legs"] = 15, + ["studded shield"] = 16, + ["stuffed dragon"] = 6000, + ["sulphurous stone"] = 100, + ["swamp grass"] = 20, + ["swamplair armor"] = 16000, + ["swampling club"] = 40, + ["swampling moss"] = 20, + ["swarmer antenna"] = 130, + ["sword"] = 25, + ["sword ring"] = 100, + ["tail of corruption"] = 240, + ["talon"] = 320, + ["tarantula egg"] = 80, + ["tarnished rhino figurine"] = 320, + ["tattered piece of robe"] = 120, + ["taurus mace"] = 500, + ["telescope eye"] = 1600, + ["tempest shield"] = 35000, + ["templar scytheblade"] = 200, + ["tentacle piece"] = 5000, + ["terra amulet"] = 1500, + ["terra boots"] = 2500, + ["terra hood"] = 2500, + ["terra legs"] = 11000, + ["terra mantle"] = 11000, + ["terra rod"] = 2000, + ["terramite eggs"] = 50, + ["terramite legs"] = 60, + ["terramite shell"] = 170, + ["terrorbird beak"] = 95, + ["thaian sword"] = 16000, + ["the avenger"] = 42000, + ["the handmaiden's protector"] = 50000, + ["the imperor's trident"] = 50000, + ["the ironworker"] = 50000, + ["the justice seeker"] = 40000, + ["the plasmother's remains"] = 50000, + ["thick fur"] = 150, + ["thorn"] = 100, + ["throwing knife"] = 2, + ["tiger eye"] = 350, + ["time ring"] = 100, + ["titan axe"] = 4000, + ["token of love"] = 440000, + ["tooth file"] = 60, + ["tooth of tazhadur"] = 10000, + ["torn shirt"] = 250, + ["tortoise shield"] = 150, + ["tower shield"] = 8000, + ["trapped bad dream monster"] = 900, + ["trashed draken boots"] = 40000, + ["tribal mask"] = 250, + ["troll green"] = 25, + ["trollroot"] = 50, + ["trophy of jaul"] = 4000, + ["trophy of obujos"] = 3000, + ["trophy of tanjis"] = 2000, + ["tunnel tyrant head"] = 500, + ["tunnel tyrant shell"] = 700, + ["turtle shell"] = 90, + ["tusk"] = 100, + ["tusk shield"] = 850, + ["twiceslicer"] = 28000, + ["twin hooks"] = 500, + ["two handed sword"] = 450, + ["undead heart"] = 200, + ["underworld rod"] = 4400, + ["unholy bone"] = 480, + ["unholy book"] = 30000, + ["unicorn figurine"] = 50000, + ["urmahlullu's mane"] = 490000, + ["urmahlullu's paw"] = 245000, + ["urmahlullu's tail"] = 210000, + ["utua's poison"] = 230, + ["vampire dust"] = 100, + ["vampire shield"] = 15000, + ["vampire teeth"] = 275, + ["vampire's cape chain"] = 150, + ["veal"] = 40, + ["vein of ore"] = 330, + ["velvet tapestry"] = 800, + ["venison"] = 55, + ["vexclaw talon"] = 1100, + ["vial"] = 5, + ["vial of hatred"] = 737000, + ["vibrant heart"] = 2100, + ["vibrant robe"] = 1200, + ["viking helmet"] = 66, + ["viking shield"] = 85, + ["vile axe"] = 30000, + ["violet crystal shard"] = 1500, + ["violet gem"] = 10000, + ["violet glass plate"] = 2150, + ["volatile proto matter"] = 300, + ["voodoo doll"] = 400, + ["wailing widow's necklace"] = 3000, + ["walnut"] = 80, + ["wand of cosmic energy"] = 2000, + ["wand of decay"] = 1000, + ["wand of defiance"] = 6500, + ["wand of draconia"] = 1500, + ["wand of dragonbreath"] = 200, + ["wand of everblazing"] = 6000, + ["wand of inferno"] = 3000, + ["wand of starstorm"] = 3600, + ["wand of voodoo"] = 4400, + ["wand of vortex"] = 100, + ["war axe"] = 12000, + ["war crystal"] = 460, + ["war hammer"] = 470, + ["war horn"] = 8000, + ["warmaster's wristguards"] = 200, + ["warrior helmet"] = 5000, + ["warrior's axe"] = 11000, + ["warrior's shield"] = 9000, + ["warwolf fur"] = 30, + ["waspoid claw"] = 320, + ["waspoid wing"] = 190, + ["watch"] = 6, + ["watermelon tourmaline"] = 30000, + ["weaver's wandtip"] = 250, + ["wedding ring"] = 100, + ["werebadger claws"] = 160, + ["werebadger skull"] = 185, + ["werebadger trophy"] = 9000, + ["werebear fur"] = 185, + ["werebear skull"] = 195, + ["werebear trophy"] = 11000, + ["wereboar hooves"] = 175, + ["wereboar loincloth"] = 1500, + ["wereboar trophy"] = 10000, + ["wereboar tusks"] = 165, + ["werefox tail"] = 200, + ["werefox trophy"] = 9000, + ["werehyaena nose"] = 220, + ["werehyaena talisman"] = 350, + ["werehyaena trophy"] = 12000, + ["werewolf amulet"] = 3000, + ["werewolf fangs"] = 180, + ["werewolf fur"] = 380, + ["white deer antlers"] = 400, + ["white deer skin"] = 245, + ["white gem"] = 12000, + ["white pearl"] = 160, + ["white piece of cloth"] = 100, + ["white silk flower"] = 9000, + ["widow's mandibles"] = 110, + ["wild flowers"] = 120, + ["wimp tooth chain"] = 120, + ["windborn colossus armor"] = 50000, + ["winged tail"] = 800, + ["winter wolf fur"] = 20, + ["witch broom"] = 60, + ["witch hat"] = 5000, + ["withered pauldrons"] = 850, + ["withered scalp"] = 900, + ["wolf paw"] = 70, + ["wolf tooth chain"] = 100, + ["wolf trophy"] = 3000, + ["wood"] = 5, + ["wood mushroom"] = 15, + ["wooden hammer"] = 15, + ["wooden shield"] = 5, + ["wool"] = 15, + ["writhing brain"] = 370000, + ["writhing heart"] = 185000, + ["wyrm scale"] = 400, + ["wyvern fang"] = 1500, + ["wyvern talisman"] = 265, + ["yellow gem"] = 1000, + ["yellow piece of cloth"] = 150, + ["yielocks"] = 600, + ["yielowax"] = 600, + ["yirkas' egg"] = 280, + ["young lich worm"] = 25000, + ["zaoan armor"] = 14000, + ["zaoan halberd"] = 500, + ["zaoan helmet"] = 45000, + ["zaoan legs"] = 14000, + ["zaoan robe"] = 12000, + ["zaoan shoes"] = 5000, + ["zaoan sword"] = 30000, + ["zaogun flag"] = 600, + ["zaogun shoulderplates"] = 150, + + -- 12.70 + ["carnisylvan bark"] = 230, + ["carnisylvan finger"] = 250, + ["human teeth"] = 2000, + ["abomination's eye"] = 650000, + ["abomination's tail"] = 700000, + ["abomination's tongue"] = 950000, + ["afflicted strider head"] = 900, + ["afflicted strider worms"] = 500, + ["bashmu fang"] = 600, + ["bashmu feather"] = 350, + ["bashmu tongue"] = 400, + ["blemished spawn abdomen"] = 550, + ["blemished spawn head"] = 800, + ["blemished spawn tail"] = 1000, + ["brainstealer's brain"] = 300000, + ["brainstealer's brainwave"] = 440000, + ["brainstealer's tissue"] = 240000, + ["cave chimera head"] = 1200, + ["cave chimera leg"] = 650, + ["curl of hair"] = 320000, + ["eyeless devourer legs"] = 650, + ["eyeless devourer maw"] = 420, + ["eyeless devourer tongue"] = 900, + ["girtablilu warrior carapace"] = 520, + ["lavafungus head"] = 900, + ["lavafungus ring"] = 390, + ["lavaworm jaws"] = 1100, + ["lavaworm spike roots"] = 600, + ["lavaworm spikes"] = 750, + ["old girtablilu carapace"] = 570, + ["old royal diary"] = 220000, + ["scorpion charm"] = 620, + ["tremendous tyrant head"] = 930, + ["tremendous tyrant shell"] = 740, + ["varnished diremaw brainpan"] = 750, + ["varnished diremaw legs"] = 670, + ["streaked devourer eyes"] = 500, + ["streaked devourer legs"] = 600, + ["streaked devourer maw"] = 400, + ["eldritch crystal"] = 48000, + + -- supplies + ["mana potion"] = 56, + ["strong mana potion"] = 93, + ["great mana potion"] = 144, + ["ultimate mana potion"] = 438, + ["health potion"] = 50, + ["strong health potion"] = 115, + ["great health potion"] = 225, + ["ultimate health potion"] = 379, + ["supreme health potion"] = 625, + ["great spirit potion"] = 228, + ["ultimate spirit potion"] = 438, + -- runes + ["cure poison rune"] = 65, + ["poison field rune"] = 21, + ["fire field rune"] = 28, + ["intense healing rune"] = 95, + ["destroy field rune"] = 15, + ["energy field rune"] = 38, + ["stalagmite rune"] = 12, + ["heavy magic missile rune"] = 12, + ["disintegrate rune"] = 26, + ["ultimate healing rune"] = 175, + ["poison bomb rune"] = 85, + ["animate death rune"] = 375, + ["chameleon rune"] = 210, + ["fireball rune"] = 30, + ["holy missile rune"] = 16, + ["icicle rune"] = 30, + ["stone shower rune"] = 37, + ["thunderstorm rune"] = 47, + ["avalanche rune"] = 57, + ["great fireball rune"] = 57, + ["convince creature rune"] = 80, + ["fire bomb rune"] = 147, + ["poison wall rune"] = 52, + ["explosion rune"] = 31, + ["fire wall rune"] = 61, + ["soulfire rune"] = 46, + ["wild growth rune"] = 160, + ["magic wall rune"] = 116, + ["energy wall rune"] = 85, + ["energy bomb rune"] = 203, + ["sudden death rune"] = 135, + ["paralyse rune"] = 700, + + ["envenomed arrow"] = 12, + ["flaming arrow"] = 5, + ["flash arrow"] = 5, + ["onyx arrow"] = 7, + ["poison arrow"] = 1, + ["shiver arrow"] = 5, + ["simple arrow"] = 2, + ["sniper arrow"] = 5, + ["tarsal arrow"] = 6, + ["arrow"] = 3, + ["burst arrow"] = 0, + ["crystalline arrow"] = 20, + ["diamond arrow"] = 100, + ["earth arrow"] = 5, + ["infernal bolt"] = 12, + ["piercing bolt"] = 5, + ["power bolt"] = 7, + ["prismatic bolt"] = 20, + ["spectral bolt"] = 70, + ["vortex bolt"] = 6, + ["bolt"] = 4, + ["drill bolt"] = 12, +} + +WasteItems = { + -- supplies + ["mana potion"] = 268, + ["strong mana potion"] = 237, + ["great mana potion"] = 238, + ["ultimate mana potion"] = 23373, + ["health potion"] = 266, + ["strong health potion"] = 236, + ["great health potion"] = 239, + ["ultimate health potion"] = 7643, + ["supreme health potion"] = 23375, + ["great spirit potion"] = 7642, + ["ultimate spirit potion"] = 23374, + -- ammo + ["envenomed arrow"] = 16143, + ["flaming arrow"] = 763, + ["flash arrow"] = 761, + ["onyx arrow"] = 7365, + ["poison arrow"] = 3448, + ["shiver arrow"] = 762, + ["simple arrow"] = 21470, + ["sniper arrow"] = 7364, + ["tarsal arrow"] = 14251, + ["arrow"] = 3447, + ["burst arrow"] = 3449, + ["crystalline arrow"] = 15793, + ["diamond arrow"] = 35901, + ["earth arrow"] = 774, + ["infernal bolt"] = 6528, + ["piercing bolt"] = 7363, + ["power bolt"] = 3450, + ["prismatic bolt"] = 16141, + ["spectral bolt"] = 35902, + ["vortex bolt"] = 14252, + ["bolt"] = 3446, + ["drill bolt"] = 16142, + -- runes + ["cure poison rune"] = 3153, + ["poison field rune"] = 3172, + ["fire field rune"] = 3188, + ["intense healing rune"] = 3152, + ["destroy field rune"] = 3148, + ["energy field rune"] = 3164, + ["stalagmite rune"] = 3179, + ["heavy magic missile rune"] = 3198, + ["disintegrate rune"] = 3197, + ["ultimate healing rune"] = 3160, + ["poison bomb rune"] = 3173, + ["animate death rune"] = 3203, + ["chameleon rune"] = 3178, + ["fireball rune"] = 3189, + ["holy missile rune"] = 3182, + ["icicle rune"] = 3158, + ["stone shower rune"] = 3175, + ["thunderstorm rune"] = 3202, + ["avalanche rune"] = 3161, + ["great fireball rune"] = 3191, + ["convince creature rune"] = 3177, + ["fire bomb rune"] = 3192, + ["poison wall rune"] = 3176, + ["explosion rune"] = 3200, + ["fire wall rune"] = 3190, + ["soulfire rune"] = 3195, + ["wild growth rune"] = 3156, + ["magic wall rune"] = 3180, + ["energy wall rune"] = 3166, + ["energy bomb rune"] = 3149, + ["sudden death rune"] = 3155, + ["paralyse rune"] = 3165 +} \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/main.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/main.lua new file mode 100644 index 0000000000..4f01c3dcfa --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/main.lua @@ -0,0 +1,40 @@ +local version = "4.8" +local currentVersion +local available = false + +storage.checkVersion = storage.checkVersion or 0 + +-- check max once per 12hours +if os.time() > storage.checkVersion + (12 * 60 * 60) then + + storage.checkVersion = os.time() + + HTTP.get("https://raw.githubusercontent.com/Vithrax/vBot/main/vBot/version.txt", function(data, err) + if err then + warn("[vBot updater]: Unable to check version:\n" .. err) + return + end + + currentVersion = data + available = true + end) + +end + +UI.Label("vBot v".. version .." \n Vithrax#5814") +UI.Button("Official OTCv8 Discord!", function() g_platform.openUrl("https://discord.gg/yhqBE4A") end) +UI.Separator() + +schedule(5000, function() + + if not available then return end + if currentVersion ~= version then + + UI.Separator() + UI.Label("New vBot is available for download! v"..currentVersion) + UI.Button("Go to vBot GitHub Page", function() g_platform.openUrl("https://github.com/Vithrax/vBot") end) + UI.Separator() + + end + +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/new_cavebot_lib.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/new_cavebot_lib.lua new file mode 100644 index 0000000000..3434a999f9 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/new_cavebot_lib.lua @@ -0,0 +1,518 @@ +CaveBot = {} -- global namespace + +------------------------------------------------------------------- +-- CaveBot lib 1.0 +-- Contains a universal set of functions to be used in CaveBot + +----------------------[[ basic assumption ]]----------------------- +-- in general, functions cannot be slowed from within, only externally, by event calls, delays etc. +-- considering that and the fact that there is no while loop, every function return action +-- thus, functions will need to be verified outside themselfs or by another function +-- overall tips to creating extension: +-- - functions return action(nil) or true(done) +-- - extensions are controlled by retries var +------------------------------------------------------------------- + +-- local variables, constants and functions, used by global functions +local LOCKERS_LIST = {3497, 3498, 3499, 3500} +local LOCKER_ACCESSTILE_MODIFIERS = { + [3497] = {0,-1}, + [3498] = {1,0}, + [3499] = {0,1}, + [3500] = {-1,0} +} + +local function CaveBotConfigParse() + local name = storage["_configs"]["targetbot_configs"]["selected"] + if not name then + return warn("[vBot] Please create a new TargetBot config and reset bot") + end + local file = configDir .. "/targetbot_configs/" .. name .. ".json" + local data = g_resources.readFileContents(file) + return Config.parse(data)['looting'] +end + +local function getNearTiles(pos) + if type(pos) ~= "table" then + pos = pos:getPosition() + end + + local tiles = {} + local dirs = { + {-1, 1}, + {0, 1}, + {1, 1}, + {-1, 0}, + {1, 0}, + {-1, -1}, + {0, -1}, + {1, -1} + } + for i = 1, #dirs do + local tile = + g_map.getTile( + { + x = pos.x - dirs[i][1], + y = pos.y - dirs[i][2], + z = pos.z + } + ) + if tile then + table.insert(tiles, tile) + end + end + + return tiles +end + +-- ##################### -- +-- [[ Information class ]] -- +-- ##################### -- + +--- global variable to reflect current CaveBot status +CaveBot.Status = "waiting" + +--- Parses config and extracts loot list. +-- @return table +function CaveBot.GetLootItems() + local t = CaveBotConfigParse() and CaveBotConfigParse()["items"] or nil + + local returnTable = {} + if type(t) == "table" then + for i, item in pairs(t) do + table.insert(returnTable, item["id"]) + end + end + + return returnTable +end + + +--- Checks whether player has any visible items to be stashed +-- @return boolean +function CaveBot.HasLootItems() + for _, container in pairs(getContainers()) do + local name = container:getName():lower() + if not name:find("depot") and not name:find("your inbox") then + for _, item in pairs(container:getItems()) do + local id = item:getId() + if table.find(CaveBot.GetLootItems(), id) then + return true + end + end + end + end +end + +--- Parses config and extracts loot containers. +-- @return table +function CaveBot.GetLootContainers() + local t = CaveBotConfigParse() and CaveBotConfigParse()["containers"] or nil + + local returnTable = {} + if type(t) == "table" then + for i, container in pairs(t) do + table.insert(returnTable, container["id"]) + end + end + + return returnTable +end + +--- Information about open containers. +-- @param amount is boolean +-- @return table or integer +function CaveBot.GetOpenedLootContainers(containerTable) + local containers = CaveBot.GetLootContainers() + + local t = {} + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + if table.find(containers, containerId) then + table.insert(t, container) + end + end + + return containerTable and t or #t +end + +--- Some actions needs to be additionally slowed down in case of high ping. +-- Maximum at 2000ms in case of lag spike. +-- @param multiplayer is integer +-- @return void +function CaveBot.PingDelay(multiplayer) + multiplayer = multiplayer or 1 + if ping() and ping() > 150 then -- in most cases ping above 150 affects CaveBot + local value = math.min(ping() * multiplayer, 2000) + return delay(value) + end +end + +-- ##################### -- +-- [[ Container class ]] -- +-- ##################### -- + +--- Closes any loot container that is open. +-- @return void or boolean +function CaveBot.CloseLootContainer() + local containers = CaveBot.GetLootContainers() + + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + if table.find(containers, containerId) then + return g_game.close(container) + end + end + + return true +end + +function CaveBot.CloseAllLootContainers() + local containers = CaveBot.GetLootContainers() + + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + if table.find(containers, containerId) then + g_game.close(container) + end + end + + return true +end + +--- Opens any loot container that isn't already opened. +-- @return void or boolean +function CaveBot.OpenLootContainer() + local containers = CaveBot.GetLootContainers() + + local t = {} + for i, container in pairs(getContainers()) do + local containerId = container:getContainerItem():getId() + table.insert(t, containerId) + end + + for _, container in pairs(getContainers()) do + for _, item in pairs(container:getItems()) do + local id = item:getId() + if table.find(containers, id) and not table.find(t, id) then + return g_game.open(item) + end + end + end + + return true +end + +-- ##################### -- +-- [[[ Position class ]] -- +-- ##################### -- + +--- Compares distance between player position and given pos. +-- @param position is table +-- @param distance is integer +-- @return boolean +function CaveBot.MatchPosition(position, distance) + local pPos = player:getPosition() + distance = distance or 1 + return getDistanceBetween(pPos, position) <= distance +end + +--- Stripped down to take less space. +-- Use only to safe position, like pz movement or reaching npc. +-- Needs to be called between 200-500ms to achieve fluid movement. +-- @param position is table +-- @param distance is integer +-- @return void +function CaveBot.GoTo(position, precision) + if not precision then + precision = 3 + end + return CaveBot.walkTo(position, 20, {ignoreCreatures = true, precision = precision}) +end + +--- Finds position of npc by name and reaches its position. +-- @return void(acion) or boolean +function CaveBot.ReachNPC(name) + name = name:lower() + + local npc = nil + for i, spec in pairs(getSpectators()) do + if spec:isNpc() and spec:getName():lower() == name then + npc = spec + end + end + + if not CaveBot.MatchPosition(npc:getPosition(), 3) then + CaveBot.GoTo(npc:getPosition()) + else + return true + end +end + +-- ##################### -- +-- [[[[ Depot class ]]]] -- +-- ##################### -- + +--- Reaches closest locker. +-- @return void(acion) or boolean + +local depositerLockerTarget = nil +local depositerLockerReachRetries = 0 +function CaveBot.ReachDepot() + local pPos = player:getPosition() + local tiles = getNearTiles(player:getPosition()) + + for i, tile in pairs(tiles) do + for i, item in pairs(tile:getItems()) do + if table.find(LOCKERS_LIST, item:getId()) then + depositerLockerTarget = nil + depositerLockerReachRetries = 0 + return true -- if near locker already then return function + end + end + end + + if depositerLockerReachRetries > 20 then + depositerLockerTarget = nil + depositerLockerReachRetries = 0 + end + + local candidates = {} + + if not depositerLockerTarget or distanceFromPlayer(depositerLockerTarget, pPos) > 12 then + for i, tile in pairs(g_map.getTiles(posz())) do + local tPos = tile:getPosition() + for i, item in pairs(tile:getItems()) do + if table.find(LOCKERS_LIST, item:getId()) then + local lockerTilePos = tile:getPosition() + lockerTilePos.x = lockerTilePos.x + LOCKER_ACCESSTILE_MODIFIERS[item:getId()][1] + lockerTilePos.y = lockerTilePos.y + LOCKER_ACCESSTILE_MODIFIERS[item:getId()][2] + local lockerTile = g_map.getTile(lockerTilePos) + if not lockerTile:hasCreature() then + if findPath(pos(), tPos, 20, {ignoreNonPathable = false, precision = 1, ignoreCreatures = true}) then + local distance = getDistanceBetween(tPos, pPos) + table.insert(candidates, {pos=tPos, dist=distance}) + end + end + end + end + end + + if #candidates > 1 then + table.sort(candidates, function(a,b) return a.dist < b.dist end) + end + end + + depositerLockerTarget = depositerLockerTarget or candidates[1].pos + + if depositerLockerTarget then + if not CaveBot.MatchPosition(depositerLockerTarget) then + depositerLockerReachRetries = depositerLockerReachRetries + 1 + return CaveBot.GoTo(depositerLockerTarget, 1) + else + depositerLockerReachRetries = 0 + depositerLockerTarget = nil + return true + end + end +end + +--- Opens locker item. +-- @return void(acion) or boolean +function CaveBot.OpenLocker() + local pPos = player:getPosition() + local tiles = getNearTiles(player:getPosition()) + + local locker = getContainerByName("Locker") + if not locker then + for i, tile in pairs(tiles) do + for i, item in pairs(tile:getItems()) do + if table.find(LOCKERS_LIST, item:getId()) then + local topThing = tile:getTopUseThing() + if not topThing:isNotMoveable() then + g_game.move(topThing, pPos, topThing:getCount()) + else + return g_game.open(item) + end + end + end + end + else + return true + end +end + +--- Opens depot chest. +-- @return void(acion) or boolean +function CaveBot.OpenDepotChest() + local depot = getContainerByName("Depot chest") + if not depot then + local locker = getContainerByName("Locker") + if not locker then + return CaveBot.OpenLocker() + end + for i, item in pairs(locker:getItems()) do + if item:getId() == 3502 then + return g_game.open(item, locker) + end + end + else + return true + end +end + +--- Opens inbox inside locker. +-- @return void(acion) or boolean +function CaveBot.OpenInbox() + local inbox = getContainerByName("Your inbox") + if not inbox then + local locker = getContainerByName("Locker") + if not locker then + return CaveBot.OpenLocker() + end + for i, item in pairs(locker:getItems()) do + if item:getId() == 12902 then + return g_game.open(item) + end + end + else + return true + end +end + +--- Opens depot box of given number. +-- @param index is integer +-- @return void or boolean +function CaveBot.OpenDepotBox(index) + local depot = getContainerByName("Depot chest") + if not depot then + return CaveBot.ReachAndOpenDepot() + end + + local foundParent = false + for i, container in pairs(getContainers()) do + if container:getName():lower():find("depot box") then + foundParent = container + break + end + end + if foundParent then return true end + + for i, container in pairs(depot:getItems()) do + if i == index then + return g_game.open(container) + end + end +end + +--- Reaches and opens depot. +-- Combined for shorthand usage. +-- @return boolean whether succeed to reach and open depot +function CaveBot.ReachAndOpenDepot() + if CaveBot.ReachDepot() and CaveBot.OpenDepotChest() then + return true + end + return false +end + +--- Reaches and opens imbox. +-- Combined for shorthand usage. +-- @return boolean whether succeed to reach and open depot +function CaveBot.ReachAndOpenInbox() + if CaveBot.ReachDepot() and CaveBot.OpenInbox() then + return true + end + return false +end + +--- Stripped down function to stash item. +-- @param item is object +-- @param index is integer +-- @param destination is object +-- @return void +function CaveBot.StashItem(item, index, destination) + destination = destination or getContainerByName("Depot chest") + if not destination then return false end + + return g_game.move(item, destination:getSlotPosition(index), item:getCount()) +end + +--- Withdraws item from depot chest or mail inbox. +-- main function for depositer/withdrawer +-- @param id is integer +-- @param amount is integer +-- @param fromDepot is boolean or integer +-- @param destination is object +-- @return void +function CaveBot.WithdrawItem(id, amount, fromDepot, destination) + if destination and type(destination) == "string" then + destination = getContainerByName(destination) + end + local itemCount = itemAmount(id) + local depot + for i, container in pairs(getContainers()) do + if container:getName():lower():find("depot box") or container:getName():lower():find("your inbox") then + depot = container + break + end + end + if not depot then + if fromDepot then + if not CaveBot.OpenDepotBox(fromDepot) then return end + else + return CaveBot.ReachAndOpenInbox() + end + return + end + if not destination then + for i, container in pairs(getContainers()) do + if container:getCapacity() > #container:getItems() and not string.find(container:getName():lower(), "quiver") and not string.find(container:getName():lower(), "depot") and not string.find(container:getName():lower(), "loot") and not string.find(container:getName():lower(), "inbox") then + destination = container + end + end + end + + if itemCount >= amount then + return true + end + + local toMove = amount - itemCount + for i, item in pairs(depot:getItems()) do + if item:getId() == id then + return g_game.move(item, destination:getSlotPosition(destination:getItemsCount()), math.min(toMove, item:getCount())) + end + end +end + +-- ##################### -- +-- [[[[[ Talk class ]]]] -- +-- ##################### -- + +--- Controlled by event caller. +-- Simple way to build npc conversations instead of multiline overcopied code. +-- @return void +function CaveBot.Conversation(...) + local expressions = {...} + local delay = storage.extras.talkDelay or 1000 + + local talkDelay = 0 + for i, expr in ipairs(expressions) do + schedule(talkDelay, function() NPC.say(expr) end) + talkDelay = talkDelay + delay + end +end + +--- Says hi trade to NPC. +-- Used as shorthand to open NPC trade window. +-- @return void +function CaveBot.OpenNpcTrade() + return CaveBot.Conversation("hi", "trade") +end + +--- Says hi destination yes to NPC. +-- Used as shorthand to travel. +-- @param destination is string +-- @return void +function CaveBot.Travel(destination) + return CaveBot.Conversation("hi", destination, "yes") +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.lua new file mode 100644 index 0000000000..ca799c9b0a --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.lua @@ -0,0 +1,456 @@ +setDefaultTab("Main") +local panelName = "newHealer" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('Friend Healer') + + Button + id: edit + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + +]]) +ui:setId(panelName) + +-- validate current settings +if not storage[panelName] or not storage[panelName].priorities then + storage[panelName] = nil +end + +if not storage[panelName] then + storage[panelName] = { + enabled = false, + customPlayers = {}, + vocations = {}, + groups = {}, + priorities = { + + {name="Custom Spell", enabled=false, custom=true}, + {name="Exura Gran Sio", enabled=true, strong = true}, + {name="Exura Sio", enabled=true, normal = true}, + {name="Exura Gran Mas Res", enabled=true, area = true}, + {name="Health Item", enabled=true, health=true}, + {name="Mana Item", enabled=true, mana=true} + + }, + settings = { + + {type="HealItem", text="Mana Item ", value=268}, + {type="HealScroll", text="Item Range: ", value=6}, + {type="HealItem", text="Health Item ", value=3160}, + {type="HealScroll", text="Mas Res Players: ", value=2}, + {type="HealScroll", text="Heal Friend at: ", value=80}, + {type="HealScroll", text="Use Gran Sio at: ", value=80}, + {type="HealScroll", text="Min Player HP%: ", value=80}, + {type="HealScroll", text="Min Player MP%: ", value=50}, + + }, + conditions = { + knights = true, + paladins = true, + druids = false, + sorcerers = false, + party = true, + guild = false, + botserver = false, + friends = false + } + } +end + +local config = storage[panelName] +local healerWindow = UI.createWindow('FriendHealer') +healerWindow:hide() +healerWindow:setId(panelName) + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) + config.enabled = not config.enabled + widget:setOn(config.enabled) +end + +ui.edit.onClick = function() + healerWindow:show() + healerWindow:raise() + healerWindow:focus() +end + +local conditions = healerWindow.conditions +local targetSettings = healerWindow.targetSettings +local customList = healerWindow.customList +local priority = healerWindow.priority + +-- customList +-- create entries on the list +for name, health in pairs(config.customPlayers) do + local widget = UI.createWidget("HealerPlayerEntry", customList.playerList.list) + widget.remove.onClick = function() + config.customPlayers[name] = nil + widget:destroy() + end + widget:setText("["..health.."%] "..name) +end + +customList.playerList.onDoubleClick = function() + customList.playerList:hide() +end + +local function clearFields() + customList.addPanel.name:setText("friend name") + customList.addPanel.health:setText("1") + customList.playerList:show() +end + +local function capitalFistLetter(str) + return (string.gsub(str, "^%l", string.upper)) + end + +customList.addPanel.add.onClick = function() + local name = "" + local words = string.split(customList.addPanel.name:getText(), " ") + local health = tonumber(customList.addPanel.health:getText()) + for i, word in ipairs(words) do + name = name .. " " .. capitalFistLetter(word) + end + + if not health then + clearFields() + return warn("[Friend Healer] Please enter health percent value!") + end + + if name:len() == 0 or name:lower() == "friend name" then + clearFields() + return warn("[Friend Healer] Please enter friend name to be added!") + end + + if config.customPlayers[name] or config.customPlayers[name:lower()] then + clearFields() + return warn("[Friend Healer] Player already added to custom list.") + else + config.customPlayers[name] = health + local widget = UI.createWidget("HealerPlayerEntry", customList.playerList.list) + widget.remove.onClick = function() + config.customPlayers[name] = nil + widget:destroy() + end + widget:setText("["..health.."%] "..name) + end + + clearFields() +end + +local function validate(widget, category) + local list = widget:getParent() + local label = list:getParent().title + -- 1 - priorities | 2 - vocation + category = category or 0 + + if category == 2 and not storage.extras.checkPlayer then + label:setColor("#d9321f") + label:setTooltip("! WARNING ! \nTurn on check players in extras to use this feature!") + return + else + label:setColor("#dfdfdf") + label:setTooltip("") + end + + local checked = false + for i, child in ipairs(list:getChildren()) do + if category == 1 and child.enabled:isChecked() or child:isChecked() then + checked = true + end + end + + if not checked then + label:setColor("#d9321f") + label:setTooltip("! WARNING ! \nNo category selected!") + else + label:setColor("#dfdfdf") + label:setTooltip("") + end +end +-- targetSettings +targetSettings.vocations.box.knights:setChecked(config.conditions.knights) +targetSettings.vocations.box.knights.onClick = function(widget) + config.conditions.knights = not config.conditions.knights + widget:setChecked(config.conditions.knights) + validate(widget, 2) +end + +targetSettings.vocations.box.paladins:setChecked(config.conditions.paladins) +targetSettings.vocations.box.paladins.onClick = function(widget) + config.conditions.paladins = not config.conditions.paladins + widget:setChecked(config.conditions.paladins) + validate(widget, 2) +end + +targetSettings.vocations.box.druids:setChecked(config.conditions.druids) +targetSettings.vocations.box.druids.onClick = function(widget) + config.conditions.druids = not config.conditions.druids + widget:setChecked(config.conditions.druids) + validate(widget, 2) +end + +targetSettings.vocations.box.sorcerers:setChecked(config.conditions.sorcerers) +targetSettings.vocations.box.sorcerers.onClick = function(widget) + config.conditions.sorcerers = not config.conditions.sorcerers + widget:setChecked(config.conditions.sorcerers) + validate(widget, 2) +end + +targetSettings.groups.box.friends:setChecked(config.conditions.friends) +targetSettings.groups.box.friends.onClick = function(widget) + config.conditions.friends = not config.conditions.friends + widget:setChecked(config.conditions.friends) + validate(widget) +end + +targetSettings.groups.box.party:setChecked(config.conditions.party) +targetSettings.groups.box.party.onClick = function(widget) + config.conditions.party = not config.conditions.party + widget:setChecked(config.conditions.party) + validate(widget) +end + +targetSettings.groups.box.guild:setChecked(config.conditions.guild) +targetSettings.groups.box.guild.onClick = function(widget) + config.conditions.guild = not config.conditions.guild + widget:setChecked(config.conditions.guild) + validate(widget) +end + +targetSettings.groups.box.botserver:setChecked(config.conditions.botserver) +targetSettings.groups.box.botserver.onClick = function(widget) + config.conditions.botserver = not config.conditions.botserver + widget:setChecked(config.conditions.botserver) + validate(widget) +end + +validate(targetSettings.vocations.box.knights) +validate(targetSettings.groups.box.friends) +validate(targetSettings.vocations.box.sorcerers, 2) + +-- conditions +for i, setting in ipairs(config.settings) do + local widget = UI.createWidget(setting.type, conditions.box) + local text = setting.text + local val = setting.value + widget.text:setText(text) + + if setting.type == "HealScroll" then + widget.text:setText(widget.text:getText()..val) + if not (text:find("Range") or text:find("Mas Res")) then + widget.text:setText(widget.text:getText().."%") + end + widget.scroll:setValue(val) + widget.scroll.onValueChange = function(scroll, value) + setting.value = value + widget.text:setText(text..value) + if not (text:find("Range") or text:find("Mas Res")) then + widget.text:setText(widget.text:getText().."%") + end + end + if text:find("Range") or text:find("Mas Res") then + widget.scroll:setMaximum(10) + end + else + widget.item:setItemId(val) + widget.item:setShowCount(false) + widget.item.onItemChange = function(widget) + setting.value = widget:getItemId() + end + end +end + + + +-- priority and toggles +local function setCrementalButtons() + for i, child in ipairs(priority.list:getChildren()) do + if i == 1 then + child.increment:disable() + elseif i == 6 then + child.decrement:disable() + else + child.increment:enable() + child.decrement:enable() + end + end +end + +for i, action in ipairs(config.priorities) do + local widget = UI.createWidget("PriorityEntry", priority.list) + + widget:setText(action.name) + widget.increment.onClick = function() + local index = priority.list:getChildIndex(widget) + local table = config.priorities + + priority.list:moveChildToIndex(widget, index-1) + table[index], table[index-1] = table[index-1], table[index] + setCrementalButtons() + end + widget.decrement.onClick = function() + local index = priority.list:getChildIndex(widget) + local table = config.priorities + + priority.list:moveChildToIndex(widget, index+1) + table[index], table[index+1] = table[index+1], table[index] + setCrementalButtons() + end + widget.enabled:setChecked(action.enabled) + widget:setColor(action.enabled and "#98BF64" or "#dfdfdf") + widget.enabled.onClick = function() + action.enabled = not action.enabled + widget:setColor(action.enabled and "#98BF64" or "#dfdfdf") + widget.enabled:setChecked(action.enabled) + validate(widget, 1) + end + if action.custom then + widget.onDoubleClick = function() + local window = modules.client_textedit.show(widget, {title = "Custom Spell", description = "Enter below formula for a custom healing spell"}) + schedule(50, function() + window:raise() + window:focus() + end) + end + widget.onTextChange = function(widget,text) + action.name = text + end + widget:setTooltip("Double click to set spell formula.") + end + + if i == #config.priorities then + validate(widget, 1) + setCrementalButtons() + end +end + +local lastItemUse = now +local function friendHealerAction(spec, targetsInRange) + local name = spec:getName() + local health = spec:getHealthPercent() + local mana = spec:getManaPercent() + local dist = distanceFromPlayer(spec:getPosition()) + targetsInRange = targetsInRange or 0 + + local masResAmount = config.settings[4].value + local itemRange = config.settings[2].value + local healItem = config.settings[3].value + local manaItem = config.settings[1].value + local normalHeal = config.customPlayers[name] or config.settings[5].value + local strongHeal = config.customPlayers[name] and normalHeal/2 or config.settings[6].value + + for i, action in ipairs(config.priorities) do + if action.enabled then + if action.area and masResAmount <= targetsInRange and canCast("exura gran mas res") then + return say("exura gran mas res") + end + if action.mana and findItem(manaItem) and mana <= normalHeal and dist <= itemRange and now - lastItemUse > 1000 then + lastItemUse = now + return useWith(manaItem, spec) + end + if action.health and findItem(healItem) and health <= normalHeal and dist <= itemRange and now - lastItemUse > 1000 then + lastItemUse = now + return useWith(healItem, spec) + end + if action.strong and health <= strongHeal and not modules.game_cooldown.isCooldownIconActive(101) then + return say('exura gran sio "'..name) + end + if (action.normal or action.custom) and health <= normalHeal and canCast('exura sio "'..name) then + return say('exura sio "'..name) + end + end + end +end + +local function isCandidate(spec) + if spec:isLocalPlayer() or not spec:isPlayer() then + return nil + end + if not spec:canShoot() then + return false + end + + local curHp = spec:getHealthPercent() + if curHp == 100 or (config.customPlayers[name] and curHp > config.customPlayers[name]) then + return false + end + + local specText = spec:getText() + local name = spec:getName() + -- check players is enabled and spectator already verified + if storage.extras.checkPlayer and specText:len() > 0 then + if specText:find("EK") and not config.conditions.knights or + specText:find("RP") and not config.conditions.paladins or + specText:find("ED") and not config.conditions.druids or + specText:find("MS") and not config.conditions.sorcerers then + if not config.customPlayers[name] then + return nil + end + end + end + + local okParty = config.conditions.party and spec:isPartyMember() + local okFriend = config.conditions.friends and isFriend(spec) + local okGuild = config.conditions.guild and spec:getEmblem() == 1 + local okBotServer = config.conditions.botserver and vBot.BotServerMembers[spec:getName()] + + if not (okParty or okFriend or okGuild or okBotServer) then + return nil + end + + local health = config.customPlayers[name] and curHp/2 or curHp + local dist = distanceFromPlayer(spec:getPosition()) + + return health, dist +end + +macro(100, function() + if not config.enabled then return end + if modules.game_cooldown.isGroupCooldownIconActive(2) then return end + + local minHp = config.settings[7].value + local minMp = config.settings[8].value + + local healTarget = {creature=nil, hp=100} + local inMasResRange = 0 + + -- check basic + if hppercent() <= minHp or manapercent() <= minMp then return end + + -- get all spectators + local spectators = getSpectators() + + -- main check + local healtR + for i, spec in ipairs(spectators) do + local health, dist = isCandidate(spec) + --mas san + if dist then + inMasResRange = dist <= 3 and inMasResRange+1 or inMasResRange + + -- best target + if health < healTarget.hp then + healTarget = {creature = spec, hp = health} + end + end + end + + -- action + if healTarget.creature then + return friendHealerAction(healTarget.creature, inMasResRange) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.otui new file mode 100644 index 0000000000..2eb55cc4de --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/new_healer.otui @@ -0,0 +1,413 @@ +CategoryCheckBox < CheckBox + font: verdana-11px-rounded + margin-top: 3 + + $checked: + color: #98BF64 + +HealScroll < Panel + + ToolTipLabel + id: text + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-align: center + text: test + + HorizontalScrollBar + id: scroll + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + minimum: 0 + maximum: 100 + step: 1 + +HealItem < Panel + + BotItem + id: item + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + size: 34 34 + + ToolTipLabel + id: text + anchors.fill: parent + anchors.left: prev.right + margin-left: 8 + text-wrap: true + text-align: left + +ToolTipLabel < UIWidget + font: verdana-11px-rounded + color: #dfdfdf + height: 14 + text-align: center + +HealerPlayerEntry < Label + background-color: alpha + text-offset: 5 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + anchors.right: parent.right + margin-right: 2 + anchors.verticalCenter: parent.verticalCenter + size: 15 15 + margin-right: 15 + text: X + tooltip: Remove player from the list + +PriorityEntry < ToolTipLabel + background-color: alpha + text-offset: 18 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + CheckBox + id: enabled + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + size: 15 15 + margin-top: 2 + margin-left: 3 + + Button + id: increment + anchors.right: parent.right + margin-right: 2 + anchors.verticalCenter: parent.verticalCenter + size: 14 14 + text: + + tooltip: Increase Priority + + Button + id: decrement + anchors.right: prev.left + margin-right: 2 + anchors.verticalCenter: parent.verticalCenter + size: 14 14 + text: - + tooltip: Decrease Priority + +TargetSettings < Panel + size: 280 125 + padding: 3 + image-source: /images/ui/window + image-border: 6 + + Label + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Heal Target Settings + + Groups + id: groups + anchors.top: prev.bottom + margin-top: 8 + anchors.left: parent.left + margin-left: 9 + + Vocations + id: vocations + anchors.left: prev.right + margin-left: 5 + anchors.verticalCenter: prev.verticalCenter + +Groups < FlatPanel + size: 150 90 + padding: 3 + padding-top: 5 + + ToolTipLabel + id: title + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + text: Groups + tooltip: Players added in custom list will always be in scope + + HorizontalSeparator + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + + Panel + id: box + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + padding: 2 + layout: + type: verticalBox + + CategoryCheckBox + id: friends + text: Friends + + CategoryCheckBox + id: party + text: Party Members + + CategoryCheckBox + id: guild + text: Guild Members + + CategoryCheckBox + id: botserver + text: BotServer Members + +Vocations < FlatPanel + size: 100 90 + padding: 3 + padding-top: 5 + + ToolTipLabel + id: title + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Vocations + + HorizontalSeparator + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + + Panel + id: box + anchors.top: prev.bottom + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + padding: 2 + + layout: + type: verticalBox + + CategoryCheckBox + id: knights + text: Knights + + CategoryCheckBox + id: paladins + text: Paladins + + CategoryCheckBox + id: druids + text: Druids + + CategoryCheckBox + id: sorcerers + text: Sorcerers + +Priority < Panel + size: 190 123 + padding: 6 + padding-top: 3 + image-source: /images/ui/window + image-border: 6 + + ToolTipLabel + id: title + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Priority & Toggles + + TextList + id: list + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + fit-children: true + padding-top: 1 + +AddPlayer < FlatPanel + padding: 5 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + font: verdana-11px-rounded + text: Add Player to Custom List + text-align: center + text-wrap: true + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + + SpinBox + id: health + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 20 + width: 50 + minimum: 1 + maximum: 99 + step: 1 + focusable: true + text-align: center + + Label + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + font: verdana-11px-rounded + text: %HP - heal if below + + TextEdit + id: name + anchors.top: health.bottom + margin-top: 5 + anchors.left: health.left + anchors.right: parent.right + font: verdana-11px-rounded + text-align: center + text: friend name + + Button + id: add + anchors.left: health.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + font: verdana-11px-rounded + text: Add Player + +PlayerList < Panel + + TextList + id: list + anchors.fill: parent + fit-children: true + padding-top: 2 + vertical-scrollbar: listScrollBar + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + +CustomList < Panel + size: 190 172 + padding: 6 + padding-top: 3 + image-source: /images/ui/window + image-border: 6 + + ToolTipLabel + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Custom Player List + tooltip: Double click on the list below to add new player. + + AddPlayer + id: addPanel + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + PlayerList + id: playerList + anchors.fill: prev + +Conditions < Panel + size: 280 170 + padding: 3 + image-source: /images/ui/window + image-border: 6 + + Label + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + font: verdana-11px-rounded + text: Player Conditions + + Panel + id: box + anchors.fill: parent + margin-top: 16 + padding: 5 + padding-top: 3 + layout: + type: grid + cell-size: 128 31 + cell-spacing: 5 + num-columns: 2 + +FriendHealer < MainWindow + !text: tr('Friend Healer') + size: 512 390 + padding-top: 30 + @onEscape: self:hide() + + Conditions + id: conditions + anchors.top: parent.top + anchors.right: parent.right + + TargetSettings + id: targetSettings + anchors.top: prev.bottom + margin-top: 10 + anchors.left: prev.left + + Priority + id: priority + anchors.top: parent.top + anchors.left: parent.left + + CustomList + id: customList + anchors.top: priority.bottom + margin-top: 10 + anchors.left: priority.left + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + @onClick: self:getParent():hide() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/npc_talk.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/npc_talk.lua new file mode 100644 index 0000000000..4ed5cf4e04 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/npc_talk.lua @@ -0,0 +1,5 @@ +onAttackingCreatureChange(function(creature, OldCreature) + if creature and creature:isNpc() and distanceFromPlayer(creature:getPosition()) <= 3 then + CaveBot.Conversation("hi", "trade") + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.lua new file mode 100644 index 0000000000..948928d69d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.lua @@ -0,0 +1,351 @@ +--[[ + configuration for check players + example made on server Gunzodus + + example link for player overview: + https://www.gunzodus.net/character/show/Sir_Vithrax + + *note that space in character name was replaced with underscore (_) - this character will be important + + in this case: + link = "https://www.gunzodus.net/character/show/" -- everything with all the characters up to the start of the name + spacing = "_" -- space replacement in character name +]] + +local link = "https://www.gunzodus.net/character/show/" +local spacing = "_" + + + +-- do not edit below +setDefaultTab("Main") +local tabs = {"Friends", "Enemies", "BlackList"} +local panelName = "playerList" +local colors = {"#03C04A", "#fc4c4e", "orange"} + +if not storage[panelName] then + storage[panelName] = { + enemyList = {}, + friendList = {}, + blackList = {}, + groupMembers = true, + outfits = false, + marks = false, + highlight = false + } +end + +local config = storage[panelName] +local playerTables = {config.friendList, config.enemyList, config.blackList} + +-- functions +local function clearCachedPlayers() + CachedFriends = {} + CachedEnemies = {} +end + +local refreshStatus = function() + for _, spec in ipairs(getSpectators()) do + if spec:isPlayer() and not spec:isLocalPlayer() then + if config.outfits then + local specOutfit = spec:getOutfit() + if isFriend(spec:getName()) then + if config.highlight then + spec:setMarked('#0000FF') + end + specOutfit.head = 88 + specOutfit.body = 88 + specOutfit.legs = 88 + specOutfit.feet = 88 + if storage.BOTserver.outfit then + local voc = vBot.BotServerMembers[spec:getName()] + specOutfit.addons = 3 + if voc == 1 then + specOutfit.type = 131 + elseif voc == 2 then + specOutfit.type = 129 + elseif voc == 3 then + specOutfit.type = 130 + elseif voc == 4 then + specOutfit.type = 144 + end + end + spec:setOutfit(specOutfit) + elseif isEnemy(spec:getName()) then + if config.highlight then + spec:setMarked('#FF0000') + end + specOutfit.head = 94 + specOutfit.body = 94 + specOutfit.legs = 94 + specOutfit.feet = 94 + spec:setOutfit(specOutfit) + end + end + end + end +end +refreshStatus() + +local checkStatus = function(creature) + if not creature:isPlayer() or creature:isLocalPlayer() then return end + + local specName = creature:getName() + local specOutfit = creature:getOutfit() + + if isFriend(specName) then + if config.highlight then + creature:setMarked('#0000FF') + end + if config.outfits then + specOutfit.head = 88 + specOutfit.body = 88 + specOutfit.legs = 88 + specOutfit.feet = 88 + if storage.BOTserver.outfit then + local voc = vBot.BotServerMembers[creature:getName()] + specOutfit.addons = 3 + if voc == 1 then + specOutfit.type = 131 + elseif voc == 2 then + specOutfit.type = 129 + elseif voc == 3 then + specOutfit.type = 130 + elseif voc == 4 then + specOutfit.type = 144 + end + end + creature:setOutfit(specOutfit) + end + elseif isEnemy(specName) then + if config.highlight then + creature:setMarked('#FF0000') + end + if config.outfits then + specOutfit.head = 94 + specOutfit.body = 94 + specOutfit.legs = 94 + specOutfit.feet = 94 + creature:setOutfit(specOutfit) + end + end +end + + +rootWidget = g_ui.getRootWidget() +if rootWidget then + local ListWindow = UI.createWindow('PlayerListWindow', rootWidget) + ListWindow:hide() + + UI.Button("Player Lists", function() + ListWindow:show() + ListWindow:raise() + ListWindow:focus() + end) + + -- settings + ListWindow.settings.Members:setChecked(config.groupMembers) + ListWindow.settings.Members.onClick = function(widget) + config.groupMembers = not config.groupMembers + if not config.groupMembers then + clearCachedPlayers() + end + refreshStatus() + widget:setChecked(config.groupMembers) + end + ListWindow.settings.Outfit:setChecked(config.outfits) + ListWindow.settings.Outfit.onClick = function(widget) + config.outfits = not config.outfits + widget:setChecked(config.outfits) + refreshStatus() + end + ListWindow.settings.NeutralsAreEnemy:setChecked(config.marks) + ListWindow.settings.NeutralsAreEnemy.onClick = function(widget) + config.marks = not config.marks + widget:setChecked(config.marks) + end + ListWindow.settings.Highlight:setChecked(config.highlight) + ListWindow.settings.Highlight.onClick = function(widget) + config.highlight = not config.highlight + widget:setChecked(config.highlight) + end + + ListWindow.settings.AutoAdd:setChecked(config.autoAdd) + ListWindow.settings.AutoAdd.onClick = function(widget) + config.autoAdd = not config.autoAdd + widget:setChecked(config.autoAdd) + end + + local TabBar = ListWindow.tmpTabBar + TabBar:setContentWidget(ListWindow.tmpTabContent) + local blacklistList + + for v = 1, 3 do + local listPanel = g_ui.createWidget("tPanel") -- Creates Panel + local playerList = playerTables[v] + listPanel:setId(tabs[v].."Tab") + TabBar:addTab(tabs[v], listPanel) + + -- elements + local addButton = listPanel.add + local nameTab = listPanel.name + local list = listPanel.list + if v == 3 then + blacklistList = list + end + + for i, name in ipairs(playerList) do + local label = UI.createWidget("PlayerLabel", list) + label:setText(name) + label.remove.onClick = function() + table.remove(playerList, table.find(playerList, name)) + label:destroy() + clearCachedPlayers() + refreshStatus() + end + label.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('Check Player', function() + local name = widget:getText():gsub(" ", spacing) + g_platform.openUrl(link..name) + end, "") + menu:addOption('Copy Name', function() + g_window.setClipboardText(widget:getText()) + end, "") + menu:display(mousePos) + return true + end + end + end + end + + local tabButton = TabBar.buttonsPanel:getChildren()[v] + + tabButton.onStyleApply = function(widget) + if TabBar:getCurrentTab() == widget then + widget:setColor(colors[v]) + end + end + + -- callbacks + addButton.onClick = function() + local names = string.split(nameTab:getText(), ",") + + if #names == 0 then + warn("vBot[PlayerList]: Name is missing!") + return + end + + for i=1,#names do + local name = names[i]:trim() + if name:len() == 0 then + warn("vBot[PlayerList]: Name is missing!") + else + if not table.find(playerList, name) then + table.insert(playerList, name) + local label = UI.createWidget("PlayerLabel", list) + label:setText(name) + label.remove.onClick = function() + table.remove(playerList, table.find(playerList, name)) + label:destroy() + end + label.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('Check Player', function() + local name = widget:getText():gsub(" ", "_") + local link = "https://www.gunzodus.net/character/show/" + g_platform.openUrl(link..name) + end, "") + menu:addOption('Copy Name', function() + g_window.setClipboardText(widget:getText()) + end, "") + menu:display(mousePos) + return true + end + end + end + nameTab:setText("") + else + warn("vBot[PlayerList]: Player ".. name .." is already added!") + nameTab:setText("") + end + clearCachedPlayers() + refreshStatus() + end + end + end + + nameTab.onKeyPress = function(widget, keyCode, keyboardModifiers) + if keyCode ~= 5 then + return false + end + addButton.onClick() + return true + end + end + + function addBlackListPlayer(name) + if table.find(config.blackList, name) then return end + + table.insert(config.blackList, name) + local label = UI.createWidget("PlayerLabel", blacklistList) + label:setText(name) + label.remove.onClick = function() + table.remove(playerList, table.find(playerList, name)) + label:destroy() + end + label.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('Check Player', function() + local name = widget:getText():gsub(" ", "_") + local link = "https://www.gunzodus.net/character/show/" + g_platform.openUrl(link..name) + end, "") + menu:addOption('Copy Name', function() + g_window.setClipboardText(widget:getText()) + end, "") + menu:display(mousePos) + return true + end + end + end + end +end + +onTextMessage(function(mode,text) + if not config.autoAdd then return end + if CaveBot.isOff() or TargetBot.isOff() then return end + if not text:find("Warning! The murder of") then return end + + text = string.split(text, "Warning! The murder of ")[1] + text = string.split(text, " was not justified.")[1] + + addBlackListPlayer(text) +end) + +onCreatureAppear(function(creature) + checkStatus(creature) + end) + +onPlayerPositionChange(function(x,y) + if x.z ~= y.z then + schedule(20, function() + refreshStatus() + end) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.otui new file mode 100644 index 0000000000..a4f0d92a5b --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/playerlist.otui @@ -0,0 +1,151 @@ +PlayerLabel < UIWidget + background-color: alpha + text-offset: 3 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('X') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + margin-right: 15 + text-align: center + text-offset: 0 1 + tooltip: Remove profile from the list. + +SettingCheckBox < CheckBox + text-wrap: true + text-auto-resize: true + margin-top: 3 + font: verdana-11px-rounded + +Settings < FlatPanel + padding: 6 + layout: + type: verticalBox + + Label + text: Additional Settings + text-align: center + font: verdana-11px-rounded + + HorizontalSeparator + + SettingCheckBox + id: Members + margin-top: 6 + text: Consider group members as friends. + + SettingCheckBox + id: Outfit + text: Color listed player outfits to red or blue. + + SettingCheckBox + id: NeutralsAreEnemy + text: Consider every non friend player as enemy. + + SettingCheckBox + id: Highlight + text: Hightlight listed players in red or blue color. + + SettingCheckBox + id: AutoAdd + text: Automatically add killed players while cave botting to blacklist. + +tPanel < Panel + margin: 3 + padding: 3 + + TextList + id: list + height: 200 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + vertical-scrollbar: listScrollBar + + VerticalScrollBar + id: listScrollBar + anchors.top: list.top + anchors.bottom: list.bottom + anchors.right: list.right + step: 14 + pixels-scroll: true + + TextEdit + id: name + anchors.top: list.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + + Button + id: add + text: Add Player + anchors.top: prev.bottom + margin-top: 3 + anchors.left: parent.left + anchors.right: parent.right + font: verdana-11px-rounded + +PlayerListWindow < MainWindow + !text: tr('Player List') + size: 405 356 + @onEscape: self:hide() + + TabBar + id: tmpTabBar + anchors.top: parent.top + anchors.left: parent.left + width: 180 + + FlatPanel + id: tmpTabContent + anchors.top: tmpTabBar.bottom + anchors.left: parent.left + width: 180 + anchors.bottom: separator.top + margin-bottom: 5 + + VerticalSeparator + id: verticalSep + anchors.top: parent.top + anchors.bottom: separator.top + margin-bottom: 5 + anchors.horizontalCenter: parent.horizontalCenter + + Settings + id: settings + anchors.left: prev.right + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: next.top + margin: 3 + margin-left: 6 + margin-bottom: 4 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 + @onClick: self:getParent():hide() \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.lua new file mode 100644 index 0000000000..0f63447005 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.lua @@ -0,0 +1,287 @@ +---@diagnostic disable: undefined-global +setDefaultTab("Main") + +local panelName = "pushmax" +local ui = setupUI([[ +Panel + height: 19 + + BotSwitch + id: title + anchors.top: parent.top + anchors.left: parent.left + text-align: center + width: 130 + !text: tr('PUSHMAX') + + Button + id: push + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 3 + height: 17 + text: Setup + +]]) +ui:setId(panelName) + +if not storage[panelName] then + storage[panelName] = { + enabled = true, + pushDelay = 1060, + pushMaxRuneId = 3188, + mwallBlockId = 2128, + pushMaxKey = "PageUp" + } +end + +local config = storage[panelName] + +ui.title:setOn(config.enabled) +ui.title.onClick = function(widget) +config.enabled = not config.enabled +widget:setOn(config.enabled) +end + +ui.push.onClick = function(widget) + pushWindow:show() + pushWindow:raise() + pushWindow:focus() +end + +rootWidget = g_ui.getRootWidget() +if rootWidget then + pushWindow = UI.createWindow('PushMaxWindow', rootWidget) + pushWindow:hide() + + pushWindow.closeButton.onClick = function(widget) + pushWindow:hide() + end + + local updateDelayText = function() + pushWindow.delayText:setText("Push Delay: ".. config.pushDelay) + end + updateDelayText() + pushWindow.delay.onValueChange = function(scroll, value) + config.pushDelay = value + updateDelayText() + end + pushWindow.delay:setValue(config.pushDelay) + + pushWindow.runeId.onItemChange = function(widget) + config.pushMaxRuneId = widget:getItemId() + end + pushWindow.runeId:setItemId(config.pushMaxRuneId) + pushWindow.mwallId.onItemChange = function(widget) + config.mwallBlockId = widget:getItemId() + end + pushWindow.mwallId:setItemId(config.mwallBlockId) + + pushWindow.hotkey.onTextChange = function(widget, text) + config.pushMaxKey = text + end + pushWindow.hotkey:setText(config.pushMaxKey) +end + + +-- variables for config +local fieldTable = {2118, 105, 2122} +local cleanTile = nil + +-- scripts + +local targetTile +local pushTarget + +local resetData = function() + for i, tile in pairs(g_map.getTiles(posz())) do + if tile:getText() == "TARGET" or tile:getText() == "DEST" or tile:getText() == "CLEAR" then + tile:setText('') + end + end + pushTarget = nil + targetTile = nil + cleanTile = nil +end + +local getCreatureById = function(id) + for i, spec in ipairs(getSpectators()) do + if spec:getId() == id then + return spec + end + end + return false +end + +local isNotOk = function(t,tile) + local tileItems = {} + + for i, item in pairs(tile:getItems()) do + table.insert(tileItems, item:getId()) + end + for i, field in ipairs(t) do + if table.find(tileItems, field) then + return true + end + end + return false +end + +local isOk = function(a,b) + return getDistanceBetween(a,b) == 1 +end + +-- to mark +local hold = 0 +onKeyDown(function(keys) + if not config.enabled then return end + if keys ~= config.pushMaxKey then return end + hold = now + local tile = getTileUnderCursor() + if not tile then return end + if pushTarget and targetTile then + resetData() + return + end + local creature = tile:getCreatures()[1] + if not pushTarget and creature then + pushTarget = creature + if pushTarget then + tile:setText('TARGET') + pushTarget:setMarked('#00FF00') + end + elseif not targetTile and pushTarget then + if pushTarget and getDistanceBetween(tile:getPosition(),pushTarget:getPosition()) ~= 1 then + resetData() + return + else + tile:setText('DEST') + targetTile = tile + end + end +end) + +-- mark tile to throw anything from it +onKeyPress(function(keys) + if not config.enabled then return end + if keys ~= config.pushMaxKey then return end + local tile = getTileUnderCursor() + if not tile then return end + + if (hold - now) < -2500 then + if cleanTile and tile ~= cleanTile then + resetData() + elseif not cleanTile then + cleanTile = tile + tile:setText("CLEAR") + end + end + hold = 0 +end) + +onCreaturePositionChange(function(creature, newPos, oldPos) + if not config.enabled then return end + if creature == player then + resetData() + end + if not pushTarget or not targetTile then return end + if creature == pushTarget and newPos == targetTile then + resetData() + end +end) + +macro(50, function() + if not config.enabled then return end + + local pushDelay = tonumber(config.pushDelay) + local rune = tonumber(config.pushMaxRuneId) + local customMwall = config.mwallBlockId + + if cleanTile then + local tilePos = cleanTile:getPosition() + local pPos = player:getPosition() + if not isOk(tilePos, pPos) then + resetData() + return + end + + if not cleanTile:hasCreature() then return end + local tiles = getNearTiles(tilePos) + local destTile + local forbidden = {} + -- unfortunately double loop + for i, tile in pairs(tiles) do + local minimapColor = g_map.getMinimapColor(tile:getPosition()) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + if stairs then + table.insert(forbidden, tile:getPosition()) + end + end + for i, tile in pairs(tiles) do + local minimapColor = g_map.getMinimapColor(tile:getPosition()) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + if tile:isWalkable() and not isNotOk(fieldTable, tile) and not tile:hasCreature() and not stairs then + local tooClose = false + if #forbidden ~= 0 then + for i=1,#forbidden do + local pos = forbidden[i] + if isOk(pos, tile:getPosition()) then + tooClose = true + break + end + end + end + if not tooClose then + destTile = tile + break + end + end + end + + if not destTile then return end + local parcel = cleanTile:getCreatures()[1] + if parcel then + test() + g_game.move(parcel,destTile:getPosition()) + delay(2000) + end + else + if not pushTarget or not targetTile then return end + local tilePos = targetTile:getPosition() + local targetPos = pushTarget:getPosition() + if not isOk(tilePos,targetPos) then return end + + local tileOfTarget = g_map.getTile(targetPos) + + if not targetTile:isWalkable() then + local topThing = targetTile:getTopUseThing():getId() + if topThing == 2129 or topThing == 2130 or topThing == customMwall then + if targetTile:getTimer() < pushDelay+500 then + vBot.isUsing = true + schedule(pushDelay+700, function() + vBot.isUsing = false + end) + end + if targetTile:getTimer() > pushDelay then + return + end + else + return resetData() + end + end + + if not tileOfTarget:getTopUseThing():isNotMoveable() and targetTile:getTimer() < pushDelay+500 then + return useWith(rune, pushTarget) + end + if isNotOk(fieldTable, targetTile) then + if targetTile:canShoot() then + return useWith(3148, targetTile:getTopUseThing()) + else + return + end + end + g_game.move(pushTarget,tilePos) + delay(2000) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.otui new file mode 100644 index 0000000000..875a4f8b4f --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/pushmax.otui @@ -0,0 +1,85 @@ +PushMaxWindow < MainWindow + !text: tr('Pushmax Settings') + size: 200 240 + @onEscape: self:hide() + + BotLabel + id: delayText + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-align: center + + HorizontalScrollBar + id: delay + anchors.left: delayText.left + anchors.right: delayText.right + anchors.top: delayText.bottom + margin-top: 5 + minimum: 800 + maximum: 2000 + step: 10 + + Label + id: label2 + anchors.top: delay.bottom + anchors.left: parent.horizontalCenter + anchors.right: parent.right + text-align: center + text: Custom WallID + margin-top: 5 + + Label + id: label3 + anchors.top: delay.bottom + anchors.right: parent.horizontalCenter + anchors.left: parent.left + text-align: center + text: VS AntiPush + margin-top: 5 + + BotItem + id: runeId + anchors.horizontalCenter: label3.horizontalCenter + anchors.top: label3.bottom + margin-top: 5 + + BotItem + id: mwallId + anchors.horizontalCenter: label2.horizontalCenter + anchors.top: label2.bottom + margin-top: 5 + + Label + id: label1 + anchors.top: mwallId.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 10 + text-align: center + text: Hotkey for PUSHMAX + + TextEdit + id: hotkey + anchors.left: parent.left + anchors.right: parent.right + anchors.top: label1.bottom + margin-top: 5 + text-align: center + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/quiver_label.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/quiver_label.lua new file mode 100644 index 0000000000..671d726831 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/quiver_label.lua @@ -0,0 +1,58 @@ +local quiverSlot = modules.game_inventory.inventoryWindow:recursiveGetChildById('slot5') +local label = quiverSlot.count + +label = label or g_ui.loadUIFromString([[ +Label + id: count + color: #bfbfbf + font: verdana-11px-rounded + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + text-align: right + margin-right: 3 + margin-left: 3 + text: +]], quiverSlot) + + +function getQuiverAmount() + -- old tibia + if g_game.getClientVersion() < 1000 then return end + + + local isQuiverEquipped = getRight() and getRight():isContainer() or false + local quiver = isQuiverEquipped and getContainerByItem(getRight():getId()) + local count = 0 + + if quiver then + for i, item in ipairs(quiver:getItems()) do + count = count + item:getCount() + end + else + return label:setText("") + end + + return label:setText(count) +end +getQuiverAmount() + +onContainerOpen(function(container, previousContainer) + getQuiverAmount() +end) + +onContainerClose(function(container) + getQuiverAmount() +end) + +onAddItem(function(container, slot, item, oldItem) + getQuiverAmount() +end) + +onRemoveItem(function(container, slot, item) + getQuiverAmount() +end) + +onContainerUpdateItem(function(container, slot, item, oldItem) + getQuiverAmount() +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/quiver_manager.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/quiver_manager.lua new file mode 100644 index 0000000000..368bd8cda6 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/quiver_manager.lua @@ -0,0 +1,91 @@ +if voc() == 2 or voc() == 12 then + local bows = { 3350, 31581, 27455, 8027, 20082, 36664, 7438, 28718, 36665, 14246, 19362, 35518, 34150, 29417, 9378, 16164, 22866, 12733, 8029, 20083, 20084, 8026, 8028, 34088} + local xbows = { 30393, 3349, 27456, 20085, 16163, 5947, 8021, 14247, 22867, 8023, 22711, 19356, 20086, 20087, 34089} + local arrows = { 16143, 763, 761, 7365, 3448, 762, 21470, 7364, 14251, 3447, 3449, 15793, 25757, 774, 35901 } + local bolts = { 6528, 7363, 3450, 16141, 25758, 14252, 3446, 16142, 35902 } + local hold = false + + onContainerOpen(function(container, previousContainer) + hold = false + end) + + onContainerClose(function(container) + hold = false + end) + + onAddItem(function(container, slot, item, oldItem) + hold = false + end) + + onRemoveItem(function(container, slot, item) + hold = false + end) + + onContainerUpdateItem(function(container, slot, item, oldItem) + hold = false + end) + + + + local function manageQuiver(isBowEquipped, quiverContainer) + local ammo = isBowEquipped and arrows or bolts + local dest = nil + local containers = getContainers() + for i, container in ipairs(containers) do + if container ~= quiverContainer and not containerIsFull(container) then + local cname = container:getName():lower() + if not cname:find("loot") and (cname:find("backpack") or cname:find("bag") or cname:find("chess")) then + dest = container + end + end + end + + -- clearing + if dest then + for i, item in ipairs(quiverContainer:getItems()) do + if not table.find(ammo, item:getId()) then + local pos = dest:getSlotPosition(dest:getItemsCount()) + return g_game.move(item, pos, item:getCount()) + end + end + end + + if not containerIsFull(quiverContainer) then + for i, container in ipairs(containers) do + if container ~= quiverContainer then + for j, item in ipairs(container:getItems()) do + if table.find(ammo, item:getId()) then + local pos = quiverContainer:getSlotPosition(quiverContainer:getItemsCount()) + return g_game.move(item, pos, item:getCount()) + end + end + end + end + end + return true + end + + UI.Separator() + macro(100, "Quiver Manager", function() + if hold then return end -- do nothing if nothing to do + local hand = getLeft() and getLeft():getId() + local quiverEquipped = getRight() and getRight():isContainer() + + if not hand then return end + if not quiverEquipped then return end + + local quiverContainer = getContainerByItem(getRight():getId()) + if not quiverContainer then return end + + local isBowEquipped = getLeft() and table.find(bows, hand) and true or false + if not isBowEquipped then + if not table.find(xbows, hand) then + return -- neither bow and xbow is equipped + end + end + + if manageQuiver(isBowEquipped, quiverContainer) then -- if true then it didn't do anything + hold = true + end + end) +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/siolist.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/siolist.otui new file mode 100644 index 0000000000..e9920753fa --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/siolist.otui @@ -0,0 +1,192 @@ +VocationPanel < Panel + padding: 3 + image-source: /images/ui/panel_flat + image-border: 6 + size: 190 55 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-align: center + text: for BotServer, Heal only: + + BotSwitch + id: ED + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.horizontalCenter + text: Druids + + BotSwitch + id: MS + anchors.bottom: parent.bottom + anchors.left: parent.horizontalCenter + anchors.right: parent.right + text: Sorcerers + + BotSwitch + id: EK + anchors.bottom: ED.top + anchors.left: parent.left + anchors.right: parent.horizontalCenter + text: Knights + + BotSwitch + id: RP + anchors.bottom: ED.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + text: Paladins + + + +SioListWindow < MainWindow + !text: tr('Healer Options') + size: 220 360 + @onEscape: self:hide() + + BotSwitch + id: exuraSio + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.horizontalCenter + text: Exura Sio + margin-right: 2 + + BotSwitch + id: exuraGranSio + anchors.top: parent.top + anchors.left: prev.right + anchors.right: parent.right + text: Exura Gran Sio + margin-left: 2 + + BotSwitch + id: exuraMasRes + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text: Exura Gran Mas Res + margin-top: 3 + + BotSwitch + id: spell + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text: Custom Spell + margin-top: 3 + text-align: center + + BotTextEdit + id: spellName + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 10 + + BotItem + id: itemId + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 10 + + BotSwitch + id: item + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + anchors.bottom: prev.verticalCenter + text-align: center + text: Item Healing + margin-left: 2 + + BotLabel + id: distText + anchors.top: itemId.verticalCenter + anchors.left: itemId.right + anchors.right: parent.right + anchors.bottom: itemId.bottom + text-align: center + text: Max Distance + + HorizontalScrollBar + id: Distance + anchors.left: parent.left + anchors.top: itemId.bottom + anchors.right: parent.right + margin-top: 3 + minimum: 1 + maximum: 10 + step: 1 + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 8 + + BotLabel + id: manaInfo + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-align: center + margin-top: 5 + + HorizontalScrollBar + id: minMana + anchors.left: spellName.left + anchors.right: spellName.right + anchors.top: manaInfo.bottom + margin-top: 2 + minimum: 1 + maximum: 100 + step: 1 + + BotLabel + id: friendHp + anchors.left: spellName.left + anchors.right: spellName.right + anchors.top: prev.bottom + text-align: center + margin-top: 5 + + HorizontalScrollBar + id: minFriendHp + anchors.left: spellName.left + anchors.right: spellName.right + anchors.top: friendHp.bottom + margin-top: 2 + minimum: 1 + maximum: 100 + step: 1 + + VocationPanel + id: vocation + anchors.top: prev.bottom + margin-top: 6 + + HorizontalSeparator + id: separator + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + margin-right: 5 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/spy_level.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/spy_level.lua new file mode 100644 index 0000000000..f225d7699d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/spy_level.lua @@ -0,0 +1,24 @@ +-- config + +local keyUp = "=" +local keyDown = "-" +setDefaultTab("Tools") + +-- script + +local lockedLevel = pos().z + +onPlayerPositionChange(function(newPos, oldPos) + lockedLevel = pos().z + modules.game_interface.getMapPanel():unlockVisibleFloor() +end) + +onKeyPress(function(keys) + if keys == keyDown then + lockedLevel = lockedLevel + 1 + modules.game_interface.getMapPanel():lockVisibleFloor(lockedLevel) + elseif keys == keyUp then + lockedLevel = lockedLevel - 1 + modules.game_interface.getMapPanel():lockVisibleFloor(lockedLevel) + end +end) \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/supplies.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/supplies.lua new file mode 100644 index 0000000000..d5fbd4d3ef --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/supplies.lua @@ -0,0 +1,473 @@ +setDefaultTab("Cave") +local panelName = "supplies" +if not SuppliesConfig[panelName] or SuppliesConfig[panelName].item1 then + SuppliesConfig[panelName] = { + currentProfile = "Default", + ["Default"] = {} + } +end + +local function convertOldConfig(config) + if config and config.items then + return config + end -- config is new + + local newConfig = { + items = {}, + capSwitch = config.capSwitch, + SoftBoots = config.SoftBoots, + imbues = config.imbues, + staminaSwitch = config.staminaSwitch, + capValue = config.capValue, + staminaValue = config.staminaValue + } + + local items = { + config.item1, + config.item2, + config.item3, + config.item4, + config.item5, + config.item6 + } + local mins = { + config.item1Min, + config.item2Min, + config.item3Min, + config.item4Min, + config.item5Min, + config.item6Min + } + local maxes = { + config.item1Max, + config.item2Max, + config.item3Max, + config.item4Max, + config.item5Max, + config.item6Max + } + + for i, item in ipairs(items) do + if item > 100 then + local min = mins[i] + local max = maxes[i] + newConfig.items[tostring(item)] = { + min = min, + max = max, + avg = 0 + } + end + end + + return newConfig +end + +-- convert old configs +for k, profile in pairs(SuppliesConfig[panelName]) do + if type(profile) == 'table' then + SuppliesConfig[panelName][k] = convertOldConfig(profile) + end +end + +local currentProfile = SuppliesConfig[panelName].currentProfile +local config = SuppliesConfig[panelName][currentProfile] + +vBotConfigSave("supply") + +if not config then + for k, v in pairs(SuppliesConfig[panelName]) do + if type(v) == "table" then + SuppliesConfig[panelName].currentProfile = k + config = SuppliesConfig[panelName][k] + break + end + end +end + +function getEmptyItemPanels() + local panel = SuppliesWindow.items + local count = 0 + + for i, child in ipairs(panel:getChildren()) do + count = child:getId() == "blank" and count + 1 or count + end + + return count +end + +function deleteFirstEmptyPanel() + local panel = SuppliesWindow.items + + for i, child in ipairs(panel:getChildren()) do + if child:getId() == "blank" then + child:destroy() + break + end + end +end + +function clearEmptyPanels() + local panel = SuppliesWindow.items + + if panel:getChildCount() > 1 then + if getEmptyItemPanels() > 1 then + deleteFirstEmptyPanel() + end + end +end + +function addItemPanel() + local parent = SuppliesWindow.items + local childs = parent:getChildCount() + local panel = UI.createWidget("ItemPanel", parent) + local item = panel.id + local min = panel.min + local max = panel.max + local avg = panel.avg + + panel:setId("blank") + item:setShowCount(false) + + item.onItemChange = function(widget) + local id = widget:getItemId() + local panelId = panel:getId() + + -- empty, verify + if id < 100 then + config.items[panelId] = nil + panel:setId("blank") + clearEmptyPanels() -- clear empty panels if any + return + end + + -- itemId was not changed, ignore + if tonumber(panelId) == id then + return + end + + -- check if isnt already added + if config[tostring(id)] then + warn("vBot[Drop Tracker]: Item already added!") + widget:setItemId(0) + return + end + + -- new item id + config.items[tostring(id)] = config.items[tostring(id)] or {} -- min, max, avg + panel:setId(id) + addItemPanel() -- add new panel + end + + return panel +end + +SuppliesWindow = UI.createWindow("SuppliesWindow") +SuppliesWindow:hide() + +UI.Button( + "Supply Settings", + function() + SuppliesWindow:setVisible(not SuppliesWindow:isVisible()) + end +) + +-- load settings +local function loadSettings() + -- panels + SuppliesWindow.items:destroyChildren() + + for id, data in pairs(config.items) do + local widget = addItemPanel() + widget:setId(id) + widget.id:setItemId(tonumber(id)) + widget.min:setText(data.min) + widget.max:setText(data.max) + widget.avg:setText(data.avg) + end + addItemPanel() -- add empty panel + + -- switches and values + SuppliesWindow.capSwitch:setOn(config.capSwitch) + SuppliesWindow.SoftBoots:setOn(config.SoftBoots) + SuppliesWindow.imbues:setOn(config.imbues) + SuppliesWindow.staminaSwitch:setOn(config.staminaSwitch) + SuppliesWindow.capValue:setText(config.capValue or 0) + SuppliesWindow.staminaValue:setText(config.staminaValue or 0) +end +loadSettings() + +-- save settings +SuppliesWindow.onVisibilityChange = function(widget, visible) + if not visible then + local currentProfile = SuppliesConfig[panelName].currentProfile + SuppliesConfig[panelName][currentProfile].items = {} + local parent = SuppliesWindow.items + + -- items + for i, panel in ipairs(parent:getChildren()) do + if panel.id:getItemId() > 100 then + local id = tostring(panel.id:getItemId()) + local min = panel.min:getValue() + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + SuppliesConfig[panelName][currentProfile].items[id] = { + min = min, + max = max, + avg = avg + } + end + end + + vBotConfigSave("supply") + end +end + +local function refreshProfileList() + local profiles = SuppliesConfig[panelName] + + SuppliesWindow.profiles:destroyChildren() + for k, v in pairs(profiles) do + if type(v) == "table" then + local label = UI.createWidget("ProfileLabel", SuppliesWindow.profiles) + label:setText(k) + label:setTooltip("Click to load this profile. \nDouble click to change the name.") + label.remove.onClick = function() + local childs = SuppliesWindow.profiles:getChildCount() + if childs == 1 then + return info("vBot[Supplies] You need at least one profile!") + end + profiles[k] = nil + label:destroy() + vBotConfigSave("supply") + end + label.onDoubleClick = function(widget) + local window = + modules.client_textedit.show( + widget, + {title = "Set Profile Name", description = "Enter a new name for selected profile"} + ) + schedule( + 50, + function() + window:raise() + window:focus() + end + ) + end + label.onClick = function() + SuppliesConfig[panelName].currentProfile = label:getText() + config = SuppliesConfig[panelName][label:getText()] + loadSettings() + vBotConfigSave("supply") + end + label.onTextChange = function(widget, text) + currentProfile = text + SuppliesConfig[panelName].currentProfile = text + profiles[text] = profiles[k] + profiles[k] = nil + vBotConfigSave("supply") + end + end + end +end +refreshProfileList() + +local function setProfileFocus() + for i, v in ipairs(SuppliesWindow.profiles:getChildren()) do + local name = v:getText() + if name == SuppliesConfig[panelName].currentProfile then + return v:focus() + end + end +end +setProfileFocus() + +SuppliesWindow.newProfile.onClick = function() + local n = SuppliesWindow.profiles:getChildCount() + if n > 6 then + return info("vBot[Supplies] - max profile count reached!") + end + local name = "Profile #" .. n + 1 + SuppliesConfig[panelName][name] = {items = {}} + refreshProfileList() + setProfileFocus() + vBotConfigSave("supply") +end + +SuppliesWindow.capSwitch.onClick = function(widget) + config.capSwitch = not config.capSwitch + widget:setOn(config.capSwitch) +end + +SuppliesWindow.SoftBoots.onClick = function(widget) + config.SoftBoots = not config.SoftBoots + widget:setOn(config.SoftBoots) +end + +SuppliesWindow.imbues.onClick = function(widget) + config.imbues = not config.imbues + widget:setOn(config.imbues) +end + +SuppliesWindow.staminaSwitch.onClick = function(widget) + config.staminaSwitch = not config.staminaSwitch + widget:setOn(config.staminaSwitch) +end + +SuppliesWindow.capValue.onTextChange = function(widget, text) + local value = tonumber(SuppliesWindow.capValue:getText()) + if not value then + SuppliesWindow.capValue:setText(0) + config.capValue = 0 + else + text = text:match("0*(%d+)") + config.capValue = text + end +end + +SuppliesWindow.staminaValue.onTextChange = function(widget, text) + local value = tonumber(SuppliesWindow.staminaValue:getText()) + if not value then + SuppliesWindow.staminaValue:setText(0) + config.staminaValue = 0 + else + text = text:match("0*(%d+)") + config.staminaValue = text + end +end + +SuppliesWindow.increment.onClick = function(widget) + for i, panel in ipairs(SuppliesWindow.items:getChildren()) do + if panel.id:getItemId() > 100 then + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + if avg > 0 then + panel.max:setText(max + avg) + end + end + end +end + +SuppliesWindow.decrement.onClick = function(widget) + for i, panel in ipairs(SuppliesWindow.items:getChildren()) do + if panel.id:getItemId() > 100 then + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + if avg > 0 then + panel.max:setText(math.max(0, max - avg)) -- dont go below 0 + end + end + end +end + +SuppliesWindow.increment.onMouseWheel = function(widget, mousePos, dir) + if dir == 1 then + SuppliesWindow.increment.onClick() + elseif dir == 2 then + SuppliesWindow.decrement.onClick() + end +end + +SuppliesWindow.decrement.onMouseWheel = SuppliesWindow.increment.onMouseWheel + +Supplies = {} -- public functions +Supplies.show = function() + SuppliesWindow:show() + SuppliesWindow:raise() + SuppliesWindow:focus() +end + +Supplies.getItemsData = function() + local t = {} + -- items + for i, panel in ipairs(SuppliesWindow.items:getChildren()) do + if panel.id:getItemId() > 100 then + local id = tostring(panel.id:getItemId()) + local min = panel.min:getValue() + local max = panel.max:getValue() + local avg = panel.avg:getValue() + + t[id] = { + min = min, + max = max, + avg = avg + } + end + end + + return t +end + +Supplies.isSupplyItem = function(id) + local data = Supplies.getItemsData() + id = tostring(id) + + if data[id] then + return data[id] + else + return false + end +end + +Supplies.hasEnough = function() + local data = Supplies.getItemsData() + + for id, values in pairs(data) do + id = tonumber(id) + local minimum = values.min + local current = player:getItemsCount(id) or 0 + + if current < minimum then + return {id=id, amount=current} + end + end + + return true +end + +hasSupplies = Supplies.hasEnough + +Supplies.setAverageValues = function(data) + for id, amount in pairs(data) do + local widget = SuppliesWindow.items[id] + + if widget then + widget.avg:setText(amount) + end + end +end + +Supplies.addSupplyItem = function(id, min, max, avg) + if not id then + return + end + + local widget = addItemPanel() + widget:setId(id) + widget.id:setItemId(tonumber(id)) + widget.min:setText(min or 0) + widget.max:setText(max or 0) + widget.avg:setText(avg or 0) +end + +Supplies.getAdditionalData = function() + local data = { + stamina = {enabled = config.staminaSwitch, value = config.staminaValue}, + capacity = {enabled = config.capSwitch, value = config.capValue}, + softBoots = {enabled = config.SoftBoots}, + imbues = {enabled = config.imbues} + } + return data +end + +Supplies.getFullData = function() + local data = { + items = Supplies.getItemsData(), + additional = Supplies.getAdditionalData() + } + + return data +end \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/supplies.otui b/modules/game_bot/default_configs/vBot_4.8/vBot/supplies.otui new file mode 100644 index 0000000000..9576c88bec --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/supplies.otui @@ -0,0 +1,244 @@ +ProfileLabel < UIWidget + background-color: alpha + text-offset: 3 1 + focusable: true + height: 16 + font: verdana-11px-rounded + text-align: left + + $focus: + background-color: #00000055 + + Button + id: remove + !text: tr('X') + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + margin-right: 3 + text-align: center + text-offset: 0 1 + tooltip: Remove profile from the list. + +SupplySpinBox < SpinBox + height: 20 + margin-left: 3 + width: 75 + minimum: 0 + maximum: 9999 + text-align: center + focusable: true + text: 0 + +ItemPanel < Panel + height: 38 + + BotItem + id: id + anchors.left: parent.left + anchors.bottom: parent.bottom + + SupplySpinBox + id: min + anchors.left: prev.right + anchors.bottom: parent.bottom + + SupplySpinBox + id: max + anchors.left: prev.right + anchors.bottom: parent.bottom + + SupplySpinBox + id: avg + anchors.left: prev.right + anchors.bottom: parent.bottom + width: 50 + + UIWidget + anchors.left: min.left + anchors.bottom: min.top + width: 75 + text-align: center + font: verdana-11px-rounded + text: Min + tooltip: Amount of given supplies for bot to leave the spawn. + + UIWidget + anchors.left: max.left + anchors.bottom: max.top + width: 75 + text-align: center + font: verdana-11px-rounded + text: Max + tooltip: Amount of given supplies to purchase + + UIWidget + anchors.left: avg.left + anchors.bottom: avg.top + width: 55 + text-align: center + font: verdana-11px-rounded + text: AVG + !tooltip: ("This is average consumption of supplies by round to help calculate the amount to purchase\n (info provided by CaveBot Stats)") + +SuppliesWindow < MainWindow + !text: tr('Supplies') + size: 430 330 + @onEscape: self:hide() + + VerticalSeparator + id: sep + anchors.top: parent.top + anchors.right: parent.right + margin-right: 140 + anchors.bottom: bottomSep.top + margin-bottom: 5 + margin-left: 10 + visible: false + + Label + anchors.left: sep.right + anchors.right: parent.right + anchors.top: parent.top + margin-left: 10 + margin-top: 3 + text-align: center + text: Additional Conditions: + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: prev.right + margin-top: 3 + + BotSwitch + id: SoftBoots + anchors.top: prev.bottom + anchors.left: sep.right + anchors.right: parent.right + margin-top: 5 + margin-left: 10 + text: No Soft + tooltip: Go refill if there's no more active soft boots. + + BotSwitch + id: capSwitch + height: 20 + anchors.left: SoftBoots.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-right: 50 + text-align: center + text: Cap Below: + tooltip: Go refill if capacity is below set value. + + BotTextEdit + id: capValue + size: 40 20 + anchors.left: prev.right + anchors.right: parent.right + anchors.top: prev.top + margin-left: 5 + + BotSwitch + id: staminaSwitch + height: 20 + anchors.left: SoftBoots.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-right: 50 + text-align: center + text: Stamina: + tooltip: Go refill if stamina is below set value. (in minutes) + + BotTextEdit + id: staminaValue + size: 40 20 + anchors.left: prev.right + anchors.right: parent.right + anchors.top: prev.top + margin-left: 5 + + BotSwitch + id: imbues + anchors.top: prev.bottom + anchors.left: sep.right + anchors.right: parent.right + margin-top: 5 + margin-left: 10 + text: No Imbues + tooltip: Go refill when mana leech imbue has worn off. + + TextList + id: profiles + anchors.top: prev.bottom + margin-top: 5 + anchors.left: prev.left + anchors.right: prev.right + anchors.bottom: bottomSep.top + margin-bottom: 25 + + BotButton + id: newProfile + anchors.left: prev.left + anchors.top: prev.bottom + size: 35 15 + text: New + font: cipsoftFont + tooltip: Create new supplies profile. + + VerticalScrollBar + id: itemsScrollBar + anchors.top: items.top + anchors.bottom: items.bottom + anchors.right: items.right + step: 14 + pixels-scroll: true + + ScrollablePanel + id: items + anchors.top: parent.top + anchors.left: parent.left + anchors.right: sep.left + anchors.bottom: bottomSep.top + margin-bottom: 8 + vertical-scrollbar: itemsScrollBar + layout: verticalBox + + HorizontalSeparator + id: bottomSep + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: closeButton.top + margin-bottom: 8 + + Button + id: closeButton + !text: tr('Close') + font: cipsoftFont + anchors.right: parent.right + anchors.bottom: parent.bottom + size: 45 21 + margin-top: 15 + tooltip: Close supplies window and save settings. + @onClick: self:getParent():hide() + + Button + id: increment + anchors.verticalCenter: prev.verticalCenter + anchors.right: items.right + text: + + width: 50 + tooltip: increase all max supplies amount by average + + Button + id: decrement + anchors.verticalCenter: prev.verticalCenter + anchors.right: prev.left + margin-right: 3 + text: - + width: 50 + tooltip: decrease all max supplies amount by average \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/tools.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/tools.lua new file mode 100644 index 0000000000..6105692f9d --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/tools.lua @@ -0,0 +1,46 @@ +-- tools tab +setDefaultTab("Tools") + +if type(storage.moneyItems) ~= "table" then + storage.moneyItems = {3031, 3035} +end +macro(1000, "Exchange money", function() + if not storage.moneyItems[1] then return end + 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(storage.moneyItems) do + if item:getId() == moneyId.id then + return g_game.use(item) + end + end + end + end + end + end +end) + +local moneyContainer = UI.Container(function(widget, items) + storage.moneyItems = items +end, true) +moneyContainer:setHeight(35) +moneyContainer:setItems(storage.moneyItems) + +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() diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/version.txt b/modules/game_bot/default_configs/vBot_4.8/vBot/version.txt new file mode 100644 index 0000000000..b6afe802d3 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/version.txt @@ -0,0 +1 @@ +4.8 \ No newline at end of file diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/vlib.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/vlib.lua new file mode 100644 index 0000000000..c93bb144c5 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/vlib.lua @@ -0,0 +1,1175 @@ +-- Author: Vithrax +-- contains mostly basic function shortcuts and code shorteners + +-- initial global variables declaration +vBot = {} -- global namespace for bot variables +vBot.BotServerMembers = {} +vBot.standTime = now +vBot.isUsingPotion = false +vBot.isUsing = false +vBot.customCooldowns = {} + +function logInfo(text) + local timestamp = os.date("%H:%M:%S") + text = tostring(text) + local start = timestamp.." [vBot]: " + + return modules.client_terminal.addLine(start..text, "orange") +end + +-- scripts / functions +onPlayerPositionChange(function(x,y) + vBot.standTime = now +end) + +function standTime() + return now - vBot.standTime +end + +function relogOnCharacter(charName) + local characters = g_ui.getRootWidget().charactersWindow.characters + for index, child in ipairs(characters:getChildren()) do + local name = child:getChildren()[1]:getText() + + if name:lower():find(charName:lower()) then + child:focus() + schedule(100, modules.client_entergame.CharacterList.doLogin) + end + end +end + +function castSpell(text) + if canCast(text) then + say(text) + end +end + +local dmgTable = {} +local lastDmgMessage = now +onTextMessage(function(mode, text) + if not text:lower():find("you lose") or not text:lower():find("due to") then + return + end + local dmg = string.match(text, "%d+") + if #dmgTable > 0 then + for k, v in ipairs(dmgTable) do + if now - v.t > 3000 then table.remove(dmgTable, k) end + end + end + lastDmgMessage = now + table.insert(dmgTable, {d = dmg, t = now}) + schedule(3050, function() + if now - lastDmgMessage > 3000 then dmgTable = {} end + end) +end) + +-- based on data collected by callback calculates per second damage +-- returns number +function burstDamageValue() + local d = 0 + local time = 0 + if #dmgTable > 1 then + for i, v in ipairs(dmgTable) do + if i == 1 then time = v.t end + d = d + v.d + end + end + return math.ceil(d / ((now - time) / 1000)) +end + +-- simplified function from modules +-- displays string as white colour message +function whiteInfoMessage(text) + return modules.game_textmessage.displayGameMessage(text) +end + +function statusMessage(text, logInConsole) + return not logInConsole and modules.game_textmessage.displayFailureMessage(text) or modules.game_textmessage.displayStatusMessage(text) +end + +-- same as above but red message +function broadcastMessage(text) + return modules.game_textmessage.displayBroadcastMessage(text) +end + +-- almost every talk action inside cavebot has to be done by using schedule +-- therefore this is simplified function that doesn't require to build a body for schedule function +function scheduleNpcSay(text, delay) + if not text or not delay then return false end + + return schedule(delay, function() NPC.say(text) end) +end + +-- returns first number in string, already formatted as number +-- returns number or nil +function getFirstNumberInText(text) + local n = nil + if string.match(text, "%d+") then n = tonumber(string.match(text, "%d+")) end + return n +end + +-- function to search if item of given ID can be found on certain tile +-- first argument is always ID +-- the rest of aguments can be: +-- - tile +-- - position +-- - or x,y,z coordinates as p1, p2 and p3 +-- returns boolean +function isOnTile(id, p1, p2, p3) + if not id then return end + local tile + if type(p1) == "table" then + tile = g_map.getTile(p1) + elseif type(p1) ~= "number" then + tile = p1 + else + local p = getPos(p1, p2, p3) + tile = g_map.getTile(p) + end + if not tile then return end + + local item = false + if #tile:getItems() ~= 0 then + for i, v in ipairs(tile:getItems()) do + if v:getId() == id then item = true end + end + else + return false + end + + return item +end + +-- position is a special table, impossible to compare with normal one +-- this is translator from x,y,z to proper position value +-- returns position table +function getPos(x, y, z) + if not x or not y or not z then return nil end + local pos = pos() + pos.x = x + pos.y = y + pos.z = z + + return pos +end + +-- opens purse... that's it +function openPurse() + return g_game.use(g_game.getLocalPlayer():getInventoryItem( + InventorySlotPurse)) +end + +-- check's whether container is full +-- c has to be container object +-- returns boolean +function containerIsFull(c) + if not c then return false end + + if c:getCapacity() > #c:getItems() then + return false + else + return true + end + +end + +function dropItem(idOrObject) + if type(idOrObject) == "number" then + idOrObject = findItem(idOrObject) + end + + g_game.move(idOrObject, pos(), idOrObject:getCount()) +end + +-- not perfect function to return whether character has utito tempo buff +-- known to be bugged if received debuff (ie. roshamuul) +-- TODO: simply a better version +-- returns boolean +function isBuffed() + local var = false + if not hasPartyBuff() then return var end + + local skillId = 0 + for i = 1, 4 do + if player:getSkillBaseLevel(i) > player:getSkillBaseLevel(skillId) then + skillId = i + end + end + + local premium = (player:getSkillLevel(skillId) - player:getSkillBaseLevel(skillId)) + local base = player:getSkillBaseLevel(skillId) + if (premium / 100) * 305 > base then + var = true + end + return var +end + +-- if using index as table element, this can be used to properly assign new idex to all values +-- table needs to contain "index" as value +-- if no index in tables, it will create one +function reindexTable(t) + if not t or type(t) ~= "table" then return end + + local i = 0 + for _, e in pairs(t) do + i = i + 1 + e.index = i + end +end + +-- supports only new tibia, ver 10+ +-- returns how many kills left to get next skull - can be red skull, can be black skull! +-- reutrns number +function killsToRs() + return math.min(g_game.getUnjustifiedPoints().killsDayRemaining, + g_game.getUnjustifiedPoints().killsWeekRemaining, + g_game.getUnjustifiedPoints().killsMonthRemaining) +end + +-- calculates exhaust for potions based on "Aaaah..." message +-- changes state of vBot variable, can be used in other scripts +-- already used in pushmax, healbot, etc + +onTalk(function(name, level, mode, text, channelId, pos) + if name ~= player:getName() then return end + if mode ~= 34 then return end + + if text == "Aaaah..." then + vBot.isUsingPotion = true + schedule(950, function() vBot.isUsingPotion = false end) + end +end) + +-- [[ canCast and cast functions ]] -- +-- callback connected to cast and canCast function +-- detects if a given spell was in fact casted based on player's text messages +-- Cast text and message text must match +-- checks only spells inserted in SpellCastTable by function cast +SpellCastTable = {} +onTalk(function(name, level, mode, text, channelId, pos) + if name ~= player:getName() then return end + text = text:lower() + + if SpellCastTable[text] then SpellCastTable[text].t = now end +end) + +-- if delay is nil or delay is lower than 100 then this function will act as a normal say function +-- checks or adds a spell to SpellCastTable and updates cast time if exist +function cast(text, delay) + text = text:lower() + if type(text) ~= "string" then return end + if not delay or delay < 100 then + return say(text) -- if not added delay or delay is really low then just treat it like casual say + end + if not SpellCastTable[text] or SpellCastTable[text].d ~= delay then + SpellCastTable[text] = {t = now - delay, d = delay} + return say(text) + end + local lastCast = SpellCastTable[text].t + local spellDelay = SpellCastTable[text].d + if now - lastCast > spellDelay then return say(text) end +end + +-- canCast is a base for AttackBot and HealBot +-- checks if spell is ready to be casted again +-- ignoreRL - if true, aparat from cooldown will also check conditions inside gamelib SpellInfo table +-- ignoreCd - it true, will ignore cooldown +-- returns boolean +local Spells = modules.gamelib.SpellInfo['Default'] +function canCast(spell, ignoreRL, ignoreCd) + if type(spell) ~= "string" then return end + spell = spell:lower() + if SpellCastTable[spell] then + if now - SpellCastTable[spell].t > SpellCastTable[spell].d or ignoreCd then + return true + else + return false + end + end + if getSpellData(spell) then + if (ignoreCd or not getSpellCoolDown(spell)) and + (ignoreRL or level() >= getSpellData(spell).level and mana() >= + getSpellData(spell).mana) then + return true + else + return false + end + end + -- if no data nor spell table then return true + return true +end + +local lastPhrase = "" +onTalk(function(name, level, mode, text, channelId, pos) + if name == player:getName() then + lastPhrase = text:lower() + end +end) + +if onSpellCooldown and onGroupSpellCooldown then + onSpellCooldown(function(iconId, duration) + schedule(1, function() + if not vBot.customCooldowns[lastPhrase] then + vBot.customCooldowns[lastPhrase] = {id = iconId} + end + end) + end) + + onGroupSpellCooldown(function(iconId, duration) + schedule(2, function() + if vBot.customCooldowns[lastPhrase] then + vBot.customCooldowns[lastPhrase] = {id = vBot.customCooldowns[lastPhrase].id, group = {[iconId] = duration}} + end + end) + end) +else + warn("Outdated OTClient! update to newest version to take benefits from all scripts!") +end + +-- exctracts data about spell from gamelib SpellInfo table +-- returns table +-- ie:['Spell Name'] = {id, words, exhaustion, premium, type, icon, mana, level, soul, group, vocations} +-- cooldown detection module +function getSpellData(spell) + if not spell then return false end + spell = spell:lower() + local t = nil + local c = nil + for k, v in pairs(Spells) do + if v.words == spell then + t = k + break + end + end + if not t then + for k, v in pairs(vBot.customCooldowns) do + if k == spell then + c = {id = v.id, mana = 1, level = 1, group = v.group} + break + end + end + end + if t then + return Spells[t] + elseif c then + return c + else + return false + end +end + +-- based on info extracted by getSpellData checks if spell is on cooldown +-- returns boolean +function getSpellCoolDown(text) + if not text then return nil end + text = text:lower() + local data = getSpellData(text) + if not data then return false end + local icon = modules.game_cooldown.isCooldownIconActive(data.id) + local group = false + for groupId, duration in pairs(data.group) do + if modules.game_cooldown.isGroupCooldownIconActive(groupId) then + group = true + break + end + end + if icon or group then + return true + else + return false + end +end + +-- global var to indicate that player is trying to do something +-- prevents action blocking by scripts +-- below callbacks are triggers to changing the var state +local isUsingTime = now +macro(100, function() + vBot.isUsing = now < isUsingTime and true or false +end) +onUse(function(pos, itemId, stackPos, subType) + if pos.x > 65000 then return end + if getDistanceBetween(player:getPosition(), pos) > 1 then return end + local tile = g_map.getTile(pos) + if not tile then return end + + local topThing = tile:getTopUseThing() + if topThing:isContainer() then return end + + isUsingTime = now + 1000 +end) +onUseWith(function(pos, itemId, target, subType) + if pos.x < 65000 then isUsingTime = now + 1000 end +end) + +-- returns first word in string +function string.starts(String, Start) + return string.sub(String, 1, string.len(Start)) == Start +end + +-- global tables for cached players to prevent unnecesary resource consumption +-- probably still can be improved, TODO in future +-- c can be creature or string +-- if exected then adds name or name and creature to tables +-- returns boolean +CachedFriends = {} +CachedEnemies = {} +function isFriend(c) + local name = c + if type(c) ~= "string" then + if c == player then return true end + name = c:getName() + end + + if CachedFriends[c] then return true end + if CachedEnemies[c] then return false end + + if table.find(storage.playerList.friendList, name) then + CachedFriends[c] = true + return true + elseif vBot.BotServerMembers[name] ~= nil then + CachedFriends[c] = true + return true + elseif storage.playerList.groupMembers then + local p = c + if type(c) == "string" then p = getCreatureByName(c, true) end + if not p then return false end + if p:isLocalPlayer() then return true end + if p:isPlayer() then + if p:isPartyMember() then + CachedFriends[c] = true + CachedFriends[p] = true + return true + end + end + else + return false + end +end + +-- similar to isFriend but lighter version +-- accepts only name string +-- returns boolean +function isEnemy(c) + local name = c + local p + if type(c) ~= "string" then + if c == player then return false end + name = c:getName() + p = c + end + if not name then return false end + if not p then + p = getCreatureByName(name, true) + end + if not p then return end + if p:isLocalPlayer() then return end + + if p:isPlayer() and table.find(storage.playerList.enemyList, name) or + (storage.playerList.marks and not isFriend(name)) or p:getEmblem() == 2 then + return true + else + return false + end +end + +function getPlayerDistribution() + local friends = {} + local neutrals = {} + local enemies = {} + for i, spec in ipairs(getSpectators()) do + if spec:isPlayer() and not spec:isLocalPlayer() then + if isFriend(spec) then + table.insert(friends, spec) + elseif isEnemy(spec) then + table.insert(enemies, spec) + else + table.insert(neutrals, spec) + end + end + end + + return friends, neutrals, enemies +end + +function getFriends() + local friends, neutrals, enemies = getPlayerDistribution() + + return friends +end + +function getNeutrals() + local friends, neutrals, enemies = getPlayerDistribution() + + return neutrals +end + +function getEnemies() + local friends, neutrals, enemies = getPlayerDistribution() + + return enemies +end + +-- based on first word in string detects if text is a offensive spell +-- returns boolean +function isAttSpell(expr) + if string.starts(expr, "exori") or string.starts(expr, "exevo") then + return true + else + return false + end +end + +-- returns dressed-up item id based on not dressed id +-- returns number +function getActiveItemId(id) + if not id then return false end + + if id == 3049 then + return 3086 + elseif id == 3050 then + return 3087 + elseif id == 3051 then + return 3088 + elseif id == 3052 then + return 3089 + elseif id == 3053 then + return 3090 + elseif id == 3091 then + return 3094 + elseif id == 3092 then + return 3095 + elseif id == 3093 then + return 3096 + elseif id == 3097 then + return 3099 + elseif id == 3098 then + return 3100 + elseif id == 16114 then + return 16264 + elseif id == 23531 then + return 23532 + elseif id == 23533 then + return 23534 + elseif id == 23544 then + return 23528 + elseif id == 23529 then + return 23530 + elseif id == 30343 then -- Sleep Shawl + return 30342 + elseif id == 30344 then -- Enchanted Pendulet + return 30345 + elseif id == 30403 then -- Enchanted Theurgic Amulet + return 30402 + elseif id == 31621 then -- Blister Ring + return 31616 + elseif id == 32621 then -- Ring of Souls + return 32635 + else + return id + end +end + +-- returns not dressed item id based on dressed-up id +-- returns number +function getInactiveItemId(id) + if not id then return false end + + if id == 3086 then + return 3049 + elseif id == 3087 then + return 3050 + elseif id == 3088 then + return 3051 + elseif id == 3089 then + return 3052 + elseif id == 3090 then + return 3053 + elseif id == 3094 then + return 3091 + elseif id == 3095 then + return 3092 + elseif id == 3096 then + return 3093 + elseif id == 3099 then + return 3097 + elseif id == 3100 then + return 3098 + elseif id == 16264 then + return 16114 + elseif id == 23532 then + return 23531 + elseif id == 23534 then + return 23533 + elseif id == 23530 then + return 23529 + elseif id == 30342 then -- Sleep Shawl + return 30343 + elseif id == 30345 then -- Enchanted Pendulet + return 30344 + elseif id == 30402 then -- Enchanted Theurgic Amulet + return 30403 + elseif id == 31616 then -- Blister Ring + return 31621 + elseif id == 32635 then -- Ring of Souls + return 32621 + else + return id + end +end + +-- returns amount of monsters within the range of position +-- does not include summons (new tibia) +-- returns number +function getMonstersInRange(pos, range) + if not pos or not range then return false end + local monsters = 0 + for i, spec in pairs(getSpectators()) do + if spec:isMonster() and + (g_game.getClientVersion() < 960 or spec:getType() < 3) and + getDistanceBetween(pos, spec:getPosition()) < range then + monsters = monsters + 1 + end + end + return monsters +end + +-- shortcut in calculating distance from local player position +-- needs only one argument +-- returns number +function distanceFromPlayer(coords) + if not coords then return false end + return getDistanceBetween(pos(), coords) +end + +-- returns amount of monsters within the range of local player position +-- does not include summons (new tibia) +-- can also check multiple floors +-- returns number +function getMonsters(range, multifloor) + if not range then range = 10 end + local mobs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + mobs = (g_game.getClientVersion() < 960 or spec:getType() < 3) and + spec:isMonster() and distanceFromPlayer(spec:getPosition()) <= + range and mobs + 1 or mobs; + end + return mobs; +end + +-- returns amount of players within the range of local player position +-- does not include party members +-- can also check multiple floors +-- returns number +function getPlayers(range, multifloor) + if not range then range = 10 end + local specs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + if not spec:isLocalPlayer() and spec:isPlayer() and distanceFromPlayer(spec:getPosition()) <= range and not ((spec:getShield() ~= 1 and spec:isPartyMember()) or spec:getEmblem() == 1) then + specs = specs + 1 + end + end + return specs; +end + +-- this is multifloor function +-- checks if player added in "Anti RS list" in player list is within the given range +-- returns boolean +function isBlackListedPlayerInRange(range) + if #storage.playerList.blackList == 0 then return end + if not range then range = 10 end + local found = false + for _, spec in pairs(getSpectators(true)) do + local specPos = spec:getPosition() + local pPos = player:getPosition() + if spec:isPlayer() then + if math.abs(specPos.z - pPos.z) <= 2 then + if specPos.z ~= pPos.z then specPos.z = pPos.z end + if distanceFromPlayer(specPos) < range then + if table.find(storage.playerList.blackList, spec:getName()) then + found = true + end + end + end + end + end + return found +end + +-- checks if there is non-friend player withing the range +-- padding is only for multifloor +-- returns boolean +function isSafe(range, multifloor, padding) + local onSame = 0 + local onAnother = 0 + if not multifloor and padding then + multifloor = false + padding = false + end + + for _, spec in pairs(getSpectators(multifloor)) do + if spec:isPlayer() and not spec:isLocalPlayer() and + not isFriend(spec:getName()) then + if spec:getPosition().z == posz() and + distanceFromPlayer(spec:getPosition()) <= range then + onSame = onSame + 1 + end + if multifloor and padding and spec:getPosition().z ~= posz() and + distanceFromPlayer(spec:getPosition()) <= (range + padding) then + onAnother = onAnother + 1 + end + end + end + + if onSame + onAnother > 0 then + return false + else + return true + end +end + +-- returns amount of players within the range of local player position +-- can also check multiple floors +-- returns number +function getAllPlayers(range, multifloor) + if not range then range = 10 end + local specs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + specs = not spec:isLocalPlayer() and spec:isPlayer() and + distanceFromPlayer(spec:getPosition()) <= range and specs + + 1 or specs; + end + return specs; +end + +-- returns amount of NPC's within the range of local player position +-- can also check multiple floors +-- returns number +function getNpcs(range, multifloor) + if not range then range = 10 end + local npcs = 0; + for _, spec in pairs(getSpectators(multifloor)) do + npcs = + spec:isNpc() and distanceFromPlayer(spec:getPosition()) <= range and + npcs + 1 or npcs; + end + return npcs; +end + +-- main function for calculatin item amount in all visible containers +-- also considers equipped items +-- returns number +function itemAmount(id) + return player:getItemsCount(id) +end + +-- self explanatory +-- a is item to use on +-- b is item to use a on +function useOnInvertoryItem(a, b) + local item = findItem(b) + if not item then return end + + return useWith(a, item) +end + +-- pos can be tile or position +-- returns table of tiles surrounding given POS/tile +function getNearTiles(pos) + if type(pos) ~= "table" then pos = pos:getPosition() end + + local tiles = {} + local dirs = { + {-1, 1}, {0, 1}, {1, 1}, {-1, 0}, {1, 0}, {-1, -1}, {0, -1}, {1, -1} + } + for i = 1, #dirs do + local tile = g_map.getTile({ + x = pos.x - dirs[i][1], + y = pos.y - dirs[i][2], + z = pos.z + }) + if tile then table.insert(tiles, tile) end + end + + return tiles +end + +-- self explanatory +-- use along with delay, it will only call action +function useGroundItem(id) + if not id then return false end + + local dest = nil + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + if item:getId() == id then + dest = item + break + end + end + end + + if dest then + return use(dest) + else + return false + end +end + +-- self explanatory +-- use along with delay, it will only call action +function reachGroundItem(id) + if not id then return false end + + local dest = nil + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + local iPos = item:getPosition() + local iId = item:getId() + if iId == id then + if findPath(pos(), iPos, 20, + {ignoreNonPathable = true, precision = 1}) then + dest = item + break + end + end + end + end + + if dest then + return autoWalk(iPos, 20, {ignoreNonPathable = true, precision = 1}) + else + return false + end +end + +-- self explanatory +-- returns object +function findItemOnGround(id) + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + if item:getId() == id then return item end + end + end +end + +-- self explanatory +-- use along with delay, it will only call action +function useOnGroundItem(a, b) + if not b then return false end + local item = findItem(a) + if not item then return false end + + local dest = nil + for i, tile in ipairs(g_map.getTiles(posz())) do + for j, item in ipairs(tile:getItems()) do + if item:getId() == id then + dest = item + break + end + end + end + + if dest then + return useWith(item, dest) + else + return false + end +end + +-- returns target creature +function target() + if not g_game.isAttacking() then + return + else + return g_game.getAttackingCreature() + end +end + +-- returns target creature +function getTarget() return target() end + +-- dist is boolean +-- returns target position/distance from player +function targetPos(dist) + if not g_game.isAttacking() then return end + if dist then + return distanceFromPlayer(target():getPosition()) + else + return target():getPosition() + end +end + +-- for gunzodus/ezodus only +-- it will reopen loot bag, necessary for depositer +function reopenPurse() + for i, c in pairs(getContainers()) do + if c:getName():lower() == "loot bag" or c:getName():lower() == + "store inbox" then g_game.close(c) end + end + schedule(100, function() + g_game.use(g_game.getLocalPlayer():getInventoryItem(InventorySlotPurse)) + end) + schedule(1400, function() + for i, c in pairs(getContainers()) do + if c:getName():lower() == "store inbox" then + for _, i in pairs(c:getItems()) do + if i:getId() == 23721 then + g_game.open(i, c) + end + end + end + end + end) + return CaveBot.delay(1500) +end + +-- getSpectator patterns +-- param1 - pos/creature +-- param2 - pattern +-- param3 - type of return +-- 1 - everyone, 2 - monsters, 3 - players +-- returns number +function getCreaturesInArea(param1, param2, param3) + local specs = 0 + local monsters = 0 + local players = 0 + for i, spec in pairs(getSpectators(param1, param2)) do + if spec ~= player then + specs = specs + 1 + if spec:isMonster() and + (g_game.getClientVersion() < 960 or spec:getType() < 3) then + monsters = monsters + 1 + elseif spec:isPlayer() and not isFriend(spec:getName()) then + players = players + 1 + end + end + end + + if param3 == 1 then + return specs + elseif param3 == 2 then + return monsters + else + return players + end +end + +-- can be improved +-- TODO in future +-- uses getCreaturesInArea, specType +-- returns number +function getBestTileByPatern(pattern, specType, maxDist, safe) + if not pattern or not specType then return end + if not maxDist then maxDist = 4 end + + local bestTile = nil + local best = nil + for _, tile in pairs(g_map.getTiles(posz())) do + if distanceFromPlayer(tile:getPosition()) <= maxDist then + local minimapColor = g_map.getMinimapColor(tile:getPosition()) + local stairs = (minimapColor >= 210 and minimapColor <= 213) + if tile:canShoot() and tile:isWalkable() then + if getCreaturesInArea(tile:getPosition(), pattern, specType) > 0 then + if (not safe or + getCreaturesInArea(tile:getPosition(), pattern, 3) == 0) then + local candidate = + { + pos = tile, + count = getCreaturesInArea(tile:getPosition(), + pattern, specType) + } + if not best or best.count <= candidate.count then + best = candidate + end + end + end + end + end + end + + bestTile = best + + if bestTile then + return bestTile + else + return false + end +end + +-- returns container object based on name +function getContainerByName(name, notFull) + if type(name) ~= "string" then return nil end + + local d = nil + for i, c in pairs(getContainers()) do + if c:getName():lower() == name:lower() and (not notFull or not containerIsFull(c)) then + d = c + break + end + end + return d +end + +-- returns container object based on container ID +function getContainerByItem(id, notFull) + if type(id) ~= "number" then return nil end + + local d = nil + for i, c in pairs(getContainers()) do + if c:getContainerItem():getId() == id and (not notFull or not containerIsFull(c)) then + d = c + break + end + end + return d +end + +-- [[ ready to use getSpectators patterns ]] -- +LargeUeArea = [[ + 0000001000000 + 0000011100000 + 0000111110000 + 0001111111000 + 0011111111100 + 0111111111110 + 1111111111111 + 0111111111110 + 0011111111100 + 0001111111000 + 0000111110000 + 0000011100000 + 0000001000000 +]] + +NormalUeAreaMs = [[ + 00000100000 + 00011111000 + 00111111100 + 01111111110 + 01111111110 + 11111111111 + 01111111110 + 01111111110 + 00111111100 + 00001110000 + 00000100000 +]] + +NormalUeAreaEd = [[ + 00000100000 + 00001110000 + 00011111000 + 00111111100 + 01111111110 + 11111111111 + 01111111110 + 00111111100 + 00011111000 + 00001110000 + 00000100000 +]] + +smallUeArea = [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 +]] + +largeRuneArea = [[ + 0011100 + 0111110 + 1111111 + 1111111 + 1111111 + 0111110 + 0011100 +]] + +adjacentArea = [[ + 111 + 101 + 111 +]] + +longBeamArea = [[ + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + 0000000N0000000 + WWWWWWW0EEEEEEE + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 + 0000000S0000000 +]] + +shortBeamArea = [[ + 00000100000 + 00000100000 + 00000100000 + 00000100000 + 00000100000 + EEEEE0WWWWW + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 + 00000S00000 +]] + +newWaveArea = [[ + 000NNNNN000 + 000NNNNN000 + 0000NNN0000 + WW00NNN00EE + WWWW0N0EEEE + WWWWW0EEEEE + WWWW0S0EEEE + WW00SSS00EE + 0000SSS0000 + 000SSSSS000 + 000SSSSS000 +]] + +bigWaveArea = [[ + 0000NNN0000 + 0000NNN0000 + 0000NNN0000 + 00000N00000 + WWW00N00EEE + WWWWW0EEEEE + WWW00S00EEE + 00000S00000 + 0000SSS0000 + 0000SSS0000 + 0000SSS0000 +]] + +smallWaveArea = [[ + 00NNN00 + 00NNN00 + WW0N0EE + WWW0EEE + WW0S0EE + 00SSS00 + 00SSS00 +]] + +diamondArrowArea = [[ + 01110 + 11111 + 11111 + 11111 + 01110 +]] diff --git a/modules/game_bot/default_configs/vBot_4.8/vBot/xeno_menu.lua b/modules/game_bot/default_configs/vBot_4.8/vBot/xeno_menu.lua new file mode 100644 index 0000000000..2583500579 --- /dev/null +++ b/modules/game_bot/default_configs/vBot_4.8/vBot/xeno_menu.lua @@ -0,0 +1,30 @@ +modules.game_interface.gameRootPanel.onMouseRelease = function(widget, mousePos, mouseButton) + if mouseButton == 2 then + local child = rootWidget:recursiveGetChildByPos(mousePos) + if child == widget then + local menu = g_ui.createWidget('PopupMenu') + menu:setId("blzMenu") + menu:setGameMenu(true) + menu:addOption('AttackBot', AttackBot.show, "OTCv8") + menu:addOption('HealBot', HealBot.show, "OTCv8") + menu:addOption('Conditions', Conditions.show, "OTCv8") + menu:addSeparator() + menu:addOption('CaveBot', function() + if CaveBot.isOn() then + CaveBot.setOff() + else + CaveBot.setOn() + end + end, CaveBot.isOn() and "ON " or "OFF ") + menu:addOption('TargetBot', function() + if TargetBot.isOn() then + TargetBot.setOff() + else + TargetBot.setOn() + end + end, TargetBot.isOn() and "ON " or "OFF ") + menu:display(mousePos) + return true + end + end +end \ No newline at end of file diff --git a/modules/game_bot/edit.otui b/modules/game_bot/edit.otui new file mode 100644 index 0000000000..e45ad6eae6 --- /dev/null +++ b/modules/game_bot/edit.otui @@ -0,0 +1,254 @@ +MainWindow + id: editWindow + !text: tr("Config editor & manager") + @onEscape: self:hide() + size: 550 570 + $mobile: + size: 550 240 + + Panel + id: manager + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 152 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Config Manager\nYou can use config manager to share configs between different machines, especially smartphones. After you configure your config, you can upload it, then you'll get unique hash code which you can use on diffent machinge (for eg. mobile phone) to download it.") + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + height: 2 + + Panel + id: upload + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.bottom: parent.bottom + margin-top: 3 + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Upload config") + + Label + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 7 + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Select config to upload") + + ComboBox + id: config + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 4 + margin-left: 20 + margin-right: 20 + text-offset: 3 0 + + Button + id: submit + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + !text: tr('Upload config') + margin-top: 4 + margin-left: 40 + margin-right: 40 + @onClick: modules.game_bot.uploadConfig() + + Panel + id: download + anchors.top: prev.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.bottom: parent.bottom + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Download config") + + Label + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 7 + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Enter config hash code") + + TextEdit + id: config + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 4 + margin-left: 20 + margin-right: 20 + + Button + id: submit + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + !text: tr('Download config') + margin-top: 4 + margin-left: 40 + margin-right: 40 + @onClick: modules.game_bot.downloadConfig() + + HorizontalSeparator + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + height: 2 + + Panel + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 5 + height: 330 + $mobile: + visible: false + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Bot configs are stored in:") + + TextEdit + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + height: 20 + width: 400 + margin-top: 5 + editable: false + !text: g_resources.getWriteDir() .. "bot" + text-align: center + + Button + id: documentationButton + !text: tr('Click here to open bot directory') + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + width: 250 + @onClick: g_platform.openDir(g_resources.getWriteDir() .. "bot") + + Label + margin-top: 5 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Every directory in bot directory is treated as different config.\nTo create new config just create new directory.") + + Label + margin-top: 5 + anchors.top: prev.bottom + anchors.horizontalCenter: parent.horizontalCenter + height: 175 + image-source: configs.png + image-fixed-ratio: true + image-size: 500 175 + + Label + margin-top: 3 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Inside config directory put .lua and .otui files.\nEvery file will be loaded and executed in alphabetical order, .otui first and then .lua.") + + Label + margin-top: 3 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("To reload configs just press On and Off in bot window.\nTo learn more about bot click Tutorials button.") + + Button + !text: tr('Documentation') + anchors.bottom: parent.bottom + anchors.left: parent.left + width: 118 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?documentation") + + Button + !text: tr('Tutorials') + anchors.bottom: parent.bottom + anchors.left: prev.right + margin-left: 5 + width: 80 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?tutorials") + + Button + !text: tr('Scripts') + anchors.bottom: parent.bottom + anchors.left: prev.right + margin-left: 5 + width: 80 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?scripts") + + Button + !text: tr('Forum') + anchors.bottom: parent.bottom + anchors.left: prev.right + margin-left: 5 + width: 80 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?forum") + + Button + !text: tr('Discord') + anchors.bottom: parent.bottom + anchors.left: prev.right + margin-left: 5 + width: 80 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?discord") + + Button + id: cancelButton + !text: tr('Close') + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 60 + @onClick: self:getParent():hide() diff --git a/modules/game_bot/executor.lua b/modules/game_bot/executor.lua new file mode 100644 index 0000000000..0ca87a93ad --- /dev/null +++ b/modules/game_bot/executor.lua @@ -0,0 +1,423 @@ +function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, reloadCallback, websockets) + -- load lua and otui files + local configFiles = g_resources.listDirectoryFiles("/bot/" .. config, true, false) + local luaFiles = {} + local uiFiles = {} + for i, file in ipairs(configFiles) do + local ext = file:split(".") + if ext[#ext]:lower() == "lua" then + table.insert(luaFiles, file) + end + if ext[#ext]:lower() == "ui" or ext[#ext]:lower() == "otui" then + table.insert(uiFiles, file) + end + end + + if #luaFiles == 0 then + return error("Config (/bot/" .. config .. ") doesn't have lua files") + end + + -- init bot variables + local context = {} + context.configDir = "/bot/".. config + context.tabs = tabs + context.mainTab = context.tabs:addTab("Main", g_ui.createWidget('BotPanel')).tabPanel.content + context.panel = context.mainTab + context.saveConfig = saveConfigCallback + context.reload = reloadCallback + + context.storage = storage + if context.storage._macros == nil then + context.storage._macros = {} -- active macros + end + + -- websockets, macros, hotkeys, scheduler, icons, callbacks + context._websockets = websockets + context._macros = {} + context._hotkeys = {} + context._scheduler = {} + context._callbacks = { + onKeyDown = {}, + onKeyUp = {}, + onKeyPress = {}, + onTalk = {}, + onTextMessage = {}, + onLoginAdvice = {}, + onAddThing = {}, + onRemoveThing = {}, + onCreatureAppear = {}, + onCreatureDisappear = {}, + onCreaturePositionChange = {}, + onCreatureHealthPercentChange = {}, + onUse = {}, + onUseWith = {}, + onContainerOpen = {}, + onContainerClose = {}, + onContainerUpdateItem = {}, + onMissle = {}, + onAnimatedText = {}, + onStaticText = {}, + onChannelList = {}, + onOpenChannel = {}, + onCloseChannel = {}, + onChannelEvent = {}, + onTurn = {}, + onWalk = {}, + onImbuementWindow = {}, + onModalDialog = {}, + onAttackingCreatureChange = {}, + onManaChange = {}, + onStatesChange = {}, + onAddItem = {}, + onGameEditText = {}, + onGroupSpellCooldown = {}, + onSpellCooldown = {}, + onRemoveItem = {}, + onInventoryChange = {} + } + + -- basic functions & classes + context.print = print + context.bit32 = bit32 + context.bit = bit + context.pairs = pairs + context.ipairs = ipairs + context.tostring = tostring + context.math = math + context.table = table + context.setmetatable = setmetatable + context.string = string + context.tonumber = tonumber + context.type = type + context.pcall = pcall + context.os = { + time = os.time, + difftime = os.difftime, + date = os.date, + clock = os.clock + } + context.load = function(str) return assert(load(str, nil, nil, context)) end + context.loadstring = context.load + context.assert = assert + context.dofile = function(file) assert(load(g_resources.readFileContents("/bot/" .. config .. "/" .. file), file, nil, context))() end + context.gcinfo = gcinfo + context.tr = tr + context.json = json + context.base64 = base64 + context.regexMatch = regexMatch + context.getDistanceBetween = function(p1, p2) + return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + end + context.isMobile = g_app.isMobile + context.getVersion = g_app.getVersion + + -- classes + context.g_resources = g_resources + context.g_game = g_game + context.g_map = g_map + context.g_ui = g_ui + context.g_sounds = g_sounds + context.g_window = g_window + context.g_mouse = g_mouse + context.g_keyboard = g_keyboard + context.g_things = g_things + context.g_settings = g_settings + context.g_platform = { + openUrl = g_platform.openUrl, + openDir = g_platform.openDir, + } + + context.Item = Item + context.Creature = Creature + context.ThingType = ThingType + context.Effect = Effect + context.Missile = Missile + context.Player = Player + context.Monster = Monster + context.StaticText = StaticText + context.HTTP = HTTP + context.OutputMessage = OutputMessage + context.modules = modules + + -- log functions + context.info = function(text) return msgCallback("info", tostring(text)) end + context.warn = function(text) return msgCallback("warn", tostring(text)) end + context.error = function(text) return msgCallback("error", tostring(text)) end + context.warning = context.warn + + -- init context + context.now = g_clock.millis() + context.time = g_clock.millis() + context.player = g_game.getLocalPlayer() + + -- init functions + G.botContext = context + dofiles("functions") + context.Panels = {} + dofiles("panels") + G.botContext = nil + + -- run ui scripts + for i, file in ipairs(uiFiles) do + g_ui.importStyle(file) + end + + -- run lua script + for i, file in ipairs(luaFiles) do + assert(load(g_resources.readFileContents(file), file, nil, context))() + context.panel = context.mainTab -- reset default tab + end + + return { + script = function() + context.now = g_clock.millis() + context.time = g_clock.millis() + + for i, macro in ipairs(context._macros) do + if macro.lastExecution + macro.timeout <= context.now and macro.enabled then + local status, result = pcall(function() + if macro.callback(macro) then + macro.lastExecution = context.now + end + end) + if not status then + context.error("Macro: " .. macro.name .. " execution error: " .. result) + end + end + end + + while #context._scheduler > 0 and context._scheduler[1].execution <= g_clock.millis() do + local status, result = pcall(function() + context._scheduler[1].callback() + end) + if not status then + context.error("Schedule execution error: " .. result) + end + table.remove(context._scheduler, 1) + end + end, + callbacks = { + onKeyDown = function(keyCode, keyboardModifiers) + local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers) + for i, macro in ipairs(context._macros) do + if macro.switch and macro.hotkey == keyDesc then + macro.switch:onClick() + end + end + local hotkey = context._hotkeys[keyDesc] + if hotkey then + if hotkey.single then + if hotkey.callback() then + hotkey.lastExecution = context.now + end + end + if hotkey.switch then + hotkey.switch:setOn(true) + end + end + for i, callback in ipairs(context._callbacks.onKeyDown) do + callback(keyDesc) + end + end, + onKeyUp = function(keyCode, keyboardModifiers) + local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers) + local hotkey = context._hotkeys[keyDesc] + if hotkey then + if hotkey.switch then + hotkey.switch:setOn(false) + end + end + for i, callback in ipairs(context._callbacks.onKeyUp) do + callback(keyDesc) + end + end, + onKeyPress = function(keyCode, keyboardModifiers, autoRepeatTicks) + local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers) + local hotkey = context._hotkeys[keyDesc] + if hotkey and not hotkey.single then + if hotkey.callback() then + hotkey.lastExecution = context.now + end + end + for i, callback in ipairs(context._callbacks.onKeyPress) do + callback(keyDesc, autoRepeatTicks) + end + end, + onTalk = function(name, level, mode, text, channelId, pos) + for i, callback in ipairs(context._callbacks.onTalk) do + callback(name, level, mode, text, channelId, pos) + end + end, + onImbuementWindow = function(itemId, slots, activeSlots, imbuements, needItems) + for i, callback in ipairs(context._callbacks.onImbuementWindow) do + callback(itemId, slots, activeSlots, imbuements, needItems) + end + end, + onTextMessage = function(mode, text) + for i, callback in ipairs(context._callbacks.onTextMessage) do + callback(mode, text) + end + end, + onLoginAdvice = function(message) + for i, callback in ipairs(context._callbacks.onLoginAdvice) do + callback(message) + end + end, + onAddThing = function(tile, thing) + for i, callback in ipairs(context._callbacks.onAddThing) do + callback(tile, thing) + end + end, + onRemoveThing = function(tile, thing) + for i, callback in ipairs(context._callbacks.onRemoveThing) do + callback(tile, thing) + end + end, + onCreatureAppear = function(creature) + for i, callback in ipairs(context._callbacks.onCreatureAppear) do + callback(creature) + end + end, + onCreatureDisappear = function(creature) + for i, callback in ipairs(context._callbacks.onCreatureDisappear) do + callback(creature) + end + end, + onCreaturePositionChange = function(creature, newPos, oldPos) + for i, callback in ipairs(context._callbacks.onCreaturePositionChange) do + callback(creature, newPos, oldPos) + end + end, + onCreatureHealthPercentChange = function(creature, healthPercent) + for i, callback in ipairs(context._callbacks.onCreatureHealthPercentChange) do + callback(creature, healthPercent) + end + end, + onUse = function(pos, itemId, stackPos, subType) + for i, callback in ipairs(context._callbacks.onUse) do + callback(pos, itemId, stackPos, subType) + end + end, + onUseWith = function(pos, itemId, target, subType) + for i, callback in ipairs(context._callbacks.onUseWith) do + callback(pos, itemId, target, subType) + end + end, + onContainerOpen = function(container, previousContainer) + for i, callback in ipairs(context._callbacks.onContainerOpen) do + callback(container, previousContainer) + end + end, + onContainerClose = function(container) + for i, callback in ipairs(context._callbacks.onContainerClose) do + callback(container) + end + end, + onContainerUpdateItem = function(container, slot, item, oldItem) + for i, callback in ipairs(context._callbacks.onContainerUpdateItem) do + callback(container, slot, item, oldItem) + end + end, + onMissle = function(missle) + for i, callback in ipairs(context._callbacks.onMissle) do + callback(missle) + end + end, + onAnimatedText = function(thing, text) + for i, callback in ipairs(context._callbacks.onAnimatedText) do + callback(thing, text) + end + end, + onStaticText = function(thing, text) + for i, callback in ipairs(context._callbacks.onStaticText) do + callback(thing, text) + end + end, + onChannelList = function(channels) + for i, callback in ipairs(context._callbacks.onChannelList) do + callback(channels) + end + end, + onOpenChannel = function(channelId, channelName) + for i, callback in ipairs(context._callbacks.onOpenChannel) do + callback(channels) + end + end, + onCloseChannel = function(channelId) + for i, callback in ipairs(context._callbacks.onCloseChannel) do + callback(channelId) + end + end, + onChannelEvent = function(channelId, name, event) + for i, callback in ipairs(context._callbacks.onChannelEvent) do + callback(channelId, name, event) + end + end, + onTurn = function(creature, direction) + for i, callback in ipairs(context._callbacks.onTurn) do + callback(creature, direction) + end + end, + onWalk = function(creature, oldPos, newPos) + for i, callback in ipairs(context._callbacks.onWalk) do + callback(creature, oldPos, newPos) + end + end, + onModalDialog = function(id, title, message, buttons, enterButton, escapeButton, choices, priority) + for i, callback in ipairs(context._callbacks.onModalDialog) do + callback(id, title, message, buttons, enterButton, escapeButton, choices, priority) + end + end, + onGameEditText = function(id, itemId, maxLength, text, writer, time) + for i, callback in ipairs(context._callbacks.onGameEditText) do + callback(id, itemId, maxLength, text, writer, time) + end + end, + onAttackingCreatureChange = function(creature, oldCreature) + for i, callback in ipairs(context._callbacks.onAttackingCreatureChange) do + callback(creature, oldCreature) + end + end, + onManaChange = function(player, mana, maxMana, oldMana, oldMaxMana) + for i, callback in ipairs(context._callbacks.onManaChange) do + callback(player, mana, maxMana, oldMana, oldMaxMana) + end + end, + onAddItem = function(container, slot, item) + for i, callback in ipairs(context._callbacks.onAddItem) do + callback(container, slot, item) + end + end, + onRemoveItem = function(container, slot, item) + for i, callback in ipairs(context._callbacks.onRemoveItem) do + callback(container, slot, item) + end + end, + onStatesChange = function(player, states, oldStates) + for i, callback in ipairs(context._callbacks.onStatesChange) do + callback(player, states, oldStates) + end + end, + onGroupSpellCooldown = function(iconId, duration) + for i, callback in ipairs(context._callbacks.onGroupSpellCooldown) do + callback(iconId, duration) + end + end, + onSpellCooldown = function(iconId, duration) + for i, callback in ipairs(context._callbacks.onSpellCooldown) do + callback(iconId, duration) + end + end, + onSpellCooldown = function(iconId, duration) + for i, callback in ipairs(context._callbacks.onSpellCooldown) do + callback(iconId, duration) + end + end, + onInventoryChange = function(player, slot, item, oldItem) + for i, callback in ipairs(context._callbacks.onInventoryChange) do + callback(player, slot, item, oldItem) + end + end + } + } +end \ No newline at end of file diff --git a/modules/game_bot/functions/callbacks.lua b/modules/game_bot/functions/callbacks.lua new file mode 100644 index 0000000000..aeefcf4c60 --- /dev/null +++ b/modules/game_bot/functions/callbacks.lua @@ -0,0 +1,271 @@ +local context = G.botContext + +-- callback(callbackType, callback) +context.callback = function(callbackType, callback) + if not context._callbacks[callbackType] then + return error("Wrong callback type: " .. callbackType) + end + if callbackType == "onAddThing" or callbackType == "onRemoveThing" then + g_game.enableTileThingLuaCallback(true) + end + + local desc = "lua" + local info = debug.getinfo(2, "Sl") + if info then + desc = info.short_src .. ":" .. info.currentline + end + + local callbackData = {} + table.insert(context._callbacks[callbackType], function(...) + if not callbackData.delay or callbackData.delay < context.now then + local prevExecution = context._currentExecution + context._currentExecution = callbackData + local start = g_clock.realMillis() + callback(...) + local executionTime = g_clock.realMillis() - start + if executionTime > 100 then + context.warning("Slow " .. callbackType .. " (" .. executionTime .. "ms): " .. desc) + end + 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) +context.onKeyDown = function(callback) + return context.callback("onKeyDown", callback) +end + +-- onKeyPress(callback) -- callback = function(keys) +context.onKeyPress = function(callback) + return context.callback("onKeyPress", callback) +end + +-- onKeyUp(callback) -- callback = function(keys) +context.onKeyUp = function(callback) + return context.callback("onKeyUp", callback) +end + +-- onTalk(callback) -- callback = function(name, level, mode, text, channelId, pos) +context.onTalk = function(callback) + return context.callback("onTalk", callback) +end + +-- onTextMessage(callback) -- callback = function(mode, text) +context.onTextMessage = function(callback) + return context.callback("onTextMessage", callback) +end + +-- onLoginAdvice(callback) -- callback = function(message) +context.onLoginAdvice = function(callback) + return context.callback("onLoginAdvice", callback) +end + +-- onAddThing(callback) -- callback = function(tile, thing) +context.onAddThing = function(callback) + return context.callback("onAddThing", callback) +end + +-- onRemoveThing(callback) -- callback = function(tile, thing) +context.onRemoveThing = function(callback) + return context.callback("onRemoveThing", callback) +end + +-- onCreatureAppear(callback) -- callback = function(creature) +context.onCreatureAppear = function(callback) + return context.callback("onCreatureAppear", callback) +end + +-- onCreatureDisappear(callback) -- callback = function(creature) +context.onCreatureDisappear = function(callback) + return context.callback("onCreatureDisappear", callback) +end + +-- onCreaturePositionChange(callback) -- callback = function(creature, newPos, oldPos) +context.onCreaturePositionChange = function(callback) + return context.callback("onCreaturePositionChange", callback) +end + +-- onCreatureHealthPercentChange(callback) -- callback = function(creature, healthPercent) +context.onCreatureHealthPercentChange = function(callback) + return context.callback("onCreatureHealthPercentChange", callback) +end + +-- onUse(callback) -- callback = function(pos, itemId, stackPos, subType) +context.onUse = function(callback) + return context.callback("onUse", callback) +end + +-- onUseWith(callback) -- callback = function(pos, itemId, target, subType) +context.onUseWith = function(callback) + return context.callback("onUseWith", callback) +end + +-- onContainerOpen -- callback = function(container, previousContainer) +context.onContainerOpen = function(callback) + return context.callback("onContainerOpen", callback) +end + +-- onContainerClose -- callback = function(container) +context.onContainerClose = function(callback) + return context.callback("onContainerClose", callback) +end + +-- onContainerUpdateItem -- callback = function(container, slot, item, oldItem) +context.onContainerUpdateItem = function(callback) + return context.callback("onContainerUpdateItem", callback) +end + +-- onMissle -- callback = function(missle) +context.onMissle = function(callback) + return context.callback("onMissle", callback) +end + +-- onAnimatedText -- callback = function(thing, text) +context.onAnimatedText = function(callback) + return context.callback("onAnimatedText", callback) +end + +-- onStaticText -- callback = function(thing, text) +context.onStaticText = function(callback) + return context.callback("onStaticText", callback) +end + +-- onChannelList -- callback = function(channels) +context.onChannelList = function(callback) + return context.callback("onChannelList", callback) +end + +-- onOpenChannel -- callback = function(channelId, name) +context.onOpenChannel = function(callback) + return context.callback("onOpenChannel", callback) +end + +-- onCloseChannel -- callback = function(channelId) +context.onCloseChannel = function(callback) + return context.callback("onCloseChannel", callback) +end + +-- onChannelEvent -- callback = function(channelId, name, event) +context.onChannelEvent = function(callback) + return context.callback("onChannelEvent", callback) +end + +-- onTurn -- callback = function(creature, direction) +context.onTurn = function(callback) + return context.callback("onTurn", callback) +end + +-- onWalk -- callback = function(creature, oldPos, newPos) +context.onWalk = function(callback) + return context.callback("onWalk", callback) +end + +-- onImbuementWindow -- callback = function(itemId, slots, activeSlots, imbuements, needItems) +context.onImbuementWindow = function(callback) + return context.callback("onImbuementWindow", callback) +end + +-- onModalDialog -- callback = function(id, title, message, buttons, enterButton, escapeButton, choices, priority) -- priority is unused, ignore it +context.onModalDialog = function(callback) + return context.callback("onModalDialog", callback) +end + +-- onAttackingCreatureChange -- callback = function(creature, oldCreature) +context.onAttackingCreatureChange = function(callback) + return context.callback("onAttackingCreatureChange", callback) +end + +-- onManaChange -- callback = function(player, mana, maxMana, oldMana, oldMaxMana) +context.onManaChange = function(callback) + return context.callback("onManaChange", callback) +end + +-- onAddItem - callback = function(container, slot, item, oldItem) +context.onAddItem = function(callback) + return context.callback("onAddItem", callback) +end + +-- onRemoveItem - callback = function(container, slot, item) +context.onRemoveItem = function(callback) + return context.callback("onRemoveItem", callback) +end + +-- onStatesChange - callback = function(player, states, oldStates) +context.onStatesChange = function(callback) + return context.callback("onStatesChange", callback) +end + +-- onGameEditText - callback = function(id, itemId, maxLength, text, writer, time) +context.onGameEditText = function(callback) + return context.callback("onGameEditText", callback) +end + +-- onSpellCooldown - callback = function(iconId, duration) +context.onSpellCooldown = function(callback) + return context.callback("onSpellCooldown", callback) +end + +-- onGroupSpellCooldown - callback = function(iconId, duration) +context.onGroupSpellCooldown = function(callback) + return context.callback("onGroupSpellCooldown", callback) +end + +-- onInventoryChange - callback = function(player, slot, item, oldItem) +context.onInventoryChange = function(callback) + return context.callback("onInventoryChange", callback) +end + +-- CUSTOM CALLBACKS + +-- listen(name, callback) -- callback = function(text, channelId, pos) +context.listen = function(name, callback) + if not name then return context.error("listen: invalid name") end + name = name:lower() + return context.onTalk(function(name2, level, mode, text, channelId, pos) + if name == name2:lower() then + callback(text, channelId, pos) + end + end) +end + +-- onPlayerPositionChange(callback) -- callback = function(newPos, oldPos) +context.onPlayerPositionChange = function(callback) + return context.onCreaturePositionChange(function(creature, newPos, oldPos) + if creature == context.player then + callback(newPos, oldPos) + end + end) +end + +-- onPlayerHealthChange(callback) -- callback = function(healthPercent) +context.onPlayerHealthChange = function(callback) + return context.onCreatureHealthPercentChange(function(creature, healthPercent) + if creature == context.player then + callback(healthPercent) + end + end) +end + +-- onPlayerInventoryChange -- callback = function(slot, item, oldItem) +context.onPlayerInventoryChange = function(callback) + return context.onInventoryChange(function(player, slot, item, oldItem) + if player == context.player then + callback(slot, item, oldItem) + end + end) +end \ No newline at end of file diff --git a/modules/game_bot/functions/config.lua b/modules/game_bot/functions/config.lua new file mode 100644 index 0000000000..4341ac58e3 --- /dev/null +++ b/modules/game_bot/functions/config.lua @@ -0,0 +1,269 @@ +--[[ +Config - create, load and save config file (.json / .cfg) +Used by cavebot and other things +]] + -- + +local context = G.botContext +context.Config = {} +local Config = context.Config + +Config.exist = function(dir) + return g_resources.directoryExists(context.configDir .. "/" .. dir) +end + +Config.create = function(dir) + g_resources.makeDir(context.configDir .. "/" .. dir) + return Config.exist(dir) +end + +Config.list = function(dir) + if not Config.exist(dir) then + if not Config.create(dir) then + return contex.error("Can't create config dir: " .. context.configDir .. "/" .. dir) + end + end + local list = g_resources.listDirectoryFiles(context.configDir .. "/" .. dir) + local correctList = {} + for k, v in ipairs(list) do -- filter files + local nv = v:gsub(".json", ""):gsub(".cfg", "") + if nv ~= v then + table.insert(correctList, nv) + end + end + return correctList +end + +-- 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 + return result + end + local status, result = pcall(function() + return table.decodeStringPairList(data) + end) + if status and type(result) == 'table' then + return result + end + return context.error("Invalid config format") +end + +Config.load = function(dir, name) + local file = context.configDir .. "/" .. dir .. "/" .. name .. ".json" + if g_resources.fileExists(file) then -- load json + 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 + 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 + +Config.loadRaw = function(dir, name) + local file = context.configDir .. "/" .. dir .. "/" .. name .. ".json" + if g_resources.fileExists(file) then -- load json + return g_resources.readFileContents(file) + end + file = context.configDir .. "/" .. dir .. "/" .. name .. ".cfg" + if g_resources.fileExists(file) then -- load cfg + return g_resources.readFileContents(file) + end + return context.error("Config " .. file .. " doesn't exist") +end + +Config.save = function(dir, name, value, forcedExtension) + if not Config.exist(dir) then + if not Config.create(dir) then + return contex.error("Can't create config dir: " .. context.configDir .. "/" .. dir) + end + end + if type(value) ~= 'table' then + return context.error("Invalid config value type: " .. type(value) .. ", should be table") + end + local file = context.configDir .. "/" .. dir .. "/" .. name + 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, 2)) + end + return true +end + +Config.remove = function(dir, name) + local file = context.configDir .. "/" .. dir .. "/" .. name .. ".json" + local ret = false + if g_resources.fileExists(file) then + g_resources.deleteFile(file) + ret = true + end + file = context.configDir .. "/" .. dir .. "/" .. name .. ".cfg" + if g_resources.fileExists(file) then + g_resources.deleteFile(file) + ret = true + end + return ret +end + +-- setup is used for BotConfig widget +-- not done yet +Config.setup = function(dir, widget, configExtension, callback) + if type(dir) ~= 'string' or dir:len() == 0 then + return context.error("Invalid config dir") + end + if not Config.exist(dir) and not Config.create(dir) then + return context.error("Can't create config dir: " .. dir) + end + if type(context.storage._configs) ~= "table" then + context.storage._configs = {} + end + if type(context.storage._configs[dir]) ~= "table" then + context.storage._configs[dir] = { + enabled = false, + selected = "" + } + else + widget.switch:setOn(context.storage._configs[dir].enabled) + end + + local isRefreshing = false + local refresh = function() + isRefreshing = true + local configs = Config.list(dir) + local configIndex = 1 + widget.list:clear() + for v, k in ipairs(configs) do + widget.list:addOption(k) + if k == context.storage._configs[dir].selected then + configIndex = v + end + end + local data = nil + if #configs > 0 then + widget.list:setCurrentIndex(configIndex) + context.storage._configs[dir].selected = widget.list:getCurrentOption().text + data = Config.load(dir, configs[configIndex]) + else + context.storage._configs[dir].selected = nil + end + context.storage._configs[dir].enabled = widget.switch:isOn() + isRefreshing = false + callback(context.storage._configs[dir].selected, widget.switch:isOn(), data) + end + + widget.list.onOptionChange = function(widget) + if not isRefreshing then + context.storage._configs[dir].selected = widget:getCurrentOption().text + refresh() + end + end + + widget.switch.onClick = function() + widget.switch:setOn(not widget.switch:isOn()) + refresh() + end + + widget.add.onClick = function() + context.UI.SinglelineEditorWindow("config_name", { title = "Enter config name" }, function(name) + name = name:gsub("%s+", "_") + if name:len() == 0 or name:len() >= 30 or name:find("/") or name:find("\\") then + return context.error("Invalid config name") + end + local file = context.configDir .. "/" .. dir .. "/" .. name .. "." .. configExtension + if g_resources.fileExists(file) then + return context.error("Config " .. name .. " already exist") + end + 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() + end) + end + + widget.edit.onClick = function() + local name = context.storage._configs[dir].selected + if not name then return end + context.UI.MultilineEditorWindow(Config.loadRaw(dir, name), { title = "Config editor - " .. name .. " in " .. dir }, + function(newValue) + local data = Config.parse(newValue) + Config.save(dir, name, data, configExtension) + refresh() + end) + end + + widget.remove.onClick = function() + local name = context.storage._configs[dir].selected + if not name then return end + context.UI.ConfirmationWindow("Config removal", "Do you want to remove config " .. name .. " from " .. dir .. "?", + function() + Config.remove(dir, name) + widget.switch:setOn(false) + refresh() + end) + end + + refresh() + + return { + isOn = function() + return widget.switch:isOn() + end, + isOff = function() + return not widget.switch:isOn() + end, + setOn = function(val) + if val == false then + if widget.switch:isOn() then + widget.switch:onClick() + end + return + end + if not widget.switch:isOn() then + widget.switch:onClick() + end + end, + setOff = function(val) + if val == false then + if not widget.switch:isOn() then + widget.switch:onClick() + end + return + end + if widget.switch:isOn() then + widget.switch:onClick() + end + end, + save = function(data) + Config.save(dir, context.storage._configs[dir].selected, data, configExtension) + end, + refresh = refresh, + reload = refresh, + getActiveConfigName = function() + return context.storage._configs[dir].selected + end + } +end diff --git a/modules/game_bot/functions/const.lua b/modules/game_bot/functions/const.lua new file mode 100644 index 0000000000..c43757d8e9 --- /dev/null +++ b/modules/game_bot/functions/const.lua @@ -0,0 +1,25 @@ +local context = G.botContext + +context.North = 0 +context.East = 1 +context.South = 2 +context.West = 3 +context.NorthEast = 4 +context.SouthEast = 5 +context.SouthWest = 6 +context.NorthWest = 7 + +context.InventorySlotOther = 0 +context.InventorySlotHead = 1 +context.InventorySlotNeck = 2 +context.InventorySlotBack = 3 +context.InventorySlotBody = 4 +context.InventorySlotRight = 5 +context.InventorySlotLeft = 6 +context.InventorySlotLeg = 7 +context.InventorySlotFeet = 8 +context.InventorySlotFinger = 9 +context.InventorySlotAmmo = 10 +context.InventorySlotPurse = 11 +context.InventorySlotFirst = 1 +context.InventorySlotLast = 10 diff --git a/modules/game_bot/functions/icon.lua b/modules/game_bot/functions/icon.lua new file mode 100644 index 0000000000..dcd48a2d82 --- /dev/null +++ b/modules/game_bot/functions/icon.lua @@ -0,0 +1,176 @@ +local context = G.botContext + +local iconsWithoutPosition = 0 + +context.addIcon = function(id, options, callback) +--[[ + Available options: + item: {id=2160, count=100} + outfit: outfit table ({}) + text: string + x: float (0.0 - 1.0) + y: float (0.0 - 1.0) + hotkey: string + switchable: true / false [default: true] + movable: true / false [default: true] + phantom: true / false [defaule: false] +]]-- + local panel = modules.game_interface.gameMapPanel + if type(id) ~= "string" or id:len() < 1 then + return context.error("Invalid id for addIcon") + end + if options.switchable == false and type(callback) ~= 'function' then + return context.error("Invalid callback for addIcon") + end + if type(context.storage._icons) ~= "table" then + context.storage._icons = {} + end + if type(context.storage._icons[id]) ~= "table" then + context.storage._icons[id] = {} + end + local config = context.storage._icons[id] + local widget = g_ui.createWidget("BotIcon", panel) + widget.botWidget = true + widget.botIcon = true + + if type(config.x) ~= 'number' and type(config.y) ~= 'number' then + if type(options.x) == 'number' and type(options.y) == 'number' then + config.x = math.min(1.0, math.max(0.0, options.x)) + config.y = math.min(1.0, math.max(0.0, options.y)) + else + config.x = 0.01 + math.floor(iconsWithoutPosition / 5) / 10 + config.y = 0.05 + (iconsWithoutPosition % 5) / 5 + iconsWithoutPosition = iconsWithoutPosition + 1 + end + end + + if options.item then + if type(options.item) == 'number' then + widget.item:setItemId(options.item) + else + widget.item:setItemId(options.item.id) + widget.item:setItemCount(options.item.count or 1) + widget.item:setShowCount(false) + end + end + + if options.outfit then + widget.creature:setOutfit(options.outfit) + end + + if options.switchable == false then + widget.status:hide() + widget.status:setOn(true) + else + if config.enabled ~= true then + config.enabled = false + end + widget.status:setOn(config.enabled) + end + + if options.text then + if options.switchable ~= false then + widget.status:hide() + if widget.status:isOn() then + widget.text:setColor('green') + else + widget.text:setColor('red') + end + end + widget.text:setText(options.text) + end + + widget.setOn = function(val) + widget.status:setOn(val) + if widget.status:isOn() then + widget.text:setColor('green') + else + widget.text:setColor('red') + end + config.enabled = widget.status:isOn() + end + + widget.onClick = function(widget) + if options.switchable ~= false then + widget.setOn(not widget.status:isOn()) + if type(callback) == 'table' then + callback.setOn(config.enabled) + return + end + end + + callback(widget, widget.status:isOn()) + end + + if options.hotkey then + widget.hotkey:setText(options.hotkey) + context.hotkey(options.hotkey, "", function() + widget:onClick() + end, nil, options.switchable ~= false) + else + widget.hotkey:hide() + end + + if options.movable ~= false then + widget.onDragEnter = function(widget, mousePos) + if not g_keyboard.isCtrlPressed() then + return false + end + widget:breakAnchors() + widget.movingReference = { x = mousePos.x - widget:getX(), y = mousePos.y - widget:getY() } + return true + end + + widget.onDragMove = function(widget, mousePos, moved) + local parentRect = widget:getParent():getRect() + local x = math.min(math.max(parentRect.x, mousePos.x - widget.movingReference.x), parentRect.x + parentRect.width - widget:getWidth()) + local y = math.min(math.max(parentRect.y - widget:getParent():getMarginTop(), mousePos.y - widget.movingReference.y), parentRect.y + parentRect.height - widget:getHeight()) + widget:move(x, y) + return true + end + + widget.onDragLeave = function(widget, pos) + local parent = widget:getParent() + local parentRect = parent:getRect() + local x = widget:getX() - parentRect.x + local y = widget:getY() - parentRect.y + local width = parentRect.width - widget:getWidth() + local height = parentRect.height - widget:getHeight() + + config.x = math.min(1, math.max(0, x / width)) + config.y = math.min(1, math.max(0, y / height)) + + widget:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) + widget:addAnchor(AnchorVerticalCenter, 'parent', AnchorVerticalCenter) + widget:setMarginTop(math.max(height * (-0.5) - parent:getMarginTop(), height * (-0.5 + config.y))) + widget:setMarginLeft(width * (-0.5 + config.x)) + return true + end + end + + widget.onGeometryChange = function(widget) + if widget:isDragging() then return end + local parent = widget:getParent() + local parentRect = parent:getRect() + local width = parentRect.width - widget:getWidth() + local height = parentRect.height - widget:getHeight() + widget:setMarginTop(math.max(height * (-0.5) - parent:getMarginTop(), height * (-0.5 + config.y))) + widget:setMarginLeft(width * (-0.5 + config.x)) + end + + if options.phantom ~= true then + widget.onMouseRelease = function() + return true + end + end + + if options.switchable ~= false then + if type(callback) == 'table' then + callback.setOn(config.enabled) + callback.icon = widget + else + callback(widget, widget.status:isOn()) + end + end + return widget +end \ No newline at end of file diff --git a/modules/game_bot/functions/main.lua b/modules/game_bot/functions/main.lua new file mode 100644 index 0000000000..211ee0c625 --- /dev/null +++ b/modules/game_bot/functions/main.lua @@ -0,0 +1,211 @@ +local context = G.botContext + +-- MAIN BOT FUNCTION +-- macro(timeout, callback) +-- macro(timeout, name, callback) +-- macro(timeout, name, callback, parent) +-- macro(timeout, name, hotkey, callback) +-- macro(timeout, name, hotkey, callback, parent) +context.macro = function(timeout, name, hotkey, callback, parent) + if type(timeout) ~= 'number' or timeout < 1 then + error("Invalid timeout for macro: " .. tostring(timeout)) + end + if type(name) == 'function' then + callback = name + name = "" + hotkey = "" + elseif type(hotkey) == 'function' then + parent = callback + callback = hotkey + hotkey = "" + elseif type(callback) ~= 'function' then + error("Invalid callback for macro: " .. tostring(callback)) + end + if hotkey == nil then + hotkey = "" + end + if type(name) ~= 'string' or type(hotkey) ~= 'string' then + error("Invalid name or hotkey for macro") + end + if not parent then + parent = context.panel + end + if hotkey:len() > 0 then + hotkey = retranslateKeyComboDesc(hotkey) + end + + -- min timeout is 50, to avoid lags + if timeout < 50 then + timeout = 50 + end + + table.insert(context._macros, { + enabled = false, + name = name, + timeout = timeout, + lastExecution = context.now + math.random(0, 100), + hotkey = hotkey, + }) + local macro = context._macros[#context._macros] + + macro.isOn = function() + return macro.enabled + end + macro.isOff = function() + return not macro.enabled + end + macro.toggle = function(widget) + if macro.isOn() then + macro.setOff() + else + macro.setOn() + end + end + macro.setOn = function(val) + if val == false then + return macro.setOff() + end + macro.enabled = true + context.storage._macros[name] = true + if macro.switch then + macro.switch:setOn(true) + end + if macro.icon then + macro.icon.setOn(true) + end + end + macro.setOff = function(val) + if val == false then + return macro.setOn() + end + macro.enabled = false + context.storage._macros[name] = false + if macro.switch then + macro.switch:setOn(false) + end + if macro.icon then + macro.icon.setOn(false) + end + end + + if name:len() > 0 then + -- creature switch + local text = name + if hotkey:len() > 0 then + text = name .. " [" .. hotkey .. "]" + end + macro.switch = context.addSwitch("macro_" .. (#context._macros + 1), text, macro.toggle, parent) + + -- load state + if context.storage._macros[name] == true then + macro.setOn() + end + else + macro.enabled = true -- unnamed macros are enabled by default + end + + local desc = "lua" + local info = debug.getinfo(2, "Sl") + if info then + desc = info.short_src .. ":" .. info.currentline + end + + macro.callback = function(macro) + if not macro.delay or macro.delay < context.now then + context._currentExecution = macro + local start = g_clock.realMillis() + callback(macro) + local executionTime = g_clock.realMillis() - start + if executionTime > 100 then + context.warning("Slow macro (" .. executionTime .. "ms): " .. macro.name .. " - " .. desc) + end + context._currentExecution = nil + return true + end + end + return macro +end + +-- hotkey(keys, callback) +-- hotkey(keys, name, callback) +-- hotkey(keys, name, callback, parent) +context.hotkey = function(keys, name, callback, parent, single) + if type(name) == 'function' then + callback = name + name = "" + end + if not parent then + parent = context.panel + end + keys = retranslateKeyComboDesc(keys) + if not keys or #keys == 0 then + return context.error("Invalid hotkey keys " .. tostring(name)) + end + if context._hotkeys[keys] then + return context.error("Duplicated hotkey: " .. keys) + end + + local switch = nil + if name:len() > 0 then + switch = context._addHotkeySwitch(name, keys, parent) + end + + context._hotkeys[keys] = { + name = name, + lastExecution = context.now, + switch = switch, + single = single + } + + local desc = "lua" + local info = debug.getinfo(2, "Sl") + if info then + desc = info.short_src .. ":" .. info.currentline + end + + local hotkeyData = context._hotkeys[keys] + hotkeyData.callback = function() + if not hotkeyData.delay or hotkeyData.delay < context.now then + context._currentExecution = hotkeyData + local start = g_clock.realMillis() + callback() + local executionTime = g_clock.realMillis() - start + if executionTime > 100 then + context.warning("Slow hotkey (" .. executionTime .. "ms): " .. hotkeyData.name .. " - " .. desc) + end + context._currentExecution = nil + return true + end + end + + return hotkeyData +end + +-- singlehotkey(keys, callback) +-- singlehotkey(keys, name, callback) +-- singlehotkey(keys, name, callback, parent) +context.singlehotkey = function(keys, name, callback, parent) + if type(name) == 'function' then + callback = name + name = "" + end + return context.hotkey(keys, name, callback, parent, true) +end + +-- schedule(timeout, callback) +context.schedule = function(timeout, callback) + local extecute_time = g_clock.millis() + timeout + table.insert(context._scheduler, { + execution = extecute_time, + callback = callback + }) + table.sort(context._scheduler, function(a, b) return a.execution < b.execution end) +end + +-- delay(duration) -- block execution of current macro/hotkey/callback for x milliseconds +context.delay = function(duration) + if not context._currentExecution then + return context.error("Invalid usage of delay function, it should be used inside callbacks") + end + context._currentExecution.delay = context.now + duration +end \ No newline at end of file diff --git a/modules/game_bot/functions/map.lua b/modules/game_bot/functions/map.lua new file mode 100644 index 0000000000..1e27c92c55 --- /dev/null +++ b/modules/game_bot/functions/map.lua @@ -0,0 +1,267 @@ +local context = G.botContext + +context.getMapView = function() return modules.game_interface.getMapPanel() end +context.getMapPanel = context.getMapView +context.zoomIn = function() modules.game_interface.getMapPanel():zoomIn() end +context.zoomOut = function() modules.game_interface.getMapPanel():zoomOut() end + +context.getSpectators = function(param1, param2) +--[[ + if param1 is table (position) then it's used for central position, then param2 is used as param1 + if param1 is creature, then creature position and direction of creature is used, then param2 is used as param1 + if param1 is true/false then it's used for multifloor, example: getSpectators(true) + if param1 is string then it's used for getSpectatorsByPattern +]]-- + local pos = context.player:getPosition() + local direction = context.player:getDirection() + if type(param1) == 'table' then + pos = param1 + direction = 8 -- invalid direction + param1 = param2 + end + if type(param1) == 'userdata' then + pos = param1:getPosition() + direction = param1:getDirection() + param1 = param2 + end + + if type(param1) == 'string' then + return g_map.getSpectatorsByPattern(pos, param1, direction) + end + + local multifloor = false + if type(param1) == 'boolean' and param1 == true then + multifloor = true + end + return g_map.getSpectators(pos, multifloor) +end + +context.getCreatureById = function(id, multifloor) + if type(id) ~= 'number' then return nil end + if multifloor ~= true then + multifloor = false + end + for i, spec in ipairs(g_map.getSpectators(context.player:getPosition(), multifloor)) do + if spec:getId() == id then + return spec + end + end + return nil +end + +context.getCreatureByName = function(name, multifloor) + if not name then return nil end + name = name:lower() + if multifloor ~= true then + multifloor = false + end + for i, spec in ipairs(g_map.getSpectators(context.player:getPosition(), multifloor)) do + if spec:getName():lower() == name then + return spec + end + end + return nil +end + +context.getPlayerByName = function(name, multifloor) + if not name then return nil end + name = name:lower() + if multifloor ~= true then + multifloor = false + end + for i, spec in ipairs(g_map.getSpectators(context.player:getPosition(), multifloor)) do + if spec:isPlayer() and spec:getName():lower() == name then + return spec + end + end + return nil +end + +context.findAllPaths = function(start, maxDist, params) + --[[ + Available params: + ignoreLastCreature + ignoreCreatures + ignoreNonPathable + ignoreNonWalkable + ignoreStairs + ignoreCost + allowUnseen + allowOnlyVisibleTiles + maxDistanceFrom + ]]-- + if type(params) ~= 'table' then + params = {} + end + for key, value in pairs(params) do + if value == nil or value == false then + params[key] = 0 + elseif value == true then + params[key] = 1 + end + end + if type(params['maxDistanceFrom']) == 'table' then + if #params['maxDistanceFrom'] == 2 then + params['maxDistanceFrom'] = params['maxDistanceFrom'][1].x .. "," .. params['maxDistanceFrom'][1].y .. + "," .. params['maxDistanceFrom'][1].z .. "," .. params['maxDistanceFrom'][2] + elseif #params['maxDistanceFrom'] == 4 then + params['maxDistanceFrom'] = params['maxDistanceFrom'][1] .. "," .. params['maxDistanceFrom'][2] .. + "," .. params['maxDistanceFrom'][3] .. "," .. params['maxDistanceFrom'][4] + end + end + return g_map.findEveryPath(start, maxDist, params) +end +context.findEveryPath = context.findAllPaths + +context.translateAllPathsToPath = function(paths, destPos) + local predirections = {} + local directions = {} + local destPosStr = destPos + if type(destPos) ~= 'string' then + destPosStr = destPos.x .. "," .. destPos.y .. "," .. destPos.z + end + + while destPosStr:len() > 0 do + local node = paths[destPosStr] + if not node then + break + end + if node[3] < 0 then + break + end + table.insert(predirections, node[3]) + destPosStr = node[4] + end + -- reverse + for i=#predirections,1,-1 do + table.insert(directions, predirections[i]) + end + return directions +end +context.translateEveryPathToPath = context.translateAllPathsToPath + + +context.findPath = function(startPos, destPos, maxDist, params) + --[[ + Available params: + ignoreLastCreature + ignoreCreatures + ignoreNonPathable + ignoreNonWalkable + ignoreStairs + ignoreCost + allowUnseen + allowOnlyVisibleTiles + precision + marginMin + marginMax + maxDistanceFrom + ]]-- + if not destPos or startPos.z ~= destPos.z then + return + end + if type(maxDist) ~= 'number' then + maxDist = 100 + end + if type(params) ~= 'table' then + params = {} + end + local destPosStr = destPos.x .. "," .. destPos.y .. "," .. destPos.z + params["destination"] = destPosStr + local paths = context.findAllPaths(startPos, maxDist, params) + local marginMin = params.marginMin or params.minMargin + local marginMax = params.marginMax or params.maxMargin + if type(marginMin) == 'number' and type(marginMax) == 'number' then + local bestCandidate = nil + local bestCandidatePos = nil + for x = -marginMax, marginMax do + for y = -marginMax, marginMax do + if math.abs(x) >= marginMin or math.abs(y) >= marginMin then + local dest = (destPos.x + x) .. "," .. (destPos.y + y) .. "," .. destPos.z + local node = paths[dest] + if node and (not bestCandidate or bestCandidate[1] > node[1]) then + bestCandidate = node + bestCandidatePos = dest + end + end + end + end + if bestCandidate then + return context.translateAllPathsToPath(paths, bestCandidatePos) + end + return + end + + if not paths[destPosStr] then + local precision = params.precision + if type(precision) == 'number' then + for p = 1, precision do + local bestCandidate = nil + local bestCandidatePos = nil + for x = -p, p do + for y = -p, p do + local dest = (destPos.x + x) .. "," .. (destPos.y + y) .. "," .. destPos.z + local node = paths[dest] + if node and (not bestCandidate or bestCandidate[1] > node[1]) then + bestCandidate = node + bestCandidatePos = dest + end + end + end + if bestCandidate then + return context.translateAllPathsToPath(paths, bestCandidatePos) + end + end + end + return nil + end + + return context.translateAllPathsToPath(paths, destPos) +end +context.getPath = context.findPath + +-- also works as autoWalk(dirs) where dirs is a list eg.: {1,2,3,0,1,1,2,} +context.autoWalk = function(destination, maxDist, params) + if type(destination) == "table" and table.isList(destination) and not maxDist and not params then + g_game.autoWalk(destination, {x=0,y=0,z=0}) + return true + end + + -- Available params same as for findPath + local path = context.findPath(context.player:getPosition(), destination, maxDist, params) + if not path then + return false + end + -- autowalk without prewalk animation + g_game.autoWalk(path, {x=0,y=0,z=0}) + return true +end + +context.getTileUnderCursor = function() + if not modules.game_interface.gameMapPanel.mousePos then return end + return modules.game_interface.gameMapPanel:getTile(modules.game_interface.gameMapPanel.mousePos) +end + +context.canShoot = function(pos, distance) + if not distance then distance = 5 end + local tile = g_map.getTile(pos, distance) + if tile then + return tile:canShoot(distance) + end + return false +end + +context.isTrapped = function(creature) + if not creature then + creature = context.player + end + local pos = creature:getPosition() + local dirs = {{-1,1}, {0,1}, {1,1}, {-1, 0}, {1, 0}, {-1, -1}, {0, -1}, {1, -1}} + for i=1,#dirs do + local tile = g_map.getTile({x=pos.x-dirs[i][1],y=pos.y-dirs[i][2],z=pos.z}) + if tile and tile:isWalkable(false) then + return false + end + end + return true +end diff --git a/modules/game_bot/functions/npc.lua b/modules/game_bot/functions/npc.lua new file mode 100644 index 0000000000..b650f4c5d5 --- /dev/null +++ b/modules/game_bot/functions/npc.lua @@ -0,0 +1,130 @@ +local context = G.botContext + +context.NPC = {} + +context.NPC.talk = function(text) + if g_game.getClientVersion() >= 810 then + g_game.talkChannel(11, 0, text) + else + return context.say(text) + end +end +context.NPC.say = context.NPC.talk + +context.NPC.isTrading = function() + return modules.game_npctrade.npcWindow and modules.game_npctrade.npcWindow:isVisible() +end +context.NPC.hasTrade = context.NPC.isTrading +context.NPC.hasTradeWindow = context.NPC.isTrading +context.NPC.isTradeOpen = context.NPC.isTrading + +context.NPC.getSellItems = function() + if not context.NPC.isTrading() then return {} end + local items = {} + for i, item in ipairs(modules.game_npctrade.tradeItems[modules.game_npctrade.SELL]) do + table.insert(items, { + item = item.ptr, + id = item.ptr:getId(), + count = item.ptr:getCount(), + name = item.name, + subType = item.ptr:getSubType(), + weight = item.weight / 100, + price = item.price + }) + end + return items +end + +context.NPC.getBuyItems = function() + if not context.NPC.isTrading() then return {} end + local items = {} + for i, item in ipairs(modules.game_npctrade.tradeItems[modules.game_npctrade.BUY]) do + table.insert(items, { + item = item.ptr, + id = item.ptr:getId(), + count = item.ptr:getCount(), + name = item.name, + subType = item.ptr:getSubType(), + weight = item.weight / 100, + price = item.price + }) + end + return items +end + +context.NPC.getSellQuantity = function(item) + if not context.NPC.isTrading() then return 0 end + if type(item) == 'number' then + item = Item.create(item) + end + return modules.game_npctrade.getSellQuantity(item) +end + +context.NPC.canTradeItem = function(item) + if not context.NPC.isTrading() then return false end + if type(item) == 'number' then + item = Item.create(item) + end + return modules.game_npctrade.canTradeItem(item) +end + +context.NPC.sell = function(item, count, ignoreEquipped) + if type(item) == 'number' then + for i, entry in ipairs(context.NPC.getSellItems()) do + if entry.id == item then + item = entry.item + break + end + end + if type(item) == 'number' then + item = Item.create(item) + end + end + if count == 0 then + count = 1 + end + if count == nil or count == -1 then + count = context.NPC.getSellQuantity(item) + end + if ignoreEquipped == nil then + ignoreEquipped = true + end + g_game.sellItem(item, count, ignoreEquipped) +end + +context.NPC.buy = function(item, count, ignoreCapacity, withBackpack) + if type(item) == 'number' then + for i, entry in ipairs(context.NPC.getBuyItems()) do + if entry.id == item then + item = entry.item + break + end + end + if type(item) == 'number' then + item = Item.create(item) + end + end + if count == nil or count <= 0 then + count = 1 + end + if ignoreCapacity == nil then + ignoreCapacity = false + end + if withBackpack == nil then + withBackpack = false + end + g_game.buyItem(item, count, ignoreCapacity, withBackpack) +end + +context.NPC.sellAll = function() + if not context.NPC.isTrading() then return false end + modules.game_npctrade.sellAll() +end + +context.NPC.closeTrade = function() + modules.game_npctrade.closeNpcTrade() +end +context.NPC.close = context.NPC.closeTrade +context.NPC.finish = context.NPC.closeTrade +context.NPC.endTrade = context.NPC.closeTrade +context.NPC.finishTrade = context.NPC.closeTrade \ No newline at end of file diff --git a/modules/game_bot/functions/player.lua b/modules/game_bot/functions/player.lua new file mode 100644 index 0000000000..4e0a3c5f40 --- /dev/null +++ b/modules/game_bot/functions/player.lua @@ -0,0 +1,176 @@ +local context = G.botContext + +context.name = function() return context.player:getName() end + +context.hp = function() return context.player:getHealth() end +context.mana = function() return context.player:getMana() end +context.hppercent = function() return context.player:getHealthPercent() end +context.manapercent = function() + if context.player:getMaxMana() <= 1 then + return 100 + else + return math.floor(context + .player:getMana() * 100 / context.player:getMaxMana()) + end +end +context.maxhp = function() return context.player:getMaxHealth() end +context.maxmana = function() return context.player:getMaxMana() end +context.hpmax = function() return context.player:getMaxHealth() end +context.manamax = function() return context.player:getMaxMana() end + +context.cap = function() return context.player:getCapacity() end +context.freecap = function() return context.player:getFreeCapacity() end +context.maxcap = function() return context.player:getTotalCapacity() end +context.capmax = function() return context.player:getTotalCapacity() end + +context.exp = function() return context.player:getExperience() end +context.lvl = function() return context.player:getLevel() end +context.level = function() return context.player:getLevel() end + +context.mlev = function() return context.player:getMagicLevel() end +context.magic = function() return context.player:getMagicLevel() end +context.mlevel = function() return context.player:getMagicLevel() end + +context.soul = function() return context.player:getSoul() end +context.stamina = function() return context.player:getStamina() end +context.voc = function() return context.player:getVocation() end +context.vocation = function() return context.player:getVocation() end + +context.bless = function() return context.player:getBlessings() end +context.blesses = function() return context.player:getBlessings() end +context.blessings = function() return context.player:getBlessings() end + + +context.pos = function() return context.player:getPosition() end +context.posx = function() return context.player:getPosition().x end +context.posy = function() return context.player:getPosition().y end +context.posz = function() return context.player:getPosition().z end + +context.direction = function() return context.player:getDirection() end +context.speed = function() return context.player:getSpeed() end +context.skull = function() return context.player:getSkull() end +context.outfit = function() return context.player:getOutfit() end + +context.setOutfit = function(outfit) + modules.game_outfit.ignoreNextOutfitWindow = g_clock.millis() + g_game.requestOutfit() + context.schedule(100, function() + g_game.changeOutfit(outfit) + end) +end +context.changeOutfit = context.setOutfit +context.setSpeed = function(value) context.player:setSpeed(value) end + +context.walk = function(dir) return modules.game_interface.smartWalk(dir) end +context.turn = function(dir) return g_game.turn(dir) end + +-- game releated +context.getChannels = function() + -- return { channelId = channelName } + return modules.game_console.channels +end +context.getChannelId = function(name) + for id, channel in pairs(context.getChannels()) do + if name:lower() == channel:lower() then + return id + end + end + return nil +end +context.getChannel = context.getChannelId + +context.say = g_game.talk +context.talk = g_game.talk +context.yell = function(text) g_game.talkChannel(3, 0, text) end +context.talkChannel = function(channel, text) g_game.talkChannel(7, channel, text) end +context.sayChannel = context.talkChannel +context.talkPrivate = function(receiver, text) g_game.talkPrivate(5, receiver, text) end +context.sayPrivate = context.talkPrivate + +context.talkNpc = function(text) + if g_game.getClientVersion() >= 810 then + g_game.talkChannel(11, 0, text) + else + return context.say(text) + end +end +context.sayNpc = context.talkNpc +context.sayNPC = context.talkNpc +context.talkNPC = context.talkNpc + +context.saySpell = function(text, lastSpellTimeout) + if not text or text:len() < 1 then + return + end + if context.lastSpell == nil then + context.lastSpell = 0 + end + if not lastSpellTimeout then + lastSpellTimeout = 1000 + end + if context.lastSpell + lastSpellTimeout > context.now then + return false + end + context.say(text) + context.lastSpell = context.now + return true +end + +context.setSpellTimeout = function() + context.lastSpell = context.now +end + +context.use = function(thing, subtype) + if type(thing) == 'number' then + return g_game.useInventoryItem(thing, subtype) + else + return g_game.use(thing) + end +end +context.usewith = function(thing, target, subtype) + if not thing then + return + end + if type(thing) == 'number' then + return g_game.useInventoryItemWith(thing, target, subtype) + else + return g_game.useWith(thing, target, subtype) + end +end +context.useWith = context.usewith + +context.useRune = function(itemid, target, lastSpellTimeout) + if context.lastRuneUse == nil then + context.lastRuneUse = 0 + end + if not lastRuneTimeout then + lastRuneTimeout = 1000 + end + if context.lastRuneUse + lastRuneTimeout > context.now then + return false + end + context.usewith(itemid, target) + context.lastRuneUse = context.now + return true +end +context.userune = context.useRune + +context.findItem = function(itemId, subType) + if subType == nil then + subType = -1 + end + return g_game.findItemInContainers(itemId, subType) +end + +context.attack = g_game.attack +context.cancelAttack = g_game.cancelAttack +context.follow = g_game.follow +context.cancelFollow = g_game.cancelFollow +context.cancelAttackAndFollow = g_game.cancelAttackAndFollow + +context.logout = g_game.forceLogout +context.safeLogout = g_game.safeLogout +context.ping = g_game.getPing + +modules.game_cooldown.isGroupCooldownIconActive(id) +modules.game_cooldown.isCooldownIconActive(id) diff --git a/modules/game_bot/functions/player_conditions.lua b/modules/game_bot/functions/player_conditions.lua new file mode 100644 index 0000000000..b87ceacdc5 --- /dev/null +++ b/modules/game_bot/functions/player_conditions.lua @@ -0,0 +1,32 @@ +local context = G.botContext + +for i, state in ipairs(PlayerStates) do + context[state] = state +end + +context.hasCondition = function(condition) return bit.band(context.player:getStates(), condition) > 0 end + +context.isPoisioned = function() return context.hasCondition(PlayerStates.Poison) end +context.isBurning = function() return context.hasCondition(PlayerStates.Burn) end +context.isEnergized = function() return context.hasCondition(PlayerStates.Energy) end +context.isDrunk = function() return context.hasCondition(PlayerStates.Drunk) end +context.hasManaShield = function() return context.hasCondition(PlayerStates.ManaShield) end +context.isParalyzed = function() return context.hasCondition(PlayerStates.Paralyze) end +context.hasHaste = function() return context.hasCondition(PlayerStates.Haste) end +context.hasSwords = function() return context.hasCondition(PlayerStates.Swords) end +context.isInFight = function() return context.hasCondition(PlayerStates.Swords) end +context.canLogout = function() return not context.hasCondition(PlayerStates.Swords) end +context.isDrowning = function() return context.hasCondition(PlayerStates.Drowning) end +context.isFreezing = function() return context.hasCondition(PlayerStates.Freezing) end +context.isDazzled = function() return context.hasCondition(PlayerStates.Dazzled) end +context.isCursed = function() return context.hasCondition(PlayerStates.Cursed) end +context.hasPartyBuff = function() return context.hasCondition(PlayerStates.PartyBuff) end +context.hasPzLock = function() return context.hasCondition(PlayerStates.PzBlock) end +context.hasPzBlock = function() return context.hasCondition(PlayerStates.PzBlock) end +context.isPzLocked = function() return context.hasCondition(PlayerStates.PzBlock) end +context.isPzBlocked = function() return context.hasCondition(PlayerStates.PzBlock) end +context.isInProtectionZone = function() return context.hasCondition(PlayerStates.Pz) end +context.hasPz = function() return context.hasCondition(PlayerStates.Pz) end +context.isInPz = function() return context.hasCondition(PlayerStates.Pz) end +context.isBleeding = function() return context.hasCondition(PlayerStates.Bleeding) end +context.isHungry = function() return context.hasCondition(PlayerStates.Hungry) end diff --git a/modules/game_bot/functions/player_inventory.lua b/modules/game_bot/functions/player_inventory.lua new file mode 100644 index 0000000000..9ea8c4c9fd --- /dev/null +++ b/modules/game_bot/functions/player_inventory.lua @@ -0,0 +1,45 @@ +local context = G.botContext + +context.SlotOther = InventorySlotOther +context.SlotHead = InventorySlotHead +context.SlotNeck = InventorySlotNeck +context.SlotBack = InventorySlotBack +context.SlotBody = InventorySlotBody +context.SlotRight = InventorySlotRight +context.SlotLeft = InventorySlotLeft +context.SlotLeg = InventorySlotLeg +context.SlotFeet = InventorySlotFeet +context.SlotFinger = InventorySlotFinger +context.SlotAmmo = InventorySlotAmmo +context.SlotPurse = InventorySlotPurse + +context.getInventoryItem = function(slot) return context.player:getInventoryItem(slot) end +context.getSlot = context.getInventoryItem + +context.getHead = function() return context.getInventoryItem(context.SlotHead) end +context.getNeck = function() return context.getInventoryItem(context.SlotNeck) end +context.getBack = function() return context.getInventoryItem(context.SlotBack) end +context.getBody = function() return context.getInventoryItem(context.SlotBody) end +context.getRight = function() return context.getInventoryItem(context.SlotRight) end +context.getLeft = function() return context.getInventoryItem(context.SlotLeft) end +context.getLeg = function() return context.getInventoryItem(context.SlotLeg) end +context.getFeet = function() return context.getInventoryItem(context.SlotFeet) end +context.getFinger = function() return context.getInventoryItem(context.SlotFinger) end +context.getAmmo = function() return context.getInventoryItem(context.SlotAmmo) end +context.getPurse = function() return context.getInventoryItem(context.SlotPurse) end + +context.getContainers = function() return g_game.getContainers() end +context.getContainer = function(index) return g_game.getContainer(index) end + +context.moveToSlot = function(item, slot, count) + if type(item) == 'number' then + item = context.findItem(item) + end + if not item then + return + end + if count == nil then + count = item:getCount() + end + return g_game.move(item, {x=65535, y=slot, z=0}, count) +end \ No newline at end of file diff --git a/modules/game_bot/functions/script_loader.lua b/modules/game_bot/functions/script_loader.lua new file mode 100644 index 0000000000..8346634ed0 --- /dev/null +++ b/modules/game_bot/functions/script_loader.lua @@ -0,0 +1,59 @@ +local context = G.botContext + +context.loadScript = function(path, onLoadCallback) + if type(path) ~= 'string' then + return context.error("Invalid path for loadScript: " .. tostring(path)) + end + if path:lower():find("http") == 1 then + return context.loadRemoteScript(path) + end + if not g_resources.fileExists(path) then + return context.error("File " .. path .. " doesn't exist") + end + + local status, result = pcall(function() + assert(load(g_resources.readFileContents(path), path, nil, context))() + end) + if not status then + return context.error("Error while loading script from: " .. path .. ":\n" .. result) + end + if onLoadCallback then + onLoadCallback() + end +end + +context.loadRemoteScript = function(url, onLoadCallback) + if type(url) ~= 'string' or url:lower():find("http") ~= 1 then + return context.error("Invalid url for loadRemoteScript: " .. tostring(url)) + end + + HTTP.get(url, function(data, err) + if err or data:len() == 0 then + -- try to load from cache + if type(context.storage.scriptsCache) ~= 'table' then + context.storage.scriptsCache = {} + end + local cache = context.storage.scriptsCache[url] + if cache and type(cache) == 'string' and cache:len() > 0 then + data = cache + else + return context.error("Can't load script from: " .. url .. ", error: " .. err) + end + end + + local status, result = pcall(function() + assert(load(data, url, nil, context))() + end) + if not status then + return context.error("Error while loading script from: " .. url .. ":\n" .. result) + end + -- cache script + if type(context.storage.scriptsCache) ~= 'table' then + context.storage.scriptsCache = {} + end + context.storage.scriptsCache[url] = data + if onLoadCallback then + onLoadCallback() + end + end) +end diff --git a/modules/game_bot/functions/server.lua b/modules/game_bot/functions/server.lua new file mode 100644 index 0000000000..f620fc5668 --- /dev/null +++ b/modules/game_bot/functions/server.lua @@ -0,0 +1,91 @@ +local context = G.botContext + +context.BotServer = {} +context.BotServer.url = "ws://bot.otclient.ovh:8000/" +context.BotServer.timeout = 3 +context.BotServer.ping = 0 +context.BotServer._callbacks = {} +context.BotServer._lastMessageId = 0 +context.BotServer._wasConnected = true -- show first warning + +context.BotServer.init = function(name, channel) + if not channel or not name or channel:len() < 1 or name:len() < 1 then + return context.error("Invalid params for BotServer.init") + end + if context.BotServer._websocket then + return context.error("BotServer is already initialized") + end + context.BotServer._websocket = HTTP.WebSocketJSON(context.BotServer.url, { + onMessage = function(message, socketId) + if not context._websockets[socketId] then + return g_http.cancel(socketId) + end + if not context.BotServer._websocket or context.BotServer._websocket.id ~= socketId then + return g_http.cancel(socketId) + end + context.BotServer._wasConnected = true + if message["type"] == "ping" then + context.BotServer.ping = message["ping"] + return context.BotServer._websocket.send({type="ping"}) + end + if message["type"] == "message" then + context.BotServer._lastMessageId = message["id"] + local topics = context.BotServer._callbacks[message["topic"]] + if topics then + for i=1,#topics do + topics[i](message["name"], message["message"], message["topic"]) + end + end + topics = context.BotServer._callbacks["*"] + if topics then + for i=1,#topics do + topics[i](message["name"], message["message"], message["topic"]) + end + end + return + end + end, + onClose = function(message, socketId) + if not context._websockets[socketId] then + return + end + context._websockets[socketId] = nil + if not context.BotServer._websocket or context.BotServer._websocket.id ~= socketId then + return + end + if context.BotServer._wasConnected then + context.warn("BotServer disconnected") + end + context.BotServer._wasConnected = false + context.BotServer._websocket = nil + context.BotServer.ping = 0 + context.BotServer.init(name, channel) + end + }, context.BotServer.timeout) + context._websockets[context.BotServer._websocket.id] = 1 + context.BotServer._websocket.send({type="init", name=name, channel=channel, lastMessage=context.BotServer._lastMessageId}) +end + +context.BotServer.terminate = function() + if context.BotServer._websocket then + context.BotServer._websocket:close() + context.BotServer._websocket = nil + end +end + +context.BotServer.listen = function(topic, callback) -- callback = function(name, message, topic) -- message is parsed json = table + if not context.BotServer._websocket then + return context.error("BotServer is not initialized") + end + if not context.BotServer._callbacks[topic] then + context.BotServer._callbacks[topic] = {} + end + table.insert(context.BotServer._callbacks[topic], callback) +end + +context.BotServer.send = function(topic, message) + if not context.BotServer._websocket then + return context.error("BotServer is not initialized") + end + context.BotServer._websocket.send({type="message", topic=topic, message=message}) +end diff --git a/modules/game_bot/functions/sound.lua b/modules/game_bot/functions/sound.lua new file mode 100644 index 0000000000..c7fffeefd4 --- /dev/null +++ b/modules/game_bot/functions/sound.lua @@ -0,0 +1,31 @@ +local context = G.botContext + +context.getSoundChannel = function() + if not g_sounds then + return + end + return g_sounds.getChannel(SoundChannels.Bot) +end + +context.playSound = function(file) + local botSoundChannel = context.getSoundChannel() + if not botSoundChannel then + return + end + botSoundChannel:setEnabled(true) + botSoundChannel:stop(0) + botSoundChannel:play(file, 0, 1.0) + return botSoundChannel +end + +context.stopSound = function() + local botSoundChannel = context.getSoundChannel() + if not botSoundChannel then + return + end + botSoundChannel:stop() +end + +context.playAlarm = function() + return context.playSound("/sounds/alarm.ogg") +end diff --git a/modules/game_bot/functions/test.lua b/modules/game_bot/functions/test.lua new file mode 100644 index 0000000000..9d6afcf098 --- /dev/null +++ b/modules/game_bot/functions/test.lua @@ -0,0 +1,3 @@ +local context = G.botContext + +context.test = function() return context.info("test") end \ No newline at end of file diff --git a/modules/game_bot/functions/tools.lua b/modules/game_bot/functions/tools.lua new file mode 100644 index 0000000000..bebae760d7 --- /dev/null +++ b/modules/game_bot/functions/tools.lua @@ -0,0 +1,19 @@ +local context = G.botContext + +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) + local box = displayGeneralBox(title, message, buttons, onEnterCallback, onEscapeCallback) + box.botWidget = true + return box +end + +context.doScreenshot = function(filename) + g_app.doScreenshot(filename) +end +context.screenshot = context.doScreenshot + +context.getVersion = function() + return g_app.getVersion() +end \ No newline at end of file diff --git a/modules/game_bot/functions/ui.lua b/modules/game_bot/functions/ui.lua new file mode 100644 index 0000000000..fe020dbc77 --- /dev/null +++ b/modules/game_bot/functions/ui.lua @@ -0,0 +1,33 @@ +local context = G.botContext +if type(context.UI) ~= "table" then + context.UI = {} +end +local UI = context.UI + +UI.createWidget = function(name, parent) + if parent == nil then + parent = context.panel + end + local widget = g_ui.createWidget(name, parent) + widget.botWidget = true + return widget +end + +UI.createMiniWindow = function(name, parent) + if parent == nil then + parent = modules.game_interface.getRightPanel() + end + local widget = g_ui.createWidget(name, parent) + widget:setup() + 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 \ No newline at end of file diff --git a/modules/game_bot/functions/ui_elements.lua b/modules/game_bot/functions/ui_elements.lua new file mode 100644 index 0000000000..34d5b80df0 --- /dev/null +++ b/modules/game_bot/functions/ui_elements.lua @@ -0,0 +1,401 @@ +local context = G.botContext +if type(context.UI) ~= "table" then + context.UI = {} +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 + +-- 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, widget) + if not widget then + widget = UI.createWidget("BotContainer", parent) + end + + local oldItems = {} + + local updateItems = function() + local items = widget:getItems() + + -- callback part + local somethingNew = (#items ~= #oldItems) + for i, item in ipairs(items) do + if type(oldItems[i]) ~= "table" then + somethingNew = true + break + end + if oldItems[i].id ~= item.id or oldItems[i].count ~= item.count then + somethingNew = true + break + end + end + + if somethingNew then + oldItems = items + callback(widget, items) + end + + widget:setItems(items) + end + + widget.setItems = function(self, items) + if type(self) == 'table' then + items = self + end + local itemsToShow = math.max(10, #items + 2) + if itemsToShow % 5 ~= 0 then + itemsToShow = itemsToShow + 5 - itemsToShow % 5 + end + widget.items:destroyChildren() + for i = 1, itemsToShow do + local widget = g_ui.createWidget("BotItem", widget.items) + if type(items[i]) == 'number' then + items[i] = {id=items[i], count=1} + end + if type(items[i]) == 'table' then + widget:setItem(Item.create(items[i].id, items[i].count)) + end + end + oldItems = items + for i, child in ipairs(widget.items:getChildren()) do + child.onItemChange = updateItems + end + end + + widget.getItems = function() + local items = {} + local duplicates = {} + for i, child in ipairs(widget.items:getChildren()) do + if child:getItemId() >= 100 then + if not duplicates[child:getItemId()] or not unique then + table.insert(items, {id=child:getItemId(), count=child:getItemCountOrSubType()}) + duplicates[child:getItemId()] = true + end + end + end + 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, + subType - number, + title - string, + min - number, + max - number, + ]] + params.title = params.title or "title" + params.item = params.item or 0 + params.subType = params.subType 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:setItem(Item.create(params.item, params.subType)) + widget.item.onItemChange = function() + params.item = widget.item:getItemId() + params.subType = widget.item:getItemSubType() + 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 + +UI.DualLabel = function(left, right, params, parent) + --[[ params: + height - int, + maxWidth - number + ]] + + left = left or "" + right = right or "" + params = params or {} + if not type(params) == "table" then + parent = params + params = {} + end + params.height = params.height or 20 + params.maxWidth = params.maxWidth or 88 + + local widget = UI.createWidget('DualLabelPanel', parent) + + widget.left:setText(left) + widget.right:setText(right) + widget:setHeight(params.height) + if widget.left:getWidth() > params.maxWidth then + widget.left:setWidth(params.maxWidth) + end + return widget +end + +UI.LabelAndTextEdit = function(params, callback, parent) + --[[ params: + left - str, + right - str, + height - int, + maxWidth - int, + ]] + + params = params or {} + params.left = params.left or "" + params.right = params.right or "" + params.height = params.height or 20 + params.maxWidth = params.maxWidth or 88 + + local widget = UI.createWidget('LabelAndTextEditPanel', parent) + + widget.left:setText(params.left) + widget.right:setText(params.right) + widget:setHeight(params.height) + if widget.left:getWidth() > params.maxWidth then + widget.left:setWidth(params.maxWidth) + end + + widget.right.onTextChange = function(widget, text) + params.right = text + if callback then + callback(widget, params) + end + end + + --[[example: + + storage.testParams = storage.testParams or {left = "hotkey", right = "F5"} + UI.LabelAndTextEdit(storage.testParams, function(widget, newParams) + storage.testParams = newParams + end) + + ]] + return widget +end + + +UI.SwitchAndButton = function(params, callbackSwitch, callbackButton, callback, parent) + --[[ params: + on - bool, + left - str, + right - str, + height - int, + maxWidth - int, + ]] + + params = params or {} + params.on = params.on or false + params.left = params.left or "" + params.right = params.right or "" + params.height = params.height or 20 + params.maxWidth = params.maxWidth or 88 + + local widget = UI.createWidget('SwitchAndButtonPanel', parent) + + widget.left:setOn(params.on) + widget.left:setText(params.left) + widget.right:setText(params.right) + widget:setHeight(params.height) + if widget.right:getWidth() > params.maxWidth then + widget.right:setWidth(params.maxWidth) + end + + widget.left.onClick = function() + params.on = not params.on + widget.left:setOn(params.on) + if callback then + callback(widget, params) + end + if callbackSwitch then + callbackSwitch() + else + warn("callback not set!") + end + end + + widget.right.onClick = function() + if callbackButton then + callbackButton() + else + warn("callback not set!") + end + end + + --[[ params: + if type(storage.test1) ~= "table" then + storage.test1 = storage.test1 or {on = false, left = "new script", right = "config"} + end + + UI.SwitchAndButton(storage.test1, test, test, function(widget, newParams) + storage.test1 = newParams + end) + ]] + return widget +end \ No newline at end of file diff --git a/modules/game_bot/functions/ui_legacy.lua b/modules/game_bot/functions/ui_legacy.lua new file mode 100644 index 0000000000..2fece4c5d1 --- /dev/null +++ b/modules/game_bot/functions/ui_legacy.lua @@ -0,0 +1,135 @@ +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 + end + g_ui.createWidget(name, parent) +end + +context.setupUI = function(otml, parent) + if parent == nil then + parent = context.panel + end + local widget = g_ui.loadUIFromString(otml, parent) + widget.botWidget = true + return widget +end + +context.importStyle = function(otml) + if type(otml) ~= "string" then + return error("Invalid parameter for importStyle, should be string") + end + if otml:find(".otui") and not otml:find("\n") then + return g_ui.importStyle(context.configDir .. "/" .. otml) + end + return g_ui.importStyleFromString(otml) +end + +context.addTab = function(name) + local tab = context.tabs:getTab(name) + if tab then -- return existing tab + return tab.tabPanel.content + end + + local smallTabs = #(context.tabs.tabs) >= 5 + local newTab = context.tabs:addTab(name, g_ui.createWidget('BotPanel')).tabPanel.content + context.tabs:setOn(true) + if smallTabs then + for k,tab in pairs(context.tabs.tabs) do + tab:setFont('small-9px') + end + end + + return newTab +end +context.getTab = context.addTab + +context.setDefaultTab = function(name) + local tab = context.addTab(name) + context.panel = tab +end + +context.addSwitch = function(id, text, onClickCallback, parent) + if not parent then + parent = context.panel + end + local switch = g_ui.createWidget('BotSwitch', parent) + switch:setId(id) + switch:setText(text) + switch.onClick = onClickCallback + return switch +end + +context.addButton = function(id, text, onClickCallback, parent) + if not parent then + parent = context.panel + end + local button = g_ui.createWidget('BotButton', parent) + button:setId(id) + button:setText(text) + button.onClick = onClickCallback + return button +end + +context.addLabel = function(id, text, parent) + if not parent then + parent = context.panel + end + local label = g_ui.createWidget('BotLabel', parent) + label:setId(id) + label:setText(text) + return label +end + +context.addTextEdit = function(id, text, onTextChangeCallback, parent) + if not parent then + parent = context.panel + end + local widget = g_ui.createWidget('BotTextEdit', parent) + widget:setId(id) + widget.onTextChange = onTextChangeCallback + widget:setText(text) + return widget +end + +context.addSeparator = function(id, parent) + if not parent then + parent = context.panel + end + local separator = g_ui.createWidget('BotSeparator', parent) + separator:setId(id) + return separator +end + +context._addMacroSwitch = function(name, keys, parent) + if not parent then + parent = context.panel + end + local text = name + if keys:len() > 0 then + text = name .. " [" .. keys .. "]" + end + local switch = context.addSwitch("macro_" .. #context._macros, text, function(widget) + context.storage._macros[name] = not context.storage._macros[name] + widget:setOn(context.storage._macros[name]) + end, parent) + switch:setOn(context.storage._macros[name]) + return switch +end + +context._addHotkeySwitch = function(name, keys, parent) + if not parent then + parent = context.panel + end + local text = name + if keys:len() > 0 then + text = name .. " [" .. keys .. "]" + end + local switch = context.addSwitch("hotkey_" .. #context._hotkeys, text, nil, parent) + switch:setOn(false) + return switch +end \ No newline at end of file diff --git a/modules/game_bot/functions/ui_windows.lua b/modules/game_bot/functions/ui_windows.lua new file mode 100644 index 0000000000..a0c0a4c3db --- /dev/null +++ b/modules/game_bot/functions/ui_windows.lua @@ -0,0 +1,49 @@ +local context = G.botContext +if type(context.UI) ~= "table" then + context.UI = {} +end +local UI = context.UI + +UI.EditorWindow = function(text, options, callback) + --[[ + Available options: + title = text + description = text + multiline = true / false + width = number + validation = text (regex) + examples = {{name, text}, {name, text}} + ]]-- + local window = modules.client_textedit.edit(text, options, callback) + window.botWidget = true + return window +end + +UI.SinglelineEditorWindow = function(text, options, callback) + options = options or {} + options.multiline = false + return UI.EditorWindow(text, options, callback) +end + +UI.MultilineEditorWindow = function(text, options, callback) + options = options or {} + options.multiline = true + return UI.EditorWindow(text, options, callback) +end + +UI.ConfirmationWindow = function(title, question, callback) + local window = nil + local onConfirm = function() + window:destroy() + callback() + end + local closeWindow = function() + window:destroy() + end + window = context.displayGeneralBox(title, question, { + { text=tr('Yes'), callback=onConfirm }, + { text=tr('No'), callback=closeWindow }, + anchor=AnchorHorizontalCenter}, onConfirm, closeWindow) + window.botWidget = true + return window +end \ No newline at end of file diff --git a/modules/game_bot/panels/DONT_USE_PANELS.txt b/modules/game_bot/panels/DONT_USE_PANELS.txt new file mode 100644 index 0000000000..a9c2c1823a --- /dev/null +++ b/modules/game_bot/panels/DONT_USE_PANELS.txt @@ -0,0 +1,3 @@ +DONT USE PANELS +THEY ONLY HERE FOR BACKWARD COMPATIBILITY +MAY BE REMOVED IN THE FUTURE \ No newline at end of file diff --git a/modules/game_bot/panels/attacking.lua b/modules/game_bot/panels/attacking.lua new file mode 100644 index 0000000000..34b1ac8503 --- /dev/null +++ b/modules/game_bot/panels/attacking.lua @@ -0,0 +1,1193 @@ +local context = G.botContext +local Panels = context.Panels + +Panels.MonsterEditor = function(monster, config, callback, parent) + local otherWindow = g_ui.getRootWidget():getChildById('monsterEditor') + if otherWindow then + otherWindow:destory() + end + + local window = context.setupUI([[ +MainWindow + id: monsterEditor + size: 450 450 + !text: tr("Edit monster") + + Label + id: info + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-align: center + text: Use monster name * for any other monster not on the list + + Label + id: info2 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-align: center + text: Add number (1-5) at the end of the name to create multiple configs + + TextEdit + id: name + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-left: 100 + margin-top: 5 + multiline: false + + Label + anchors.verticalCenter: prev.verticalCenter + anchors.left: parent.left + text: Target name: + + Label + id: priorityText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 10 + margin-top: 10 + text: Priority + text-align: center + + HorizontalScrollBar + id: priority + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 5 + minimum: 0 + maximum: 10 + step: 1 + + Label + id: dangerText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 10 + margin-top: 10 + text: Danger + text-align: center + + HorizontalScrollBar + id: danger + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 5 + minimum: 0 + maximum: 10 + step: 1 + + Label + id: maxDistanceText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 10 + margin-top: 10 + text: Max distance to target + text-align: center + + HorizontalScrollBar + id: maxDistance + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 5 + minimum: 1 + maximum: 10 + step: 1 + + Label + id: distanceText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 10 + margin-top: 10 + text: Keep distance + text-align: center + + HorizontalScrollBar + id: distance + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 5 + minimum: 0 + maximum: 5 + step: 1 + + Label + id: minHealthText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 10 + margin-top: 10 + text: Minimum Health + text-align: center + + HorizontalScrollBar + id: minHealth + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 5 + minimum: 0 + maximum: 100 + step: 1 + + Label + id: maxHealthText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 10 + margin-top: 10 + text: Maximum Health + text-align: center + + HorizontalScrollBar + id: maxHealth + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 5 + minimum: 0 + maximum: 100 + step: 1 + + Label + id: dangerText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 5 + margin-top: 10 + text: If total danger is high (>8) bot won't auto loot until it's low again and will be trying to minimize it + text-align: center + text-wrap: true + text-auto-resize: true + + Label + id: attackSpellText + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-right: 5 + margin-top: 10 + text: Attack spell and attack rune are only used when you have more than 30% health + text-align: center + text-wrap: true + text-auto-resize: true + + BotSwitch + id: attack + anchors.left: parent.horizontalCenter + anchors.top: name.bottom + margin-left: 10 + margin-top: 10 + width: 55 + text: Attack + + BotSwitch + id: ignore + anchors.left: prev.right + anchors.top: name.bottom + margin-left: 18 + margin-top: 10 + width: 55 + text: Ignore + + BotSwitch + id: avoid + anchors.left: prev.right + anchors.top: name.bottom + margin-left: 18 + margin-top: 10 + width: 55 + text: Avoid + + BotSwitch + id: keepDistance + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: prev.bottom + margin-left: 10 + margin-top: 10 + text: Keep distance + + BotSwitch + id: avoidAttacks + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: prev.bottom + margin-left: 10 + margin-top: 10 + text: Avoid monster attacks + + BotSwitch + id: chase + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: prev.bottom + margin-left: 10 + margin-top: 10 + text: Chase when has low health + + BotSwitch + id: loot + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: prev.bottom + margin-left: 10 + margin-top: 10 + text: Loot corpse + + BotSwitch + id: monstersOnly + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: prev.bottom + margin-left: 10 + margin-top: 10 + text: Only for monsters + + BotSwitch + id: dontWalk + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: prev.bottom + margin-left: 10 + margin-top: 10 + text: Don't walk to target + + Label + id: attackSpellText + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: prev.bottom + margin-left: 10 + margin-top: 10 + text: Attack Spell: + text-align: center + + TextEdit + id: attackSpell + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 2 + + Label + id: attackItemText + anchors.left: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 20 + margin-left: 20 + text: Attack rune: + text-align: left + + BotItem + id: attackItem + anchors.right: parent.right + anchors.verticalCenter: prev.verticalCenter + margin-right: 30 + + Button + id: okButton + !text: tr('Ok') + anchors.bottom: parent.bottom + anchors.right: next.left + margin-right: 10 + width: 60 + + Button + id: cancelButton + !text: tr('Cancel') + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 60 +]], g_ui.getRootWidget()) + + local destroy = function() + window:destroy() + end + local doneFunc = function() + local monster = window.name:getText() + local config = { + priority = window.priority:getValue(), + danger = window.danger:getValue(), + maxDistance = window.maxDistance:getValue(), + distance = window.distance:getValue(), + minHealth = window.minHealth:getValue(), + maxHealth = window.maxHealth:getValue(), + attack = window.attack:isOn(), + ignore = window.ignore:isOn(), + avoid = window.avoid:isOn(), + keepDistance = window.keepDistance:isOn(), + avoidAttacks = window.avoidAttacks:isOn(), + chase = window.chase:isOn(), + loot = window.loot:isOn(), + monstersOnly = window.monstersOnly:isOn(), + dontWalk = window.dontWalk:isOn(), + attackItem = window.attackItem:getItemId(), + attackSpell = window.attackSpell:getText() + } + destroy() + callback(monster, config) + end + + window.okButton.onClick = doneFunc + window.cancelButton.onClick = destroy + window.onEnter = doneFunc + window.onEscape = destroy + + + window.priority.onValueChange = function(scroll, value) + window.priorityText:setText("Priority: " .. value) + end + window.danger.onValueChange = function(scroll, value) + window.dangerText:setText("Danger: " .. value) + end + window.maxDistance.onValueChange = function(scroll, value) + window.maxDistanceText:setText("Max distance to target: " .. value) + end + window.distance.onValueChange = function(scroll, value) + window.distanceText:setText("Keep distance: " .. value) + end + window.minHealth.onValueChange = function(scroll, value) + window.minHealthText:setText("Minimum health: " .. value .. "%") + end + window.maxHealth.onValueChange = function(scroll, value) + window.maxHealthText:setText("Maximum health: " .. value .. "%") + end + + window.priority:setValue(config.priority or 1) + window.danger:setValue(config.danger or 1) + window.maxDistance:setValue(config.maxDistance or 6) + window.distance:setValue(config.distance or 1) + window.minHealth:setValue(1) -- to force onValueChange update + window.maxHealth:setValue(1) -- to force onValueChange update + window.minHealth:setValue(config.minHealth or 0) + window.maxHealth:setValue(config.maxHealth or 100) + + window.attackSpell:setText(config.attackSpell or "") + window.attackItem:setItemId(config.attackItem or 0) + + window.attack.onClick = function(widget) + if widget:isOn() then + return + end + widget:setOn(true) + window.ignore:setOn(false) + window.avoid:setOn(false) + end + window.ignore.onClick = function(widget) + if widget:isOn() then + return + end + widget:setOn(true) + window.attack:setOn(false) + window.avoid:setOn(false) + end + window.avoid.onClick = function(widget) + if widget:isOn() then + return + end + widget:setOn(true) + window.attack:setOn(false) + window.ignore:setOn(false) + end + + window.attack:setOn(config.attack) + window.ignore:setOn(config.ignore) + window.avoid:setOn(config.avoid) + if not window.attack:isOn() and not window.ignore:isOn() and not window.avoid:isOn() then + window.attack:setOn(true) + end + + window.keepDistance.onClick = function(widget) + widget:setOn(not widget:isOn()) + end + window.avoidAttacks.onClick = function(widget) + widget:setOn(not widget:isOn()) + end + window.chase.onClick = function(widget) + widget:setOn(not widget:isOn()) + end + window.loot.onClick = function(widget) + widget:setOn(not widget:isOn()) + end + window.monstersOnly.onClick = function(widget) + widget:setOn(not widget:isOn()) + end + window.dontWalk.onClick = function(widget) + widget:setOn(not widget:isOn()) + end + + window.keepDistance:setOn(config.keepDistance) + window.avoidAttacks:setOn(config.avoidAttacks) + window.chase:setOn(config.chase) + window.loot:setOn(config.loot) + if config.loot == nil then + window.loot:setOn(true) + end + window.monstersOnly:setOn(config.monstersOnly) + if config.monstersOnly == nil then + window.monstersOnly:setOn(true) + end + window.dontWalk:setOn(config.dontWalk) + + window.name:setText(monster) + + window:show() + window:raise() + window:focus() +end + +Panels.Attacking = function(parent) + local ui = context.setupUI([[ +Panel + id: attacking + height: 140 + + BotLabel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text: Attacking + + ComboBox + id: config + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 5 + text-offset: 3 0 + width: 130 + + Button + id: enableButton + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + + Button + margin-top: 1 + id: add + anchors.top: prev.bottom + anchors.left: parent.left + text: Add + width: 60 + height: 17 + + Button + id: edit + anchors.top: prev.top + anchors.horizontalCenter: parent.horizontalCenter + text: Edit + width: 60 + height: 17 + + Button + id: remove + anchors.top: prev.top + anchors.right: parent.right + text: Remove + width: 60 + height: 17 + + TextList + id: list + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + vertical-scrollbar: listScrollbar + margin-right: 15 + margin-top: 2 + height: 60 + focusable: false + auto-focus: first + + VerticalScrollBar + id: listScrollbar + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.right: parent.right + pixels-scroll: true + step: 5 + + Button + margin-top: 2 + id: mAdd + anchors.top: prev.bottom + anchors.left: parent.left + text: Add + width: 60 + height: 17 + + Button + id: mEdit + anchors.top: prev.top + anchors.horizontalCenter: parent.horizontalCenter + text: Edit + width: 60 + height: 17 + + Button + id: mRemove + anchors.top: prev.top + anchors.right: parent.right + text: Remove + width: 60 + height: 17 + +]], parent) + + if type(context.storage.attacking) ~= "table" then + context.storage.attacking = {} + end + if type(context.storage.attacking.configs) ~= "table" then + context.storage.attacking.configs = {} + end + + local getConfigName = function(config) + local matches = regexMatch(config, [[name:\s*([^\n]*)$]]) + if matches[1] and matches[1][2] then + return matches[1][2]:trim() + end + return nil + end + + local commands = {} + local monsters = {} + local configName = nil + local refreshConfig = nil -- declared later + + local createNewConfig = function() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + + local newConfig = "" + if configName ~= nil then + newConfig = "name:" .. configName .. "\n" + end + for monster, config in pairs(monsters) do + newConfig = newConfig .. "\n" .. monster .. ":" .. json.encode(config, 2) .. "\n" + end + + context.storage.attacking.configs[context.storage.attacking.activeConfig] = newConfig + refreshConfig() + end + + local parseConfig = function(config) + commands = {} + monsters = {} + configName = nil + + local matches = regexMatch(config, [[([^:^\n]+)(:?)([^\n]*)]]) + for i = 1, #matches do + local command = matches[i][2] + local validation = (matches[i][3] == ":") + local text = matches[i][4] + if validation then + table.insert(commands, { command = command:lower(), text = text }) + elseif #commands > 0 then + commands[#commands].text = commands[#commands].text .. "\n" .. matches[i][1] + end + end + local labels = {} + for i, command in ipairs(commands) do + if commands[i].command == "name" then + configName = commands[i].text + else + local status, result = pcall(function() return json.decode(command.text) end) + if not status then + context.error("Invalid monster config: " .. commands[i].command .. ", error: " .. result) + else + monsters[commands[i].command] = result + table.insert(labels, commands[i].command) + end + end + end + table.sort(labels) + for i, text in ipairs(labels) do + local label = g_ui.createWidget("CaveBotLabel", ui.list) + label:setText(text) + end + end + + local ignoreOnOptionChange = true + refreshConfig = function(scrollDown) + ignoreOnOptionChange = true + if context.storage.attacking.enabled then + ui.enableButton:setText("On") + ui.enableButton:setColor('#00AA00FF') + else + ui.enableButton:setText("Off") + ui.enableButton:setColor('#FF0000FF') + end + + ui.config:clear() + for i, config in ipairs(context.storage.attacking.configs) do + local name = getConfigName(config) + if not name then + name = "Unnamed config" + end + ui.config:addOption(name) + end + + if (not context.storage.attacking.activeConfig or context.storage.attacking.activeConfig == 0) and #context.storage.attacking.configs > 0 then + context.storage.attacking.activeConfig = 1 + end + + ui.list:destroyChildren() + + if context.storage.attacking.activeConfig and context.storage.attacking.configs[context.storage.attacking.activeConfig] then + ui.config:setCurrentIndex(context.storage.attacking.activeConfig) + parseConfig(context.storage.attacking.configs[context.storage.attacking.activeConfig]) + end + + context.saveConfig() + if scrollDown and ui.list:getLastChild() then + ui.list:focusChild(ui.list:getLastChild()) + end + + ignoreOnOptionChange = false + end + + + ui.config.onOptionChange = function(widget) + if not ignoreOnOptionChange then + context.storage.attacking.activeConfig = widget.currentIndex + refreshConfig() + end + end + ui.enableButton.onClick = function() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + context.storage.attacking.enabled = not context.storage.attacking.enabled + refreshConfig() + end + ui.add.onClick = function() + modules.client_textedit.multilineEditor("Target list editor", "name:Config name", function(newText) + table.insert(context.storage.attacking.configs, newText) + context.storage.attacking.activeConfig = #context.storage.attacking.configs + refreshConfig() + end) + end + ui.edit.onClick = function() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + modules.client_textedit.multilineEditor("Target list editor", + context.storage.attacking.configs[context.storage.attacking.activeConfig], function(newText) + context.storage.attacking.configs[context.storage.attacking.activeConfig] = newText + refreshConfig() + end) + end + ui.remove.onClick = function() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + local questionWindow = nil + local closeWindow = function() + questionWindow:destroy() + end + local removeConfig = function() + closeWindow() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + context.storage.attacking.enabled = false + table.remove(context.storage.attacking.configs, context.storage.attacking.activeConfig) + context.storage.attacking.activeConfig = 0 + refreshConfig() + end + questionWindow = context.displayGeneralBox(tr('Remove config'), tr('Do you want to remove current attacking config?'), + { + { text = tr('Yes'), callback = removeConfig }, + { text = tr('No'), callback = closeWindow }, + anchor = AnchorHorizontalCenter + }, removeConfig, closeWindow) + end + + + ui.mAdd.onClick = function() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + Panels.MonsterEditor("", {}, function(name, config) + if name:len() > 0 then + monsters[name] = config + end + createNewConfig() + end, parent) + end + ui.mEdit.onClick = function() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + local monsterWidget = ui.list:getFocusedChild() + if not monsterWidget or not monsters[monsterWidget:getText()] then + return + end + Panels.MonsterEditor(monsterWidget:getText(), monsters[monsterWidget:getText()], function(name, config) + monsters[monsterWidget:getText()] = nil + if name:len() > 0 then + monsters[name] = config + end + createNewConfig() + end, parent) + end + ui.mRemove.onClick = function() + if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then + return + end + local monsterWidget = ui.list:getFocusedChild() + if not monsterWidget or not monsters[monsterWidget:getText()] then + return + end + monsters[monsterWidget:getText()] = nil + createNewConfig() + end + + refreshConfig() + + -- processing + local isConfigPassingConditions = function(monster, config) + if not config or type(config.priority) ~= 'number' or type(config.danger) ~= 'number' then + return false + end + + if not config.attack then + return false + end + + if monster:isPlayer() and (config.monstersOnly == true or config.monstersOnly == nil) then + return false + end + + local pos = context.player:getPosition() + local mpos = monster:getPosition() + local hp = monster:getHealthPercent() + + if config.minHealth > hp or config.maxHealth < hp then + return false + end + + local maxDistance = 5 + if type(config.maxDistance) == 'number' then + maxDistance = config.maxDistance + end + if config.chase and hp < 25 then + maxDistance = maxDistance + 2 + end + + local distance = math.max(math.abs(pos.x - mpos.x), math.abs(pos.y - mpos.y)) + if distance > maxDistance then + return false + end + + local pathTo = context.findPath(context.player:getPosition(), { x = mpos.x, y = mpos.y, z = mpos.z }, maxDistance + 2, + { ignoreNonPathable = true, precision = 1, allowOnlyVisibleTiles = true, ignoreCost = true }) + if not pathTo or #pathTo > maxDistance + 1 then + return false + end + return true + end + + local getMonsterConfig = function(monster) + local name = monster:getName():lower() + local hasConfig = false + hasConfig = hasConfig or (monsters[name] ~= nil) + if isConfigPassingConditions(monster, monsters[name]) then + return monsters[name] + end + for i = 1, 5 do + hasConfig = hasConfig or (monsters[name .. i] ~= nil) + if isConfigPassingConditions(monster, monsters[name .. i]) then + return monsters[name .. i] + end + end + if not hasConfig and isConfigPassingConditions(monster, monsters["*"]) then + return monsters["*"] + end + return nil + end + + local calculatePriority = function(monster) + local priority = 0 + local config = getMonsterConfig(monster) + if not config then + return -1 + end + + local pos = context.player:getPosition() + local mpos = monster:getPosition() + local hp = monster:getHealthPercent() + local pathTo = context.findPath(context.player:getPosition(), { x = mpos.x, y = mpos.y, z = mpos.z }, 10, + { ignoreNonPathable = true, ignoreLastCreature = true, precision = 0, allowOnlyVisibleTiles = true }) + if not pathTo then + pathTo = context.findPath(context.player:getPosition(), { x = mpos.x, y = mpos.y, z = mpos.z }, 10, + { ignoreNonPathable = true, precision = 1, allowOnlyVisibleTiles = true }) + if not pathTo then + return -1 + end + end + local distance = #pathTo + + if monster == g_game.getAttackingCreature() then + priority = priority + 10 + end + + if distance <= 4 then + priority = priority + 10 + end + if distance <= 2 then + priority = priority + 10 + end + if distance <= 1 then + priority = priority + 10 + end + + if hp <= 25 and config.chase then + priority = priority + 30 + end + + if hp <= 10 then + priority = priority + 10 + end + if hp <= 25 then + priority = priority + 10 + end + if hp <= 50 then + priority = priority + 10 + end + if hp <= 75 then + priority = priority + 10 + end + + priority = priority + config.priority * 10 + return priority + end + + local calculateMonsterDanger = function(monster) + local danger = 0 + local config = getMonsterConfig(monster) + if not config or type(config.danger) ~= 'number' then + return danger + end + danger = danger + config.danger + return danger + end + + local lastAttack = context.now + local lootContainers = {} + local lootTries = 0 + local openContainerRequest = 0 + local waitForLooting = 0 + local lastAttackSpell = 0 + local lastAttackRune = 0 + + local goForLoot = function() + if #lootContainers == 0 or not context.storage.looting.enabled then + return false + end + if modules.game_interface.lastManualWalk + 500 > context.now then + return true + end + + local pos = context.player:getPosition() + table.sort(lootContainers, function(pos1, pos2) + local dist1 = math.max(math.abs(pos.x - pos1.x), math.abs(pos.y - pos1.y)) + local dist2 = math.max(math.abs(pos.x - pos2.x), math.abs(pos.y - pos2.y)) + return dist1 < dist2 + end) + + local cpos = lootContainers[1] + if cpos.z ~= pos.z then + table.remove(lootContainers, 1) + return true + end + + if lootTries >= 5 then + lootTries = 0 + table.remove(lootContainers, 1) + return true + end + local dist = math.max(math.abs(pos.x - cpos.x), math.abs(pos.y - cpos.y)) + if dist <= 5 then + local tile = g_map.getTile(cpos) + if not tile then + table.remove(lootContainers, 1) + return true + end + + local topItem = tile:getTopUseThing() + if not topItem or not topItem:isContainer() then + table.remove(lootContainers, 1) + return true + end + topItem:setMarked('orange') + + if dist <= 1 then + lootTries = lootTries + 1 + openContainerRequest = context.now + g_game.open(topItem) + waitForLooting = math.max(waitForLooting, context.now + 500) + return true + end + end + + if dist <= 25 then + if context.player:isWalking() then + return true + end + + lootTries = lootTries + 1 + if context.autoWalk(cpos, 20, { precision = 1 }) then + return true + end + + if context.autoWalk(cpos, 20, { ignoreNonPathable = true, precision = 1 }) then + return true + end + + if context.autoWalk(cpos, 20, { ignoreNonPathable = true, precision = 2 }) then + return true + end + + if context.autoWalk(cpos, 20, { ignoreNonPathable = true, ignoreCreatures = true, precision = 2 }) then + return true + end + else + table.remove(lootContainers, 1) + return false + end + return true + end + + context.onCreatureDisappear(function(creature) + if not creature:isMonster() then + return + end + local pos = context.player:getPosition() + local tpos = creature:getPosition() + if tpos.z ~= pos.z then + return + end + + local config = getMonsterConfig(creature) + if not config or not config.loot then + return + end + local distance = math.max(math.abs(pos.x - tpos.x), math.abs(pos.y - tpos.y)) + if distance > 6 then + return + end + + local tile = g_map.getTile(tpos) + if not tile then + return + end + + local topItem = tile:getTopUseThing() + if not topItem or not topItem:isContainer() then + return + end + + topItem:setMarked('blue') + table.insert(lootContainers, tpos) + end) + + context.onContainerOpen(function(container, prevContainer) + lootTries = 0 + if not context.storage.attacking.enabled then + return + end + + if openContainerRequest + 500 > context.now and #lootContainers > 0 then + waitForLooting = math.max(waitForLooting, context.now + 1000 + container:getItemsCount() * 100) + table.remove(lootContainers, 1) + end + if prevContainer then + container.autoLooting = prevContainer.autoLooting + else + container.autoLooting = (openContainerRequest + 3000 > context.now) + end + end) + + context.macro(200, function() + if not context.storage.attacking.enabled then + return + end + + local attacking = nil + local following = nil + local attackingCandidate = g_game.getAttackingCreature() + local followingCandidate = g_game.getFollowingCreature() + local spectators = context.getSpectators() + local monsters = {} + local danger = 0 + + for i, spec in ipairs(spectators) do + if attackingCandidate and attackingCandidate:getId() == spec:getId() then + attacking = spec + end + if followingCandidate and followingCandidate:getId() == spec:getId() then + following = spec + end + if spec:isMonster() or (spec:isPlayer() and not spec:isLocalPlayer()) then + danger = danger + calculateMonsterDanger(spec) + spec.attackingPriority = calculatePriority(spec) + table.insert(monsters, spec) + end + end + + if following then + return + end + + if waitForLooting > context.now then + return + end + + if #monsters == 0 or context.isInProtectionZone() then + goForLoot() + return + end + + table.sort(monsters, function(a, b) + return a.attackingPriority > b.attackingPriority + end) + + local target = monsters[1] + if target.attackingPriority < 0 then + return + end + + local pos = context.player:getPosition() + local tpos = target:getPosition() + local config = getMonsterConfig(target) + local offsetX = pos.x - tpos.x + local offsetY = pos.y - tpos.y + + local justStartedAttack = false + if target ~= attacking then + g_game.attack(target) + attacking = target + lastAttack = context.now + justStartedAttack = true + end + + -- proceed attack + if not target:isPlayer() and lastAttack + 15000 < context.now then + -- stop and attack again, just in case + g_game.cancelAttack() + g_game.attack(target) + lastAttack = context.now + return + end + + if not justStartedAttack and config.attackSpell and config.attackSpell:len() > 0 then + if context.now > lastAttackSpell + 1000 and context.player:getHealthPercent() > 30 then + if context.saySpell(config.attackSpell, 1500) then + lastAttackRune = context.now + end + end + end + + if not justStartedAttack and config.attackItem and config.attackItem >= 100 then + if context.now > lastAttackRune + 1000 and context.player:getHealthPercent() > 30 then + if context.useRune(config.attackItem, target, 1500) then + lastAttackRune = context.now + end + end + end + + if modules.game_interface.lastManualWalk + 500 > context.now then + return + end + + if danger < 8 then + -- low danger, go for loot first + if goForLoot() then + return + end + end + + target.ignoreByWaypoints = config.dontWalk + if config.dontWalk then + if goForLoot() then + return + end + return + end + + local distance = math.max(math.abs(offsetX), math.abs(offsetY)) + if config.keepDistance then + local minDistance = config.distance + if target:getHealthPercent() <= 25 and config.chase and danger < 10 then + minDistance = 1 + end + if (distance == minDistance or distance == minDistance + 1) then + return + else + local bestDist = 10 + local bestPos = pos + if not context.autoWalk(tpos, 10, { minMargin = minDistance, maxMargin = minDistance + 1, allowOnlyVisibleTiles = true }) then + if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, minMargin = minDistance, maxMargin = minDistance + + 1, allowOnlyVisibleTiles = true }) then + if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, ignoreCreatures = true, minMargin = minDistance, maxMargin = + minDistance + 2, allowOnlyVisibleTiles = true }) then + return + end + end + end + if not target:isPlayer() then + context.delay(300) + end + end + return + end + + if config.avoidAttacks and distance <= 1 then + if (offsetX == 0 and offsetY ~= 0) then + if context.player:canWalk(Directions.East) then + g_game.walk(Directions.East) + elseif context.player:canWalk(Directions.West) then + g_game.walk(Directions.West) + end + elseif (offsetX ~= 0 and offsetY == 0) then + if context.player:canWalk(Directions.North) then + g_game.walk(Directions.North) + elseif context.player:canWalk(Directions.South) then + g_game.walk(Directions.South) + end + end + end + + if distance > 1 then + if not context.autoWalk(tpos, 10, { precision = 1, allowOnlyVisibleTiles = true }) then + if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, precision = 1, allowOnlyVisibleTiles = true }) then + if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, precision = 2, allowOnlyVisibleTiles = true }) then + return + end + end + end + if not target:isPlayer() then + context.delay(300) + end + end + end) +end diff --git a/modules/game_bot/panels/basic.lua b/modules/game_bot/panels/basic.lua new file mode 100644 index 0000000000..40b546876b --- /dev/null +++ b/modules/game_bot/panels/basic.lua @@ -0,0 +1,57 @@ +local context = G.botContext +local Panels = context.Panels + +Panels.Turning = function(parent) + context.macro(1000, "Turning / AntiIdle", nil, function() + context.turn(math.random(1, 4)) + end, parent) +end +Panels.AntiIdle = Panels.Turning + +Panels.AttackSpell = function(parent) + context.macro(500, "Auto attack spell", nil, function() + local target = g_game.getAttackingCreature() + if target and context.getCreatureById(target:getId()) and context.storage.autoAttackText:len() > 0 then + if context.saySpell(context.storage.autoAttackText, 1000) then + context.delay(1000) + end + end + end, parent) + context.addTextEdit("autoAttackText", context.storage.autoAttackText or "exori vis", function(widget, text) + context.storage.autoAttackText = text + end, parent) +end + +Panels.AttackItem = function(parent) + if not parent then + parent = context.panel + end + + local panelName = "attackItem" + local ui = g_ui.createWidget("ItemAndButtonPanel", parent) + ui:setId(panelName) + + ui.title:setText("Auto attack item") + + if not context.storage.attackItem then + context.storage.attackItem = {} + end + + ui.title:setOn(context.storage.attackItem.enabled) + ui.title.onClick = function(widget) + context.storage.attackItem.enabled = not context.storage.attackItem.enabled + widget:setOn(context.storage.attackItem.enabled) + end + + ui.item.onItemChange = function(widget) + context.storage.attackItem.item = widget:getItemId() + end + ui.item:setItemId(context.storage.attackItem.item or 3155) + + context.macro(500, function() + local target = g_game.getAttackingCreature() + if context.storage.attackItem.enabled and target and context.getCreatureById(target:getId()) and context.storage.attackItem.item and context.storage.attackItem.item >= 100 then + context.useWith(context.storage.attackItem.item, target) + end + end) +end diff --git a/modules/game_bot/panels/healing.lua b/modules/game_bot/panels/healing.lua new file mode 100644 index 0000000000..fe4171d07c --- /dev/null +++ b/modules/game_bot/panels/healing.lua @@ -0,0 +1,346 @@ +local context = G.botContext +local Panels = context.Panels + +Panels.Haste = function(parent) + context.macro(500, "Auto Haste", nil, function() + if not context.hasHaste() and context.storage.autoHasteText:len() > 0 then + if context.saySpell(context.storage.autoHasteText, 2500) then + context.delay(5000) + end + end + end, parent) + context.addTextEdit("autoHasteText", context.storage.autoHasteText or "utani hur", function(widget, text) + context.storage.autoHasteText = text + end, parent) +end + +Panels.ManaShield = function(parent) + local lastManaShield = 0 + context.macro(100, "Auto Mana Shield", nil, function() + if not context.hasManaShield() or context.now > lastManaShield + 90000 then + if context.saySpell("utamo vita", 200) then + lastManaShield = context.now + end + end + end, parent) +end + +Panels.AntiParalyze = function(parent) + context.macro(100, "Anti Paralyze", nil, function() + if context.isParalyzed() and context.storage.autoAntiParalyzeText:len() > 0 then + context.saySpell(context.storage.autoAntiParalyzeText, 750) + end + end, parent) + context.addTextEdit("autoAntiParalyzeText", context.storage.autoAntiParalyzeText or "utani hur", function(widget, text) + context.storage.autoAntiParalyzeText = text + end, parent) +end + + +Panels.Health = function(parent) + if not parent then + parent = context.panel + end + + local panelName = "autoHealthPanel" + local panelId = 1 + while parent:getChildById(panelName .. panelId) do + panelId = panelId + 1 + end + panelName = panelName .. panelId + + local ui = g_ui.createWidget("DualScrollPanel", parent) + ui:setId(panelName) + + if not context.storage[panelName] then + context.storage[panelName] = { + item = 266, + min = 20, + max = 80, + text = "exura" + } + end + + ui.title:setOn(context.storage[panelName].enabled) + ui.title.onClick = function(widget) + context.storage[panelName].enabled = not context.storage[panelName].enabled + widget:setOn(context.storage[panelName].enabled) + end + + ui.text.onTextChange = function(widget, text) + context.storage[panelName].text = text + end + ui.text:setText(context.storage[panelName].text or "exura") + + local updateText = function() + ui.title:setText("" .. context.storage[panelName].min .. "% <= hp <= " .. context.storage[panelName].max .. "%") + end + + ui.scroll1.onValueChange = function(scroll, value) + context.storage[panelName].min = value + updateText() + end + ui.scroll2.onValueChange = function(scroll, value) + context.storage[panelName].max = value + updateText() + end + + ui.scroll1:setValue(context.storage[panelName].min) + ui.scroll2:setValue(context.storage[panelName].max) + + context.macro(25, function() + if context.storage[panelName].enabled and context.storage[panelName].text:len() > 0 and context.storage[panelName].min <= context.hppercent() and context.hppercent() <= context.storage[panelName].max then + if context.saySpell(context.storage[panelName].text, 500) then + context.delay(200) + end + end + end) +end + +Panels.HealthItem = function(parent) + if not parent then + parent = context.panel + end + + local panelName = "autoHealthItemPanel" + local panelId = 1 + while parent:getChildById(panelName .. panelId) do + panelId = panelId + 1 + end + panelName = panelName .. panelId + + local ui = g_ui.createWidget("DualScrollItemPanel", parent) + ui:setId(panelName) + + if not context.storage[panelName] then + context.storage[panelName] = { + item = 266, + min = 0, + max = 60 + } + end + + ui.title:setOn(context.storage[panelName].enabled) + ui.title.onClick = function(widget) + context.storage[panelName].enabled = not context.storage[panelName].enabled + widget:setOn(context.storage[panelName].enabled) + end + + ui.item.onItemChange = function(widget) + context.storage[panelName].item = widget:getItemId() + end + ui.item:setItemId(context.storage[panelName].item) + + local updateText = function() + ui.title:setText("" .. (context.storage[panelName].min or "") .. "% <= hp <= " .. (context.storage[panelName].max or "") .. "%") + end + + ui.scroll1.onValueChange = function(scroll, value) + context.storage[panelName].min = value + updateText() + end + ui.scroll2.onValueChange = function(scroll, value) + context.storage[panelName].max = value + updateText() + end + + ui.scroll1:setValue(context.storage[panelName].min) + ui.scroll2:setValue(context.storage[panelName].max) + + context.macro(25, function() + if context.storage[panelName].enabled and context.storage[panelName].item >= 100 and context.storage[panelName].min <= context.hppercent() and context.hppercent() <= context.storage[panelName].max then + if context.useRune(context.storage[panelName].item, context.player, 500) then + context.delay(300) + end + end + end) +end + +Panels.Mana = function(parent) + if not parent then + parent = context.panel + end + + local panelName = "autoManaItemPanel" + local panelId = 1 + while parent:getChildById(panelName .. panelId) do + panelId = panelId + 1 + end + panelName = panelName .. panelId + + local ui = g_ui.createWidget("DualScrollItemPanel", parent) + ui:setId(panelName) + + if not context.storage[panelName] then + context.storage[panelName] = { + item = 268, + min = 0, + max = 60 + } + end + + ui.title:setOn(context.storage[panelName].enabled) + ui.title.onClick = function(widget) + context.storage[panelName].enabled = not context.storage[panelName].enabled + widget:setOn(context.storage[panelName].enabled) + end + + ui.item.onItemChange = function(widget) + context.storage[panelName].item = widget:getItemId() + end + ui.item:setItemId(context.storage[panelName].item) + + local updateText = function() + ui.title:setText("" .. (context.storage[panelName].min or "") .. "% <= mana <= " .. (context.storage[panelName].max or "") .. "%") + end + + ui.scroll1.onValueChange = function(scroll, value) + context.storage[panelName].min = value + updateText() + end + ui.scroll2.onValueChange = function(scroll, value) + context.storage[panelName].max = value + updateText() + end + + ui.scroll1:setValue(context.storage[panelName].min) + ui.scroll2:setValue(context.storage[panelName].max) + + context.macro(25, function() + if context.storage[panelName].enabled and context.storage[panelName].item >= 100 and context.storage[panelName].min <= context.manapercent() and context.manapercent() <= context.storage[panelName].max then + if context.useRune(context.storage[panelName].item, context.player, 500) then + context.delay(300) + end + end + end) +end +Panels.ManaItem = Panels.Mana + +Panels.Equip = function(parent) + if not parent then + parent = context.panel + end + + local panelName = "autoEquipItem" + local panelId = 1 + while parent:getChildById(panelName .. panelId) do + panelId = panelId + 1 + end + panelName = panelName .. panelId + + local ui = g_ui.createWidget("TwoItemsAndSlotPanel", parent) + ui:setId(panelName) + + if not context.storage[panelName] then + context.storage[panelName] = {} + if panelId == 1 then + context.storage[panelName].item1 = 3052 + context.storage[panelName].item2 = 3089 + context.storage[panelName].slot = 9 + end + end + + ui.title:setText("Auto equip") + ui.title:setOn(context.storage[panelName].enabled) + ui.title.onClick = function(widget) + context.storage[panelName].enabled = not context.storage[panelName].enabled + widget:setOn(context.storage[panelName].enabled) + end + + ui.item1:setItemId(context.storage[panelName].item1 or 0) + ui.item1.onItemChange = function(widget) + context.storage[panelName].item1 = widget:getItemId() + end + + ui.item2:setItemId(context.storage[panelName].item2 or 0) + ui.item2.onItemChange = function(widget) + context.storage[panelName].item2 = widget:getItemId() + end + + if not context.storage[panelName].slot then + context.storage[panelName].slot = 1 + end + ui.slot:setCurrentIndex(context.storage[panelName].slot) + ui.slot.onOptionChange = function(widget) + context.storage[panelName].slot = widget.currentIndex + end + + context.macro(250, function() + if context.storage[panelName].enabled and context.storage[panelName].slot > 0 then + local item1 = context.storage[panelName].item1 or 0 + local item2 = context.storage[panelName].item2 or 0 + if item1 < 100 and item2 < 100 then + return + end + local slotItem = context.getSlot(context.storage[panelName].slot) + if slotItem and (slotItem:getId() == item1 or slotItem:getId() == item2) then + return + end + local newItem = context.findItem(context.storage[panelName].item1) + if not newItem then + newItem = context.findItem(context.storage[panelName].item2) + if not newItem then + return + end + end + g_game.move(newItem, {x=65535, y=context.storage[panelName].slot, z=0}) + context.delay(1000) + end + end) +end +Panels.AutoEquip = Panels.Equip + +Panels.Eating = function(parent) + if not parent then + parent = context.panel + end + + local panelName = "autoEatingPanel" + local panelId = 1 + while parent:getChildById(panelName .. panelId) do + panelId = panelId + 1 + end + panelName = panelName .. panelId + + local ui = g_ui.createWidget("ItemsPanel", parent) + ui:setId(panelName) + + if not context.storage[panelName] then + context.storage[panelName] = {} + end + + ui.title:setText("Auto eating") + ui.title:setOn(context.storage[panelName].enabled) + ui.title.onClick = function(widget) + context.storage[panelName].enabled = not context.storage[panelName].enabled + widget:setOn(context.storage[panelName].enabled) + end + + if type(context.storage[panelName].items) ~= 'table' then + context.storage[panelName].items = {3725, 0, 0, 0, 0} + end + + for i=1,5 do + ui.items:getChildByIndex(i).onItemChange = function(widget) + context.storage[panelName].items[i] = widget:getItemId() + end + ui.items:getChildByIndex(i):setItemId(context.storage[panelName].items[i]) + end + + context.macro(15000, function() + if not context.storage[panelName].enabled then + return + end + local candidates = {} + for i, item in pairs(context.storage[panelName].items) do + if item >= 100 then + table.insert(candidates, item) + end + end + if #candidates == 0 then + return + end + context.use(candidates[math.random(1, #candidates)]) + end) +end + diff --git a/modules/game_bot/panels/looting.lua b/modules/game_bot/panels/looting.lua new file mode 100644 index 0000000000..cf2785a73f --- /dev/null +++ b/modules/game_bot/panels/looting.lua @@ -0,0 +1,431 @@ +local context = G.botContext +local Panels = context.Panels + +Panels.Looting = function(parent) + local ui = context.setupUI([[ +Panel + id: looting + height: 180 + + BotLabel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text: Looting + + ComboBox + id: config + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 5 + text-offset: 3 0 + width: 130 + + Button + id: enableButton + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + + Button + margin-top: 1 + id: add + anchors.top: prev.bottom + anchors.left: parent.left + text: Add + width: 60 + height: 17 + + Button + id: edit + anchors.top: prev.top + anchors.horizontalCenter: parent.horizontalCenter + text: Edit + width: 60 + height: 17 + + Button + id: remove + anchors.top: prev.top + anchors.right: parent.right + text: Remove + width: 60 + height: 17 + + ScrollablePanel + id: items + anchors.top: prev.bottom + anchors.right: parent.right + anchors.left: parent.left + vertical-scrollbar: scrollBar + margin-right: 5 + margin-top: 2 + height: 70 + layout: + type: grid + cell-size: 34 34 + flow: true + + BotSmallScrollBar + id: scrollBar + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.right: parent.right + step: 10 + pixels-scroll: true + + BotLabel + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 4 + text: Loot Containers + + ItemsRow + id: containers + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 33 + margin-top: 2 + +]], parent) + + local lootContainers = { ui.containers.item1, ui.containers.item2, ui.containers.item3, ui.containers.item4, ui.containers.item5 } + + if type(context.storage.looting) ~= "table" then + context.storage.looting = {} + end + if type(context.storage.looting.configs) ~= "table" then + context.storage.looting.configs = {} + end + + local getConfigName = function(config) + local matches = regexMatch(config, [[name:\s*([^\n]*)$]]) + if matches[1] and matches[1][2] then + return matches[1][2]:trim() + end + return nil + end + + local items = {} + local itemsByKey = {} + local containers = {} + local commands = {} + local refreshConfig = nil -- declared later + + local createNewConfig = function(focusedWidget) + if not context.storage.looting.activeConfig or not context.storage.looting.configs[context.storage.looting.activeConfig] then + return + end + + local tmpItems = {} + local tmpContainers = {} + local focusIndex = 0 + + local newConfig = "" + for i, text in ipairs(commands) do + newConfig = newConfig .. text .. "\n" + end + for i=1,ui.items:getChildCount() do + local widget = ui.items:getChildByIndex(i) + if widget and widget:getItemId() >= 100 then + if tmpItems[widget:getItemId()] == nil then + tmpItems[widget:getItemId()] = 1 + newConfig = newConfig .. "\n" .. widget:getItemId() + end + end + if widget == focusedWidget then + focusIndex = i + end + end + for i, widget in ipairs(lootContainers) do + if widget:getItemId() >= 100 then + if tmpContainers[widget:getItemId()] == nil then + tmpContainers[widget:getItemId()] = 1 -- remove duplicates + newConfig = newConfig .. "\ncontainer:" .. widget:getItemId() + end + end + end + + context.storage.looting.configs[context.storage.looting.activeConfig] = newConfig + refreshConfig(focusIndex) + end + + local parseConfig = function(config) + items = {} + itemsByKey = {} + containers = {} + commands = {} + local matches = regexMatch(config, [[([^:^\n^\s]+)(:?)([^\n]*)]]) + for i=1,#matches do + local command = matches[i][2] + local validation = (matches[i][3] == ":") + local text = matches[i][4] + local commandAsNumber = tonumber(command) + local textAsNumber = tonumber(text) + if commandAsNumber and commandAsNumber >= 100 then + table.insert(items, commandAsNumber) + itemsByKey[commandAsNumber] = 1 + elseif command == "container" and validation and textAsNumber and textAsNumber >= 100 then + containers[textAsNumber] = 1 + elseif validation then + table.insert(commands, command .. ":" .. text) + end + end + + local itemsToShow = #items + 2 + if itemsToShow % 5 ~= 0 then + itemsToShow = itemsToShow + 5 - itemsToShow % 5 + end + if itemsToShow < 10 then + itemsToShow = 10 + end + + for i=1,itemsToShow do + local widget = g_ui.createWidget("BotItem", ui.items) + local itemId = 0 + if i <= #items then + itemId = items[i] + end + widget:setItemId(itemId) + widget.onItemChange = createNewConfig + end + + for i, widget in ipairs(lootContainers) do + widget:setItemId(0) + end + local containerIndex = 1 + for containerId, i in pairs(containers) do + if lootContainers[containerIndex] then + lootContainers[containerIndex]:setItemId(containerId) + end + containerIndex = containerIndex + 1 + end + for i, widget in ipairs(lootContainers) do + widget.onItemChange = createNewConfig + end + end + + local ignoreOnOptionChange = true + refreshConfig = function(focusIndex) + ignoreOnOptionChange = true + if context.storage.looting.enabled then + ui.enableButton:setText("On") + ui.enableButton:setColor('#00AA00FF') + else + ui.enableButton:setText("Off") + ui.enableButton:setColor('#FF0000FF') + end + + ui.config:clear() + for i, config in ipairs(context.storage.looting.configs) do + local name = getConfigName(config) + if not name then + name = "Unnamed config" + end + ui.config:addOption(name) + end + + if (not context.storage.looting.activeConfig or context.storage.looting.activeConfig == 0) and #context.storage.looting.configs > 0 then + context.storage.looting.activeConfig = 1 + end + + ui.items:destroyChildren() + for i, widget in ipairs(lootContainers) do + widget.onItemChange = nil + widget:setItemId(0) + widget:setItemCount(0) + end + + if context.storage.looting.activeConfig and context.storage.looting.configs[context.storage.looting.activeConfig] then + ui.config:setCurrentIndex(context.storage.looting.activeConfig) + parseConfig(context.storage.looting.configs[context.storage.looting.activeConfig]) + end + + context.saveConfig() + if focusIndex and focusIndex > 0 and ui.items:getChildByIndex(focusIndex) then + ui.items:focusChild(ui.items:getChildByIndex(focusIndex)) + end + + ignoreOnOptionChange = false + end + + ui.config.onOptionChange = function(widget) + if not ignoreOnOptionChange then + context.storage.looting.activeConfig = widget.currentIndex + refreshConfig() + end + end + ui.enableButton.onClick = function() + if not context.storage.looting.activeConfig or not context.storage.looting.configs[context.storage.looting.activeConfig] then + return + end + context.storage.looting.enabled = not context.storage.looting.enabled + refreshConfig() + end + ui.add.onClick = function() + modules.client_textedit.multilineEditor("Looting editor", "name:Config name", function(newText) + table.insert(context.storage.looting.configs, newText) + context.storage.looting.activeConfig = #context.storage.looting.configs + refreshConfig() + end) + end + ui.edit.onClick = function() + if not context.storage.looting.activeConfig or not context.storage.looting.configs[context.storage.looting.activeConfig] then + return + end + modules.client_textedit.multilineEditor("Looting editor", context.storage.looting.configs[context.storage.looting.activeConfig], function(newText) + context.storage.looting.configs[context.storage.looting.activeConfig] = newText + refreshConfig() + end) + end + ui.remove.onClick = function() + if not context.storage.looting.activeConfig or not context.storage.looting.configs[context.storage.looting.activeConfig] then + return + end + local questionWindow = nil + local closeWindow = function() + questionWindow:destroy() + end + local removeConfig = function() + closeWindow() + if not context.storage.looting.activeConfig or not context.storage.looting.configs[context.storage.looting.activeConfig] then + return + end + context.storage.looting.enabled = false + table.remove(context.storage.looting.configs, context.storage.looting.activeConfig) + context.storage.looting.activeConfig = 0 + refreshConfig() + end + questionWindow = context.displayGeneralBox(tr('Remove config'), tr('Do you want to remove current looting config?'), { + { text=tr('Yes'), callback=removeConfig }, + { text=tr('No'), callback=closeWindow }, + anchor=AnchorHorizontalCenter}, removeConfig, closeWindow) + end + refreshConfig() + + context.onContainerOpen(function(container, prevContainer) + if context.storage.attacking.enabled then + return + end + if prevContainer then + container.autoLooting = prevContainer.autoLooting + else + container.autoLooting = true + end + end) + + context.macro(200, function() + if not context.storage.looting.enabled then + return + end + local candidates = {} + local lootContainersCandidates = {} + for containerId, container in pairs(g_game.getContainers()) do + local containerItem = container:getContainerItem() + if container.autoLooting and container:getItemsCount() > 0 and (not containerItem or containers[containerItem:getId()] == nil) then + table.insert(candidates, container) + elseif containerItem and containers[containerItem:getId()] ~= nil then + table.insert(lootContainersCandidates, container) + end + end + if #lootContainersCandidates == 0 then + for slot = InventorySlotFirst, InventorySlotLast do + local item = context.getInventoryItem(slot) + if item and item:isContainer() and containers[item:getId()] ~= nil then + table.insert(lootContainersCandidates, item) + end + end + if #lootContainersCandidates > 0 then + -- try to open inventory backpack + local target = lootContainersCandidates[math.random(1,#lootContainersCandidates)] + g_game.open(target, nil) + context.delay(200) + end + return + end + + if #candidates == 0 then + return + end + + local container = candidates[math.random(1,#candidates)] + local nextContainers = {} + local foundItem = nil + for i, item in ipairs(container:getItems()) do + if item:isContainer() then + table.insert(nextContainers, item) + elseif itemsByKey[item:getId()] ~= nil then + foundItem = item + break + end + end + + -- found item to loot + if foundItem then + -- find backpack for it, first backpack with same items + for i, container in ipairs(lootContainersCandidates) do + if container:getItemsCount() < container:getCapacity() or foundItem:isStackable() then -- has space + for j, item in ipairs(container:getItems()) do + if item:getId() == foundItem:getId() then + if foundItem:isStackable() then + if item:getCount() ~= 100 then + g_game.move(foundItem, container:getSlotPosition(j - 1), foundItem:getCount()) + return + end + else + g_game.move(foundItem, container:getSlotPosition(container:getItemsCount()), foundItem:getCount()) + return + end + end + end + end + end + -- now any backpack with empty slot + for i, container in ipairs(lootContainersCandidates) do + if container:getItemsCount() < container:getCapacity() then -- has space + g_game.move(foundItem, container:getSlotPosition(container:getItemsCount()), foundItem:getCount()) + return + end + end + + -- can't find backpack, try to open new + for i, container in ipairs(lootContainersCandidates) do + local candidates = {} + for j, item in ipairs(container:getItems()) do + if item:isContainer() and containers[item:getId()] ~= nil then + table.insert(candidates, item) + end + end + if #candidates > 0 then + g_game.open(candidates[math.random(1,#candidates)], container) + return + end + -- full, close it + g_game.close(container) + return + end + return + end + + -- open remaining containers + if #nextContainers == 0 then + return + end + local delay = 1 + for i=2,#nextContainers do + -- if more than 1 container, open them in new window + context.schedule(delay, function() + g_game.open(nextContainers[i], nil) + end) + delay = delay + 250 + end + context.schedule(delay, function() + g_game.open(nextContainers[1], container) + end) + context.delay(150 + delay) + end) +end + diff --git a/modules/game_bot/panels/tools.lua b/modules/game_bot/panels/tools.lua new file mode 100644 index 0000000000..1094b6e6e7 --- /dev/null +++ b/modules/game_bot/panels/tools.lua @@ -0,0 +1,36 @@ +local context = G.botContext +local Panels = context.Panels + +Panels.TradeMessage = function(parent) + context.macro(60000, "Send message on trade", nil, function() + local trade = context.getChannelId("advertising") + if not trade then + trade = context.getChannelId("trade") + end + if context.storage.autoTradeMessage:len() > 0 and trade then + context.sayChannel(trade, context.storage.autoTradeMessage) + end + end, parent) + context.addTextEdit("autoTradeMessage", context.storage.autoTradeMessage or "I'm using OTClientV8 - https://github.com/OTCv8/otclientv8", function(widget, text) + context.storage.autoTradeMessage = text + end, parent) +end + +Panels.AutoStackItems = function(parent) + context.macro(500, "Auto stacking items", nil, function() + local containers = context.getContainers() + for i, container in pairs(containers) do + local toStack = {} + for j, item in ipairs(container:getItems()) do + if item:isStackable() and item:getCount() ~= 100 then + local otherItem = toStack[item:getId()] + if otherItem then + g_game.move(item, otherItem, item:getCount()) + return + end + toStack[item:getId()] = container:getSlotPosition(j - 1) + end + end + end + end, parent) +end \ No newline at end of file diff --git a/modules/game_bot/panels/war.lua b/modules/game_bot/panels/war.lua new file mode 100644 index 0000000000..44cf767f45 --- /dev/null +++ b/modules/game_bot/panels/war.lua @@ -0,0 +1,127 @@ +local context = G.botContext +local Panels = context.Panels + +Panels.AttackLeaderTarget = function(parent) + local toAttack = nil + context.onMissle(function(missle) + if not context.storage.attackLeader or context.storage.attackLeader:len() == 0 then + return + end + local src = missle:getSource() + if src.z ~= context.posz() then + return + end + local from = g_map.getTile(src) + local to = g_map.getTile(missle:getDestination()) + if not from or not to then + return + end + local fromCreatures = from:getCreatures() + local toCreatures = to:getCreatures() + if #fromCreatures ~= 1 or #toCreatures ~= 1 then + return + end + local c1 = fromCreatures[1] + if c1:getName():lower() == context.storage.attackLeader:lower() then + toAttack = toCreatures[1] + end + end) + context.macro(50, "Attack leader's target", nil, function() + if toAttack and context.storage.attackLeader:len() > 0 and toAttack ~= g_game.getAttackingCreature() then + g_game.attack(toAttack) + toAttack = nil + end + end, parent) + context.addTextEdit("attackLeader", context.storage.attackLeader or "player name", function(widget, text) + context.storage.attackLeader = text + end, parent) +end + + +Panels.LimitFloor = function(parent) + context.onPlayerPositionChange(function(pos) + if context.storage.limitFloor then + local gameMapPanel = modules.game_interface.getMapPanel() + if gameMapPanel then + gameMapPanel:lockVisibleFloor(pos.z) + end + end + end) + + local switch = context.addSwitch("limitFloor", "Don't show higher floors", function(widget) + widget:setOn(not widget:isOn()) + context.storage.limitFloor = widget:isOn() + local gameMapPanel = modules.game_interface.getMapPanel() + if gameMapPanel then + if context.storage.limitFloor then + gameMapPanel:lockVisibleFloor(context.posz()) + else + gameMapPanel:unlockVisibleFloor() + end + end + end, parent) + switch:setOn(context.storage.limitFloor) +end + +Panels.AntiPush = function(parent) + if not parent then + parent = context.panel + end + + local panelName = "antiPushPanel" + local ui = g_ui.createWidget("ItemsPanel", parent) + ui:setId(panelName) + + if not context.storage[panelName] then + context.storage[panelName] = {} + end + + ui.title:setText("Anti push") + ui.title:setOn(context.storage[panelName].enabled) + ui.title.onClick = function(widget) + context.storage[panelName].enabled = not context.storage[panelName].enabled + widget:setOn(context.storage[panelName].enabled) + end + + if type(context.storage[panelName].items) ~= 'table' then + context.storage[panelName].items = {3031, 3035, 0, 0, 0} + end + + for i=1,5 do + ui.items:getChildByIndex(i).onItemChange = function(widget) + context.storage[panelName].items[i] = widget:getItemId() + end + ui.items:getChildByIndex(i):setItemId(context.storage[panelName].items[i]) + end + + context.macro(100, function() + if not context.storage[panelName].enabled then + return + end + local tile = g_map.getTile(context.player:getPosition()) + if not tile then + return + end + local topItem = tile:getTopUseThing() + if topItem and topItem:isStackable() then + topItem = topItem:getId() + else + topItem = 0 + end + local candidates = {} + for i, item in pairs(context.storage[panelName].items) do + if item >= 100 and item ~= topItem and context.findItem(item) then + table.insert(candidates, item) + end + end + if #candidates == 0 then + return + end + if type(context.storage[panelName].lastItem) ~= 'number' or context.storage[panelName].lastItem > #candidates then + context.storage[panelName].lastItem = 1 + end + local item = context.findItem(candidates[context.storage[panelName].lastItem]) + g_game.move(item, context.player:getPosition(), 1) + context.storage[panelName].lastItem = context.storage[panelName].lastItem + 1 + end) +end diff --git a/modules/game_bot/panels/waypoints.lua b/modules/game_bot/panels/waypoints.lua new file mode 100644 index 0000000000..ac29ef99a1 --- /dev/null +++ b/modules/game_bot/panels/waypoints.lua @@ -0,0 +1,775 @@ +local context = G.botContext +local Panels = context.Panels + +Panels.Waypoints = function(parent) + local ui = context.setupUI([[ +Panel + id: waypoints + height: 206 + + BotLabel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text: Waypoints + + ComboBox + id: config + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 5 + text-offset: 3 0 + width: 130 + + Button + id: enableButton + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + + Button + margin-top: 1 + id: add + anchors.top: prev.bottom + anchors.left: parent.left + text: Add + width: 60 + height: 17 + + Button + id: edit + anchors.top: prev.top + anchors.horizontalCenter: parent.horizontalCenter + text: Edit + width: 60 + height: 17 + + Button + id: remove + anchors.top: prev.top + anchors.right: parent.right + text: Remove + width: 60 + height: 17 + + TextList + id: list + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + vertical-scrollbar: listScrollbar + margin-right: 15 + margin-top: 2 + height: 60 + focusable: false + auto-focus: first + + VerticalScrollBar + id: listScrollbar + anchors.top: prev.top + anchors.bottom: prev.bottom + anchors.right: parent.right + pixels-scroll: true + step: 5 + + Label + id: pos + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-align: center + margin-top: 2 + + Button + id: wGoto + anchors.top: prev.bottom + anchors.left: parent.left + text: Goto + width: 61 + margin-top: 1 + height: 17 + + Button + id: wUse + anchors.top: prev.top + anchors.left: prev.right + text: Use + width: 61 + height: 17 + + Button + id: wUseWith + anchors.top: prev.top + anchors.left: prev.right + text: UseWith + width: 61 + height: 17 + + Button + id: wWait + anchors.top: prev.bottom + anchors.left: parent.left + text: Wait + width: 61 + margin-top: 1 + height: 17 + + Button + id: wSay + anchors.top: prev.top + anchors.left: prev.right + text: Say + width: 61 + height: 17 + + Button + id: wNpc + anchors.top: prev.top + anchors.left: prev.right + text: Say NPC + width: 61 + height: 17 + + Button + id: wLabel + anchors.top: prev.bottom + anchors.left: parent.left + text: Label + width: 61 + margin-top: 1 + height: 17 + + Button + id: wFollow + anchors.top: prev.top + anchors.left: prev.right + text: Follow + width: 61 + height: 17 + + Button + id: wFunction + anchors.top: prev.top + anchors.left: prev.right + text: Function + width: 61 + height: 17 + + BotSwitch + id: recording + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text: Auto Recording + height: 17 + +]], parent) + + if type(context.storage.cavebot) ~= "table" then + context.storage.cavebot = {} + end + if type(context.storage.cavebot.configs) ~= "table" then + context.storage.cavebot.configs = {} + end + + local getConfigName = function(config) + local matches = regexMatch(config, [[name:\s*([^\n]*)$]]) + if matches[1] and matches[1][2] then + return matches[1][2]:trim() + end + return nil + end + + local isValidCommand = function(command) + if command == "goto" then + return true + elseif command == "use" then + return true + elseif command == "usewith" then + return true + elseif command == "wait" then + return true + elseif command == "say" then + return true + elseif command == "npc" then + return true + elseif command == "follow" then + return true + elseif command == "label" then + return true + elseif command == "gotolabel" then + return true + elseif command == "comment" then + return true + elseif command == "function" then + return true + end + return false + end + + local commands = {} + local waitTo = 0 + local autoRecording = false + + local parseConfig = function(config) + commands = {} + local matches = regexMatch(config, [[([^:^\n^\s]+)(:?)([^\n]*)]]) + for i = 1, #matches do + local command = matches[i][2] + local validation = (matches[i][3] == ":") + if not validation or isValidCommand(command) then + local text = matches[i][4] + if validation then + table.insert(commands, { command = command:lower(), text = text }) + elseif #commands > 0 then + commands[#commands].text = commands[#commands].text .. "\n" .. matches[i][1] + end + end + end + + for i = 1, #commands do + local label = g_ui.createWidget("CaveBotLabel", ui.list) + label:setText(commands[i].command .. ":" .. commands[i].text) + if commands[i].command == "goto" then + label:setColor("green") + elseif commands[i].command == "label" then + label:setColor("yellow") + elseif commands[i].command == "comment" then + label:setText(commands[i].text) + label:setColor("white") + elseif commands[i].command == "use" or commands[i].command == "usewith" then + label:setColor("orange") + elseif commands[i].command == "gotolabel" then + label:setColor("red") + end + end + end + + local ignoreOnOptionChange = true + local refreshConfig = function(scrollDown) + ignoreOnOptionChange = true + if context.storage.cavebot.enabled then + autoRecording = false + ui.recording:setOn(false) + ui.enableButton:setText("On") + ui.enableButton:setColor('#00AA00FF') + else + ui.enableButton:setText("Off") + ui.enableButton:setColor('#FF0000FF') + ui.recording:setOn(autoRecording) + end + + ui.config:clear() + for i, config in ipairs(context.storage.cavebot.configs) do + local name = getConfigName(config) + if not name then + name = "Unnamed config" + end + ui.config:addOption(name) + end + + if (not context.storage.cavebot.activeConfig or context.storage.cavebot.activeConfig == 0) and #context.storage.cavebot.configs > 0 then + context.storage.cavebot.activeConfig = 1 + end + + ui.list:destroyChildren() + + if context.storage.cavebot.activeConfig and context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + ui.config:setCurrentIndex(context.storage.cavebot.activeConfig) + parseConfig(context.storage.cavebot.configs[context.storage.cavebot.activeConfig]) + end + + context.saveConfig() + if scrollDown and ui.list:getLastChild() then + ui.list:focusChild(ui.list:getLastChild()) + end + + waitTo = 0 + ignoreOnOptionChange = false + end + + + ui.config.onOptionChange = function(widget) + if not ignoreOnOptionChange then + context.storage.cavebot.activeConfig = widget.currentIndex + refreshConfig() + end + end + ui.enableButton.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + context.storage.cavebot.enabled = not context.storage.cavebot.enabled + if autoRecording then + refreshConfig() + elseif context.storage.cavebot.enabled then + ui.enableButton:setText("On") + ui.enableButton:setColor('#00AA00FF') + else + ui.enableButton:setText("Off") + ui.enableButton:setColor('#FF0000FF') + end + end + ui.add.onClick = function() + modules.client_textedit.multilineEditor("Waypoints editor", "name:Config name\nlabel:start\n", function(newText) + table.insert(context.storage.cavebot.configs, newText) + context.storage.cavebot.activeConfig = #context.storage.cavebot.configs + refreshConfig() + end) + end + ui.edit.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + modules.client_textedit.multilineEditor("Waypoints editor", + context.storage.cavebot.configs[context.storage.cavebot.activeConfig], function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = newText + refreshConfig() + end) + end + ui.remove.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + local questionWindow = nil + local closeWindow = function() + questionWindow:destroy() + end + local removeConfig = function() + closeWindow() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + context.storage.cavebot.enabled = false + table.remove(context.storage.cavebot.configs, context.storage.cavebot.activeConfig) + context.storage.cavebot.activeConfig = 0 + refreshConfig() + end + questionWindow = context.displayGeneralBox(tr('Remove config'), tr('Do you want to remove current waypoints config?'), + { + { text = tr('Yes'), callback = removeConfig }, + { text = tr('No'), callback = closeWindow }, + anchor = AnchorHorizontalCenter + }, removeConfig, closeWindow) + end + + -- waypoint editor + -- auto recording + local stepsSincleLastPos = 0 + + context.onPlayerPositionChange(function(newPos, oldPos) + ui.pos:setText("Position: " .. newPos.x .. ", " .. newPos.y .. ", " .. newPos.z) + if not autoRecording then + return + end + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + local newText = "" + if newPos.z ~= oldPos.z then + newText = "goto:" .. oldPos.x .. "," .. oldPos.y .. "," .. oldPos.z + newText = newText .. "\ngoto:" .. newPos.x .. "," .. newPos.y .. "," .. newPos.z + stepsSincleLastPos = 0 + else + stepsSincleLastPos = stepsSincleLastPos + 1 + if stepsSincleLastPos > 10 then + newText = "goto:" .. oldPos.x .. "," .. oldPos.y .. "," .. oldPos.z + stepsSincleLastPos = 0 + end + end + + if newText:len() > 0 then + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\n" .. newText + refreshConfig(true) + end + end) + + context.onUse(function(pos, itemId, stackPos, subType) + if not autoRecording then + return + end + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + if pos.x == 0xFFFF then + return + end + stepsSincleLastPos = 0 + local playerPos = context.player:getPosition() + newText = "goto:" .. + playerPos.x .. "," .. playerPos.y .. "," .. playerPos.z .. "\nuse:" .. pos.x .. "," .. pos.y .. "," .. pos.z + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\n" .. newText + refreshConfig(true) + end) + context.onUseWith(function(pos, itemId, target, subType) + if not autoRecording then + return + end + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + if not target:isItem() then + return + end + local targetPos = target:getPosition() + if targetPos.x == 0xFFFF then + return + end + stepsSincleLastPos = 0 + local playerPos = context.player:getPosition() + newText = "goto:" .. + playerPos.x .. + "," .. + playerPos.y .. + "," .. playerPos.z .. "\nusewith:" .. itemId .. "," .. targetPos.x .. "," .. targetPos.y .. "," .. targetPos.z + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\n" .. newText + refreshConfig(true) + end) + + -- ui + local pos = context.player:getPosition() + ui.pos:setText("Position: " .. pos.x .. ", " .. pos.y .. ", " .. pos.z) + + ui.wGoto.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + local pos = context.player:getPosition() + modules.client_textedit.singlelineEditor("" .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\ngoto:" .. newText + refreshConfig(true) + end) + end + + ui.wUse.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + local pos = context.player:getPosition() + modules.client_textedit.singlelineEditor("" .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nuse:" .. newText + refreshConfig(true) + end) + end + + ui.wUseWith.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + local pos = context.player:getPosition() + modules.client_textedit.singlelineEditor("ITEMID," .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nusewith:" .. newText + refreshConfig(true) + end) + end + + ui.wWait.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + modules.client_textedit.singlelineEditor("1000", function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nwait:" .. newText + refreshConfig(true) + end) + end + + ui.wSay.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + modules.client_textedit.singlelineEditor("text", function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nsay:" .. newText + refreshConfig(true) + end) + end + + ui.wNpc.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + modules.client_textedit.singlelineEditor("text", function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nnpc:" .. newText + refreshConfig(true) + end) + end + + ui.wLabel.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + modules.client_textedit.singlelineEditor("label name", function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nlabel:" .. newText + refreshConfig(true) + end) + end + + ui.wFollow.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + modules.client_textedit.singlelineEditor("creature name", function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nfollow:" .. newText + refreshConfig(true) + end) + end + + ui.wFunction.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + modules.client_textedit.multilineEditor("Add function", + "function(waypoints)\n -- your lua code, function is executed if previous goto was successful or is just after label\n\n -- must return true to execute next command, otherwise will run in loop till correct return\n return true\nend", + function(newText) + context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs + [context.storage.cavebot.activeConfig] .. "\nfunction:" .. newText + refreshConfig(true) + end) + end + + ui.recording.onClick = function() + if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then + return + end + autoRecording = not autoRecording + if autoRecording then + context.storage.cavebot.enabled = false + stepsSincleLastPos = 10 + end + refreshConfig(true) + end + + refreshConfig() + + local usedGotoLabel = false + local executeNextMacroCall = false + local commandExecutionNo = 0 + local lastGotoSuccesful = true + local lastOpenedContainer = 0 + + local functions = { + enable = function() + context.storage.cavebot.enabled = true + refreshConfig() + end, + disable = function() + context.storage.cavebot.enabled = false + refreshConfig() + end, + refresh = function() + refreshConfig() + end, + wait = function(peroid) + waitTo = context.now + peroid + end, + waitTo = function(timepoint) + waitTo = timepoint + end, + gotoLabel = function(name) + for i = 1, ui.list:getChildCount() do + local command = commands[i] + if command and command.command == "label" and command.text == name then + ui.list:focusChild(ui.list:getChildByIndex(i)) + usedGotoLabel = true + lastGotoSuccesful = true + return true + end + end + end + } + + context.onContainerOpen(function(container) + if container:getItemsCount() > 0 then + lastOpenedContainer = context.now + container:getItemsCount() * 100 + end + end) + + + context.macro(250, function() + if not context.storage.cavebot.enabled then + return + end + + if modules.game_interface.lastManualWalk + 500 > context.now then + return + end + + -- wait if walked or opened container recently + if context.player:isWalking() or lastOpenedContainer + 1000 > context.now then + executeNextMacroCall = false + return + end + + -- wait if attacking/following creature + local attacking = g_game.getAttackingCreature() + local following = g_game.getFollowingCreature() + if (attacking and context.getCreatureById(attacking:getId()) and not attacking.ignoreByWaypoints) or (following and context.getCreatureById(following:getId())) then + executeNextMacroCall = false + return + end + + if not executeNextMacroCall then + executeNextMacroCall = true + return + end + executeNextMacroCall = false + + local commandWidget = ui.list:getFocusedChild() + if not commandWidget then + if ui.list:getFirstChild() then + ui.list:focusChild(ui.list:getFirstChild()) + end + return + end + + local commandIndex = ui.list:getChildIndex(commandWidget) + local command = commands[commandIndex] + if not command then + if ui.list:getFirstChild() then + ui.list:focusChild(ui.list:getFirstChild()) + end + return + end + + if commandIndex == 1 then + lastGotoSuccesful = true + end + + if command.command == "goto" or command.command == "follow" then + local matches = regexMatch(command.text, [[([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)]]) + if (#matches == 1 and #matches[1] == 4) or command.command == "follow" then + local position = nil + if command.command == "follow" then + local creature = context.getCreatureByName(command.text) + if creature then + position = creature:getPosition() + end + else + position = { x = tonumber(matches[1][2]), y = tonumber(matches[1][3]), z = tonumber(matches[1][4]) } + end + local distance = 0 + if position then + distance = context.getDistanceBetween(position, context.player:getPosition()) + end + if distance > 100 or not position or position.z ~= context.player:getPosition().z then + lastGotoSuccesful = false + elseif distance > 0 then + if not context.findPath(context.player:getPosition(), position, 100, { ignoreNonPathable = true, precision = 1, ignoreCreatures = true }) then + lastGotoSuccesful = false + executeNextMacroCall = true + else + commandExecutionNo = commandExecutionNo + 1 + lastGotoSuccesful = false + if commandExecutionNo <= 3 then -- try max 3 times + if not context.autoWalk(position, distance * 2, { ignoreNonPathable = false }) then + if commandExecutionNo > 1 then + if context.autoWalk(position, distance * 2, { ignoreNonPathable = true, precision = 1 }) then + context.delay(500) + end + end + return + end + return + elseif commandExecutionNo == 4 then -- try last time, location close to destination + if context.autoWalk(position, distance * 2, { ignoreNonPathable = true, ignoreLastCreature = true, precision = 2, allowUnseen = true }) then + context.delay(500) + return + end + elseif distance <= 2 then + lastGotoSuccesful = true + executeNextMacroCall = true + end + end + else + lastGotoSuccesful = true + executeNextMacroCall = true + end + else + context.error("Waypoints: invalid use of goto function") + end + elseif command.command == "use" then + local matches = regexMatch(command.text, [[([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)]]) + if #matches == 1 and #matches[1] == 4 then + local position = { x = tonumber(matches[1][2]), y = tonumber(matches[1][3]), z = tonumber(matches[1][4]) } + if context.player:getPosition().z == position.z then + local tile = g_map.getTile(position) + if tile then + local topThing = tile:getTopUseThing() + if topThing then + g_game.use(topThing) + context.delay(500) + end + end + end + else + context.error("Waypoints: invalid use of use function") + end + elseif command.command == "usewith" then + local matches = regexMatch(command.text, [[([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)]]) + if #matches == 1 and #matches[1] == 5 then + local itemId = tonumber(matches[1][2]) + local position = { x = tonumber(matches[1][3]), y = tonumber(matches[1][4]), z = tonumber(matches[1][5]) } + if context.player:getPosition().z == position.z then + local tile = g_map.getTile(position) + if tile then + local topThing = tile:getTopUseThing() + if topThing then + context.useWith(itemId, topThing) + context.delay(500) + end + end + end + else + context.error("Waypoints: invalid use of usewith function") + end + elseif command.command == "wait" and lastGotoSuccesful then + if not waitTo or waitTo == 0 then + waitTo = context.now + tonumber(command.text) + end + if context.now < waitTo then + return + end + waitTo = 0 + elseif command.command == "say" and lastGotoSuccesful then + context.say(command.text) + elseif command.command == "npc" and lastGotoSuccesful then + context.sayNpc(command.text) + elseif command.command == "function" and lastGotoSuccesful then + usedGotoLabel = false + local status, result = pcall(function() + return assert(load("return " .. command.text, nil, nil, context))()(functions) + end) + if not status then + context.error("Waypoints function execution error:\n" .. result) + context.delay(2500) + end + if not result or usedGotoLabel then + return + end + elseif command.command == "gotolabel" then + if functions.gotoLabel(command.text) then + return + end + end + + local nextIndex = 1 + commandIndex % #commands + local nextChild = ui.list:getChildByIndex(nextIndex) + if nextChild then + ui.list:focusChild(nextChild) + commandExecutionNo = 0 + end + end) + + return functions +end diff --git a/modules/game_bot/scripts.png b/modules/game_bot/scripts.png new file mode 100644 index 0000000000000000000000000000000000000000..640c0e9e4a2b7c9e69ca22e5d8cd3070c92df3df GIT binary patch literal 14295 zcmcJWWmFvRm!Ja#2p-%aKyY`r#$AKEySoN=clY4#9$XuD3Bldnx0Bz0c6R2>%%1(Q zAG*5goa(B2yQ}W|+~?j7lb01kfW?9R@Zkf3gt)NchYw&kp!MWWP@pA6mWM6q?W3un zjNpe4wJ~rn`jDV)7&~!w#}6M6`~SSalBf}JKYU$KdFT#Dj>5_W37IPO>@oPqOPXGEw6b_RIUuCdT== zncGcs+_TewUmqd$u!8lme#jY8-n>5Fk9`7d1nWOxu>W#1tN|4NYm)#O{mFbo^-Dt3 zdsO9N-r}7(^F{nIgVT-DRqCJpQ?p6)0q-@NbY6R9)e*d-Gv;PCE0Wd(9lZ9xky>7o zacUxhyB(^MJ1)U#6+VY@@zLi(Vfl7XqWxAXcCUD7aw6&*ONu7jB<&$ZQCK4 z`cQTS{d!;51I=AX)6DU*s-31uJ*{o`t4k)Yr^-NEF?&OlPDmEFE7=P-RpKy4o2D_{ zou_2ZERfS-CltQ6O+hFGQA9{6pyK>;d+73EJ4Wo3;U<}|N$2k9$b#o)N~q52l8l4| z0vd@RakWcS^-SCK`1me8@w|QGwW_1|hKBF?q#pAYd%GzdLEC&NJ8)F&9ML1BlUH1@8NR$`GmWyT)1h9bUMjhPGjHqWt%WABZC^^bSQm{z8f!t-F7e< zpL+*l236kH?RcTG#ctC-{169~Oga`en~OUPP_go(THyZ7G3Oa?W_EVBUi|)MDo==t zm1#{SY+i*|Thzlf5|2|6|5gSi4{^OGXly--iM@GRz0N2Mo-A)DE*JV`UR8G>rTfJg z+bn16Q@z+jx$djvFXeSeOiCc4hhSl0p?HyOY@_VHe6DvqyU93K$MaeH@c4K`#p9Hh zO`b=`+tcyGZME-uB_VxM%BajPd5NplLW=)cbTf6iHup5=oQlb>DYhcZ)Mx&u>6qRt zWDiaZS`DMD{ou2PDDBs*GkjjLogcH;M+dj0PRlGOf`_uFpFM!KUfZ}=8c(dCs}haG zB8tgKPv3EGcRf!IA4OPG;hj|p)FCf6%>opXS1c(RIGYPQW;fxAdyzkYqY zoA>cuWhQA2KTqU__z1amcEx*R?9$kVtf->jjvp{j4^NuYZ-&?EieG4l?l4Jyl0XSNg{FgbmW zd3tPI#FpR0zP z(f`hCHh zOSOhq9HWICkJXm+M4+ z1OcitA)paU3iODJkZ*ENSDTSkwVji^%%}Qsr~49Y5T*;1Tm9`bCJ`NqU+;b2+27vo zyGdwi5jZ(HsRnp&re*_U%gU(Gx20W2S7!V3t+&8B;Y(h{=gwMvQ)ORJ5zkaa zphplTC1u^_L5Fs`OKE0L3(yogDZ=CTO+gW(&{3KzXdcXE-|M4_^npl~e!WKv!FO9gv^`Q93tMMxzB254%^V=R>dFl1@~>1 z>jrMI(!$i~U%BQ8lo8`4F83btb?A%hf=45qP8tZ)V{)5*SSmb#eoMkcjHs$61i{6c za{jyxZvW8=erV-AapyVX42S$ip#xUnvmQal6z|+_ThJ=io3(NB?3p&;z&UCmdir4E zHS#87cFZi9yx|pi_v2>jK8>zDH}@wO{R(eGzyyqU@~6&75AI+cA^zw@QDKV&VWO=K z2mV@S!W7*E>bnzKN5|E>w;YC-Cts$fgr?Az>=2q`P1{cN?nj_);j=oqBF=2RyzTR` z?mL%`=PBzU7-*_LG)KARMRSMOe#2Ey!#y-Gw8kF9PVX1DVRJxbgim;g6%^UN+gl@t zb+Tf=FD(|IyO*D1yfO6sb&#Vg`uvQh6Nllx>bV+7fT_~9MCQGv zbR-#)Bye* zAob&S^uN1){ZKK54LUd(7?wc9KJ#nHY_Z(-pK!1F<2;_il9L$|+Q$b4wQ1@Zxv*bK zQgTO9?0SFFv}gi!EP)!pIeF^n%iHt0n7ut?*Yo;$og(Py5?|Ev?jf9veHOJ{M>w9C z{VL`_r$7lQtT&x}0)a#YVw!Lunv1qJKABv0g#mb~Z(mNz#Hr>K76n2UgS6`JeR4+q zpa}-=&pt%V>wY;o{ejwN|MBX-4AR~6x^4OGUv#4uZz2s8DB}rU`u5L{?f=f+qzczh zr9)jdF7pXF3T=6_yp~sCkjB4-Y;c9r=~;Wf(2sw7Te`(vyDdb*a)-`-WUEn>bA;@U zA?D@h9>df>JU$XAIOi35jp0x`7-@OCaC9?B;WEP7BaV&C-yORMk%XPK1KLX2t3DDW z?R}(d`{q_FgMWt+EGMd7RO%&y9mLda-BhwzkibYx5fuix9RV>3v|e%SAMLnD#Y}lF z?B%x8>!aqVh{g_XO^xjBrtgMW?C!$l{wBRahOK(-MHdW1!e@9;n!xDXFQumLEiLj< zh%T$0K)$CAC(*g)7=8SkxzR3Rm5%XlR{5AE+)n$k-^SPBh;aR_{tJ4&8F503qLw9^*=4sAgOhUv~ zRhE-W25jri`<#tg%$OFLa9mw!n=VYNbyq>Uti|_|U1a__grDG&8Cibs-VVqrTRtS! z96s;O^ySb19$|+y>5()9p7SYpJuI@!i;dixEoaQ~Oo;6}*S%fWXOt=;SI#2ZZV2dX zmLz+-74QfDy$@I)QyAAEPn31o>#$9OH?slux*M?Y%O_rAA*-Y4k<#aDv>7vGSx0#>7X=*AF;Q*5Vqp>xXaV$VblDyN>MRRuII!DuZ8Iq-8`2%?q8x&^BVSzjnc9X|ZmQHK&_EHZ*L%#*MUu-|b3?4tf|qB(*^Cxoj{*QcI^? zFEVB$uD?MUzH%#FjoW)2sqwtG)h$t;PD!a1&xo75xkPX$v18O>N49W6xNRF(ctU_7 zMHb?({!+B2%OO9_zVd7n3#$iU!skRn>Po3VgV(KGqP)?!>b=$DFFK6?*iBCT`3B-_ zgpl)n;`{ zxM%Sn9wnrg{=0al2Z|t|paJtiF5K{+h)3)6CyGjZP|N$j#Kzp!`n54{i;%y`UIf5i zG!pHJ!@AY+2f5iG9OU7{p{Bw8o?=CHHr$Z$F29-R>8!}Zz^~H_1s{e1yOxQ(E4;PW3m4}?dq*}V8;wZPjgh`#%Ws*zmw147SydykKAEgg zA^McxA##bD)}p7kz+x2=^4?p2qmbB3GgKdS_|@k1C|B^jw%`mkQX1$0s2-laO43CP+N6{C6 zIulg)No6naywc&t$%2bOt43lVunxv|CTU#r_+kaxVgtc8%(4nNw2tayQJoerU5MNRd{`n@kjuC$LIkXkfZMO+N7(*BnxeCi@ zza?z6=HoiGaDH#NKe#_N)1a9J_F{c6>&3!?T`^%8s=Rlt7a3|~_;xMhRt~fc#Akgt z0x}6wiE8Vsb##Dr7gW}~by1NtZ5LI>C7LphN1)us439Uvh!+pN#8S za5xFK!qu8LM_owQ@jL8|Z>&sUrk5A{?JAc#%B8V9W9QrG zqK#~OHNW5J)--`lMidqSm;?d`nw%%UaGRp&xNlNQr>2rLZ+CGB?x}aUF&^pa&?i0d zKTH;E?tJNBd?Z+#^#c50UN62gmq|M>L2#+m%?LSHg)6E;gn%UxIQ`+!3Vav^f0=b^ zh1)o1Z)*Y<`^AUZy@o8`!SB5?g8DnFYSiUhG16V1@+J16t46r(&T0AO7=d!2@{nyA z(^e%fhxn?FB3rP&2RWnaK~`3l!?U~u3?HizLl?6p2Hoq;XG=>1s#^sa)!(DZMX3x# z5IGMwrr)S$dDHFfs#iK6`PMAB&oWFYS*7&VQmYQF))+|hD+ z2R}~MX)heE;09$`{`PbuwdkB<*IDC%Rh?~OPma=0)e+L7aoQTbOmN#5kdj~WEyS0Q zkZ?Px9Wojw-0pkq&gw}zw-tC}|1omiyBtV}Flm3T;vA%!c1a<`W8RwsWL(IwacWqr zlAToXjN&L+1Ukr+Py9R$qFX=PR10Um!HIVm$IlMTsQg_SMKXri%8_>~bAc>N1}fxH6_SwWUbB_Rsha8TzIt1? zBbyS>T1}9%U_-N~F2crbep$7A`(kz7(06Youvh-9?DT29&8By=uY649C*~eQV?^^L z_3Erwv{l|UxV25=d06E`C+TD