Initial project structure: reusable isometric bot engine with D2R implementation

This commit is contained in:
Hoid 2026-02-14 08:50:36 +00:00
commit e0282a7111
44 changed files with 3433 additions and 0 deletions

View file

View file

@ -0,0 +1,46 @@
"""Character movement control for isometric games.
Handles click-to-move navigation with human-like patterns.
"""
from typing import Tuple, Optional
import logging
import time
import numpy as np
from engine.input.mouse import MouseController
from engine.input.humanize import Humanizer
from engine.navigation.pathfinder import Waypoint, WaypointGraph
logger = logging.getLogger(__name__)
class MovementController:
"""Controls character movement via click-to-move."""
def __init__(self, mouse: MouseController, humanizer: Humanizer):
self.mouse = mouse
self.humanizer = humanizer
self.waypoints = WaypointGraph()
def click_to_move(self, x: int, y: int) -> None:
"""Click a screen position to move there."""
jx, jy = self.humanizer.jitter_position(x, y)
self.mouse.move_to(jx, jy)
self.humanizer.wait()
self.mouse.click()
def navigate_waypoints(self, start: str, goal: str) -> bool:
"""Navigate between named waypoints."""
path = self.waypoints.find_path(start, goal)
if not path:
logger.warning(f"No path from {start} to {goal}")
return False
for waypoint in path[1:]: # Skip start
self.click_to_move(waypoint.screen_x, waypoint.screen_y)
# Wait for movement (game-specific timing)
time.sleep(self.humanizer.reaction_delay() + 0.5)
return True

View file

@ -0,0 +1,78 @@
"""Pathfinding for isometric game navigation.
Implements A* and click-to-move navigation for isometric games
where the bot needs to move between known locations.
"""
from typing import List, Tuple, Optional, Dict
from dataclasses import dataclass
import heapq
import math
import logging
logger = logging.getLogger(__name__)
@dataclass
class Waypoint:
"""A named location in the game world."""
name: str
screen_x: int
screen_y: int
metadata: Dict = None
class WaypointGraph:
"""Graph of connected waypoints for navigation."""
def __init__(self):
self._waypoints: Dict[str, Waypoint] = {}
self._edges: Dict[str, List[str]] = {}
def add_waypoint(self, waypoint: Waypoint) -> None:
self._waypoints[waypoint.name] = waypoint
self._edges.setdefault(waypoint.name, [])
def connect(self, name_a: str, name_b: str, bidirectional: bool = True) -> None:
self._edges.setdefault(name_a, []).append(name_b)
if bidirectional:
self._edges.setdefault(name_b, []).append(name_a)
def find_path(self, start: str, goal: str) -> Optional[List[Waypoint]]:
"""A* pathfinding between waypoints."""
if start not in self._waypoints or goal not in self._waypoints:
return None
goal_wp = self._waypoints[goal]
def heuristic(name: str) -> float:
wp = self._waypoints[name]
return math.hypot(wp.screen_x - goal_wp.screen_x, wp.screen_y - goal_wp.screen_y)
open_set = [(heuristic(start), 0, start)]
came_from: Dict[str, str] = {}
g_score: Dict[str, float] = {start: 0}
while open_set:
_, cost, current = heapq.heappop(open_set)
if current == goal:
path = []
while current in came_from:
path.append(self._waypoints[current])
current = came_from[current]
path.append(self._waypoints[start])
return list(reversed(path))
for neighbor in self._edges.get(current, []):
n_wp = self._waypoints[neighbor]
c_wp = self._waypoints[current]
edge_cost = math.hypot(n_wp.screen_x - c_wp.screen_x, n_wp.screen_y - c_wp.screen_y)
tentative_g = g_score[current] + edge_cost
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
heapq.heappush(open_set, (tentative_g + heuristic(neighbor), tentative_g, neighbor))
return None