1local context = G.botContext
2local Panels = context.Panels
3
4Panels.MonsterEditor = function(monster, config, callback, parent)
5 local otherWindow = g_ui.getRootWidget():getChildById('monsterEditor')
6 if otherWindow then
7 otherWindow:destory()
8 end
9
10 local window = context.setupUI([[
11MainWindow
12 id: monsterEditor
13 size: 450 450
14 !text: tr("Edit monster")
15
16 Label
17 id: info
18 anchors.left: parent.left
19 anchors.right: parent.right
20 anchors.top: parent.top
21 text-align: center
22 text: Use monster name * for any other monster not on the list
23
24 Label
25 id: info2
26 anchors.left: parent.left
27 anchors.right: parent.right
28 anchors.top: prev.bottom
29 text-align: center
30 text: Add number (1-5) at the end of the name to create multiple configs
31
32 TextEdit
33 id: name
34 anchors.top: prev.bottom
35 anchors.left: parent.left
36 anchors.right: parent.right
37 margin-left: 100
38 margin-top: 5
39 multiline: false
40
41 Label
42 anchors.verticalCenter: prev.verticalCenter
43 anchors.left: parent.left
44 text: Target name:
45
46 Label
47 id: priorityText
48 anchors.left: parent.left
49 anchors.right: parent.horizontalCenter
50 anchors.top: prev.bottom
51 margin-right: 10
52 margin-top: 10
53 text: Priority
54 text-align: center
55
56 HorizontalScrollBar
57 id: priority
58 anchors.left: prev.left
59 anchors.right: prev.right
60 anchors.top: prev.bottom
61 margin-top: 5
62 minimum: 0
63 maximum: 10
64 step: 1
65
66 Label
67 id: dangerText
68 anchors.left: parent.left
69 anchors.right: parent.horizontalCenter
70 anchors.top: prev.bottom
71 margin-right: 10
72 margin-top: 10
73 text: Danger
74 text-align: center
75
76 HorizontalScrollBar
77 id: danger
78 anchors.left: prev.left
79 anchors.right: prev.right
80 anchors.top: prev.bottom
81 margin-top: 5
82 minimum: 0
83 maximum: 10
84 step: 1
85
86 Label
87 id: maxDistanceText
88 anchors.left: parent.left
89 anchors.right: parent.horizontalCenter
90 anchors.top: prev.bottom
91 margin-right: 10
92 margin-top: 10
93 text: Max distance to target
94 text-align: center
95
96 HorizontalScrollBar
97 id: maxDistance
98 anchors.left: prev.left
99 anchors.right: prev.right
100 anchors.top: prev.bottom
101 margin-top: 5
102 minimum: 1
103 maximum: 10
104 step: 1
105
106 Label
107 id: distanceText
108 anchors.left: parent.left
109 anchors.right: parent.horizontalCenter
110 anchors.top: prev.bottom
111 margin-right: 10
112 margin-top: 10
113 text: Keep distance
114 text-align: center
115
116 HorizontalScrollBar
117 id: distance
118 anchors.left: prev.left
119 anchors.right: prev.right
120 anchors.top: prev.bottom
121 margin-top: 5
122 minimum: 0
123 maximum: 5
124 step: 1
125
126 Label
127 id: minHealthText
128 anchors.left: parent.left
129 anchors.right: parent.horizontalCenter
130 anchors.top: prev.bottom
131 margin-right: 10
132 margin-top: 10
133 text: Minimum Health
134 text-align: center
135
136 HorizontalScrollBar
137 id: minHealth
138 anchors.left: prev.left
139 anchors.right: prev.right
140 anchors.top: prev.bottom
141 margin-top: 5
142 minimum: 0
143 maximum: 100
144 step: 1
145
146 Label
147 id: maxHealthText
148 anchors.left: parent.left
149 anchors.right: parent.horizontalCenter
150 anchors.top: prev.bottom
151 margin-right: 10
152 margin-top: 10
153 text: Maximum Health
154 text-align: center
155
156 HorizontalScrollBar
157 id: maxHealth
158 anchors.left: prev.left
159 anchors.right: prev.right
160 anchors.top: prev.bottom
161 margin-top: 5
162 minimum: 0
163 maximum: 100
164 step: 1
165
166 Label
167 id: dangerText
168 anchors.left: parent.left
169 anchors.right: parent.horizontalCenter
170 anchors.top: prev.bottom
171 margin-right: 5
172 margin-top: 10
173 text: If total danger is high (>8) bot won't auto loot until it's low again and will be trying to minimize it
174 text-align: center
175 text-wrap: true
176 text-auto-resize: true
177
178 Label
179 id: attackSpellText
180 anchors.left: parent.left
181 anchors.right: parent.horizontalCenter
182 anchors.top: prev.bottom
183 margin-right: 5
184 margin-top: 10
185 text: Attack spell and attack rune are only used when you have more than 30% health
186 text-align: center
187 text-wrap: true
188 text-auto-resize: true
189
190 BotSwitch
191 id: attack
192 anchors.left: parent.horizontalCenter
193 anchors.top: name.bottom
194 margin-left: 10
195 margin-top: 10
196 width: 55
197 text: Attack
198
199 BotSwitch
200 id: ignore
201 anchors.left: prev.right
202 anchors.top: name.bottom
203 margin-left: 18
204 margin-top: 10
205 width: 55
206 text: Ignore
207
208 BotSwitch
209 id: avoid
210 anchors.left: prev.right
211 anchors.top: name.bottom
212 margin-left: 18
213 margin-top: 10
214 width: 55
215 text: Avoid
216
217 BotSwitch
218 id: keepDistance
219 anchors.left: parent.horizontalCenter
220 anchors.right: parent.right
221 anchors.top: prev.bottom
222 margin-left: 10
223 margin-top: 10
224 text: Keep distance
225
226 BotSwitch
227 id: avoidAttacks
228 anchors.left: parent.horizontalCenter
229 anchors.right: parent.right
230 anchors.top: prev.bottom
231 margin-left: 10
232 margin-top: 10
233 text: Avoid monster attacks
234
235 BotSwitch
236 id: chase
237 anchors.left: parent.horizontalCenter
238 anchors.right: parent.right
239 anchors.top: prev.bottom
240 margin-left: 10
241 margin-top: 10
242 text: Chase when has low health
243
244 BotSwitch
245 id: loot
246 anchors.left: parent.horizontalCenter
247 anchors.right: parent.right
248 anchors.top: prev.bottom
249 margin-left: 10
250 margin-top: 10
251 text: Loot corpse
252
253 BotSwitch
254 id: monstersOnly
255 anchors.left: parent.horizontalCenter
256 anchors.right: parent.right
257 anchors.top: prev.bottom
258 margin-left: 10
259 margin-top: 10
260 text: Only for monsters
261
262 BotSwitch
263 id: dontWalk
264 anchors.left: parent.horizontalCenter
265 anchors.right: parent.right
266 anchors.top: prev.bottom
267 margin-left: 10
268 margin-top: 10
269 text: Don't walk to target
270
271 Label
272 id: attackSpellText
273 anchors.left: parent.horizontalCenter
274 anchors.right: parent.right
275 anchors.top: prev.bottom
276 margin-left: 10
277 margin-top: 10
278 text: Attack Spell:
279 text-align: center
280
281 TextEdit
282 id: attackSpell
283 anchors.left: prev.left
284 anchors.right: prev.right
285 anchors.top: prev.bottom
286 margin-top: 2
287
288 Label
289 id: attackItemText
290 anchors.left: parent.horizontalCenter
291 anchors.top: prev.bottom
292 margin-top: 20
293 margin-left: 20
294 text: Attack rune:
295 text-align: left
296
297 BotItem
298 id: attackItem
299 anchors.right: parent.right
300 anchors.verticalCenter: prev.verticalCenter
301 margin-right: 30
302
303 Button
304 id: okButton
305 !text: tr('Ok')
306 anchors.bottom: parent.bottom
307 anchors.right: next.left
308 margin-right: 10
309 width: 60
310
311 Button
312 id: cancelButton
313 !text: tr('Cancel')
314 anchors.bottom: parent.bottom
315 anchors.right: parent.right
316 width: 60
317]], g_ui.getRootWidget())
318
319 local destroy = function()
320 window:destroy()
321 end
322 local doneFunc = function()
323 local monster = window.name:getText()
324 local config = {
325 priority = window.priority:getValue(),
326 danger = window.danger:getValue(),
327 maxDistance = window.maxDistance:getValue(),
328 distance = window.distance:getValue(),
329 minHealth = window.minHealth:getValue(),
330 maxHealth = window.maxHealth:getValue(),
331 attack = window.attack:isOn(),
332 ignore = window.ignore:isOn(),
333 avoid = window.avoid:isOn(),
334 keepDistance = window.keepDistance:isOn(),
335 avoidAttacks = window.avoidAttacks:isOn(),
336 chase = window.chase:isOn(),
337 loot = window.loot:isOn(),
338 monstersOnly = window.monstersOnly:isOn(),
339 dontWalk = window.dontWalk:isOn(),
340 attackItem = window.attackItem:getItemId(),
341 attackSpell = window.attackSpell:getText()
342 }
343 destroy()
344 callback(monster, config)
345 end
346
347 window.okButton.onClick = doneFunc
348 window.cancelButton.onClick = destroy
349 window.onEnter = doneFunc
350 window.onEscape = destroy
351
352
353 window.priority.onValueChange = function(scroll, value)
354 window.priorityText:setText("Priority: " .. value)
355 end
356 window.danger.onValueChange = function(scroll, value)
357 window.dangerText:setText("Danger: " .. value)
358 end
359 window.maxDistance.onValueChange = function(scroll, value)
360 window.maxDistanceText:setText("Max distance to target: " .. value)
361 end
362 window.distance.onValueChange = function(scroll, value)
363 window.distanceText:setText("Keep distance: " .. value)
364 end
365 window.minHealth.onValueChange = function(scroll, value)
366 window.minHealthText:setText("Minimum health: " .. value .. "%")
367 end
368 window.maxHealth.onValueChange = function(scroll, value)
369 window.maxHealthText:setText("Maximum health: " .. value .. "%")
370 end
371
372 window.priority:setValue(config.priority or 1)
373 window.danger:setValue(config.danger or 1)
374 window.maxDistance:setValue(config.maxDistance or 6)
375 window.distance:setValue(config.distance or 1)
376 window.minHealth:setValue(1) -- to force onValueChange update
377 window.maxHealth:setValue(1) -- to force onValueChange update
378 window.minHealth:setValue(config.minHealth or 0)
379 window.maxHealth:setValue(config.maxHealth or 100)
380
381 window.attackSpell:setText(config.attackSpell or "")
382 window.attackItem:setItemId(config.attackItem or 0)
383
384 window.attack.onClick = function(widget)
385 if widget:isOn() then
386 return
387 end
388 widget:setOn(true)
389 window.ignore:setOn(false)
390 window.avoid:setOn(false)
391 end
392 window.ignore.onClick = function(widget)
393 if widget:isOn() then
394 return
395 end
396 widget:setOn(true)
397 window.attack:setOn(false)
398 window.avoid:setOn(false)
399 end
400 window.avoid.onClick = function(widget)
401 if widget:isOn() then
402 return
403 end
404 widget:setOn(true)
405 window.attack:setOn(false)
406 window.ignore:setOn(false)
407 end
408
409 window.attack:setOn(config.attack)
410 window.ignore:setOn(config.ignore)
411 window.avoid:setOn(config.avoid)
412 if not window.attack:isOn() and not window.ignore:isOn() and not window.avoid:isOn() then
413 window.attack:setOn(true)
414 end
415
416 window.keepDistance.onClick = function(widget)
417 widget:setOn(not widget:isOn())
418 end
419 window.avoidAttacks.onClick = function(widget)
420 widget:setOn(not widget:isOn())
421 end
422 window.chase.onClick = function(widget)
423 widget:setOn(not widget:isOn())
424 end
425 window.loot.onClick = function(widget)
426 widget:setOn(not widget:isOn())
427 end
428 window.monstersOnly.onClick = function(widget)
429 widget:setOn(not widget:isOn())
430 end
431 window.dontWalk.onClick = function(widget)
432 widget:setOn(not widget:isOn())
433 end
434
435 window.keepDistance:setOn(config.keepDistance)
436 window.avoidAttacks:setOn(config.avoidAttacks)
437 window.chase:setOn(config.chase)
438 window.loot:setOn(config.loot)
439 if config.loot == nil then
440 window.loot:setOn(true)
441 end
442 window.monstersOnly:setOn(config.monstersOnly)
443 if config.monstersOnly == nil then
444 window.monstersOnly:setOn(true)
445 end
446 window.dontWalk:setOn(config.dontWalk)
447
448 window.name:setText(monster)
449
450 window:show()
451 window:raise()
452 window:focus()
453end
454
455Panels.Attacking = function(parent)
456 local ui = context.setupUI([[
457Panel
458 id: attacking
459 height: 140
460
461 BotLabel
462 anchors.top: parent.top
463 anchors.left: parent.left
464 anchors.right: parent.right
465 text: Attacking
466
467 ComboBox
468 id: config
469 anchors.top: prev.bottom
470 anchors.left: parent.left
471 margin-top: 5
472 text-offset: 3 0
473 width: 130
474
475 Button
476 id: enableButton
477 anchors.top: prev.top
478 anchors.left: prev.right
479 anchors.right: parent.right
480 margin-left: 5
481
482 Button
483 margin-top: 1
484 id: add
485 anchors.top: prev.bottom
486 anchors.left: parent.left
487 text: Add
488 width: 60
489 height: 17
490
491 Button
492 id: edit
493 anchors.top: prev.top
494 anchors.horizontalCenter: parent.horizontalCenter
495 text: Edit
496 width: 60
497 height: 17
498
499 Button
500 id: remove
501 anchors.top: prev.top
502 anchors.right: parent.right
503 text: Remove
504 width: 60
505 height: 17
506
507 TextList
508 id: list
509 anchors.top: prev.bottom
510 anchors.left: parent.left
511 anchors.right: parent.right
512 vertical-scrollbar: listScrollbar
513 margin-right: 15
514 margin-top: 2
515 height: 60
516 focusable: false
517 auto-focus: first
518
519 VerticalScrollBar
520 id: listScrollbar
521 anchors.top: prev.top
522 anchors.bottom: prev.bottom
523 anchors.right: parent.right
524 pixels-scroll: true
525 step: 5
526
527 Button
528 margin-top: 2
529 id: mAdd
530 anchors.top: prev.bottom
531 anchors.left: parent.left
532 text: Add
533 width: 60
534 height: 17
535
536 Button
537 id: mEdit
538 anchors.top: prev.top
539 anchors.horizontalCenter: parent.horizontalCenter
540 text: Edit
541 width: 60
542 height: 17
543
544 Button
545 id: mRemove
546 anchors.top: prev.top
547 anchors.right: parent.right
548 text: Remove
549 width: 60
550 height: 17
551
552]], parent)
553
554 if type(context.storage.attacking) ~= "table" then
555 context.storage.attacking = {}
556 end
557 if type(context.storage.attacking.configs) ~= "table" then
558 context.storage.attacking.configs = {}
559 end
560
561 local getConfigName = function(config)
562 local matches = regexMatch(config, [[name:\s*([^\n]*)$]])
563 if matches[1] and matches[1][2] then
564 return matches[1][2]:trim()
565 end
566 return nil
567 end
568
569 local commands = {}
570 local monsters = {}
571 local configName = nil
572 local refreshConfig = nil -- declared later
573
574 local createNewConfig = function()
575 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
576 return
577 end
578
579 local newConfig = ""
580 if configName ~= nil then
581 newConfig = "name:" .. configName .. "\n"
582 end
583 for monster, config in pairs(monsters) do
584 newConfig = newConfig .. "\n" .. monster .. ":" .. json.encode(config, 2) .. "\n"
585 end
586
587 context.storage.attacking.configs[context.storage.attacking.activeConfig] = newConfig
588 refreshConfig()
589 end
590
591 local parseConfig = function(config)
592 commands = {}
593 monsters = {}
594 configName = nil
595
596 local matches = regexMatch(config, [[([^:^\n]+)(:?)([^\n]*)]])
597 for i=1,#matches do
598 local command = matches[i][2]
599 local validation = (matches[i][3] == ":")
600 local text = matches[i][4]
601 if validation then
602 table.insert(commands, {command=command:lower(), text=text})
603 elseif #commands > 0 then
604 commands[#commands].text = commands[#commands].text .. "\n" .. matches[i][1]
605 end
606 end
607 local labels = {}
608 for i, command in ipairs(commands) do
609 if commands[i].command == "name" then
610 configName = commands[i].text
611 else
612 local status, result = pcall(function() return json.decode(command.text) end)
613 if not status then
614 context.error("Invalid monster config: " .. commands[i].command .. ", error: " .. result)
615 else
616 monsters[commands[i].command] = result
617 table.insert(labels, commands[i].command)
618 end
619 end
620 end
621 table.sort(labels)
622 for i, text in ipairs(labels) do
623 local label = g_ui.createWidget("CaveBotLabel", ui.list)
624 label:setText(text)
625 end
626 end
627
628 local ignoreOnOptionChange = true
629 refreshConfig = function(scrollDown)
630 ignoreOnOptionChange = true
631 if context.storage.attacking.enabled then
632 ui.enableButton:setText("On")
633 ui.enableButton:setColor('#00AA00FF')
634 else
635 ui.enableButton:setText("Off")
636 ui.enableButton:setColor('#FF0000FF')
637 end
638
639 ui.config:clear()
640 for i, config in ipairs(context.storage.attacking.configs) do
641 local name = getConfigName(config)
642 if not name then
643 name = "Unnamed config"
644 end
645 ui.config:addOption(name)
646 end
647
648 if (not context.storage.attacking.activeConfig or context.storage.attacking.activeConfig == 0) and #context.storage.attacking.configs > 0 then
649 context.storage.attacking.activeConfig = 1
650 end
651
652 ui.list:destroyChildren()
653
654 if context.storage.attacking.activeConfig and context.storage.attacking.configs[context.storage.attacking.activeConfig] then
655 ui.config:setCurrentIndex(context.storage.attacking.activeConfig)
656 parseConfig(context.storage.attacking.configs[context.storage.attacking.activeConfig])
657 end
658
659 context.saveConfig()
660 if scrollDown and ui.list:getLastChild() then
661 ui.list:focusChild(ui.list:getLastChild())
662 end
663
664 ignoreOnOptionChange = false
665 end
666
667
668 ui.config.onOptionChange = function(widget)
669 if not ignoreOnOptionChange then
670 context.storage.attacking.activeConfig = widget.currentIndex
671 refreshConfig()
672 end
673 end
674 ui.enableButton.onClick = function()
675 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
676 return
677 end
678 context.storage.attacking.enabled = not context.storage.attacking.enabled
679 refreshConfig()
680 end
681 ui.add.onClick = function()
682 modules.client_textedit.multilineEditor("Target list editor", "name:Config name", function(newText)
683 table.insert(context.storage.attacking.configs, newText)
684 context.storage.attacking.activeConfig = #context.storage.attacking.configs
685 refreshConfig()
686 end)
687 end
688 ui.edit.onClick = function()
689 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
690 return
691 end
692 modules.client_textedit.multilineEditor("Target list editor", context.storage.attacking.configs[context.storage.attacking.activeConfig], function(newText)
693 context.storage.attacking.configs[context.storage.attacking.activeConfig] = newText
694 refreshConfig()
695 end)
696 end
697 ui.remove.onClick = function()
698 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
699 return
700 end
701 local questionWindow = nil
702 local closeWindow = function()
703 questionWindow:destroy()
704 end
705 local removeConfig = function()
706 closeWindow()
707 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
708 return
709 end
710 context.storage.attacking.enabled = false
711 table.remove(context.storage.attacking.configs, context.storage.attacking.activeConfig)
712 context.storage.attacking.activeConfig = 0
713 refreshConfig()
714 end
715 questionWindow = context.displayGeneralBox(tr('Remove config'), tr('Do you want to remove current attacking config?'), {
716 { text=tr('Yes'), callback=removeConfig },
717 { text=tr('No'), callback=closeWindow },
718 anchor=AnchorHorizontalCenter}, removeConfig, closeWindow)
719 end
720
721
722 ui.mAdd.onClick = function()
723 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
724 return
725 end
726 Panels.MonsterEditor("", {}, function(name, config)
727 if name:len() > 0 then
728 monsters[name] = config
729 end
730 createNewConfig()
731 end, parent)
732 end
733 ui.mEdit.onClick = function()
734 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
735 return
736 end
737 local monsterWidget = ui.list:getFocusedChild()
738 if not monsterWidget or not monsters[monsterWidget:getText()] then
739 return
740 end
741 Panels.MonsterEditor(monsterWidget:getText(), monsters[monsterWidget:getText()], function(name, config)
742 monsters[monsterWidget:getText()] = nil
743 if name:len() > 0 then
744 monsters[name] = config
745 end
746 createNewConfig()
747 end, parent)
748 end
749 ui.mRemove.onClick = function()
750 if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
751 return
752 end
753 local monsterWidget = ui.list:getFocusedChild()
754 if not monsterWidget or not monsters[monsterWidget:getText()] then
755 return
756 end
757 monsters[monsterWidget:getText()] = nil
758 createNewConfig()
759 end
760
761 refreshConfig()
762
763 -- processing
764 local isConfigPassingConditions = function(monster, config)
765 if not config or type(config.priority) ~= 'number' or type(config.danger) ~= 'number' then
766 return false
767 end
768
769 if not config.attack then
770 return false
771 end
772
773 if monster:isPlayer() and (config.monstersOnly == true or config.monstersOnly == nil) then
774 return false
775 end
776
777 local pos = context.player:getPosition()
778 local mpos = monster:getPosition()
779 local hp = monster:getHealthPercent()
780
781 if config.minHealth > hp or config.maxHealth < hp then
782 return false
783 end
784
785 local maxDistance = 5
786 if type(config.maxDistance) == 'number' then
787 maxDistance = config.maxDistance
788 end
789 if config.chase and hp < 25 then
790 maxDistance = maxDistance + 2
791 end
792
793 local distance = math.max(math.abs(pos.x-mpos.x), math.abs(pos.y-mpos.y))
794 if distance > maxDistance then
795 return false
796 end
797
798 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 })
799 if not pathTo or #pathTo > maxDistance + 1 then
800 return false
801 end
802 return true
803 end
804
805 local getMonsterConfig = function(monster)
806 local name = monster:getName():lower()
807 local hasConfig = false
808 hasConfig = hasConfig or (monsters[name] ~= nil)
809 if isConfigPassingConditions(monster, monsters[name]) then
810 return monsters[name]
811 end
812 for i=1, 5 do
813 hasConfig = hasConfig or (monsters[name .. i] ~= nil)
814 if isConfigPassingConditions(monster, monsters[name .. i]) then
815 return monsters[name .. i]
816 end
817 end
818 if not hasConfig and isConfigPassingConditions(monster, monsters["*"]) then
819 return monsters["*"]
820 end
821 return nil
822 end
823
824 local calculatePriority = function(monster)
825 local priority = 0
826 local config = getMonsterConfig(monster)
827 if not config then
828 return -1
829 end
830
831 local pos = context.player:getPosition()
832 local mpos = monster:getPosition()
833 local hp = monster:getHealthPercent()
834 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 })
835 if not pathTo then
836 pathTo = context.findPath(context.player:getPosition(), {x=mpos.x, y=mpos.y, z=mpos.z}, 10, { ignoreNonPathable = true, precision=1, allowOnlyVisibleTiles = true })
837 if not pathTo then
838 return -1
839 end
840 end
841 local distance = #pathTo
842
843 if monster == g_game.getAttackingCreature() then
844 priority = priority + 10
845 end
846
847 if distance <= 4 then
848 priority = priority + 10
849 end
850 if distance <= 2 then
851 priority = priority + 10
852 end
853 if distance <= 1 then
854 priority = priority + 10
855 end
856
857 if hp <= 25 and config.chase then
858 priority = priority + 30
859 end
860
861 if hp <= 10 then
862 priority = priority + 10
863 end
864 if hp <= 25 then
865 priority = priority + 10
866 end
867 if hp <= 50 then
868 priority = priority + 10
869 end
870 if hp <= 75 then
871 priority = priority + 10
872 end
873
874 priority = priority + config.priority * 10
875 return priority
876 end
877
878 local calculateMonsterDanger = function(monster)
879 local danger = 0
880 local config = getMonsterConfig(monster)
881 if not config or type(config.danger) ~= 'number' then
882 return danger
883 end
884 danger = danger + config.danger
885 return danger
886 end
887
888 local lastAttack = context.now
889 local lootContainers = {}
890 local lootTries = 0
891 local openContainerRequest = 0
892 local waitForLooting = 0
893 local lastAttackSpell = 0
894 local lastAttackRune = 0
895
896 local goForLoot = function()
897 if #lootContainers == 0 or not context.storage.looting.enabled then
898 return false
899 end
900 if modules.game_walking.lastManualWalk + 500 > context.now then
901 return true
902 end
903
904 local pos = context.player:getPosition()
905 table.sort(lootContainers, function(pos1, pos2)
906 local dist1 = math.max(math.abs(pos.x-pos1.x), math.abs(pos.y-pos1.y))
907 local dist2 = math.max(math.abs(pos.x-pos2.x), math.abs(pos.y-pos2.y))
908 return dist1 < dist2
909 end)
910
911 local cpos = lootContainers[1]
912 if cpos.z ~= pos.z then
913 table.remove(lootContainers, 1)
914 return true
915 end
916
917 if lootTries >= 5 then
918 lootTries = 0
919 table.remove(lootContainers, 1)
920 return true
921 end
922 local dist = math.max(math.abs(pos.x-cpos.x), math.abs(pos.y-cpos.y))
923 if dist <= 5 then
924 local tile = g_map.getTile(cpos)
925 if not tile then
926 table.remove(lootContainers, 1)
927 return true
928 end
929
930 local topItem = tile:getTopUseThing()
931 if not topItem or not topItem:isContainer() then
932 table.remove(lootContainers, 1)
933 return true
934 end
935 topItem:setMarked('orange')
936
937 if dist <= 1 then
938 lootTries = lootTries + 1
939 openContainerRequest = context.now
940 g_game.open(topItem)
941 waitForLooting = math.max(waitForLooting, context.now + 500)
942 return true
943 end
944 end
945
946 if dist <= 25 then
947 if context.player:isWalking() then
948 return true
949 end
950
951 lootTries = lootTries + 1
952 if context.autoWalk(cpos, 20, { precision = 1}) then
953 return true
954 end
955
956 if context.autoWalk(cpos, 20, { ignoreNonPathable = true, precision = 1}) then
957 return true
958 end
959
960 if context.autoWalk(cpos, 20, { ignoreNonPathable = true, precision = 2}) then
961 return true
962 end
963
964 if context.autoWalk(cpos, 20, { ignoreNonPathable = true, ignoreCreatures = true, precision = 2}) then
965 return true
966 end
967 else
968 table.remove(lootContainers, 1)
969 return false
970 end
971 return true
972 end
973
974 context.onCreatureDisappear(function(creature)
975 if not creature:isMonster() then
976 return
977 end
978 local pos = context.player:getPosition()
979 local tpos = creature:getPosition()
980 if tpos.z ~= pos.z then
981 return
982 end
983
984 local config = getMonsterConfig(creature)
985 if not config or not config.loot then
986 return
987 end
988 local distance = math.max(math.abs(pos.x-tpos.x), math.abs(pos.y-tpos.y))
989 if distance > 6 then
990 return
991 end
992
993 local tile = g_map.getTile(tpos)
994 if not tile then
995 return
996 end
997
998 local topItem = tile:getTopUseThing()
999 if not topItem or not topItem:isContainer() then
1000 return
1001 end
1002
1003 topItem:setMarked('blue')
1004 table.insert(lootContainers, tpos)
1005 end)
1006
1007 context.onContainerOpen(function(container, prevContainer)
1008 lootTries = 0
1009 if not context.storage.attacking.enabled then
1010 return
1011 end
1012
1013 if openContainerRequest + 500 > context.now and #lootContainers > 0 then
1014 waitForLooting = math.max(waitForLooting, context.now + 1000 + container:getItemsCount() * 100)
1015 table.remove(lootContainers, 1)
1016 end
1017 if prevContainer then
1018 container.autoLooting = prevContainer.autoLooting
1019 else
1020 container.autoLooting = (openContainerRequest + 3000 > context.now)
1021 end
1022 end)
1023
1024 context.macro(200, function()
1025 if not context.storage.attacking.enabled then
1026 return
1027 end
1028
1029 local attacking = nil
1030 local following = nil
1031 local attackingCandidate = g_game.getAttackingCreature()
1032 local followingCandidate = g_game.getFollowingCreature()
1033 local spectators = context.getSpectators()
1034 local monsters = {}
1035 local danger = 0
1036
1037 for i, spec in ipairs(spectators) do
1038 if attackingCandidate and attackingCandidate:getId() == spec:getId() then
1039 attacking = spec
1040 end
1041 if followingCandidate and followingCandidate:getId() == spec:getId() then
1042 following = spec
1043 end
1044 if spec:isMonster() or (spec:isPlayer() and not spec:isLocalPlayer()) then
1045 danger = danger + calculateMonsterDanger(spec)
1046 spec.attackingPriority = calculatePriority(spec)
1047 table.insert(monsters, spec)
1048 end
1049 end
1050
1051 if following then
1052 return
1053 end
1054
1055 if waitForLooting > context.now then
1056 return
1057 end
1058
1059 if #monsters == 0 or context.isInProtectionZone() then
1060 goForLoot()
1061 return
1062 end
1063
1064 table.sort(monsters, function(a, b)
1065 return a.attackingPriority > b.attackingPriority
1066 end)
1067
1068 local target = monsters[1]
1069 if target.attackingPriority < 0 then
1070 return
1071 end
1072
1073 local pos = context.player:getPosition()
1074 local tpos = target:getPosition()
1075 local config = getMonsterConfig(target)
1076 local offsetX = pos.x - tpos.x
1077 local offsetY = pos.y - tpos.y
1078
1079 local justStartedAttack = false
1080 if target ~= attacking then
1081 g_game.attack(target)
1082 attacking = target
1083 lastAttack = context.now
1084 justStartedAttack = true
1085 end
1086
1087 -- proceed attack
1088 if not target:isPlayer() and lastAttack + 15000 < context.now then
1089 -- stop and attack again, just in case
1090 g_game.cancelAttack()
1091 g_game.attack(target)
1092 lastAttack = context.now
1093 return
1094 end
1095
1096 if not justStartedAttack and config.attackSpell and config.attackSpell:len() > 0 then
1097 if context.now > lastAttackSpell + 1000 and context.player:getHealthPercent() > 30 then
1098 if context.saySpell(config.attackSpell, 1500) then
1099 lastAttackRune = context.now
1100 end
1101 end
1102 end
1103
1104 if not justStartedAttack and config.attackItem and config.attackItem >= 100 then
1105 if context.now > lastAttackRune + 1000 and context.player:getHealthPercent() > 30 then
1106 if context.useRune(config.attackItem, target, 1500) then
1107 lastAttackRune = context.now
1108 end
1109 end
1110 end
1111
1112 if modules.game_walking.lastManualWalk + 500 > context.now then
1113 return
1114 end
1115
1116 if danger < 8 then
1117 -- low danger, go for loot first
1118 if goForLoot() then
1119 return
1120 end
1121 end
1122
1123 target.ignoreByWaypoints = config.dontWalk
1124 if config.dontWalk then
1125 if goForLoot() then
1126 return
1127 end
1128 return
1129 end
1130
1131 local distance = math.max(math.abs(offsetX), math.abs(offsetY))
1132 if config.keepDistance then
1133 local minDistance = config.distance
1134 if target:getHealthPercent() <= 25 and config.chase and danger < 10 then
1135 minDistance = 1
1136 end
1137 if (distance == minDistance or distance == minDistance + 1) then
1138 return
1139 else
1140 local bestDist = 10
1141 local bestPos = pos
1142 if not context.autoWalk(tpos, 10, { minMargin=minDistance, maxMargin=minDistance + 1, allowOnlyVisibleTiles = true}) then
1143 if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, minMargin=minDistance, maxMargin=minDistance + 1, allowOnlyVisibleTiles = true}) then
1144 if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, ignoreCreatures = true, minMargin=minDistance, maxMargin=minDistance + 2, allowOnlyVisibleTiles = true}) then
1145 return
1146 end
1147 end
1148 end
1149 if not target:isPlayer() then
1150 context.delay(300)
1151 end
1152 end
1153 return
1154 end
1155
1156 if config.avoidAttacks and distance <= 1 then
1157 if (offsetX == 0 and offsetY ~= 0) then
1158 if context.player:canWalk(Directions.East) then
1159 g_game.walk(Directions.East)
1160 elseif context.player:canWalk(Directions.West) then
1161 g_game.walk(Directions.West)
1162 end
1163 elseif (offsetX ~= 0 and offsetY == 0) then
1164 if context.player:canWalk(Directions.North) then
1165 g_game.walk(Directions.North)
1166 elseif context.player:canWalk(Directions.South) then
1167 g_game.walk(Directions.South)
1168 end
1169 end
1170 end
1171
1172 if distance > 1 then
1173 if not context.autoWalk(tpos, 10, { precision = 1, allowOnlyVisibleTiles = true}) then
1174 if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, precision = 1, allowOnlyVisibleTiles = true}) then
1175 if not context.autoWalk(tpos, 10, { ignoreNonPathable = true, precision = 2, allowOnlyVisibleTiles = true}) then
1176 return
1177 end
1178 end
1179 end
1180 if not target:isPlayer() then
1181 context.delay(300)
1182 end
1183 end
1184 end)
1185end
1186
1187