Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional vendor check for keys and scrolls #628

Merged
merged 10 commits into from
Feb 6, 2025
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