Add back light radius checks.
[tabakrolletjie.git] / tabakrolletjie / rays.py
1 """ Light ray manipulation. Pew. Pew. Pew. Wommmm. """
2
3 import math
4
5 import pygame.rect
6
7 import pymunk
8 import pymunk.autogeometry
9 import pymunk.pygame_util
10
11 from .constants import SCREEN_SIZE
12 from .utils import debug_timer
13
14
15 def screen_rays(pos):
16     """ An iterable that returns ordered rays from pos to the edge of the
17         screen, starting with the edge point (0, 0) and continuing clockwise
18         in pymunk coordinates.
19     """
20     w, h = SCREEN_SIZE
21     left, right, bottom, top = 0, w, 0, h
22     step = 1
23     for y in range(0, h, step):
24         yield pymunk.Vec2d(left, y)
25     for x in range(0, w, step):
26         yield pymunk.Vec2d(x, top)
27     for y in range(top, -1, -step):
28         yield pymunk.Vec2d(right, y)
29     for x in range(right, -1, -step):
30         yield pymunk.Vec2d(x, bottom)
31
32
33 @debug_timer("rays.calculate_ray_polys")
34 def calculate_ray_polys(space, position, light_filter):
35     """ Calculate a set of convex RayPolys that cover all the areas that light
36         can reach from the given position, taking into account the obstacles
37         present in the space.
38     """
39     position = pymunk.Vec2d(position)
40     vertices = [position]
41     start, end = None, None
42     ray_polys = []
43     for ray in screen_rays(position):
44         info = space.segment_query_first(position, ray, 1, light_filter)
45         point = ray if info is None else info.point
46         vertices.append(point)
47         if len(vertices) == 2:
48             start = vertices[1]
49         elif len(vertices) > 3:
50             trial_poly = pymunk.Poly(None, vertices)
51             trial_poly.update(pymunk.Transform.identity())
52             query_prev = trial_poly.point_query(end)
53             query_pos = trial_poly.point_query(position)
54             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
55                 ray_polys.append(RayPoly(position, vertices[:-1]))
56                 start = vertices[-1]
57                 vertices = [position, start]
58             else:
59                 vertices = trial_poly.get_vertices()
60         end = point
61     if len(vertices) > 2:
62         ray_polys.append(RayPoly(position, vertices))
63     return ray_polys
64
65
66 def to_pymunk_radians(deg):
67     """ Convert degrees in [0, 360] to radians in (-pi, pi].
68
69         Return None if degrees is None.
70     """
71     if deg is None:
72         return None
73     deg = deg * math.pi / 180.0
74     if deg > math.pi:
75         deg -= 2 * math.pi
76     return deg
77
78
79 class RayPolyManager(object):
80     def __init__(
81             self, body, position, ray_filter, radius_limits, angle_limits):
82         self._body = body  # light's body
83         self._position = pymunk.Vec2d(position)  # light's position
84         self._ray_filter = ray_filter  # light filter
85         self._rays = []  # list of RayPolys
86         self._start = None  # normal vector in direction of start angle limit
87         self._end = None  # normal vector in direction of end angle limit
88         self._set_angle_limits(angle_limits)
89         self._max_radius = None  # maximum radius in pixels
90         self._min_radius = None  # minimum radius in pixels
91         self._set_radius_limits(radius_limits)
92         self._old_poly_cache = None  # last polys added to the space
93         self._poly_cache = None  # list of pymunk.Polys for rays
94         self._space = None  # space the rays form part of
95
96     def set_space(self, space):
97         self._space = space
98         self._rays = calculate_ray_polys(
99             self._space, self._position, self._ray_filter)
100         self._poly_cache = None
101
102     def update_shapes(self):
103         if self._old_poly_cache:
104             self._space.remove(*self._old_poly_cache)
105         new_polys = self._old_poly_cache = self.polys()
106         self._space.add(*new_polys)
107
108     @property
109     def position(self):
110         return self._position
111
112     @property
113     def max_radius(self):
114         return self._max_radius
115
116     @max_radius.setter
117     def max_radius_setter(self, value):
118         self._max_radius = value or 0.0
119
120     @property
121     def min_radius(self):
122         return self._min_radius
123
124     @min_radius.setter
125     def min_radius_setter(self, value):
126         self._min_radius = value or 0.0
127
128     def reaches(self, position):
129         distance = self.position.get_distance(self.position)
130         return (self._min_radius <= distance <= self._max_radius)
131
132     def _set_radius_limits(self, radius_limits):
133         if radius_limits is None or not radius_limits[0]:
134             self._min_radius = 0
135         else:
136             self._min_radius = radius_limits[0]
137         if radius_limits is None or not radius_limits[1]:
138             self._max_radius = 50.0
139         else:
140             self._max_radius = radius_limits[1]
141
142     def rotate_degrees(self, degrees):
143         self._start.rotate_degrees(degrees)
144         self._end.rotate_degrees(degrees)
145         self._poly_cache = None
146
147     def _set_angle_limits(self, angle_limits):
148         if angle_limits is None:
149             self._start = None
150             self._end = None
151         else:
152             self._start = pymunk.Vec2d(1, 0).rotated(
153                 to_pymunk_radians(angle_limits[0]))
154             self._end = pymunk.Vec2d(1, 0).rotated(
155                 to_pymunk_radians(angle_limits[1]))
156         self._poly_cache = None
157
158     def polys(self):
159         if self._poly_cache is None:
160             self._poly_cache = poly_cache = []
161             for rp in self._rays:
162                 poly = rp.poly(self._start, self._end)
163                 if poly:
164                     poly.body = self._body
165                     poly.filter = self._ray_filter
166                     poly_cache.append(poly)
167         return self._poly_cache
168
169     def pygame_position(self, surface):
170         return pymunk.pygame_util.to_pygame(self._position, surface)
171
172     def pygame_rect(self, surface):
173         half_width = self.max_radius
174         rect_width = half_width * 2
175         rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
176         dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
177         dest_rect.move_ip(-half_width, -half_width)
178         return dest_rect
179
180     def pygame_polys(self, surface):
181         return [
182             [pymunk.pygame_util.to_pygame(v, surface)
183              for v in poly.get_vertices()]
184             for poly in self.polys()
185         ]
186
187
188 class RayPoly(object):
189     def __init__(self, position, vertices):
190         self.position = position  # pointy end of the conical polygon
191         self.vertices = vertices  # all vertices in the polygon
192
193     def _between(self, v, start, end):
194         if start < end:
195             return start <= v <= end
196         return (start <= v) or (v <= end)
197
198     def poly(self, start, end):
199         trial = pymunk.Poly(None, self.vertices)
200         trial.update(pymunk.Transform.identity())
201
202         if start is None or end is None:
203             return trial  # no limits
204
205         start_info = trial.segment_query(
206             self.position + 1250 * start, self.position + 0.1 * start, 0)
207         end_info = trial.segment_query(
208             self.position + 1250 * end, self.position + 0.1 * end, 0)
209
210         vertices = self.vertices[:]
211         vertices = [
212             v for v in vertices
213             if self._between((v - self.position).angle, start.angle, end.angle)
214         ]
215         if start_info.shape is not None:
216             vertices.append(start_info.point)
217         if end_info.shape is not None:
218             vertices.append(end_info.point)
219         vertices.append(self.position)
220
221         poly = pymunk.Poly(None, vertices)
222         if len(poly.get_vertices()) < 3:
223             return None
224         return poly