Skip to content

Commit

Permalink
Additional vendor check for keys and scrolls (#628)
Browse files Browse the repository at this point in the history
Co-authored-by: Sorc <>
Co-authored-by: Josh Becker <[email protected]>
  • Loading branch information
sorc653 and Geesu authored Feb 6, 2025
1 parent f19a0db commit 0d0efd6
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 39 deletions.
1 change: 1 addition & 0 deletions config/template/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,4 @@ backtotown:
noHpPotions: true
noMpPotions: false
mercDied: true
noKeys: false
15 changes: 10 additions & 5 deletions internal/action/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +34,7 @@ func InteractNPC(npc npc.ID) error {
continue
}

err = step.InteractNPC(npc)
err = step.InteractNPC(NPC)
if err != nil {
continue
}
Expand All @@ -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
}
Expand Down
24 changes: 24 additions & 0 deletions internal/action/item_pickup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
189 changes: 178 additions & 11 deletions internal/action/vendor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()
}

Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions internal/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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() {

Expand All @@ -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"
}
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down
1 change: 1 addition & 0 deletions internal/server/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions internal/server/templates/character_settings.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,10 @@
<input id="equip_broken" type="checkbox" name="equipmentBroken" {{ if .Config.BackToTown.EquipmentBroken }}checked{{ end }}/>
Equipment Broken
</label>
<label>
<input id="no_keys" type="checkbox" name="noKeys" {{ if .Config.BackToTown.NoKeys }}checked{{ end }}/>
No Keys
</label>
</fieldset>
<fieldset class="grid">
<a href="/"><input type="button" value="Cancel" class="secondary"/></a>
Expand Down
Loading

0 comments on commit 0d0efd6

Please sign in to comment.