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

clear_level.go optimization (ClearRoom) #598

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 80 additions & 26 deletions internal/action/clear_level.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package action

import (
"errors"
"fmt"
"sort"

"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/stat"
"github.com/hectorgimenez/koolo/internal/context"
"github.com/hectorgimenez/koolo/internal/game"
"github.com/hectorgimenez/koolo/internal/pather"
"github.com/hectorgimenez/koolo/internal/utils"
)

Expand All @@ -18,7 +19,7 @@ func ClearCurrentLevel(openChests bool, filter data.MonsterFilter) error {
rooms := ctx.PathFinder.OptimizeRoomsTraverseOrder()
for _, r := range rooms {
err := clearRoom(r, filter)
if err != nil {
if err != nil && err.Error() != "" {
ctx.Logger.Warn("Failed to clear room: %v", err)
}

Expand Down Expand Up @@ -52,49 +53,85 @@ func clearRoom(room data.Room, filter data.MonsterFilter) error {
ctx := context.Get()
ctx.SetLastAction("clearRoom")

path, _, found := ctx.PathFinder.GetClosestWalkablePath(room.GetCenter())
if !found {
return errors.New("failed to find a path to the room center")
if err := moveToRoomPosition(room); err != nil {
return err
}

to := data.Position{
X: path.To().X + ctx.Data.AreaOrigin.X,
Y: path.To().Y + ctx.Data.AreaOrigin.Y,
return clearRoomMonsters(room, filter)
}

func moveToRoomPosition(room data.Room) error {
ctx := context.Get()

center := room.GetCenter()

// Try center position first
if ctx.Data.AreaData.IsWalkable(center) {
if err := MoveToCoords(center); err == nil {
return nil
}
}
err := MoveToCoords(to)
if err != nil {
return fmt.Errorf("failed moving to room center: %w", err)

// For room clearing, use larger radius bounded by room size
maxRadius := min(room.Width/2, room.Height/2)
if walkablePoint, found := ctx.PathFinder.FindNearbyWalkablePosition(center, maxRadius); found {
if err := MoveToCoords(walkablePoint); err == nil {
return nil
}
}

return fmt.Errorf("") // No walkable position found in room but don't log it.
}

func clearRoomMonsters(room data.Room, filter data.MonsterFilter) error {
ctx := context.Get()

for {
monsters := getMonstersInRoom(room, filter)
if len(monsters) == 0 {
return nil
}

// Check if there are monsters that can summon new monsters, and kill them first
targetMonster := monsters[0]
for _, m := range monsters {
if m.IsMonsterRaiser() {
targetMonster = m
sort.Slice(monsters, func(i, j int) bool {
if monsters[i].IsMonsterRaiser() != monsters[j].IsMonsterRaiser() {
return monsters[i].IsMonsterRaiser()
}
distI := ctx.PathFinder.DistanceFromMe(monsters[i].Position)
distJ := ctx.PathFinder.DistanceFromMe(monsters[j].Position)
return distI < distJ
})

for _, monster := range monsters {
if !ctx.Data.AreaData.IsInside(monster.Position) {
continue
}
}

path, _, mPathFound := ctx.PathFinder.GetPath(targetMonster.Position)
if mPathFound {
// Check for door obstacles for non-teleporting characters
if !ctx.Data.CanTeleport() {
for _, o := range ctx.Data.Objects {
if o.IsDoor() && o.Selectable && path.Intersects(*ctx.Data, o.Position, 4) {
ctx.Logger.Debug("Door is blocking the path to the monster, moving closer")
MoveToCoords(targetMonster.Position)
path, _, found := ctx.PathFinder.GetPath(monster.Position)
if found {
for _, o := range ctx.Data.Objects {
if o.IsDoor() && o.Selectable && path.Intersects(*ctx.Data, o.Position, 4) {
ctx.Logger.Debug("Door is blocking the path to the monster, moving closer")
if err := MoveToCoords(monster.Position); err != nil {
continue
}
}
}
}
}

if !ctx.PathFinder.LineOfSight(ctx.Data.PlayerUnit.Position, monster.Position) {
// Let MoveToCoords handle the pathing (it uses GetPath)
if err := MoveToCoords(monster.Position); err != nil {
continue
}
}

ctx.Char.KillMonsterSequence(func(d game.Data) (data.UnitID, bool) {
m, found := d.Monsters.FindByID(targetMonster.UnitID)
m, found := d.Monsters.FindByID(monster.UnitID)
if found && m.Stats[stat.Life] > 0 {
return targetMonster.UnitID, true
return monster.UnitID, true
}
return 0, false
}, nil)
Expand All @@ -108,7 +145,24 @@ func getMonstersInRoom(room data.Room, filter data.MonsterFilter) []data.Monster

monstersInRoom := make([]data.Monster, 0)
for _, m := range ctx.Data.Monsters.Enemies(filter) {
if m.Stats[stat.Life] > 0 && room.IsInside(m.Position) || ctx.PathFinder.DistanceFromMe(m.Position) < 30 {
// Skip dead monsters or those outside current area
if m.Stats[stat.Life] <= 0 || !ctx.Data.AreaData.IsInside(m.Position) {
continue
}

inRoom := room.IsInside(m.Position)
nearPlayer := ctx.PathFinder.DistanceFromMe(m.Position) < 30

if !inRoom && !nearPlayer {
roomCenter := room.GetCenter()
distToRoom := pather.DistanceFromPoint(roomCenter, m.Position)
if distToRoom < room.Width/2+15 || distToRoom < room.Height/2+15 {
monstersInRoom = append(monstersInRoom, m)
continue
}
}

if inRoom || nearPlayer {
monstersInRoom = append(monstersInRoom, m)
}
}
Expand Down
77 changes: 32 additions & 45 deletions internal/pather/path_finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package pather

import (
"fmt"
"math"

"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/area"
Expand Down Expand Up @@ -33,8 +32,8 @@ func (pf *PathFinder) GetPath(to data.Position) (Path, int, bool) {
return path, distance, true
}

// If direct path fails, try to find nearby walkable position
if walkableTo, found := pf.findNearbyWalkablePosition(to); found {
// If direct path fails, try to find nearby walkable position with default radius for pathing
if walkableTo, found := pf.FindNearbyWalkablePosition(to, 0); found {
return pf.GetPathFrom(pf.data.PlayerUnit.Position, walkableTo)
}

Expand Down Expand Up @@ -160,58 +159,46 @@ func copyGrid(dest [][]game.CollisionType, src [][]game.CollisionType, offsetX,
}
}

func (pf *PathFinder) GetClosestWalkablePath(dest data.Position) (Path, int, bool) {
return pf.GetClosestWalkablePathFrom(pf.data.PlayerUnit.Position, dest)
}

func (pf *PathFinder) GetClosestWalkablePathFrom(from, dest data.Position) (Path, int, bool) {
a := pf.data.AreaData
if a.IsWalkable(dest) || !a.IsInside(dest) {
path, distance, found := pf.GetPath(dest)
if found {
return path, distance, found
}
}

maxRange := 20
step := 4
dst := 1

for dst < maxRange {
for i := -dst; i < dst; i += 1 {
for j := -dst; j < dst; j += 1 {
if math.Abs(float64(i)) >= math.Abs(float64(dst)) || math.Abs(float64(j)) >= math.Abs(float64(dst)) {
cgY := dest.Y - pf.data.AreaOrigin.Y + j
cgX := dest.X - pf.data.AreaOrigin.X + i
if cgX > 0 && cgY > 0 && a.Height > cgY && a.Width > cgX && a.CollisionGrid[cgY][cgX] == game.CollisionTypeWalkable {
return pf.GetPathFrom(from, data.Position{
X: dest.X + i,
Y: dest.Y + j,
})
}
}
}
}
dst += step
func (pf *PathFinder) FindNearbyWalkablePosition(target data.Position, radius int) (data.Position, bool) {
// Use default radius of 3 if none specified
searchRadius := 3
if radius > 0 {
searchRadius = radius
}

return nil, 0, false
}
// Convert target to grid coordinates
gridTarget := pf.data.AreaData.Grid.RelativePosition(target)

func (pf *PathFinder) findNearbyWalkablePosition(target data.Position) (data.Position, bool) {
// Search in expanding squares around the target position
for radius := 1; radius <= 3; radius++ {
for x := -radius; x <= radius; x++ {
for y := -radius; y <= radius; y++ {
for r := 1; r <= searchRadius; r++ {
for x := -r; x <= r; x++ {
for y := -r; y <= r; y++ {
if x == 0 && y == 0 {
continue
}
pos := data.Position{X: target.X + x, Y: target.Y + y}
if pf.data.AreaData.IsWalkable(pos) {
return pos, true

// Work in grid coordinates
gridPos := data.Position{
X: gridTarget.X + x,
Y: gridTarget.Y + y,
}

// Check boundaries and walkability in grid coordinates
if gridPos.X < 0 || gridPos.X >= pf.data.AreaData.Width ||
gridPos.Y < 0 || gridPos.Y >= pf.data.AreaData.Height ||
pf.data.AreaData.CollisionGrid[gridPos.Y][gridPos.X] == game.CollisionTypeNonWalkable {
continue
}

// Convert back to world coordinates before returning
worldPos := data.Position{
X: gridPos.X + pf.data.AreaData.OffsetX,
Y: gridPos.Y + pf.data.AreaData.OffsetY,
}
return worldPos, true
}
}
}

return data.Position{}, false
}