From 75ce42f5b91d97d76f78e08ff166886623e4cb19 Mon Sep 17 00:00:00 2001 From: Arto Simonyan Date: Sat, 17 Aug 2024 17:23:03 +0300 Subject: [PATCH] Revert "Rename council" (#427) --- cmd/koolo/main.go | 6 +- go.mod | 6 +- go.sum | 18 +- internal/config/runs.go | 4 +- internal/event/event.go | 138 +++++++ internal/event/events.go | 141 ------- internal/game/keyboard.go | 7 - internal/game/map_client/client.go | 2 +- internal/helper/windows.go | 3 +- internal/run/council.go | 2 +- internal/run/leveling_act2.go | 2 +- internal/run/run.go | 2 +- internal/server/http_server.go | 16 +- internal/server/template_parameters.go | 4 +- internal/v2/action/buff.go | 168 -------- internal/v2/action/clear_area.go | 34 -- internal/v2/action/clear_level.go | 104 ----- internal/v2/action/cube_recipes.go | 279 ------------- internal/v2/action/gambling.go | 127 ------ internal/v2/action/heal_at_npc.go | 32 -- internal/v2/action/horadric_cube.go | 138 ------- internal/v2/action/identify.go | 86 ---- internal/v2/action/interaction.go | 80 ---- internal/v2/action/item.go | 53 --- internal/v2/action/item_pickup.go | 183 --------- internal/v2/action/legacy_mode.go | 30 -- internal/v2/action/leveling_tools.go | 407 ------------------- internal/v2/action/move.go | 231 ----------- internal/v2/action/recover_corpse.go | 23 -- internal/v2/action/repair.go | 97 ----- internal/v2/action/revive_merc.go | 34 -- internal/v2/action/stash.go | 267 ------------ internal/v2/action/step/attack.go | 174 -------- internal/v2/action/step/close_all_menus.go | 32 -- internal/v2/action/step/interact_entrance.go | 71 ---- internal/v2/action/step/interact_npc.go | 71 ---- internal/v2/action/step/interact_object.go | 91 ----- internal/v2/action/step/move.go | 96 ----- internal/v2/action/step/open_portal.go | 40 -- internal/v2/action/step/pickup_item.go | 98 ----- internal/v2/action/step/set_skill.go | 17 - internal/v2/action/step/swap_weapon.go | 44 -- internal/v2/action/tools.go | 49 --- internal/v2/action/town.go | 73 ---- internal/v2/action/tp_actions.go | 64 --- internal/v2/action/vendor.go | 106 ----- internal/v2/action/waypoint.go | 117 ------ internal/v2/action/waypoint_discover.go | 31 -- internal/v2/bot/bot.go | 112 ----- internal/v2/bot/manager.go | 328 --------------- internal/v2/bot/single_supervisor.go | 126 ------ internal/v2/bot/stats.go | 145 ------- internal/v2/bot/supervisor.go | 132 ------ internal/v2/character/berserk_barb.go | 329 --------------- internal/v2/character/blizzard_sorceress.go | 257 ------------ internal/v2/character/character.go | 82 ---- internal/v2/character/foh.go | 260 ------------ internal/v2/character/hammerdin.go | 216 ---------- internal/v2/character/javazon.go | 256 ------------ internal/v2/character/lightning_sorceress.go | 306 -------------- internal/v2/character/mosaic.go | 266 ------------ internal/v2/character/trapsin.go | 222 ---------- internal/v2/character/wind_druid.go | 305 -------------- internal/v2/context/character.go | 39 -- internal/v2/context/context.go | 100 ----- internal/v2/health/belt_manager.go | 123 ------ internal/v2/health/health_manager.go | 99 ----- internal/v2/pather/line_of_sight.go | 55 --- internal/v2/pather/path.go | 49 --- internal/v2/pather/path_finder.go | 345 ---------------- internal/v2/pather/path_finding_tools.go | 100 ----- internal/v2/pather/rooms.go | 33 -- internal/v2/pather/tile.go | 72 ---- internal/v2/pather/world.go | 140 ------- internal/v2/run/ancient_tunnels.go | 48 --- internal/v2/run/andariel.go | 105 ----- internal/v2/run/arachnid_lair.go | 40 -- internal/v2/run/baal.go | 147 ------- internal/v2/run/countess.go | 68 ---- internal/v2/run/pindleskin.go | 61 --- internal/v2/run/run.go | 93 ----- internal/v2/run/travincal.go | 44 -- internal/v2/town/A1.go | 50 --- internal/v2/town/A2.go | 42 -- internal/v2/town/A3.go | 42 -- internal/v2/town/A4.go | 42 -- internal/v2/town/A5.go | 42 -- internal/v2/town/shop_manager.go | 189 --------- internal/v2/town/town.go | 33 -- internal/v2/ui/coordinates.go | 109 ----- internal/v2/ui/ui_manager.go | 46 --- internal/v2/utils/image.go | 17 - internal/v2/utils/randomness.go | 14 - internal/v2/utils/screen_cords.go | 19 - internal/v2/utils/sleep.go | 14 - internal/v2/utils/spiral.go | 14 - internal/v2/utils/windows.go | 21 - internal/v2/utils/winproc/gdi32.go | 13 - internal/v2/utils/winproc/kernel32.go | 13 - internal/v2/utils/winproc/user32.go | 10 - 100 files changed, 173 insertions(+), 9558 deletions(-) delete mode 100644 internal/event/events.go delete mode 100644 internal/v2/action/buff.go delete mode 100644 internal/v2/action/clear_area.go delete mode 100644 internal/v2/action/clear_level.go delete mode 100644 internal/v2/action/cube_recipes.go delete mode 100644 internal/v2/action/gambling.go delete mode 100644 internal/v2/action/heal_at_npc.go delete mode 100644 internal/v2/action/horadric_cube.go delete mode 100644 internal/v2/action/identify.go delete mode 100644 internal/v2/action/interaction.go delete mode 100644 internal/v2/action/item.go delete mode 100644 internal/v2/action/item_pickup.go delete mode 100644 internal/v2/action/legacy_mode.go delete mode 100644 internal/v2/action/leveling_tools.go delete mode 100644 internal/v2/action/move.go delete mode 100644 internal/v2/action/recover_corpse.go delete mode 100644 internal/v2/action/repair.go delete mode 100644 internal/v2/action/revive_merc.go delete mode 100644 internal/v2/action/stash.go delete mode 100644 internal/v2/action/step/attack.go delete mode 100644 internal/v2/action/step/close_all_menus.go delete mode 100644 internal/v2/action/step/interact_entrance.go delete mode 100644 internal/v2/action/step/interact_npc.go delete mode 100644 internal/v2/action/step/interact_object.go delete mode 100644 internal/v2/action/step/move.go delete mode 100644 internal/v2/action/step/open_portal.go delete mode 100644 internal/v2/action/step/pickup_item.go delete mode 100644 internal/v2/action/step/set_skill.go delete mode 100644 internal/v2/action/step/swap_weapon.go delete mode 100644 internal/v2/action/tools.go delete mode 100644 internal/v2/action/town.go delete mode 100644 internal/v2/action/tp_actions.go delete mode 100644 internal/v2/action/vendor.go delete mode 100644 internal/v2/action/waypoint.go delete mode 100644 internal/v2/action/waypoint_discover.go delete mode 100644 internal/v2/bot/bot.go delete mode 100644 internal/v2/bot/manager.go delete mode 100644 internal/v2/bot/single_supervisor.go delete mode 100644 internal/v2/bot/stats.go delete mode 100644 internal/v2/bot/supervisor.go delete mode 100644 internal/v2/character/berserk_barb.go delete mode 100644 internal/v2/character/blizzard_sorceress.go delete mode 100644 internal/v2/character/character.go delete mode 100644 internal/v2/character/foh.go delete mode 100644 internal/v2/character/hammerdin.go delete mode 100644 internal/v2/character/javazon.go delete mode 100644 internal/v2/character/lightning_sorceress.go delete mode 100644 internal/v2/character/mosaic.go delete mode 100644 internal/v2/character/trapsin.go delete mode 100644 internal/v2/character/wind_druid.go delete mode 100644 internal/v2/context/character.go delete mode 100644 internal/v2/context/context.go delete mode 100644 internal/v2/health/belt_manager.go delete mode 100644 internal/v2/health/health_manager.go delete mode 100644 internal/v2/pather/line_of_sight.go delete mode 100644 internal/v2/pather/path.go delete mode 100644 internal/v2/pather/path_finder.go delete mode 100644 internal/v2/pather/path_finding_tools.go delete mode 100644 internal/v2/pather/rooms.go delete mode 100644 internal/v2/pather/tile.go delete mode 100644 internal/v2/pather/world.go delete mode 100644 internal/v2/run/ancient_tunnels.go delete mode 100644 internal/v2/run/andariel.go delete mode 100644 internal/v2/run/arachnid_lair.go delete mode 100644 internal/v2/run/baal.go delete mode 100644 internal/v2/run/countess.go delete mode 100644 internal/v2/run/pindleskin.go delete mode 100644 internal/v2/run/run.go delete mode 100644 internal/v2/run/travincal.go delete mode 100644 internal/v2/town/A1.go delete mode 100644 internal/v2/town/A2.go delete mode 100644 internal/v2/town/A3.go delete mode 100644 internal/v2/town/A4.go delete mode 100644 internal/v2/town/A5.go delete mode 100644 internal/v2/town/shop_manager.go delete mode 100644 internal/v2/town/town.go delete mode 100644 internal/v2/ui/coordinates.go delete mode 100644 internal/v2/ui/ui_manager.go delete mode 100644 internal/v2/utils/image.go delete mode 100644 internal/v2/utils/randomness.go delete mode 100644 internal/v2/utils/screen_cords.go delete mode 100644 internal/v2/utils/sleep.go delete mode 100644 internal/v2/utils/spiral.go delete mode 100644 internal/v2/utils/windows.go delete mode 100644 internal/v2/utils/winproc/gdi32.go delete mode 100644 internal/v2/utils/winproc/kernel32.go delete mode 100644 internal/v2/utils/winproc/user32.go diff --git a/cmd/koolo/main.go b/cmd/koolo/main.go index db911eb7..31d70ad8 100644 --- a/cmd/koolo/main.go +++ b/cmd/koolo/main.go @@ -9,6 +9,7 @@ import ( "runtime/debug" sloggger "github.com/hectorgimenez/koolo/cmd/koolo/log" + koolo "github.com/hectorgimenez/koolo/internal" "github.com/hectorgimenez/koolo/internal/config" "github.com/hectorgimenez/koolo/internal/event" "github.com/hectorgimenez/koolo/internal/helper" @@ -16,7 +17,6 @@ import ( "github.com/hectorgimenez/koolo/internal/remote/discord" "github.com/hectorgimenez/koolo/internal/remote/telegram" "github.com/hectorgimenez/koolo/internal/server" - "github.com/hectorgimenez/koolo/internal/v2/bot" "github.com/inkeliz/gowebview" "golang.org/x/sync/errgroup" ) @@ -52,8 +52,8 @@ func main() { winproc.SetProcessDpiAware.Call() // Set DPI awareness to be able to read the correct scale and show the window correctly eventListener := event.NewListener(logger) - //manager := koolo.NewSupervisorManager(logger, eventListener) - manager := bot.NewSupervisorManager(logger, eventListener) + manager := koolo.NewSupervisorManager(logger, eventListener) + srv, err := server.New(logger, manager) if err != nil { log.Fatalf("Error starting local server: %s", err.Error()) diff --git a/go.mod b/go.mod index 7ac452ec..ca6b907a 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,13 @@ require ( github.com/bwmarrin/discordgo v0.28.1 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/gorilla/websocket v1.5.3 - github.com/hectorgimenez/d2go v0.0.0-20240816162330-5a01fd3214eb + github.com/hectorgimenez/d2go v0.0.0-20240810110236-8e6728494046 github.com/inkeliz/gowebview v1.0.1 github.com/inkeliz/w32 v1.0.2 github.com/lxn/win v0.0.0-20210218163916-a377121e959e github.com/otiai10/copy v1.14.0 - golang.org/x/sync v0.8.0 - golang.org/x/sys v0.24.0 + golang.org/x/sync v0.7.0 + golang.org/x/sys v0.22.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 460eb9fa..4ca624a5 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,14 @@ github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hectorgimenez/d2go v0.0.0-20240816162330-5a01fd3214eb h1:RBoRZULEPpT8+i7zw1U+A5WFARTXJajBXUyg8L7CgqU= -github.com/hectorgimenez/d2go v0.0.0-20240816162330-5a01fd3214eb/go.mod h1:EOVayMaK8D13wsZiZ6n8AK3+Qflm1wHZsCqnzlVIci0= +github.com/hectorgimenez/d2go v0.0.0-20240705125326-9d801c51dfb4 h1:RZshwZXAq6RIlFqf5UKFRgk8yFO5Tznfmye68SEQ/3o= +github.com/hectorgimenez/d2go v0.0.0-20240705125326-9d801c51dfb4/go.mod h1:haet+1Z13euImIuyXR6IAFmJ2P4eaAqe5m3gEPjT1e8= +github.com/hectorgimenez/d2go v0.0.0-20240801152822-67692abea333 h1:XmzQpvZ/Md0w/a/jXU+JbHrYCWEsGuwXpvSYdx4I5wA= +github.com/hectorgimenez/d2go v0.0.0-20240801152822-67692abea333/go.mod h1:haet+1Z13euImIuyXR6IAFmJ2P4eaAqe5m3gEPjT1e8= +github.com/hectorgimenez/d2go v0.0.0-20240805144806-938bfeb753b7 h1:xoY0NXV7R5pOXr4vuw0PZr1UI1AB7fzoq7qKzNdbnYQ= +github.com/hectorgimenez/d2go v0.0.0-20240805144806-938bfeb753b7/go.mod h1:haet+1Z13euImIuyXR6IAFmJ2P4eaAqe5m3gEPjT1e8= +github.com/hectorgimenez/d2go v0.0.0-20240810110236-8e6728494046 h1:va3llh8JReMziJ0x2WOaoq9kQ1b9xesST9ti0dAgce0= +github.com/hectorgimenez/d2go v0.0.0-20240810110236-8e6728494046/go.mod h1:haet+1Z13euImIuyXR6IAFmJ2P4eaAqe5m3gEPjT1e8= github.com/inkeliz/gowebview v1.0.1 h1:4gpLE2qt4kV3DB+xHkHKUeLLiGPN5Xw3or9A3hVqYyA= github.com/inkeliz/gowebview v1.0.1/go.mod h1:4SNjXp/fogE11MwvJD67kMBmSObY2BBqinEgH8+8eM8= github.com/inkeliz/w32 v1.0.2 h1:Es8Bmw9ApOY0PVRpGs7wsqIKdK5C3xBkP5TOATfVmtU= @@ -42,16 +48,16 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/config/runs.go b/internal/config/runs.go index f0c86703..a6a27eeb 100644 --- a/internal/config/runs.go +++ b/internal/config/runs.go @@ -10,7 +10,7 @@ const ( SummonerRun Run = "summoner" DurielRun Run = "duriel" MephistoRun Run = "mephisto" - TravincalRun Run = "travincal" + CouncilRun Run = "council" EldritchRun Run = "eldritch" PindleskinRun Run = "pindleskin" NihlathakRun Run = "nihlathak" @@ -40,7 +40,7 @@ var AvailableRuns = map[Run]interface{}{ SummonerRun: nil, DurielRun: nil, MephistoRun: nil, - TravincalRun: nil, + CouncilRun: nil, EldritchRun: nil, PindleskinRun: nil, NihlathakRun: nil, diff --git a/internal/event/event.go b/internal/event/event.go index af785366..c0d96c93 100644 --- a/internal/event/event.go +++ b/internal/event/event.go @@ -3,6 +3,20 @@ package event import ( "image" "time" + + "github.com/hectorgimenez/d2go/pkg/data" +) + +const ( + FinishedOK FinishReason = "ok" + FinishedDied FinishReason = "death" + FinishedChicken FinishReason = "chicken" + FinishedMercChicken FinishReason = "merc chicken" + FinishedError FinishReason = "error" + + InteractionTypeEntrance InteractionType = "entrance" + InteractionTypeNPC InteractionType = "npc" + InteractionTypeObject InteractionType = "object" ) type FinishReason string @@ -54,3 +68,127 @@ func Text(supervisor string, message string) BaseEvent { supervisor: supervisor, } } + +type UsedPotionEvent struct { + BaseEvent + PotionType data.PotionType + OnMerc bool +} + +func UsedPotion(be BaseEvent, pt data.PotionType, onMerc bool) UsedPotionEvent { + return UsedPotionEvent{ + BaseEvent: be, + PotionType: pt, + OnMerc: onMerc, + } +} + +type GameCreatedEvent struct { + BaseEvent + Name string + Password string +} + +func GameCreated(be BaseEvent, name string, password string) GameCreatedEvent { + return GameCreatedEvent{ + BaseEvent: be, + Name: name, + Password: password, + } +} + +type GameFinishedEvent struct { + BaseEvent + Reason FinishReason +} + +func GameFinished(be BaseEvent, reason FinishReason) GameFinishedEvent { + return GameFinishedEvent{ + BaseEvent: be, + Reason: reason, + } +} + +type RunFinishedEvent struct { + BaseEvent + RunName string + Reason FinishReason +} + +func RunFinished(be BaseEvent, runName string, reason FinishReason) RunFinishedEvent { + return RunFinishedEvent{ + BaseEvent: be, + RunName: runName, + Reason: reason, + } +} + +type ItemStashedEvent struct { + BaseEvent + Item data.Drop +} + +func ItemStashed(be BaseEvent, drop data.Drop) ItemStashedEvent { + return ItemStashedEvent{ + BaseEvent: be, + Item: drop, + } +} + +type RunStartedEvent struct { + BaseEvent + RunName string +} + +func RunStarted(be BaseEvent, runName string) RunStartedEvent { + return RunStartedEvent{ + BaseEvent: be, + RunName: runName, + } +} + +type CompanionLeaderAttackEvent struct { + BaseEvent + TargetUnitID data.UnitID +} + +func CompanionLeaderAttack(be BaseEvent, targetUnitID data.UnitID) CompanionLeaderAttackEvent { + return CompanionLeaderAttackEvent{ + BaseEvent: be, + TargetUnitID: targetUnitID, + } +} + +type CompanionRequestedTPEvent struct { + BaseEvent +} + +func CompanionRequestedTP(be BaseEvent) CompanionRequestedTPEvent { + return CompanionRequestedTPEvent{BaseEvent: be} +} + +type InteractedToEvent struct { + BaseEvent + ID int + InteractionType InteractionType +} + +func InteractedTo(be BaseEvent, id int, it InteractionType) InteractedToEvent { + return InteractedToEvent{ + BaseEvent: be, + ID: id, + InteractionType: it, + } +} + +type GamePausedEvent struct { + BaseEvent + Paused bool +} + +func GamePaused(be BaseEvent, paused bool) GamePausedEvent { + return GamePausedEvent{ + BaseEvent: be, + Paused: paused, + } +} diff --git a/internal/event/events.go b/internal/event/events.go deleted file mode 100644 index c6177b0e..00000000 --- a/internal/event/events.go +++ /dev/null @@ -1,141 +0,0 @@ -package event - -import ( - "github.com/hectorgimenez/d2go/pkg/data" -) - -const ( - FinishedOK FinishReason = "ok" - FinishedDied FinishReason = "death" - FinishedChicken FinishReason = "chicken" - FinishedMercChicken FinishReason = "merc chicken" - FinishedError FinishReason = "error" - - InteractionTypeEntrance InteractionType = "entrance" - InteractionTypeNPC InteractionType = "npc" - InteractionTypeObject InteractionType = "object" -) - -type UsedPotionEvent struct { - BaseEvent - PotionType data.PotionType - OnMerc bool -} - -func UsedPotion(be BaseEvent, pt data.PotionType, onMerc bool) UsedPotionEvent { - return UsedPotionEvent{ - BaseEvent: be, - PotionType: pt, - OnMerc: onMerc, - } -} - -type GameCreatedEvent struct { - BaseEvent - Name string - Password string -} - -func GameCreated(be BaseEvent, name string, password string) GameCreatedEvent { - return GameCreatedEvent{ - BaseEvent: be, - Name: name, - Password: password, - } -} - -type GameFinishedEvent struct { - BaseEvent - Reason FinishReason -} - -func GameFinished(be BaseEvent, reason FinishReason) GameFinishedEvent { - return GameFinishedEvent{ - BaseEvent: be, - Reason: reason, - } -} - -type RunFinishedEvent struct { - BaseEvent - RunName string - Reason FinishReason -} - -func RunFinished(be BaseEvent, runName string, reason FinishReason) RunFinishedEvent { - return RunFinishedEvent{ - BaseEvent: be, - RunName: runName, - Reason: reason, - } -} - -type ItemStashedEvent struct { - BaseEvent - Item data.Drop -} - -func ItemStashed(be BaseEvent, drop data.Drop) ItemStashedEvent { - return ItemStashedEvent{ - BaseEvent: be, - Item: drop, - } -} - -type RunStartedEvent struct { - BaseEvent - RunName string -} - -func RunStarted(be BaseEvent, runName string) RunStartedEvent { - return RunStartedEvent{ - BaseEvent: be, - RunName: runName, - } -} - -type CompanionLeaderAttackEvent struct { - BaseEvent - TargetUnitID data.UnitID -} - -func CompanionLeaderAttack(be BaseEvent, targetUnitID data.UnitID) CompanionLeaderAttackEvent { - return CompanionLeaderAttackEvent{ - BaseEvent: be, - TargetUnitID: targetUnitID, - } -} - -type CompanionRequestedTPEvent struct { - BaseEvent -} - -func CompanionRequestedTP(be BaseEvent) CompanionRequestedTPEvent { - return CompanionRequestedTPEvent{BaseEvent: be} -} - -type InteractedToEvent struct { - BaseEvent - ID int - InteractionType InteractionType -} - -func InteractedTo(be BaseEvent, id int, it InteractionType) InteractedToEvent { - return InteractedToEvent{ - BaseEvent: be, - ID: id, - InteractionType: it, - } -} - -type GamePausedEvent struct { - BaseEvent - Paused bool -} - -func GamePaused(be BaseEvent, paused bool) GamePausedEvent { - return GamePausedEvent{ - BaseEvent: be, - Paused: paused, - } -} diff --git a/internal/game/keyboard.go b/internal/game/keyboard.go index 2e18325e..7bceeb17 100644 --- a/internal/game/keyboard.go +++ b/internal/game/keyboard.go @@ -23,13 +23,6 @@ func (hid *HID) PressKey(key byte) { win.PostMessage(hid.gr.HWND, win.WM_KEYUP, uintptr(key), hid.calculatelParam(key, false)) } -func (hid *HID) KeySequence(keysToPress ...byte) { - for _, key := range keysToPress { - hid.PressKey(key) - time.Sleep(200 * time.Millisecond) - } -} - // PressKeyWithModifier works the same as PressKey but with a modifier key (shift, ctrl, alt) func (hid *HID) PressKeyWithModifier(key byte, modifier ModifierKey) { hid.gi.OverrideGetKeyState(byte(modifier)) diff --git a/internal/game/map_client/client.go b/internal/game/map_client/client.go index 257c8470..d21a2ce1 100644 --- a/internal/game/map_client/client.go +++ b/internal/game/map_client/client.go @@ -20,7 +20,7 @@ func GetMapData(seed string, difficulty difficulty.Difficulty) (MapData, error) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} stdout, err := cmd.Output() if err != nil { - return nil, fmt.Errorf("error fetching Map data from Diablo II: LoD 1.13c game: %w", err) + return nil, fmt.Errorf("error fetching Map Data from Diablo II: LoD 1.13c game: %w", err) } stdoutLines := strings.Split(string(stdout), "\r\n") diff --git a/internal/helper/windows.go b/internal/helper/windows.go index 90261941..e9b3697c 100644 --- a/internal/helper/windows.go +++ b/internal/helper/windows.go @@ -1,10 +1,9 @@ package helper import ( + "golang.org/x/sys/windows" "os" "syscall" - - "golang.org/x/sys/windows" ) func HasAdminPermission() bool { diff --git a/internal/run/council.go b/internal/run/council.go index 0e610d47..f08437c7 100644 --- a/internal/run/council.go +++ b/internal/run/council.go @@ -13,7 +13,7 @@ type Council struct { } func (s Council) Name() string { - return string(config.TravincalRun) + return string(config.CouncilRun) } func (s Council) BuildActions() []action.Action { diff --git a/internal/run/leveling_act2.go b/internal/run/leveling_act2.go index eb987abc..adc98292 100644 --- a/internal/run/leveling_act2.go +++ b/internal/run/leveling_act2.go @@ -71,7 +71,7 @@ func (a Leveling) act2() action.Action { } //func (a Leveling) radament() action.Action { -// return action.NewChain(func(d game.data) (actions []action.Action) { +// return action.NewChain(func(d game.Data) (actions []action.Action) { // actions = append(actions, // a.builder.WayPoint(area.SewersLevel2Act2), // a.builder.MoveToArea(area.SewersLevel3Act2), diff --git a/internal/run/run.go b/internal/run/run.go index 1dadec61..b007fc95 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -81,7 +81,7 @@ func (f *Factory) BuildRuns() (runs []Run) { runs = append(runs, Duriel{baseRun}) case config.MephistoRun: runs = append(runs, Mephisto{baseRun}) - case config.TravincalRun: + case config.CouncilRun: runs = append(runs, Council{baseRun}) case config.DiabloRun: runs = append(runs, Diablo{ diff --git a/internal/server/http_server.go b/internal/server/http_server.go index b7b5780a..c0da7e53 100644 --- a/internal/server/http_server.go +++ b/internal/server/http_server.go @@ -20,15 +20,15 @@ import ( "github.com/hectorgimenez/d2go/pkg/data/area" "github.com/hectorgimenez/d2go/pkg/data/difficulty" "github.com/hectorgimenez/d2go/pkg/data/stat" + koolo "github.com/hectorgimenez/koolo/internal" "github.com/hectorgimenez/koolo/internal/config" "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/bot" ) type HttpServer struct { logger *slog.Logger server *http.Server - manager *bot.SupervisorManager + manager *koolo.SupervisorManager templates *template.Template wsServer *WebSocketServer } @@ -161,7 +161,7 @@ func (s *HttpServer) BroadcastStatus() { } } -func New(logger *slog.Logger, manager *bot.SupervisorManager) (*HttpServer, error) { +func New(logger *slog.Logger, manager *koolo.SupervisorManager) (*HttpServer, error) { var templates *template.Template helperFuncs := template.FuncMap{ "isInSlice": func(slice []stat.Resist, value string) bool { @@ -237,7 +237,7 @@ func (s *HttpServer) initialData(w http.ResponseWriter, r *http.Request) { } func (s *HttpServer) getStatusData() IndexData { - status := make(map[string]bot.Stats) + status := make(map[string]koolo.Stats) drops := make(map[string]int) for _, supervisorName := range s.manager.AvailableSupervisors() { @@ -347,7 +347,7 @@ func (s *HttpServer) startSupervisor(w http.ResponseWriter, r *http.Request) { continue } - if s.manager.GetSupervisorStats(sup).SupervisorStatus == bot.Starting { + if s.manager.GetSupervisorStats(sup).SupervisorStatus == koolo.Starting { // Prevent launching if we're using token auth & another client is starting (no matter what auth method) if supCfg.AuthMethod == "TokenAuth" { @@ -379,12 +379,12 @@ func (s *HttpServer) togglePause(w http.ResponseWriter, r *http.Request) { } func (s *HttpServer) index(w http.ResponseWriter) { - status := make(map[string]bot.Stats) + status := make(map[string]koolo.Stats) drops := make(map[string]int) for _, supervisorName := range s.manager.AvailableSupervisors() { - status[supervisorName] = bot.Stats{ - SupervisorStatus: bot.NotStarted, + status[supervisorName] = koolo.Stats{ + SupervisorStatus: koolo.NotStarted, } status[supervisorName] = s.manager.Status(supervisorName) diff --git a/internal/server/template_parameters.go b/internal/server/template_parameters.go index 87ac5932..acdded25 100644 --- a/internal/server/template_parameters.go +++ b/internal/server/template_parameters.go @@ -2,14 +2,14 @@ package server import ( "github.com/hectorgimenez/d2go/pkg/data" + koolo "github.com/hectorgimenez/koolo/internal" "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/bot" ) type IndexData struct { ErrorMessage string Version string - Status map[string]bot.Stats + Status map[string]koolo.Stats DropCount map[string]int } diff --git a/internal/v2/action/buff.go b/internal/v2/action/buff.go deleted file mode 100644 index 528397a4..00000000 --- a/internal/v2/action/buff.go +++ /dev/null @@ -1,168 +0,0 @@ -package action - -import ( - "log/slog" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/pather" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" - - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/data/state" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/action/step" -) - -func BuffIfRequired(d game.Data) { - if !IsRebuffRequired() { - return - } - - // Don't buff if we have 2 or more monsters close to the character. - // Don't merge with the previous if, because we want to avoid this expensive check if we don't need to buff - closeMonsters := 0 - for _, m := range d.Monsters { - if pather.DistanceFromMe(d, m.Position) < 15 { - closeMonsters++ - } - } - if closeMonsters >= 2 { - return - } - - Buff() -} - -func Buff() { - ctx := context.Get() - ctx.ContextDebug.LastAction = "Buff" - - if ctx.Data.PlayerUnit.Area.IsTown() || time.Since(ctx.LastBuffAt) < time.Second*30 { - return - } - - preKeys := make([]data.KeyBinding, 0) - for _, buff := range ctx.Char.PreCTABuffSkills() { - kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(buff) - if !found { - ctx.Logger.Info("Key binding not found, skipping buff", slog.String("skill", buff.Desc().Name)) - } else { - preKeys = append(preKeys, kb) - } - } - - if len(preKeys) > 0 { - ctx.Logger.Debug("PRE CTA Buffing...") - for _, kb := range preKeys { - helper.Sleep(100) - ctx.HID.PressKeyBinding(kb) - helper.Sleep(180) - ctx.HID.Click(game.RightButton, 640, 340) - helper.Sleep(100) - } - } - - buffCTA() - - postKeys := make([]data.KeyBinding, 0) - for _, buff := range ctx.Char.BuffSkills() { - kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(buff) - if !found { - ctx.Logger.Info("Key binding not found, skipping buff", slog.String("skill", buff.Desc().Name)) - } else { - postKeys = append(postKeys, kb) - } - } - - if len(postKeys) > 0 { - ctx.Logger.Debug("Post CTA Buffing...") - - for _, kb := range postKeys { - helper.Sleep(100) - ctx.HID.PressKeyBinding(kb) - helper.Sleep(180) - ctx.HID.Click(game.RightButton, 640, 340) - helper.Sleep(100) - } - ctx.LastBuffAt = time.Now() - } -} - -func IsRebuffRequired() bool { - ctx := context.Get() - ctx.ContextDebug.LastAction = "IsRebuffRequired" - - // Don't buff if we are in town, or we did it recently (it prevents double buffing because of network lag) - if ctx.Data.PlayerUnit.Area.IsTown() || time.Since(ctx.LastBuffAt) < time.Second*30 { - return false - } - - if ctaFound(*ctx.Data) && (!ctx.Data.PlayerUnit.States.HasState(state.Battleorders) || !ctx.Data.PlayerUnit.States.HasState(state.Battlecommand)) { - return true - } - - // TODO: Find a better way to convert skill to state - buffs := ctx.Char.BuffSkills() - for _, buff := range buffs { - if _, found := ctx.Data.KeyBindings.KeyBindingForSkill(buff); found { - if buff == skill.HolyShield && !ctx.Data.PlayerUnit.States.HasState(state.Holyshield) { - return true - } - if buff == skill.FrozenArmor && (!ctx.Data.PlayerUnit.States.HasState(state.Frozenarmor) && !ctx.Data.PlayerUnit.States.HasState(state.Shiverarmor) && !ctx.Data.PlayerUnit.States.HasState(state.Chillingarmor)) { - return true - } - if buff == skill.EnergyShield && !ctx.Data.PlayerUnit.States.HasState(state.Energyshield) { - return true - } - if buff == skill.CycloneArmor && !ctx.Data.PlayerUnit.States.HasState(state.Cyclonearmor) { - return true - } - } - } - - return false -} - -func buffCTA() { - ctx := context.Get() - ctx.ContextDebug.LastAction = "buffCTA" - - if ctaFound(*ctx.Data) { - ctx.Logger.Debug("CTA found: swapping weapon and casting Battle Command / Battle Orders") - - // Swap weapon only in case we don't have the CTA, sometimes CTA is already equipped (for example chicken previous game during buff stage) - if _, found := ctx.Data.PlayerUnit.Skills[skill.BattleCommand]; !found { - step.SwapToCTA() - } - - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.MustKBForSkill(skill.BattleCommand)) - utils.Sleep(180) - ctx.HID.Click(game.RightButton, 300, 300) - utils.Sleep(100) - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.MustKBForSkill(skill.BattleOrders)) - utils.Sleep(180) - ctx.HID.Click(game.RightButton, 300, 300) - utils.Sleep(100) - - utils.Sleep(500) - step.SwapToMainWeapon() - } -} - -func ctaFound(d game.Data) bool { - for _, itm := range d.Inventory.ByLocation(item.LocationEquipped) { - _, boFound := itm.FindStat(stat.NonClassSkill, int(skill.BattleOrders)) - _, bcFound := itm.FindStat(stat.NonClassSkill, int(skill.BattleCommand)) - - if boFound && bcFound { - return true - } - } - - return false -} diff --git a/internal/v2/action/clear_area.go b/internal/v2/action/clear_area.go deleted file mode 100644 index 4f953a4a..00000000 --- a/internal/v2/action/clear_area.go +++ /dev/null @@ -1,34 +0,0 @@ -package action - -import ( - "log/slog" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/pather" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func ClearAreaAroundPlayer(distance int, filter data.MonsterFilter) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "ClearAreaAroundPlayer" - - originalPosition := data.Position{} - return ctx.Char.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - if originalPosition.X == 0 && originalPosition.Y == 0 { - originalPosition = d.PlayerUnit.Position - } - - for _, m := range d.Monsters.Enemies(filter) { - monsterDist := pather.DistanceFromPoint(originalPosition, m.Position) - shouldEngage := isMonsterSealElite(m) || pather.IsWalkable(m.Position, d.AreaOrigin, d.CollisionGrid) - - if monsterDist <= distance && shouldEngage { - ctx.Logger.Debug("Clearing area...", slog.Int("monsterID", int(m.Name))) - return m.UnitID, true - } - } - - return 0, false - }, nil) -} diff --git a/internal/v2/action/clear_level.go b/internal/v2/action/clear_level.go deleted file mode 100644 index 2c6031b6..00000000 --- a/internal/v2/action/clear_level.go +++ /dev/null @@ -1,104 +0,0 @@ -package action - -import ( - "errors" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "ClearLevel" - - for _, r := range ctx.PathFinder.OptimizeRoomsTraverseOrder() { - err := clearRoom(r, filter) - if err != nil { - ctx.Logger.Warn("Failed to clear room: %v", err) - } - - if !openChests { - continue - } - - for _, o := range ctx.Data.Objects { - if o.IsChest() && o.Selectable && r.IsInside(o.Position) { - err = MoveToCoords(o.Position) - if err != nil { - ctx.Logger.Warn("Failed moving to chest: %v", err) - continue - } - err = InteractObject(o, func() bool { - chest, _ := ctx.Data.Objects.FindByID(o.ID) - return !chest.Selectable - }) - if err != nil { - ctx.Logger.Warn("Failed interacting with chest: %v", err) - } - } - } - } - - return nil -} - -func clearRoom(room data.Room, filter data.MonsterFilter) error { - ctx := context.Get() - - path, _, found := ctx.PathFinder.GetClosestWalkablePath(room.GetCenter()) - if !found { - return errors.New("failed to find a path to the room center") - } - - err := MoveToCoords(path.To()) - if err != nil { - ctx.Logger.Warn("Failed moving to room center: %v", err) - } - - for monsters := getMonstersInRoom(room, filter); len(monsters) > 0; { - // Check if there are monsters that can summon new monsters, and kill them first - targetMonster := monsters[0] - for _, m := range monsters { - if m.IsMonsterRaiser() { - targetMonster = m - } - } - - path, _, mPathFound := ctx.PathFinder.GetPath(targetMonster.Position) - if mPathFound { - if !ctx.Data.CanTeleport() { - for _, o := range ctx.Data.Objects { - if o.IsDoor() && o.Selectable && path.Intersects(*ctx.Data, o.Position, 4) { - ctx.Logger.Debug("Door is blocking the path to the monster, moving closer") - MoveToCoords(targetMonster.Position) - } - } - } - - ctx.Char.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindByID(targetMonster.UnitID) - if found && m.Stats[stat.Life] > 0 { - return targetMonster.UnitID, true - } - return 0, false - }, nil) - } - } - - return nil -} - -func getMonstersInRoom(room data.Room, filter data.MonsterFilter) []data.Monster { - ctx := context.Get() - - monstersInRoom := make([]data.Monster, 0) - for _, m := range ctx.Data.Monsters.Enemies(filter) { - if room.IsInside(m.Position) || ctx.PathFinder.DistanceFromMe(m.Position) < 30 { - monstersInRoom = append(monstersInRoom, m) - } - } - - return monstersInRoom -} diff --git a/internal/v2/action/cube_recipes.go b/internal/v2/action/cube_recipes.go deleted file mode 100644 index 46d1c7fe..00000000 --- a/internal/v2/action/cube_recipes.go +++ /dev/null @@ -1,279 +0,0 @@ -package action - -import ( - "slices" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -type CubeRecipe struct { - Name string - Items []string -} - -var ( - recipes = []CubeRecipe{ - - // Perfects - { - Name: "Perfect Amethyst", - Items: []string{"FlawlessAmethyst", "FlawlessAmethyst", "FlawlessAmethyst"}, - }, - { - Name: "Perfect Diamond", - Items: []string{"FlawlessDiamond", "FlawlessDiamond", "FlawlessDiamond"}, - }, - { - Name: "Perfect Emerald", - Items: []string{"FlawlessEmerald", "FlawlessEmerald", "FlawlessEmerald"}, - }, - { - Name: "Perfect Ruby", - Items: []string{"FlawlessRuby", "FlawlessRuby", "FlawlessRuby"}, - }, - { - Name: "Perfect Sapphire", - Items: []string{"FlawlessSapphire", "FlawlessSapphire", "FlawlessSapphire"}, - }, - { - Name: "Perfect Topaz", - Items: []string{"FlawlessTopaz", "FlawlessTopaz", "FlawlessTopaz"}, - }, - { - Name: "Perfect Skull", - Items: []string{"FlawlessSkull", "FlawlessSkull", "FlawlessSkull"}, - }, - - // Token - { - Name: "Token of Absolution", - Items: []string{"TwistedEssenceOfSuffering", "ChargedEssenceOfHatred", "BurningEssenceOfTerror", "FesteringEssenceOfDestruction"}, - }, - - // Runes - { - Name: "Upgrade El", - Items: []string{"ElRune", "ElRune", "ElRune"}, - }, - { - Name: "Upgrade Eld", - Items: []string{"EldRune", "EldRune", "EldRune"}, - }, - { - Name: "Upgrade Tir", - Items: []string{"TirRune", "TirRune", "TirRune"}, - }, - { - Name: "Upgrade Nef", - Items: []string{"NefRune", "NefRune", "NefRune"}, - }, - { - Name: "Upgrade Eth", - Items: []string{"EthRune", "EthRune", "EthRune"}, - }, - { - Name: "Upgrade Ith", - Items: []string{"IthRune", "IthRune", "IthRune"}, - }, - { - Name: "Upgrade Tal", - Items: []string{"TalRune", "TalRune", "TalRune"}, - }, - { - Name: "Upgrade Ral", - Items: []string{"RalRune", "RalRune", "RalRune"}, - }, - { - Name: "Upgrade Ort", - Items: []string{"OrtRune", "OrtRune", "OrtRune"}, - }, - { - Name: "Upgrade Thul", - Items: []string{"ThulRune", "ThulRune", "ThulRune", "ChippedTopaz"}, - }, - { - Name: "Upgrade Amn", - Items: []string{"AmnRune", "AmnRune", "AmnRune", "ChippedAmethyst"}, - }, - { - Name: "Upgrade Sol", - Items: []string{"SolRune", "SolRune", "SolRune", "ChippedSapphire"}, - }, - { - Name: "Upgrade Shael", - Items: []string{"ShaelRune", "ShaelRune", "ShaelRune", "ChippedRuby"}, - }, - { - Name: "Upgrade Dol", - Items: []string{"DolRune", "DolRune", "DolRune", "ChippedEmerald"}, - }, - { - Name: "Upgrade Hel", - Items: []string{"HelRune", "HelRune", "HelRune", "ChippedDiamond"}, - }, - { - Name: "Upgrade Io", - Items: []string{"IoRune", "IoRune", "IoRune", "FlawedTopaz"}, - }, - { - Name: "Upgrade Lum", - Items: []string{"LumRune", "LumRune", "LumRune", "FlawedAmethyst"}, - }, - { - Name: "Upgrade Ko", - Items: []string{"KoRune", "KoRune", "KoRune", "FlawedSapphire"}, - }, - { - Name: "Upgrade Fal", - Items: []string{"FalRune", "FalRune", "FalRune", "FlawedRuby"}, - }, - { - Name: "Upgrade Lem", - Items: []string{"LemRune", "LemRune", "LemRune", "FlawedEmerald"}, - }, - { - Name: "Upgrade Pul", - Items: []string{"PulRune", "PulRune", "FlawedDiamond"}, - }, - { - Name: "Upgrade Um", - Items: []string{"UmRune", "UmRune", "Topaz"}, - }, - { - Name: "Upgrade Mal", - Items: []string{"MalRune", "MalRune", "Amethyst"}, - }, - { - Name: "Upgrade Ist", - Items: []string{"IstRune", "IstRune", "Sapphire"}, - }, - { - Name: "Upgrade Gul", - Items: []string{"GulRune", "GulRune", "Ruby"}, - }, - { - Name: "Upgrade Vex", - Items: []string{"VexRune", "VexRune", "Emerald"}, - }, - { - Name: "Upgrade Ohm", - Items: []string{"OhmRune", "OhmRune", "Diamond"}, - }, - { - Name: "Upgrade Lo", - Items: []string{"LoRune", "LoRune", "FlawlessTopaz"}, - }, - { - Name: "Upgrade Sur", - Items: []string{"SurRune", "SurRune", "FlawlessAmethyst"}, - }, - { - Name: "Upgrade Ber", - Items: []string{"BerRune", "BerRune", "FlawlessSapphire"}, - }, - { - Name: "Upgrade Jah", - Items: []string{"JahRune", "JahRune", "FlawlessRuby"}, - }, - { - Name: "Upgrade Cham", - Items: []string{"ChamRune", "ChamRune", "FlawlessEmerald"}, - }, - } -) - -func CubeRecipes() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "CubeRecipes" - - // If cubing is disabled from settings just return nil - if !ctx.CharacterCfg.CubeRecipes.Enabled { - return nil - } - - itemsInStash := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash) - for _, recipe := range recipes { - // Check if the current recipe is Enabled - if !slices.Contains(ctx.CharacterCfg.CubeRecipes.EnabledRecipes, recipe.Name) { - continue - } - - continueProcessing := true - for continueProcessing { - if items, hasItems := hasItemsForRecipe(itemsInStash, recipe); hasItems { - // Add items to the cube and perform the transmutation - err := CubeAddItems(items...) - if err != nil { - return err - } - if err = CubeTransmute(); err != nil { - return err - } - - // Add items to the stash - _ = Stash(true) - - // Remove or decrement the used items from itemsInStash - itemsInStash = removeUsedItems(itemsInStash, items) - } else { - continueProcessing = false - } - } - } - - return nil -} - -func hasItemsForRecipe(items []data.Item, recipe CubeRecipe) ([]data.Item, bool) { - // Create a map of the items we need for the recipe. - recipeItems := make(map[string]int) - for _, item := range recipe.Items { - recipeItems[item]++ - } - - itemsForRecipe := []data.Item{} - - // Iterate over the items in our stash to see if we have the items for the recipe. - for _, item := range items { - if count, ok := recipeItems[string(item.Name)]; ok { - itemsForRecipe = append(itemsForRecipe, item) - - // Check if we now have exactly the needed count before decrementing - count -= 1 - if count == 0 { - delete(recipeItems, string(item.Name)) - if len(recipeItems) == 0 { - return itemsForRecipe, true - } - } else { - recipeItems[string(item.Name)] = count - } - } - } - - // We don't have all the items for the recipe. - return nil, false -} - -func removeUsedItems(stash []data.Item, usedItems []data.Item) []data.Item { - remainingItems := make([]data.Item, 0) - usedItemMap := make(map[string]int) - - // Populate a map with the count of used items - for _, item := range usedItems { - usedItemMap[string(item.Name)] += 1 // Assuming 'ID' uniquely identifies items in 'usedItems' - } - - // Filter the stash by excluding used items based on the count in the map - for _, item := range stash { - if count, exists := usedItemMap[string(item.Name)]; exists && count > 0 { - usedItemMap[string(item.Name)] -= 1 - } else { - remainingItems = append(remainingItems, item) - } - } - - return remainingItems -} diff --git a/internal/v2/action/gambling.go b/internal/v2/action/gambling.go deleted file mode 100644 index 92b82674..00000000 --- a/internal/v2/action/gambling.go +++ /dev/null @@ -1,127 +0,0 @@ -package action - -import ( - "errors" - "log/slog" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/nip" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/town" - "github.com/hectorgimenez/koolo/internal/v2/ui" - "github.com/hectorgimenez/koolo/internal/v2/utils" - "github.com/lxn/win" -) - -func Gamble() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "Gamble" - - stashedGold, _ := ctx.Data.PlayerUnit.FindStat(stat.StashGold, 0) - if ctx.CharacterCfg.Gambling.Enabled && stashedGold.Value >= 2500000 { - ctx.Logger.Info("Time to gamble! Visiting vendor...") - - vendorNPC := town.GetTownByArea(ctx.Data.PlayerUnit.Area).GamblingNPC() - - // Fix for Anya position - if vendorNPC == npc.Drehya { - _ = MoveToCoords(data.Position{ - X: 5107, - Y: 5119, - }) - } - - InteractNPC(vendorNPC) - // Jamella gamble button is the second one - if vendorNPC == npc.Jamella { - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - } else { - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_DOWN, win.VK_RETURN) - } - - if !ctx.Data.OpenMenus.NPCShop { - return errors.New("failed opening gambling window") - } - - return gambleItems() - } - - return nil -} - -func gambleItems() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "gambleItems" - - var itemBought data.Item - currentIdx := 0 - lastStep := false - for { - if lastStep { - utils.Sleep(200) - ctx.Logger.Info("Finished gambling", slog.Int("currentGold", ctx.Data.PlayerUnit.TotalPlayerGold())) - - return step.CloseAllMenus() - } - - if itemBought.Name != "" { - for _, itm := range ctx.Data.Inventory.ByLocation(item.LocationInventory) { - if itm.UnitID == itemBought.UnitID { - itemBought = itm - ctx.Logger.Debug("Gambled for item", slog.Any("item", itemBought)) - break - } - } - - if _, result := ctx.Data.CharacterCfg.Runtime.Rules.EvaluateAll(itemBought); result == nip.RuleResultFullMatch { - lastStep = true - - } else { - // Filter not pass, selling the item - town.SellItem(itemBought) - itemBought = data.Item{} - } - continue - } - - if ctx.Data.PlayerUnit.TotalPlayerGold() < 500000 { - lastStep = true - continue - } - - for idx, itmName := range ctx.Data.CharacterCfg.Gambling.Items { - // Let's try to get one of each every time - if currentIdx == len(ctx.CharacterCfg.Gambling.Items) { - currentIdx = 0 - } - - if currentIdx > idx { - continue - } - - itm, found := ctx.Data.Inventory.Find(itmName, item.LocationVendor) - if !found { - ctx.Logger.Debug("Item not found in gambling window, refreshing...", slog.String("item", string(itmName))) - - if ctx.Data.LegacyGraphics { - ctx.HID.Click(game.LeftButton, ui.GambleRefreshButtonXClassic, ui.GambleRefreshButtonYClassic) - } else { - ctx.HID.Click(game.LeftButton, ui.GambleRefreshButtonX, ui.GambleRefreshButtonY) - } - - helper.Sleep(500) - continue - } - - town.BuyItem(itm, 1) - itemBought = itm - currentIdx++ - } - } -} diff --git a/internal/v2/action/heal_at_npc.go b/internal/v2/action/heal_at_npc.go deleted file mode 100644 index dfe500e4..00000000 --- a/internal/v2/action/heal_at_npc.go +++ /dev/null @@ -1,32 +0,0 @@ -package action - -import ( - "github.com/hectorgimenez/koolo/internal/town" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func HealAtNPC() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "HealAtNPC" - - shouldHeal := false - if ctx.Data.PlayerUnit.HPPercent() < 80 { - ctx.Logger.Info("Current life is %d%%, healing on NPC", ctx.Data.PlayerUnit.HPPercent()) - shouldHeal = true - } - - if ctx.Data.PlayerUnit.HasDebuff() { - ctx.Logger.Info("Debuff detected, healing on NPC") - shouldHeal = true - } - - if shouldHeal { - err := InteractNPC(town.GetTownByArea(ctx.Data.PlayerUnit.Area).HealNPC()) - if err != nil { - ctx.Logger.Warn("Failed to heal on NPC: %v", err) - } - } - - return step.CloseAllMenus() -} diff --git a/internal/v2/action/horadric_cube.go b/internal/v2/action/horadric_cube.go deleted file mode 100644 index 88503a0e..00000000 --- a/internal/v2/action/horadric_cube.go +++ /dev/null @@ -1,138 +0,0 @@ -package action - -import ( - "errors" - "log/slog" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/ui" -) - -func CubeAddItems(items ...data.Item) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "CubeAddItems" - - cube, found := ctx.Data.Inventory.Find("HoradricCube", item.LocationInventory, item.LocationStash) - if !found { - ctx.Logger.Info("No Horadric Cube found in inventory") - return nil - } - - // Ensure stash is open - if !ctx.Data.OpenMenus.Stash { - bank, _ := ctx.Data.Objects.FindOne(object.Bank) - err := InteractObject(bank, func() bool { - return ctx.Data.OpenMenus.Stash - }) - if err != nil { - return err - } - } - - ctx.Logger.Info("Adding items to the Horadric Cube", slog.Any("items", items)) - - // If items are on the Stash, pickup them to the inventory - for _, itm := range items { - nwIt := itm - if nwIt.Location.LocationType != item.LocationStash && nwIt.Location.LocationType != item.LocationSharedStash { - continue - } - - // Check in which tab the item is and switch to it - switch nwIt.Location.LocationType { - case item.LocationStash: - SwitchTab(1) - case item.LocationSharedStash: - SwitchTab(nwIt.Location.Page + 1) - } - - ctx.Logger.Debug("Item found on the stash, picking it up", slog.String("Item", string(nwIt.Name))) - screenPos := ui.GetScreenCoordsForItem(nwIt) - - ctx.HID.ClickWithModifier(game.LeftButton, screenPos.X, screenPos.Y, game.CtrlKey) - helper.Sleep(300) - } - - err := ensureCubeIsOpen(cube) - if err != nil { - return err - } - - for _, itm := range items { - for _, updatedItem := range ctx.Data.Inventory.AllItems { - if itm.UnitID == updatedItem.UnitID { - ctx.Logger.Debug("Moving Item to the Horadric Cube", slog.String("Item", string(itm.Name))) - - screenPos := ui.GetScreenCoordsForItem(updatedItem) - - ctx.HID.ClickWithModifier(game.LeftButton, screenPos.X, screenPos.Y, game.CtrlKey) - helper.Sleep(300) - } - } - } - - return nil -} - -func CubeTransmute() error { - ctx := context.Get() - - cube, found := ctx.Data.Inventory.Find("HoradricCube", item.LocationInventory, item.LocationStash) - if !found { - ctx.Logger.Info("No Horadric Cube found in inventory") - return nil - } - - err := ensureCubeIsOpen(cube) - if err != nil { - return err - } - - ctx.Logger.Debug("Transmuting items in the Horadric Cube") - helper.Sleep(150) - - if ctx.Data.LegacyGraphics { - ctx.HID.Click(game.LeftButton, ui.CubeTransmuteBtnXClassic, ui.CubeTransmuteBtnYClassic) - } else { - ctx.HID.Click(game.LeftButton, ui.CubeTransmuteBtnX, ui.CubeTransmuteBtnY) - } - - helper.Sleep(2000) - - if ctx.Data.LegacyGraphics { - ctx.HID.ClickWithModifier(game.LeftButton, ui.CubeTakeItemXClassic, ui.CubeTakeItemYClassic, game.CtrlKey) - } else { - ctx.HID.ClickWithModifier(game.LeftButton, ui.CubeTakeItemX, ui.CubeTakeItemY, game.CtrlKey) - } - - helper.Sleep(300) - - return step.CloseAllMenus() -} - -func ensureCubeIsOpen(cube data.Item) error { - ctx := context.Get() - ctx.Logger.Debug("Opening Horadric Cube...") - - // Switch to the tab - SwitchTab(cube.Location.Page + 1) - - screenPos := ui.GetScreenCoordsForItem(cube) - - helper.Sleep(300) - ctx.HID.Click(game.RightButton, screenPos.X, screenPos.Y) - helper.Sleep(200) - - if ctx.Data.OpenMenus.Cube { - ctx.Logger.Debug("Horadric Cube window detected") - return nil - } - - return errors.New("horadric Cube window not detected") -} diff --git a/internal/v2/action/identify.go b/internal/v2/action/identify.go deleted file mode 100644 index 5dc24b77..00000000 --- a/internal/v2/action/identify.go +++ /dev/null @@ -1,86 +0,0 @@ -package action - -import ( - "fmt" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/nip" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/ui" -) - -func IdentifyAll(skipIdentify bool) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "IdentifyAll" - - items := itemsToIdentify() - - ctx.Logger.Debug("Checking for items to identify...") - if len(items) == 0 || skipIdentify { - ctx.Logger.Debug("No items to identify...") - return nil - } - - idTome, found := ctx.Data.Inventory.Find(item.TomeOfIdentify, item.LocationInventory) - if !found { - ctx.Logger.Warn("ID Tome not found, not identifying items") - return nil - } - - if st, statFound := idTome.FindStat(stat.Quantity, 0); !statFound || st.Value < len(items) { - ctx.Logger.Info("Not enough ID scrolls, refilling...") - VendorRefill(true, false) - } - - ctx.Logger.Info(fmt.Sprintf("Identifying %d items...", len(items))) - step.CloseAllMenus() - for !ctx.Data.OpenMenus.Inventory { - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.Inventory) - helper.Sleep(300) - } - for _, i := range items { - identifyItem(idTome, i) - } - step.CloseAllMenus() - - return nil -} - -func itemsToIdentify() (items []data.Item) { - ctx := context.Get() - ctx.ContextDebug.LastAction = "itemsToIdentify" - - for _, i := range ctx.Data.Inventory.ByLocation(item.LocationInventory) { - if i.Identified || i.Quality == item.QualityNormal || i.Quality == item.QualitySuperior { - continue - } - - // Skip identifying items that fully match a rule when unid - if _, result := ctx.CharacterCfg.Runtime.Rules.EvaluateAll(i); result == nip.RuleResultFullMatch { - continue - } - - items = append(items, i) - } - - return -} - -func identifyItem(idTome data.Item, i data.Item) { - ctx := context.Get() - screenPos := ui.GetScreenCoordsForItem(idTome) - - helper.Sleep(500) - ctx.HID.Click(game.RightButton, screenPos.X, screenPos.Y) - helper.Sleep(1000) - - screenPos = ui.GetScreenCoordsForItem(i) - - ctx.HID.Click(game.LeftButton, screenPos.X, screenPos.Y) - helper.Sleep(350) -} diff --git a/internal/v2/action/interaction.go b/internal/v2/action/interaction.go deleted file mode 100644 index 6054961c..00000000 --- a/internal/v2/action/interaction.go +++ /dev/null @@ -1,80 +0,0 @@ -package action - -import ( - "fmt" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/event" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func InteractNPC(npc npc.ID) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "InteractNPC" - - pos, found := getNPCPosition(npc, ctx.Data) - if !found { - return fmt.Errorf("npc with ID %d not found", npc) - } - - err := step.MoveTo(pos) - if err != nil { - return err - } - - err = step.InteractNPC(npc) - if err != nil { - return err - } - - event.Send(event.InteractedTo(event.Text(ctx.Name, ""), int(npc), event.InteractionTypeNPC)) - - return nil -} - -func InteractObject(o data.Object, isCompletedFn func() bool) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "InteractObject" - - pos := o.Position - if ctx.Data.PlayerUnit.Area == area.RiverOfFlame && o.IsWaypoint() { - pos = data.Position{X: 7800, Y: 5919} - } - - err := step.MoveTo(pos) - if err != nil { - return err - } - - return step.InteractObject(o, isCompletedFn) -} - -func InteractObjectByID(id data.UnitID, isCompletedFn func() bool) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "InteractObjectByID" - - o, found := ctx.Data.Objects.FindByID(id) - if !found { - return fmt.Errorf("object with ID %d not found", id) - } - - return InteractObject(o, isCompletedFn) -} - -func getNPCPosition(npc npc.ID, d *game.Data) (data.Position, bool) { - monster, found := d.Monsters.FindOne(npc, data.MonsterTypeNone) - if found { - return monster.Position, true - } - - n, found := d.NPCs.FindOne(npc) - if !found { - return data.Position{}, false - } - - return data.Position{X: n.Positions[0].X, Y: n.Positions[0].Y}, true -} diff --git a/internal/v2/action/item.go b/internal/v2/action/item.go deleted file mode 100644 index 6277de9e..00000000 --- a/internal/v2/action/item.go +++ /dev/null @@ -1,53 +0,0 @@ -package action - -import ( - "fmt" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/nip" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func doesExceedQuantity(i data.Item, rule nip.Rule) bool { - ctx := context.Get() - ctx.ContextDebug.LastAction = "doesExceedQuantity" - - stashItems := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash) - - maxQuantity := rule.MaxQuantity() - if maxQuantity == 0 { - return false - } - - if maxQuantity == 0 { - ctx.Logger.Debug(fmt.Sprintf("Max quantity for %s item is 0, skipping further logic", i.Name)) - return false - } - - matchedItemsInStash := 0 - - for _, stashItem := range stashItems { - res, _ := rule.Evaluate(stashItem) - if res == nip.RuleResultFullMatch { - matchedItemsInStash += 1 - } - } - - ctx.Logger.Debug(fmt.Sprintf("For item %s found %d max quantity from pickit rule, number of items in the stash tabs %d", i.Name, maxQuantity, matchedItemsInStash)) - - return matchedItemsInStash >= maxQuantity -} - -func DropMouseItem() { - ctx := context.Get() - ctx.ContextDebug.LastStep = "DropMouseItem" - - if len(ctx.Data.Inventory.ByLocation(item.LocationCursor)) > 0 { - helper.Sleep(1000) - ctx.HID.Click(game.LeftButton, 500, 500) - helper.Sleep(1000) - } -} diff --git a/internal/v2/action/item_pickup.go b/internal/v2/action/item_pickup.go deleted file mode 100644 index 14bdc2c6..00000000 --- a/internal/v2/action/item_pickup.go +++ /dev/null @@ -1,183 +0,0 @@ -package action - -import ( - "fmt" - "log/slog" - "slices" - - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/nip" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/stat" -) - -func ItemPickup(maxDistance int) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "ItemPickup" - - var itemBeingPickedUp data.UnitID - - for itemsToPickup := GetItemsToPickup(maxDistance); len(itemsToPickup) > 0; itemsToPickup = GetItemsToPickup(maxDistance) { - for _, m := range ctx.Data.Monsters.Enemies() { - if dist := ctx.PathFinder.DistanceFromMe(m.Position); dist < 7 { - ctx.Logger.Debug("Aborting item pickup, monster nearby", slog.Any("monster", m)) - itemBeingPickedUp = -1 - _ = ctx.Char.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - return m.UnitID, true - }, nil) - continue - } - } - - i := itemsToPickup[0] - - // Error picking up Item, go back to town, sell junk, stash and try again. - if itemBeingPickedUp == i.UnitID { - ctx.Logger.Debug("Item could not be picked up, going back to town to sell junk and stash") - itemBeingPickedUp = -1 - InRunReturnTownRoutine() - continue - } - - ctx.Logger.Debug(fmt.Sprintf( - "Item Detected: %s [%d] at X:%d Y:%d", - i.Name, - i.Quality, - i.Position.X, - i.Position.Y, - )) - - itemBeingPickedUp = i.UnitID - err := MoveToCoords(i.Position) - if err != nil { - ctx.Logger.Warn("Failed moving closer to item, trying to pickup it anyway", err) - } - - // TODO Handle proper error & multiple items and blacklist - err = step.PickupItem(i) - if err != nil { - ctx.Logger.Warn("Failed picking up item, skipping", err) - } - } - - return nil -} - -func GetItemsToPickup(maxDistance int) []data.Item { - ctx := context.Get() - ctx.ContextDebug.LastStep = "GetItemsToPickup" - - missingHealingPotions := ctx.BeltManager.GetMissingCount(data.HealingPotion) - missingManaPotions := ctx.BeltManager.GetMissingCount(data.ManaPotion) - missingRejuvenationPotions := ctx.BeltManager.GetMissingCount(data.RejuvenationPotion) - var itemsToPickup []data.Item - _, isLevelingChar := ctx.Char.(context.LevelingCharacter) - for _, itm := range ctx.Data.Inventory.ByLocation(item.LocationGround) { - // Skip itempickup on party leveling Maggot Lair, is too narrow and causes characters to get stuck - if ctx.CharacterCfg.Companion.Enabled && isLevelingChar && !itm.IsFromQuest() && (ctx.Data.PlayerUnit.Area == area.MaggotLairLevel1 || ctx.Data.PlayerUnit.Area == area.MaggotLairLevel2 || ctx.Data.PlayerUnit.Area == area.MaggotLairLevel3 || ctx.Data.PlayerUnit.Area == area.ArcaneSanctuary) { - continue - } - - // Skip items that are outside pickup radius, this is useful when clearing big areas to prevent - // character going back to pickup potions all the time after using them - itemDistance := ctx.PathFinder.DistanceFromMe(itm.Position) - if maxDistance > 0 && itemDistance > maxDistance { - continue - } - - if !shouldBePickedUp(itm) { - continue - } - - // Pickup potions only if they are required - if itm.IsHealingPotion() && missingHealingPotions > 0 { - itemsToPickup = append(itemsToPickup, itm) - missingHealingPotions-- - continue - } - if itm.IsManaPotion() && missingManaPotions > 0 { - itemsToPickup = append(itemsToPickup, itm) - missingManaPotions-- - continue - } - if itm.IsRejuvPotion() && missingRejuvenationPotions > 0 { - itemsToPickup = append(itemsToPickup, itm) - missingRejuvenationPotions-- - continue - } - - if !itm.IsPotion() { - itemsToPickup = append(itemsToPickup, itm) - } - } - - return itemsToPickup -} - -func shouldBePickedUp(i data.Item) bool { - ctx := context.Get() - ctx.ContextDebug.LastStep = "shouldBePickedUp" - - if i.IsRuneword { - return true - } - - // Skip picking up gold if we can not carry more - gold, _ := ctx.Data.PlayerUnit.FindStat(stat.Gold, 0) - if gold.Value >= ctx.Data.PlayerUnit.MaxGold() && i.Name == "Gold" { - ctx.Logger.Debug("Skipping gold pickup, inventory full") - return false - } - - // Always pickup WirtsLeg! - if i.Name == "WirtsLeg" { - return true - } - - // Pick up quest items if we're in leveling or questing run - specialRuns := slices.Contains(ctx.CharacterCfg.Game.Runs, "quests") || slices.Contains(ctx.CharacterCfg.Game.Runs, "leveling") - switch i.Name { - case "Scrollofinifuss", "LamEsensTome", "HoradricCube", "AmuletoftheViper", "StaffofKings", "HoradricStaff", "AJadeFigurine", "KhalimsEye", "KhalimsBrain", "KhalimsHeart", "KhalimsFlail": - if specialRuns { - return true - } - } - - // Book of Skill doesnt work by name, so we find it by ID - if i.ID == 552 { - return true - } - - // Only during leveling if gold amount is low pickup items to sell as junk - _, isLevelingChar := ctx.Char.(context.LevelingCharacter) - - // Skip picking up gold, usually early game there are small amounts of gold in many places full of enemies, better - // stay away of that - if isLevelingChar && ctx.Data.PlayerUnit.TotalPlayerGold() < 50000 && i.Name != "Gold" { - return true - } - - minGoldPickupThreshold := ctx.CharacterCfg.Game.MinGoldPickupThreshold - // Pickup all magic or superior items if total gold is low, filter will not pass and items will be sold to vendor - if ctx.Data.PlayerUnit.TotalPlayerGold() < minGoldPickupThreshold && i.Quality >= item.QualityMagic { - return true - } - - matchedRule, result := ctx.Data.CharacterCfg.Runtime.Rules.EvaluateAll(i) - if result == nip.RuleResultNoMatch { - return false - } - - if result == nip.RuleResultPartial { - return true - } - - exceedQuantity := doesExceedQuantity(i, matchedRule) - - return !exceedQuantity -} diff --git a/internal/v2/action/legacy_mode.go b/internal/v2/action/legacy_mode.go deleted file mode 100644 index bf9b4643..00000000 --- a/internal/v2/action/legacy_mode.go +++ /dev/null @@ -1,30 +0,0 @@ -package action - -import ( - "time" - - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/ui" - "github.com/hectorgimenez/koolo/internal/v2/context" - - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/game" -) - -func SwitchToLegacyMode() { - ctx := context.Get() - ctx.ContextDebug.LastAction = "SwitchToLegacyMode" - - if ctx.CharacterCfg.ClassicMode && !ctx.Data.LegacyGraphics { - ctx.Logger.Debug("Switching to legacy mode...") - ctx.HID.PressKey(ctx.Data.KeyBindings.LegacyToggle.Key1[0]) - step.Wait(time.Millisecond * 500) // Add small delay to allow the game to switch - - // Close the mini panel if option is enabled - if ctx.CharacterCfg.CloseMiniPanel { - helper.Sleep(100) - ctx.HID.Click(game.LeftButton, ui.CloseMiniPanelClassicX, ui.CloseMiniPanelClassicY) - helper.Sleep(100) - } - } -} diff --git a/internal/v2/action/leveling_tools.go b/internal/v2/action/leveling_tools.go deleted file mode 100644 index a225dfce..00000000 --- a/internal/v2/action/leveling_tools.go +++ /dev/null @@ -1,407 +0,0 @@ -package action - -import ( - "slices" - - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" - "github.com/lxn/win" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/difficulty" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/town" - "github.com/hectorgimenez/koolo/internal/ui" - "github.com/hectorgimenez/koolo/internal/v2/action/step" -) - -var uiStatButtonPosition = map[stat.ID]data.Position{ - stat.Strength: {X: 240, Y: 210}, - stat.Dexterity: {X: 240, Y: 290}, - stat.Vitality: {X: 240, Y: 380}, - stat.Energy: {X: 240, Y: 430}, -} - -var uiSkillPagePosition = [3]data.Position{ - {X: 1100, Y: 140}, - {X: 1010, Y: 140}, - {X: 910, Y: 140}, -} - -var uiSkillRowPosition = [6]int{190, 250, 310, 365, 430, 490} -var uiSkillColumnPosition = [3]int{920, 1010, 1095} - -var uiStatButtonPositionLegacy = map[stat.ID]data.Position{ - stat.Strength: {X: 430, Y: 180}, - stat.Dexterity: {X: 430, Y: 250}, - stat.Vitality: {X: 430, Y: 360}, - stat.Energy: {X: 430, Y: 435}, -} - -var uiSkillPagePositionLegacy = [3]data.Position{ - {X: 970, Y: 510}, - {X: 970, Y: 390}, - {X: 970, Y: 260}, -} - -var uiSkillRowPositionLegacy = [6]int{110, 195, 275, 355, 440, 520} -var uiSkillColumnPositionLegacy = [3]int{690, 770, 855} - -func EnsureStatPoints() error { - // TODO finish this - return nil - //return NewStepChain(func(d game.Data) []step.Step { - // char, isLevelingChar := b.ch.(LevelingCharacter) - // _, unusedStatPoints := d.PlayerUnit.FindStat(stat.StatPoints, 0) - // if !isLevelingChar || !unusedStatPoints { - // if d.OpenMenus.Character { - // return []step.Step{ - // step.SyncStep(func(_ game.Data) error { - // b.HID.PressKey(win.VK_ESCAPE) - // return nil - // }), - // } - // } - // - // return nil - // } - // - // for st, targetPoints := range char.StatPoints(d) { - // currentPoints, found := d.PlayerUnit.FindStat(st, 0) - // if !found || currentPoints.Value >= targetPoints { - // continue - // } - // - // if !d.OpenMenus.Character { - // return []step.Step{ - // step.SyncStep(func(_ game.Data) error { - // b.HID.PressKeyBinding(d.KeyBindings.CharacterScreen) - // return nil - // }), - // } - // } - // - // var statBtnPosition data.Position - // if d.LegacyGraphics { - // statBtnPosition = uiStatButtonPositionLegacy[st] - // } else { - // statBtnPosition = uiStatButtonPosition[st] - // } - // - // return []step.Step{ - // step.SyncStep(func(_ game.Data) error { - // helper.Sleep(100) - // b.HID.Click(game.LeftButton, statBtnPosition.X, statBtnPosition.Y) - // helper.Sleep(500) - // return nil - // }), - // } - // } - // - // return nil - //}, RepeatUntilNoSteps()) -} - -func EnsureSkillPoints() error { - // TODO finish this - return nil - //ctx := context.Get() - // - //char, isLevelingChar := ctx.Char.(LevelingCharacter) - //availablePoints, unusedSkillPoints := ctx.Data.PlayerUnit.FindStat(stat.SkillPoints, 0) - // - //assignedPoints := make(map[skill.ID]int) - //for _, sk := range char.SkillPoints() { - // currentPoints, found := assignedPoints[sk] - // if !found { - // currentPoints = 0 - // } - // - // assignedPoints[sk] = currentPoints + 1 - // - // characterPoints, found := ctx.Data.PlayerUnit.Skills[sk] - // if !found || int(characterPoints.Level) < assignedPoints[sk] { - // skillDesc, skFound := skill.Desc[sk] - // if !skFound { - // ctx.Logger.Error("skill not found for character", slog.Any("skill", sk)) - // return nil - // } - // - // if !ctx.Data.OpenMenus.SkillTree { - // ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.SkillTree) - // } - // - // helper.Sleep(100) - // if ctx.Data.LegacyGraphics { - // ctx.HID.Click(game.LeftButton, uiSkillPagePositionLegacy[skillDesc.Page-1].X, uiSkillPagePositionLegacy[skillDesc.Page-1].Y) - // } else { - // ctx.HID.Click(game.LeftButton, uiSkillPagePosition[skillDesc.Page-1].X, uiSkillPagePosition[skillDesc.Page-1].Y) - // } - // helper.Sleep(200) - // if ctx.Data.LegacyGraphics { - // ctx.HID.Click(game.LeftButton, uiSkillColumnPositionLegacy[skillDesc.Column-1], uiSkillRowPositionLegacy[skillDesc.Row-1]) - // } else { - // ctx.HID.Click(game.LeftButton, uiSkillColumnPosition[skillDesc.Column-1], uiSkillRowPosition[skillDesc.Row-1]) - // } - // helper.Sleep(500) - // return nil - // } - //} - // - //return nil -} - -func UpdateQuestLog() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "UpdateQuestLog" - - if _, isLevelingChar := ctx.Char.(context.LevelingCharacter); !isLevelingChar { - return nil - } - - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.QuestLog) - utils.Sleep(1000) - - return step.CloseAllMenus() -} -func getAvailableSkillKB() []data.KeyBinding { - availableSkillKB := make([]data.KeyBinding, 0) - ctx := context.Get() - ctx.ContextDebug.LastStep = "getAvailableSkillKB" - - for _, sb := range ctx.Data.KeyBindings.Skills { - if sb.SkillID == -1 && (sb.Key1[0] != 0 && sb.Key1[0] != 255) || (sb.Key2[0] != 0 && sb.Key2[0] != 255) { - availableSkillKB = append(availableSkillKB, sb.KeyBinding) - } - } - - return availableSkillKB -} - -func EnsureSkillBindings() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "EnsureSkillBindings" - - char, isLevelingChar := ctx.Char.(context.LevelingCharacter) - if !isLevelingChar { - return nil - } - - mainSkill, skillsToBind := char.SkillsToBind() - skillsToBind = append(skillsToBind, skill.TomeOfTownPortal) - notBoundSkills := make([]skill.ID, 0) - for _, sk := range skillsToBind { - if _, found := ctx.Data.KeyBindings.KeyBindingForSkill(sk); !found && ctx.Data.PlayerUnit.Skills[sk].Level > 0 { - notBoundSkills = append(notBoundSkills, sk) - } - } - - if len(notBoundSkills) > 0 { - ctx.HID.Click(game.LeftButton, ui.SecondarySkillButtonX, ui.SecondarySkillButtonY) - utils.Sleep(300) - ctx.HID.MovePointer(10, 10) - utils.Sleep(300) - - availableKB := getAvailableSkillKB() - - for i, sk := range notBoundSkills { - skillPosition, found := calculateSkillPositionInUI(false, sk) - if !found { - continue - } - - ctx.HID.MovePointer(skillPosition.X, skillPosition.Y) - utils.Sleep(100) - ctx.HID.PressKeyBinding(availableKB[i]) - utils.Sleep(300) - } - - } - - if ctx.Data.PlayerUnit.LeftSkill != mainSkill { - ctx.HID.Click(game.LeftButton, ui.MainSkillButtonX, ui.MainSkillButtonY) - helper.Sleep(300) - ctx.HID.MovePointer(10, 10) - helper.Sleep(300) - - skillPosition, found := calculateSkillPositionInUI(true, mainSkill) - if found { - ctx.HID.MovePointer(skillPosition.X, skillPosition.Y) - helper.Sleep(100) - ctx.HID.Click(game.LeftButton, skillPosition.X, skillPosition.Y) - helper.Sleep(300) - } - } - - return nil -} - -func calculateSkillPositionInUI(mainSkill bool, skillID skill.ID) (data.Position, bool) { - d := context.Get().Data - - var scrolls = []skill.ID{ - skill.TomeOfTownPortal, skill.ScrollOfTownPortal, skill.TomeOfIdentify, skill.ScrollOfIdentify, - } - - if _, found := d.PlayerUnit.Skills[skillID]; !found { - return data.Position{}, false - } - - targetSkill := skill.Skills[skillID] - descs := make(map[skill.ID]skill.Skill) - row := 0 - totalRows := make([]int, 0) - column := 0 - skillsWithCharges := 0 - for skID, points := range d.PlayerUnit.Skills { - sk := skill.Skills[skID] - // Skip skills that can not be bind - if sk.Desc().ListRow < 0 { - continue - } - - // Skip skills that can not be bind to current mouse button - if (mainSkill == true && !sk.LeftSkill) || (mainSkill == false && !sk.RightSkill) { - continue - } - - if points.Charges > 0 { - skillsWithCharges++ - continue - } - - if slices.Contains(scrolls, sk.ID) { - continue - } - descs[skID] = sk - - if skID != targetSkill.ID && sk.Desc().Page == targetSkill.Desc().Page { - if sk.Desc().ListRow > targetSkill.Desc().ListRow { - column++ - } else if sk.Desc().ListRow == targetSkill.Desc().ListRow && sk.Desc().Column > targetSkill.Desc().Column { - column++ - } - } - - totalRows = append(totalRows, sk.Desc().ListRow) - if row == targetSkill.Desc().ListRow { - continue - } - - row++ - } - - slices.Sort(totalRows) - totalRows = slices.Compact(totalRows) - - // If we don't have any skill of a specific tree, the entire row gets one line down - for i, currentRow := range totalRows { - if currentRow == row { - row = i - break - } - } - - // Scrolls and charges are not in the same list - if slices.Contains(scrolls, skillID) { - column = skillsWithCharges - row = len(totalRows) - for _, skID := range scrolls { - if d.PlayerUnit.Skills[skID].Quantity > 0 { - if skID == skillID { - break - } - column++ - } - } - } - - skillOffsetX := ui.MainSkillListFirstSkillX - (ui.SkillListSkillOffset * column) - if !mainSkill { - skillOffsetX = ui.SecondarySkillListFirstSkillX + (ui.SkillListSkillOffset * column) - } - - return data.Position{ - X: skillOffsetX, - Y: ui.SkillListFirstSkillY - ui.SkillListSkillOffset*row, - }, true -} - -func HireMerc() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "HireMerc" - - _, isLevelingChar := ctx.Char.(context.LevelingCharacter) - if isLevelingChar && ctx.CharacterCfg.Character.UseMerc { - // Hire the merc if we don't have one, we have enough gold, and we are in act 2. We assume that ReviveMerc was called before this. - if ctx.CharacterCfg.Game.Difficulty == difficulty.Normal && ctx.Data.MercHPPercent() <= 0 && ctx.Data.PlayerUnit.TotalPlayerGold() > 30000 && ctx.Data.PlayerUnit.Area == area.LutGholein { - ctx.Logger.Info("Hiring merc...") - // TODO: Hire Holy Freeze merc if available, if not, hire Defiance merc. - err := InteractNPC(town.GetTownByArea(ctx.Data.PlayerUnit.Area).MercContractorNPC()) - if err != nil { - return err - } - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - utils.Sleep(2000) - ctx.HID.Click(game.LeftButton, ui.FirstMercFromContractorListX, ui.FirstMercFromContractorListY) - utils.Sleep(500) - ctx.HID.Click(game.LeftButton, ui.FirstMercFromContractorListX, ui.FirstMercFromContractorListY) - } - } - - return nil -} - -func ResetStats() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "ResetStats" - - ch, isLevelingChar := ctx.Char.(context.LevelingCharacter) - if isLevelingChar && ch.ShouldResetSkills() { - currentArea := ctx.Data.PlayerUnit.Area - if ctx.Data.PlayerUnit.Area != area.RogueEncampment { - err := WayPoint(area.RogueEncampment) - if err != nil { - return err - } - } - InteractNPC(npc.Akara) - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_DOWN, win.VK_RETURN) - utils.Sleep(1000) - ctx.HID.KeySequence(win.VK_HOME, win.VK_RETURN) - - if currentArea != area.RogueEncampment { - return WayPoint(currentArea) - } - } - - return nil -} - -func WaitForAllMembersWhenLeveling() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "WaitForAllMembersWhenLeveling" - - for { - _, isLeveling := ctx.Char.(context.LevelingCharacter) - if ctx.CharacterCfg.Companion.Enabled && ctx.CharacterCfg.Companion.Leader && !ctx.Data.PlayerUnit.Area.IsTown() && isLeveling { - allMembersAreaCloseToMe := true - for _, member := range ctx.Data.Roster { - if member.Name != ctx.Data.PlayerUnit.Name && ctx.PathFinder.DistanceFromMe(member.Position) > 20 { - allMembersAreaCloseToMe = false - } - } - - if allMembersAreaCloseToMe { - return nil - } - - ClearAreaAroundPlayer(5, data.MonsterAnyFilter()) - } - } -} diff --git a/internal/v2/action/move.go b/internal/v2/action/move.go deleted file mode 100644 index b37ff438..00000000 --- a/internal/v2/action/move.go +++ /dev/null @@ -1,231 +0,0 @@ -package action - -import ( - "fmt" - "log/slog" - "sort" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/event" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/pather" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func MoveToArea(dst area.ID) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "MoveToArea" - - // Exception for Arcane Sanctuary, we need to find the portal first - if dst == area.ArcaneSanctuary && ctx.Data.PlayerUnit.Area == area.PalaceCellarLevel3 { - ctx.Logger.Debug("Arcane Sanctuary detected, finding the Portal") - portal, _ := ctx.Data.Objects.FindOne(object.ArcaneSanctuaryPortal) - MoveToCoords(portal.Position) - - return step.InteractObject(portal, func() bool { - return ctx.Data.PlayerUnit.Area == area.ArcaneSanctuary - }) - } - - toFun := func() (data.Position, bool) { - if ctx.Data.PlayerUnit.Area == dst { - ctx.Logger.Debug("Reached area", slog.String("area", dst.Area().Name)) - return data.Position{}, false - } - - switch dst { - case area.MonasteryGate: - ctx.Logger.Debug("Monastery Gate detected, moving to static coords") - return data.Position{X: 15139, Y: 5056}, true - } - - for _, a := range ctx.Data.AdjacentLevels { - if a.Area == dst { - // To correctly detect the two possible exits from Lut Gholein - if dst == area.RockyWaste && ctx.Data.PlayerUnit.Area == area.LutGholein { - if _, _, found := ctx.PathFinder.GetPath(data.Position{X: 5004, Y: 5065}); found { - return data.Position{X: 4989, Y: 5063}, true - } else { - return data.Position{X: 5096, Y: 4997}, true - } - } - - // This means it's a cave, we don't want to load the map, just find the entrance and interact - if a.IsEntrance { - return a.Position, true - } - - lvl, _ := ctx.GameReader.GetCachedMapData(false).GetLevelData(a.Area) - _, _, objects, _ := ctx.GameReader.GetCachedMapData(false).NPCsExitsAndObjects(lvl.Offset, a.Area) - - // Sort objects by the distance from me - sort.Slice(objects, func(i, j int) bool { - distanceI := ctx.PathFinder.DistanceFromMe(objects[i].Position) - distanceJ := ctx.PathFinder.DistanceFromMe(objects[j].Position) - - return distanceI < distanceJ - }) - - // Let's try to find any random object to use as a destination point, once we enter the level we will exit this flow - for _, obj := range objects { - _, _, found := ctx.PathFinder.GetPath(obj.Position) - if found { - return obj.Position, true - } - } - - return a.Position, true - } - } - - ctx.Logger.Debug("Destination area not found", slog.String("area", dst.Area().Name)) - - return data.Position{}, false - } - - MoveTo(toFun) - err := step.InteractEntrance(dst) - if err != nil { - return err - } - - event.Send(event.InteractedTo(event.Text(ctx.Name, ""), int(dst), event.InteractionTypeEntrance)) - - return nil -} - -func MoveToCoords(to data.Position) error { - return MoveTo(func() (data.Position, bool) { - return to, true - }) -} - -func MoveTo(toFunc func() (data.Position, bool)) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "MoveTo" - - openedDoors := make(map[object.Name]data.Position) - previousIterationPosition := data.Position{} - for { - to, found := toFunc() - if !found { - return nil - } - - // If we can teleport, don't bother with the rest, stop here - if ctx.Data.CanTeleport() { - return step.MoveTo(to) - } - - _, distance, _ := ctx.PathFinder.GetPath(to) - // This prevents we stuck in an infinite loop when we can not get closer to the destination - if distance < 7 { - return nil - } - - // Check if there is a door blocking our path - for _, o := range ctx.Data.Objects { - if o.IsDoor() && ctx.PathFinder.DistanceFromMe(o.Position) < 10 && openedDoors[o.Name] != o.Position { - if o.Selectable { - ctx.Logger.Info("Door detected and teleport is not available, trying to open it...") - openedDoors[o.Name] = o.Position - err := step.InteractObject(o, func() bool { - obj, found := ctx.Data.Objects.FindByID(o.ID) - - return found && !obj.Selectable - }) - if err != nil { - return err - } - } - } - } - - // Check if there is any object blocking our path - for _, o := range ctx.Data.Objects { - if o.Name == object.Barrel && ctx.PathFinder.DistanceFromMe(o.Position) < 3 { - err := step.InteractObject(o, func() bool { - obj, found := ctx.Data.Objects.FindByID(o.ID) - //additional click on barrel to avoid getting stuck - x, y := ctx.PathFinder.GameCoordsToScreenCords(o.Position.X, o.Position.Y) - ctx.HID.Click(game.LeftButton, x, y) - return found && !obj.Selectable - }) - if err != nil { - return err - } - } - } - - // Detect if there are monsters close to the player - closestMonster := data.Monster{} - closestMonsterDistance := 9999999 - targetedNormalEnemies := make([]data.Monster, 0) - targetedElites := make([]data.Monster, 0) - minDistance := 6 - minDistanceForElites := 20 // This will make the character to kill elites even if they are far away, ONLY during leveling - stuck := ctx.PathFinder.DistanceFromMe(previousIterationPosition) < 5 // Detect if character was not able to move from last iteration - for _, m := range ctx.Data.Monsters.Enemies() { - // Skip if monster is already dead - if m.Stats[stat.Life] <= 0 { - continue - } - - dist := ctx.PathFinder.DistanceFromMe(m.Position) - appended := false - if m.IsElite() && dist <= minDistanceForElites { - targetedElites = append(targetedElites, m) - appended = true - } - - if dist <= minDistance { - targetedNormalEnemies = append(targetedNormalEnemies, m) - appended = true - } - - if appended { - if dist < closestMonsterDistance { - closestMonsterDistance = dist - closestMonster = m - } - } - } - - if len(targetedNormalEnemies) > 5 || len(targetedElites) > 0 || (stuck && (len(targetedNormalEnemies) > 0 || len(targetedElites) > 0)) || (pather.IsNarrowMap(ctx.Data.PlayerUnit.Area) && (len(targetedNormalEnemies) > 0 || len(targetedElites) > 0)) { - if stuck { - ctx.Logger.Info("Character stuck and monsters detected, trying to kill monsters around") - } else { - ctx.Logger.Info(fmt.Sprintf("At least %d monsters detected close to the character, targeting closest one: %d", len(targetedNormalEnemies)+len(targetedElites), closestMonster.Name)) - } - - path, _, mPathFound := ctx.PathFinder.GetPath(closestMonster.Position) - if mPathFound { - doorIsBlocking := false - for _, o := range ctx.Data.Objects { - if o.IsDoor() && o.Selectable && path.Intersects(*ctx.Data, o.Position, 4) { - ctx.Logger.Debug("Door is blocking the path to the monster, skipping attack sequence") - doorIsBlocking = true - } - } - - if !doorIsBlocking { - ctx.Char.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - return closestMonster.UnitID, true - }, nil) - } - } - } - - // Continue moving - WaitForAllMembersWhenLeveling() - previousIterationPosition = ctx.Data.PlayerUnit.Position - err := step.MoveTo(to) - if err != nil { - return err - } - } -} diff --git a/internal/v2/action/recover_corpse.go b/internal/v2/action/recover_corpse.go deleted file mode 100644 index 9c1b0f31..00000000 --- a/internal/v2/action/recover_corpse.go +++ /dev/null @@ -1,23 +0,0 @@ -package action - -import ( - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" -) - -func RecoverCorpse() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "RecoverCorpse" - - if ctx.Data.Corpse.Found { - ctx.Logger.Info("Corpse found, let's recover our stuff...") - x, y := utils.GameCoordsToScreenCords( - ctx.Data.Corpse.Position.X, - ctx.Data.Corpse.Position.Y, - ) - ctx.HID.Click(game.LeftButton, x, y) - } - - return nil -} diff --git a/internal/v2/action/repair.go b/internal/v2/action/repair.go deleted file mode 100644 index 5ab9afff..00000000 --- a/internal/v2/action/repair.go +++ /dev/null @@ -1,97 +0,0 @@ -package action - -import ( - "fmt" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/town" - "github.com/hectorgimenez/koolo/internal/ui" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/lxn/win" -) - -func Repair() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "Repair" - - for _, i := range ctx.Data.Inventory.ByLocation(item.LocationEquipped) { - // Get the durability stats - durability, found := i.FindStat(stat.Durability, 0) - maxDurability, maxDurabilityFound := i.FindStat(stat.MaxDurability, 0) - - // Calculate Durability percent - durabilityPercent := -1 - - if maxDurabilityFound && found { - durabilityPercent = int((float64(durability.Value) / float64(maxDurability.Value)) * 100) - } - - // Restructured conditionals for when to attempt repair - if (maxDurabilityFound && !found) || - (durabilityPercent != -1 && found && durabilityPercent <= 20) || - (found && durabilityPercent == -1 && durability.Value <= 2) { - - ctx.Logger.Info(fmt.Sprintf("Repairing %s, item durability is %d percent", i.Name, durabilityPercent)) - - // Get the repair NPC for the town - repairNPC := town.GetTownByArea(ctx.Data.PlayerUnit.Area).RepairNPC() - - // Act3 repair NPC handling - if repairNPC == npc.Hratli { - MoveToCoords(data.Position{X: 5224, Y: 5045}) - } - - if repairNPC != npc.Halbu { - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - } else { - ctx.HID.KeySequence(win.VK_HOME, win.VK_RETURN) - } - - helper.Sleep(100) - if ctx.Data.LegacyGraphics { - ctx.HID.Click(game.LeftButton, ui.RepairButtonXClassic, ui.RepairButtonYClassic) - } else { - ctx.HID.Click(game.LeftButton, ui.RepairButtonX, ui.RepairButtonY) - } - helper.Sleep(500) - - return step.CloseAllMenus() - } - } - - return nil -} - -func RepairRequired() bool { - ctx := context.Get() - ctx.ContextDebug.LastStep = "RepairRequired" - - for _, i := range ctx.Data.Inventory.ByLocation(item.LocationEquipped) { - currentDurability, currentDurabilityFound := i.FindStat(stat.Durability, 0) - maxDurability, maxDurabilityFound := i.FindStat(stat.MaxDurability, 0) - - durabilityPercent := -1 - - if maxDurabilityFound && currentDurabilityFound { - durabilityPercent = int((float64(currentDurability.Value) / float64(maxDurability.Value)) * 100) - } - - // If we don't find the stats just continue - if !currentDurabilityFound && !maxDurabilityFound { - continue - } - - // Let's check if the item requires repair plus a few fail-safes - if maxDurabilityFound && !currentDurabilityFound || durabilityPercent != -1 && currentDurabilityFound && durabilityPercent <= 20 || currentDurabilityFound && currentDurability.Value <= 5 { - return true - } - } - - return false -} diff --git a/internal/v2/action/revive_merc.go b/internal/v2/action/revive_merc.go deleted file mode 100644 index 92d5e76c..00000000 --- a/internal/v2/action/revive_merc.go +++ /dev/null @@ -1,34 +0,0 @@ -package action - -import ( - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/difficulty" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/town" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/lxn/win" -) - -func ReviveMerc() { - ctx := context.Get() - ctx.ContextDebug.LastAction = "ReviveMerc" - - _, isLevelingChar := ctx.Char.(context.LevelingCharacter) - if ctx.CharacterCfg.Character.UseMerc && ctx.Data.MercHPPercent() <= 0 { - if isLevelingChar && ctx.Data.PlayerUnit.Area == area.RogueEncampment && ctx.CharacterCfg.Game.Difficulty == difficulty.Normal { - // Ignoring because merc is not hired yet - return - } - - ctx.Logger.Info("Merc is dead, let's revive it!") - - mercNPC := town.GetTownByArea(ctx.Data.PlayerUnit.Area).MercContractorNPC() - InteractNPC(mercNPC) - - if mercNPC == npc.Tyrael2 { - ctx.HID.KeySequence(win.VK_END, win.VK_UP, win.VK_RETURN, win.VK_ESCAPE) - } else { - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN, win.VK_ESCAPE) - } - } -} diff --git a/internal/v2/action/stash.go b/internal/v2/action/stash.go deleted file mode 100644 index 76214382..00000000 --- a/internal/v2/action/stash.go +++ /dev/null @@ -1,267 +0,0 @@ -package action - -import ( - "fmt" - "log/slog" - "strconv" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/d2go/pkg/nip" - "github.com/hectorgimenez/koolo/internal/event" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/ui" -) - -const ( - maxGoldPerStashTab = 2500000 -) - -func Stash(forceStash bool) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "Stash" - - ctx.Logger.Debug("Checking for items to stash...") - if !isStashingRequired(forceStash) { - return nil - } - - ctx.Logger.Info("Stashing items...") - - switch ctx.Data.PlayerUnit.Area { - case area.KurastDocks: - MoveToCoords(data.Position{X: 5146, Y: 5067}) - case area.LutGholein: - MoveToCoords(data.Position{X: 5130, Y: 5086}) - } - - bank, _ := ctx.Data.Objects.FindOne(object.Bank) - InteractObject(bank, - func() bool { - return ctx.Data.OpenMenus.Stash - }, - ) - - stashGold() - orderInventoryPotions() - stashInventory(forceStash) - step.CloseAllMenus() - - return nil -} - -func orderInventoryPotions() { - ctx := context.Get() - ctx.ContextDebug.LastStep = "orderInventoryPotions" - - for _, i := range ctx.Data.Inventory.ByLocation(item.LocationInventory) { - if i.IsPotion() { - if ctx.CharacterCfg.Inventory.InventoryLock[i.Position.Y][i.Position.X] == 0 { - continue - } - - screenPos := ui.GetScreenCoordsForItem(i) - helper.Sleep(100) - ctx.HID.Click(game.RightButton, screenPos.X, screenPos.Y) - helper.Sleep(200) - } - } -} - -func isStashingRequired(firstRun bool) bool { - ctx := context.Get() - ctx.ContextDebug.LastStep = "isStashingRequired" - - for _, i := range ctx.Data.Inventory.ByLocation(item.LocationInventory) { - stashIt, _, _ := shouldStashIt(i, firstRun) - if stashIt { - return true - } - } - - isStashFull := true - for _, goldInStash := range ctx.Data.Inventory.StashedGold { - if goldInStash < maxGoldPerStashTab { - isStashFull = false - } - } - - if ctx.Data.Inventory.Gold > ctx.Data.PlayerUnit.MaxGold()/3 && !isStashFull { - return true - } - - return false -} - -func stashGold() { - ctx := context.Get() - ctx.ContextDebug.LastAction = "stashGold" - - if ctx.Data.Inventory.Gold == 0 { - return - } - - ctx.Logger.Info("Stashing gold...", slog.Int("gold", ctx.Data.Inventory.Gold)) - - for tab, goldInStash := range ctx.Data.Inventory.StashedGold { - ctx.RefreshGameData() - if ctx.Data.Inventory.Gold == 0 { - return - } - - if goldInStash < maxGoldPerStashTab { - SwitchTab(tab + 1) - clickStashGoldBtn() - helper.Sleep(500) - } - } - - ctx.Logger.Info("All stash tabs are full of gold :D") -} - -func stashInventory(firstRun bool) { - ctx := context.Get() - ctx.ContextDebug.LastAction = "stashInventory" - - currentTab := 1 - if ctx.CharacterCfg.Character.StashToShared { - currentTab = 2 - } - SwitchTab(currentTab) - - for _, i := range ctx.Data.Inventory.ByLocation(item.LocationInventory) { - stashIt, matchedRule, ruleFile := shouldStashIt(i, firstRun) - - if !stashIt { - continue - } - for currentTab < 5 { - if stashItemAction(i, matchedRule, ruleFile, firstRun) { - r, res := ctx.CharacterCfg.Runtime.Rules.EvaluateAll(i) - - if res != nip.RuleResultFullMatch && firstRun { - ctx.Logger.Info( - fmt.Sprintf("Item %s [%s] stashed because it was found in the inventory during the first run.", i.Desc().Name, i.Quality.ToString()), - ) - break - } - - ctx.Logger.Info( - fmt.Sprintf("Item %s [%s] stashed", i.Desc().Name, i.Quality.ToString()), - slog.String("nipFile", fmt.Sprintf("%s:%d", r.Filename, r.LineNumber)), - slog.String("rawRule", r.RawLine), - ) - break - } - if currentTab == 5 { - // TODO: Stop the bot, stash is full - } - ctx.Logger.Debug(fmt.Sprintf("Tab %d is full, switching to next one", currentTab)) - currentTab++ - SwitchTab(currentTab) - } - } -} - -func shouldStashIt(i data.Item, firstRun bool) (bool, string, string) { - ctx := context.Get() - ctx.ContextDebug.LastStep = "shouldStashIt" - - // Don't stash items from quests during leveling process, it makes things easier to track - if _, isLevelingChar := ctx.Char.(context.LevelingCharacter); isLevelingChar && i.IsFromQuest() { - return false, "", "" - } - - if i.IsRuneword { - return true, "runeword", "" - } - - // Don't stash the Tomes, keys and WirtsLeg - if i.Name == item.TomeOfTownPortal || i.Name == item.TomeOfIdentify || i.Name == item.Key || i.Name == "WirtsLeg" { - return false, "", "" - } - - if i.Position.Y >= len(ctx.CharacterCfg.Inventory.InventoryLock) || i.Position.X >= len(ctx.CharacterCfg.Inventory.InventoryLock[0]) { - return false, "", "" - } - - if i.Location.LocationType == item.LocationInventory && ctx.CharacterCfg.Inventory.InventoryLock[i.Position.Y][i.Position.X] == 0 || i.IsPotion() { - return false, "", "" - } - - // Let's stash everything during first run, we don't want to sell items from the user - if firstRun { - return true, "FirstRun", "" - } - - rule, res := ctx.CharacterCfg.Runtime.Rules.EvaluateAll(i) - if res == nip.RuleResultFullMatch && doesExceedQuantity(i, rule) { - return false, "", "" - } - - return true, rule.RawLine, rule.Filename + ":" + strconv.Itoa(rule.LineNumber) -} - -func stashItemAction(i data.Item, rule string, ruleFile string, firstRun bool) bool { - ctx := context.Get() - ctx.ContextDebug.LastAction = "stashItemAction" - - screenPos := ui.GetScreenCoordsForItem(i) - ctx.HID.MovePointer(screenPos.X, screenPos.Y) - helper.Sleep(170) - screenshot := ctx.GameReader.Screenshot() - helper.Sleep(150) - ctx.HID.ClickWithModifier(game.LeftButton, screenPos.X, screenPos.Y, game.CtrlKey) - helper.Sleep(500) - - // Don't log items that we already have in inventory during first run - if !firstRun { - event.Send(event.ItemStashed(event.WithScreenshot(ctx.Name, fmt.Sprintf("Item %s [%d] stashed", i.Name, i.Quality), screenshot), data.Drop{Item: i, Rule: rule, RuleFile: ruleFile})) - } - - return true -} - -func clickStashGoldBtn() { - ctx := context.Get() - ctx.ContextDebug.LastStep = "clickStashGoldBtn" - - helper.Sleep(170) - if ctx.GameReader.LegacyGraphics() { - ctx.HID.Click(game.LeftButton, ui.StashGoldBtnXClassic, ui.StashGoldBtnYClassic) - helper.Sleep(1000) - ctx.HID.Click(game.LeftButton, ui.StashGoldBtnConfirmXClassic, ui.StashGoldBtnConfirmYClassic) - } else { - ctx.HID.Click(game.LeftButton, ui.StashGoldBtnX, ui.StashGoldBtnY) - helper.Sleep(1000) - ctx.HID.Click(game.LeftButton, ui.StashGoldBtnConfirmX, ui.StashGoldBtnConfirmY) - } -} - -func SwitchTab(tab int) { - ctx := context.Get() - ctx.ContextDebug.LastStep = "SwitchTab" - - if ctx.GameReader.LegacyGraphics() { - x := ui.SwitchStashTabBtnXClassic - y := ui.SwitchStashTabBtnYClassic - - tabSize := ui.SwitchStashTabBtnTabSizeClassic - x = x + tabSize*tab - tabSize/2 - ctx.HID.Click(game.LeftButton, x, y) - helper.Sleep(500) - } else { - x := ui.SwitchStashTabBtnX - y := ui.SwitchStashTabBtnY - - tabSize := ui.SwitchStashTabBtnTabSize - x = x + tabSize*tab - tabSize/2 - ctx.HID.Click(game.LeftButton, x, y) - helper.Sleep(500) - } -} diff --git a/internal/v2/action/step/attack.go b/internal/v2/action/step/attack.go deleted file mode 100644 index ddac431d..00000000 --- a/internal/v2/action/step/attack.go +++ /dev/null @@ -1,174 +0,0 @@ -package step - -import ( - "log/slog" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/pather" -) - -const attackCycleDuration = 120 * time.Millisecond - -type attackSettings struct { - primaryAttack bool - skill skill.ID - followEnemy bool - minDistance int - maxDistance int - aura skill.ID - target data.UnitID - shouldStandStill bool - numOfAttacks int -} -type AttackOption func(step *attackSettings) - -func Distance(minimum, maximum int) AttackOption { - return func(step *attackSettings) { - step.followEnemy = true - step.minDistance = minimum - step.maxDistance = maximum - } -} - -func EnsureAura(aura skill.ID) AttackOption { - return func(step *attackSettings) { - step.aura = aura - } -} - -func PrimaryAttack(target data.UnitID, numOfAttacks int, standStill bool, opts ...AttackOption) error { - settings := attackSettings{ - target: target, - numOfAttacks: numOfAttacks, - shouldStandStill: standStill, - primaryAttack: true, - } - for _, o := range opts { - o(&settings) - } - - return attack(settings) -} - -func SecondaryAttack(skill skill.ID, target data.UnitID, numOfAttacks int, opts ...AttackOption) error { - settings := attackSettings{ - target: target, - numOfAttacks: numOfAttacks, - skill: skill, - } - for _, o := range opts { - o(&settings) - } - - return attack(settings) -} - -func attack(settings attackSettings) error { - ctx := context.Get() - ctx.ContextDebug.LastStep = "Attack" - - numOfAttacksRemaining := settings.numOfAttacks - aoe := settings.target == 0 - lastRun := time.Time{} - - for { - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - monster, found := ctx.Data.Monsters.FindByID(settings.target) - if !found || monster.Stats[stat.Life] <= 0 || numOfAttacksRemaining <= 0 { - return nil - } - - if !aoe { - // Move into the attack distance range before starting - if settings.followEnemy { - if !ensureEnemyIsInRange(monster, settings.maxDistance, settings.minDistance) { - // We cannot reach the enemy, let's skip the attack sequence - ctx.Logger.Info("Enemy is out of range and can not be reached", slog.Any("monster", monster.Name)) - return nil - } - } else { - // Since we are not following the enemy, and it's not in range, we can't attack it - _, distance, found := ctx.PathFinder.GetPath(monster.Position) - if !found || distance > settings.maxDistance { - return nil - } - } - } - - // If we are not using the primary attack, we need to ensure the right skill is selected - if !settings.primaryAttack && ctx.Data.PlayerUnit.RightSkill != settings.skill { - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.MustKBForSkill(settings.skill)) - time.Sleep(time.Millisecond * 80) - } - - // If we have an aura, let's ensure it's active - if settings.aura != 0 && lastRun.IsZero() { - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.MustKBForSkill(settings.aura)) - } - - if time.Since(lastRun) > ctx.Data.PlayerCastDuration()-attackCycleDuration && numOfAttacksRemaining > 0 { - if settings.shouldStandStill { - ctx.HID.KeyDown(ctx.Data.KeyBindings.StandStill) - } - x, y := ctx.PathFinder.GameCoordsToScreenCords(monster.Position.X, monster.Position.Y) - - if settings.primaryAttack { - ctx.HID.Click(game.LeftButton, x, y) - } else { - ctx.HID.Click(game.RightButton, x, y) - } - if settings.shouldStandStill { - ctx.HID.KeyUp(ctx.Data.KeyBindings.StandStill) - } - lastRun = time.Now() - numOfAttacksRemaining-- - } - } -} -func ensureEnemyIsInRange(monster data.Monster, maxDistance, minDistance int) bool { - ctx := context.Get() - ctx.ContextDebug.LastStep = "ensureEnemyIsInRange" - - path, distance, found := ctx.PathFinder.GetPath(monster.Position) - - // We cannot reach the enemy, let's skip the attack sequence - if !found { - return false - } - - hasLoS := ctx.PathFinder.LineOfSight(ctx.Data.PlayerUnit.Position, monster.Position) - - if distance > maxDistance || !hasLoS { - if distance > minDistance && minDistance > 0 { - moveTo := minDistance - 1 - if len(path) < minDistance { - moveTo = len(path) - 1 // Ensure moveTo is within path bounds - } - - for i := moveTo; i > 0; i-- { - posTile := path[i].(*pather.Tile) - pos := data.Position{ - X: posTile.X + ctx.Data.AreaOrigin.X, - Y: posTile.Y + ctx.Data.AreaOrigin.Y, - } - - hasLoS = ctx.PathFinder.LineOfSight(pos, monster.Position) - if hasLoS { - path, distance, _ = ctx.PathFinder.GetPath(pos) - _ = MoveTo(pos) - } - } - } - } - - return true -} diff --git a/internal/v2/action/step/close_all_menus.go b/internal/v2/action/step/close_all_menus.go deleted file mode 100644 index 0a84eecd..00000000 --- a/internal/v2/action/step/close_all_menus.go +++ /dev/null @@ -1,32 +0,0 @@ -package step - -import ( - "errors" - - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/lxn/win" -) - -func CloseAllMenus() error { - ctx := context.Get() - ctx.ContextDebug.LastStep = "CloseAllMenus" - - attempts := 0 - for ctx.Data.OpenMenus.IsMenuOpen() { - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - ctx.RefreshGameData() - if attempts > 10 { - return errors.New("failed closing game menu") - } - ctx.HID.PressKey(win.VK_ESCAPE) - helper.Sleep(200) - attempts++ - } - - return nil -} diff --git a/internal/v2/action/step/interact_entrance.go b/internal/v2/action/step/interact_entrance.go deleted file mode 100644 index 1ce130e5..00000000 --- a/internal/v2/action/step/interact_entrance.go +++ /dev/null @@ -1,71 +0,0 @@ -package step - -import ( - "errors" - "fmt" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/context" - - "github.com/hectorgimenez/d2go/pkg/data/area" -) - -func InteractEntrance(area area.ID) error { - maxInteractionAttempts := 5 - interactionAttempts := 0 - waitingForInteraction := false - currentMouseCoords := data.Position{} - lastRun := time.Time{} - - ctx := context.Get() - ctx.ContextDebug.LastStep = "InteractEntrance" - - for { - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - // Give some extra time to render the UI - if ctx.Data.PlayerUnit.Area == area && time.Since(lastRun) > time.Millisecond*500 { - return nil - } - - if interactionAttempts > maxInteractionAttempts { - return fmt.Errorf("area %s [%d] could not be interacted", area.Area().Name, area) - } - - if (waitingForInteraction && time.Since(lastRun) < time.Millisecond*500) || ctx.Data.PlayerUnit.Area == area { - continue - } - - lastRun = time.Now() - for _, l := range ctx.Data.AdjacentLevels { - if l.Area == area { - distance := ctx.PathFinder.DistanceFromMe(l.Position) - if distance > 10 { - return errors.New("entrance too far away") - } - - if l.IsEntrance { - lx, ly := ctx.PathFinder.GameCoordsToScreenCords(l.Position.X-2, l.Position.Y-2) - if ctx.Data.HoverData.UnitType == 5 || ctx.Data.HoverData.UnitType == 2 && ctx.Data.HoverData.IsHovered { - ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) - waitingForInteraction = true - } - - x, y := helper.Spiral(interactionAttempts) - currentMouseCoords = data.Position{X: lx + x, Y: ly + y} - ctx.HID.MovePointer(lx+x, ly+y) - interactionAttempts++ - continue - } - - return fmt.Errorf("area %s [%d] is not an entrance", area.Area().Name, area) - } - } - } -} diff --git a/internal/v2/action/step/interact_npc.go b/internal/v2/action/step/interact_npc.go deleted file mode 100644 index 0839f23f..00000000 --- a/internal/v2/action/step/interact_npc.go +++ /dev/null @@ -1,71 +0,0 @@ -package step - -import ( - "errors" - "fmt" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" -) - -func InteractNPC(npcID npc.ID) error { - maxInteractionAttempts := 5 - interactionAttempts := 0 - waitingForInteraction := false - currentMouseCoords := data.Position{} - lastRun := time.Time{} - - ctx := context.Get() - ctx.ContextDebug.LastStep = "InteractNPC" - - for { - ctx.RefreshGameData() - - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - if ctx.Data.OpenMenus.NPCInteract { - return nil - } - - if interactionAttempts >= maxInteractionAttempts { - return errors.New("failed interacting with NPC") - } - - // Give some time before retrying the interaction - if waitingForInteraction && time.Since(lastRun) < time.Second { - continue - } - - lastRun = time.Now() - m, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone) - if found { - if m.IsHovered { - ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) - waitingForInteraction = true - interactionAttempts++ - continue - } - - distance := ctx.PathFinder.DistanceFromMe(m.Position) - if distance > 15 { - return fmt.Errorf("NPC is too far away: %d. Current distance: %d", npcID, distance) - } - - x, y := utils.GameCoordsToScreenCords(m.Position.X, m.Position.Y) - // Act 4 Tyrael has a super weird hitbox - if npcID == npc.Tyrael2 { - y = y - 40 - } - currentMouseCoords = data.Position{X: x, Y: y} - ctx.HID.MovePointer(x, y) - interactionAttempts++ - } - } -} diff --git a/internal/v2/action/step/interact_object.go b/internal/v2/action/step/interact_object.go deleted file mode 100644 index 806fc7d4..00000000 --- a/internal/v2/action/step/interact_object.go +++ /dev/null @@ -1,91 +0,0 @@ -package step - -import ( - "errors" - "fmt" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" -) - -func InteractObject(obj data.Object, isCompletedFn func() bool) error { - maxInteractionAttempts := 5 - interactionAttempts := 0 - waitingForInteraction := false - currentMouseCoords := data.Position{} - lastRun := time.Time{} - - // If there is no check, just assume the interaction is completed after clicking - if isCompletedFn == nil { - isCompletedFn = func() bool { - return waitingForInteraction - } - } - - ctx := context.Get() - ctx.ContextDebug.LastStep = "InteractObject" - - for !isCompletedFn() { - ctx.RefreshGameData() - - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - if interactionAttempts >= maxInteractionAttempts { - return errors.New("failed interacting with object") - } - - // Give some time before retrying the interaction - if waitingForInteraction && time.Since(lastRun) < time.Millisecond*500 { - continue - } - - var o data.Object - var found bool - if obj.ID != 0 { - o, found = ctx.Data.Objects.FindByID(obj.ID) - if !found { - return fmt.Errorf("object %v not found", obj) - } - } else { - o, found = ctx.Data.Objects.FindOne(obj.Name) - if !found { - return fmt.Errorf("object %v not found", obj) - } - } - - lastRun = time.Now() - if o.IsHovered { - ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) - waitingForInteraction = true - interactionAttempts++ - continue - } else { - objectX := o.Position.X - 2 - objectY := o.Position.Y - 2 - distance := ctx.PathFinder.DistanceFromMe(o.Position) - if distance > 15 { - return fmt.Errorf("object is too far away: %d. Current distance: %d", o.Name, distance) - } - - mX, mY := utils.GameCoordsToScreenCords(objectX, objectY) - // In order to avoid the spiral (super slow and shitty) let's try to point the mouse to the top of the portal directly - if interactionAttempts == 2 && o.Name == object.TownPortal { - mX, mY = utils.GameCoordsToScreenCords(objectX-4, objectY-4) - } - - x, y := utils.Spiral(interactionAttempts) - currentMouseCoords = data.Position{X: mX + x, Y: mY + y} - ctx.HID.MovePointer(mX+x, mY+y) - interactionAttempts++ - } - } - - return nil -} diff --git a/internal/v2/action/step/move.go b/internal/v2/action/step/move.go deleted file mode 100644 index c80846f5..00000000 --- a/internal/v2/action/step/move.go +++ /dev/null @@ -1,96 +0,0 @@ -package step - -import ( - "errors" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func MoveTo(dest data.Position) error { - ctx := context.Get() - ctx.ContextDebug.LastStep = "MoveTo" - - timeout := time.Second * 30 - stopAtDistance := 7 - - // Press the Teleport keybinding if it's available, otherwise use vigor (if available) - if ctx.Data.CanTeleport() { - if ctx.Data.PlayerUnit.RightSkill != skill.Teleport { - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.MustKBForSkill(skill.Teleport)) - } - } else if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.Vigor); found { - if ctx.Data.PlayerUnit.RightSkill != skill.Vigor { - ctx.HID.PressKeyBinding(kb) - } - } - - startedAt := time.Now() - lastRun := time.Time{} - - for { - ctx.RefreshGameData() - - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - distance := ctx.PathFinder.DistanceFromMe(dest) - if distance <= stopAtDistance { - // In case distance is lower, we double-check with the pathfinder and the full path instead of euclidean distance - _, distance, found := ctx.PathFinder.GetPath(dest) - if !found || distance <= stopAtDistance { - return nil - } - } - - if timeout > 0 && time.Since(startedAt) > timeout { - return nil - } - - // Add some delay between clicks to let the character move to destination - walkDuration := helper.RandomDurationMs(600, 1200) - if !ctx.Data.CanTeleport() && time.Since(lastRun) < walkDuration { - continue - } - - if ctx.Data.CanTeleport() && time.Since(lastRun) < ctx.Data.PlayerCastDuration() { - continue - } - - // TODO Implement stuck & cache? - - path, _, found := ctx.PathFinder.GetClosestWalkablePath(dest) - if !found { - if ctx.PathFinder.DistanceFromMe(dest) < stopAtDistance+5 { - return nil - } - - return errors.New("path could not be calculated, maybe there is an obstacle or a flying platform (arcane sanctuary)") - } - lastRun = time.Now() - if len(path) == 0 { - return nil - } - //lastRunPositions = append(m.lastRunPositions, d.PlayerUnit.Position) - ctx.PathFinder.MoveThroughPath(path, calculateMaxDistance(ctx.Data, walkDuration)) - } -} - -func calculateMaxDistance(d *game.Data, duration time.Duration) int { - // We don't care too much if teleport is available, we can ignore corners, 90 degrees turns, etc - if d.CanTeleport() { - return 25 - } - - // Calculate the distance we can walk in the given duration, based on the randomized time - proposedDistance := int(float64(25) * duration.Seconds()) - realDistance := proposedDistance - - return realDistance -} diff --git a/internal/v2/action/step/open_portal.go b/internal/v2/action/step/open_portal.go deleted file mode 100644 index 86ad958f..00000000 --- a/internal/v2/action/step/open_portal.go +++ /dev/null @@ -1,40 +0,0 @@ -package step - -import ( - "time" - - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - - "github.com/hectorgimenez/koolo/internal/helper" -) - -func OpenPortal() error { - ctx := context.Get() - ctx.ContextDebug.LastStep = "OpenPortal" - - lastRun := time.Time{} - for { - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - _, found := ctx.Data.Objects.FindOne(object.TownPortal) - if found { - return nil - } - - // Give some time to portal to popup before retrying... - if time.Since(lastRun) < time.Second*2 { - continue - } - - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.MustKBForSkill(skill.TomeOfTownPortal)) - helper.Sleep(250) - ctx.HID.Click(game.RightButton, 300, 300) - lastRun = time.Now() - } -} diff --git a/internal/v2/action/step/pickup_item.go b/internal/v2/action/step/pickup_item.go deleted file mode 100644 index ea79e2ba..00000000 --- a/internal/v2/action/step/pickup_item.go +++ /dev/null @@ -1,98 +0,0 @@ -package step - -import ( - "fmt" - "log/slog" - "time" - - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/koolo/internal/helper" -) - -const maxInteractions = 45 - -func PickupItem(it data.Item) error { - ctx := context.Get() - ctx.ContextDebug.LastStep = "PickupItem" - - ctx.Logger.Debug(fmt.Sprintf("Picking up: %s [%s]", it.Desc().Name, it.Quality.ToString())) - - waitingForInteraction := time.Time{} - mouseOverAttempts := 0 - currentMouseCoords := data.Position{} - lastRun := time.Now() - - for { - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - for _, i := range ctx.Data.Inventory.AllItems { - if i.UnitID == it.UnitID { - it = i - } - } - - if it.Location.LocationType != item.LocationGround { - return nil - } - - if mouseOverAttempts > maxInteractions || !waitingForInteraction.IsZero() && time.Since(waitingForInteraction) > time.Second*3 { - return fmt.Errorf("item %s [%s] could not be picked up", it.Desc().Name, it.Quality.ToString()) - } - - if time.Since(lastRun) < utils.RandomDurationMs(120, 320) { - continue - } - - if !waitingForInteraction.IsZero() && time.Since(lastRun) < time.Second { - continue - } - - lastRun = time.Now() - objectX := it.Position.X - 1 - objectY := it.Position.Y - 1 - mX, mY := utils.GameCoordsToScreenCords(objectX, objectY) - - if it.IsHovered { - ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) - if waitingForInteraction.IsZero() { - waitingForInteraction = time.Now() - } - continue - } else { - // Sometimes we got stuck because mouse is hovering a chest and item is in behind, it usually happens a lot - // on Andariel, so we open it - if isChestHovered() { - ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) - } - - distance := ctx.PathFinder.DistanceFromMe(it.Position) - if distance > 10 { - ctx.Logger.Info("item is too far away", slog.String("item", it.Desc().Name)) - return fmt.Errorf("item is too far away: %s", it.Desc().Name) - } - - x, y := helper.Spiral(mouseOverAttempts) - currentMouseCoords = data.Position{X: mX + x, Y: mY + y} - ctx.HID.MovePointer(mX+x, mY+y) - mouseOverAttempts++ - } - } -} - -func isChestHovered() bool { - for _, o := range context.Get().Data.Objects { - if o.IsChest() && o.IsHovered { - return true - } - } - - return false -} diff --git a/internal/v2/action/step/set_skill.go b/internal/v2/action/step/set_skill.go deleted file mode 100644 index a83661f4..00000000 --- a/internal/v2/action/step/set_skill.go +++ /dev/null @@ -1,17 +0,0 @@ -package step - -import ( - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func SetSkill(id skill.ID) { - ctx := context.Get() - ctx.ContextDebug.LastStep = "SetSkill" - - if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(id); found { - if ctx.Data.PlayerUnit.RightSkill != id { - ctx.HID.PressKeyBinding(kb) - } - } -} diff --git a/internal/v2/action/step/swap_weapon.go b/internal/v2/action/step/swap_weapon.go deleted file mode 100644 index b2d350c8..00000000 --- a/internal/v2/action/step/swap_weapon.go +++ /dev/null @@ -1,44 +0,0 @@ -package step - -import ( - "time" - - "github.com/hectorgimenez/koolo/internal/v2/context" - - "github.com/hectorgimenez/d2go/pkg/data/skill" -) - -func SwapToMainWeapon() error { - return swapWeapon(false) -} - -func SwapToCTA() error { - return swapWeapon(true) -} - -func swapWeapon(toCTA bool) error { - lastRun := time.Time{} - - ctx := context.Get() - ctx.ContextDebug.LastStep = "SwapToCTA" - - for { - // Pause the execution if the priority is not the same as the execution priority - if ctx.ExecutionPriority != ctx.Priority { - continue - } - - if time.Since(lastRun) < time.Second { - continue - } - - _, found := ctx.Data.PlayerUnit.Skills[skill.BattleOrders] - if (toCTA && found) || (!toCTA && !found) { - return nil - } - - ctx.HID.PressKeyBinding(ctx.Data.KeyBindings.SwapWeapons) - - lastRun = time.Now() - } -} diff --git a/internal/v2/action/tools.go b/internal/v2/action/tools.go deleted file mode 100644 index eebb8de7..00000000 --- a/internal/v2/action/tools.go +++ /dev/null @@ -1,49 +0,0 @@ -package action - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func OpenTPIfLeader() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "OpenTPIfLeader" - - isLeader := ctx.CharacterCfg.Companion.Enabled && ctx.CharacterCfg.Companion.Leader - - if isLeader { - return step.OpenPortal() - } - - return nil -} - -func isMonsterSealElite(monster data.Monster) bool { - if monster.Type == data.MonsterTypeSuperUnique && (monster.Name == npc.OblivionKnight || monster.Name == npc.VenomLord || monster.Name == npc.StormCaster) { - return true - } - - return false -} - -func PostRun(isLastRun bool) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "PostRun" - - // For companions, we don't need them to do anything, just follow the leader - if ctx.CharacterCfg.Companion.Enabled && !ctx.CharacterCfg.Companion.Leader { - return nil - } - - ClearAreaAroundPlayer(5, data.MonsterAnyFilter()) - ItemPickup(-1) - - // Don't return town on last run - if !isLastRun { - return ReturnTown() - } - - return nil -} diff --git a/internal/v2/action/town.go b/internal/v2/action/town.go deleted file mode 100644 index b190bcd2..00000000 --- a/internal/v2/action/town.go +++ /dev/null @@ -1,73 +0,0 @@ -package action - -import ( - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func PreRun(firstRun bool) error { - ctx := context.Get() - - DropMouseItem() - step.SetSkill(skill.Vigor) - RecoverCorpse() - - if firstRun { - Stash(firstRun) - } - - UpdateQuestLog() - IdentifyAll(firstRun) - VendorRefill(false, true) - Stash(firstRun) - Gamble() - Stash(false) - CubeRecipes() - - if ctx.CharacterCfg.Game.Leveling.EnsurePointsAllocation { - ResetStats() - EnsureStatPoints() - EnsureSkillPoints() - } - - if ctx.CharacterCfg.Game.Leveling.EnsureKeyBinding { - EnsureSkillBindings() - } - - HealAtNPC() - ReviveMerc() - HireMerc() - - return Repair() -} - -func InRunReturnTownRoutine() error { - ctx := context.Get() - - ReturnTown() - step.SetSkill(skill.Vigor) - RecoverCorpse() - IdentifyAll(false) - VendorRefill(false, true) - Stash(false) - Gamble() - Stash(false) - CubeRecipes() - - if ctx.CharacterCfg.Game.Leveling.EnsurePointsAllocation { - EnsureStatPoints() - EnsureSkillPoints() - } - - if ctx.CharacterCfg.Game.Leveling.EnsureKeyBinding { - EnsureSkillBindings() - } - - HealAtNPC() - ReviveMerc() - HireMerc() - Repair() - - return UsePortalInTown() -} diff --git a/internal/v2/action/tp_actions.go b/internal/v2/action/tp_actions.go deleted file mode 100644 index ac14422d..00000000 --- a/internal/v2/action/tp_actions.go +++ /dev/null @@ -1,64 +0,0 @@ -package action - -import ( - "errors" - - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/koolo/internal/helper" - "github.com/hectorgimenez/koolo/internal/town" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func ReturnTown() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "ReturnTown" - - if ctx.Data.PlayerUnit.Area.IsTown() { - return nil - } - - _ = step.OpenPortal() - tp, found := ctx.Data.Objects.FindOne(object.TownPortal) - if !found { - return errors.New("town portal not found") - } - - return InteractObject(tp, func() bool { - return ctx.Data.PlayerUnit.Area.IsTown() - }) -} - -func UsePortalInTown() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "UsePortalInTown" - - tpArea := town.GetTownByArea(ctx.Data.PlayerUnit.Area).TPWaitingArea(*ctx.Data) - _ = MoveToCoords(tpArea) - - return UsePortalFrom(ctx.Data.PlayerUnit.Name) -} - -func UsePortalFrom(owner string) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "UsePortalFrom" - - if !ctx.Data.PlayerUnit.Area.IsTown() { - return nil - } - - for _, obj := range ctx.Data.Objects { - if obj.IsPortal() && obj.Owner == owner { - return InteractObjectByID(obj.ID, func() bool { - if !ctx.Data.PlayerUnit.Area.IsTown() { - helper.Sleep(500) - return true - } - - return false - }) - } - } - - return nil -} diff --git a/internal/v2/action/vendor.go b/internal/v2/action/vendor.go deleted file mode 100644 index c85e1d40..00000000 --- a/internal/v2/action/vendor.go +++ /dev/null @@ -1,106 +0,0 @@ -package action - -import ( - "log/slog" - - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/lxn/win" - - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/town" -) - -func VendorRefill(forceRefill, sellJunk bool) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "VendorRefill" - - if !forceRefill && !shouldVisitVendor() { - return nil - } - - ctx.Logger.Info("Visiting vendor...", slog.Bool("forceRefill", forceRefill)) - - vendorNPC := town.GetTownByArea(ctx.Data.PlayerUnit.Area).RefillNPC() - if vendorNPC == npc.Drognan { - _, needsBuy := town.ShouldBuyKeys() - if needsBuy { - vendorNPC = npc.Lysander - } - } - err := InteractNPC(vendorNPC) - if err != nil { - return err - } - - // Jamella trade button is the first one - if vendorNPC == npc.Jamella { - ctx.HID.KeySequence(win.VK_HOME, win.VK_RETURN) - } else { - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - } - - SwitchTab(4) - ctx.RefreshGameData() - town.BuyConsumables(forceRefill) - - if sellJunk { - town.SellJunk() - } - - return step.CloseAllMenus() -} - -func BuyAtVendor(vendor npc.ID, items ...VendorItemRequest) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "BuyAtVendor" - - err := InteractNPC(vendor) - if err != nil { - return err - } - - // Jamella trade button is the first one - if vendor == npc.Jamella { - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - } else { - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - } - - for _, i := range items { - SwitchTab(i.Tab) - itm, found := ctx.Data.Inventory.Find(i.Item, item.LocationVendor) - if found { - town.BuyItem(itm, i.Quantity) - } else { - ctx.Logger.Warn("Item not found in vendor", slog.String("Item", string(i.Item))) - } - } - - return step.CloseAllMenus() -} - -type VendorItemRequest struct { - Item item.Name - Quantity int - Tab int // At this point I have no idea how to detect the Tab the Item is in the vendor (1-4) -} - -func shouldVisitVendor() bool { - ctx := context.Get() - ctx.ContextDebug.LastStep = "shouldVisitVendor" - - // Check if we should sell junk - if len(town.ItemsToBeSold()) > 0 { - return true - } - - // Skip the vendor if we don't have enough gold to do anything... this is not the optimal scenario, - // but I have no idea how to check vendor Item prices. - if ctx.Data.PlayerUnit.TotalPlayerGold() < 1000 { - return false - } - - return ctx.BeltManager.ShouldBuyPotions() || town.ShouldBuyTPs() || town.ShouldBuyIDs() -} diff --git a/internal/v2/action/waypoint.go b/internal/v2/action/waypoint.go deleted file mode 100644 index fbb591d7..00000000 --- a/internal/v2/action/waypoint.go +++ /dev/null @@ -1,117 +0,0 @@ -package action - -import ( - "fmt" - "log/slog" - "slices" - - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/ui" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" -) - -func WayPoint(dest area.ID) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "WayPoint" - - // TODO Check if we are in town, otherwise go back town - - if ctx.Data.PlayerUnit.Area == dest { - return nil - } - - wpCoords, found := area.WPAddresses[dest] - if !found { - return fmt.Errorf("area destination %s is not mapped to a WayPoint (waypoint.go)", area.Areas[dest].Name) - } - - for _, o := range ctx.Data.Objects { - if o.IsWaypoint() { - err := InteractObject(o, func() bool { - return ctx.Data.OpenMenus.Waypoint - }) - if err != nil { - return err - } - - if ctx.Data.LegacyGraphics { - actTabX := ui.WpTabStartXClassic + (wpCoords.Tab-1)*ui.WpTabSizeXClassic + (ui.WpTabSizeXClassic / 2) - ctx.HID.Click(game.LeftButton, actTabX, ui.WpTabStartYClassic) - } else { - actTabX := ui.WpTabStartX + (wpCoords.Tab-1)*ui.WpTabSizeX + (ui.WpTabSizeX / 2) - ctx.HID.Click(game.LeftButton, actTabX, ui.WpTabStartY) - } - utils.Sleep(200) - } - } - - return useWP(dest) -} - -func useWP(dest area.ID) error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "useWP" - - finalDestination := dest - traverseAreas := make([]area.ID, 0) - currentWP := area.WPAddresses[dest] - if !slices.Contains(ctx.Data.PlayerUnit.AvailableWaypoints, dest) { - for { - traverseAreas = append(currentWP.LinkedFrom, traverseAreas...) - - if currentWP.LinkedFrom != nil { - dest = currentWP.LinkedFrom[0] - } - - currentWP = area.WPAddresses[currentWP.LinkedFrom[0]] - - if slices.Contains(ctx.Data.PlayerUnit.AvailableWaypoints, dest) { - break - } - } - } - - currentWP = area.WPAddresses[dest] - - // First use the previous available waypoint that we have discovered - if ctx.Data.LegacyGraphics { - areaBtnY := ui.WpListStartYClassic + (currentWP.Row-1)*ui.WpAreaBtnHeightClassic + (ui.WpAreaBtnHeightClassic / 2) - ctx.HID.Click(game.LeftButton, ui.WpListPositionXClassic, areaBtnY) - } else { - areaBtnY := ui.WpListStartY + (currentWP.Row-1)*ui.WpAreaBtnHeight + (ui.WpAreaBtnHeight / 2) - ctx.HID.Click(game.LeftButton, ui.WpListPositionX, areaBtnY) - } - utils.Sleep(1000) - - // We have the WP discovered, just use it - if len(traverseAreas) == 0 { - return nil - } - - traverseAreas = append(traverseAreas, finalDestination) - - // Next keep traversing all the areas from the previous available waypoint until we reach the destination, trying to discover WPs during the way - ctx.Logger.Info("Traversing areas to reach destination", slog.Any("areas", traverseAreas)) - - for i, dst := range traverseAreas { - if !dst.IsTown() { - Buff() - } - - if i > 0 { - err := MoveToArea(dst) - if err != nil { - return err - } - - err = DiscoverWaypoint() - if err != nil { - return err - } - } - } - - return nil -} diff --git a/internal/v2/action/waypoint_discover.go b/internal/v2/action/waypoint_discover.go deleted file mode 100644 index 7395434c..00000000 --- a/internal/v2/action/waypoint_discover.go +++ /dev/null @@ -1,31 +0,0 @@ -package action - -import ( - "log/slog" - - "github.com/hectorgimenez/koolo/internal/v2/action/step" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func DiscoverWaypoint() error { - ctx := context.Get() - ctx.ContextDebug.LastAction = "DiscoverWaypoint" - - ctx.Logger.Info("Trying to autodiscover Waypoint for current area", slog.String("area", ctx.Data.PlayerUnit.Area.Area().Name)) - for _, o := range ctx.Data.Objects { - if o.IsWaypoint() { - err := InteractObject(o, func() bool { - return ctx.Data.OpenMenus.Waypoint - }) - if err != nil { - return err - } - - ctx.Logger.Info("Waypoint discovered", slog.String("area", ctx.Data.PlayerUnit.Area.Area().Name)) - step.CloseAllMenus() - } - } - - ctx.Logger.Info("Waypoint not found :(", slog.String("area", ctx.Data.PlayerUnit.Area.Area().Name)) - return nil -} diff --git a/internal/v2/bot/bot.go b/internal/v2/bot/bot.go deleted file mode 100644 index a4ea1c97..00000000 --- a/internal/v2/bot/bot.go +++ /dev/null @@ -1,112 +0,0 @@ -package bot - -import ( - "context" - "time" - - "github.com/hectorgimenez/koolo/internal/event" - "github.com/hectorgimenez/koolo/internal/v2/action" - botCtx "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/run" - "golang.org/x/sync/errgroup" -) - -type Bot struct { - ctx *botCtx.Context -} - -func NewBot(ctx *botCtx.Context) *Bot { - return &Bot{ - ctx: ctx, - } -} - -func (b *Bot) Run(ctx context.Context, firstRun bool, runs []run.Run) error { - ctx, cancel := context.WithCancel(ctx) - g, ctx := errgroup.WithContext(ctx) - - // Let's make sure we have updated game data before we start the runs - b.ctx.RefreshGameData() - - // This routine is in charge of refreshing the game data and handling cancellation, will work in parallel with any other execution - g.Go(func() error { - b.ctx.AttachRoutine(botCtx.PriorityBackground) - ticker := time.NewTicker(10 * time.Millisecond) - for { - select { - case <-ctx.Done(): - cancel() - b.Stop() - return nil - case <-ticker.C: - b.ctx.RefreshGameData() - } - } - }) - - // This routine is in charge of handling the health/chicken of the bot, will work in parallel with any other execution - g.Go(func() error { - b.ctx.AttachRoutine(botCtx.PriorityBackground) - - ticker := time.NewTicker(100 * time.Millisecond) - for { - select { - case <-ctx.Done(): - return nil - case <-ticker.C: - err := b.ctx.HealthManager.HandleHealthAndMana() - if err != nil { - cancel() - b.Stop() - return err - } - } - } - }) - - // High priority loop, this will interrupt (pause) low priority loop - g.Go(func() error { - b.ctx.AttachRoutine(botCtx.PriorityHigh) - ticker := time.NewTicker(time.Second) - for { - select { - case <-ctx.Done(): - return nil - case <-ticker.C: - if len(action.GetItemsToPickup(30)) > 0 { - b.ctx.ExecutionPriority = botCtx.PriorityHigh - _ = action.ItemPickup(30) - b.ctx.ExecutionPriority = botCtx.PriorityNormal - } - } - } - }) - - // Low priority loop, this will keep executing main run scripts - g.Go(func() error { - b.ctx.AttachRoutine(botCtx.PriorityNormal) - for _, r := range runs { - event.Send(event.RunStarted(event.Text(b.ctx.Name, "Starting run"), r.Name())) - err := action.PreRun(firstRun) - if err != nil { - return err - } - - err = r.Run() - if err != nil { - return err - } - err = action.PostRun(false) - if err != nil { - return err - } - } - return nil - }) - - return g.Wait() -} - -func (b *Bot) Stop() { - b.ctx.Cancel() -} diff --git a/internal/v2/bot/manager.go b/internal/v2/bot/manager.go deleted file mode 100644 index 7f3022ee..00000000 --- a/internal/v2/bot/manager.go +++ /dev/null @@ -1,328 +0,0 @@ -package bot - -import ( - "fmt" - "log/slog" - "strconv" - "time" - - "github.com/hectorgimenez/koolo/cmd/koolo/log" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/event" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/character" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/health" - "github.com/hectorgimenez/koolo/internal/v2/pather" - "github.com/hectorgimenez/koolo/internal/v2/utils" - "github.com/lxn/win" -) - -type SupervisorManager struct { - logger *slog.Logger - supervisors map[string]Supervisor - crashDetectors map[string]*game.CrashDetector - eventListener *event.Listener -} - -func NewSupervisorManager(logger *slog.Logger, eventListener *event.Listener) *SupervisorManager { - return &SupervisorManager{ - logger: logger, - supervisors: make(map[string]Supervisor), - crashDetectors: make(map[string]*game.CrashDetector), - eventListener: eventListener, - } -} - -func (mng *SupervisorManager) AvailableSupervisors() []string { - availableSupervisors := make([]string, 0) - for name := range config.Characters { - if name != "template" { - availableSupervisors = append(availableSupervisors, name) - } - } - - return availableSupervisors -} - -func (mng *SupervisorManager) Start(supervisorName string) error { - // Avoid multiple instances of the supervisor - shitstorm prevention - if _, exists := mng.supervisors[supervisorName]; exists { - return fmt.Errorf("supervisor %s is already running", supervisorName) - } - - // Reload config to get the latest local changes before starting the supervisor - err := config.Load() - if err != nil { - return fmt.Errorf("error loading config: %w", err) - } - - supervisorLogger, err := log.NewLogger(config.Koolo.Debug.Log, config.Koolo.LogSaveDirectory, supervisorName) - if err != nil { - return err - } - - // This function will be used to restart the client - passed to the crashDetector - restartFunc := func() { - mng.logger.Info("Restarting supervisor after crash", slog.String("supervisor", supervisorName)) - mng.Stop(supervisorName) - time.Sleep(5 * time.Second) // Wait a bit before restarting - - // Get a list of all available Supervisors - supervisorList := mng.AvailableSupervisors() - - for { - - // Set the default state - tokenAuthStarting := false - - // Get the current supervisor's config - supCfg := config.Characters[supervisorName] - - for _, sup := range supervisorList { - - // If the current don't check against the one we're trying to launch - if sup == supervisorName { - continue - } - - if mng.GetSupervisorStats(sup).SupervisorStatus == Starting { - if supCfg.AuthMethod == "TokenAuth" { - tokenAuthStarting = true - mng.logger.Info("Waiting before restart as another client is already starting and we're using token auth", slog.String("supervisor", sup)) - break - } - - sCfg, found := config.Characters[sup] - if found { - if sCfg.AuthMethod == "TokenAuth" { - // A client that uses token auth is currently starting, hold off restart - tokenAuthStarting = true - mng.logger.Info("Waiting before restart as a client that's using token auth is already starting", slog.String("supervisor", sup)) - break - } - } - } - } - - if !tokenAuthStarting { - break - } - - // Wait 5 seconds before checking again - utils.Sleep(5000) - } - - err := mng.Start(supervisorName) - if err != nil { - mng.logger.Error("Failed to restart supervisor", slog.String("supervisor", supervisorName), slog.String("Error: ", err.Error())) - } - } - - supervisor, crashDetector, err := mng.buildSupervisor(supervisorName, supervisorLogger, restartFunc) - if err != nil { - return err - } - - if oldCrashDetector, exists := mng.crashDetectors[supervisorName]; exists { - oldCrashDetector.Stop() // Stop the old crash detector if it exists - } - - mng.supervisors[supervisorName] = supervisor - mng.crashDetectors[supervisorName] = crashDetector - - if config.Koolo.GameWindowArrangement { - go func() { - // When the game starts, its doing some weird stuff like repositioning and resizing window automatically - // we need to wait until this is done in order to reposition, or it will be overridden - time.Sleep(time.Second * 5) - mng.rearrangeWindows() - }() - } - - //defer func() { - // if r := recover(); r != nil { - // mng.logger.Error( - // "fatal error detected, forcing shutdown", - // slog.String("supervisor", supervisorName), - // slog.Any("error", r), - // slog.String("Stacktrace", string(debug.Stack())), - // ) - // } - //}() - - // Start the Crash Detector in a thread to avoid blocking and speed up start - go crashDetector.Start() - - err = supervisor.Start() - if err != nil { - mng.logger.Error(fmt.Sprintf("error running supervisor %s: %s", supervisorName, err.Error())) - } - - return nil -} - -func (mng *SupervisorManager) StopAll() { - for _, s := range mng.supervisors { - s.Stop() - } -} - -func (mng *SupervisorManager) Stop(supervisor string) { - - s, found := mng.supervisors[supervisor] - if found { - - // Stop the Supervisor - s.Stop() - - // Delete him from the list of Supervisors - delete(mng.supervisors, supervisor) - - if cd, ok := mng.crashDetectors[supervisor]; ok { - cd.Stop() - delete(mng.crashDetectors, supervisor) - } - } -} - -func (mng *SupervisorManager) TogglePause(supervisor string) { - s, found := mng.supervisors[supervisor] - if found { - s.TogglePause() - } -} - -func (mng *SupervisorManager) Status(characterName string) Stats { - for name, supervisor := range mng.supervisors { - if name == characterName { - return supervisor.Stats() - } - } - - return Stats{} -} - -func (mng *SupervisorManager) GetData(characterName string) *game.Data { - for name, supervisor := range mng.supervisors { - if name == characterName { - return supervisor.GetData() - } - } - - return &game.Data{} -} - -func (mng *SupervisorManager) buildSupervisor(supervisorName string, logger *slog.Logger, restartFunc func()) (Supervisor, *game.CrashDetector, error) { - cfg, found := config.Characters[supervisorName] - if !found { - return nil, nil, fmt.Errorf("character %s not found", supervisorName) - } - - pid, hwnd, err := game.StartGame(cfg.Username, cfg.Password, cfg.AuthMethod, cfg.AuthToken, cfg.Realm, cfg.CommandLineArgs, config.Koolo.UseCustomSettings) - if err != nil { - return nil, nil, fmt.Errorf("error starting game: %w", err) - } - - gr, err := game.NewGameReader(cfg, supervisorName, pid, hwnd, logger) - if err != nil { - return nil, nil, fmt.Errorf("error creating game reader: %w", err) - } - - gi, err := game.InjectorInit(logger, gr.GetPID()) - if err != nil { - return nil, nil, fmt.Errorf("error creating game injector: %w", err) - } - - ctx := context.NewContext(supervisorName) - - hidM := game.NewHID(gr, gi) - pf := pather.NewPathFinder(gr, ctx.Data, hidM, cfg) - - bm := health.NewBeltManager(ctx.Data, hidM, logger, supervisorName) - hm := health.NewHealthManager(bm, ctx.Data) - - ctx.CharacterCfg = cfg - ctx.EventListener = mng.eventListener - ctx.HID = hidM - ctx.Logger = logger - ctx.Manager = game.NewGameManager(gr, hidM, supervisorName) - ctx.GameReader = gr - ctx.MemoryInjector = gi - ctx.PathFinder = pf - ctx.BeltManager = bm - ctx.HealthManager = hm - char, err := character.BuildCharacter(logger, cfg, ctx.Data, pf) - if err != nil { - return nil, nil, fmt.Errorf("error creating character: %w", err) - } - ctx.Char = char - - bot := NewBot(ctx.Context) - - statsHandler := NewStatsHandler(supervisorName, logger) - mng.eventListener.Register(statsHandler.Handle) - - var supervisor Supervisor - if config.Characters[supervisorName].Companion.Enabled { - //supervisor, err = NewCompanionSupervisor(supervisorName, bot, runFactory, statsHandler, c, pid, uintptr(hwnd)) - } else { - supervisor, err = NewSinglePlayerSupervisor(supervisorName, bot, statsHandler) - } - - if err != nil { - return nil, nil, err - } - - crashDetector := game.NewCrashDetector(supervisorName, int32(pid), uintptr(hwnd), mng.logger, restartFunc) - - return supervisor, crashDetector, nil -} - -func (mng *SupervisorManager) GetSupervisorStats(supervisor string) Stats { - if mng.supervisors[supervisor] == nil { - return Stats{} - } - return mng.supervisors[supervisor].Stats() -} - -func (mng *SupervisorManager) rearrangeWindows() { - width := win.GetSystemMetrics(0) - height := win.GetSystemMetrics(1) - var windowBorderX int32 = 2 // left + right window border is 2px - var windowBorderY int32 = 40 // upper window border is usually 40px - var windowOffsetX int32 = -10 // offset horizontal window placement by -10 pixel - maxColumns := width / (1280 + windowBorderX) - maxRows := height / (720 + windowBorderY) - - mng.logger.Debug( - "Arranging windows", - slog.String("displaywidth", strconv.FormatInt(int64(width), 10)), - slog.String("displayheight", strconv.FormatInt(int64(height), 10)), - slog.String("max columns", strconv.FormatInt(int64(maxColumns+1), 10)), // +1 as we are counting from 0 - slog.String("max rows", strconv.FormatInt(int64(maxRows+1), 10)), - ) - - var column, row int32 - for _, sp := range mng.supervisors { - // reminder that columns are vertical (they go up and down) and rows are horizontal (they go left and right) - if column > maxColumns { - column = 0 - row++ - } - - if row <= maxRows { - sp.SetWindowPosition(int(column*(1280+windowBorderX)+windowOffsetX), int(row*(720+windowBorderY))) - mng.logger.Debug( - "Window Positions", - slog.String("supervisor", sp.Name()), - slog.String("column", strconv.FormatInt(int64(column), 10)), - slog.String("row", strconv.FormatInt(int64(row), 10)), - slog.String("position", strconv.FormatInt(int64(column*(1280+windowBorderX)+windowOffsetX), 10)+"x"+strconv.FormatInt(int64(row*(720+windowBorderY)), 10)), - ) - column++ - } else { - mng.logger.Debug("Window position of supervisor " + sp.Name() + " was not changed, no free space for it") - } - } -} diff --git a/internal/v2/bot/single_supervisor.go b/internal/v2/bot/single_supervisor.go deleted file mode 100644 index 97ba268b..00000000 --- a/internal/v2/bot/single_supervisor.go +++ /dev/null @@ -1,126 +0,0 @@ -package bot - -import ( - "context" - "errors" - "fmt" - "log/slog" - "math/rand" - "time" - - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/run" - - "github.com/hectorgimenez/koolo/internal/health" - - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/event" -) - -type SinglePlayerSupervisor struct { - *baseSupervisor -} - -func (s *SinglePlayerSupervisor) GetData() *game.Data { - return s.bot.ctx.Data -} - -func NewSinglePlayerSupervisor(name string, bot *Bot, statsHandler *StatsHandler) (*SinglePlayerSupervisor, error) { - bs, err := newBaseSupervisor(bot, name, statsHandler) - if err != nil { - return nil, err - } - - return &SinglePlayerSupervisor{ - baseSupervisor: bs, - }, nil -} - -// Start will return error if it can not be started, otherwise will always return nil -func (s *SinglePlayerSupervisor) Start() error { - ctx, cancel := context.WithCancel(context.Background()) - s.cancelFn = cancel - - err := s.ensureProcessIsRunningAndPrepare(ctx) - if err != nil { - return fmt.Errorf("error preparing game: %w", err) - } - - firstRun := true - for { - select { - case <-ctx.Done(): - return nil - default: - if firstRun { - err = s.waitUntilCharacterSelectionScreen() - if err != nil { - return fmt.Errorf("error waiting for character selection screen: %w", err) - } - } - if !s.bot.ctx.Manager.InGame() { - if err = s.bot.ctx.Manager.NewGame(); err != nil { - s.bot.ctx.Logger.Error(fmt.Sprintf("Error creating new game: %s", err.Error())) - continue - } - } - - runs := run.BuildRuns(s.bot.ctx.CharacterCfg) - gameStart := time.Now() - if config.Characters[s.name].Game.RandomizeRuns { - rand.Shuffle(len(runs), func(i, j int) { runs[i], runs[j] = runs[j], runs[i] }) - } - if config.Koolo.Discord.EnableGameCreatedMessages { - event.Send(event.GameCreated(event.Text(s.name, "New game created"), "", "")) - } else { - event.Send(event.GameCreated(event.Text(s.name, ""), "", "")) - } - s.bot.ctx.LastBuffAt = time.Time{} - s.logGameStart(runs) - err = s.bot.Run(ctx, firstRun, runs) - if err != nil { - if errors.Is(context.Canceled, ctx.Err()) { - continue - } - - switch { - case errors.Is(err, health.ErrChicken): - if config.Koolo.Discord.EnableDiscordChickenMessages { - event.Send(event.GameFinished(event.WithScreenshot(s.name, err.Error(), s.bot.ctx.GameReader.Screenshot()), event.FinishedChicken)) - } else { - event.Send(event.GameFinished(event.Text(s.name, ""), event.FinishedChicken)) - } - s.bot.ctx.Logger.Warn(err.Error(), slog.Float64("gameLength", time.Since(gameStart).Seconds())) - case errors.Is(err, health.ErrMercChicken): - if config.Koolo.Discord.EnableDiscordChickenMessages { - event.Send(event.GameFinished(event.WithScreenshot(s.name, err.Error(), s.bot.ctx.GameReader.Screenshot()), event.FinishedMercChicken)) - } else { - event.Send(event.GameFinished(event.Text(s.name, ""), event.FinishedMercChicken)) - } - s.bot.ctx.Logger.Warn(err.Error(), slog.Float64("gameLength", time.Since(gameStart).Seconds())) - case errors.Is(err, health.ErrDied): - if config.Koolo.Discord.EnableDiscordChickenMessages { - event.Send(event.GameFinished(event.WithScreenshot(s.name, err.Error(), s.bot.ctx.GameReader.Screenshot()), event.FinishedDied)) - } else { - event.Send(event.GameFinished(event.Text(s.name, ""), event.FinishedDied)) - } - s.bot.ctx.Logger.Warn(err.Error(), slog.Float64("gameLength", time.Since(gameStart).Seconds())) - default: - event.Send(event.GameFinished(event.WithScreenshot(s.name, err.Error(), s.bot.ctx.GameReader.Screenshot()), event.FinishedError)) - s.bot.ctx.Logger.Warn( - fmt.Sprintf("Game finished with errors, reason: %s. Game total time: %0.2fs", err.Error(), time.Since(gameStart).Seconds()), - slog.String("supervisor", s.name), - slog.Uint64("mapSeed", uint64(s.bot.ctx.GameReader.CachedMapSeed)), - ) - } - } - if exitErr := s.bot.ctx.Manager.ExitGame(); exitErr != nil { - errMsg := fmt.Sprintf("Error exiting game %s", err.Error()) - event.Send(event.GameFinished(event.WithScreenshot(s.name, errMsg, s.bot.ctx.GameReader.Screenshot()), event.FinishedError)) - - return errors.New(errMsg) - } - firstRun = false - } - } -} diff --git a/internal/v2/bot/stats.go b/internal/v2/bot/stats.go deleted file mode 100644 index 064ab42a..00000000 --- a/internal/v2/bot/stats.go +++ /dev/null @@ -1,145 +0,0 @@ -package bot - -import ( - "context" - "log/slog" - "strings" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/event" -) - -const ( - NotStarted SupervisorStatus = "Not Started" - Starting SupervisorStatus = "Starting" - InGame SupervisorStatus = "In game" - Paused SupervisorStatus = "Paused" - Crashed SupervisorStatus = "Crashed" -) - -type SupervisorStatus string - -type StatsHandler struct { - stats *Stats - name string - logger *slog.Logger -} - -func NewStatsHandler(name string, logger *slog.Logger) *StatsHandler { - return &StatsHandler{ - name: name, - logger: logger, - stats: &Stats{ - SupervisorStatus: Starting, - StartedAt: time.Now(), - }, - } -} - -func (h *StatsHandler) Handle(_ context.Context, e event.Event) error { - // Only handle events from the supervisor - if !strings.EqualFold(e.Supervisor(), h.name) { - return nil - } - - switch evt := e.(type) { - case event.GameCreatedEvent: - h.stats.Games = append(h.stats.Games, GameStats{ - StartedAt: evt.OccurredAt(), - }) - h.stats.SupervisorStatus = InGame - case event.GameFinishedEvent: - h.stats.Games[len(h.stats.Games)-1].FinishedAt = evt.OccurredAt() - h.stats.Games[len(h.stats.Games)-1].Reason = evt.Reason - h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].FinishedAt = evt.OccurredAt() - h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].Reason = evt.Reason - case event.RunStartedEvent: - if len(h.stats.Games) == 0 { - h.stats.Games = append(h.stats.Games, GameStats{ - StartedAt: evt.OccurredAt(), - }) - h.stats.SupervisorStatus = InGame - } - h.stats.Games[len(h.stats.Games)-1].Runs = append(h.stats.Games[len(h.stats.Games)-1].Runs, RunStats{ - Name: evt.RunName, - StartedAt: evt.OccurredAt(), - }) - case event.GamePausedEvent: - if evt.Paused { - h.stats.SupervisorStatus = Paused - } else { - h.stats.SupervisorStatus = InGame - } - case event.RunFinishedEvent: - h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].FinishedAt = evt.OccurredAt() - h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].Reason = evt.Reason - case event.ItemStashedEvent: - // The hell is this Hector o.O - //h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].Items = append(h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].Items, evt.Item) - - // Ain't this much easier? - h.stats.Drops = append(h.stats.Drops, evt.Item) - case event.UsedPotionEvent: - h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].UsedPotions = append(h.stats.Games[len(h.stats.Games)-1].Runs[len(h.stats.Games[len(h.stats.Games)-1].Runs)-1].UsedPotions, evt) - } - - return nil -} - -func (h *StatsHandler) Stats() Stats { - return *h.stats -} - -type Stats struct { - StartedAt time.Time - SupervisorStatus SupervisorStatus - Details string - Drops []data.Drop - Games []GameStats -} - -type GameStats struct { - StartedAt time.Time - FinishedAt time.Time - Reason event.FinishReason - Runs []RunStats -} - -type RunStats struct { - Name string - Reason event.FinishReason - StartedAt time.Time - Items []data.Item - FinishedAt time.Time - UsedPotions []event.UsedPotionEvent -} - -func (s Stats) TotalGames() int { - return len(s.Games) -} - -func (s Stats) TotalDeaths() int { - return s.totalRunsByReason(event.FinishedDied) -} - -func (s Stats) TotalChickens() int { - return s.totalRunsByReason(event.FinishedChicken) + s.totalRunsByReason(event.FinishedMercChicken) -} - -func (s Stats) TotalErrors() int { - return s.totalRunsByReason(event.FinishedError) -} - -func (s Stats) totalRunsByReason(reason event.FinishReason) int { - total := 0 - for _, g := range s.Games { - for _, r := range g.Runs { - if r.Reason == reason { - total++ - } - } - } - - return total -} diff --git a/internal/v2/bot/supervisor.go b/internal/v2/bot/supervisor.go deleted file mode 100644 index da12e011..00000000 --- a/internal/v2/bot/supervisor.go +++ /dev/null @@ -1,132 +0,0 @@ -package bot - -import ( - "context" - "fmt" - "log/slog" - "os" - "strings" - "time" - - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper/winproc" - "github.com/hectorgimenez/koolo/internal/v2/run" - "github.com/lxn/win" -) - -type Supervisor interface { - Start() error - Name() string - Stop() - Stats() Stats - TogglePause() - SetWindowPosition(x, y int) - GetData() *game.Data -} - -type baseSupervisor struct { - bot *Bot - name string - statsHandler *StatsHandler - cancelFn context.CancelFunc -} - -func newBaseSupervisor( - bot *Bot, - name string, - statsHandler *StatsHandler, -) (*baseSupervisor, error) { - return &baseSupervisor{ - bot: bot, - name: name, - statsHandler: statsHandler, - }, nil -} - -func (s *baseSupervisor) Name() string { - return s.name -} - -func (s *baseSupervisor) Stats() Stats { - return s.statsHandler.Stats() -} - -func (s *baseSupervisor) TogglePause() { - //s.bot.TogglePause() -} - -func (s *baseSupervisor) Stop() { - s.bot.ctx.Logger.Info("Stopping...", slog.String("configuration", s.name)) - if s.cancelFn != nil { - s.cancelFn() - } - - s.bot.ctx.MemoryInjector.Unload() - s.bot.ctx.GameReader.Close() - - if s.bot.ctx.CharacterCfg.KillD2OnStop { - process, err := os.FindProcess(int(s.bot.ctx.GameReader.Process.GetPID())) - if err != nil { - s.bot.ctx.Logger.Info("Failed to find process", slog.String("configuration", s.name)) - } - err = process.Kill() - if err != nil { - s.bot.ctx.Logger.Info("Failed to kill process", slog.String("configuration", s.name)) - } - } - s.bot.ctx.Logger.Info("Finished stopping", slog.String("configuration", s.name)) -} - -func (s *baseSupervisor) ensureProcessIsRunningAndPrepare(ctx context.Context) error { - // Prevent screen from turning off - winproc.SetThreadExecutionState.Call(winproc.EXECUTION_STATE_ES_DISPLAY_REQUIRED | winproc.EXECUTION_STATE_ES_CONTINUOUS) - - return s.bot.ctx.MemoryInjector.Load() -} - -func (s *baseSupervisor) logGameStart(runs []run.Run) { - runNames := "" - for _, r := range runs { - runNames += r.Name() + ", " - } - s.bot.ctx.Logger.Info(fmt.Sprintf("Starting Game #%d. Run list: %s", s.statsHandler.Stats().TotalGames(), runNames[:len(runNames)-2])) -} - -func (s *baseSupervisor) waitUntilCharacterSelectionScreen() error { - s.bot.ctx.Logger.Info("Waiting for character selection screen...") - - for s.bot.ctx.GameReader.GameReader.GetSelectedCharacterName() == "" { - s.bot.ctx.HID.Click(game.LeftButton, 100, 100) - time.Sleep(time.Second) - } - - time.Sleep(time.Second) // Add an extra second to allow UI to properly render on slow computers - - s.bot.ctx.Logger.Info("Character selection screen found") - - if s.bot.ctx.CharacterCfg.CharacterName != "" { - s.bot.ctx.Logger.Info("Selecting character...") - previousSelection := "" - for { - characterName := s.bot.ctx.GameReader.GameReader.GetSelectedCharacterName() - if strings.EqualFold(previousSelection, characterName) { - return fmt.Errorf("character %s not found", s.bot.ctx.CharacterCfg.CharacterName) - } - if strings.EqualFold(characterName, s.bot.ctx.CharacterCfg.CharacterName) { - s.bot.ctx.Logger.Info("Character found") - return nil - } - - s.bot.ctx.HID.PressKey(win.VK_DOWN) - time.Sleep(time.Millisecond * 500) - previousSelection = characterName - } - } - - return nil -} - -func (s *baseSupervisor) SetWindowPosition(x, y int) { - uFlags := win.SWP_NOZORDER | win.SWP_NOSIZE | win.SWP_NOACTIVATE - win.SetWindowPos(s.bot.ctx.GameReader.HWND, 0, int32(x), int32(y), 0, 0, uint32(uFlags)) -} diff --git a/internal/v2/character/berserk_barb.go b/internal/v2/character/berserk_barb.go deleted file mode 100644 index d240748c..00000000 --- a/internal/v2/character/berserk_barb.go +++ /dev/null @@ -1,329 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "sort" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/data/state" - "github.com/hectorgimenez/d2go/pkg/utils" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/game" -) - -type Berserker struct { - BaseCharacter - *game.HID -} - -func (s Berserker) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.BattleCommand, skill.BattleOrders, skill.Shout, skill.FindItem, skill.TomeOfTownPortal} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - missingKeybindings = append(missingKeybindings, cskill) - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s Berserker) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - findItemAttempts := 0 - const maxFindItemAttempts = 1 - const maxRange = 15 - const berserkerMaxAttacksLoop = 5 // Adjust this value as needed - - for { - id, found := monsterSelector(*s.data) - if !found { - // Find Item logic - if findItemAttempts < maxFindItemAttempts { - foundCorpse := s.FindItemOnNearbyCorpses(*s.data, maxRange, time.Millisecond*100) - if foundCorpse { - findItemAttempts++ - step.Wait(time.Millisecond * 100) - } else { - findItemAttempts = maxFindItemAttempts - } - } - - if findItemAttempts >= maxFindItemAttempts { - return nil - } - continue - } - - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= berserkerMaxAttacksLoop { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - step.MoveTo(monster.Position) - step.PrimaryAttack(id, 1, false, step.Distance(1, maxRange)) - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s Berserker) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s *Berserker) FindItemOnNearbyCorpses(d game.Data, maxRange int, waitTime time.Duration) bool { - s.logger.Debug("Attempting Find Item on nearby corpses", slog.Int("total_corpses", len(d.Corpses))) - - findItemKey, found := d.KeyBindings.KeyBindingForSkill(skill.FindItem) - if !found { - s.logger.Debug("Find Item skill not found in key bindings") - return false - } - - playerPos := d.PlayerUnit.Position - originalPosition := playerPos - corpseFound := false - successfulFindItems := 0 - checkedCorpses := make(map[data.UnitID]bool) - - // Sort corpses by distance from the player - sort.Slice(d.Corpses, func(i, j int) bool { - distI := utils.DistanceFromPoint(playerPos, d.Corpses[i].Position) - distJ := utils.DistanceFromPoint(playerPos, d.Corpses[j].Position) - return distI < distJ - }) - - for _, corpse := range d.Corpses { - distance := utils.DistanceFromPoint(playerPos, corpse.Position) - - if distance > maxRange { - break - } - - // Check if this corpse has already been processed - if checkedCorpses[corpse.UnitID] { - continue - } - - if corpse.Type != data.MonsterTypeChampion && - corpse.Type != data.MonsterTypeMinion && - corpse.Type != data.MonsterTypeUnique && - corpse.Type != data.MonsterTypeSuperUnique { - continue - } - - screenX, screenY := s.pf.GameCoordsToScreenCords( - corpse.Position.X, corpse.Position.Y, - ) - - s.HID.MovePointer(screenX, screenY) - time.Sleep(waitTime) - - s.HID.PressKeyBinding(findItemKey) - time.Sleep(waitTime) - - s.HID.Click(game.RightButton, screenX, screenY) - s.logger.Debug("Find Item used on corpse", slog.Any("corpse_id", corpse.UnitID)) - time.Sleep(waitTime) - - // Mark this corpse as checked - checkedCorpses[corpse.UnitID] = true - - corpseFound = true - successfulFindItems++ - - if d.PlayerUnit.States.HasState(state.Cooldown) { - break - } - - playerPos = d.PlayerUnit.Position - - if utils.DistanceFromPoint(originalPosition, playerPos) > maxRange { - break - } - } - - s.logger.Debug("Find Item sequence completed", - slog.Bool("corpse_found", corpseFound), - slog.Int("successful_find_items", successfulFindItems)) - return corpseFound -} - -// Placeholder for possible Howl addition -/// -// func (s Berserker) ensureHowlSkill(d game.Data) []step.Step { -// if d.PlayerUnit.RightSkill != skill.Howl { -// if howlKey, found := d.KeyBindings.KeyBindingForSkill(skill.Howl); found { -// s.logger.Debug(fmt.Sprintf("Activating Howl skill with key binding: %v", howlKey)) -// return []step.Step{step.SyncStep(func(d game.Data) error { -// s.container.HID.PressKeyBinding(howlKey) -// time.Sleep(80 * time.Millisecond) // Short delay after key press -// return nil -// })} -// } else { -// s.logger.Debug("Howl key binding not found") -// } -// } else { -// s.logger.Debug("Howl skill already active") -// } -// return nil -// } - -func (s Berserker) PreCTABuffSkills() []skill.ID { - return []skill.ID{} -} - -func (s Berserker) BuffSkills() []skill.ID { - skillsList := make([]skill.ID, 0) - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.BattleCommand); found { - skillsList = append(skillsList, skill.BattleCommand) - } - - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.Shout); found { - skillsList = append(skillsList, skill.Shout) - } - - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.BattleOrders); found { - skillsList = append(skillsList, skill.BattleOrders) - } - - return skillsList -} - -func (s Berserker) KillCountess() error { - return s.killMonster(npc.DarkStalker, data.MonsterTypeSuperUnique) -} - -func (s Berserker) KillAndariel() error { - return s.killMonster(npc.Andariel, data.MonsterTypeNone) -} - -func (s Berserker) KillSummoner() error { - return s.killMonster(npc.Summoner, data.MonsterTypeNone) -} - -func (s Berserker) KillDuriel() error { - return s.killMonster(npc.Duriel, data.MonsterTypeNone) -} - -func (s Berserker) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - for _, m := range d.Monsters { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - councilMembers = append(councilMembers, m) - } - } - - // Order council members by distance - sort.Slice(councilMembers, func(i, j int) bool { - distanceI := s.pf.DistanceFromMe(councilMembers[i].Position) - distanceJ := s.pf.DistanceFromMe(councilMembers[j].Position) - - return distanceI < distanceJ - }) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s Berserker) KillMephisto() error { - return s.killMonster(npc.Mephisto, data.MonsterTypeNone) -} - -func (s Berserker) KillIzual() error { - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - - return s.killMonster(npc.Izual, data.MonsterTypeNone) -} - -func (s Berserker) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s Berserker) KillPindle() error { - return s.killMonster(npc.DefiledWarrior, data.MonsterTypeSuperUnique) -} - -func (s Berserker) KillNihlathak() error { - return s.killMonster(npc.Nihlathak, data.MonsterTypeSuperUnique) -} - -func (s Berserker) KillBaal() error { - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - - return s.killMonster(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/blizzard_sorceress.go b/internal/v2/character/blizzard_sorceress.go deleted file mode 100644 index ee0e2bbe..00000000 --- a/internal/v2/character/blizzard_sorceress.go +++ /dev/null @@ -1,257 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/data/state" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/action/step" -) - -const ( - sorceressMaxAttacksLoop = 40 - sorceressMinDistance = 25 - sorceressMaxDistance = 30 -) - -type BlizzardSorceress struct { - BaseCharacter -} - -func (s BlizzardSorceress) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.Blizzard, skill.Teleport, skill.TomeOfTownPortal, skill.ShiverArmor, skill.StaticField} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - switch cskill { - // Since we can have one of 3 armors: - case skill.ShiverArmor: - _, found1 := s.data.KeyBindings.KeyBindingForSkill(skill.FrozenArmor) - _, found2 := s.data.KeyBindings.KeyBindingForSkill(skill.ChillingArmor) - if !found1 && !found2 { - missingKeybindings = append(missingKeybindings, skill.ShiverArmor) - } - default: - missingKeybindings = append(missingKeybindings, cskill) - } - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s BlizzardSorceress) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - previousSelfBlizzard := time.Time{} - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= sorceressMaxAttacksLoop { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - opts := step.Distance(sorceressMinDistance, sorceressMaxDistance) - - // Cast a Blizzard on very close mobs, in order to clear possible trash close the player, every two attack rotations - if time.Since(previousSelfBlizzard) > time.Second*4 && !s.data.PlayerUnit.States.HasState(state.Cooldown) { - for _, m := range s.data.Monsters.Enemies() { - if dist := s.pf.DistanceFromMe(m.Position); dist < 4 { - s.logger.Debug("Monster detected close to the player, casting Blizzard over it") - previousSelfBlizzard = time.Now() - step.SecondaryAttack(skill.Blizzard, m.UnitID, 1, opts) - } - } - } - - if s.data.PlayerUnit.States.HasState(state.Cooldown) { - step.PrimaryAttack(id, 2, true, opts) - } - - step.SecondaryAttack(skill.Blizzard, id, 1, opts) - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s BlizzardSorceress) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s BlizzardSorceress) killMonsterByName(id npc.ID, monsterType data.MonsterType, maxDistance int, _ bool, skipOnImmunities []stat.Resist) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - if m, found := d.Monsters.FindOne(id, monsterType); found { - return m.UnitID, true - } - - return 0, false - }, skipOnImmunities) -} - -func (s BlizzardSorceress) BuffSkills() []skill.ID { - skillsList := make([]skill.ID, 0) - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.EnergyShield); found { - skillsList = append(skillsList, skill.EnergyShield) - } - - armors := []skill.ID{skill.ChillingArmor, skill.ShiverArmor, skill.FrozenArmor} - for _, armor := range armors { - if _, found := s.data.KeyBindings.KeyBindingForSkill(armor); found { - skillsList = append(skillsList, armor) - return skillsList - } - } - - return skillsList -} - -func (s BlizzardSorceress) PreCTABuffSkills() []skill.ID { - return []skill.ID{} -} - -func (s BlizzardSorceress) KillCountess() error { - return s.killMonsterByName(npc.DarkStalker, data.MonsterTypeSuperUnique, sorceressMaxDistance, false, nil) -} - -func (s BlizzardSorceress) KillAndariel() error { - return s.killMonsterByName(npc.Andariel, data.MonsterTypeNone, sorceressMaxDistance, false, nil) -} - -func (s BlizzardSorceress) KillSummoner() error { - return s.killMonsterByName(npc.Summoner, data.MonsterTypeNone, sorceressMaxDistance, false, nil) -} - -func (s BlizzardSorceress) KillDuriel() error { - return s.killMonsterByName(npc.Duriel, data.MonsterTypeNone, sorceressMaxDistance, true, nil) -} - -func (s BlizzardSorceress) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - var coldImmunes []data.Monster - for _, m := range d.Monsters.Enemies() { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - if m.IsImmune(stat.ColdImmune) { - coldImmunes = append(coldImmunes, m) - } else { - councilMembers = append(councilMembers, m) - } - } - } - - councilMembers = append(councilMembers, coldImmunes...) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s BlizzardSorceress) KillMephisto() error { - return s.killMonsterByName(npc.Mephisto, data.MonsterTypeNone, sorceressMaxDistance, true, nil) -} - -func (s BlizzardSorceress) KillIzual() error { - m, _ := s.data.Monsters.FindOne(npc.Izual, data.MonsterTypeNone) - _ = step.SecondaryAttack(skill.StaticField, m.UnitID, 7, step.Distance(5, 8)) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - - return s.killMonster(npc.Izual, data.MonsterTypeNone) -} - -func (s BlizzardSorceress) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - _ = step.SecondaryAttack(skill.StaticField, diablo.UnitID, 5, step.Distance(3, 8)) - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s BlizzardSorceress) KillPindle() error { - return s.killMonsterByName(npc.DefiledWarrior, data.MonsterTypeSuperUnique, sorceressMaxDistance, false, s.cfg.Game.Pindleskin.SkipOnImmunities) -} - -func (s BlizzardSorceress) KillNihlathak() error { - return s.killMonsterByName(npc.Nihlathak, data.MonsterTypeSuperUnique, sorceressMaxDistance, false, nil) -} - -func (s BlizzardSorceress) KillBaal() error { - m, _ := s.data.Monsters.FindOne(npc.BaalCrab, data.MonsterTypeNone) - step.SecondaryAttack(skill.StaticField, m.UnitID, 5, step.Distance(5, 8)) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - - return s.killMonster(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/character.go b/internal/v2/character/character.go deleted file mode 100644 index fec6a1f0..00000000 --- a/internal/v2/character/character.go +++ /dev/null @@ -1,82 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "strings" - - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/pather" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/stat" -) - -func BuildCharacter(logger *slog.Logger, cfg *config.CharacterCfg, data *game.Data, pf *pather.PathFinder) (context.Character, error) { - bc := BaseCharacter{ - logger: logger, - data: data, - cfg: cfg, - pf: pf, - } - - //if container.CharacterCfg.Game.Runs[0] == "leveling" { - // switch strings.ToLower(container.CharacterCfg.Character.Class) { - // case "sorceress_leveling_lightning": - // return SorceressLevelingLightning{BaseCharacter: bc}, nil - // case "sorceress": - // return SorceressLeveling{BaseCharacter: bc}, nil - // case "paladin": - // return PaladinLeveling{BaseCharacter: bc}, nil - // } - // - // return nil, fmt.Errorf("leveling only available for sorceress and paladin") - //} - - switch strings.ToLower(cfg.Character.Class) { - case "sorceress": - return BlizzardSorceress{BaseCharacter: bc}, nil - case "lightning": - return LightningSorceress{BaseCharacter: bc}, nil - case "hammerdin": - return Hammerdin{BaseCharacter: bc}, nil - case "foh": - return Foh{BaseCharacter: bc}, nil - case "trapsin": - return Trapsin{BaseCharacter: bc}, nil - case "mosaic": - return MosaicSin{BaseCharacter: bc}, nil - case "winddruid": - return WindDruid{BaseCharacter: bc}, nil - case "javazon": - return Javazon{BaseCharacter: bc}, nil - case "berserker": - return Berserker{BaseCharacter: bc}, nil - } - - return nil, fmt.Errorf("class %s not implemented", cfg.Character.Class) -} - -type BaseCharacter struct { - logger *slog.Logger - data *game.Data - cfg *config.CharacterCfg - pf *pather.PathFinder -} - -func (bc BaseCharacter) preBattleChecks(id data.UnitID, skipOnImmunities []stat.Resist) bool { - monster, found := bc.data.Monsters.FindByID(id) - if !found { - return false - } - for _, i := range skipOnImmunities { - if monster.IsImmune(i) { - bc.logger.Info("Monster is immune! skipping", slog.String("immuneTo", string(i))) - return false - } - } - - return true -} diff --git a/internal/v2/character/foh.go b/internal/v2/character/foh.go deleted file mode 100644 index 395c0057..00000000 --- a/internal/v2/character/foh.go +++ /dev/null @@ -1,260 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "sort" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" -) - -const ( - fohMaxAttacksLoop = 20 - fohMinDistance = 5 - fohMaxDistance = 20 -) - -type Foh struct { - BaseCharacter - *game.HID -} - -func (s Foh) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.Conviction, skill.HolyShield, skill.TomeOfTownPortal} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - missingKeybindings = append(missingKeybindings, cskill) - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s Foh) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= fohMaxAttacksLoop { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - if s.data.PlayerUnit.LeftSkill != skill.FistOfTheHeavens { - fohKey, fohFound := s.data.KeyBindings.KeyBindingForSkill(skill.FistOfTheHeavens) - if fohFound { - step.SyncStep(func(_ game.Data) error { - helper.Sleep(40) - s.HID.PressKeyBinding(fohKey) - return nil - }) - } - } - - step.PrimaryAttack( - id, - 3, - true, - step.Distance(fohMinDistance, fohMaxDistance), - step.EnsureAura(skill.Conviction), - ) - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s Foh) killMonsterByName(id npc.ID, monsterType data.MonsterType, _ bool) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - if m, found := d.Monsters.FindOne(id, monsterType); found { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s Foh) killBoss(id npc.ID, monsterType data.MonsterType) error { - for { - monster, found := s.data.Monsters.FindOne(id, monsterType) - if !found || monster.Stats[stat.Life] <= 0 { - helper.Sleep(100) - return nil - } - - hbKey, holyBoltFound := s.data.KeyBindings.KeyBindingForSkill(skill.HolyBolt) - fohKey, fohFound := s.data.KeyBindings.KeyBindingForSkill(skill.FistOfTheHeavens) - - // Switch between foh and holy bolt while attacking - if holyBoltFound && fohFound { - helper.Sleep(50) - - step.PrimaryAttack( - monster.UnitID, - 1, - true, - step.Distance(fohMinDistance, fohMaxDistance), - step.EnsureAura(skill.Conviction), - ) - s.HID.PressKeyBinding(hbKey) - helper.Sleep(40) - - step.PrimaryAttack( - monster.UnitID, - 3, - true, - step.Distance(fohMinDistance, fohMaxDistance), - step.EnsureAura(skill.Conviction), - ) - - helper.Sleep(40) - s.HID.PressKeyBinding(fohKey) - } else { - helper.Sleep(100) - // Don't switch because no keybindings found - step.PrimaryAttack( - monster.UnitID, - 3, - true, - step.Distance(fohMinDistance, fohMaxDistance), - step.EnsureAura(skill.Conviction), - ) - } - } -} - -func (s Foh) BuffSkills() []skill.ID { - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.HolyShield); found { - return []skill.ID{skill.HolyShield} - } - return []skill.ID{} -} - -func (s Foh) PreCTABuffSkills() []skill.ID { - return []skill.ID{} -} - -func (s Foh) KillCountess() error { - return s.killMonsterByName(npc.DarkStalker, data.MonsterTypeSuperUnique, false) -} - -func (s Foh) KillAndariel() error { - return s.killBoss(npc.Andariel, data.MonsterTypeNone) -} - -func (s Foh) KillSummoner() error { - return s.killMonsterByName(npc.Summoner, data.MonsterTypeNone, false) -} - -func (s Foh) KillDuriel() error { - return s.killBoss(npc.Duriel, data.MonsterTypeNone) -} - -func (s Foh) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - for _, m := range d.Monsters { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - councilMembers = append(councilMembers, m) - } - } - - // Order council members by distance - sort.Slice(councilMembers, func(i, j int) bool { - distanceI := s.pf.DistanceFromMe(councilMembers[i].Position) - distanceJ := s.pf.DistanceFromMe(councilMembers[j].Position) - - return distanceI < distanceJ - }) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s Foh) KillMephisto() error { - return s.killBoss(npc.Mephisto, data.MonsterTypeNone) -} - -func (s Foh) KillIzual() error { - return s.killMonsterByName(npc.Izual, data.MonsterTypeNone, false) -} - -func (s Foh) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - return s.killBoss(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s Foh) KillPindle() error { - return s.killMonsterByName(npc.DefiledWarrior, data.MonsterTypeSuperUnique, false) -} - -func (s Foh) KillNihlathak() error { - return s.killMonsterByName(npc.Nihlathak, data.MonsterTypeSuperUnique, false) -} - -func (s Foh) KillBaal() error { - return s.killBoss(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/hammerdin.go b/internal/v2/character/hammerdin.go deleted file mode 100644 index 69088ec1..00000000 --- a/internal/v2/character/hammerdin.go +++ /dev/null @@ -1,216 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "sort" - "time" - - "github.com/hectorgimenez/koolo/internal/game" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/action/step" -) - -const ( - hammerdinMaxAttacksLoop = 20 // Adjust from 5-20 depending on DMG and rotation, lower attack loops would cause higher attack rotation whereas bigger would perform multiple(longer) attacks on one spot. -) - -type Hammerdin struct { - BaseCharacter -} - -func (s Hammerdin) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.Concentration, skill.HolyShield, skill.TomeOfTownPortal} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - missingKeybindings = append(missingKeybindings, cskill) - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s Hammerdin) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= hammerdinMaxAttacksLoop { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - // Add a random movement, maybe hammer is not hitting the target - if previousUnitID == int(id) { - if monster.Stats[stat.Life] > 0 { - s.pf.RandomMovement(*s.data) - } - return nil - } - - step.PrimaryAttack( - id, - 3, - true, - step.Distance(2, 2), // X,Y coords of 2,2 is the perfect hammer angle attack for NPC targeting/attacking, you can adjust accordingly anything between 1,1 - 3,3 is acceptable, where the higher the number, the bigger the distance from the player (usually used for De Seis) - step.EnsureAura(skill.Concentration), - ) - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s Hammerdin) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s Hammerdin) killMonsterByName(id npc.ID, monsterType data.MonsterType, _ bool) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - if m, found := d.Monsters.FindOne(id, monsterType); found { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s Hammerdin) BuffSkills() []skill.ID { - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.HolyShield); found { - return []skill.ID{skill.HolyShield} - } - return []skill.ID{} -} - -func (s Hammerdin) PreCTABuffSkills() []skill.ID { - return []skill.ID{} -} - -func (s Hammerdin) KillCountess() error { - return s.killMonsterByName(npc.DarkStalker, data.MonsterTypeSuperUnique, false) -} - -func (s Hammerdin) KillAndariel() error { - return s.killMonsterByName(npc.Andariel, data.MonsterTypeSuperUnique, false) -} - -func (s Hammerdin) KillSummoner() error { - return s.killMonsterByName(npc.Summoner, data.MonsterTypeSuperUnique, false) -} - -func (s Hammerdin) KillDuriel() error { - return s.killMonsterByName(npc.Duriel, data.MonsterTypeSuperUnique, false) -} - -func (s Hammerdin) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - for _, m := range d.Monsters { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - councilMembers = append(councilMembers, m) - } - } - - // Order council members by distance - sort.Slice(councilMembers, func(i, j int) bool { - distanceI := s.pf.DistanceFromMe(councilMembers[i].Position) - distanceJ := s.pf.DistanceFromMe(councilMembers[j].Position) - - return distanceI < distanceJ - }) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s Hammerdin) KillMephisto() error { - return s.killMonsterByName(npc.Mephisto, data.MonsterTypeSuperUnique, false) -} - -func (s Hammerdin) KillIzual() error { - return s.killMonster(npc.Izual, data.MonsterTypeNone) -} - -func (s Hammerdin) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s Hammerdin) KillPindle() error { - return s.killMonsterByName(npc.DefiledWarrior, data.MonsterTypeSuperUnique, false) -} - -func (s Hammerdin) KillNihlathak() error { - return s.killMonsterByName(npc.Nihlathak, data.MonsterTypeSuperUnique, false) -} - -func (s Hammerdin) KillBaal() error { - return s.killMonster(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/javazon.go b/internal/v2/character/javazon.go deleted file mode 100644 index 7926198d..00000000 --- a/internal/v2/character/javazon.go +++ /dev/null @@ -1,256 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "sort" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/pather" -) - -const ( - maxJavazonAttackLoops = 10 - minJavazonDistance = 10 - maxJavazonDistance = 30 -) - -type Javazon struct { - BaseCharacter -} - -func (s Javazon) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.LightningFury, skill.TomeOfTownPortal} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - missingKeybindings = append(missingKeybindings, cskill) - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s Javazon) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - const numOfAttacks = 5 - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= maxJavazonAttackLoops { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - closeMonsters := 0 - for _, mob := range s.data.Monsters { - if mob.IsPet() || mob.IsMerc() || mob.IsGoodNPC() || mob.IsSkip() || monster.Stats[stat.Life] <= 0 && mob.UnitID != monster.UnitID { - continue - } - if pather.DistanceFromPoint(mob.Position, monster.Position) <= 15 { - closeMonsters++ - } - if closeMonsters >= 3 { - break - } - } - - if closeMonsters >= 3 { - step.SecondaryAttack(skill.LightningFury, id, numOfAttacks, step.Distance(minJavazonDistance, maxJavazonDistance)) - } else { - step.PrimaryAttack(id, numOfAttacks, false, step.Distance(1, 1)) - } - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s Javazon) KillBossSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - const numOfAttacks = 5 - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= maxJavazonAttackLoops { - return nil - } - - completedAttackLoops++ - previousUnitID = int(id) - - step.PrimaryAttack(id, numOfAttacks, false, step.Distance(1, 1)) - } -} - -func (s Javazon) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s Javazon) killBoss(npc npc.ID, t data.MonsterType) error { - return s.KillBossSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s Javazon) PreCTABuffSkills() []skill.ID { - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.Valkyrie); found { - return []skill.ID{skill.Valkyrie} - } else { - return []skill.ID{} - } -} - -func (s Javazon) BuffSkills() []skill.ID { - return []skill.ID{} -} - -func (a Javazon) KillCountess() error { - return a.killMonster(npc.DarkStalker, data.MonsterTypeSuperUnique) -} - -func (a Javazon) KillAndariel() error { - return a.killBoss(npc.Andariel, data.MonsterTypeNone) -} - -func (a Javazon) KillSummoner() error { - return a.killMonster(npc.Summoner, data.MonsterTypeNone) -} - -func (a Javazon) KillDuriel() error { - return a.killBoss(npc.Duriel, data.MonsterTypeNone) -} - -func (s Javazon) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - for _, m := range d.Monsters { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - councilMembers = append(councilMembers, m) - } - } - - // Order council members by distance - sort.Slice(councilMembers, func(i, j int) bool { - distanceI := s.pf.DistanceFromMe(councilMembers[i].Position) - distanceJ := s.pf.DistanceFromMe(councilMembers[j].Position) - - return distanceI < distanceJ - }) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (a Javazon) KillMephisto() error { - return a.killBoss(npc.Mephisto, data.MonsterTypeNone) -} - -func (a Javazon) KillIzual() error { - return a.killBoss(npc.Izual, data.MonsterTypeNone) -} - -func (s Javazon) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (a Javazon) KillPindle() error { - return a.killBoss(npc.DefiledWarrior, data.MonsterTypeSuperUnique) -} - -func (a Javazon) KillNihlathak() error { - return a.killBoss(npc.Nihlathak, data.MonsterTypeSuperUnique) -} - -func (a Javazon) KillBaal() error { - return a.killBoss(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/lightning_sorceress.go b/internal/v2/character/lightning_sorceress.go deleted file mode 100644 index 3e3b7617..00000000 --- a/internal/v2/character/lightning_sorceress.go +++ /dev/null @@ -1,306 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "time" - - "github.com/hectorgimenez/koolo/internal/game" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/action/step" -) - -const ( - lightningSorceressMaxAttacksLoop = 10 - lightningSorceressMinDistance = 8 - lightningSorceressMaxDistance = 13 -) - -type LightningSorceress struct { - BaseCharacter -} - -func (s LightningSorceress) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.Nova, skill.Teleport, skill.TomeOfTownPortal, skill.FrozenArmor, skill.StaticField} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - switch cskill { - // Since we can have one of 3 armors: - case skill.FrozenArmor: - _, found1 := s.data.KeyBindings.KeyBindingForSkill(skill.ShiverArmor) - _, found2 := s.data.KeyBindings.KeyBindingForSkill(skill.ChillingArmor) - if !found1 && !found2 { - missingKeybindings = append(missingKeybindings, skill.FrozenArmor) - } - default: - missingKeybindings = append(missingKeybindings, cskill) - } - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s LightningSorceress) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= lightningSorceressMaxAttacksLoop { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - opts := step.Distance(lightningSorceressMinDistance, lightningSorceressMaxDistance) - - if s.shouldCastStatic() { - step.SecondaryAttack(skill.StaticField, id, 1, opts) - } - - if completedAttackLoops%2 == 0 { - for _, m := range s.data.Monsters.Enemies() { - if dist := s.pf.DistanceFromMe(m.Position); dist < 5 { - s.logger.Debug("Monster detected close to the player, casting Nova") - step.SecondaryAttack(skill.Blizzard, m.UnitID, 1, opts) - break - } - } - } - - // In case monster is stuck behind a wall or character is not able to reach it we will short the distance - if completedAttackLoops > 5 { - if completedAttackLoops == 6 { - s.logger.Debug("Looks like monster is not reachable, reducing max attack distance.") - } - opts = step.Distance(0, 1) - } - - step.SecondaryAttack(skill.Nova, id, 5, opts) - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s LightningSorceress) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s LightningSorceress) killMonsterByName(id npc.ID, monsterType data.MonsterType, maxDistance int, _ bool, skipOnImmunities []stat.Resist) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - if m, found := d.Monsters.FindOne(id, monsterType); found { - return m.UnitID, true - } - - return 0, false - }, skipOnImmunities) -} - -func (s LightningSorceress) shouldCastStatic() bool { - - // Iterate through all mobs within max range and collect them - mobs := make([]data.Monster, 0) - - for _, m := range s.data.Monsters.Enemies() { - if s.pf.DistanceFromMe(m.Position) <= lightningSorceressMaxDistance+5 { - mobs = append(mobs, m) - } else { - continue - } - } - - // Iterate through the mob list and check their if more than 50% of the mobs are above 60% hp - var mobsAbove60Percent int - for _, mob := range mobs { - - life := mob.Stats[stat.Life] - maxLife := mob.Stats[stat.MaxLife] - - lifePercentage := int((float64(life) / float64(maxLife)) * 100) - - if lifePercentage > 60 { - mobsAbove60Percent++ - } - } - - return mobsAbove60Percent > len(mobs)/2 -} - -func (s LightningSorceress) BuffSkills() []skill.ID { - skillsList := make([]skill.ID, 0) - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.EnergyShield); found { - skillsList = append(skillsList, skill.EnergyShield) - } - - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.ThunderStorm); found { - skillsList = append(skillsList, skill.ThunderStorm) - } - - armors := []skill.ID{skill.ChillingArmor, skill.ShiverArmor, skill.FrozenArmor} - for _, armor := range armors { - if _, found := s.data.KeyBindings.KeyBindingForSkill(armor); found { - skillsList = append(skillsList, armor) - return skillsList - } - } - - return skillsList -} - -func (s LightningSorceress) PreCTABuffSkills() []skill.ID { - return []skill.ID{} -} - -func (s LightningSorceress) KillCountess() error { - return s.killMonsterByName(npc.DarkStalker, data.MonsterTypeSuperUnique, lightningSorceressMaxDistance, false, nil) -} - -func (s LightningSorceress) KillAndariel() error { - m, _ := s.data.Monsters.FindOne(npc.Andariel, data.MonsterTypeNone) - _ = step.SecondaryAttack(skill.StaticField, m.UnitID, 7, step.Distance(8, 13)) - - return s.killMonster(npc.Andariel, data.MonsterTypeNone) -} - -func (s LightningSorceress) KillSummoner() error { - return s.killMonsterByName(npc.Summoner, data.MonsterTypeNone, lightningSorceressMaxDistance, false, nil) -} - -func (s LightningSorceress) KillDuriel() error { - m, _ := s.data.Monsters.FindOne(npc.Duriel, data.MonsterTypeNone) - _ = step.SecondaryAttack(skill.StaticField, m.UnitID, 7, step.Distance(8, 13)) - - return s.killMonster(npc.Duriel, data.MonsterTypeNone) -} - -func (s LightningSorceress) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - var lightningImmunes []data.Monster - for _, m := range d.Monsters.Enemies() { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - if m.IsImmune(stat.LightImmune) { - lightningImmunes = append(lightningImmunes, m) - } else { - councilMembers = append(councilMembers, m) - } - } - } - - councilMembers = append(councilMembers, lightningImmunes...) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s LightningSorceress) KillMephisto() error { - m, _ := s.data.Monsters.FindOne(npc.Mephisto, data.MonsterTypeNone) - _ = step.SecondaryAttack(skill.StaticField, m.UnitID, 7, step.Distance(8, 13)) - - return s.killMonster(npc.Mephisto, data.MonsterTypeNone) -} - -func (s LightningSorceress) KillIzual() error { - m, _ := s.data.Monsters.FindOne(npc.Izual, data.MonsterTypeNone) - _ = step.SecondaryAttack(skill.StaticField, m.UnitID, 7, step.Distance(8, 13)) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - - return s.killMonster(npc.Izual, data.MonsterTypeNone) -} - -func (s LightningSorceress) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - _ = step.SecondaryAttack(skill.StaticField, diablo.UnitID, 5, step.Distance(8, 13)) - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s LightningSorceress) KillPindle() error { - return s.killMonsterByName(npc.DefiledWarrior, data.MonsterTypeSuperUnique, sorceressMaxDistance, false, s.cfg.Game.Pindleskin.SkipOnImmunities) -} - -func (s LightningSorceress) KillNihlathak() error { - return s.killMonsterByName(npc.Nihlathak, data.MonsterTypeSuperUnique, lightningSorceressMaxDistance, false, nil) -} - -func (s LightningSorceress) KillBaal() error { - m, _ := s.data.Monsters.FindOne(npc.BaalCrab, data.MonsterTypeNone) - step.SecondaryAttack(skill.StaticField, m.UnitID, 5, step.Distance(8, 13)) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - - return s.killMonster(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/mosaic.go b/internal/v2/character/mosaic.go deleted file mode 100644 index 91adef86..00000000 --- a/internal/v2/character/mosaic.go +++ /dev/null @@ -1,266 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "sort" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/data/state" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/game" -) - -type MosaicSin struct { - BaseCharacter -} - -func (s MosaicSin) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.TigerStrike, skill.CobraStrike, skill.PhoenixStrike, skill.ClawsOfThunder, skill.BladesOfIce, skill.TomeOfTownPortal} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - missingKeybindings = append(missingKeybindings, cskill) - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s MosaicSin) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - opts := step.Distance(1, 2) - - if !s.MobAlive(id, *s.data) { // Check if the mob is still alive - return nil - } - - // Tiger Strike - if !s.data.PlayerUnit.States.HasState(state.Tigerstrike) { - step.SecondaryAttack(skill.TigerStrike, id, 4, opts) - } - - if !s.MobAlive(id, *s.data) { // Check if the mob is still alive - return nil - } - - // Cobra Strike - if !s.data.PlayerUnit.States.HasState(state.Cobrastrike) { - step.SecondaryAttack(skill.CobraStrike, id, 4, opts) - } - - if !s.MobAlive(id, *s.data) { // Check if the mob is still alive - return nil - } - - // Phoenix Strike - if !s.data.PlayerUnit.States.HasState(state.Phoenixstrike) { - step.SecondaryAttack(skill.PhoenixStrike, id, 4, opts) - } - - if !s.MobAlive(id, *s.data) { // Check if the mob is still alive - return nil - } - - // Claws of Thunder - if !s.data.PlayerUnit.States.HasState(state.Clawsofthunder) { - step.SecondaryAttack(skill.ClawsOfThunder, id, 4, opts) - } - - if !s.MobAlive(id, *s.data) { // Check if the mob is still alive - return nil - } - - // Blades of Ice - if !s.data.PlayerUnit.States.HasState(state.Bladesofice) { - step.SecondaryAttack(skill.BladesOfIce, id, 4, opts) - } - - if !s.MobAlive(id, *s.data) { // Check if the mob is still alive - return nil - } - - // Fists of Fire - //if !s.data.PlayerUnit.States.HasState(state.Fistsoffire) { - // step.SecondaryAttack(skill.FistsOfFire, id, 4, opts) - //} - - // Finish it off with primary attack - step.PrimaryAttack(id, 1, false, opts) - } -} - -func (s MosaicSin) MobAlive(mob data.UnitID, d game.Data) bool { - monster, found := s.data.Monsters.FindByID(mob) - return found && monster.Stats[stat.Life] > 0 -} - -func (s MosaicSin) BuffSkills() []skill.ID { - skillsList := make([]skill.ID, 0) - - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.Fade); found { - skillsList = append(skillsList, skill.Fade) - } else { - - // If we don't use fade but we use Burst of Speed - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.BurstOfSpeed); found { - skillsList = append(skillsList, skill.BurstOfSpeed) - } - } - - return skillsList -} - -func (s MosaicSin) PreCTABuffSkills() []skill.ID { - - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.ShadowMaster); found { - return []skill.ID{skill.ShadowMaster} - } else if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.ShadowWarrior); found { - return []skill.ID{skill.ShadowWarrior} - } else { - return []skill.ID{} - } - -} - -func (s MosaicSin) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s MosaicSin) KillCountess() error { - return s.killMonster(npc.DarkStalker, data.MonsterTypeSuperUnique) -} - -func (s MosaicSin) KillAndariel() error { - return s.killMonster(npc.Andariel, data.MonsterTypeNone) -} - -func (s MosaicSin) KillSummoner() error { - return s.killMonster(npc.Summoner, data.MonsterTypeNone) -} - -func (s MosaicSin) KillDuriel() error { - return s.killMonster(npc.Duriel, data.MonsterTypeNone) -} - -func (s MosaicSin) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - for _, m := range d.Monsters { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - councilMembers = append(councilMembers, m) - } - } - - // Order council members by distance - sort.Slice(councilMembers, func(i, j int) bool { - distanceI := s.pf.DistanceFromMe(councilMembers[i].Position) - distanceJ := s.pf.DistanceFromMe(councilMembers[j].Position) - - return distanceI < distanceJ - }) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s MosaicSin) KillMephisto() error { - return s.killMonster(npc.Mephisto, data.MonsterTypeNone) -} - -func (s MosaicSin) KillIzual() error { - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - s.killMonster(npc.Izual, data.MonsterTypeNone) - - return s.killMonster(npc.Izual, data.MonsterTypeNone) -} - -func (s MosaicSin) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s MosaicSin) KillPindle() error { - return s.killMonster(npc.DefiledWarrior, data.MonsterTypeSuperUnique) -} - -func (s MosaicSin) KillNihlathak() error { - return s.killMonster(npc.Nihlathak, data.MonsterTypeSuperUnique) -} - -func (s MosaicSin) KillBaal() error { - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - s.killMonster(npc.BaalCrab, data.MonsterTypeNone) - - return s.killMonster(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/trapsin.go b/internal/v2/character/trapsin.go deleted file mode 100644 index 73895c5c..00000000 --- a/internal/v2/character/trapsin.go +++ /dev/null @@ -1,222 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "sort" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" -) - -const ( - maxAttacksLoop = 5 - minDistance = 25 - maxDistance = 30 -) - -type Trapsin struct { - BaseCharacter -} - -func (s Trapsin) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.DeathSentry, skill.LightningSentry, skill.TomeOfTownPortal} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - missingKeybindings = append(missingKeybindings, cskill) - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s Trapsin) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - if completedAttackLoops >= maxAttacksLoop { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - opts := step.Distance(minDistance, maxDistance) - - helper.Sleep(100) - step.SecondaryAttack(skill.LightningSentry, id, 3, opts) - step.SecondaryAttack(skill.DeathSentry, id, 2, opts) - step.PrimaryAttack(id, 2, true, opts) - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s Trapsin) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s Trapsin) BuffSkills() []skill.ID { - armor := skill.Fade - armors := []skill.ID{skill.BurstOfSpeed, skill.Fade} - for _, arm := range armors { - if _, found := s.data.KeyBindings.KeyBindingForSkill(arm); found { - armor = arm - } - } - - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.BladeShield); found { - return []skill.ID{armor, skill.BladeShield} - } - - return []skill.ID{armor} -} - -func (s Trapsin) PreCTABuffSkills() []skill.ID { - armor := skill.ShadowWarrior - armors := []skill.ID{skill.ShadowWarrior, skill.ShadowMaster} - hasShadow := false - for _, arm := range armors { - if _, found := s.data.KeyBindings.KeyBindingForSkill(arm); found { - armor = arm - hasShadow = true - } - } - - if hasShadow { - return []skill.ID{armor} - } - - return []skill.ID{} -} - -func (s Trapsin) KillCountess() error { - return s.killMonster(npc.DarkStalker, data.MonsterTypeSuperUnique) -} - -func (s Trapsin) KillAndariel() error { - return s.killMonster(npc.Andariel, data.MonsterTypeNone) -} - -func (s Trapsin) KillSummoner() error { - return s.killMonster(npc.Summoner, data.MonsterTypeNone) -} - -func (s Trapsin) KillDuriel() error { - return s.killMonster(npc.Duriel, data.MonsterTypeNone) -} - -func (s Trapsin) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - for _, m := range d.Monsters { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - councilMembers = append(councilMembers, m) - } - } - - // Order council members by distance - sort.Slice(councilMembers, func(i, j int) bool { - distanceI := s.pf.DistanceFromMe(councilMembers[i].Position) - distanceJ := s.pf.DistanceFromMe(councilMembers[j].Position) - - return distanceI < distanceJ - }) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s Trapsin) KillMephisto() error { - return s.killMonster(npc.Mephisto, data.MonsterTypeNone) -} - -func (s Trapsin) KillIzual() error { - return s.killMonster(npc.Izual, data.MonsterTypeNone) -} - -func (s Trapsin) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s Trapsin) KillPindle() error { - return s.killMonster(npc.DefiledWarrior, data.MonsterTypeSuperUnique) -} - -func (s Trapsin) KillNihlathak() error { - return s.killMonster(npc.Nihlathak, data.MonsterTypeSuperUnique) -} - -func (s Trapsin) KillBaal() error { - return s.killMonster(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/character/wind_druid.go b/internal/v2/character/wind_druid.go deleted file mode 100644 index 64da71b3..00000000 --- a/internal/v2/character/wind_druid.go +++ /dev/null @@ -1,305 +0,0 @@ -package character - -import ( - "fmt" - "log/slog" - "sort" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/data/state" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" -) - -const ( - druMaxAttacksLoop = 20 - druMinDistance = 2 - druMaxDistance = 8 -) - -var lastRavenAt = map[string]time.Time{} - -type WindDruid struct { - BaseCharacter - *game.HID -} - -func (s WindDruid) CheckKeyBindings() []skill.ID { - requireKeybindings := []skill.ID{skill.Hurricane, skill.OakSage, skill.CycloneArmor, skill.TomeOfTownPortal} - missingKeybindings := []skill.ID{} - - for _, cskill := range requireKeybindings { - if _, found := s.data.KeyBindings.KeyBindingForSkill(cskill); !found { - missingKeybindings = append(missingKeybindings, cskill) - } - } - - if len(missingKeybindings) > 0 { - s.logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) - } - - return missingKeybindings -} - -func (s WindDruid) KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, -) error { - completedAttackLoops := 0 - previousUnitID := 0 - - for { - id, found := monsterSelector(*s.data) - if !found { - return nil - } - if previousUnitID != int(id) { - completedAttackLoops = 0 - } - - if !s.preBattleChecks(id, skipOnImmunities) { - return nil - } - - s.RecastBuffs() - - if completedAttackLoops >= maxAttacksLoop { - return nil - } - - monster, found := s.data.Monsters.FindByID(id) - if !found { - s.logger.Info("Monster not found", slog.String("monster", fmt.Sprintf("%v", monster))) - return nil - } - - // Add a random movement, maybe tornado is not hitting the target - if previousUnitID == int(id) { - if monster.Stats[stat.Life] > 0 { - s.pf.RandomMovement(*s.data) - } - return nil - } - - step.PrimaryAttack( - id, - 3, - true, - step.Distance(druMinDistance, druMaxDistance), - ) - - completedAttackLoops++ - previousUnitID = int(id) - } -} - -func (s WindDruid) killMonster(npc npc.ID, t data.MonsterType) error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - m, found := d.Monsters.FindOne(npc, t) - if !found { - return 0, false - } - - return m.UnitID, true - }, nil) -} - -func (s WindDruid) RecastBuffs() { - skills := []skill.ID{skill.Hurricane, skill.OakSage, skill.CycloneArmor} - states := []state.State{state.Hurricane, state.Oaksage, state.Cyclonearmor} - - for i, druSkill := range skills { - if kb, found := s.data.KeyBindings.KeyBindingForSkill(druSkill); found { - if !s.data.PlayerUnit.States.HasState(states[i]) { - s.HID.PressKeyBinding(kb) - helper.Sleep(180) - s.HID.Click(game.RightButton, 640, 340) - helper.Sleep(100) - } - } - } -} - -func (s WindDruid) BuffSkills() (buffs []skill.ID) { - if _, found := s.data.KeyBindings.KeyBindingForSkill(skill.CycloneArmor); found { - buffs = append(buffs, skill.CycloneArmor) - } - if _, ravenFound := s.data.KeyBindings.KeyBindingForSkill(skill.Raven); ravenFound { - buffs = append(buffs, skill.Raven, skill.Raven, skill.Raven, skill.Raven, skill.Raven) - } - if _, hurricaneFound := s.data.KeyBindings.KeyBindingForSkill(skill.Hurricane); hurricaneFound { - buffs = append(buffs, skill.Hurricane) - } - return buffs -} - -func (s WindDruid) PreCTABuffSkills() (skills []skill.ID) { - needsBear := true - wolves := 5 - direWolves := 3 - needsOak := true - needsCreeper := true - - for _, monster := range s.data.Monsters { - if monster.IsPet() { - if monster.Name == npc.DruBear { - needsBear = false - } - if monster.Name == npc.DruFenris { - direWolves-- - } - if monster.Name == npc.DruSpiritWolf { - wolves-- - } - if monster.Name == npc.OakSage { - needsOak = false - } - if monster.Name == npc.DruCycleOfLife { - needsCreeper = false - } - if monster.Name == npc.VineCreature { - needsCreeper = false - } - if monster.Name == npc.DruPlaguePoppy { - needsCreeper = false - } - } - } - - if s.data.PlayerUnit.States.HasState(state.Oaksage) { - needsOak = false - } - - _, foundDireWolf := s.data.KeyBindings.KeyBindingForSkill(skill.SummonDireWolf) - _, foundWolf := s.data.KeyBindings.KeyBindingForSkill(skill.SummonSpiritWolf) - _, foundBear := s.data.KeyBindings.KeyBindingForSkill(skill.SummonGrizzly) - _, foundOak := s.data.KeyBindings.KeyBindingForSkill(skill.OakSage) - _, foundSolarCreeper := s.data.KeyBindings.KeyBindingForSkill(skill.SolarCreeper) - _, foundCarrionCreeper := s.data.KeyBindings.KeyBindingForSkill(skill.CarrionVine) - _, foundPoisonCreeper := s.data.KeyBindings.KeyBindingForSkill(skill.PoisonCreeper) - - if foundWolf { - for i := 0; i < wolves; i++ { - skills = append(skills, skill.SummonSpiritWolf) - } - } - if foundDireWolf { - for i := 0; i < direWolves; i++ { - skills = append(skills, skill.SummonDireWolf) - } - } - if foundBear && needsBear { - skills = append(skills, skill.SummonGrizzly) - } - if foundOak && needsOak { - skills = append(skills, skill.OakSage) - } - if foundSolarCreeper && needsCreeper { - skills = append(skills, skill.SolarCreeper) - } - if foundCarrionCreeper && needsCreeper { - skills = append(skills, skill.CarrionVine) - } - if foundPoisonCreeper && needsCreeper { - skills = append(skills, skill.PoisonCreeper) - } - - return skills -} - -func (s WindDruid) KillCountess() error { - return s.killMonster(npc.DarkStalker, data.MonsterTypeSuperUnique) -} - -func (s WindDruid) KillAndariel() error { - return s.killMonster(npc.Andariel, data.MonsterTypeNone) -} - -func (s WindDruid) KillSummoner() error { - return s.killMonster(npc.Summoner, data.MonsterTypeNone) -} - -func (s WindDruid) KillDuriel() error { - return s.killMonster(npc.Duriel, data.MonsterTypeNone) -} - -func (s WindDruid) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - // Exclude monsters that are not council members - var councilMembers []data.Monster - for _, m := range d.Monsters { - if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { - councilMembers = append(councilMembers, m) - } - } - - // Order council members by distance - sort.Slice(councilMembers, func(i, j int) bool { - distanceI := s.pf.DistanceFromMe(councilMembers[i].Position) - distanceJ := s.pf.DistanceFromMe(councilMembers[j].Position) - - return distanceI < distanceJ - }) - - for _, m := range councilMembers { - return m.UnitID, true - } - - return 0, false - }, nil) -} - -func (s WindDruid) KillMephisto() error { - return s.killMonster(npc.Mephisto, data.MonsterTypeNone) -} - -func (s WindDruid) KillIzual() error { - return s.killMonster(npc.Izual, data.MonsterTypeNone) -} - -func (s WindDruid) KillDiablo() error { - timeout := time.Second * 20 - startTime := time.Now() - diabloFound := false - - for { - if time.Since(startTime) > timeout && !diabloFound { - s.logger.Error("Diablo was not found, timeout reached") - return nil - } - - diablo, found := s.data.Monsters.FindOne(npc.Diablo, data.MonsterTypeNone) - if !found || diablo.Stats[stat.Life] <= 0 { - // Already dead - if diabloFound { - return nil - } - - // Keep waiting... - time.Sleep(100) - continue - } - - diabloFound = true - s.logger.Info("Diablo detected, attacking") - - return s.killMonster(npc.Diablo, data.MonsterTypeNone) - } -} - -func (s WindDruid) KillPindle() error { - return s.killMonster(npc.DefiledWarrior, data.MonsterTypeSuperUnique) -} - -func (s WindDruid) KillNihlathak() error { - return s.killMonster(npc.Nihlathak, data.MonsterTypeSuperUnique) -} - -func (s WindDruid) KillBaal() error { - return s.killMonster(npc.BaalCrab, data.MonsterTypeNone) -} diff --git a/internal/v2/context/character.go b/internal/v2/context/character.go deleted file mode 100644 index e66b8d9e..00000000 --- a/internal/v2/context/character.go +++ /dev/null @@ -1,39 +0,0 @@ -package context - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/skill" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/game" -) - -type Character interface { - CheckKeyBindings() []skill.ID - BuffSkills() []skill.ID - PreCTABuffSkills() []skill.ID - KillCountess() error - KillAndariel() error - KillSummoner() error - KillDuriel() error - KillMephisto() error - KillPindle() error - KillNihlathak() error - KillCouncil() error - KillDiablo() error - KillIzual() error - KillBaal() error - KillMonsterSequence( - monsterSelector func(d game.Data) (data.UnitID, bool), - skipOnImmunities []stat.Resist, - ) error -} - -type LevelingCharacter interface { - Character - // StatPoints Stats will be assigned in the order they are returned by this function. - StatPoints() map[stat.ID]int - SkillPoints() []skill.ID - SkillsToBind() (skill.ID, []skill.ID) - ShouldResetSkills() bool - KillAncients() error -} diff --git a/internal/v2/context/context.go b/internal/v2/context/context.go deleted file mode 100644 index 8527ad54..00000000 --- a/internal/v2/context/context.go +++ /dev/null @@ -1,100 +0,0 @@ -package context - -import ( - "log/slog" - "runtime" - "strconv" - "strings" - "sync" - "time" - - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/event" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/health" - "github.com/hectorgimenez/koolo/internal/v2/pather" -) - -var mu sync.Mutex -var botContexts = make(map[uint64]*Status) - -type Priority int - -const ( - PriorityHigh = 0 - PriorityNormal = 1 - PriorityBackground = 5 -) - -type Status struct { - *Context - Priority Priority -} - -type Context struct { - Name string - ExecutionPriority Priority - CharacterCfg *config.CharacterCfg - Data *game.Data - EventListener *event.Listener - HID *game.HID - Logger *slog.Logger - Manager *game.Manager - GameReader *game.MemoryReader - MemoryInjector *game.MemoryInjector - PathFinder *pather.PathFinder - BeltManager *health.BeltManager - HealthManager *health.Manager - Char Character - LastBuffAt time.Time - ContextDebug *ContextDebug -} - -type ContextDebug struct { - LastAction string - LastStep string -} - -func NewContext(name string) *Status { - ctx := &Context{ - Name: name, - Data: &game.Data{}, - ExecutionPriority: PriorityNormal, - ContextDebug: &ContextDebug{}, - } - botContexts[getGoroutineID()] = &Status{Priority: PriorityNormal, Context: ctx} - - return botContexts[getGoroutineID()] -} - -func Get() *Status { - return botContexts[getGoroutineID()] -} - -func getGoroutineID() uint64 { - var buf [64]byte - n := runtime.Stack(buf[:], false) - stackTrace := string(buf[:n]) - fields := strings.Fields(stackTrace) - id, _ := strconv.ParseUint(fields[1], 10, 64) - - return id -} - -func (ctx *Context) RefreshGameData() { - *ctx.Data = ctx.GameReader.GetData(false) -} - -func (ctx *Context) Cancel() { - -} - -func (ctx *Context) AttachRoutine(priority Priority) { - mu.Lock() - defer mu.Unlock() - botContexts[getGoroutineID()] = &Status{Priority: priority, Context: ctx} -} - -func (ctx *Context) SwitchPriority(priority Priority) { - ctx.ExecutionPriority = priority -} diff --git a/internal/v2/health/belt_manager.go b/internal/v2/health/belt_manager.go deleted file mode 100644 index 659f7a66..00000000 --- a/internal/v2/health/belt_manager.go +++ /dev/null @@ -1,123 +0,0 @@ -package health - -import ( - "fmt" - "log/slog" - "strings" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/event" - "github.com/hectorgimenez/koolo/internal/game" -) - -type BeltManager struct { - data *game.Data - hid *game.HID - logger *slog.Logger - supervisor string -} - -func NewBeltManager(data *game.Data, hid *game.HID, logger *slog.Logger, supervisor string) *BeltManager { - return &BeltManager{ - data: data, - hid: hid, - logger: logger, - supervisor: supervisor, - } -} - -func (bm BeltManager) DrinkPotion(potionType data.PotionType, merc bool) bool { - p, found := bm.data.Inventory.Belt.GetFirstPotion(potionType) - if found { - binding := bm.data.KeyBindings.UseBelt[p.X] - if merc { - bm.hid.PressKeyWithModifier(binding.Key1[0], game.ShiftKey) - bm.logger.Debug(fmt.Sprintf("Using %s potion on Mercenary [Column: %d]. HP: %d", potionType, p.X+1, bm.data.MercHPPercent())) - event.Send(event.UsedPotion(event.Text(bm.supervisor, ""), potionType, true)) - return true - } - bm.hid.PressKeyBinding(binding) - bm.logger.Debug(fmt.Sprintf("Using %s potion [Column: %d]. HP: %d MP: %d", potionType, p.X+1, bm.data.PlayerUnit.HPPercent(), bm.data.PlayerUnit.MPPercent())) - event.Send(event.UsedPotion(event.Text(bm.supervisor, ""), potionType, false)) - return true - } - - return false -} - -// ShouldBuyPotions will return true if more than 25% of belt is empty (ignoring rejuv) -func (bm BeltManager) ShouldBuyPotions() bool { - targetHealingAmount := bm.data.CharacterCfg.Inventory.BeltColumns.Total(data.HealingPotion) * bm.data.Inventory.Belt.Rows() - targetManaAmount := bm.data.CharacterCfg.Inventory.BeltColumns.Total(data.ManaPotion) * bm.data.Inventory.Belt.Rows() - targetRejuvAmount := bm.data.CharacterCfg.Inventory.BeltColumns.Total(data.RejuvenationPotion) * bm.data.Inventory.Belt.Rows() - - currentHealing, currentMana, currentRejuv := bm.getCurrentPotions() - - bm.logger.Debug(fmt.Sprintf( - "Belt Stats Health: %d/%d healing, %d/%d mana, %d/%d rejuv.", - currentHealing, - targetHealingAmount, - currentMana, - targetManaAmount, - currentRejuv, - targetRejuvAmount, - )) - - if currentHealing < int(float32(targetHealingAmount)*0.75) || currentMana < int(float32(targetManaAmount)*0.75) { - bm.logger.Debug("Need more pots, let's buy them.") - return true - } - - return false -} - -func (bm BeltManager) getCurrentPotions() (int, int, int) { - currentHealing := 0 - currentMana := 0 - currentRejuv := 0 - for _, i := range bm.data.Inventory.Belt.Items { - if strings.Contains(string(i.Name), string(data.HealingPotion)) { - currentHealing++ - continue - } - if strings.Contains(string(i.Name), string(data.ManaPotion)) { - currentMana++ - continue - } - if strings.Contains(string(i.Name), string(data.RejuvenationPotion)) { - currentRejuv++ - } - } - - return currentHealing, currentMana, currentRejuv -} - -func (bm BeltManager) GetMissingCount(potionType data.PotionType) int { - currentHealing, currentMana, currentRejuv := bm.getCurrentPotions() - - switch potionType { - case data.HealingPotion: - targetAmount := bm.data.CharacterCfg.Inventory.BeltColumns.Total(data.HealingPotion) * bm.data.Inventory.Belt.Rows() - missingPots := targetAmount - currentHealing - if missingPots < 0 { - return 0 - } - return missingPots - case data.ManaPotion: - targetAmount := bm.data.CharacterCfg.Inventory.BeltColumns.Total(data.ManaPotion) * bm.data.Inventory.Belt.Rows() - missingPots := targetAmount - currentMana - if missingPots < 0 { - return 0 - } - return missingPots - case data.RejuvenationPotion: - targetAmount := bm.data.CharacterCfg.Inventory.BeltColumns.Total(data.RejuvenationPotion) * bm.data.Inventory.Belt.Rows() - missingPots := targetAmount - currentRejuv - if missingPots < 0 { - return 0 - } - return missingPots - } - - return 0 -} diff --git a/internal/v2/health/health_manager.go b/internal/v2/health/health_manager.go deleted file mode 100644 index 12e1a167..00000000 --- a/internal/v2/health/health_manager.go +++ /dev/null @@ -1,99 +0,0 @@ -package health - -import ( - "errors" - "fmt" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/game" -) - -var ErrDied = errors.New("you died :(") -var ErrChicken = errors.New("chicken") -var ErrMercChicken = errors.New("mercenary chicken") - -const ( - healingInterval = time.Second * 4 - healingMercInterval = time.Second * 6 - manaInterval = time.Second * 4 - rejuvInterval = time.Second * 2 -) - -// Manager responsibility is to keep our character and mercenary alive, monitoring life and giving potions when needed -type Manager struct { - lastRejuv time.Time - lastRejuvMerc time.Time - lastHeal time.Time - lastMana time.Time - lastMercHeal time.Time - beltManager *BeltManager - data *game.Data -} - -func NewHealthManager(bm *BeltManager, data *game.Data) *Manager { - return &Manager{ - beltManager: bm, - data: data, - } -} - -func (hm *Manager) HandleHealthAndMana() error { - hpConfig := hm.data.CharacterCfg.Health - // Safe area, skipping - if hm.data.PlayerUnit.Area.IsTown() { - return nil - } - - if hm.data.PlayerUnit.HPPercent() <= 0 { - return ErrDied - } - - usedRejuv := false - if time.Since(hm.lastRejuv) > rejuvInterval && (hm.data.PlayerUnit.HPPercent() <= hpConfig.RejuvPotionAtLife || hm.data.PlayerUnit.MPPercent() < hpConfig.RejuvPotionAtMana) { - usedRejuv = hm.beltManager.DrinkPotion(data.RejuvenationPotion, false) - if usedRejuv { - hm.lastRejuv = time.Now() - } - } - - if !usedRejuv { - if hm.data.PlayerUnit.HPPercent() <= hpConfig.ChickenAt { - return fmt.Errorf("%w: Current Health: %d percent", ErrChicken, hm.data.PlayerUnit.HPPercent()) - } - - if hm.data.PlayerUnit.HPPercent() <= hpConfig.HealingPotionAt && time.Since(hm.lastHeal) > healingInterval { - hm.beltManager.DrinkPotion(data.HealingPotion, false) - hm.lastHeal = time.Now() - } - - if hm.data.PlayerUnit.MPPercent() <= hpConfig.ManaPotionAt && time.Since(hm.lastMana) > manaInterval { - hm.beltManager.DrinkPotion(data.ManaPotion, false) - hm.lastMana = time.Now() - } - } - - // Mercenary - if hm.data.MercHPPercent() > 0 { - usedMercRejuv := false - if time.Since(hm.lastRejuvMerc) > rejuvInterval && hm.data.MercHPPercent() <= hpConfig.MercRejuvPotionAt { - usedMercRejuv = hm.beltManager.DrinkPotion(data.RejuvenationPotion, true) - if usedMercRejuv { - hm.lastRejuvMerc = time.Now() - } - } - - if !usedMercRejuv { - if hm.data.MercHPPercent() <= hpConfig.MercChickenAt { - return fmt.Errorf("%w: Current Merc Health: %d percent", ErrMercChicken, hm.data.MercHPPercent()) - } - - if hm.data.MercHPPercent() <= hpConfig.MercHealingPotionAt && time.Since(hm.lastMercHeal) > healingMercInterval { - hm.beltManager.DrinkPotion(data.HealingPotion, true) - hm.lastMercHeal = time.Now() - } - } - } - - return nil -} diff --git a/internal/v2/pather/line_of_sight.go b/internal/v2/pather/line_of_sight.go deleted file mode 100644 index 0c713fcb..00000000 --- a/internal/v2/pather/line_of_sight.go +++ /dev/null @@ -1,55 +0,0 @@ -package pather - -import ( - "math" - - "github.com/hectorgimenez/d2go/pkg/data" -) - -func (pf *PathFinder) LineOfSight(origin data.Position, destination data.Position) bool { - x0, y0 := origin.X-pf.data.AreaOrigin.X, origin.Y-pf.data.AreaOrigin.Y - x1, y1 := destination.X-pf.data.AreaOrigin.X, destination.Y-pf.data.AreaOrigin.Y - - dx := int(math.Abs(float64(x1 - x0))) - dy := int(math.Abs(float64(y1 - y0))) - sx := -1 - sy := -1 - - if x0 < x1 { - sx = 1 - } - if y0 < y1 { - sy = 1 - } - - err := dx - dy - - for { - // Boundary check for x0, y0 - if x0 < 0 || y0 < 0 || x0 >= len(pf.data.CollisionGrid[0]) || y0 >= len(pf.data.CollisionGrid) { - return false - } - - // Check if the current position is not walkable - if !pf.data.CollisionGrid[y0][x0] { - return false - } - - // Check if we have reached the end point - if x0 == x1 && y0 == y1 { - break - } - - e2 := 2 * err - if e2 > -dy { - err -= dy - x0 += sx - } - if e2 < dx { - err += dx - y0 += sy - } - } - - return true -} diff --git a/internal/v2/pather/path.go b/internal/v2/pather/path.go deleted file mode 100644 index 4833d0b2..00000000 --- a/internal/v2/pather/path.go +++ /dev/null @@ -1,49 +0,0 @@ -package pather - -import ( - "github.com/beefsack/go-astar" - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/game" -) - -type Pather []astar.Pather - -func (p Pather) Distance() int { - return len(p) -} - -// Intersects checks if the given position intersects with the path, padding parameter is used to increase the area -func (p Pather) Intersects(d game.Data, position data.Position, padding int) bool { - position = data.Position{ - X: position.X - d.AreaOrigin.X, - Y: position.Y - d.AreaOrigin.Y, - } - - for _, path := range p { - pT := path.(*Tile) - xMatch := false - yMatch := false - for i := range padding { - if pT.X == position.X+i || pT.X == position.X-i { - xMatch = true - } - if pT.Y == position.Y+i || pT.Y == position.Y-i { - yMatch = true - } - } - - if xMatch && yMatch { - return true - } - } - - return false -} - -func (p Pather) To() data.Position { - if len(p) == 0 { - return data.Position{} - } - - return data.Position{X: p[len(p)-1].(*Tile).X, Y: p[len(p)-1].(*Tile).Y} -} diff --git a/internal/v2/pather/path_finder.go b/internal/v2/pather/path_finder.go deleted file mode 100644 index d90d51a3..00000000 --- a/internal/v2/pather/path_finder.go +++ /dev/null @@ -1,345 +0,0 @@ -package pather - -import ( - "fmt" - "math" - "math/rand" - - "github.com/beefsack/go-astar" - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/helper" -) - -type PathFinder struct { - gr *game.MemoryReader - data *game.Data - hid *game.HID - cfg *config.CharacterCfg - worldCache World - worldCacheHash string -} - -func NewPathFinder(gr *game.MemoryReader, data *game.Data, hid *game.HID, cfg *config.CharacterCfg) *PathFinder { - return &PathFinder{gr: gr, hid: hid, cfg: cfg, data: data} -} - -func (pf *PathFinder) GetPathFrom(from, to data.Position, blacklistedCoords ...[2]int) (path Pather, distance int, found bool) { - outsideCurrentLevel := outsideBoundary(pf.data, to) - collisionGrid := pf.data.CollisionGrid - - collisionGridOffset := data.Position{ - X: 0, - Y: 0, - } - - if outsideCurrentLevel { - lvl, lvlFound := pf.gr.GetCachedMapData(false).LevelDataForCoords(to, pf.data.PlayerUnit.Area.Area()) - if !lvlFound { - panic("Error occurred calculating path, destination point outside current level and matching level not found") - } - - // We're not going to calculate intersection, instead we're going to expand the collision grid, set the new one - // starting point where it is supposed to be and cross fingers to make them match - relativeStartX, relativeStartY := lvl.Offset.X-pf.data.AreaOrigin.X, lvl.Offset.Y-pf.data.AreaOrigin.Y - - collisionGridOffset = data.Position{ - X: relativeStartX, - Y: relativeStartY, - } - - // Let's create a new collision grid with the new size - expandedCG := make([][]bool, len(collisionGrid)+len(lvl.CollisionGrid)) - for i := range expandedCG { - expandedCG[i] = make([]bool, len(collisionGrid[0])+len(lvl.CollisionGrid[0])) - } - - // Let's copy both collision grids into the new one - for y, row := range pf.data.CollisionGrid { - cgY := y - if relativeStartY < 0 { - cgY = y + int(math.Abs(float64(relativeStartY))) - } - for x := range row { - cgX := x - if relativeStartX < 0 { - cgX = int(math.Abs(float64(relativeStartX))) + x - } - - if cgY+1 >= len(expandedCG) || cgX+1 >= len(expandedCG[cgY]) { - continue - } - expandedCG[cgY][cgX] = pf.data.CollisionGrid[y][x] - } - } - - for y, row := range lvl.CollisionGrid { - cgY := y - if relativeStartY > 0 { - cgY = y + int(math.Abs(float64(relativeStartY))) - } - for x := range row { - cgX := x - if relativeStartX > 0 { - cgX = int(math.Abs(float64(relativeStartX))) + x - } - - if cgY+1 >= len(expandedCG) || cgX+1 >= len(expandedCG[cgY]) { - continue - } - expandedCG[cgY][cgX] = lvl.CollisionGrid[y][x] - } - } - - collisionGrid = expandedCG - } - - // Convert to relative coordinates (Current player position) - fromX, fromY := relativePosition(pf.data, from, collisionGridOffset) - - // Convert to relative coordinates (Target position) - toX, toY := relativePosition(pf.data, to, collisionGridOffset) - - // Ensure the target coordinates are within the collision grid bounds before accessing - if toX < 0 || toX >= len(collisionGrid[0]) || toY < 0 || toY >= len(collisionGrid) { - return nil, 0, false - } - - // Ensure the origin coordinates are within the collision grid bounds before accessing - if fromX < 0 || fromX >= len(collisionGrid[0]) || fromY < 0 || fromY >= len(collisionGrid) { - return nil, 0, false - } - - // Origin and destination are the same point - if fromX == toX && fromY == toY { - return nil, 0, true - } - - // Lut Gholein map is a bit bugged, we should close this fake path to avoid pathing issues - if pf.data.PlayerUnit.Area == area.LutGholein { - collisionGrid[13][210] = false - } - - // Cache the world map, so we don't need to calculate it every time - worldCacheHash := fmt.Sprintf("%d-%d-%d-%d", pf.gr.CachedMapSeed, pf.data.PlayerUnit.Area, len(collisionGrid), len(collisionGrid[0])) - if pf.worldCacheHash != worldCacheHash { - pf.worldCache = parseWorld(collisionGrid, pf.data) - pf.worldCacheHash = worldCacheHash - } - - // Set Origin and Destination points - pf.worldCache.SetFrom(data.Position{X: fromX, Y: fromY}) - pf.worldCache.SetTo(data.Position{X: toX, Y: toY}) - - // Add some padding to the origin/destination, sometimes when the origin or destination are close to a non-walkable - // area, pather is not able to calculate the path, so we add some padding around origin/dest to avoid this - // If character can not teleport if apply this hacky thing it will try to kill monsters across walls - if pf.data.CanTeleport() { - for i := -3; i < 4; i++ { - for k := -3; k < 4; k++ { - pf.worldCache.SetTile(pf.worldCache.NewTile(KindPlain, ensureValueInCG(toX+k, len(collisionGrid[0])), ensureValueInCG(toY+i, len(collisionGrid)))) - } - } - } - - for _, cord := range blacklistedCoords { - if len(collisionGrid) < cord[1] && len(collisionGrid[0]) < cord[1] { - pf.worldCache.SetTile(pf.worldCache.NewTile(KindBlocker, cord[0], cord[1])) - } - } - - // aster path is returning the effort to reach that point, but we want to know the real distance in tiles, we count the tiles in the path - p, _, found := astar.Path(pf.worldCache.From(), pf.worldCache.To()) - - // Debug only, this will render a png file with map and origin/destination points - if config.Koolo.Debug.RenderMap { - pf.worldCache.renderPathImg(pf.data, p, collisionGridOffset) - } - - return p, len(p), found -} - -func (pf *PathFinder) GetPath(to data.Position, blacklistedCoords ...[2]int) (path Pather, distance int, found bool) { - return pf.GetPathFrom(pf.data.PlayerUnit.Position, to, blacklistedCoords...) -} - -func ensureValueInCG(val, cgSize int) int { - if val < 0 { - return 0 - } - - if val >= cgSize { - return cgSize - 1 - } - - return val -} - -func (pf *PathFinder) GetClosestWalkablePath(dest data.Position, blacklistedCoords ...[2]int) (path Pather, distance int, found bool) { - return pf.GetClosestWalkablePathFrom(pf.data.PlayerUnit.Position, dest, blacklistedCoords...) -} - -func (pf *PathFinder) GetClosestWalkablePathFrom(from, dest data.Position, blacklistedCoords ...[2]int) (path Pather, distance int, found bool) { - if IsWalkable(dest, pf.data.AreaOrigin, pf.data.CollisionGrid) || outsideBoundary(pf.data, dest) { - path, distance, found = pf.GetPath(dest, blacklistedCoords...) - if found { - return - } - } - - maxRange := 20 - step := 4 - dst := 1 - - for dst < maxRange { - for i := -dst; i < dst; i += 1 { - for j := -dst; j < dst; j += 1 { - if math.Abs(float64(i)) >= math.Abs(float64(dst)) || math.Abs(float64(j)) >= math.Abs(float64(dst)) { - cgY := dest.Y - pf.data.AreaOrigin.Y + j - cgX := dest.X - pf.data.AreaOrigin.X + i - if cgX > 0 && cgY > 0 && len(pf.data.CollisionGrid) > cgY && len(pf.data.CollisionGrid[cgY]) > cgX && pf.data.CollisionGrid[cgY][cgX] { - return pf.GetPathFrom(from, data.Position{ - X: dest.X + i, - Y: dest.Y + j, - }, blacklistedCoords...) - } - } - } - } - dst += step - } - - return nil, 0, false -} - -func (pf *PathFinder) MoveThroughPath(p Pather, distance int) { - moveTo := p[0].(*Tile) - if distance > 0 && len(p) > distance { - moveTo = p[len(p)-distance].(*Tile) - } - - screenX, screenY := pf.gameCoordsToScreenCords(p[len(p)-1].(*Tile).X, p[len(p)-1].(*Tile).Y, moveTo.X, moveTo.Y) - // Prevent mouse overlap the HUD - if screenY > int(float32(pf.gr.GameAreaSizeY)/1.21) { - screenY = int(float32(pf.gr.GameAreaSizeY) / 1.21) - } - - if distance > 0 { - pf.MoveCharacter(screenX, screenY) - } -} - -func (pf *PathFinder) MoveCharacter(x, y int) { - if pf.data.CanTeleport() { - pf.hid.Click(game.RightButton, x, y) - } else { - pf.hid.MovePointer(x, y) - pf.hid.PressKeyBinding(pf.data.KeyBindings.ForceMove) - helper.Sleep(50) - } -} - -func (pf *PathFinder) GameCoordsToScreenCords(destinationX, destinationY int) (int, int) { - return pf.gameCoordsToScreenCords(pf.data.PlayerUnit.Position.X, pf.data.PlayerUnit.Position.Y, destinationX, destinationY) -} - -func (pf *PathFinder) gameCoordsToScreenCords(playerX, playerY, destinationX, destinationY int) (int, int) { - // Calculate diff between current player position and destination - diffX := destinationX - playerX - diffY := destinationY - playerY - - // Transform cartesian movement (World) to isometric (screen) - // Helpful documentation: https://clintbellanger.net/articles/isometric_math/ - screenX := int((float32(diffX-diffY) * 19.8) + float32(pf.gr.GameAreaSizeX/2)) - screenY := int((float32(diffX+diffY) * 9.9) + float32(pf.gr.GameAreaSizeY/2)) - - return screenX, screenY -} - -func (pf *PathFinder) RandomMovement(d game.Data) { - midGameX := pf.gr.GameAreaSizeX / 2 - midGameY := pf.gr.GameAreaSizeY / 2 - x := midGameX + rand.Intn(midGameX) - (midGameX / 2) - y := midGameY + rand.Intn(midGameY) - (midGameY / 2) - pf.hid.MovePointer(x, y) - pf.hid.PressKeyBinding(d.KeyBindings.ForceMove) - helper.Sleep(50) -} - -func (pf *PathFinder) DistanceFromMe(p data.Position) int { - return DistanceFromPoint(pf.data.PlayerUnit.Position, p) -} - -func (pf *PathFinder) OptimizeRoomsTraverseOrder() []data.Room { - distanceMatrix := make(map[data.Room]map[data.Room]int) - - for _, room1 := range pf.data.Rooms { - distanceMatrix[room1] = make(map[data.Room]int) - for _, room2 := range pf.data.Rooms { - if room1 != room2 { - _, distance, found := pf.GetClosestWalkablePathFrom(room1.GetCenter(), room2.GetCenter()) - if found { - distanceMatrix[room1][room2] = distance - } else { - distanceMatrix[room1][room2] = math.MaxInt - } - } else { - distanceMatrix[room1][room2] = 0 - } - } - } - - currentRoom := data.Room{} - for _, r := range pf.data.Rooms { - if r.IsInside(pf.data.PlayerUnit.Position) { - currentRoom = r - } - } - - visited := make(map[data.Room]bool) - order := []data.Room{currentRoom} - visited[currentRoom] = true - - for len(order) < len(pf.data.Rooms) { - nextRoom := data.Room{} - minDistance := math.MaxInt - - // Find the nearest unvisited room - for _, room := range pf.data.Rooms { - if !visited[room] && distanceMatrix[currentRoom][room] < minDistance { - nextRoom = room - minDistance = distanceMatrix[currentRoom][room] - } - } - - // Add the next room to the order of visit - order = append(order, nextRoom) - visited[nextRoom] = true - currentRoom = nextRoom - } - - return order -} - -func relativePosition(d *game.Data, p data.Position, cgOffset data.Position) (int, int) { - x, y := p.X-d.AreaOrigin.X, p.Y-d.AreaOrigin.Y - - if cgOffset.X < 0 { - x += int(math.Abs(float64(cgOffset.X))) - } - - if cgOffset.Y < 0 { - y += int(math.Abs(float64(cgOffset.Y))) - } - - return x, y -} - -func outsideBoundary(d *game.Data, p data.Position) bool { - relativeToX := p.X - d.AreaOrigin.X - relativeToY := p.Y - d.AreaOrigin.Y - - return relativeToX < 0 || relativeToY < 0 || relativeToX > len(d.CollisionGrid[0]) || relativeToY > len(d.CollisionGrid) -} diff --git a/internal/v2/pather/path_finding_tools.go b/internal/v2/pather/path_finding_tools.go deleted file mode 100644 index 67b4ebeb..00000000 --- a/internal/v2/pather/path_finding_tools.go +++ /dev/null @@ -1,100 +0,0 @@ -package pather - -import ( - "math" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/koolo/internal/game" -) - -// parseWorld parses a textual representation of a World into a World map. -func parseWorld(collisionGrid [][]bool, d *game.Data) World { - gridSizeX := len(collisionGrid[0]) - gridSizeY := len(collisionGrid) - - w := World{ - World: make([][]*Tile, gridSizeX), - } - for x := 0; x < gridSizeX; x++ { - w.World[x] = make([]*Tile, gridSizeY) - } - - for x, xValues := range collisionGrid { - for y, walkable := range xValues { - kind := KindBlocker - - // Hacky solution to avoid Arcane Sanctuary A* errors - if d.PlayerUnit.Area == area.ArcaneSanctuary && d.CanTeleport() { - kind = KindSoftBlocker - } - - if walkable { - // Add some padding around non-walkable areas, this prevents problems when cornering without teleport - if !d.CanTeleport() && ((y > 1 && (!xValues[y-1] || !xValues[y-2])) || (y < len(xValues)-2 && (!xValues[y+1] || !xValues[y+2])) || - (x > 1 && (!collisionGrid[x-1][y] || !collisionGrid[x-2][y])) || (x < len(collisionGrid)-2 && (!collisionGrid[x+1][y] || !collisionGrid[x+2][y]))) { - kind = KindSoftBlocker - } else { - kind = KindPlain - } - } - - w.SetTile(w.NewTile(kind, y, x)) - } - } - - return w -} - -func IsNarrowMap(a area.ID) bool { - switch a { - case area.MaggotLairLevel1, area.MaggotLairLevel2, area.MaggotLairLevel3, area.ArcaneSanctuary, area.ClawViperTempleLevel2, area.RiverOfFlame, area.ChaosSanctuary: - return true - } - - return false -} - -func DistanceFromPoint(from data.Position, to data.Position) int { - first := math.Pow(float64(to.X-from.X), 2) - second := math.Pow(float64(to.Y-from.Y), 2) - - return int(math.Sqrt(first + second)) -} - -func IsWalkable(pos data.Position, areaOriginPos data.Position, collisionGrid [][]bool) bool { - indexX := pos.X - areaOriginPos.X - indexY := pos.Y - areaOriginPos.Y - - // When we are close to the level border, we need to check if monster is outside the collision grid - if indexX < 0 || indexY < 0 || indexY >= len(collisionGrid) || indexX >= len(collisionGrid[indexY]) { - return false - } - - return collisionGrid[indexY][indexX] -} - -// FindFirstWalkable finds the first walkable position from a given position and radius -func FindFirstWalkable(from data.Position, areaOriginPos data.Position, grid [][]bool, radius int) (int, int) { - startX := from.X - areaOriginPos.X - startY := from.Y - areaOriginPos.Y - - for r := radius; r >= 0; r-- { - for dx := -r; dx <= r; dx++ { - dy := int(math.Sqrt(float64(r*r - dx*dx))) - positions := [][2]int{ - {startX + dx, startY + dy}, - {startX + dx, startY - dy}, - {startX - dx, startY + dy}, - {startX - dx, startY - dy}, - } - for _, pos := range positions { - newX, newY := pos[0]+areaOriginPos.X, pos[1]+areaOriginPos.Y - if pos[0] >= 0 && pos[0] < len(grid) && pos[1] >= 0 && pos[1] < len(grid[0]) && IsWalkable(data.Position{newX, newY}, areaOriginPos, grid) { - return newX, newY - } - } - } - } - return -1, -1 -} diff --git a/internal/v2/pather/rooms.go b/internal/v2/pather/rooms.go deleted file mode 100644 index 9db0c2ff..00000000 --- a/internal/v2/pather/rooms.go +++ /dev/null @@ -1,33 +0,0 @@ -package pather - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/game" -) - -func (pf *PathFinder) GetWalkableRooms(d game.Data) []data.Position { - roomPositions := make([]data.Position, 0) - - for _, room := range d.Rooms { - found := false - for y := range room.Height - 1 { - for x := range room.Width - 1 { - if d.CollisionGrid[room.Y-d.AreaOrigin.Y+y][room.X-d.AreaOrigin.X+x] { - pos := data.Position{ - X: room.Position.X + x, - Y: room.Position.Y + y, - } - if _, _, found = pf.GetPath(pos); found { - roomPositions = append(roomPositions, pos) - break - } - } - } - if found { - break - } - } - } - - return roomPositions -} diff --git a/internal/v2/pather/tile.go b/internal/v2/pather/tile.go deleted file mode 100644 index 766f749f..00000000 --- a/internal/v2/pather/tile.go +++ /dev/null @@ -1,72 +0,0 @@ -package pather - -import "github.com/beefsack/go-astar" - -var allowedMovement = [][]int{ - {-1, 0}, - {1, 0}, - {0, -1}, - {0, 1}, -} - -// Kind* constants refer to tile kinds for input and output. -const ( - // KindPlain (.) is a plain tile with a movement cost of 1. - KindPlain uint8 = iota - // KindSoftBlocker (S) is a tile to fake some blocking areas with increased cost of moving, like Arcane Sanctuary platforms - KindSoftBlocker - // KindBlocker (X) is a tile which blocks movement. - KindBlocker -) - -// KindCosts map tile kinds to movement costs. -var KindCosts = map[uint8]float64{ - KindPlain: 1.0, - KindSoftBlocker: 1000, -} - -// A Tile is a tile in a grid which implements Pather. -type Tile struct { - Walkable bool - Cost float64 - X, Y int - Kind uint8 - W *World -} - -// PathNeighbors returns the neighbors of the tile, excluding blockers and -// tiles off the edge of the board. -func (t *Tile) PathNeighbors() []astar.Pather { - if t == nil { - return []astar.Pather{} - } - - neighbors := make([]astar.Pather, 0, 4) - for _, offset := range allowedMovement { - if n := t.W.Tile(t.X+offset[0], t.Y+offset[1]); n != nil && n.Walkable { - neighbors = append(neighbors, n) - } - } - - return neighbors -} - -// PathNeighborCost returns the movement cost of the directly neighboring tile. -func (t *Tile) PathNeighborCost(to astar.Pather) float64 { - return to.(*Tile).Cost -} - -// PathEstimatedCost uses Manhattan distance to estimate orthogonal distance -// between non-adjacent nodes. -func (t *Tile) PathEstimatedCost(to astar.Pather) float64 { - toT := to.(*Tile) - absX := toT.X - t.X - if absX < 0 { - absX = -absX - } - absY := toT.Y - t.Y - if absY < 0 { - absY = -absY - } - return float64(absX + absY) -} diff --git a/internal/v2/pather/world.go b/internal/v2/pather/world.go deleted file mode 100644 index b38647d3..00000000 --- a/internal/v2/pather/world.go +++ /dev/null @@ -1,140 +0,0 @@ -package pather - -import ( - "fmt" - "image" - "image/color" - "image/draw" - "image/png" - "os" - - "github.com/beefsack/go-astar" - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/koolo/internal/game" -) - -type World struct { - World [][]*Tile - from data.Position - to data.Position -} - -// Tile gets the tile at the given coordinates in the World. -func (w *World) Tile(x, y int) *Tile { - if x < 0 || x > len(w.World)-1 || w.World[x] == nil || y > len(w.World[x])-1 || y < 0 { - return nil - } - - return w.World[x][y] -} - -// SetTile sets a tile at the given coordinates in the World, if the World is big enough. -func (w *World) SetTile(t *Tile) { - if len(w.World) >= t.X+1 && len(w.World[t.X]) >= t.Y+1 { - w.World[t.X][t.Y] = t - } -} - -func (w *World) NewTile(kind uint8, x, y int) *Tile { - walkable := false - cost := 0.0 - if kind != KindBlocker { - walkable = true - cost = KindCosts[kind] - } - - return &Tile{ - Kind: kind, - Walkable: walkable, - Cost: cost, - X: x, - Y: y, - W: w, - } -} - -func (w *World) From() *Tile { - return w.World[w.from.X][w.from.Y] -} - -func (w *World) To() *Tile { - return w.World[w.to.X][w.to.Y] -} - -func (w *World) SetFrom(position data.Position) { - w.from = position -} -func (w *World) SetTo(position data.Position) { - w.to = position -} - -// RenderPathImg renders a path on top of a World. -func (w *World) renderPathImg(d *game.Data, path []astar.Pather, cgOffset data.Position) { - width := len(w.World) - if width == 0 { - return - } - height := len(w.World[0]) - - img := image.NewRGBA(image.Rect(0, 0, width, height)) - draw.Draw(img, img.Bounds(), img, image.Point{}, draw.Over) - - pathLocs := map[string]bool{} - for _, p := range path { - pT := p.(*Tile) - pathLocs[fmt.Sprintf("%d,%d", pT.X, pT.Y)] = true - } - for x := 0; x < width; x++ { - for y := 0; y < height; y++ { - t := w.Tile(x, y) - if pathLocs[fmt.Sprintf("%d,%d", x, y)] { - img.Set(x, y, color.RGBA{ - R: 36, - G: 255, - B: 0, - A: 255, - }) - } else if t != nil { - switch t.Kind { - case KindPlain: - img.Set(x, y, color.White) - case KindBlocker: - img.Set(x, y, color.Black) - case KindSoftBlocker: - img.Set(x, y, color.RGBA{238, 238, 238, 255}) - } - } - } - } - - for _, r := range d.Rooms { - rPosX, rPosY := relativePosition(d, r.GetCenter(), cgOffset) - img.Set(rPosX, rPosY, color.RGBA{204, 204, 0, 255}) - } - - for _, o := range d.Objects { - oPosX, oPosY := relativePosition(d, o.Position, cgOffset) - if o.IsDoor() { - img.Set(oPosX, oPosY, color.RGBA{101, 67, 33, 255}) - } else { - img.Set(oPosX, oPosY, color.RGBA{255, 165, 0, 255}) - } - } - - for _, m := range d.Monsters { - mPosX, mPosY := relativePosition(d, m.Position, cgOffset) - img.Set(mPosX, mPosY, color.RGBA{255, 0, 255, 255}) - } - - img.Set(w.From().X, w.From().Y, color.RGBA{ - R: 255, G: 0, B: 0, A: 255, - }) - - img.Set(w.To().X, w.To().Y, color.RGBA{ - R: 0, G: 0, B: 255, A: 255, - }) - - outFile, _ := os.Create("cg.png") - defer outFile.Close() - png.Encode(outFile, img) -} diff --git a/internal/v2/run/ancient_tunnels.go b/internal/v2/run/ancient_tunnels.go deleted file mode 100644 index 157ef05f..00000000 --- a/internal/v2/run/ancient_tunnels.go +++ /dev/null @@ -1,48 +0,0 @@ -package run - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/action" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -type AncientTunnels struct { - ctx *context.Status -} - -func NewAncientTunnels() *AncientTunnels { - return &AncientTunnels{ - ctx: context.Get(), - } -} - -func (a AncientTunnels) Name() string { - return string(config.AncientTunnelsRun) -} - -func (a AncientTunnels) Run() error { - openChests := a.ctx.CharacterCfg.Game.AncientTunnels.OpenChests - onlyElites := a.ctx.CharacterCfg.Game.AncientTunnels.FocusOnElitePacks - filter := data.MonsterAnyFilter() - - if onlyElites { - filter = data.MonsterEliteFilter() - } - - err := action.WayPoint(area.LostCity) // Moving to starting point (Lost City) - if err != nil { - return err - } - - err = action.MoveToArea(area.AncientTunnels) // Travel to ancient tunnels - if err != nil { - return err - } - action.OpenTPIfLeader() - - // Clear Ancient Tunnels - - return action.ClearCurrentLevel(openChests, filter) -} diff --git a/internal/v2/run/andariel.go b/internal/v2/run/andariel.go deleted file mode 100644 index 3bb12bcb..00000000 --- a/internal/v2/run/andariel.go +++ /dev/null @@ -1,105 +0,0 @@ -package run - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/action" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -var andarielStartingPosition = data.Position{ - X: 22561, - Y: 9553, -} - -var andarielClearPos1 = data.Position{ - X: 22570, - Y: 9591, -} - -var andarielClearPos2 = data.Position{ - X: 22547, - Y: 9593, -} - -var andarielClearPos3 = data.Position{ - X: 22533, - Y: 9591, -} - -var andarielClearPos4 = data.Position{ - X: 22535, - Y: 9579, -} - -var andarielClearPos5 = data.Position{ - X: 22548, - Y: 9580, -} - -var andarielAttackPos1 = data.Position{ - X: 22548, - Y: 9570, -} - -// Placeholder for second attack position -//var andarielAttackPos2 = data.Position{ -// X: 22548, -// Y: 9590, -//} - -type Andariel struct { - ctx *context.Status -} - -func NewAndariel() *Andariel { - return &Andariel{ - ctx: context.Get(), - } -} - -func (a Andariel) Name() string { - return string(config.AndarielRun) -} - -func (a Andariel) Run() error { - // Moving to Catacombs Level 4 - a.ctx.Logger.Info("Moving to Catacombs 4") - err := action.WayPoint(area.CatacombsLevel2) - if err != nil { - return err - } - - err = action.MoveToArea(area.CatacombsLevel3) - action.MoveToArea(area.CatacombsLevel4) - if err != nil { - return err - } - - // Clearing inside room - a.ctx.Logger.Info("Clearing inside room") - - if a.ctx.CharacterCfg.Game.Andariel.ClearRoom { - action.MoveToCoords(andarielClearPos1) - action.ClearAreaAroundPlayer(10, data.MonsterAnyFilter()) - action.MoveToCoords(andarielClearPos2) - action.ClearAreaAroundPlayer(10, data.MonsterAnyFilter()) - action.MoveToCoords(andarielClearPos3) - action.ClearAreaAroundPlayer(10, data.MonsterAnyFilter()) - action.MoveToCoords(andarielClearPos4) - action.ClearAreaAroundPlayer(10, data.MonsterAnyFilter()) - action.MoveToCoords(andarielClearPos5) - action.ClearAreaAroundPlayer(10, data.MonsterAnyFilter()) - action.MoveToCoords(andarielAttackPos1) - action.ClearAreaAroundPlayer(20, data.MonsterAnyFilter()) - - } else { - action.MoveToCoords(andarielStartingPosition) - } - - // Attacking Andariel - a.ctx.Logger.Info("Killing Andariel") - - return a.ctx.Char.KillAndariel() -} diff --git a/internal/v2/run/arachnid_lair.go b/internal/v2/run/arachnid_lair.go deleted file mode 100644 index b4460ce5..00000000 --- a/internal/v2/run/arachnid_lair.go +++ /dev/null @@ -1,40 +0,0 @@ -package run - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/action" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -type ArachnidLair struct { - ctx *context.Status -} - -func NewArachnidLair() *ArachnidLair { - return &ArachnidLair{ - ctx: context.Get(), - } -} - -func (a ArachnidLair) Name() string { - return string(config.ArachnidLairRun) -} - -func (a ArachnidLair) Run() error { - err := action.WayPoint(area.SpiderForest) - if err != nil { - return err - } - - err = action.MoveToArea(area.SpiderCave) - if err != nil { - return err - } - - action.OpenTPIfLeader() - - // Clear ArachnidLair - return action.ClearCurrentLevel(true, data.MonsterAnyFilter()) -} diff --git a/internal/v2/run/baal.go b/internal/v2/run/baal.go deleted file mode 100644 index 3b53f260..00000000 --- a/internal/v2/run/baal.go +++ /dev/null @@ -1,147 +0,0 @@ -package run - -import ( - "errors" - - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/utils" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/koolo/internal/pather" - "github.com/hectorgimenez/koolo/internal/v2/action" -) - -var baalThronePosition = data.Position{ - X: 15095, - Y: 5042, -} - -type Baal struct { - ctx *context.Status -} - -func NewBaal() *Baal { - return &Baal{ - ctx: context.Get(), - } -} - -func (s Baal) Name() string { - return string(config.BaalRun) -} - -func (s Baal) Run() error { - // Set filter - filter := data.MonsterAnyFilter() - if s.ctx.CharacterCfg.Game.Baal.OnlyElites { - filter = data.MonsterEliteFilter() - } - - err := action.WayPoint(area.TheWorldStoneKeepLevel2) - if err != nil { - return err - } - - if s.ctx.CharacterCfg.Game.Baal.ClearFloors { - action.ClearCurrentLevel(false, filter) - } - - err = action.MoveToArea(area.TheWorldStoneKeepLevel3) - if err != nil { - return err - } - - if s.ctx.CharacterCfg.Game.Baal.ClearFloors { - action.ClearCurrentLevel(false, filter) - } - - err = action.MoveToArea(area.ThroneOfDestruction) - if err != nil { - return err - } - err = action.MoveToCoords(baalThronePosition) - if err != nil { - return err - } - if s.checkForSoulsOrDolls() { - return errors.New("souls or dolls detected, skipping") - } - - // Let's move to a safe area and open the portal in companion mode - if s.ctx.CharacterCfg.Companion.Enabled && s.ctx.CharacterCfg.Companion.Leader { - action.MoveToCoords(data.Position{ - X: 15116, - Y: 5071, - }) - action.OpenTPIfLeader() - } - - err = action.ClearAreaAroundPlayer(50, data.MonsterAnyFilter()) - if err != nil { - return err - } - - // Force rebuff before waves - action.Buff() - - // Come back to previous position - err = action.MoveToCoords(baalThronePosition) - if err != nil { - return err - } - - lastWave := false - for !lastWave { - if _, found := s.ctx.Data.Monsters.FindOne(npc.BaalsMinion, data.MonsterTypeMinion); found { - lastWave = true - } - - enemies := false - for _, e := range s.ctx.Data.Monsters.Enemies() { - dist := pather.DistanceFromPoint(baalThronePosition, e.Position) - if dist < 50 { - enemies = true - } - } - if enemies { - err = action.ClearAreaAroundPlayer(50, data.MonsterAnyFilter()) - if err != nil { - return err - } - } - action.MoveToCoords(baalThronePosition) - } - - _, isLevelingChar := s.ctx.Char.(context.LevelingCharacter) - if s.ctx.CharacterCfg.Game.Baal.KillBaal || isLevelingChar { - utils.Sleep(10000) - action.Buff() - baalPortal, _ := s.ctx.Data.Objects.FindOne(object.BaalsPortal) - err = action.InteractObjectByID(baalPortal.ID, func() bool { - return s.ctx.Data.PlayerUnit.Area == area.TheWorldstoneChamber - }) - if err != nil { - return err - } - return s.ctx.Char.KillBaal() - } - - return nil -} - -func (s Baal) checkForSoulsOrDolls() bool { - var npcIds []npc.ID - - if s.ctx.CharacterCfg.Game.Baal.DollQuit { - npcIds = append(npcIds, npc.UndeadStygianDoll2, npc.UndeadSoulKiller2) - } - if s.ctx.CharacterCfg.Game.Baal.SoulQuit { - npcIds = append(npcIds, npc.BlackSoul2, npc.BurningSoul2) - } - - return len(npcIds) != 0 -} diff --git a/internal/v2/run/countess.go b/internal/v2/run/countess.go deleted file mode 100644 index 09015808..00000000 --- a/internal/v2/run/countess.go +++ /dev/null @@ -1,68 +0,0 @@ -package run - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/action" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -type Countess struct { - ctx *context.Status -} - -func NewCountess() *Countess { - return &Countess{ - ctx: context.Get(), - } -} - -func (c Countess) Name() string { - return string(config.CountessRun) -} - -func (c Countess) Run() error { - // Travel to boss level - err := action.WayPoint(area.BlackMarsh) - if err != nil { - return err - } - - areas := []area.ID{ - area.ForgottenTower, - area.TowerCellarLevel1, - area.TowerCellarLevel2, - area.TowerCellarLevel3, - area.TowerCellarLevel4, - area.TowerCellarLevel5, - } - - for _, a := range areas { - err = action.MoveToArea(a) - if err != nil { - return err - } - } - - // Try to move around Countess area - action.MoveTo(func() (data.Position, bool) { - for _, o := range c.ctx.Data.Objects { - if o.Name == object.GoodChest { - return o.Position, true - } - } - - // Try to teleport over Countess in case we are not able to find the chest position, a bit more risky - if countess, found := c.ctx.Data.Monsters.FindOne(npc.DarkStalker, data.MonsterTypeSuperUnique); found { - return countess.Position, true - } - - return data.Position{}, false - }) - - // Kill Countess - return c.ctx.Char.KillCountess() -} diff --git a/internal/v2/run/pindleskin.go b/internal/v2/run/pindleskin.go deleted file mode 100644 index 1efe4d6b..00000000 --- a/internal/v2/run/pindleskin.go +++ /dev/null @@ -1,61 +0,0 @@ -package run - -import ( - "errors" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/action" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -var fixedPlaceNearRedPortal = data.Position{ - X: 5130, - Y: 5120, -} - -var pindleSafePosition = data.Position{ - X: 10058, - Y: 13236, -} - -type Pindleskin struct { - ctx *context.Status -} - -func NewPindleskin() *Pindleskin { - return &Pindleskin{ - ctx: context.Get(), - } -} - -func (p Pindleskin) Name() string { - return string(config.PindleskinRun) -} - -func (p Pindleskin) Run() error { - err := action.WayPoint(area.Harrogath) - if err != nil { - return err - } - - _ = action.MoveToCoords(fixedPlaceNearRedPortal) - - redPortal, found := p.ctx.Data.Objects.FindOne(object.PermanentTownPortal) - if !found { - return errors.New("red portal not found") - } - - err = action.InteractObject(redPortal, func() bool { - return p.ctx.Data.PlayerUnit.Area == area.NihlathaksTemple - }) - if err != nil { - return err - } - - _ = action.MoveToCoords(pindleSafePosition) - - return p.ctx.Char.KillPindle() -} diff --git a/internal/v2/run/run.go b/internal/v2/run/run.go deleted file mode 100644 index 59c40bca..00000000 --- a/internal/v2/run/run.go +++ /dev/null @@ -1,93 +0,0 @@ -package run - -import "github.com/hectorgimenez/koolo/internal/config" - -type Run interface { - Name() string - Run() error -} - -func BuildRuns(cfg *config.CharacterCfg) (runs []Run) { - //if cfg.Companion.Enabled && !cfg.Companion.Leader { - // return []Run{Companion{baseRun: baseRun}} - //} - // - //for _, run := range f.container.CharacterCfg.Game.Runs { - // // Prepend terror zone runs, we want to run it always first - // if run == config.TerrorZoneRun { - // tz := TerrorZone{baseRun: baseRun} - // - // if len(tz.AvailableTZs(d)) > 0 { - // runs = append(runs, tz) - // // If we are skipping other runs, we can return here - // if f.container.CharacterCfg.Game.TerrorZone.SkipOtherRuns { - // return runs - // } - // } - // } - //} - - for _, run := range cfg.Game.Runs { - switch run { - case config.CountessRun: - runs = append(runs, NewCountess()) - case config.AndarielRun: - runs = append(runs, NewAndariel()) - //case config.SummonerRun: - //runs = append(runs, Summoner{baseRun}) - //case config.DurielRun: - //runs = append(runs, Duriel{baseRun}) - //case config.MephistoRun: - //runs = append(runs, Mephisto{baseRun}) - case config.TravincalRun: - runs = append(runs, NewTravincal()) - //case config.DiabloRun: - //runs = append(runs, Diablo{ - // baseRun: baseRun, - // bm: f.bm, - //}) - //case config.EldritchRun: - //runs = append(runs, Eldritch{ - // baseRun: baseRun, - //}) - case config.PindleskinRun: - runs = append(runs, NewPindleskin()) - //case config.NihlathakRun: - //runs = append(runs, Nihlathak{baseRun}) - case config.AncientTunnelsRun: - runs = append(runs, NewAncientTunnels()) - //case config.MausoleumRun: - // runs = append(runs, Mausoleum{baseRun}) - //case config.PitRun: - // runs = append(runs, Pit{baseRun}) - //case config.StonyTombRun: - // runs = append(runs, StonyTomb{baseRun}) - case config.ArachnidLairRun: - runs = append(runs, NewArachnidLair()) - //case config.TristramRun: - // runs = append(runs, Tristram{baseRun}) - //case config.LowerKurastRun: - // runs = append(runs, LowerKurast{baseRun}) - //case config.LowerKurastChestRun: - // runs = append(runs, LowerKurastChest{baseRun}) - case config.BaalRun: - runs = append(runs, NewBaal()) - //case config.TalRashaTombsRun: - // runs = append(runs, TalRashaTombs{baseRun}) - //case config.LevelingRun: - // runs = append(runs, Leveling{baseRun: baseRun, bm: f.bm}) - //case config.QuestsRun: - // runs = append(runs, Quests{baseRun}) - //case config.CowsRun: - // runs = append(runs, Cows{baseRun}) - //case config.ThreshsocketRun: - // runs = append(runs, Threshsocket{baseRun}) - //case config.DrifterCavernRun: - // runs = append(runs, DrifterCavern{baseRun}) - //case config.EnduguRun: - // runs = append(runs, Endugu{baseRun}) - } - } - - return runs -} diff --git a/internal/v2/run/travincal.go b/internal/v2/run/travincal.go deleted file mode 100644 index 0b0b2b5f..00000000 --- a/internal/v2/run/travincal.go +++ /dev/null @@ -1,44 +0,0 @@ -package run - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/koolo/internal/config" - "github.com/hectorgimenez/koolo/internal/v2/action" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -type Council struct { - ctx *context.Status -} - -func NewTravincal() *Council { - return &Council{ - ctx: context.Get(), - } -} - -func (s Council) Name() string { - return string(config.TravincalRun) -} - -func (s Council) Run() error { - err := action.WayPoint(area.Travincal) // Moving to starting point (Travincal) - if err != nil { - return err - } - - for _, al := range s.ctx.Data.AdjacentLevels { - if al.Area == area.DuranceOfHateLevel1 { - err = action.MoveToCoords(data.Position{ - X: al.Position.X - 1, - Y: al.Position.Y + 3, - }) - if err != nil { - s.ctx.Logger.Warn("Error moving to council area", err) - } - } - } - - return s.ctx.Char.KillCouncil() -} diff --git a/internal/v2/town/A1.go b/internal/v2/town/A1.go deleted file mode 100644 index 4520b7b9..00000000 --- a/internal/v2/town/A1.go +++ /dev/null @@ -1,50 +0,0 @@ -package town - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/d2go/pkg/data/object" - "github.com/hectorgimenez/koolo/internal/game" -) - -type A1 struct { -} - -func (a A1) GamblingNPC() npc.ID { - return npc.Gheed -} - -func (a A1) HealNPC() npc.ID { - return npc.Akara -} - -func (a A1) MercContractorNPC() npc.ID { - return npc.Kashya -} - -func (a A1) RefillNPC() npc.ID { - return npc.Akara -} - -func (a A1) RepairNPC() npc.ID { - return npc.Charsi -} - -func (a A1) TPWaitingArea(d game.Data) data.Position { - rogueBonfire, found := d.Objects.FindOne(object.RogueBonfire) - if found { - return data.Position{ - X: rogueBonfire.Position.X + 10, - Y: rogueBonfire.Position.Y + 10, - } - } - - cain, _ := d.NPCs.FindOne(npc.Kashya) - - return cain.Positions[0] -} - -func (a A1) TownArea() area.ID { - return area.RogueEncampment -} diff --git a/internal/v2/town/A2.go b/internal/v2/town/A2.go deleted file mode 100644 index cb6ae565..00000000 --- a/internal/v2/town/A2.go +++ /dev/null @@ -1,42 +0,0 @@ -package town - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/game" -) - -type A2 struct { -} - -func (a A2) GamblingNPC() npc.ID { - return npc.Elzix -} - -func (a A2) HealNPC() npc.ID { - return npc.Fara -} - -func (a A2) MercContractorNPC() npc.ID { - return npc.Greiz -} - -func (a A2) RefillNPC() npc.ID { - return npc.Drognan -} - -func (a A2) RepairNPC() npc.ID { - return npc.Fara -} - -func (a A2) TPWaitingArea(_ game.Data) data.Position { - return data.Position{ - X: 5161, - Y: 5059, - } -} - -func (a A2) TownArea() area.ID { - return area.LutGholein -} diff --git a/internal/v2/town/A3.go b/internal/v2/town/A3.go deleted file mode 100644 index 80368690..00000000 --- a/internal/v2/town/A3.go +++ /dev/null @@ -1,42 +0,0 @@ -package town - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/game" -) - -type A3 struct { -} - -func (a A3) GamblingNPC() npc.ID { - return npc.Alkor -} - -func (a A3) HealNPC() npc.ID { - return npc.Ormus -} - -func (a A3) MercContractorNPC() npc.ID { - return npc.Asheara -} - -func (a A3) RefillNPC() npc.ID { - return npc.Ormus -} - -func (a A3) RepairNPC() npc.ID { - return npc.Hratli -} - -func (a A3) TPWaitingArea(_ game.Data) data.Position { - return data.Position{ - X: 5151, - Y: 5068, - } -} - -func (a A3) TownArea() area.ID { - return area.KurastDocks -} diff --git a/internal/v2/town/A4.go b/internal/v2/town/A4.go deleted file mode 100644 index f8929f3d..00000000 --- a/internal/v2/town/A4.go +++ /dev/null @@ -1,42 +0,0 @@ -package town - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/game" -) - -type A4 struct { -} - -func (a A4) GamblingNPC() npc.ID { - return npc.Jamella -} - -func (a A4) HealNPC() npc.ID { - return npc.Jamella -} - -func (a A4) MercContractorNPC() npc.ID { - return npc.Tyrael2 -} - -func (a A4) RefillNPC() npc.ID { - return npc.Jamella -} - -func (a A4) RepairNPC() npc.ID { - return npc.Halbu -} - -func (a A4) TPWaitingArea(_ game.Data) data.Position { - return data.Position{ - X: 5047, - Y: 5033, - } -} - -func (a A4) TownArea() area.ID { - return area.ThePandemoniumFortress -} diff --git a/internal/v2/town/A5.go b/internal/v2/town/A5.go deleted file mode 100644 index 36f69b5c..00000000 --- a/internal/v2/town/A5.go +++ /dev/null @@ -1,42 +0,0 @@ -package town - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/game" -) - -type A5 struct { -} - -func (a A5) GamblingNPC() npc.ID { - return npc.Drehya // Anya -} - -func (a A5) HealNPC() npc.ID { - return npc.Malah -} - -func (a A5) MercContractorNPC() npc.ID { - return npc.QualKehk -} - -func (a A5) RefillNPC() npc.ID { - return npc.Malah -} - -func (a A5) RepairNPC() npc.ID { - return npc.Larzuk -} - -func (a A5) TPWaitingArea(_ game.Data) data.Position { - return data.Position{ - X: 5104, - Y: 5019, - } -} - -func (a A5) TownArea() area.ID { - return area.Harrogath -} diff --git a/internal/v2/town/shop_manager.go b/internal/v2/town/shop_manager.go deleted file mode 100644 index fd4b6d41..00000000 --- a/internal/v2/town/shop_manager.go +++ /dev/null @@ -1,189 +0,0 @@ -package town - -import ( - "fmt" - "math/rand" - "time" - - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/d2go/pkg/nip" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/v2/context" - "github.com/hectorgimenez/koolo/internal/v2/ui" -) - -func BuyConsumables(forceRefill bool) { - ctx := context.Get() - - missingHealingPots := ctx.BeltManager.GetMissingCount(data.HealingPotion) - missingManaPots := ctx.BeltManager.GetMissingCount(data.ManaPotion) - - ctx.Logger.Debug(fmt.Sprintf("Buying: %d Healing potions and %d Mana potions", missingHealingPots, missingManaPots)) - - // We traverse the items in reverse order because vendor has the best potions at the end - pot, found := findFirstMatch("superhealingpotion", "greaterhealingpotion", "healingpotion", "lighthealingpotion", "minorhealingpotion") - if found && missingHealingPots > 0 { - BuyItem(pot, missingHealingPots) - missingHealingPots = 0 - } - - pot, found = findFirstMatch("supermanapotion", "greatermanapotion", "manapotion", "lightmanapotion", "minormanapotion") - // In Normal greater potions are expensive as we are low level, let's keep with cheap ones - if ctx.CharacterCfg.Game.Difficulty == "normal" { - pot, found = findFirstMatch("manapotion", "lightmanapotion", "minormanapotion") - } - if found && missingManaPots > 0 { - BuyItem(pot, missingManaPots) - missingManaPots = 0 - } - - if ShouldBuyTPs() || forceRefill { - if _, found := ctx.Data.Inventory.Find(item.TomeOfTownPortal, item.LocationInventory); !found { - ctx.Logger.Info("TP Tome not found, buying one...") - if itm, itmFound := ctx.Data.Inventory.Find(item.TomeOfTownPortal, item.LocationVendor); itmFound { - BuyItem(itm, 1) - } - } - ctx.Logger.Debug("Filling TP Tome...") - if itm, found := ctx.Data.Inventory.Find(item.ScrollOfTownPortal, item.LocationVendor); found { - buyFullStack(itm) - } - } - - if ShouldBuyIDs() || forceRefill { - if _, found := ctx.Data.Inventory.Find(item.TomeOfIdentify, item.LocationInventory); !found { - ctx.Logger.Info("ID Tome not found, buying one...") - if itm, itmFound := ctx.Data.Inventory.Find(item.TomeOfIdentify, item.LocationVendor); itmFound { - BuyItem(itm, 1) - } - } - ctx.Logger.Debug("Filling IDs Tome...") - if itm, found := ctx.Data.Inventory.Find(item.ScrollOfIdentify, item.LocationVendor); found { - buyFullStack(itm) - } - } - - keyQuantity, shouldBuyKeys := ShouldBuyKeys() - if shouldBuyKeys || forceRefill { - if itm, found := ctx.Data.Inventory.Find(item.Key, item.LocationVendor); found { - ctx.Logger.Debug("Vendor with keys detected, provisioning...") - qty, _ := itm.FindStat(stat.Quantity, 0) - if (qty.Value + keyQuantity) <= 12 { - buyFullStack(itm) - } - } - } -} - -func findFirstMatch(itemNames ...string) (data.Item, bool) { - ctx := context.Get() - for _, name := range itemNames { - if itm, found := ctx.Data.Inventory.Find(item.Name(name), item.LocationVendor); found { - return itm, true - } - } - - return data.Item{}, false -} - -func ShouldBuyTPs() bool { - portalTome, found := context.Get().Data.Inventory.Find(item.TomeOfTownPortal, item.LocationInventory) - if !found { - return true - } - - qty, found := portalTome.FindStat(stat.Quantity, 0) - - return qty.Value <= rand.Intn(5-1)+1 || !found -} - -func ShouldBuyIDs() bool { - idTome, found := context.Get().Data.Inventory.Find(item.TomeOfIdentify, item.LocationInventory) - if !found { - return true - } - - qty, found := idTome.FindStat(stat.Quantity, 0) - - return qty.Value <= rand.Intn(7-3)+1 || !found -} - -func ShouldBuyKeys() (int, bool) { - keys, found := context.Get().Data.Inventory.Find(item.Key, item.LocationInventory) - if !found { - return 12, false - } - - qty, found := keys.FindStat(stat.Quantity, 0) - if found && qty.Value >= 12 { - return 12, false - } - - return qty.Value, true -} - -func SellJunk() { - for _, i := range ItemsToBeSold() { - if context.Get().Data.CharacterCfg.Inventory.InventoryLock[i.Position.Y][i.Position.X] == 1 { - SellItem(i) - } - } -} - -func SellItem(i data.Item) { - ctx := context.Get() - screenPos := ui.GetScreenCoordsForItem(i) - - time.Sleep(500 * time.Millisecond) - ctx.HID.ClickWithModifier(game.LeftButton, screenPos.X, screenPos.Y, game.CtrlKey) - time.Sleep(500 * time.Millisecond) - ctx.Logger.Debug(fmt.Sprintf("Item %s [%s] sold", i.Desc().Name, i.Quality.ToString())) -} - -func BuyItem(i data.Item, quantity int) { - ctx := context.Get() - screenPos := ui.GetScreenCoordsForItem(i) - - time.Sleep(250 * time.Millisecond) - for k := 0; k < quantity; k++ { - ctx.HID.Click(game.RightButton, screenPos.X, screenPos.Y) - time.Sleep(900 * time.Millisecond) - ctx.Logger.Debug(fmt.Sprintf("Purchased %s [X:%d Y:%d]", i.Desc().Name, i.Position.X, i.Position.Y)) - } -} - -func buyFullStack(i data.Item) { - screenPos := ui.GetScreenCoordsForItem(i) - - context.Get().HID.ClickWithModifier(game.RightButton, screenPos.X, screenPos.Y, game.ShiftKey) - time.Sleep(500 * time.Millisecond) -} - -func ItemsToBeSold() (items []data.Item) { - ctx := context.Get() - for _, itm := range ctx.Data.Inventory.ByLocation(item.LocationInventory) { - if itm.IsFromQuest() { - continue - } - - if itm.Name == item.TomeOfTownPortal || itm.Name == item.TomeOfIdentify || itm.Name == item.Key || itm.Name == "WirtsLeg" { - continue - } - - if itm.IsRuneword { - continue - } - - if ctx.Data.CharacterCfg.Inventory.InventoryLock[itm.Position.Y][itm.Position.X] == 1 { - // If item is a full match will be stashed, we don't want to sell it - if _, result := ctx.Data.CharacterCfg.Runtime.Rules.EvaluateAll(itm); result == nip.RuleResultFullMatch && !itm.IsPotion() { - continue - } - items = append(items, itm) - } - } - - return -} diff --git a/internal/v2/town/town.go b/internal/v2/town/town.go deleted file mode 100644 index be75c1a3..00000000 --- a/internal/v2/town/town.go +++ /dev/null @@ -1,33 +0,0 @@ -package town - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/area" - "github.com/hectorgimenez/d2go/pkg/data/npc" - "github.com/hectorgimenez/koolo/internal/game" -) - -type Town interface { - RefillNPC() npc.ID - HealNPC() npc.ID - RepairNPC() npc.ID - MercContractorNPC() npc.ID - GamblingNPC() npc.ID - TPWaitingArea(d game.Data) data.Position - TownArea() area.ID -} - -func GetTownByArea(a area.ID) Town { - switch a.Act() { - case 1: - return A1{} - case 2: - return A2{} - case 3: - return A3{} - case 4: - return A4{} - } - - return A5{} -} diff --git a/internal/v2/ui/coordinates.go b/internal/v2/ui/coordinates.go deleted file mode 100644 index 4db528c9..00000000 --- a/internal/v2/ui/coordinates.go +++ /dev/null @@ -1,109 +0,0 @@ -package ui - -const ( - itemBoxSize = 33 - itemBoxSizeClassic = 35 - - inventoryTopLeftX = 846 - inventoryTopLeftXClassic = 663 - - inventoryTopLeftY = 369 - inventoryTopLeftYClassic = 379 - - topCornerVendorWindowX = 109 - topCornerVendorWindowXClassic = 275 - - topCornerVendorWindowY = 147 - topCornerVendorWindowYClassic = 149 - - MercAvatarPositionX = 36 - MercAvatarPositionXClassic = 208 - - MercAvatarPositionY = 39 - MercAvatarPositionYClassic = 53 - - CubeTransmuteBtnX = 273 - CubeTransmuteBtnXClassic = 451 - - CubeTransmuteBtnY = 411 - CubeTransmuteBtnYClassic = 405 - - CubeTakeItemX = 306 - CubeTakeItemXClassic = 484 - - CubeTakeItemY = 365 - CubeTakeItemYClassic = 358 - - WpTabStartX = 131 - WpTabStartXClassic = 266 - - WpTabStartY = 148 - WpTabStartYClassic = 95 - - WpTabSizeX = 57 - WpTabSizeXClassic = 75 - - WpListPositionX = 200 - WpListPositionXClassic = 357 - - WpListStartY = 158 - WpListStartYClassic = 145 - - WpAreaBtnHeight = 41 - WpAreaBtnHeightClassic = 43 - - RepairButtonX = 390 - RepairButtonXClassic = 602 - - RepairButtonY = 515 - RepairButtonYClassic = 557 - - AnvilCenterX = 272 - AnvilCenterY = 333 - AnvilBtnX = 272 - AnvilBtnY = 450 - - MainSkillButtonX = 596 - MainSkillButtonY = 693 - - SecondarySkillButtonX = 686 - SecondarySkillButtonY = 693 - - GambleRefreshButtonX = 390 - GambleRefreshButtonXClassic = 540 - - GambleRefreshButtonY = 515 - GambleRefreshButtonYClassic = 553 - - SecondarySkillListFirstSkillX = 687 - MainSkillListFirstSkillX = 592 - SkillListFirstSkillY = 590 - SkillListSkillOffset = 45 - - FirstMercFromContractorListX = 175 - FirstMercFromContractorListY = 142 - - StashGoldBtnX = 966 - StashGoldBtnXClassic = 754 - - StashGoldBtnY = 526 - StashGoldBtnYClassic = 552 - - StashGoldBtnConfirmX = 547 - StashGoldBtnConfirmXClassic = 579 - - StashGoldBtnConfirmY = 388 - StashGoldBtnConfirmYClassic = 423 - - SwitchStashTabBtnX = 107 - SwitchStashTabBtnXClassic = 258 - - SwitchStashTabBtnY = 128 - SwitchStashTabBtnYClassic = 84 - - SwitchStashTabBtnTabSize = 82 - SwitchStashTabBtnTabSizeClassic = 96 - - CloseMiniPanelClassicX = 639 - CloseMiniPanelClassicY = 686 -) diff --git a/internal/v2/ui/ui_manager.go b/internal/v2/ui/ui_manager.go deleted file mode 100644 index 6b5345d0..00000000 --- a/internal/v2/ui/ui_manager.go +++ /dev/null @@ -1,46 +0,0 @@ -package ui - -import ( - "github.com/hectorgimenez/d2go/pkg/data" - "github.com/hectorgimenez/d2go/pkg/data/item" - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func GetScreenCoordsForItem(itm data.Item) data.Position { - ctx := context.Get() - if ctx.GameReader.LegacyGraphics() { - return getScreenCoordsForItemClassic(itm) - } - - return getScreenCoordsForItem(itm) -} - -func getScreenCoordsForItem(itm data.Item) data.Position { - switch itm.Location.LocationType { - case item.LocationVendor, item.LocationStash, item.LocationSharedStash: - x := topCornerVendorWindowX + itm.Position.X*itemBoxSize + (itemBoxSize / 2) - y := topCornerVendorWindowY + itm.Position.Y*itemBoxSize + (itemBoxSize / 2) - - return data.Position{X: x, Y: y} - } - - x := inventoryTopLeftX + itm.Position.X*itemBoxSize + (itemBoxSize / 2) - y := inventoryTopLeftY + itm.Position.Y*itemBoxSize + (itemBoxSize / 2) - - return data.Position{X: x, Y: y} -} - -func getScreenCoordsForItemClassic(itm data.Item) data.Position { - switch itm.Location.LocationType { - case item.LocationVendor, item.LocationStash, item.LocationSharedStash: - x := topCornerVendorWindowXClassic + itm.Position.X*itemBoxSizeClassic + (itemBoxSizeClassic / 2) - y := topCornerVendorWindowYClassic + itm.Position.Y*itemBoxSizeClassic + (itemBoxSizeClassic / 2) - - return data.Position{X: x, Y: y} - } - - x := inventoryTopLeftXClassic + itm.Position.X*itemBoxSizeClassic + (itemBoxSizeClassic / 2) - y := inventoryTopLeftYClassic + itm.Position.Y*itemBoxSizeClassic + (itemBoxSizeClassic / 2) - - return data.Position{X: x, Y: y} -} diff --git a/internal/v2/utils/image.go b/internal/v2/utils/image.go deleted file mode 100644 index 02dfeca8..00000000 --- a/internal/v2/utils/image.go +++ /dev/null @@ -1,17 +0,0 @@ -package utils - -import ( - "image" - "image/jpeg" - "os" -) - -func SaveImageJPEG(img image.Image, path string) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - - return jpeg.Encode(f, img, &jpeg.Options{Quality: 80}) -} diff --git a/internal/v2/utils/randomness.go b/internal/v2/utils/randomness.go deleted file mode 100644 index 50c92c63..00000000 --- a/internal/v2/utils/randomness.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - "math/rand" - "time" -) - -func RandRng(min, max int) int { - return rand.Intn(max+1-min) + min -} - -func RandomDurationMs(min, max int) time.Duration { - return time.Duration(RandRng(min, max)) * time.Millisecond -} diff --git a/internal/v2/utils/screen_cords.go b/internal/v2/utils/screen_cords.go deleted file mode 100644 index 8d23f399..00000000 --- a/internal/v2/utils/screen_cords.go +++ /dev/null @@ -1,19 +0,0 @@ -package utils - -import ( - "github.com/hectorgimenez/koolo/internal/v2/context" -) - -func GameCoordsToScreenCords(destinationX, destinationY int) (int, int) { - ctx := context.Get() - // Calculate diff between current player position and destination - diffX := destinationX - ctx.Data.PlayerUnit.Position.X - diffY := destinationY - ctx.Data.PlayerUnit.Position.Y - - // Transform cartesian movement (World) to isometric (screen) - // Helpful documentation: https://clintbellanger.net/articles/isometric_math/ - screenX := int((float32(diffX-diffY) * 19.8) + float32(ctx.GameReader.GameAreaSizeX/2)) - screenY := int((float32(diffX+diffY) * 9.9) + float32(ctx.GameReader.GameAreaSizeY/2)) - - return screenX, screenY -} diff --git a/internal/v2/utils/sleep.go b/internal/v2/utils/sleep.go deleted file mode 100644 index d7d62e70..00000000 --- a/internal/v2/utils/sleep.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - "time" -) - -// Sleep provides a Sleep function that randomize the sleep time up/down to a maximum of 30% -func Sleep(milliseconds int) { - maxTime := int(float32(milliseconds) * 1.3) - minTime := int(float32(milliseconds) * 0.7) - sleepTime := RandRng(minTime, maxTime) - - time.Sleep(time.Duration(sleepTime) * time.Millisecond) -} diff --git a/internal/v2/utils/spiral.go b/internal/v2/utils/spiral.go deleted file mode 100644 index d39a314b..00000000 --- a/internal/v2/utils/spiral.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import "math" - -func Spiral(position int) (int, int) { - t := position * 40 - a := 4.0 - b := -2.0 - trad := float64(t) * math.Pi / 180.0 - x := (a + b*trad) * math.Cos(trad) - y := (a + b*trad) * math.Sin(trad) - - return int(x), int(y) -} diff --git a/internal/v2/utils/windows.go b/internal/v2/utils/windows.go deleted file mode 100644 index e7071300..00000000 --- a/internal/v2/utils/windows.go +++ /dev/null @@ -1,21 +0,0 @@ -package utils - -import ( - "os" - "syscall" - - "golang.org/x/sys/windows" -) - -func HasAdminPermission() bool { - _, err := os.Open("\\\\.\\PHYSICALDRIVE0") - - return err == nil -} - -func ShowDialog(title, message string) { - t, _ := syscall.UTF16PtrFromString(title) - txt, _ := syscall.UTF16PtrFromString(message) - - windows.MessageBox(0, txt, t, 0) -} diff --git a/internal/v2/utils/winproc/gdi32.go b/internal/v2/utils/winproc/gdi32.go deleted file mode 100644 index b614df60..00000000 --- a/internal/v2/utils/winproc/gdi32.go +++ /dev/null @@ -1,13 +0,0 @@ -package winproc - -import "golang.org/x/sys/windows" - -var ( - GDI32 = windows.NewLazySystemDLL("gdi32.dll") - CreateCompatibleDC = GDI32.NewProc("CreateCompatibleDC") - CreateCompatibleBitmap = GDI32.NewProc("CreateCompatibleBitmap") - SelectObject = GDI32.NewProc("SelectObject") - DeleteObject = GDI32.NewProc("DeleteObject") - DeleteDC = GDI32.NewProc("DeleteDC") - GetDIBits = GDI32.NewProc("GetDIBits") -) diff --git a/internal/v2/utils/winproc/kernel32.go b/internal/v2/utils/winproc/kernel32.go deleted file mode 100644 index 24dad312..00000000 --- a/internal/v2/utils/winproc/kernel32.go +++ /dev/null @@ -1,13 +0,0 @@ -package winproc - -import "golang.org/x/sys/windows" - -const ( - EXECUTION_STATE_ES_DISPLAY_REQUIRED = 0x00000002 - EXECUTION_STATE_ES_CONTINUOUS = 0x80000000 -) - -var ( - KERNEL32 = windows.NewLazySystemDLL("kernel32.dll") - SetThreadExecutionState = KERNEL32.NewProc("SetThreadExecutionState") -) diff --git a/internal/v2/utils/winproc/user32.go b/internal/v2/utils/winproc/user32.go deleted file mode 100644 index e6d62f7e..00000000 --- a/internal/v2/utils/winproc/user32.go +++ /dev/null @@ -1,10 +0,0 @@ -package winproc - -import "golang.org/x/sys/windows" - -var ( - USER32 = windows.NewLazySystemDLL("user32.dll") - PrintWindow = USER32.NewProc("PrintWindow") - GetWindowDC = USER32.NewProc("GetWindowDC") - SetProcessDpiAware = USER32.NewProc("SetProcessDPIAware") -)