diff --git a/awpy/data/__init__.py b/awpy/data/__init__.py new file mode 100644 index 000000000..93a0ce4d2 --- /dev/null +++ b/awpy/data/__init__.py @@ -0,0 +1 @@ +"""Module to hold Counter-Strike 2 data.""" diff --git a/awpy/data/map_data.py b/awpy/data/map_data.py new file mode 100644 index 000000000..6fcffbda6 --- /dev/null +++ b/awpy/data/map_data.py @@ -0,0 +1,126 @@ +"""Dictionary that holds map data for Counter-Strike 2.""" + +# pos_x is upper left world coordinate +MAP_DATA = { + "ar_baggage": { + "pos_x": -1316, + "pos_y": 1288, + "scale": 2.539062, + "rotate": 1, + "zoom": 1.3, + "selections": [ + {"name": "default", "altitude_max": 10000, "altitude_min": -5}, + {"name": "lower", "altitude_max": -5, "altitude_min": -10000}, + ], + }, + "ar_shoots": { + "pos_x": -1368, + "pos_y": 1952, + "scale": 2.687500, + "rotate": None, + "zoom": None, + "selections": [], + }, + "cs_office": { + "pos_x": -1838, + "pos_y": 1858, + "scale": 4.1, + "rotate": None, + "zoom": None, + "selections": [], + }, + "cs_italy": { + "pos_x": -2647, + "pos_y": 2592, + "scale": 4.6, + "rotate": 1, + "zoom": 1.5, + "selections": [], + }, + "de_ancient": { + "pos_x": -2953, + "pos_y": 2164, + "scale": 5, + "rotate": 0, + "zoom": 0, + "selections": [], + }, + "de_anubis": { + "pos_x": -2796, + "pos_y": 3328, + "scale": 5.22, + "rotate": None, + "zoom": None, + "selections": [], + }, + "de_dust": { + "pos_x": -2850, + "pos_y": 4073, + "scale": 6, + "rotate": 1, + "zoom": 1.3, + "selections": [], + }, + "de_dust2": { + "pos_x": -2476, + "pos_y": 3239, + "scale": 4.4, + "rotate": 1, + "zoom": 1.1, + "selections": [], + }, + "de_inferno": { + "pos_x": -2087, + "pos_y": 3870, + "scale": 4.9, + "rotate": None, + "zoom": None, + "selections": [], + }, + "de_inferno_s2": { + "pos_x": -2087, + "pos_y": 3870, + "scale": 4.9, + "rotate": None, + "zoom": None, + "selections": [], + }, + "de_mirage": { + "pos_x": -3230, + "pos_y": 1713, + "scale": 5, + "rotate": 0, + "zoom": 0, + "selections": [], + }, + "de_nuke": { + "pos_x": -3453, + "pos_y": 2887, + "scale": 7, + "rotate": None, + "zoom": None, + "selections": [ + {"name": "default", "altitude_max": 10000, "altitude_min": -495}, + {"name": "lower", "altitude_max": -495, "altitude_min": -10000}, + ], + }, + "de_overpass": { + "pos_x": -4831, + "pos_y": 1781, + "scale": 5.2, + "rotate": 0, + "zoom": 0, + "selections": [], + }, + "de_vertigo": { + "pos_x": -3168, + "pos_y": 1762, + "scale": 4, + "rotate": None, + "zoom": None, + "selections": [ + {"name": "default", "altitude_max": 20000, "altitude_min": 11700}, + {"name": "lower", "altitude_max": 11700, "altitude_min": -10000}, + ], + }, +} diff --git a/awpy/data/maps/__init__.py b/awpy/data/maps/__init__.py new file mode 100644 index 000000000..93f4ef7e0 --- /dev/null +++ b/awpy/data/maps/__init__.py @@ -0,0 +1 @@ +"""Contains map radar image files.""" diff --git a/awpy/data/maps/ar_baggage.png b/awpy/data/maps/ar_baggage.png new file mode 100644 index 000000000..a748a8aaa Binary files /dev/null and b/awpy/data/maps/ar_baggage.png differ diff --git a/awpy/data/maps/ar_baggage_lower.png b/awpy/data/maps/ar_baggage_lower.png new file mode 100644 index 000000000..6d444537f Binary files /dev/null and b/awpy/data/maps/ar_baggage_lower.png differ diff --git a/awpy/data/maps/ar_shoots.png b/awpy/data/maps/ar_shoots.png new file mode 100644 index 000000000..5ddaa6ce1 Binary files /dev/null and b/awpy/data/maps/ar_shoots.png differ diff --git a/awpy/data/maps/cs_italy.png b/awpy/data/maps/cs_italy.png new file mode 100644 index 000000000..7687e0b41 Binary files /dev/null and b/awpy/data/maps/cs_italy.png differ diff --git a/awpy/data/maps/cs_office.png b/awpy/data/maps/cs_office.png new file mode 100644 index 000000000..ce0686cbe Binary files /dev/null and b/awpy/data/maps/cs_office.png differ diff --git a/awpy/data/maps/de_ancient.png b/awpy/data/maps/de_ancient.png new file mode 100644 index 000000000..f0f9efd19 Binary files /dev/null and b/awpy/data/maps/de_ancient.png differ diff --git a/awpy/data/maps/de_anubis.png b/awpy/data/maps/de_anubis.png new file mode 100644 index 000000000..aa7ffd123 Binary files /dev/null and b/awpy/data/maps/de_anubis.png differ diff --git a/awpy/data/maps/de_dust2.png b/awpy/data/maps/de_dust2.png new file mode 100644 index 000000000..d30eb1312 Binary files /dev/null and b/awpy/data/maps/de_dust2.png differ diff --git a/awpy/data/maps/de_inferno.png b/awpy/data/maps/de_inferno.png new file mode 100644 index 000000000..2b2a703ca Binary files /dev/null and b/awpy/data/maps/de_inferno.png differ diff --git a/awpy/data/maps/de_mirage.png b/awpy/data/maps/de_mirage.png new file mode 100644 index 000000000..8ff8669a7 Binary files /dev/null and b/awpy/data/maps/de_mirage.png differ diff --git a/awpy/data/maps/de_nuke.png b/awpy/data/maps/de_nuke.png new file mode 100644 index 000000000..8cc5eac98 Binary files /dev/null and b/awpy/data/maps/de_nuke.png differ diff --git a/awpy/data/maps/de_nuke_lower.png b/awpy/data/maps/de_nuke_lower.png new file mode 100644 index 000000000..17767117b Binary files /dev/null and b/awpy/data/maps/de_nuke_lower.png differ diff --git a/awpy/data/maps/de_overpass.png b/awpy/data/maps/de_overpass.png new file mode 100644 index 000000000..e0c086a11 Binary files /dev/null and b/awpy/data/maps/de_overpass.png differ diff --git a/awpy/data/maps/de_vertigo.png b/awpy/data/maps/de_vertigo.png new file mode 100644 index 000000000..523ff623e Binary files /dev/null and b/awpy/data/maps/de_vertigo.png differ diff --git a/awpy/data/maps/de_vertigo_lower.png b/awpy/data/maps/de_vertigo_lower.png new file mode 100644 index 000000000..8057187fd Binary files /dev/null and b/awpy/data/maps/de_vertigo_lower.png differ diff --git a/awpy/viz/__init__.py b/awpy/viz/__init__.py new file mode 100644 index 000000000..79a30b796 --- /dev/null +++ b/awpy/viz/__init__.py @@ -0,0 +1,4 @@ +"""Awpy visualization module.""" + +SIDE_COLORS = {"ct": "#5d79ae", "t": "#de9b35"} +SUPPORTED_MAPS = ["de_dust2"] diff --git a/awpy/viz/plot.py b/awpy/viz/plot.py new file mode 100644 index 000000000..792b2535b --- /dev/null +++ b/awpy/viz/plot.py @@ -0,0 +1,36 @@ +"""Module for plotting Counter-Strike data.""" + +import importlib.resources + +import matplotlib.image as mpimg +import matplotlib.pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure + + +def plot_map(map_name: str, *, lower: bool = False) -> tuple[Figure, Axes]: + """Plot a Counter-Strike map. + + Args: + map_name (str): Name of the map to plot. + lower (bool, optional): Allows plotting the lower layer. Defaults to False. + + Raises: + FileNotFoundError: Raises a FileNotFoundError if the map image is not found. + + Returns: + tuple[Figure, Axes]: Matplotlib Figure and Axes objects. + """ + if lower is True: + map_name += "_lower" + + with importlib.resources.path("awpy.data.maps", f"{map_name}.png") as map_img_path: + if not map_img_path.exists(): + map_img_not_found_msg = f"Map image not found: {map_img_path}" + raise FileNotFoundError(map_img_not_found_msg) + + map_bg = mpimg.imread(map_img_path) + + figure, axes = plt.subplots() + axes.imshow(map_bg, zorder=0) + return figure, axes diff --git a/awpy/viz/utils.py b/awpy/viz/utils.py new file mode 100644 index 000000000..12e5e2b06 --- /dev/null +++ b/awpy/viz/utils.py @@ -0,0 +1,76 @@ +"""Utilities for plotting and visualization.""" + +from typing import Literal + +from awpy.data.map_data import MAP_DATA + + +# Position function courtesy of PureSkill.gg +def position_transform_axis( + map_name: str, position: float, axis: Literal["x", "y"] +) -> float: + """Transforms an X or Y-axis value. + + Args: + map_name (str): Map to search + position (float): X or Y coordinate + axis (str): Either "x" or "y" + + Returns: + float: Transformed position + + Raises: + ValueError: Raises a ValueError if axis not 'x' or 'y' + """ + axis = axis.lower() + if axis not in ["x", "y"]: + msg = f"'axis' has to be 'x' or 'y', not {axis}" + raise ValueError(msg) + start = MAP_DATA[map_name]["pos_" + axis] + scale = MAP_DATA[map_name]["scale"] + + if axis == "x": + return (position - start) / scale + return (start - position) / scale + + +def position_transform( + map_name: str, position: tuple[float, float, float] +) -> tuple[float, float, float]: + """Transforms an single coordinate (X,Y,Z). + + Args: + map_name (str): Map to transform coordinates. + position (tuple): (X,Y,Z) coordinates. + + Returns: + Tuple[float, float, float]: Transformed coordinates (X,Y,Z). + """ + return ( + position_transform_axis(map_name, position[0], "x"), + position_transform_axis(map_name, position[1], "y"), + position[2], + ) + + +def is_position_on_lower_level( + map_name: str, position: tuple[float, float, float] +) -> bool: + """Check if a position is on a lower level of a map. + + Args: + map_name (str): Map to check the position level. + position (Tuple[float, float, float]): (X,Y,Z) coordinates. + + Returns: + bool: True if the position on the lower level, False otherwise. + """ + metadata = MAP_DATA[map_name] + if len(metadata["selections"]) == 0: + return False + + for level in metadata["selections"]: + if position[2] > level["altitude_max"] and position[2] <= level["altitude_min"]: + return True + + return False diff --git a/pyproject.toml b/pyproject.toml index 706a52f54..617ed402a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ ] dependencies = [ "loguru>=0.7.2", + "matplotlib>=3.9.0", "numpy>=1.26.3", "pandas>=2.2.2", "setuptools>=69.2.0", @@ -40,11 +41,7 @@ include-package-data = true [tool.setuptools.package-data] "*" = [ - "data/map/*.png", - "data/map/*.json", - "data/nav/*.txt", - "data/nav/*.csv", - "data/nav/*.json", + "data/maps/*.png" ] [tool.ruff] diff --git a/tests/conftest.py b/tests/conftest.py index a65f2c204..35285b6a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -"""Global test configuration.""" +"""Awpy test configuration.""" import json import os @@ -22,12 +22,12 @@ def teardown(): # noqa: PT004, ANN201 """Cleans testing environment by deleting all .dem and .json files.""" yield for file in os.listdir(): - if file.endswith(".json"): + if file.endswith((".json", ".dem")): os.remove(file) def _get_demofile(demo_link: str, demo_name: str) -> None: - """Sends a request to get a demofile from MediaFire. + """Sends a request to get a demofile from the object storage. Args: demo_link (str): Link to demo.