Skip to content

Commit

Permalink
#650 merge
Browse files Browse the repository at this point in the history
Merge @xVoidByte pr with it.   Lets make them compatible.
  • Loading branch information
elobo91 committed Feb 25, 2025
1 parent 44128a7 commit 59e93a4
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 168 deletions.
2 changes: 2 additions & 0 deletions internal/action/clear_area.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func ClearAreaAroundPosition(pos data.Position, radius int, filter data.MonsterF
return 0, false
}, nil)
}

// TODO handle repath when stuck with hidden stash/ shrines. this is root of hidden stash stuck issue in chaos
func ClearThroughPath(pos data.Position, radius int, filter data.MonsterFilter) error {
ctx := context.Get()
lastMovement := false
Expand Down
10 changes: 9 additions & 1 deletion internal/action/move.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func ensureAreaSync(ctx *context.Status, expectedArea area.ID) error {
return fmt.Errorf("area sync timeout - expected: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area)
}

//TODO something is off with Diablo action.MoveToArea(area.ChaosSanctuary) , the transition between movetoarea and clearthrough path makes
// bot telestomp until timeout before resuming with the run

func MoveToArea(dst area.ID) error {
ctx := context.Get()
ctx.SetLastAction("MoveToArea")
Expand Down Expand Up @@ -238,6 +241,10 @@ func MoveTo(toFunc func() (data.Position, bool)) error {
return fmt.Errorf("path could not be calculated")
}

//TODO if character detects monster on other side of the wall and we are past door detection
// it will ignore doors and try to move directly to it. Either line of sight fail or constantly check for doors intersects

// Walkable logic
// Handle obstacles in current area
if !ctx.Data.CanTeleport() {
// Handle doors in path
Expand Down Expand Up @@ -282,7 +289,8 @@ func MoveTo(toFunc func() (data.Position, bool)) error {
}
}
// Continue moving
WaitForAllMembersWhenLeveling()

// WaitForAllMembersWhenLeveling is breaking walkable logic, lets not use it for now.

if lastMovement {
return nil
Expand Down
24 changes: 23 additions & 1 deletion internal/action/step/move.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,37 @@ func WithDistanceToFinish(distance int) MoveOption {
}
}

// TODO town pathing stucks?

func MoveTo(dest data.Position, options ...MoveOption) error {
ctx := context.Get()
ctx.SetLastStep("MoveTo")

//Todo if we add this it will make walkable character move like a broken disk. However is helping with waypoint interaction
// reducing walkduration seems to help with this issue.

/* defer func() {
for {
switch ctx.Data.PlayerUnit.Mode {
case mode.Walking, mode.WalkingInTown, mode.Running, mode.CastingSkill:
utils.Sleep(50)
// ctx.RefreshGameData()
continue
default:
return
}
}
}()*/

const (
refreshInterval = 200 * time.Millisecond
timeout = 30 * time.Second
)

startedAt := time.Now()

//TODO use the pathcache directly from path.go ?

// Initialize or reuse path cache
var pathCache *context.PathCache
if ctx.CurrentGame.PathCache != nil &&
Expand Down Expand Up @@ -63,7 +83,7 @@ func MoveTo(dest data.Position, options ...MoveOption) error {
}

// Add some delay between clicks to let the character move to destination
walkDuration := utils.RandomDurationMs(600, 1200)
walkDuration := utils.RandomDurationMs(700, 900)

for {
time.Sleep(50 * time.Millisecond)
Expand Down Expand Up @@ -93,6 +113,8 @@ func MoveTo(dest data.Position, options ...MoveOption) error {
} else if pathCache.Path == nil ||
!IsPathValid(currentPos, pathCache) ||
(distanceToDest <= 15 && distanceToDest > pathCache.DistanceToFinish) {
//TODO this looks like the telestomp issue, IsSamePosition is true but it never enter this condition, need something else to force refresh

// Only recalculate when truly needed
path, _, found := ctx.PathFinder.GetPath(dest)
if !found {
Expand Down
5 changes: 2 additions & 3 deletions internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,10 @@ type PathCache struct {
Path []data.Position
DestPosition data.Position
StartPosition data.Position
LastRun time.Time
LastCheck time.Time
PreviousPosition data.Position
LastCheck time.Time
LastRun time.Time
DistanceToFinish int
LastMoveDistance int
}

type CurrentGameHelper struct {
Expand Down
10 changes: 6 additions & 4 deletions internal/game/memory_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,22 @@ func (gd *MemoryReader) FetchMapData() error {
return fmt.Errorf("error fetching map data: %w", err)
}

areas := make(map[area.ID]AreaData)
areas := make(map[area.ID]AreaData, len(mapData)) // Pre-allocate map size
var mu sync.Mutex
g := errgroup.Group{}
for _, lvl := range mapData {
lvl := lvl // Capture local copy for go-routine
g.Go(func() error {
cg := lvl.CollisionGrid()
resultGrid := make([][]CollisionType, lvl.Size.Height)
for i := range resultGrid {
resultGrid[i] = make([]CollisionType, lvl.Size.Width)
}

for y := 0; y < lvl.Size.Height; y++ {
for x := 0; x < lvl.Size.Width; x++ {
if cg[y][x] {
// Optimized grid population using range iteration
for y, row := range cg {
for x, val := range row {
if val {
resultGrid[y][x] = CollisionTypeWalkable
} else {
resultGrid[y][x] = CollisionTypeNonWalkable
Expand Down
197 changes: 126 additions & 71 deletions internal/pather/astar/astar.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,27 @@ import (
"github.com/hectorgimenez/koolo/internal/game"
)

var directions = []data.Position{
{0, 1}, // Down
{1, 0}, // Right
{0, -1}, // Up
{-1, 0}, // Left
{1, 1}, // Down-Right (Southeast)
{-1, 1}, // Down-Left (Southwest)
{1, -1}, // Up-Right (Northeast)
{-1, -1}, // Up-Left (Northwest)
}
var (
// Cardinal directions for movement in narrow spaces
cardinalDirections = []data.Position{
{0, 1}, // Down
{1, 0}, // Right
{0, -1}, // Up
{-1, 0}, // Left
}

// All possible movement directions including diagonals
allDirections = []data.Position{
{0, 1}, // Down
{1, 0}, // Right
{0, -1}, // Up
{-1, 0}, // Left
{1, 1}, // Down-Right (Southeast)
{-1, 1}, // Down-Left (Southwest)
{1, -1}, // Up-Right (Northeast)
{-1, -1}, // Up-Left (Northwest)
}
)

type Node struct {
data.Position
Expand All @@ -27,16 +38,12 @@ type Node struct {
Index int
}

func direction(from, to data.Position) (dx, dy int) {
dx = to.X - from.X
dy = to.Y - from.Y
return
}

func CalculatePath(g *game.Grid, area area.ID, start, goal data.Position) ([]data.Position, int, bool) {
// Find the shortest path between two points using A* algorithm with optimizations for specific game areas
func CalculatePath(g *game.Grid, areaID area.ID, start, goal data.Position, teleport bool) ([]data.Position, int, bool) {
pq := make(PriorityQueue, 0)
heap.Init(&pq)

// Use a 2D slice to store the cost of each node
costSoFar := make([][]int, g.Width)
cameFrom := make([][]data.Position, g.Width)
for i := range costSoFar {
Expand All @@ -52,41 +59,36 @@ func CalculatePath(g *game.Grid, area area.ID, start, goal data.Position) ([]dat
costSoFar[start.X][start.Y] = 0

neighbors := make([]data.Position, 0, 8)
nodesExplored := 0

// Use appropriate directions based on map type
directions := allDirections
if IsNarrowMap(areaID) {
// Restrict to cardinal directions for narrow maps to prevent pathing issues
directions = cardinalDirections
}

for pq.Len() > 0 {
current := heap.Pop(&pq).(*Node)
nodesExplored++

// Early exit if we reach the goal
if current.Position == goal {
var path []data.Position
for p := goal; p != start; p = cameFrom[p.X][p.Y] {
path = append([]data.Position{p}, path...)
}
path = append([]data.Position{start}, path...)
return path, len(path), true
// Validate and smooth path before returning
return validateAndSmoothPath(reconstructPath(cameFrom, start, goal)), nodesExplored, true
}

updateNeighbors(g, current, &neighbors)

updateNeighbors(g, current, directions, &neighbors, teleport)
for _, neighbor := range neighbors {

var tileCost int
tileType := g.CollisionGrid[neighbor.Y][neighbor.X]

if area.IsTown() {
// Extra cost near edges in town
if neighbor.X <= 2 || neighbor.X >= g.Width-2 ||
neighbor.Y <= 2 || neighbor.Y >= g.Height-2 {
tileCost += 20
}
} else {
tileCost = getCost(tileType, area)
tileCost := getCost(g.CollisionGrid[neighbor.Y][neighbor.X], teleport)
if tileCost == math.MaxInt32 {
continue // Skip completely blocked tiles
}

newCost := costSoFar[current.X][current.Y] + tileCost

if newCost < costSoFar[neighbor.X][neighbor.Y] {
costSoFar[neighbor.X][neighbor.Y] = newCost
priority := newCost + int(0.5*float64(heuristic(neighbor, goal)))
priority := newCost + heuristic(neighbor, goal)
heap.Push(&pq, &Node{Position: neighbor, Cost: newCost, Priority: priority})
cameFrom[neighbor.X][neighbor.Y] = current.Position
}
Expand All @@ -96,53 +98,106 @@ func CalculatePath(g *game.Grid, area area.ID, start, goal data.Position) ([]dat
return nil, 0, false
}

// Get walkable neighbors of a given node
func updateNeighbors(grid *game.Grid, node *Node, neighbors *[]data.Position) {
*neighbors = (*neighbors)[:0]
// Builds the final path from cameFrom logic
func reconstructPath(cameFrom [][]data.Position, start, goal data.Position) []data.Position {
var path []data.Position
for p := goal; p != start; p = cameFrom[p.X][p.Y] {
path = append([]data.Position{p}, path...)
}
return append([]data.Position{start}, path...)
}

// validateAndSmoothPath ensures teleport paths skip unwalkable middle nodes
func validateAndSmoothPath(path []data.Position) []data.Position {
if len(path) < 3 {
return path
}

smoothed := make([]data.Position, 0, len(path))
smoothed = append(smoothed, path[0])

// Remove unnecessary intermediate nodes that can be skipped
for i := 1; i < len(path)-1; i++ {
if canSkipNode(path[i-1], path[i+1]) {
continue
}
smoothed = append(smoothed, path[i])
}

smoothed = append(smoothed, path[len(path)-1])
return smoothed
}

// canSkipNode checks if two nodes are adjacent enough to skip intermediate nodes
func canSkipNode(prev, next data.Position) bool {
dx := abs(next.X - prev.X)
dy := abs(next.Y - prev.Y)
return dx <= 1 && dy <= 1
}

// Find valid adjacent nodes considering collision detection
func updateNeighbors(grid *game.Grid, node *Node, directions []data.Position, neighbors *[]data.Position, teleport bool) {
*neighbors = (*neighbors)[:0]
x, y := node.X, node.Y
gridWidth, gridHeight := grid.Width, grid.Height

// Check all possible directions for valid neighbors
for _, d := range directions {
newX, newY := x+d.X, y+d.Y
// Check if the new neighbor is within grid bounds
if newX >= 0 && newX < gridWidth && newY >= 0 && newY < gridHeight {
if newX >= 0 && newX < grid.Width && newY >= 0 && newY < grid.Height {
tileType := grid.CollisionGrid[newY][newX]
// Include non-walkable nodes when teleporting
if !teleport && tileType == game.CollisionTypeNonWalkable {
continue // Skip non-walkable tiles when not teleporting
}
*neighbors = append(*neighbors, data.Position{X: newX, Y: newY})
}
}
}

func getCost(tileType game.CollisionType, currentArea area.ID) int {
if currentArea.IsTown() {
// Define movement cost for different collision types with teleport consideration
var tileCost = map[game.CollisionType]int{
game.CollisionTypeWalkable: 1, // Walkable
game.CollisionTypeMonster: 16, // Monster blocking penalty
game.CollisionTypeObject: 4, // Soft blocker (barrels, etc)
game.CollisionTypeLowPriority: 20, // Preferred walkable areas
game.CollisionTypeNonWalkable: math.MaxInt32, // Completely block non-walkable
}

// getCost returns movement cost for tile type with teleport adjustments
func getCost(tileType game.CollisionType, teleport bool) int {
if teleport {
switch tileType {
case game.CollisionTypeWalkable:
return 1
case game.CollisionTypeMonster:
return 30 // Higher cost in town
case game.CollisionTypeNonWalkable:
return 20 // Reduced cost for teleport through walls
case game.CollisionTypeObject:
return 15 // Higher cost in town
case game.CollisionTypeLowPriority:
return 40 // Much higher in town
default:
return math.MaxInt32
return 10 // Lower penalty for objects when teleporting
}
}
return tileCost[tileType]
}

switch tileType {
case game.CollisionTypeWalkable:
return 1
case game.CollisionTypeMonster:
return 16
case game.CollisionTypeObject:
return 4
case game.CollisionTypeLowPriority:
return 20
default:
return math.MaxInt32
// Use heuristic distance for faster calculations
func heuristic(a, b data.Position) int {
dx := abs(a.X - b.X)
dy := abs(a.Y - b.Y)
return dx + dy
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func heuristic(a, b data.Position) int {
dx := math.Abs(float64(a.X - b.X))
dy := math.Abs(float64(a.Y - b.Y))
return int(dx + dy + (math.Sqrt(2)-2)*math.Min(dx, dy))

// Identify areas that require restricted movement directions to prevent pathfinding issues in tight spaces
func IsNarrowMap(a area.ID) bool {
switch a {
case area.TowerCellarLevel2, area.TowerCellarLevel3, area.TowerCellarLevel4,
area.MaggotLairLevel1, area.MaggotLairLevel2, area.MaggotLairLevel3,
area.ArcaneSanctuary, area.ClawViperTempleLevel2, area.RiverOfFlame,
area.ChaosSanctuary:
return true
}
return false
}
Loading

0 comments on commit 59e93a4

Please sign in to comment.