From 1edf274c8d23f5b32050274026ec1272722ffdb3 Mon Sep 17 00:00:00 2001 From: elobo91 Date: Tue, 12 Nov 2024 18:30:59 -0500 Subject: [PATCH 01/13] Fix Foh Boss Attack rotation, also improved decision for regular attack sequence. --- internal/character/foh.go | 153 +++++++++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 35 deletions(-) diff --git a/internal/character/foh.go b/internal/character/foh.go index f09a006f..f3ce41fd 100644 --- a/internal/character/foh.go +++ b/internal/character/foh.go @@ -1,19 +1,19 @@ package character import ( + "fmt" + "github.com/hectorgimenez/d2go/pkg/data/mode" "log/slog" "sort" "time" - "github.com/hectorgimenez/d2go/pkg/data/state" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/context" - "github.com/hectorgimenez/koolo/internal/utils" - "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/context" "github.com/hectorgimenez/koolo/internal/game" ) @@ -22,11 +22,13 @@ const ( fohMaxDistance = 15 hbMinDistance = 6 hbMaxDistance = 12 - fohMaxAttacksLoop = 35 // Maximum attack attempts before resetting + fohMaxAttacksLoop = 35 // Maximum attack attempts before resetting + castingTimeout = 3 * time.Second // Maximum time to wait for a cast to complete ) type Foh struct { BaseCharacter + lastCastTime time.Time } func (s Foh) CheckKeyBindings() []skill.ID { @@ -45,10 +47,33 @@ func (s Foh) CheckKeyBindings() []skill.ID { return missingKeybindings } + +// waitForCastComplete waits until the character is no longer in casting animation +func (f *Foh) waitForCastComplete() bool { + ctx := context.Get() + startTime := time.Now() + + for time.Since(startTime) < castingTimeout { + ctx.RefreshGameData() + + // Check if we're no longer casting and enough time has passed since last cast + if ctx.Data.PlayerUnit.Mode != mode.CastingSkill && + time.Since(f.lastCastTime) > 150*time.Millisecond { //150 for Foh but if we make that generic it would need tuning maybe from skill desc + return true + } + + time.Sleep(16 * time.Millisecond) // Small sleep to avoid hammering CPU + } + + return false +} func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, bool), skipOnImmunities []stat.Resist) error { ctx := context.Get() lastRefresh := time.Now() completedAttackLoops := 0 + var currentTargetID data.UnitID + useHolyBolt := false + fohOpts := []step.AttackOption{ step.StationaryDistance(fohMinDistance, fohMaxDistance), step.EnsureAura(skill.Conviction), @@ -58,57 +83,98 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, step.EnsureAura(skill.Conviction), } + // Initial target selection and analysis + initialTargetAnalysis := func() (data.UnitID, bool, bool) { + id, found := monsterSelector(*s.Data) + if !found { + return 0, false, false + } + + // Count initial valid targets + validTargets := 0 + monstersInRange := make([]data.Monster, 0) + monster, found := s.Data.Monsters.FindByID(id) + if !found { + return 0, false, false + } + + for _, m := range ctx.Data.Monsters.Enemies() { + if ctx.Data.AreaData.IsInside(m.Position) { + dist := ctx.PathFinder.DistanceFromMe(m.Position) + if dist <= fohMaxDistance && dist >= fohMinDistance && m.Stats[stat.Life] > 0 { + validTargets++ + monstersInRange = append(monstersInRange, m) + } + } + } + + // Determine if we should use Holy Bolt + // Only use Holy Bolt if it's a single target and it's immune to lightning + shouldUseHB := validTargets == 1 && monster.IsImmune(stat.LightImmune) + + return id, true, shouldUseHB + } + for { + // Refresh game data periodically if time.Since(lastRefresh) > time.Millisecond*100 { ctx.RefreshGameData() lastRefresh = time.Now() } + ctx.PauseIfNotPriority() + if completedAttackLoops >= fohMaxAttacksLoop { return nil } - id, found := monsterSelector(*s.Data) - if !found { - return nil - } - if !s.preBattleChecks(id, skipOnImmunities) { - return nil + + // If we don't have a current target, get one and analyze the situation + if currentTargetID == 0 { + var found bool + currentTargetID, found, useHolyBolt = initialTargetAnalysis() + if !found { + return nil + } } - monster, found := s.Data.Monsters.FindByID(id) + + // Verify our target still exists and is alive + monster, found := s.Data.Monsters.FindByID(currentTargetID) if !found || monster.Stats[stat.Life] <= 0 { + currentTargetID = 0 // Reset target + continue + } + + if !s.preBattleChecks(currentTargetID, skipOnImmunities) { return nil } + + // Ensure Conviction is active if !s.Data.PlayerUnit.States.HasState(state.Conviction) { if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.Conviction); found { ctx.HID.PressKeyBinding(kb) } } - // Handle attacks - validTargets := 0 - lightImmuneTargets := 0 - for _, m := range ctx.Data.Monsters.Enemies() { - if ctx.Data.AreaData.IsInside(m.Position) { - dist := ctx.PathFinder.DistanceFromMe(m.Position) - if dist <= fohMaxDistance && dist >= fohMinDistance && m.Stats[stat.Life] > 0 { - validTargets++ - if m.IsImmune(stat.LightImmune) && !m.States.HasState(state.Conviction) { - lightImmuneTargets++ - } - } - } - } - if monster.IsImmune(stat.LightImmune) && !monster.States.HasState(state.Conviction) && validTargets == 1 { + // Cast appropriate skill + if useHolyBolt { if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.HolyBolt); found { ctx.HID.PressKeyBinding(kb) - if err := step.PrimaryAttack(id, 1, true, hbOpts...); err == nil { + if err := step.PrimaryAttack(currentTargetID, 1, true, hbOpts...); err == nil { + if !s.waitForCastComplete() { + continue + } + s.lastCastTime = time.Now() completedAttackLoops++ } } } else { if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.FistOfTheHeavens); found { ctx.HID.PressKeyBinding(kb) - if err := step.PrimaryAttack(id, 1, true, fohOpts...); err == nil { + if err := step.PrimaryAttack(currentTargetID, 1, true, fohOpts...); err == nil { + if !s.waitForCastComplete() { + continue + } + s.lastCastTime = time.Now() completedAttackLoops++ } } @@ -116,18 +182,35 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, } } -func (s Foh) handleBoss(bossID data.UnitID, fohOpts, hbOpts []step.AttackOption, completedAttackLoops *int) error { +func (f *Foh) handleBoss(bossID data.UnitID, fohOpts, hbOpts []step.AttackOption, completedAttackLoops *int) error { ctx := context.Get() + + // Cast FoH if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.FistOfTheHeavens); found { ctx.HID.PressKeyBinding(kb) - utils.Sleep(100) + if err := step.PrimaryAttack(bossID, 1, true, fohOpts...); err == nil { - time.Sleep(ctx.Data.PlayerCastDuration()) + // Wait for FoH cast to complete + if !f.waitForCastComplete() { + return fmt.Errorf("FoH cast timed out") + } + f.lastCastTime = time.Now() + + // Switch to Holy Bolt if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.HolyBolt); found { ctx.HID.PressKeyBinding(kb) - if err := step.PrimaryAttack(bossID, 3, true, hbOpts...); err == nil { - (*completedAttackLoops)++ + + // Cast 3 Holy Bolts + for i := 0; i < 3; i++ { + if err := step.PrimaryAttack(bossID, 1, true, hbOpts...); err == nil { + if !f.waitForCastComplete() { + return fmt.Errorf("Holy Bolt cast timed out") + } + f.lastCastTime = time.Now() + } } + + (*completedAttackLoops)++ } } } From 337bdcf06168e8c7f245e585f606d12c4d993c9c Mon Sep 17 00:00:00 2001 From: elobo91 Date: Tue, 12 Nov 2024 19:04:33 -0500 Subject: [PATCH 02/13] Cleaned code, defer foh skill after attack --- internal/character/foh.go | 136 +++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/internal/character/foh.go b/internal/character/foh.go index f3ce41fd..a7c34fb2 100644 --- a/internal/character/foh.go +++ b/internal/character/foh.go @@ -31,25 +31,25 @@ type Foh struct { lastCastTime time.Time } -func (s Foh) CheckKeyBindings() []skill.ID { +func (f Foh) CheckKeyBindings() []skill.ID { requireKeybindings := []skill.ID{skill.Conviction, skill.HolyShield, skill.TomeOfTownPortal, skill.FistOfTheHeavens, skill.HolyBolt} - missingKeybindings := []skill.ID{} + missingKeybindings := make([]skill.ID, 0) for _, cskill := range requireKeybindings { - if _, found := s.Data.KeyBindings.KeyBindingForSkill(cskill); !found { + if _, found := f.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)) + f.Logger.Debug("There are missing required key bindings.", slog.Any("Bindings", missingKeybindings)) } return missingKeybindings } // waitForCastComplete waits until the character is no longer in casting animation -func (f *Foh) waitForCastComplete() bool { +func (f Foh) waitForCastComplete() bool { ctx := context.Get() startTime := time.Now() @@ -67,13 +67,20 @@ func (f *Foh) waitForCastComplete() bool { return false } -func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, bool), skipOnImmunities []stat.Resist) error { +func (f Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, bool), skipOnImmunities []stat.Resist) error { ctx := context.Get() lastRefresh := time.Now() completedAttackLoops := 0 var currentTargetID data.UnitID useHolyBolt := false + // Ensure we always return to FoH when done + defer func() { + if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.FistOfTheHeavens); found { + ctx.HID.PressKeyBinding(kb) + } + }() + fohOpts := []step.AttackOption{ step.StationaryDistance(fohMinDistance, fohMaxDistance), step.EnsureAura(skill.Conviction), @@ -85,7 +92,7 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, // Initial target selection and analysis initialTargetAnalysis := func() (data.UnitID, bool, bool) { - id, found := monsterSelector(*s.Data) + id, found := monsterSelector(*f.Data) if !found { return 0, false, false } @@ -93,7 +100,7 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, // Count initial valid targets validTargets := 0 monstersInRange := make([]data.Monster, 0) - monster, found := s.Data.Monsters.FindByID(id) + monster, found := f.Data.Monsters.FindByID(id) if !found { return 0, false, false } @@ -109,8 +116,8 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, } // Determine if we should use Holy Bolt - // Only use Holy Bolt if it's a single target and it's immune to lightning - shouldUseHB := validTargets == 1 && monster.IsImmune(stat.LightImmune) + // Only use Holy Bolt if it's a single target and it's immune to lightning after conviction + shouldUseHB := validTargets == 1 && monster.IsImmune(stat.LightImmune) && monster.States.HasState(state.Convicted) return id, true, shouldUseHB } @@ -138,18 +145,18 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, } // Verify our target still exists and is alive - monster, found := s.Data.Monsters.FindByID(currentTargetID) + monster, found := f.Data.Monsters.FindByID(currentTargetID) if !found || monster.Stats[stat.Life] <= 0 { currentTargetID = 0 // Reset target continue } - if !s.preBattleChecks(currentTargetID, skipOnImmunities) { + if !f.preBattleChecks(currentTargetID, skipOnImmunities) { return nil } // Ensure Conviction is active - if !s.Data.PlayerUnit.States.HasState(state.Conviction) { + if !f.Data.PlayerUnit.States.HasState(state.Conviction) { if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.Conviction); found { ctx.HID.PressKeyBinding(kb) } @@ -160,10 +167,10 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.HolyBolt); found { ctx.HID.PressKeyBinding(kb) if err := step.PrimaryAttack(currentTargetID, 1, true, hbOpts...); err == nil { - if !s.waitForCastComplete() { + if !f.waitForCastComplete() { continue } - s.lastCastTime = time.Now() + f.lastCastTime = time.Now() completedAttackLoops++ } } @@ -171,18 +178,17 @@ func (s Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.FistOfTheHeavens); found { ctx.HID.PressKeyBinding(kb) if err := step.PrimaryAttack(currentTargetID, 1, true, fohOpts...); err == nil { - if !s.waitForCastComplete() { + if !f.waitForCastComplete() { continue } - s.lastCastTime = time.Now() + f.lastCastTime = time.Now() completedAttackLoops++ } } } } } - -func (f *Foh) handleBoss(bossID data.UnitID, fohOpts, hbOpts []step.AttackOption, completedAttackLoops *int) error { +func (f Foh) handleBoss(bossID data.UnitID, fohOpts, hbOpts []step.AttackOption, completedAttackLoops *int) error { ctx := context.Get() // Cast FoH @@ -192,7 +198,7 @@ func (f *Foh) handleBoss(bossID data.UnitID, fohOpts, hbOpts []step.AttackOption if err := step.PrimaryAttack(bossID, 1, true, fohOpts...); err == nil { // Wait for FoH cast to complete if !f.waitForCastComplete() { - return fmt.Errorf("FoH cast timed out") + return fmt.Errorf("foh cast timed out") } f.lastCastTime = time.Now() @@ -204,22 +210,30 @@ func (f *Foh) handleBoss(bossID data.UnitID, fohOpts, hbOpts []step.AttackOption for i := 0; i < 3; i++ { if err := step.PrimaryAttack(bossID, 1, true, hbOpts...); err == nil { if !f.waitForCastComplete() { - return fmt.Errorf("Holy Bolt cast timed out") + return fmt.Errorf("holy Bolt cast timed out") } f.lastCastTime = time.Now() } } - (*completedAttackLoops)++ + *completedAttackLoops++ } } } return nil } -func (s Foh) KillBossSequence(monsterSelector func(d game.Data) (data.UnitID, bool), skipOnImmunities []stat.Resist) error { +func (f Foh) KillBossSequence(monsterSelector func(d game.Data) (data.UnitID, bool), skipOnImmunities []stat.Resist) error { ctx := context.Get() lastRefresh := time.Now() completedAttackLoops := 0 + + // Ensure we always return to FoH when done + defer func() { + if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.FistOfTheHeavens); found { + ctx.HID.PressKeyBinding(kb) + } + }() + fohOpts := []step.AttackOption{ step.StationaryDistance(fohMinDistance, fohMaxDistance), step.EnsureAura(skill.Conviction), @@ -238,42 +252,42 @@ func (s Foh) KillBossSequence(monsterSelector func(d game.Data) (data.UnitID, bo if completedAttackLoops >= fohMaxAttacksLoop { return nil } - id, found := monsterSelector(*s.Data) + id, found := monsterSelector(*f.Data) if !found { return nil } - if !s.preBattleChecks(id, skipOnImmunities) { + if !f.preBattleChecks(id, skipOnImmunities) { return nil } - monster, found := s.Data.Monsters.FindByID(id) + monster, found := f.Data.Monsters.FindByID(id) if !found || monster.Stats[stat.Life] <= 0 { return nil } - if !s.Data.PlayerUnit.States.HasState(state.Conviction) { + if !f.Data.PlayerUnit.States.HasState(state.Conviction) { if kb, found := ctx.Data.KeyBindings.KeyBindingForSkill(skill.Conviction); found { ctx.HID.PressKeyBinding(kb) } } - if err := s.handleBoss(monster.UnitID, fohOpts, hbOpts, &completedAttackLoops); err == nil { + if err := f.handleBoss(monster.UnitID, fohOpts, hbOpts, &completedAttackLoops); err == nil { continue } } } -func (s Foh) BuffSkills() []skill.ID { - if _, found := s.Data.KeyBindings.KeyBindingForSkill(skill.HolyShield); found { +func (f Foh) BuffSkills() []skill.ID { + if _, found := f.Data.KeyBindings.KeyBindingForSkill(skill.HolyShield); found { return []skill.ID{skill.HolyShield} } - return []skill.ID{} + return make([]skill.ID, 0) } -func (s Foh) PreCTABuffSkills() []skill.ID { - return []skill.ID{} +func (f Foh) PreCTABuffSkills() []skill.ID { + return make([]skill.ID, 0) } -func (s Foh) killBoss(npc npc.ID, t data.MonsterType) error { - return s.KillBossSequence(func(d game.Data) (data.UnitID, bool) { +func (f Foh) killBoss(npc npc.ID, t data.MonsterType) error { + return f.KillBossSequence(func(d game.Data) (data.UnitID, bool) { m, found := d.Monsters.FindOne(npc, t) if !found || m.Stats[stat.Life] <= 0 { return 0, false @@ -282,24 +296,24 @@ func (s Foh) killBoss(npc npc.ID, t data.MonsterType) error { }, nil) } -func (s Foh) KillCountess() error { - return s.killBoss(npc.DarkStalker, data.MonsterTypeSuperUnique) +func (f Foh) KillCountess() error { + return f.killBoss(npc.DarkStalker, data.MonsterTypeSuperUnique) } -func (s Foh) KillAndariel() error { - return s.killBoss(npc.Andariel, data.MonsterTypeUnique) +func (f Foh) KillAndariel() error { + return f.killBoss(npc.Andariel, data.MonsterTypeUnique) } -func (s Foh) KillSummoner() error { - return s.killBoss(npc.Summoner, data.MonsterTypeUnique) +func (f Foh) KillSummoner() error { + return f.killBoss(npc.Summoner, data.MonsterTypeUnique) } -func (s Foh) KillDuriel() error { - return s.killBoss(npc.Duriel, data.MonsterTypeUnique) +func (f Foh) KillDuriel() error { + return f.killBoss(npc.Duriel, data.MonsterTypeUnique) } -func (s Foh) KillCouncil() error { - return s.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { +func (f Foh) KillCouncil() error { + return f.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { var councilMembers []data.Monster for _, m := range d.Monsters { if m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3 { @@ -307,7 +321,7 @@ func (s Foh) KillCouncil() error { } } sort.Slice(councilMembers, func(i, j int) bool { - return s.PathFinder.DistanceFromMe(councilMembers[i].Position) < s.PathFinder.DistanceFromMe(councilMembers[j].Position) + return f.PathFinder.DistanceFromMe(councilMembers[i].Position) < f.PathFinder.DistanceFromMe(councilMembers[j].Position) }) if len(councilMembers) > 0 { return councilMembers[0].UnitID, true @@ -316,26 +330,26 @@ func (s Foh) KillCouncil() error { }, nil) } -func (s Foh) KillMephisto() error { - return s.killBoss(npc.Mephisto, data.MonsterTypeUnique) +func (f Foh) KillMephisto() error { + return f.killBoss(npc.Mephisto, data.MonsterTypeUnique) } -func (s Foh) KillIzual() error { - return s.killBoss(npc.Izual, data.MonsterTypeUnique) +func (f Foh) KillIzual() error { + return f.killBoss(npc.Izual, data.MonsterTypeUnique) } -func (s Foh) KillDiablo() error { +func (f 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") + f.Logger.Error("Diablo was not found, timeout reached") return nil } - diablo, found := s.Data.Monsters.FindOne(npc.Diablo, data.MonsterTypeUnique) + diablo, found := f.Data.Monsters.FindOne(npc.Diablo, data.MonsterTypeUnique) if !found || diablo.Stats[stat.Life] <= 0 { if diabloFound { return nil @@ -345,20 +359,20 @@ func (s Foh) KillDiablo() error { } diabloFound = true - s.Logger.Info("Diablo detected, attacking") + f.Logger.Info("Diablo detected, attacking") - return s.killBoss(npc.Diablo, data.MonsterTypeUnique) + return f.killBoss(npc.Diablo, data.MonsterTypeUnique) } } -func (s Foh) KillPindle() error { - return s.killBoss(npc.DefiledWarrior, data.MonsterTypeSuperUnique) +func (f Foh) KillPindle() error { + return f.killBoss(npc.DefiledWarrior, data.MonsterTypeSuperUnique) } -func (s Foh) KillNihlathak() error { - return s.killBoss(npc.Nihlathak, data.MonsterTypeSuperUnique) +func (f Foh) KillNihlathak() error { + return f.killBoss(npc.Nihlathak, data.MonsterTypeSuperUnique) } -func (s Foh) KillBaal() error { - return s.killBoss(npc.BaalCrab, data.MonsterTypeUnique) +func (f Foh) KillBaal() error { + return f.killBoss(npc.BaalCrab, data.MonsterTypeUnique) } From 8cb0bca9e02d614820a7435e4ef4a620a9787f12 Mon Sep 17 00:00:00 2001 From: elobo91 Date: Tue, 12 Nov 2024 19:34:08 -0500 Subject: [PATCH 03/13] . --- internal/character/foh.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/character/foh.go b/internal/character/foh.go index a7c34fb2..3db12491 100644 --- a/internal/character/foh.go +++ b/internal/character/foh.go @@ -116,8 +116,8 @@ func (f Foh) KillMonsterSequence(monsterSelector func(d game.Data) (data.UnitID, } // Determine if we should use Holy Bolt - // Only use Holy Bolt if it's a single target and it's immune to lightning after conviction - shouldUseHB := validTargets == 1 && monster.IsImmune(stat.LightImmune) && monster.States.HasState(state.Convicted) + // Only use Holy Bolt if it's a single target and it's immune to lightning + shouldUseHB := validTargets == 1 && monster.IsImmune(stat.LightImmune) return id, true, shouldUseHB } @@ -216,7 +216,7 @@ func (f Foh) handleBoss(bossID data.UnitID, fohOpts, hbOpts []step.AttackOption, } } - *completedAttackLoops++ + (*completedAttackLoops)++ } } } From 857a6eff4323508a19412465e97e46f1bc12a9eb Mon Sep 17 00:00:00 2001 From: elobo91 Date: Tue, 12 Nov 2024 20:53:25 -0500 Subject: [PATCH 04/13] Travincal Foh optimization --- internal/character/foh.go | 60 ++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/internal/character/foh.go b/internal/character/foh.go index 3db12491..689ec56d 100644 --- a/internal/character/foh.go +++ b/internal/character/foh.go @@ -3,8 +3,8 @@ package character import ( "fmt" "github.com/hectorgimenez/d2go/pkg/data/mode" + "github.com/hectorgimenez/koolo/internal/action" "log/slog" - "sort" "time" "github.com/hectorgimenez/d2go/pkg/data" @@ -313,21 +313,55 @@ func (f Foh) KillDuriel() error { } func (f Foh) KillCouncil() error { - return f.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { - 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) + // Disable item pickup while killing council members + context.Get().DisableItemPickup() + defer context.Get().EnableItemPickup() + + err := f.killAllCouncilMembers() + if err != nil { + return err + } + + // Wait a moment for items to settle + time.Sleep(300 * time.Millisecond) + + // Re-enable item pickup and do a final pickup pass + err = action.ItemPickup(40) + if err != nil { + f.Logger.Warn("Error during final item pickup after council", "error", err) + } + + return nil +} +func (f Foh) killAllCouncilMembers() error { + for { + if !f.anyCouncilMemberAlive() { + return nil + } + + err := f.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) { + for _, m := range d.Monsters.Enemies() { + if (m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3) && m.Stats[stat.Life] > 0 { + return m.UnitID, true + } } + return 0, false + }, nil) + + if err != nil { + return err } - sort.Slice(councilMembers, func(i, j int) bool { - return f.PathFinder.DistanceFromMe(councilMembers[i].Position) < f.PathFinder.DistanceFromMe(councilMembers[j].Position) - }) - if len(councilMembers) > 0 { - return councilMembers[0].UnitID, true + } +} + +func (f Foh) anyCouncilMemberAlive() bool { + for _, m := range f.Data.Monsters.Enemies() { + if (m.Name == npc.CouncilMember || m.Name == npc.CouncilMember2 || m.Name == npc.CouncilMember3) && m.Stats[stat.Life] > 0 { + return true } - return 0, false - }, nil) + + } + return false } func (f Foh) KillMephisto() error { From 43f852fd9f70d3f5d3b978fe3419bb5953ad8e28 Mon Sep 17 00:00:00 2001 From: elobo91 Date: Wed, 13 Nov 2024 18:11:28 -0500 Subject: [PATCH 05/13] Resolve unsync pathing issue --- internal/action/move.go | 98 ++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/internal/action/move.go b/internal/action/move.go index edcbaa04..5cb7db97 100644 --- a/internal/action/move.go +++ b/internal/action/move.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "sort" + "time" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/area" @@ -13,9 +14,39 @@ import ( "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/event" "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/pather" ) +const ( + maxAreaSyncAttempts = 10 + areaSyncDelay = 100 * time.Millisecond +) + +func ensureAreaSync(ctx *context.Status, expectedArea area.ID) error { + // Skip sync check if we're already in the expected area and have valid area data + if ctx.Data.PlayerUnit.Area == expectedArea { + if areaData, ok := ctx.Data.Areas[expectedArea]; ok && areaData.IsInside(ctx.Data.PlayerUnit.Position) { + return nil + } + } + + // Wait for area data to sync + for attempts := 0; attempts < maxAreaSyncAttempts; attempts++ { + ctx.RefreshGameData() + + if ctx.Data.PlayerUnit.Area == expectedArea { + if areaData, ok := ctx.Data.Areas[expectedArea]; ok { + if areaData.IsInside(ctx.Data.PlayerUnit.Position) { + return nil + } + } + } + + time.Sleep(areaSyncDelay) + } + + return fmt.Errorf("area sync timeout - expected: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area) +} + func MoveToArea(dst area.ID) error { ctx := context.Get() ctx.SetLastAction("MoveToArea") @@ -25,7 +56,11 @@ func MoveToArea(dst area.ID) error { ctx.CurrentGame.AreaCorrection.Enabled = true }() - // Exception for Arcane Sanctuary, we need to find the portal first + if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { + return err + } + + // Exception for Arcane Sanctuary 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) @@ -108,13 +143,17 @@ func MoveToArea(dst area.ID) error { if err != nil { return err } + + // Wait for area transition to complete + if err := ensureAreaSync(ctx, dst); err != nil { + return err + } } event.Send(event.InteractedTo(event.Text(ctx.Name, ""), int(dst), event.InteractionTypeEntrance)) return nil } - func MoveToCoords(to data.Position) error { ctx := context.Get() ctx.CurrentGame.AreaCorrection.Enabled = false @@ -123,11 +162,16 @@ func MoveToCoords(to data.Position) error { ctx.CurrentGame.AreaCorrection.Enabled = true }() + if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { + return err + } + return MoveTo(func() (data.Position, bool) { return to, true }) } +// MoveTo moves to a destination, ensuring area data is synchronized func MoveTo(toFunc func() (data.Position, bool)) error { ctx := context.Get() ctx.SetLastAction("MoveTo") @@ -135,6 +179,12 @@ func MoveTo(toFunc func() (data.Position, bool)) error { openedDoors := make(map[object.Name]data.Position) previousIterationPosition := data.Position{} lastMovement := false + + // Initial sync check + if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { + return err + } + for { ctx.RefreshGameData() to, found := toFunc() @@ -142,12 +192,12 @@ func MoveTo(toFunc func() (data.Position, bool)) error { return nil } - // If we can teleport, don't bother with the rest, stop here + // If we can teleport, don't bother with the rest if ctx.Data.CanTeleport() { return step.MoveTo(to) } - // Check if there is a door blocking our path + // Check for doors blocking path for _, o := range ctx.Data.Objects { if o.IsDoor() && ctx.PathFinder.DistanceFromMe(o.Position) < 10 && openedDoors[o.Name] != o.Position { if o.Selectable { @@ -155,7 +205,6 @@ func MoveTo(toFunc func() (data.Position, bool)) error { 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 { @@ -165,32 +214,16 @@ func MoveTo(toFunc func() (data.Position, bool)) error { } } - // 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 + // Check for monsters close to 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 + minDistanceForElites := 20 + stuck := ctx.PathFinder.DistanceFromMe(previousIterationPosition) < 5 + for _, m := range ctx.Data.Monsters.Enemies() { - // Skip if monster is already dead if m.Stats[stat.Life] <= 0 { continue } @@ -207,15 +240,13 @@ func MoveTo(toFunc func() (data.Position, bool)) error { appended = true } - if appended { - if dist < closestMonsterDistance { - closestMonsterDistance = dist - closestMonster = m - } + if appended && 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 len(targetedNormalEnemies) > 5 || len(targetedElites) > 0 || (stuck && (len(targetedNormalEnemies) > 0 || len(targetedElites) > 0)) { if stuck { ctx.Logger.Info("Character stuck and monsters detected, trying to kill monsters around") } else { @@ -240,15 +271,12 @@ func MoveTo(toFunc func() (data.Position, bool)) error { } } - // Continue moving - WaitForAllMembersWhenLeveling() previousIterationPosition = ctx.Data.PlayerUnit.Position if lastMovement { return nil } - // TODO: refactor this to use the same approach as ClearThroughPath if _, distance, _ := ctx.PathFinder.GetPathFrom(ctx.Data.PlayerUnit.Position, to); distance <= step.DistanceToFinishMoving { lastMovement = true } From 320cf5561505f0197ee22fa37616ac66ca24134e Mon Sep 17 00:00:00 2001 From: elobo91 Date: Wed, 13 Nov 2024 18:24:15 -0500 Subject: [PATCH 06/13] . --- internal/action/move.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/action/move.go b/internal/action/move.go index 5cb7db97..135aa02f 100644 --- a/internal/action/move.go +++ b/internal/action/move.go @@ -2,6 +2,7 @@ package action import ( "fmt" + "github.com/hectorgimenez/koolo/internal/pather" "log/slog" "sort" "time" @@ -220,10 +221,11 @@ func MoveTo(toFunc func() (data.Position, bool)) error { targetedNormalEnemies := make([]data.Monster, 0) targetedElites := make([]data.Monster, 0) minDistance := 6 - minDistanceForElites := 20 - stuck := ctx.PathFinder.DistanceFromMe(previousIterationPosition) < 5 + 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 } @@ -246,7 +248,7 @@ func MoveTo(toFunc func() (data.Position, bool)) error { } } - if len(targetedNormalEnemies) > 5 || len(targetedElites) > 0 || (stuck && (len(targetedNormalEnemies) > 0 || len(targetedElites) > 0)) { + 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 { @@ -271,12 +273,15 @@ func MoveTo(toFunc func() (data.Position, bool)) error { } } + // Continue moving + WaitForAllMembersWhenLeveling() previousIterationPosition = ctx.Data.PlayerUnit.Position if lastMovement { return nil } + // TODO: refactor this to use the same approach as ClearThroughPath if _, distance, _ := ctx.PathFinder.GetPathFrom(ctx.Data.PlayerUnit.Position, to); distance <= step.DistanceToFinishMoving { lastMovement = true } From c93252a315f4d21c4d0c5b9b8b0560c584fc3b79 Mon Sep 17 00:00:00 2001 From: elobo91 Date: Wed, 13 Nov 2024 22:27:10 -0500 Subject: [PATCH 07/13] Fix on interact entrance / red portals ++ --- internal/action/clear_level.go | 38 ++++++++++-- internal/action/move.go | 48 +++++++++++++-- internal/action/step/interact_entrance.go | 22 ++++--- internal/action/step/interact_object.go | 71 +++++++++++++++++------ internal/action/tp_actions.go | 42 ++++++++++++-- 5 files changed, 182 insertions(+), 39 deletions(-) diff --git a/internal/action/clear_level.go b/internal/action/clear_level.go index 98ffde84..78a3f7d0 100644 --- a/internal/action/clear_level.go +++ b/internal/action/clear_level.go @@ -15,22 +15,38 @@ func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error { ctx := context.Get() ctx.SetLastAction("ClearCurrentLevel") + // First check if we're in town, if so, exit early + if ctx.Data.PlayerUnit.Area.IsTown() { + return fmt.Errorf("cannot clear level in town") + } + + currentArea := ctx.Data.PlayerUnit.Area rooms := ctx.PathFinder.OptimizeRoomsTraverseOrder() for _, r := range rooms { + // If we're no longer in the same area (e.g., took portal to town), stop clearing + if ctx.Data.PlayerUnit.Area != currentArea { + return nil + } + err := clearRoom(r, filter) if err != nil { - ctx.Logger.Warn("Failed to clear room: %v", err) + ctx.Logger.Warn("Failed to clear room", "error", err) } if !openChests { continue } + // Again check area before chest operations + if ctx.Data.PlayerUnit.Area != currentArea { + return nil + } + 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) + ctx.Logger.Warn("Failed moving to chest", "error", err) continue } err = InteractObject(o, func() bool { @@ -38,20 +54,21 @@ func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error { return !chest.Selectable }) if err != nil { - ctx.Logger.Warn("Failed interacting with chest: %v", err) + ctx.Logger.Warn("Failed interacting with chest", "error", err) } - utils.Sleep(500) // Add small delay to allow the game to open the chest and drop the content + utils.Sleep(500) } } } return nil } - func clearRoom(room data.Room, filter data.MonsterFilter) error { ctx := context.Get() ctx.SetLastAction("clearRoom") + currentArea := ctx.Data.PlayerUnit.Area + path, _, found := ctx.PathFinder.GetClosestWalkablePath(room.GetCenter()) if !found { return errors.New("failed to find a path to the room center") @@ -61,12 +78,23 @@ func clearRoom(room data.Room, filter data.MonsterFilter) error { X: path.To().X + ctx.Data.AreaOrigin.X, Y: path.To().Y + ctx.Data.AreaOrigin.Y, } + + // Check if we're still in the same area before moving + if ctx.Data.PlayerUnit.Area != currentArea { + return nil + } + err := MoveToCoords(to) if err != nil { return fmt.Errorf("failed moving to room center: %w", err) } for { + // Exit if we've changed areas + if ctx.Data.PlayerUnit.Area != currentArea { + return nil + } + monsters := getMonstersInRoom(room, filter) if len(monsters) == 0 { return nil diff --git a/internal/action/move.go b/internal/action/move.go index 135aa02f..13441010 100644 --- a/internal/action/move.go +++ b/internal/action/move.go @@ -3,6 +3,7 @@ package action import ( "fmt" "github.com/hectorgimenez/koolo/internal/pather" + "github.com/hectorgimenez/koolo/internal/utils" "log/slog" "sort" "time" @@ -140,9 +141,49 @@ func MoveToArea(dst area.ID) error { } if lvl.IsEntrance { - err = step.InteractEntrance(dst) + maxAttempts := 3 + for attempt := 0; attempt < maxAttempts; attempt++ { + // Check current distance + currentDistance := ctx.PathFinder.DistanceFromMe(lvl.Position) + // If we're too far, try to get closer using direct clicks + if currentDistance > 4 { + ctx.Logger.Debug("Attempting to move closer to entrance using direct movement") + + // Calculate screen coordinates for a position closer to the entrance + screenX, screenY := ctx.PathFinder.GameCoordsToScreenCords( + lvl.Position.X-2, + lvl.Position.Y-2, + ) + + // Use direct click to move closer + ctx.HID.Click(game.LeftButton, screenX, screenY) + + // Give time for movement + utils.Sleep(800) + ctx.RefreshGameData() + + // Verify new position + newDistance := ctx.PathFinder.DistanceFromMe(lvl.Position) + ctx.Logger.Debug("New distance after move attempt", + slog.Int("distance", newDistance), + slog.Int("attempt", attempt+1)) + } + + err = step.InteractEntrance(dst) + if err == nil { + break + } + + if attempt < maxAttempts-1 { + ctx.Logger.Debug("Entrance interaction failed, retrying...", + slog.Int("attempt", attempt+1), + slog.String("error", err.Error())) + utils.Sleep(1000) + } + } + if err != nil { - return err + return fmt.Errorf("failed to interact with area %s after %d attempts: %v", dst.Area().Name, maxAttempts, err) } // Wait for area transition to complete @@ -152,9 +193,9 @@ func MoveToArea(dst area.ID) error { } event.Send(event.InteractedTo(event.Text(ctx.Name, ""), int(dst), event.InteractionTypeEntrance)) - return nil } + func MoveToCoords(to data.Position) error { ctx := context.Get() ctx.CurrentGame.AreaCorrection.Enabled = false @@ -172,7 +213,6 @@ func MoveToCoords(to data.Position) error { }) } -// MoveTo moves to a destination, ensuring area data is synchronized func MoveTo(toFunc func() (data.Position, bool)) error { ctx := context.Get() ctx.SetLastAction("MoveTo") diff --git a/internal/action/step/interact_entrance.go b/internal/action/step/interact_entrance.go index 097a4ef0..ad4d7d2c 100644 --- a/internal/action/step/interact_entrance.go +++ b/internal/action/step/interact_entrance.go @@ -1,15 +1,17 @@ package step import ( - "errors" "fmt" - "time" - "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/area" "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/game" "github.com/hectorgimenez/koolo/internal/utils" + "time" +) + +const ( + maxEntranceDistance = 6 // Increased from 3 to accommodate DistanceToFinishMoving ) func InteractEntrance(area area.ID) error { @@ -23,12 +25,10 @@ func InteractEntrance(area area.ID) error { ctx.SetLastStep("InteractEntrance") for { - // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() // Give some extra time to render the UI if ctx.Data.AreaData.Area == area && time.Since(lastRun) > time.Millisecond*500 && ctx.Data.AreaData.IsInside(ctx.Data.PlayerUnit.Position) { - // We've successfully entered the new area return nil } @@ -44,21 +44,27 @@ func InteractEntrance(area area.ID) error { 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 distance > maxEntranceDistance { + return fmt.Errorf("entrance too far away (distance: %d)", distance) } if l.IsEntrance { - lx, ly := ctx.PathFinder.GameCoordsToScreenCords(l.Position.X-2, l.Position.Y-2) + // Adjust click position to be slightly closer to entrance + lx, ly := ctx.PathFinder.GameCoordsToScreenCords(l.Position.X-1, l.Position.Y-1) 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 + utils.Sleep(200) // Small delay after click } + // Smaller spiral pattern for more precise clicks x, y := utils.Spiral(interactionAttempts) + x = x / 3 // Reduce spiral size further + y = y / 3 currentMouseCoords = data.Position{X: lx + x, Y: ly + y} ctx.HID.MovePointer(lx+x, ly+y) interactionAttempts++ + utils.Sleep(100) // Small delay for mouse movement continue } diff --git a/internal/action/step/interact_object.go b/internal/action/step/interact_object.go index df9be8a2..73a2230f 100644 --- a/internal/action/step/interact_object.go +++ b/internal/action/step/interact_object.go @@ -1,47 +1,57 @@ package step import ( - "errors" "fmt" - "time" - "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/context" "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/ui" "github.com/hectorgimenez/koolo/internal/utils" + "time" +) + +const ( + maxInteractionAttempts = 5 + portalSyncDelay = 200 + maxPortalSyncAttempts = 15 ) func InteractObject(obj data.Object, isCompletedFn func() bool) error { - maxInteractionAttempts := 10 interactionAttempts := 0 - maxMouseOverAttempts := 20 mouseOverAttempts := 0 waitingForInteraction := false currentMouseCoords := data.Position{} lastRun := time.Time{} - // If there is no check, just assume the interaction is completed after clicking + ctx := context.Get() + ctx.SetLastStep("InteractObject") + + // If there is no completion check, just assume the interaction is completed after clicking if isCompletedFn == nil { isCompletedFn = func() bool { return waitingForInteraction } } - ctx := context.Get() - ctx.SetLastStep("InteractObject") + // For portals, we need to ensure proper area sync + expectedArea := area.ID(0) + if obj.IsRedPortal() { + // For red portals, we need to determine the expected destination + switch { + case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.StonyField: + expectedArea = area.Tristram + case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.RogueEncampment: + expectedArea = area.MooMooFarm + } + } for !isCompletedFn() { // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() - if time.Since(lastRun) < time.Millisecond*100 { - continue - } - - if interactionAttempts >= maxInteractionAttempts || mouseOverAttempts >= maxMouseOverAttempts { - return errors.New("failed interacting with object") + if interactionAttempts >= maxInteractionAttempts || mouseOverAttempts >= 20 { + return fmt.Errorf("failed interacting with object") } ctx.RefreshGameData() @@ -70,6 +80,33 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) waitingForInteraction = true interactionAttempts++ + + // For portals, we need to wait for proper area sync + if expectedArea != 0 { + utils.Sleep(500) // Initial delay for area transition + for attempts := 0; attempts < maxPortalSyncAttempts; attempts++ { + ctx.RefreshGameData() + if ctx.Data.PlayerUnit.Area == expectedArea { + if areaData, ok := ctx.Data.Areas[expectedArea]; ok { + if areaData.IsInside(ctx.Data.PlayerUnit.Position) { + // For special areas, ensure we have proper object data loaded + switch expectedArea { + case area.Tristram, area.MooMooFarm: + if len(ctx.Data.Objects) > 0 { + return nil + } + default: + return nil + } + } + } + } + utils.Sleep(portalSyncDelay) + } + if !isCompletedFn() { + return fmt.Errorf("portal sync timeout - expected area: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area) + } + } continue } else { objectX := o.Position.X - 2 @@ -79,10 +116,10 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { return fmt.Errorf("object is too far away: %d. Current distance: %d", o.Name, distance) } - mX, mY := ui.GameCoordsToScreenCords(objectX, objectY) + mX, mY := ctx.PathFinder.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 mouseOverAttempts == 2 && o.Name == object.TownPortal { - mX, mY = ui.GameCoordsToScreenCords(objectX-4, objectY-4) + mX, mY = ctx.PathFinder.GameCoordsToScreenCords(objectX-4, objectY-4) } x, y := utils.Spiral(mouseOverAttempts) diff --git a/internal/action/tp_actions.go b/internal/action/tp_actions.go index 6c0699c6..75486782 100644 --- a/internal/action/tp_actions.go +++ b/internal/action/tp_actions.go @@ -3,6 +3,7 @@ package action import ( "errors" "fmt" + "time" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/object" @@ -35,9 +36,33 @@ func ReturnTown() error { } // Now that it is safe, interact with portal - return InteractObject(portal, func() bool { + err = InteractObject(portal, func() bool { return ctx.Data.PlayerUnit.Area.IsTown() }) + if err != nil { + return err + } + + // Wait for area transition and data sync + utils.Sleep(1000) + ctx.RefreshGameData() + + // Wait for town area data to be fully loaded + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + if ctx.Data.PlayerUnit.Area.IsTown() { + // Verify area data exists and is loaded + if townData, ok := ctx.Data.Areas[ctx.Data.PlayerUnit.Area]; ok { + if townData.IsInside(ctx.Data.PlayerUnit.Position) { + return nil + } + } + } + utils.Sleep(100) + ctx.RefreshGameData() + } + + return fmt.Errorf("failed to verify town area data after portal transition") } func UsePortalInTown() error { @@ -52,11 +77,12 @@ func UsePortalInTown() error { return err } - // Wait for the game to fully load after using the portal - ctx.WaitForGameToLoad() - - // Refresh game data to ensure we have the latest information + // Wait for area sync before attempting any movement + utils.Sleep(500) ctx.RefreshGameData() + if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { + return err + } // Ensure we're not in town if ctx.Data.PlayerUnit.Area.IsTown() { @@ -84,7 +110,13 @@ func UsePortalFrom(owner string) error { if obj.IsPortal() && obj.Owner == owner { return InteractObjectByID(obj.ID, func() bool { if !ctx.Data.PlayerUnit.Area.IsTown() { + // Ensure area data is synced after portal transition utils.Sleep(500) + ctx.RefreshGameData() + + if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { + return false + } return true } return false From eea0bcc9bbc47226098164726c81ac19e38d3641 Mon Sep 17 00:00:00 2001 From: elobo91 Date: Wed, 13 Nov 2024 22:37:35 -0500 Subject: [PATCH 08/13] . --- internal/action/clear_level.go | 2 +- internal/action/step/interact_entrance.go | 5 +++-- internal/action/step/interact_object.go | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/action/clear_level.go b/internal/action/clear_level.go index 78a3f7d0..4b9f10c9 100644 --- a/internal/action/clear_level.go +++ b/internal/action/clear_level.go @@ -56,7 +56,7 @@ func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error { if err != nil { ctx.Logger.Warn("Failed interacting with chest", "error", err) } - utils.Sleep(500) + utils.Sleep(500) // Add small delay to allow the game to open the chest and drop the content } } } diff --git a/internal/action/step/interact_entrance.go b/internal/action/step/interact_entrance.go index ad4d7d2c..a5a67c3c 100644 --- a/internal/action/step/interact_entrance.go +++ b/internal/action/step/interact_entrance.go @@ -11,7 +11,7 @@ import ( ) const ( - maxEntranceDistance = 6 // Increased from 3 to accommodate DistanceToFinishMoving + maxEntranceDistance = 6 ) func InteractEntrance(area area.ID) error { @@ -25,10 +25,12 @@ func InteractEntrance(area area.ID) error { ctx.SetLastStep("InteractEntrance") for { + // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() // Give some extra time to render the UI if ctx.Data.AreaData.Area == area && time.Since(lastRun) > time.Millisecond*500 && ctx.Data.AreaData.IsInside(ctx.Data.PlayerUnit.Position) { + // We've successfully entered the new area return nil } @@ -57,7 +59,6 @@ func InteractEntrance(area area.ID) error { utils.Sleep(200) // Small delay after click } - // Smaller spiral pattern for more precise clicks x, y := utils.Spiral(interactionAttempts) x = x / 3 // Reduce spiral size further y = y / 3 diff --git a/internal/action/step/interact_object.go b/internal/action/step/interact_object.go index 73a2230f..298c9010 100644 --- a/internal/action/step/interact_object.go +++ b/internal/action/step/interact_object.go @@ -7,6 +7,7 @@ import ( "github.com/hectorgimenez/d2go/pkg/data/object" "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/game" + "github.com/hectorgimenez/koolo/internal/ui" "github.com/hectorgimenez/koolo/internal/utils" "time" ) @@ -116,10 +117,10 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { return fmt.Errorf("object is too far away: %d. Current distance: %d", o.Name, distance) } - mX, mY := ctx.PathFinder.GameCoordsToScreenCords(objectX, objectY) + mX, mY := ui.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 mouseOverAttempts == 2 && o.Name == object.TownPortal { - mX, mY = ctx.PathFinder.GameCoordsToScreenCords(objectX-4, objectY-4) + mX, mY = ui.GameCoordsToScreenCords(objectX-4, objectY-4) } x, y := utils.Spiral(mouseOverAttempts) From 5225bc348b23f72357c427d089db7b6171584cf5 Mon Sep 17 00:00:00 2001 From: elobo91 Date: Wed, 13 Nov 2024 22:43:03 -0500 Subject: [PATCH 09/13] . --- internal/action/move.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/action/move.go b/internal/action/move.go index 13441010..ce0c9fd4 100644 --- a/internal/action/move.go +++ b/internal/action/move.go @@ -254,6 +254,21 @@ func MoveTo(toFunc func() (data.Position, bool)) error { } } } + // 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 + } + } + } // Check for monsters close to player closestMonster := data.Monster{} From 73388ac0d84a66b89c69839dd74debc00bc2f69f Mon Sep 17 00:00:00 2001 From: elobo91 Date: Fri, 15 Nov 2024 17:52:52 -0500 Subject: [PATCH 10/13] interact entrance corrections. --- internal/action/clear_level.go | 36 +++-------------------- internal/action/step/interact_entrance.go | 35 +++++++++++++++++----- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/internal/action/clear_level.go b/internal/action/clear_level.go index 4b9f10c9..98ffde84 100644 --- a/internal/action/clear_level.go +++ b/internal/action/clear_level.go @@ -15,38 +15,22 @@ func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error { ctx := context.Get() ctx.SetLastAction("ClearCurrentLevel") - // First check if we're in town, if so, exit early - if ctx.Data.PlayerUnit.Area.IsTown() { - return fmt.Errorf("cannot clear level in town") - } - - currentArea := ctx.Data.PlayerUnit.Area rooms := ctx.PathFinder.OptimizeRoomsTraverseOrder() for _, r := range rooms { - // If we're no longer in the same area (e.g., took portal to town), stop clearing - if ctx.Data.PlayerUnit.Area != currentArea { - return nil - } - err := clearRoom(r, filter) if err != nil { - ctx.Logger.Warn("Failed to clear room", "error", err) + ctx.Logger.Warn("Failed to clear room: %v", err) } if !openChests { continue } - // Again check area before chest operations - if ctx.Data.PlayerUnit.Area != currentArea { - return nil - } - 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", "error", err) + ctx.Logger.Warn("Failed moving to chest: %v", err) continue } err = InteractObject(o, func() bool { @@ -54,7 +38,7 @@ func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error { return !chest.Selectable }) if err != nil { - ctx.Logger.Warn("Failed interacting with chest", "error", err) + ctx.Logger.Warn("Failed interacting with chest: %v", err) } utils.Sleep(500) // Add small delay to allow the game to open the chest and drop the content } @@ -63,12 +47,11 @@ func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error { return nil } + func clearRoom(room data.Room, filter data.MonsterFilter) error { ctx := context.Get() ctx.SetLastAction("clearRoom") - currentArea := ctx.Data.PlayerUnit.Area - path, _, found := ctx.PathFinder.GetClosestWalkablePath(room.GetCenter()) if !found { return errors.New("failed to find a path to the room center") @@ -78,23 +61,12 @@ func clearRoom(room data.Room, filter data.MonsterFilter) error { X: path.To().X + ctx.Data.AreaOrigin.X, Y: path.To().Y + ctx.Data.AreaOrigin.Y, } - - // Check if we're still in the same area before moving - if ctx.Data.PlayerUnit.Area != currentArea { - return nil - } - err := MoveToCoords(to) if err != nil { return fmt.Errorf("failed moving to room center: %w", err) } for { - // Exit if we've changed areas - if ctx.Data.PlayerUnit.Area != currentArea { - return nil - } - monsters := getMonstersInRoom(room, filter) if len(monsters) == 0 { return nil diff --git a/internal/action/step/interact_entrance.go b/internal/action/step/interact_entrance.go index a5a67c3c..6935a12d 100644 --- a/internal/action/step/interact_entrance.go +++ b/internal/action/step/interact_entrance.go @@ -12,6 +12,7 @@ import ( const ( maxEntranceDistance = 6 + maxMoveRetries = 3 ) func InteractEntrance(area area.ID) error { @@ -25,12 +26,9 @@ func InteractEntrance(area area.ID) error { ctx.SetLastStep("InteractEntrance") for { - // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() - // Give some extra time to render the UI if ctx.Data.AreaData.Area == area && time.Since(lastRun) > time.Millisecond*500 && ctx.Data.AreaData.IsInside(ctx.Data.PlayerUnit.Position) { - // We've successfully entered the new area return nil } @@ -47,25 +45,46 @@ func InteractEntrance(area area.ID) error { if l.Area == area { distance := ctx.PathFinder.DistanceFromMe(l.Position) if distance > maxEntranceDistance { - return fmt.Errorf("entrance too far away (distance: %d)", distance) + // Try to move closer with retries + for retry := 0; retry < maxMoveRetries; retry++ { + if err := MoveTo(l.Position); err != nil { + // If MoveTo fails, try direct movement + screenX, screenY := ctx.PathFinder.GameCoordsToScreenCords( + l.Position.X-2, + l.Position.Y-2, + ) + ctx.HID.Click(game.LeftButton, screenX, screenY) + utils.Sleep(800) + ctx.RefreshGameData() + } + + // Check if we're close enough now + newDistance := ctx.PathFinder.DistanceFromMe(l.Position) + if newDistance <= maxEntranceDistance { + break + } + + if retry == maxMoveRetries-1 { + return fmt.Errorf("entrance too far away (distance: %d)", distance) + } + } } if l.IsEntrance { - // Adjust click position to be slightly closer to entrance lx, ly := ctx.PathFinder.GameCoordsToScreenCords(l.Position.X-1, l.Position.Y-1) 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 - utils.Sleep(200) // Small delay after click + utils.Sleep(200) } x, y := utils.Spiral(interactionAttempts) - x = x / 3 // Reduce spiral size further + x = x / 3 y = y / 3 currentMouseCoords = data.Position{X: lx + x, Y: ly + y} ctx.HID.MovePointer(lx+x, ly+y) interactionAttempts++ - utils.Sleep(100) // Small delay for mouse movement + utils.Sleep(100) continue } From bcdd732cc5a6d2d7e009c3d594e31c980f9e727b Mon Sep 17 00:00:00 2001 From: elobo91 Date: Fri, 15 Nov 2024 20:25:16 -0500 Subject: [PATCH 11/13] . --- internal/action/tp_actions.go | 135 ++++++++++++++++++++++++++-------- 1 file changed, 104 insertions(+), 31 deletions(-) diff --git a/internal/action/tp_actions.go b/internal/action/tp_actions.go index 75486782..029fbb2a 100644 --- a/internal/action/tp_actions.go +++ b/internal/action/tp_actions.go @@ -3,6 +3,7 @@ package action import ( "errors" "fmt" + "log/slog" "time" "github.com/hectorgimenez/d2go/pkg/data" @@ -13,6 +14,12 @@ import ( "github.com/hectorgimenez/koolo/internal/utils" ) +const ( + portalTransitionTimeout = 3 * time.Second + portalSyncDelay = 100 + initialTransitionDelay = 200 +) + func ReturnTown() error { ctx := context.Get() ctx.SetLastAction("ReturnTown") @@ -22,10 +29,19 @@ func ReturnTown() error { return nil } + // Store initial state + fromArea := ctx.Data.PlayerUnit.Area + townArea := town.GetTownByArea(fromArea).TownArea() + + ctx.Logger.Debug("Starting town return sequence", + slog.String("from_area", fromArea.Area().Name), + slog.String("to_area", townArea.Area().Name)) + err := step.OpenPortal() if err != nil { return err } + portal, found := ctx.Data.Objects.FindOne(object.TownPortal) if !found { return errors.New("portal not found") @@ -35,6 +51,15 @@ func ReturnTown() error { ctx.Logger.Warn("Error clearing area around portal", "error", err) } + // Disable area correction before portal interaction + ctx.CurrentGame.AreaCorrection.Enabled = false + ctx.SwitchPriority(context.PriorityHigh) + defer func() { + ctx.CurrentGame.AreaCorrection.Enabled = true + ctx.SwitchPriority(context.PriorityNormal) + }() + + ctx.Logger.Debug("Interacting with portal") // Now that it is safe, interact with portal err = InteractObject(portal, func() bool { return ctx.Data.PlayerUnit.Area.IsTown() @@ -43,59 +68,103 @@ func ReturnTown() error { return err } - // Wait for area transition and data sync - utils.Sleep(1000) + // Initial delay to let the game process the transition request + utils.Sleep(initialTransitionDelay) ctx.RefreshGameData() - // Wait for town area data to be fully loaded - deadline := time.Now().Add(2 * time.Second) + // Verify we've actually started the transition + if ctx.Data.PlayerUnit.Area == fromArea { + ctx.Logger.Debug("Still in source area after initial delay, starting transition wait") + } + + // Wait for proper town transition + deadline := time.Now().Add(portalTransitionTimeout) + transitionStartTime := time.Now() + for time.Now().Before(deadline) { - if ctx.Data.PlayerUnit.Area.IsTown() { - // Verify area data exists and is loaded - if townData, ok := ctx.Data.Areas[ctx.Data.PlayerUnit.Area]; ok { - if townData.IsInside(ctx.Data.PlayerUnit.Position) { + ctx.RefreshGameData() + currentArea := ctx.Data.PlayerUnit.Area + + ctx.Logger.Debug("Waiting for transition", + slog.String("current_area", currentArea.Area().Name), + slog.Duration("elapsed", time.Since(transitionStartTime))) + + if currentArea == townArea { + if areaData, ok := ctx.Data.Areas[townArea]; ok { + if areaData.IsInside(ctx.Data.PlayerUnit.Position) { + ctx.Logger.Debug("Successfully reached town area") + utils.Sleep(300) // Extra wait to ensure everything is loaded + ctx.RefreshGameData() return nil } } } - utils.Sleep(100) - ctx.RefreshGameData() + utils.Sleep(portalSyncDelay) } - return fmt.Errorf("failed to verify town area data after portal transition") + return fmt.Errorf("failed to verify town transition within timeout (start area: %s)", fromArea.Area().Name) } - func UsePortalInTown() error { ctx := context.Get() ctx.SetLastAction("UsePortalInTown") tpArea := town.GetTownByArea(ctx.Data.PlayerUnit.Area).TPWaitingArea(*ctx.Data) - _ = MoveToCoords(tpArea) + if err := MoveToCoords(tpArea); err != nil { + return err + } + + // Store initial state + fromArea := ctx.Data.PlayerUnit.Area + + // Disable area correction and raise priority during transition + ctx.CurrentGame.AreaCorrection.Enabled = false + ctx.SwitchPriority(context.PriorityHigh) + defer func() { + ctx.CurrentGame.AreaCorrection.Enabled = true + ctx.SwitchPriority(context.PriorityNormal) + }() err := UsePortalFrom(ctx.Data.PlayerUnit.Name) if err != nil { return err } - // Wait for area sync before attempting any movement - utils.Sleep(500) + // Initial delay to let the game process the transition request + utils.Sleep(initialTransitionDelay) ctx.RefreshGameData() - if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { - return err - } - // Ensure we're not in town - if ctx.Data.PlayerUnit.Area.IsTown() { - return fmt.Errorf("failed to leave town area") - } + // Wait for proper area transition + deadline := time.Now().Add(portalTransitionTimeout) + transitionStartTime := time.Now() - // Perform item pickup after re-entering the portal - err = ItemPickup(40) - if err != nil { - ctx.Logger.Warn("Error during item pickup after portal use", "error", err) + for time.Now().Before(deadline) { + ctx.RefreshGameData() + currentArea := ctx.Data.PlayerUnit.Area + + ctx.Logger.Debug("Waiting for transition", + slog.String("current_area", currentArea.Area().Name), + slog.Duration("elapsed", time.Since(transitionStartTime))) + + if currentArea != fromArea { + if areaData, ok := ctx.Data.Areas[currentArea]; ok { + if areaData.IsInside(ctx.Data.PlayerUnit.Position) { + ctx.Logger.Debug("Successfully reached destination area") + utils.Sleep(300) // Extra wait to ensure everything is loaded + ctx.RefreshGameData() + + // Perform item pickup after re-entering the portal + err = ItemPickup(40) + if err != nil { + ctx.Logger.Warn("Error during item pickup after portal use", "error", err) + } + return nil + } + } + } + utils.Sleep(portalSyncDelay) } - return nil + return fmt.Errorf("failed to verify area transition within timeout (start area: %s)", fromArea.Area().Name) } func UsePortalFrom(owner string) error { @@ -110,14 +179,18 @@ func UsePortalFrom(owner string) error { if obj.IsPortal() && obj.Owner == owner { return InteractObjectByID(obj.ID, func() bool { if !ctx.Data.PlayerUnit.Area.IsTown() { - // Ensure area data is synced after portal transition - utils.Sleep(500) + // Initial delay to let the game process the transition + utils.Sleep(initialTransitionDelay) ctx.RefreshGameData() - if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { + // Verify we're no longer in town + if ctx.Data.PlayerUnit.Area.IsTown() { return false } - return true + + if areaData, ok := ctx.Data.Areas[ctx.Data.PlayerUnit.Area]; ok { + return areaData.IsInside(ctx.Data.PlayerUnit.Position) + } } return false }) From 83effcae718ac8aa91771628a0e7b16068327e63 Mon Sep 17 00:00:00 2001 From: elobo91 Date: Fri, 15 Nov 2024 22:48:00 -0500 Subject: [PATCH 12/13] Revert "." This reverts commit bcdd732cc5a6d2d7e009c3d594e31c980f9e727b. --- internal/action/tp_actions.go | 135 ++++++++-------------------------- 1 file changed, 31 insertions(+), 104 deletions(-) diff --git a/internal/action/tp_actions.go b/internal/action/tp_actions.go index 029fbb2a..75486782 100644 --- a/internal/action/tp_actions.go +++ b/internal/action/tp_actions.go @@ -3,7 +3,6 @@ package action import ( "errors" "fmt" - "log/slog" "time" "github.com/hectorgimenez/d2go/pkg/data" @@ -14,12 +13,6 @@ import ( "github.com/hectorgimenez/koolo/internal/utils" ) -const ( - portalTransitionTimeout = 3 * time.Second - portalSyncDelay = 100 - initialTransitionDelay = 200 -) - func ReturnTown() error { ctx := context.Get() ctx.SetLastAction("ReturnTown") @@ -29,19 +22,10 @@ func ReturnTown() error { return nil } - // Store initial state - fromArea := ctx.Data.PlayerUnit.Area - townArea := town.GetTownByArea(fromArea).TownArea() - - ctx.Logger.Debug("Starting town return sequence", - slog.String("from_area", fromArea.Area().Name), - slog.String("to_area", townArea.Area().Name)) - err := step.OpenPortal() if err != nil { return err } - portal, found := ctx.Data.Objects.FindOne(object.TownPortal) if !found { return errors.New("portal not found") @@ -51,15 +35,6 @@ func ReturnTown() error { ctx.Logger.Warn("Error clearing area around portal", "error", err) } - // Disable area correction before portal interaction - ctx.CurrentGame.AreaCorrection.Enabled = false - ctx.SwitchPriority(context.PriorityHigh) - defer func() { - ctx.CurrentGame.AreaCorrection.Enabled = true - ctx.SwitchPriority(context.PriorityNormal) - }() - - ctx.Logger.Debug("Interacting with portal") // Now that it is safe, interact with portal err = InteractObject(portal, func() bool { return ctx.Data.PlayerUnit.Area.IsTown() @@ -68,103 +43,59 @@ func ReturnTown() error { return err } - // Initial delay to let the game process the transition request - utils.Sleep(initialTransitionDelay) + // Wait for area transition and data sync + utils.Sleep(1000) ctx.RefreshGameData() - // Verify we've actually started the transition - if ctx.Data.PlayerUnit.Area == fromArea { - ctx.Logger.Debug("Still in source area after initial delay, starting transition wait") - } - - // Wait for proper town transition - deadline := time.Now().Add(portalTransitionTimeout) - transitionStartTime := time.Now() - + // Wait for town area data to be fully loaded + deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { - ctx.RefreshGameData() - currentArea := ctx.Data.PlayerUnit.Area - - ctx.Logger.Debug("Waiting for transition", - slog.String("current_area", currentArea.Area().Name), - slog.Duration("elapsed", time.Since(transitionStartTime))) - - if currentArea == townArea { - if areaData, ok := ctx.Data.Areas[townArea]; ok { - if areaData.IsInside(ctx.Data.PlayerUnit.Position) { - ctx.Logger.Debug("Successfully reached town area") - utils.Sleep(300) // Extra wait to ensure everything is loaded - ctx.RefreshGameData() + if ctx.Data.PlayerUnit.Area.IsTown() { + // Verify area data exists and is loaded + if townData, ok := ctx.Data.Areas[ctx.Data.PlayerUnit.Area]; ok { + if townData.IsInside(ctx.Data.PlayerUnit.Position) { return nil } } } - utils.Sleep(portalSyncDelay) + utils.Sleep(100) + ctx.RefreshGameData() } - return fmt.Errorf("failed to verify town transition within timeout (start area: %s)", fromArea.Area().Name) + return fmt.Errorf("failed to verify town area data after portal transition") } + func UsePortalInTown() error { ctx := context.Get() ctx.SetLastAction("UsePortalInTown") tpArea := town.GetTownByArea(ctx.Data.PlayerUnit.Area).TPWaitingArea(*ctx.Data) - if err := MoveToCoords(tpArea); err != nil { - return err - } - - // Store initial state - fromArea := ctx.Data.PlayerUnit.Area - - // Disable area correction and raise priority during transition - ctx.CurrentGame.AreaCorrection.Enabled = false - ctx.SwitchPriority(context.PriorityHigh) - defer func() { - ctx.CurrentGame.AreaCorrection.Enabled = true - ctx.SwitchPriority(context.PriorityNormal) - }() + _ = MoveToCoords(tpArea) err := UsePortalFrom(ctx.Data.PlayerUnit.Name) if err != nil { return err } - // Initial delay to let the game process the transition request - utils.Sleep(initialTransitionDelay) + // Wait for area sync before attempting any movement + utils.Sleep(500) ctx.RefreshGameData() + if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { + return err + } - // Wait for proper area transition - deadline := time.Now().Add(portalTransitionTimeout) - transitionStartTime := time.Now() - - for time.Now().Before(deadline) { - ctx.RefreshGameData() - currentArea := ctx.Data.PlayerUnit.Area - - ctx.Logger.Debug("Waiting for transition", - slog.String("current_area", currentArea.Area().Name), - slog.Duration("elapsed", time.Since(transitionStartTime))) - - if currentArea != fromArea { - if areaData, ok := ctx.Data.Areas[currentArea]; ok { - if areaData.IsInside(ctx.Data.PlayerUnit.Position) { - ctx.Logger.Debug("Successfully reached destination area") - utils.Sleep(300) // Extra wait to ensure everything is loaded - ctx.RefreshGameData() + // Ensure we're not in town + if ctx.Data.PlayerUnit.Area.IsTown() { + return fmt.Errorf("failed to leave town area") + } - // Perform item pickup after re-entering the portal - err = ItemPickup(40) - if err != nil { - ctx.Logger.Warn("Error during item pickup after portal use", "error", err) - } - return nil - } - } - } - utils.Sleep(portalSyncDelay) + // Perform item pickup after re-entering the portal + err = ItemPickup(40) + if err != nil { + ctx.Logger.Warn("Error during item pickup after portal use", "error", err) } - return fmt.Errorf("failed to verify area transition within timeout (start area: %s)", fromArea.Area().Name) + return nil } func UsePortalFrom(owner string) error { @@ -179,18 +110,14 @@ func UsePortalFrom(owner string) error { if obj.IsPortal() && obj.Owner == owner { return InteractObjectByID(obj.ID, func() bool { if !ctx.Data.PlayerUnit.Area.IsTown() { - // Initial delay to let the game process the transition - utils.Sleep(initialTransitionDelay) + // Ensure area data is synced after portal transition + utils.Sleep(500) ctx.RefreshGameData() - // Verify we're no longer in town - if ctx.Data.PlayerUnit.Area.IsTown() { + if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil { return false } - - if areaData, ok := ctx.Data.Areas[ctx.Data.PlayerUnit.Area]; ok { - return areaData.IsInside(ctx.Data.PlayerUnit.Position) - } + return true } return false }) From 3842e99bbbc7dd2880dde53d711f7a9642d8608b Mon Sep 17 00:00:00 2001 From: elobo91 Date: Sat, 16 Nov 2024 12:20:26 -0500 Subject: [PATCH 13/13] portals sync --- internal/action/step/interact_object.go | 54 +++++++++++++++++++------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/internal/action/step/interact_object.go b/internal/action/step/interact_object.go index 298c9010..798ff2b9 100644 --- a/internal/action/step/interact_object.go +++ b/internal/action/step/interact_object.go @@ -4,9 +4,11 @@ import ( "fmt" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/area" + "github.com/hectorgimenez/d2go/pkg/data/mode" "github.com/hectorgimenez/d2go/pkg/data/object" "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/game" + "github.com/hectorgimenez/koolo/internal/town" "github.com/hectorgimenez/koolo/internal/ui" "github.com/hectorgimenez/koolo/internal/utils" "time" @@ -44,11 +46,27 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { expectedArea = area.Tristram case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.RogueEncampment: expectedArea = area.MooMooFarm + case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.Harrogath: + expectedArea = area.NihlathaksTemple + case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.ArcaneSanctuary: + expectedArea = area.CanyonOfTheMagi + } + } else if obj.IsPortal() { + // For blue town portals, determine the town area based on current area + fromArea := ctx.Data.PlayerUnit.Area + if !fromArea.IsTown() { + expectedArea = town.GetTownByArea(fromArea).TownArea() + } else { + // When using portal from town, we need to wait for any non-town area + isCompletedFn = func() bool { + return !ctx.Data.PlayerUnit.Area.IsTown() && + ctx.Data.AreaData.IsInside(ctx.Data.PlayerUnit.Position) && + len(ctx.Data.Objects) > 0 + } } } for !isCompletedFn() { - // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() if interactionAttempts >= maxInteractionAttempts || mouseOverAttempts >= 20 { @@ -77,12 +95,28 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { } lastRun = time.Now() + + // Check portal states + if o.IsPortal() || o.IsRedPortal() { + // If portal is still being created, wait + if o.Mode == mode.ObjectModeOperating { + utils.Sleep(100) + continue + } + + // Only interact when portal is fully opened + if o.Mode != mode.ObjectModeOpened { + utils.Sleep(100) + continue + } + } + if o.IsHovered { ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) waitingForInteraction = true interactionAttempts++ - // For portals, we need to wait for proper area sync + // For portals with expected area, we need to wait for proper area sync if expectedArea != 0 { utils.Sleep(500) // Initial delay for area transition for attempts := 0; attempts < maxPortalSyncAttempts; attempts++ { @@ -90,13 +124,11 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { if ctx.Data.PlayerUnit.Area == expectedArea { if areaData, ok := ctx.Data.Areas[expectedArea]; ok { if areaData.IsInside(ctx.Data.PlayerUnit.Position) { + if expectedArea.IsTown() { + return nil // For town areas, we can return immediately + } // For special areas, ensure we have proper object data loaded - switch expectedArea { - case area.Tristram, area.MooMooFarm: - if len(ctx.Data.Objects) > 0 { - return nil - } - default: + if len(ctx.Data.Objects) > 0 { return nil } } @@ -104,9 +136,7 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { } utils.Sleep(portalSyncDelay) } - if !isCompletedFn() { - return fmt.Errorf("portal sync timeout - expected area: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area) - } + return fmt.Errorf("portal sync timeout - expected area: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area) } continue } else { @@ -119,7 +149,7 @@ func InteractObject(obj data.Object, isCompletedFn func() bool) error { mX, mY := ui.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 mouseOverAttempts == 2 && o.Name == object.TownPortal { + if mouseOverAttempts == 2 && o.IsPortal() { mX, mY = ui.GameCoordsToScreenCords(objectX-4, objectY-4) }