from __future__ import annotations import math # Clockwise from east, matching the y-down world convention used throughout (see camera.py). _EIGHT_WAY_DIRECTIONS: tuple[tuple[int, int], ...] = ( (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), ) def snap_to_8way(dx: float, dy: float) -> tuple[int, int]: """Snaps a free direction to the nearest of the 8 compass directions. The mouse can point anywhere ("no fixed angles" while aiming), but combat/interaction still resolves against the tile grid, so this is where continuous aim meets discrete tile logic. """ if dx == 0.0 and dy == 0.0: return 0, 1 angle = math.atan2(dy, dx) index = round(angle / (math.pi / 4)) % 8 return _EIGHT_WAY_DIRECTIONS[index] def apply_cone_deviation(dx: int, dy: int, cone_degrees: float, rng) -> tuple[int, int]: """Randomly deviates a direction within +/- cone_degrees/2, snapped back to the grid. This is where "the exact direction of the shot is randomized" within the aim cone happens - the caller (e.g. perform_shoot) supplies the cone's size (from skill + weapon mods). """ if cone_degrees <= 0: return dx, dy base_angle = math.atan2(dy, dx) deviation = math.radians(rng.uniform(-cone_degrees / 2, cone_degrees / 2)) angle = base_angle + deviation return snap_to_8way(math.cos(angle), math.sin(angle))