diff --git a/src/model/runelite_bot.py b/src/model/runelite_bot.py index 64281f2a..5ff0fa87 100644 --- a/src/model/runelite_bot.py +++ b/src/model/runelite_bot.py @@ -132,8 +132,8 @@ def pick_up_loot(self, items: Union[str, List[str]], supress_warning=True) -> bo # Locate Ground Items text if item_text := ocr.find_text(items, self.win.game_view, ocr.PLAIN_11, clr.PURPLE): for item in item_text: - item.set_rectangle_reference(self.win.game_view) - sorted_by_closest = sorted(item_text, key=Rectangle.distance_from_center) + item.set_parent_rectangle(self.win.game_view) + sorted_by_closest = sorted(item_text, key=Rectangle.distance_from_point) self.mouse.move_to(sorted_by_closest[0].get_center()) for _ in range(5): if self.mouseover_text(contains=["Take"] + items, color=[clr.OFF_WHITE, clr.OFF_ORANGE]): @@ -197,7 +197,7 @@ def get_nearest_tagged_NPC(self, include_in_combat: bool = False) -> RuneLiteObj print("No tagged NPCs found.") return None for obj in objs: - obj.set_rectangle_reference(self.win.game_view) + obj.set_parent_rectangle(self.win.game_view) # Sort shapes by distance from player objs = sorted(objs, key=RuneLiteObject.distance_from_rect_center) if include_in_combat: @@ -220,7 +220,7 @@ def get_all_tagged_in_rect(self, rect: Rectangle, color: clr.Color) -> List[Rune isolated_colors = clr.isolate_colors(img_rect, color) objs = rcv.extract_objects(isolated_colors) for obj in objs: - obj.set_rectangle_reference(rect) + obj.set_parent_rectangle(rect) return objs def get_nearest_tag(self, color: clr.Color) -> RuneLiteObject: diff --git a/src/utilities/geometry.py b/src/utilities/geometry.py index 00b6e52f..1e397872 100644 --- a/src/utilities/geometry.py +++ b/src/utilities/geometry.py @@ -1,11 +1,18 @@ import math +import os from typing import List, NamedTuple import cv2 import mss import numpy as np +if __name__ == "__main__": + import sys + + sys.path[0] = os.path.dirname(sys.path[0]) + import utilities.random_util as rd +import utilities.debug as debug Point = NamedTuple("Point", x=int, y=int) @@ -22,7 +29,7 @@ class Rectangle: """ subtract_list: List[dict] = [] - reference_rect = None + parent_rect = None def __init__(self, left: int, top: int, width: int, height: int): """ @@ -41,14 +48,53 @@ def __init__(self, left: int, top: int, width: int, height: int): self.width = width self.height = height - def set_rectangle_reference(self, rect): + def scale(self, scale_width: float = 1, scale_height: float = 1, anchor_x: float = 0.5, anchor_y: float = 0.5): + """ + Scales the rectangle by the given factors for width and height, and adjusts its position based on the anchor point. + Args: + scale_width: The scaling factor for the width of the rectangle (default 1). + scale_height: The scaling factor for the height of the rectangle (default 1). + anchor_x: The horizontal anchor point for scaling (default 0.5, which corresponds to the center). + anchor_y: The vertical anchor point for scaling (default 0.5, which corresponds to the center). + Returns: + The Rectangle object, after scaling. + Examples: + rect = Rectangle(left=10, top=10, width=100, height=100) + + # Scale the rectangle by a factor of 2, using the center as the anchor point (default behavior). + rect.scale(2, 2) + + # Scale the rectangle by a factor of 2, using the top-left corner as the anchor point. + rect.scale(2, 2, anchor_x=0, anchor_y=0) + + # Scale the rectangle by a factor of 2, using the bottom-right corner as the anchor point. + rect.scale(2, 2, anchor_x=1, anchor_y=1) + + # Scale the rectangle width by a factor of 1.5 and height by a factor of 2, using the top-right corner as the anchor point. + rect.scale(scale_width=1.5, scale_height=2, anchor_x=1, anchor_y=0) + """ + old_width = self.width + old_height = self.height + + new_width = int(self.width * scale_width) + new_height = int(self.height * scale_height) + + x_offset = int(old_width * (1 - scale_width) * anchor_x) + y_offset = int(old_height * (1 - scale_height) * anchor_y) + + new_left = self.left + x_offset + new_top = self.top + y_offset + + return Rectangle(new_left, new_top, new_width, new_height) + + def set_parent_rectangle(self, rect): """ Sets the rectangle reference of the object. Args: rect: A reference to the the rectangle that this object belongs in (E.g., Bot.win.game_view). """ - self.reference_rect = rect + self.parent_rect = rect @classmethod def from_points(cls, start_point: Point, end_point: Point): @@ -110,19 +156,28 @@ def get_center(self) -> Point: """ return Point(self.left + self.width // 2, self.top + self.height // 2) - # TODO: Consider changing to this to accept a Point to check against; `distance_from(point: Point)` - def distance_from_center(self) -> Point: + def distance_from_point(self, reference_point: Point = None) -> float: """ - Gets the distance between the object and it's Rectangle parent center. + Gets the distance between the object and the given reference point. Useful for sorting lists of Rectangles. + Args: + reference_point: A Point representing the reference point for distance calculation. + Default: The center of the parent rectangle, if available. Returns: The distance from the point to the center of the object. - """ - if self.reference_rect is None: - raise ReferenceError("A Rectangle being sorted is missing a reference to the Rectangle it's contained in and therefore cannot be sorted.") + Example: + >>> # Sort based on an arbitrary point + >>> arbitrary_point = Point(100, 200) + >>> sorted_by_arbitrary_point = sorted(some_rectangles, key=lambda rect: rect.distance_from_point(arbitrary_point)) + """ + if reference_point is None: + if self.parent_rect is not None: + reference_point = self.parent_rect.get_center() + else: + raise ValueError("A reference point must be provided if there is no parent rectangle.") + center: Point = self.get_center() - rect_center: Point = self.reference_rect.get_center() - return math.dist([center.x, center.y], [rect_center.x, rect_center.y]) + return math.dist([center.x, center.y], [reference_point.x, reference_point.y]) def get_top_left(self) -> Point: """ @@ -132,6 +187,22 @@ def get_top_left(self) -> Point: """ return Point(self.left, self.top) + def get_center_left(self) -> Point: + """ + Gets the center left point of the rectangle. + Returns: + A Point representing the center left of the rectangle. + """ + return Point(self.left, self.top + self.height // 2) + + def get_center_left(self) -> Point: + """ + Gets the center left point of the rectangle. + Returns: + A Point representing the center left of the rectangle. + """ + return Point(self.left, self.top + self.height // 2) + def get_top_right(self) -> Point: """ Gets the top right point of the rectangle. @@ -194,18 +265,77 @@ def __init__(self, x_min, x_max, y_min, y_max, width, height, center, axis): self._center = center self._axis = axis - def set_rectangle_reference(self, rect: Rectangle): + def scale(self, scale_width: float = 1, scale_height: float = 1, anchor_x: float = 0.5, anchor_y: float = 0.5): """ - Sets the rectangle reference of the object. + Scales the RuneLiteObject by the given factors for width and height, and adjusts its position based on the anchor point. + Args: + scale_width: The scaling factor for the width of the RuneLiteObject (default 1). + scale_height: The scaling factor for the height of the RuneLiteObject (default 1). + anchor_x: The horizontal anchor point for scaling (default 0.5, which corresponds to the center). + anchor_y: The vertical anchor point for scaling (default 0.5, which corresponds to the center). + Returns: + The RuneLiteObject, after scaling. + Examples: + obj = RuneLiteObject(x_min=10, x_max=110, y_min=10, y_max=110, width=100, height=100, center=(60, 60), axis=None) + + # Scale the object by a factor of 2, using the center as the anchor point (default behavior). + obj.scale(2, 2) + + # Scale the object by a factor of 2, using the top-left corner as the anchor point. + obj.scale(2, 2, anchor_x=0, anchor_y=0) + + # Scale the object by a factor of 2, using the bottom-right corner as the anchor point. + obj.scale(2, 2, anchor_x=1, anchor_y=1) + + # Scale the object width by a factor of 1.5 and height by a factor of 2, using the top-right corner as the anchor point. + obj.scale(scale_width=1.5, scale_height=2, anchor_x=1, anchor_y=0) + """ + newObject = self + old_width = self._width + old_height = self._height + + new_width = int(self._width * scale_width) + new_height = int(self._height * scale_height) + + x_offset = int(old_width * (1 - scale_width) * anchor_x) + y_offset = int(old_height * (1 - scale_height) * anchor_y) + + new_x_min = self._x_min + x_offset + new_x_max = new_x_min + new_width + new_y_min = self._y_min + y_offset + new_y_max = new_y_min + new_height + + new_center = (round((new_x_min + new_x_max) / 2), round((new_y_min + new_y_max) / 2)) + + # Generate all possible combinations of x and y coordinates inside the bounding box + x_coords = np.arange(new_x_min, new_x_max + 1) + y_coords = np.arange(new_y_min, new_y_max + 1) + xx, yy = np.meshgrid(x_coords, y_coords) + scaled_axis = np.column_stack((xx.ravel(), yy.ravel())) + + newObject._x_min = new_x_min + newObject._x_max = new_x_max + newObject._y_min = new_y_min + newObject._y_max = new_y_max + newObject._width = new_width + newObject._height = new_height + newObject._center = new_center + newObject._axis = scaled_axis + + return newObject + + def set_parent_rectangle(self, rect: Rectangle): + """ + Sets the parent rectangle of the object. Args: rect: A reference to the the rectangle that this object belongs in (E.g., Bot.win.game_view). """ self.rect = rect - def center(self) -> Point: # sourcery skip: raise-specific-error + def get_center(self) -> Point: """ - Gets the center of the object relative to the containing Rectangle. + Gets the center of the object relative to the screen. Returns: A Point. """ @@ -213,6 +343,20 @@ def center(self) -> Point: # sourcery skip: raise-specific-error raise ReferenceError("The RuneLiteObject is missing a reference to the Rectangle it's contained in and therefore the center cannot be determined.") return Point(self._center[0] + self.rect.left, self._center[1] + self.rect.top) + def distance_from_point(self, point: Point) -> float: + """ + Gets the distance between the object and the given point. + Args: + point: A tuple (x, y) representing the coordinates of the point. + Returns: + The distance from the point to the center of the object. + Example: + >>> reference_point = Point(300, 200) + >>> sorted_by_distance = sorted(rl_objects, key=lambda obj: obj.distance_from_point(reference_point)) + """ + center: Point = self.get_center() + return math.dist([center.x, center.y], [point.x, point.y]) + def distance_from_rect_center(self) -> float: """ Gets the distance between the object and it's Rectangle parent center. @@ -222,10 +366,49 @@ def distance_from_rect_center(self) -> float: Note: Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle. """ - center: Point = self.center() + center: Point = self.get_center() rect_center: Point = self.rect.get_center() return math.dist([center.x, center.y], [rect_center.x, rect_center.y]) + def distance_from_rect_left(self) -> float: + """ + Gets the distance between the object and it's Rectangle parent left edge. + Useful for sorting lists of RuneLiteObjects. + Returns: + The distance from the point to the center of the object. + Note: + Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle. + """ + center: Point = self.get_center() + rect_left: Point = self.rect.get_center_left() + return math.dist([center.x, center.y], [rect_left.x, rect_left.y]) + + def distance_from_top_left(self) -> float: + """ + Gets the distance between the object and it's Rectangle parent top left corner. + Useful for sorting lists of RuneLiteObjects. + Returns: + The distance from the point to the center of the object. + Note: + Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle. + """ + center: Point = self.get_center() + rect_left: Point = self.rect.get_top_left() + return math.dist([center.x, center.y], [rect_left.x, rect_left.y]) + + def distance_from_top_right(self) -> float: + """ + Gets the distance between the object and it's Rectangle parent top right corner. + Useful for sorting lists of RuneLiteObjects. + Returns: + The distance from the point to the center of the object. + Note: + Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle. + """ + center: Point = self.get_center() + rect_left: Point = self.rect.get_top_right() + return math.dist([center.x, center.y], [rect_left.x, rect_left.y]) + def random_point(self, custom_seeds: List[List[int]] = None) -> Point: """ Gets a random point within the object. @@ -239,7 +422,7 @@ def random_point(self, custom_seeds: List[List[int]] = None) -> Point: if custom_seeds is None: custom_seeds = rd.random_seeds(mod=(self._center[0] + self._center[1])) x, y = rd.random_point_in(self._x_min, self._y_min, self._width, self._height, custom_seeds) - return self.__relative_point([x, y]) if self.__point_exists([x, y]) else self.center() + return self.__relative_point([x, y]) if self.__point_exists([x, y]) else self.get_center() def __relative_point(self, point: List[int]) -> Point: """ @@ -258,3 +441,26 @@ def __point_exists(self, p: list) -> bool: p: The point to check in the format [x, y]. """ return (self._axis == np.array(p)).all(axis=1).any() + + +if __name__ == "__main__": + """ + Run this file directly to test this module. You must have an instance of RuneLite open for this to work. + """ + # Get/focus the RuneLite window currently running + win = debug.get_test_window() + + # Screenshot the chat box and display it + img = win.chat.screenshot() + cv2.imshow("Chat Box", img) + cv2.waitKey(0) + + # Screenshot control panel and display it + img = win.control_panel.screenshot() + cv2.imshow("Control Panel", img) + cv2.waitKey(0) + + # Screenshot game view and display it + img = win.game_view.screenshot() + cv2.imshow("Game View", img) + cv2.waitKey(0)