diff --git a/internal/action/identify.go b/internal/action/identify.go index 8d46f51c..5f21d7c3 100644 --- a/internal/action/identify.go +++ b/internal/action/identify.go @@ -2,6 +2,7 @@ package action import ( "fmt" + "time" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/item" @@ -30,6 +31,10 @@ func IdentifyAll(skipIdentify bool) error { if ctx.CharacterCfg.Game.UseCainIdentify { ctx.Logger.Debug("Identifying all item with Cain...") + // Close any open menus first + step.CloseAllMenus() + utils.Sleep(500) + err := CainIdentify() // if identifying with cain fails then we should continue to identify using tome if err == nil { @@ -72,29 +77,40 @@ func CainIdentify() error { stayAwhileAndListen := town.GetTownByArea(ctx.Data.PlayerUnit.Area).IdentifyNPC() + // Close any open menus first + step.CloseAllMenus() + utils.Sleep(200) + err := InteractNPC(stayAwhileAndListen) if err != nil { - ctx.Logger.Error("Error interacting with Cain: ", "error", err.Error()) - return err + return fmt.Errorf("error interacting with Cain: %w", err) } - // Select the identify option - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - utils.Sleep(500) - - if len(itemsToIdentify()) > 0 { - - // Close the NPC interact menu if it's open + // Verify menu opened + menuWait := time.Now().Add(2 * time.Second) + for time.Now().Before(menuWait) { + ctx.PauseIfNotPriority() + ctx.RefreshGameData() if ctx.Data.OpenMenus.NPCInteract { - ctx.HID.KeySequence(win.VK_ESCAPE) + break } + utils.Sleep(100) + } - return fmt.Errorf("failed to identify items") + if !ctx.Data.OpenMenus.NPCInteract { + return fmt.Errorf("NPC menu did not open") } - utils.Sleep(500) + // Select identify option + ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) + utils.Sleep(800) - return step.CloseAllMenus() + // Close menu if still open + if ctx.Data.OpenMenus.NPCInteract { + step.CloseAllMenus() + } + + return nil } func itemsToIdentify() (items []data.Item) { diff --git a/internal/action/step/interact_npc.go b/internal/action/step/interact_npc.go index c73b1988..f044e9f8 100644 --- a/internal/action/step/interact_npc.go +++ b/internal/action/step/interact_npc.go @@ -1,7 +1,6 @@ package step import ( - "errors" "fmt" "time" @@ -9,61 +8,81 @@ import ( "github.com/hectorgimenez/d2go/pkg/data/npc" "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/game" + "github.com/hectorgimenez/koolo/internal/pather" "github.com/hectorgimenez/koolo/internal/ui" ) func InteractNPC(npcID npc.ID) error { - maxInteractionAttempts := 5 - interactionAttempts := 0 - waitingForInteraction := false - currentMouseCoords := data.Position{} - lastRun := time.Time{} - ctx := context.Get() ctx.SetLastStep("InteractNPC") - for { - ctx.RefreshGameData() + const ( + maxAttempts = 8 + minMenuOpenWait = 300 * time.Millisecond + maxDistance = 15 + hoverWait = 800 * time.Millisecond + ) + + var targetNPCID data.UnitID + for attempts := 0; attempts < maxAttempts; attempts++ { // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() - if ctx.Data.OpenMenus.NPCInteract { - return nil - } + // Check if interaction succeeded and menu is open + if ctx.Data.OpenMenus.NPCInteract || ctx.Data.OpenMenus.NPCShop { + // Find current NPC position + if targetNPCID != 0 { + if currentNPC, found := ctx.Data.Monsters.FindByID(targetNPCID); found { + currentDistance := pather.DistanceFromPoint(currentNPC.Position, ctx.Data.PlayerUnit.Position) + if currentDistance <= maxDistance { + time.Sleep(minMenuOpenWait) + return nil + } + } + } - if interactionAttempts >= maxInteractionAttempts { - return errors.New("failed interacting with NPC") + // Wrong NPC, too far, or NPC moved - close menu and retry + CloseAllMenus() + time.Sleep(200 * time.Millisecond) + targetNPCID = 0 + continue } - // Give some time before retrying the interaction - if waitingForInteraction && time.Since(lastRun) < time.Millisecond*200 { + townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone) + if !found { + if attempts == maxAttempts-1 { + return fmt.Errorf("NPC %d not found after %d attempts", npcID, maxAttempts) + } + time.Sleep(200 * time.Millisecond) 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(townNPC.Position) + if distance > maxDistance { + return fmt.Errorf("NPC %d is too far away (distance: %d)", npcID, distance) + } - distance := ctx.PathFinder.DistanceFromMe(m.Position) - if distance > 15 { - return fmt.Errorf("NPC is too far away: %d. Current distance: %d", npcID, distance) - } + // Calculate click position + x, y := ui.GameCoordsToScreenCords(townNPC.Position.X, townNPC.Position.Y) + if npcID == npc.Tyrael2 { + y = y - 40 // Act 4 Tyrael has a super weird hitbox + } + + // Move mouse and wait for hover + ctx.HID.MovePointer(x, y) + hoverStart := time.Now() - x, y := ui.GameCoordsToScreenCords(m.Position.X, m.Position.Y) - // Act 4 Tyrael has a super weird hitbox - if npcID == npc.Tyrael2 { - y = y - 40 + for time.Since(hoverStart) < hoverWait { + if currentNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found && currentNPC.IsHovered { + targetNPCID = currentNPC.UnitID + ctx.HID.Click(game.LeftButton, x, y) + time.Sleep(minMenuOpenWait) + break } - currentMouseCoords = data.Position{X: x, Y: y} - ctx.HID.MovePointer(x, y) - interactionAttempts++ + time.Sleep(50 * time.Millisecond) } } + + return fmt.Errorf("failed to interact with NPC after %d attempts", maxAttempts) }