Update angle in gamestate as well for passing to night
[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, direction,
82             spread):
83         self._body = body  # light's body
84         self._position = pymunk.Vec2d(position)  # light's position
85         self._ray_filter = ray_filter  # light filter
86         self._rays = []  # list of RayPolys
87         self._direction = None  # normal vector for direction
88         self._start = None  # normal vector in direction of start angle limit
89         self._end = None  # normal vector in direction of end angle limit
90         self._set_angle_limits(direction, spread)
91         if direction:
92             self.direction = direction  # Update direction
93         self._max_radius = None  # maximum radius in pixels
94         self._min_radius = None  # minimum radius in pixels
95         self._set_radius_limits(radius_limits)
96         self._old_poly_cache = None  # last polys added to the space
97         self._poly_cache = None  # list of pymunk.Polys for rays
98         self._space = None  # space the rays form part of
99
100     def set_space(self, space):
101         self._space = space
102         self._rays = calculate_ray_polys(
103             self._space, self._position, self._ray_filter)
104         self._poly_cache = None
105
106     def update_shapes(self):
107         if self._old_poly_cache:
108             self._space.remove(*self._old_poly_cache)
109         new_polys = self._old_poly_cache = self.polys()
110         self._space.add(*new_polys)
111
112     @property
113     def position(self):
114         return self._position
115
116     @property
117     def max_radius(self):
118         return self._max_radius
119
120     @max_radius.setter
121     def max_radius(self, value):
122         self._max_radius = value or 0.0
123
124     @property
125     def min_radius(self):
126         return self._min_radius
127
128     @min_radius.setter
129     def min_radius(self, value):
130         self._min_radius = value or 0.0
131
132     def reaches(self, position):
133         distance = self.position.get_distance(self.position)
134         return (self._min_radius <= distance <= self._max_radius)
135
136     def _set_radius_limits(self, radius_limits):
137         if radius_limits is None or not radius_limits[0]:
138             self._min_radius = 0
139         else:
140             self._min_radius = radius_limits[0]
141         if radius_limits is None or not radius_limits[1]:
142             self._max_radius = 50.0
143         else:
144             self._max_radius = radius_limits[1]
145
146     def rotatable(self):
147         return self._direction is not None
148
149     @property
150     def direction(self):
151         return self._direction.angle_degrees
152
153     @direction.setter
154     def direction(self, degrees):
155         spread = self._direction.get_angle_between(self._start)
156         self._direction.angle_degrees = degrees
157         self._start = self._direction.rotated(spread)
158         self._end = self._direction.rotated(-spread)
159         self._poly_cache = None
160
161     @property
162     def spread(self):
163         if not self._direction:
164             return 2 * math.pi
165         return math.fabs(self._start.get_angle_between(self._end))
166
167     def _set_angle_limits(self, direction, spread):
168         if direction is None or spread is None:
169             self._direction = None
170             self._start = None
171             self._end = None
172         else:
173             self._direction = pymunk.Vec2d(1, 0)
174             self._start = self._direction.rotated_degrees(-spread/2.)
175             self._end = self._direction.rotated_degrees(spread/2.)
176         self._poly_cache = None
177
178     def polys(self):
179         if self._poly_cache is None:
180             self._poly_cache = poly_cache = []
181             for rp in self._rays:
182                 poly = rp.poly(self._start, self._end)
183                 if poly:
184                     poly.body = self._body
185                     poly.filter = self._ray_filter
186                     poly_cache.append(poly)
187         return self._poly_cache
188
189     def pygame_position(self, surface):
190         return pymunk.pygame_util.to_pygame(self._position, surface)
191
192     def pygame_rect(self, surface):
193         half_width = self.max_radius
194         rect_width = half_width * 2
195         rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
196         dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
197         dest_rect.move_ip(-half_width, -half_width)
198         return dest_rect
199
200     def pygame_polys(self, surface):
201         return [
202             [pymunk.pygame_util.to_pygame(v, surface)
203              for v in poly.get_vertices()]
204             for poly in self.polys()
205         ]
206
207
208 class RayPoly(object):
209     def __init__(self, position, vertices):
210         self.position = position  # pointy end of the conical polygon
211         self.vertices = vertices  # all vertices in the polygon
212
213     def _between(self, v, start, end):
214         if start < end:
215             return start <= v <= end
216         return (start <= v) or (v <= end)
217
218     def poly(self, start, end):
219         trial = pymunk.Poly(None, self.vertices)
220         trial.update(pymunk.Transform.identity())
221
222         if start is None or end is None:
223             return trial  # no limits
224
225         start_info = trial.segment_query(
226             self.position + 1250 * start, self.position + 0.1 * start, 0)
227         end_info = trial.segment_query(
228             self.position + 1250 * end, self.position + 0.1 * end, 0)
229
230         vertices = self.vertices[:]
231         vertices = [
232             v for v in vertices
233             if self._between((v - self.position).angle, start.angle, end.angle)
234         ]
235         if start_info.shape is not None:
236             vertices.append(start_info.point)
237         if end_info.shape is not None:
238             vertices.append(end_info.point)
239         vertices.append(self.position)
240
241         poly = pymunk.Poly(None, vertices)
242         if len(poly.get_vertices()) < 3:
243             return None
244         return poly