From 0d0efd6ac617ed04c5a664fec5c044fa0bfa1c00 Mon Sep 17 00:00:00 2001 From: Sorc <103583469+sorc653@users.noreply.github.com> Date: Thu, 6 Feb 2025 08:46:45 -0500 Subject: [PATCH] Additional vendor check for keys and scrolls (#628) Co-authored-by: Sorc <> Co-authored-by: Josh Becker --- config/template/config.yaml | 1 + internal/action/interaction.go | 15 +- internal/action/item_pickup.go | 24 +++ internal/action/vendor.go | 189 +++++++++++++++++- internal/bot/bot.go | 5 + internal/config/config.go | 1 + internal/server/http_server.go | 1 + .../templates/character_settings.gohtml | 4 + internal/town/shop_manager.go | 63 +++--- 9 files changed, 264 insertions(+), 39 deletions(-) diff --git a/config/template/config.yaml b/config/template/config.yaml index 823782b0e..19f13c087 100644 --- a/config/template/config.yaml +++ b/config/template/config.yaml @@ -191,3 +191,4 @@ backtotown: noHpPotions: true noMpPotions: false mercDied: true + noKeys: false diff --git a/internal/action/interaction.go b/internal/action/interaction.go index 310a9f89a..0fbe2328f 100644 --- a/internal/action/interaction.go +++ b/internal/action/interaction.go @@ -13,13 +13,18 @@ import ( "github.com/hectorgimenez/koolo/internal/game" ) -func InteractNPC(npc npc.ID) error { +func InteractNPC(NPC npc.ID) error { ctx := context.Get() ctx.SetLastAction("InteractNPC") - pos, found := getNPCPosition(npc, ctx.Data) + pos, found := getNPCPosition(NPC, ctx.Data) if !found { - return fmt.Errorf("npc with ID %d not found", npc) + + if NPC == npc.Hratli { + pos = data.Position{X: 5224, Y: 5039} + } else { + return fmt.Errorf("npc with ID %d not found", NPC) + } } var err error @@ -29,7 +34,7 @@ func InteractNPC(npc npc.ID) error { continue } - err = step.InteractNPC(npc) + err = step.InteractNPC(NPC) if err != nil { continue } @@ -39,7 +44,7 @@ func InteractNPC(npc npc.ID) error { return err } - event.Send(event.InteractedTo(event.Text(ctx.Name, ""), int(npc), event.InteractionTypeNPC)) + event.Send(event.InteractedTo(event.Text(ctx.Name, ""), int(NPC), event.InteractionTypeNPC)) return nil } diff --git a/internal/action/item_pickup.go b/internal/action/item_pickup.go index 0b41f89f5..afdb6bb9f 100644 --- a/internal/action/item_pickup.go +++ b/internal/action/item_pickup.go @@ -251,6 +251,30 @@ func shouldBePickedUp(i data.Item) bool { return true } + // Check if we should pick up some keys. The goal is to have 12 keys in total (single stack) + if i.Name == "Key" { + quantityOnGround := 0 + st, statFound := i.FindStat(stat.Quantity, 0) + if statFound { + quantityOnGround = st.Value + } + + quantityInInventory := 0 + for _, item := range ctx.Data.Inventory.AllItems { + if item.Name == "Key" { + qty, _ := item.FindStat(stat.Quantity, 0) + quantityInInventory += qty.Value + } + } + + // We only want to pick them up if either: + // 1. They have the option to return to town enabled or + // 2. They already had some keys in inventory, so we just want to get closer to 12 + if (ctx.CharacterCfg.BackToTown.NoKeys || quantityInInventory > 0) && quantityOnGround+quantityInInventory <= 12 { + 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") if specialRuns { diff --git a/internal/action/vendor.go b/internal/action/vendor.go index 784db81dc..639e31a66 100644 --- a/internal/action/vendor.go +++ b/internal/action/vendor.go @@ -8,10 +8,64 @@ import ( "github.com/hectorgimenez/koolo/internal/town" "github.com/lxn/win" + "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/npc" + "github.com/hectorgimenez/d2go/pkg/data/stat" ) +func openTradeWindow(vendorNPC npc.ID) error { + ctx := context.Get() + + 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) + } + + SwitchStashTab(4) + ctx.RefreshGameData() + + return nil +} + +// Act 1 Vendors: +// Potions: Akara +// Keys: Akara +// Scrolls: Akara +// Arrows/Bolts: Charsi + +// Act 2 Vendors: +// Potions: Lysander +// Keys: Lysander +// Scrolls: Drognan +// Arrows/Bolts: Fara + +// Act 3 Vendors: +// Potions: Ormus +// Keys: Hratli +// Scrolls: Ormus +// Arrows/Bolts: Hratli + +// Act 4 Vendors: +// Potions: Jamella +// Keys: Jamella +// Scrolls: Jamella +// Arrows/Bolts: Halbu + +// Act 5 Vendors: +// Potions: Malah +// Keys: Malah +// Scrolls: Malah +// Arrows/Bolts: Larzuk + func VendorRefill(forceRefill, sellJunk bool) error { ctx := context.Get() ctx.SetLastAction("VendorRefill") @@ -29,26 +83,133 @@ func VendorRefill(forceRefill, sellJunk bool) error { vendorNPC = npc.Lysander } } - err := InteractNPC(vendorNPC) + + err := openTradeWindow(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) - } - - SwitchStashTab(4) - ctx.RefreshGameData() town.BuyConsumables(forceRefill) if sellJunk { town.SellJunk() } + // At this point we are guaranteed to have purchased potions, as the selected vendorNPC will always have these. + // Depending on the act, we may still need keys or scrolls. + + if town.ShouldBuyTPs() || town.ShouldBuyIDs() { + restockTomes() + } + + if ctx.Data.PlayerUnit.Class != data.Assassin { + _, shouldBuyKeys := town.ShouldBuyKeys() + if shouldBuyKeys { + restockKeys() + } + } + + return step.CloseAllMenus() +} + +func restockTomes() error { + ctx := context.Get() + + shouldBuyTPs := town.ShouldBuyTPs() + shouldBuyIDs := town.ShouldBuyIDs() + + if !shouldBuyTPs && !shouldBuyIDs { + return nil + } + + ctx.Logger.Info("Visiting vendor to buy scrolls...") + + var vendorNPC npc.ID + currentArea := ctx.Data.PlayerUnit.Area + if currentArea == area.RogueEncampment { + vendorNPC = npc.Akara + } else if currentArea == area.LutGholein { + vendorNPC = npc.Drognan + } else if currentArea == area.KurastDocks { + vendorNPC = npc.Ormus + } else if currentArea == area.ThePandemoniumFortress { + vendorNPC = npc.Jamella + } else if currentArea == area.Harrogath { + vendorNPC = npc.Malah + } + + if vendorNPC == 0 { + ctx.Logger.Info("Unable to find scroll vendor...") + + return nil + } + + err := openTradeWindow(vendorNPC) + if err != nil { + return err + } + + if shouldBuyTPs { + town.BuyTPs() + } + + if shouldBuyIDs { + town.BuyIDs() + } + + return nil +} + +func restockKeys() error { + ctx := context.Get() + + if ctx.Data.PlayerUnit.Class == data.Assassin { + return nil + } + + keyQuantity, needsBuy := town.ShouldBuyKeys() + + if !needsBuy { + return nil + } + + ctx.Logger.Info("Visiting vendor to buy keys...") + + var vendorNPC npc.ID + currentArea := ctx.Data.PlayerUnit.Area + if currentArea == area.RogueEncampment { + vendorNPC = npc.Akara + } else if currentArea == area.LutGholein { + vendorNPC = npc.Lysander + } else if currentArea == area.KurastDocks { + vendorNPC = npc.Hratli + } else if currentArea == area.ThePandemoniumFortress { + vendorNPC = npc.Jamella + } else if currentArea == area.Harrogath { + vendorNPC = npc.Malah + } + + if vendorNPC == 0 { + ctx.Logger.Info("Unable to find keys vendor...") + + return nil + } + + err := openTradeWindow(vendorNPC) + if err != nil { + ctx.Logger.Info("Unable to interact with keys vendor", "error", err) + return err + } + + 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 { + town.BuyFullStack(itm) + } + } + return step.CloseAllMenus() } @@ -102,5 +263,11 @@ func shouldVisitVendor() bool { return false } - return ctx.BeltManager.ShouldBuyPotions() || town.ShouldBuyTPs() || town.ShouldBuyIDs() + shouldVisit := ctx.BeltManager.ShouldBuyPotions() || town.ShouldBuyTPs() || town.ShouldBuyIDs() + if shouldVisit { + return true + } + + _, shouldBuyKeys := town.ShouldBuyKeys() + return shouldBuyKeys } diff --git a/internal/bot/bot.go b/internal/bot/bot.go index a834f4c48..bb4a85b28 100644 --- a/internal/bot/bot.go +++ b/internal/bot/bot.go @@ -7,6 +7,7 @@ import ( "time" "github.com/hectorgimenez/d2go/pkg/data" + "github.com/hectorgimenez/d2go/pkg/data/item" "github.com/hectorgimenez/koolo/internal/action" botCtx "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/event" @@ -151,11 +152,13 @@ func (b *Bot) Run(ctx context.Context, firstRun bool, runs []run.Run) error { _, healingPotsFound := b.ctx.Data.Inventory.Belt.GetFirstPotion(data.HealingPotion) _, manaPotsFound := b.ctx.Data.Inventory.Belt.GetFirstPotion(data.ManaPotion) + _, keysFound := b.ctx.Data.Inventory.Find(item.Key, item.LocationInventory) // Check if we need to go back to town (no pots or merc died) if (b.ctx.CharacterCfg.BackToTown.NoHpPotions && !healingPotsFound || b.ctx.CharacterCfg.BackToTown.EquipmentBroken && action.RepairRequired() || b.ctx.CharacterCfg.BackToTown.NoMpPotions && !manaPotsFound || + b.ctx.CharacterCfg.BackToTown.NoKeys && !keysFound || b.ctx.CharacterCfg.BackToTown.MercDied && b.ctx.Data.MercHPPercent() <= 0 && b.ctx.CharacterCfg.Character.UseMerc) && !b.ctx.Data.PlayerUnit.Area.IsTown() { @@ -167,6 +170,8 @@ func (b *Bot) Run(ctx context.Context, firstRun bool, runs []run.Run) error { reason = "Equipment broken" } else if b.ctx.CharacterCfg.BackToTown.NoMpPotions && !manaPotsFound { reason = "No mana potions found" + } else if b.ctx.CharacterCfg.BackToTown.NoKeys && !keysFound { + reason = "No keys found" } else if b.ctx.CharacterCfg.BackToTown.MercDied && b.ctx.Data.MercHPPercent() <= 0 && b.ctx.CharacterCfg.Character.UseMerc { reason = "Mercenary is dead" } diff --git a/internal/config/config.go b/internal/config/config.go index 7afa7b5ab..2842a1434 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -250,6 +250,7 @@ type CharacterCfg struct { NoMpPotions bool `yaml:"noMpPotions"` MercDied bool `yaml:"mercDied"` EquipmentBroken bool `yaml:"equipmentBroken"` + NoKeys bool `yaml:"noKeys"` } `yaml:"backtotown"` Runtime struct { Rules nip.Rules `yaml:"-"` diff --git a/internal/server/http_server.go b/internal/server/http_server.go index 765e7b33e..3881b4a37 100644 --- a/internal/server/http_server.go +++ b/internal/server/http_server.go @@ -978,6 +978,7 @@ func (s *HttpServer) characterSettings(w http.ResponseWriter, r *http.Request) { cfg.BackToTown.NoMpPotions = r.Form.Has("noMpPotions") cfg.BackToTown.MercDied = r.Form.Has("mercDied") cfg.BackToTown.EquipmentBroken = r.Form.Has("equipmentBroken") + cfg.BackToTown.NoKeys = r.Form.Has("noKeys") config.SaveSupervisorConfig(supervisorName, cfg) http.Redirect(w, r, "/", http.StatusSeeOther) diff --git a/internal/server/templates/character_settings.gohtml b/internal/server/templates/character_settings.gohtml index 61650c230..c7631a075 100644 --- a/internal/server/templates/character_settings.gohtml +++ b/internal/server/templates/character_settings.gohtml @@ -487,6 +487,10 @@ Equipment Broken +
diff --git a/internal/town/shop_manager.go b/internal/town/shop_manager.go index df6dda278..4b955cc5c 100644 --- a/internal/town/shop_manager.go +++ b/internal/town/shop_manager.go @@ -13,6 +13,37 @@ import ( "github.com/hectorgimenez/koolo/internal/ui" ) + +func BuyTPs() { + ctx := context.Get() + + 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) + } +} + +func BuyIDs() { + ctx := context.Get() + + 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) + } +} + func BuyConsumables(forceRefill bool) { ctx := context.Get() @@ -39,29 +70,11 @@ func BuyConsumables(forceRefill bool) { } 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) - } + BuyTPs() } 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) - } + BuyIDs() } keyQuantity, shouldBuyKeys := ShouldBuyKeys() @@ -71,7 +84,7 @@ func BuyConsumables(forceRefill bool) { qty, _ := itm.FindStat(stat.Quantity, 0) if (qty.Value + keyQuantity) <= 12 { - buyFullStack(itm) + BuyFullStack(itm) } } } @@ -111,9 +124,13 @@ func ShouldBuyIDs() bool { } func ShouldBuyKeys() (int, bool) { + if !context.Get().CharacterCfg.BackToTown.NoKeys { + return 0, false + } + keys, found := context.Get().Data.Inventory.Find(item.Key, item.LocationInventory) if !found { - return 12, false + return 0, true } qty, found := keys.FindStat(stat.Quantity, 0) @@ -154,7 +171,7 @@ func BuyItem(i data.Item, quantity int) { } } -func buyFullStack(i data.Item) { +func BuyFullStack(i data.Item) { screenPos := ui.GetScreenCoordsForItem(i) context.Get().HID.ClickWithModifier(game.RightButton, screenPos.X, screenPos.Y, game.ShiftKey)