Initial project structure: reusable isometric bot engine with D2R implementation
This commit is contained in:
commit
e0282a7111
44 changed files with 3433 additions and 0 deletions
0
games/__init__.py
Normal file
0
games/__init__.py
Normal file
0
games/d2r/__init__.py
Normal file
0
games/d2r/__init__.py
Normal file
83
games/d2r/config.py
Normal file
83
games/d2r/config.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"""Diablo II: Resurrected configuration.
|
||||
|
||||
Game-specific settings for screen regions, colors, and timings.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class D2RScreenRegions:
|
||||
"""Screen regions for UI elements at 1920x1080."""
|
||||
|
||||
health_orb: Tuple[int, int, int, int] = (28, 545, 170, 170)
|
||||
mana_orb: Tuple[int, int, int, int] = (1722, 545, 170, 170)
|
||||
xp_bar: Tuple[int, int, int, int] = (0, 1058, 1920, 22)
|
||||
belt: Tuple[int, int, int, int] = (838, 1010, 244, 48)
|
||||
minimap: Tuple[int, int, int, int] = (1600, 0, 320, 320)
|
||||
inventory: Tuple[int, int, int, int] = (960, 330, 530, 440)
|
||||
stash: Tuple[int, int, int, int] = (430, 330, 530, 440)
|
||||
chat: Tuple[int, int, int, int] = (0, 800, 500, 200)
|
||||
skill_left: Tuple[int, int, int, int] = (194, 1036, 52, 52)
|
||||
skill_right: Tuple[int, int, int, int] = (1674, 1036, 52, 52)
|
||||
|
||||
|
||||
@dataclass
|
||||
class D2RColors:
|
||||
"""HSV color ranges for game element detection."""
|
||||
|
||||
health_filled: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
|
||||
(0, 100, 100), (10, 255, 255) # Red
|
||||
)
|
||||
mana_filled: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
|
||||
(100, 100, 100), (130, 255, 255) # Blue
|
||||
)
|
||||
item_unique: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
|
||||
(15, 100, 180), (30, 255, 255) # Gold/unique
|
||||
)
|
||||
item_set: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
|
||||
(35, 100, 150), (55, 255, 255) # Green/set
|
||||
)
|
||||
item_rare: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
|
||||
(15, 50, 200), (25, 150, 255) # Yellow/rare
|
||||
)
|
||||
portal_blue: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
|
||||
(90, 150, 150), (120, 255, 255) # Town portal blue
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class D2RTimings:
|
||||
"""Game-specific timing constants in seconds."""
|
||||
|
||||
loading_screen_max: float = 15.0
|
||||
town_portal_cast: float = 3.5
|
||||
teleport_delay: float = 0.15
|
||||
potion_cooldown: float = 1.0
|
||||
click_delay: float = 0.1
|
||||
pickup_delay: float = 0.3
|
||||
vendor_interaction: float = 0.5
|
||||
|
||||
|
||||
@dataclass
|
||||
class D2RConfig:
|
||||
"""Master D2R configuration."""
|
||||
|
||||
resolution: Tuple[int, int] = (1920, 1080)
|
||||
regions: D2RScreenRegions = field(default_factory=D2RScreenRegions)
|
||||
colors: D2RColors = field(default_factory=D2RColors)
|
||||
timings: D2RTimings = field(default_factory=D2RTimings)
|
||||
|
||||
# Loot filter
|
||||
pickup_uniques: bool = True
|
||||
pickup_sets: bool = True
|
||||
pickup_rares: bool = True
|
||||
pickup_runes: bool = True
|
||||
min_rune_tier: int = 10 # Lem+
|
||||
pickup_gems: bool = False
|
||||
|
||||
# Safety
|
||||
health_potion_threshold: float = 0.5
|
||||
mana_potion_threshold: float = 0.3
|
||||
chicken_threshold: float = 0.2 # Exit game if health below this
|
||||
97
games/d2r/game.py
Normal file
97
games/d2r/game.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""Main D2R bot class — orchestrates the bot loop.
|
||||
|
||||
Entry point for the Diablo II: Resurrected bot. Manages the main
|
||||
game loop, state transitions, and routine execution.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from engine.screen.capture import ScreenCapture
|
||||
from engine.input.humanize import Humanizer
|
||||
from engine.state.events import EventBus
|
||||
from engine.safety.timing import SessionTimer
|
||||
from games.d2r.config import D2RConfig
|
||||
from games.d2r.screens.ingame import InGameDetector
|
||||
from games.d2r.screens.menu import MenuDetector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class D2RBot:
|
||||
"""Main Diablo II: Resurrected bot."""
|
||||
|
||||
def __init__(self, config: Optional[D2RConfig] = None):
|
||||
self.config = config or D2RConfig()
|
||||
self.screen = ScreenCapture()
|
||||
self.humanizer = Humanizer()
|
||||
self.events = EventBus()
|
||||
self.session_timer = SessionTimer()
|
||||
self.menu_detector = MenuDetector(self.config)
|
||||
self.ingame_detector = InGameDetector(self.config)
|
||||
|
||||
self._running = False
|
||||
self._current_routine = None
|
||||
|
||||
def start(self, routine_name: str = "mephisto") -> None:
|
||||
"""Start the bot with a specific farming routine."""
|
||||
logger.info(f"Starting D2R bot with routine: {routine_name}")
|
||||
self._running = True
|
||||
|
||||
try:
|
||||
self._main_loop(routine_name)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot stopped by user")
|
||||
except Exception as e:
|
||||
logger.error(f"Bot error: {e}", exc_info=True)
|
||||
finally:
|
||||
self._running = False
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Signal the bot to stop."""
|
||||
self._running = False
|
||||
|
||||
def _main_loop(self, routine_name: str) -> None:
|
||||
"""Core bot loop."""
|
||||
while self._running:
|
||||
# Check session timing
|
||||
if self.session_timer.should_stop_session():
|
||||
break_duration = self.session_timer.get_break_duration()
|
||||
logger.info(f"Session break: {break_duration/60:.0f} min")
|
||||
time.sleep(break_duration)
|
||||
self.session_timer.start_new_session()
|
||||
|
||||
# Check for breaks
|
||||
break_time = self.humanizer.should_take_break()
|
||||
if break_time:
|
||||
time.sleep(break_time)
|
||||
|
||||
# Capture screen
|
||||
frame = self.screen.capture_screen()
|
||||
|
||||
# Detect state and act
|
||||
# TODO: Implement state detection and routine execution
|
||||
|
||||
# Small delay between loop iterations
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="D2R Bot")
|
||||
parser.add_argument("--routine", default="mephisto", choices=["mephisto", "pindle", "countess"])
|
||||
parser.add_argument("--resolution", default="1920x1080")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s")
|
||||
|
||||
config = D2RConfig()
|
||||
bot = D2RBot(config)
|
||||
bot.start(args.routine)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
games/d2r/routines/__init__.py
Normal file
0
games/d2r/routines/__init__.py
Normal file
84
games/d2r/routines/countess.py
Normal file
84
games/d2r/routines/countess.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""Countess farming routine for D2R.
|
||||
|
||||
Rune farming: Create game → Black Marsh WP → Tower Cellar →
|
||||
Kill Countess → Loot runes → Exit → Repeat
|
||||
"""
|
||||
|
||||
import logging
|
||||
from enum import Enum, auto
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CountessPhase(Enum):
|
||||
CREATE_GAME = auto()
|
||||
WAYPOINT_TO_MARSH = auto()
|
||||
FIND_TOWER = auto()
|
||||
NAVIGATE_CELLAR = auto()
|
||||
KILL_COUNTESS = auto()
|
||||
LOOT = auto()
|
||||
EXIT_GAME = auto()
|
||||
|
||||
|
||||
class CountessRoutine:
|
||||
"""Automated Countess farming for rune drops.
|
||||
|
||||
Best route for mid-tier rune farming (up to Ist).
|
||||
Requires navigating 5 tower cellar levels.
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.phase = CountessPhase.CREATE_GAME
|
||||
self.run_count = 0
|
||||
|
||||
def execute_run(self) -> bool:
|
||||
"""Execute a single Countess run."""
|
||||
logger.info(f"Starting Countess run #{self.run_count + 1}")
|
||||
|
||||
phases = [
|
||||
(CountessPhase.CREATE_GAME, self._create_game),
|
||||
(CountessPhase.WAYPOINT_TO_MARSH, self._go_to_marsh),
|
||||
(CountessPhase.FIND_TOWER, self._find_tower),
|
||||
(CountessPhase.NAVIGATE_CELLAR, self._navigate_cellar),
|
||||
(CountessPhase.KILL_COUNTESS, self._kill_countess),
|
||||
(CountessPhase.LOOT, self._loot_runes),
|
||||
(CountessPhase.EXIT_GAME, self._exit_game),
|
||||
]
|
||||
|
||||
for phase, handler in phases:
|
||||
self.phase = phase
|
||||
if not handler():
|
||||
return False
|
||||
self.bot.humanizer.wait()
|
||||
|
||||
self.run_count += 1
|
||||
return True
|
||||
|
||||
def _create_game(self) -> bool:
|
||||
return True
|
||||
|
||||
def _go_to_marsh(self) -> bool:
|
||||
"""Take waypoint to Black Marsh."""
|
||||
return True
|
||||
|
||||
def _find_tower(self) -> bool:
|
||||
"""Navigate from Black Marsh to Forgotten Tower entrance."""
|
||||
# TODO: This is the hardest part — tower location is random
|
||||
return True
|
||||
|
||||
def _navigate_cellar(self) -> bool:
|
||||
"""Navigate through 5 cellar levels to level 5."""
|
||||
# TODO: Find stairs on each level, descend
|
||||
return True
|
||||
|
||||
def _kill_countess(self) -> bool:
|
||||
"""Kill the Countess."""
|
||||
return True
|
||||
|
||||
def _loot_runes(self) -> bool:
|
||||
"""Pick up rune drops (Countess has special rune drop table)."""
|
||||
return True
|
||||
|
||||
def _exit_game(self) -> bool:
|
||||
return True
|
||||
105
games/d2r/routines/mephisto.py
Normal file
105
games/d2r/routines/mephisto.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""Mephisto farming routine for D2R.
|
||||
|
||||
Classic Mephisto run: Create game → Teleport to Durance 3 →
|
||||
Kill Mephisto → Loot → Exit → Repeat
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from enum import Enum, auto
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MephistoPhase(Enum):
|
||||
CREATE_GAME = auto()
|
||||
TELEPORT_TO_DURANCE = auto()
|
||||
FIND_MEPHISTO = auto()
|
||||
KILL_MEPHISTO = auto()
|
||||
LOOT = auto()
|
||||
TOWN_PORTAL = auto()
|
||||
STASH_ITEMS = auto()
|
||||
EXIT_GAME = auto()
|
||||
|
||||
|
||||
class MephistoRoutine:
|
||||
"""Automated Mephisto farming runs.
|
||||
|
||||
Designed for Sorceress with Teleport. Can be adapted for
|
||||
other classes with Enigma runeword.
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.phase = MephistoPhase.CREATE_GAME
|
||||
self.run_count = 0
|
||||
self.items_found = 0
|
||||
|
||||
def execute_run(self) -> bool:
|
||||
"""Execute a single Mephisto run. Returns True if successful."""
|
||||
logger.info(f"Starting Mephisto run #{self.run_count + 1}")
|
||||
|
||||
phases = [
|
||||
(MephistoPhase.CREATE_GAME, self._create_game),
|
||||
(MephistoPhase.TELEPORT_TO_DURANCE, self._teleport_to_durance),
|
||||
(MephistoPhase.FIND_MEPHISTO, self._find_mephisto),
|
||||
(MephistoPhase.KILL_MEPHISTO, self._kill_mephisto),
|
||||
(MephistoPhase.LOOT, self._loot_items),
|
||||
(MephistoPhase.TOWN_PORTAL, self._town_portal),
|
||||
(MephistoPhase.STASH_ITEMS, self._stash_items),
|
||||
(MephistoPhase.EXIT_GAME, self._exit_game),
|
||||
]
|
||||
|
||||
for phase, handler in phases:
|
||||
self.phase = phase
|
||||
logger.debug(f"Phase: {phase.name}")
|
||||
|
||||
if not handler():
|
||||
logger.warning(f"Phase {phase.name} failed")
|
||||
return False
|
||||
|
||||
self.bot.humanizer.wait()
|
||||
|
||||
self.run_count += 1
|
||||
logger.info(f"Run #{self.run_count} complete. Total items: {self.items_found}")
|
||||
return True
|
||||
|
||||
def _create_game(self) -> bool:
|
||||
"""Create a new game."""
|
||||
# TODO: Navigate menu → create game with random name
|
||||
return True
|
||||
|
||||
def _teleport_to_durance(self) -> bool:
|
||||
"""Teleport from Act 3 town to Durance of Hate Level 3."""
|
||||
# TODO: Navigate waypoint → Durance 2 → teleport to Durance 3
|
||||
return True
|
||||
|
||||
def _find_mephisto(self) -> bool:
|
||||
"""Locate Mephisto on Durance 3."""
|
||||
# TODO: Teleport around to find Mephisto (moat trick position)
|
||||
return True
|
||||
|
||||
def _kill_mephisto(self) -> bool:
|
||||
"""Kill Mephisto using appropriate skill rotation."""
|
||||
# TODO: Position at moat trick spot, cast spells
|
||||
return True
|
||||
|
||||
def _loot_items(self) -> bool:
|
||||
"""Pick up valuable items."""
|
||||
# TODO: Detect and pick up items based on loot filter
|
||||
return True
|
||||
|
||||
def _town_portal(self) -> bool:
|
||||
"""Cast town portal and go to town."""
|
||||
# TODO: Cast TP, click portal
|
||||
return True
|
||||
|
||||
def _stash_items(self) -> bool:
|
||||
"""Stash items if inventory is getting full."""
|
||||
# TODO: Open stash, transfer items
|
||||
return True
|
||||
|
||||
def _exit_game(self) -> bool:
|
||||
"""Exit the current game."""
|
||||
# TODO: Save & Exit
|
||||
return True
|
||||
80
games/d2r/routines/pindle.py
Normal file
80
games/d2r/routines/pindle.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""Pindleskin farming routine for D2R.
|
||||
|
||||
Fastest MF run: Create game → Take red portal in Harrogath →
|
||||
Kill Pindleskin → Loot → Exit → Repeat
|
||||
"""
|
||||
|
||||
import logging
|
||||
from enum import Enum, auto
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PindlePhase(Enum):
|
||||
CREATE_GAME = auto()
|
||||
TAKE_PORTAL = auto()
|
||||
FIND_PINDLE = auto()
|
||||
KILL_PINDLE = auto()
|
||||
LOOT = auto()
|
||||
EXIT_GAME = auto()
|
||||
|
||||
|
||||
class PindleRoutine:
|
||||
"""Automated Pindleskin farming runs.
|
||||
|
||||
The simplest and fastest MF route. Requires Act 5 red portal
|
||||
(Anya quest completed). Works with any class.
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.phase = PindlePhase.CREATE_GAME
|
||||
self.run_count = 0
|
||||
|
||||
def execute_run(self) -> bool:
|
||||
"""Execute a single Pindleskin run."""
|
||||
logger.info(f"Starting Pindle run #{self.run_count + 1}")
|
||||
|
||||
phases = [
|
||||
(PindlePhase.CREATE_GAME, self._create_game),
|
||||
(PindlePhase.TAKE_PORTAL, self._take_red_portal),
|
||||
(PindlePhase.FIND_PINDLE, self._find_pindle),
|
||||
(PindlePhase.KILL_PINDLE, self._kill_pindle),
|
||||
(PindlePhase.LOOT, self._loot_items),
|
||||
(PindlePhase.EXIT_GAME, self._exit_game),
|
||||
]
|
||||
|
||||
for phase, handler in phases:
|
||||
self.phase = phase
|
||||
if not handler():
|
||||
return False
|
||||
self.bot.humanizer.wait()
|
||||
|
||||
self.run_count += 1
|
||||
return True
|
||||
|
||||
def _create_game(self) -> bool:
|
||||
return True
|
||||
|
||||
def _take_red_portal(self) -> bool:
|
||||
"""Navigate to and enter the red portal near Anya."""
|
||||
# TODO: Find red portal in Harrogath, click it
|
||||
return True
|
||||
|
||||
def _find_pindle(self) -> bool:
|
||||
"""Locate Pindleskin in Nihlathak's Temple entrance."""
|
||||
# TODO: Move toward Pindle's fixed spawn location
|
||||
return True
|
||||
|
||||
def _kill_pindle(self) -> bool:
|
||||
"""Kill Pindleskin and his minions."""
|
||||
# TODO: Attack routine
|
||||
return True
|
||||
|
||||
def _loot_items(self) -> bool:
|
||||
"""Pick up valuable drops."""
|
||||
return True
|
||||
|
||||
def _exit_game(self) -> bool:
|
||||
"""Exit game."""
|
||||
return True
|
||||
0
games/d2r/screens/__init__.py
Normal file
0
games/d2r/screens/__init__.py
Normal file
89
games/d2r/screens/ingame.py
Normal file
89
games/d2r/screens/ingame.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""In-game state detection for D2R.
|
||||
|
||||
Detects health/mana, location, enemies, items on ground, etc.
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Tuple
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from engine.vision.color import ColorAnalyzer
|
||||
from engine.vision.detector import ElementDetector, Detection
|
||||
from games.d2r.config import D2RConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InGameDetector:
|
||||
"""Detects in-game state from screen captures."""
|
||||
|
||||
def __init__(self, config: D2RConfig):
|
||||
self.config = config
|
||||
self.color = ColorAnalyzer()
|
||||
self.detector = ElementDetector()
|
||||
|
||||
def is_in_game(self, screen: np.ndarray) -> bool:
|
||||
"""Check if we're in an active game (health/mana orbs visible)."""
|
||||
health = self.get_health_percentage(screen)
|
||||
return health > 0
|
||||
|
||||
def get_health_percentage(self, screen: np.ndarray) -> float:
|
||||
"""Read current health from the health orb."""
|
||||
return self.color.read_bar_percentage(
|
||||
screen,
|
||||
self.config.regions.health_orb,
|
||||
self.config.colors.health_filled,
|
||||
)
|
||||
|
||||
def get_mana_percentage(self, screen: np.ndarray) -> float:
|
||||
"""Read current mana from the mana orb."""
|
||||
return self.color.read_bar_percentage(
|
||||
screen,
|
||||
self.config.regions.mana_orb,
|
||||
self.config.colors.mana_filled,
|
||||
)
|
||||
|
||||
def is_dead(self, screen: np.ndarray) -> bool:
|
||||
"""Check if character is dead."""
|
||||
# Health at 0 + death screen elements
|
||||
return self.get_health_percentage(screen) == 0
|
||||
|
||||
def should_use_health_potion(self, screen: np.ndarray) -> bool:
|
||||
"""Check if health is below potion threshold."""
|
||||
return self.get_health_percentage(screen) < self.config.health_potion_threshold
|
||||
|
||||
def should_chicken(self, screen: np.ndarray) -> bool:
|
||||
"""Check if health is critically low (exit game)."""
|
||||
return self.get_health_percentage(screen) < self.config.chicken_threshold
|
||||
|
||||
def find_items_on_ground(self, screen: np.ndarray) -> List[Detection]:
|
||||
"""Detect item labels on the ground."""
|
||||
items = []
|
||||
|
||||
if self.config.pickup_uniques:
|
||||
items.extend(self.detector.find_by_color(
|
||||
screen, *self.config.colors.item_unique,
|
||||
min_area=50, label="unique",
|
||||
))
|
||||
|
||||
if self.config.pickup_sets:
|
||||
items.extend(self.detector.find_by_color(
|
||||
screen, *self.config.colors.item_set,
|
||||
min_area=50, label="set",
|
||||
))
|
||||
|
||||
return items
|
||||
|
||||
def find_portal(self, screen: np.ndarray) -> Optional[Detection]:
|
||||
"""Detect a town portal on screen."""
|
||||
portals = self.detector.find_by_color(
|
||||
screen, *self.config.colors.portal_blue,
|
||||
min_area=200, label="portal",
|
||||
)
|
||||
return portals[0] if portals else None
|
||||
|
||||
def is_inventory_open(self, screen: np.ndarray) -> bool:
|
||||
"""Check if the inventory panel is open."""
|
||||
# TODO: Template match inventory panel
|
||||
return False
|
||||
40
games/d2r/screens/inventory.py
Normal file
40
games/d2r/screens/inventory.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""Inventory management for D2R.
|
||||
|
||||
Handles inventory scanning, item identification, stash management.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from engine.vision.detector import ElementDetector, Detection
|
||||
from games.d2r.config import D2RConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InventoryManager:
|
||||
"""Manages inventory state and item operations."""
|
||||
|
||||
def __init__(self, config: D2RConfig):
|
||||
self.config = config
|
||||
self.detector = ElementDetector()
|
||||
|
||||
def is_full(self, screen: np.ndarray) -> bool:
|
||||
"""Check if inventory is full."""
|
||||
# TODO: Scan inventory grid for empty slots
|
||||
return False
|
||||
|
||||
def find_empty_slot(self, screen: np.ndarray) -> Optional[Tuple[int, int]]:
|
||||
"""Find an empty inventory slot."""
|
||||
# TODO: Grid scanning
|
||||
return None
|
||||
|
||||
def count_items(self, screen: np.ndarray) -> int:
|
||||
"""Count items in inventory."""
|
||||
return 0
|
||||
|
||||
def should_go_to_town(self, screen: np.ndarray) -> bool:
|
||||
"""Check if inventory is full enough to warrant a town trip."""
|
||||
return self.is_full(screen)
|
||||
45
games/d2r/screens/menu.py
Normal file
45
games/d2r/screens/menu.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"""Main menu and character select screen detection for D2R."""
|
||||
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from engine.vision.detector import ElementDetector, Detection
|
||||
from games.d2r.config import D2RConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MenuDetector:
|
||||
"""Detects D2R menu screens (main menu, character select, lobby)."""
|
||||
|
||||
def __init__(self, config: D2RConfig):
|
||||
self.config = config
|
||||
self.detector = ElementDetector()
|
||||
|
||||
def is_main_menu(self, screen: np.ndarray) -> bool:
|
||||
"""Check if we're on the main menu."""
|
||||
# TODO: Template match for main menu elements
|
||||
return False
|
||||
|
||||
def is_character_select(self, screen: np.ndarray) -> bool:
|
||||
"""Check if we're on character select screen."""
|
||||
return False
|
||||
|
||||
def is_lobby(self, screen: np.ndarray) -> bool:
|
||||
"""Check if we're in the game lobby."""
|
||||
return False
|
||||
|
||||
def is_loading(self, screen: np.ndarray) -> bool:
|
||||
"""Check if a loading screen is active."""
|
||||
return False
|
||||
|
||||
def select_character(self, screen: np.ndarray, char_index: int = 0) -> Optional[Detection]:
|
||||
"""Find and return the character slot to click."""
|
||||
# TODO: Detect character slots
|
||||
return None
|
||||
|
||||
def find_create_game_button(self, screen: np.ndarray) -> Optional[Detection]:
|
||||
"""Find the create game button."""
|
||||
return None
|
||||
0
games/d2r/templates/.gitkeep
Normal file
0
games/d2r/templates/.gitkeep
Normal file
Loading…
Add table
Add a link
Reference in a new issue