Add shape-based light query
[tabakrolletjie.git] / tabakrolletjie / lights.py
1 """ May it be a light for you in dark places, when all other lights go out.
2 """
3
4 import pymunk
5 import pymunk.pygame_util
6 import pygame.display
7 import pygame.draw
8
9 from .constants import (
10     SCREEN_SIZE, LIGHT_CATEGORY, FITTINGS_CATEGORY)
11 from .utils import debug_timer
12
13 LIGHT_FILTER = pymunk.ShapeFilter(
14     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
15         LIGHT_CATEGORY | FITTINGS_CATEGORY),
16     categories=LIGHT_CATEGORY)
17
18 FITTINGS_FILTER = pymunk.ShapeFilter(
19     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
20         LIGHT_CATEGORY | FITTINGS_CATEGORY),
21     categories=FITTINGS_CATEGORY)
22
23 # Just match lights, nothing else
24 LIT_BY_FILTER = pymunk.ShapeFilter(mask=LIGHT_CATEGORY)
25
26
27 def screen_rays(pos):
28     """ An iterable that returns ordered rays from pos to the edge of the
29         screen, starting with the edge point (0, 0) and continuing clockwise
30         in pymunk coordinates.
31     """
32     w, h = SCREEN_SIZE
33     left, right, bottom, top = 0, w, 0, h
34     step = 1
35     for y in range(0, h, step):
36         yield pymunk.Vec2d(left, y)
37     for x in range(0, w, step):
38         yield pymunk.Vec2d(x, top)
39     for y in range(top, -1, -step):
40         yield pymunk.Vec2d(right, y)
41     for x in range(right, -1, -step):
42         yield pymunk.Vec2d(x, bottom)
43
44
45 @debug_timer("lights.calculate_ray_polys")
46 def calculate_ray_polys(space, body, position):
47     position = pymunk.Vec2d(position)
48     vertices = [position]
49     ray_polys = []
50     for ray in screen_rays(position):
51         info = space.segment_query_first(position, ray, 1, LIGHT_FILTER)
52         point = ray if info is None else info.point
53         vertices.append(point)
54         if len(vertices) > 3:
55             trial_poly = pymunk.Poly(None, vertices)
56             trial_poly.update(pymunk.Transform.identity())
57             query_prev = trial_poly.point_query(vertices[-2])
58             query_pos = trial_poly.point_query(position)
59             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
60                 new_poly = pymunk.Poly(body, vertices[:-1])
61                 vertices = [position, vertices[-1]]
62                 ray_polys.append(new_poly)
63             else:
64                 vertices = trial_poly.get_vertices() + [point]
65     if len(vertices) > 2:
66         ray_polys.append(pymunk.Poly(body, vertices))
67     return ray_polys
68
69
70 class LightManager(object):
71     """ Manages a set of lights. """
72
73     def __init__(self, space, gamestate):
74         self._space = space
75         self._lights = [
76             BaseLight.load(cfg) for cfg in gamestate.station["lights"]]
77         for light in self._lights:
78             light.add(self._space)
79
80     def toggle_nearest(self, *args, **kw):
81         light = self.nearest(*args, **kw)
82         if light:
83             light.toggle()
84
85     def nearest(self, pos, surfpos=False, max_distance=1.0):
86         if surfpos:
87             surface = pygame.display.get_surface()
88             pos = pymunk.pygame_util.from_pygame(pos, surface)
89         point_info = self._space.point_query_nearest(
90             pos, max_distance, pymunk.ShapeFilter(mask=FITTINGS_CATEGORY))
91         if point_info is not None:
92             return point_info.shape.body.light
93         return None
94
95     def lit_by(self, pos, surfpos=False, max_distance=0.0):
96         if surfpos:
97             surface = pygame.display.get_surface()
98             pos = pymunk.pygame_util.from_pygame(pos, surface)
99         point_info_list = self._space.point_query(
100             pos, max_distance, pymunk.ShapeFilter(mask=LIGHT_CATEGORY))
101         lights = [p.shape.body.light for p in point_info_list]
102         return [light for light in lights if light.on]
103
104     def light_query(self, shape):
105         """Query the lights by shape"""
106         old_filter = shape.filter
107         # We need to restrict matches to only the lights
108         shape.filter = LIT_BY_FILTER
109         shape_info_list = self._space.shape_query(shape)
110         shape.filter = old_filter
111         lights = [p.shape.body.light for p in shape_info_list]
112         return [light for light in lights if light.on]
113
114     def render_light(self, surface):
115         for light in self._lights:
116             light.render_light(surface)
117
118     def render_fittings(self, surface):
119         for light in self._lights:
120             light.render_fitting(surface)
121
122
123 class BaseLight(object):
124     """ Common light functionality. """
125
126     COLOURS = {
127         "red": (255, 0, 0),
128         "green": (0, 255, 0),
129         "blue": (0, 255, 255),
130         "yellow": (255, 255, 0),
131         "white": (255, 255, 255),
132     }
133
134     def __init__(
135             self, colour, position, intensity=1.0,
136             radius_limits=(None, None), angle_limits=(None, None)):
137         self.colour = colour
138         self.position = pymunk.Vec2d(position)
139         self.on = True
140         self.intensity = intensity
141         self.radius_limits = radius_limits
142         self.angle_limits = angle_limits
143         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
144         self.fitting = pymunk.Circle(self.body, 10.0, self.position)
145         self.body.light = self
146
147     @classmethod
148     def load(cls, config):
149         kw = config.copy()
150         light_type = kw.pop("type")
151         [light_class] = [
152             c for c in cls.__subclasses__()
153             if c.__name__.lower() == light_type]
154         return light_class(**kw)
155
156     def add(self, space):
157         if self.body.space is not None:
158             space.remove(self.body, *self.body.shapes)
159         shapes = self.shapes_for_ray_polys(
160             calculate_ray_polys(space, self.body, self.position))
161         for shape in shapes:
162             shape.filter = LIGHT_FILTER
163         self.fitting.filter = FITTINGS_FILTER
164         space.add(self.body, self.fitting, *shapes)
165
166     def shapes_for_ray_polys(self, ray_polys):
167         return ray_polys
168
169     def toggle(self):
170         self.on = not self.on
171
172     def render_light(self, surface):
173         if not self.on:
174             return
175
176         raypoly_mask = surface.copy()
177         white, black = (255, 255, 255, 255), (0, 0, 0, 0)
178         raypoly_mask.fill(black)
179         for shape in self.body.shapes:
180             if shape is self.fitting:
181                 continue
182             pygame_poly = [
183                 pymunk.pygame_util.to_pygame(v, surface) for v in
184                 shape.get_vertices()]
185             pygame.draw.polygon(raypoly_mask, white, pygame_poly, 0)
186             pygame.draw.aalines(raypoly_mask, white, True, pygame_poly, 1)
187
188         limits_mask = surface.copy()
189         limits_mask.fill(black)
190         centre = pymunk.pygame_util.to_pygame(self.position, surface)
191         max_radius = self.radius_limits[1] or 50.0
192         box = (centre[0] - max_radius, centre[1] - max_radius,
193                max_radius * 2, max_radius * 2)
194         width = max_radius - (self.radius_limits[0] or 0)
195         box2 = (box[0] + 1,) + tuple(box[1:])
196         box3 = (box[0] + 2,) + tuple(box[1:])
197         import math
198         start_angle = (self.angle_limits[0] or 0.0) * (math.pi / 180.0)
199         end_angle = (self.angle_limits[1] or 360.0) * (math.pi / 180.0)
200         pygame.draw.arc(
201             limits_mask, white, box, start_angle, end_angle, int(width))
202         pygame.draw.arc(
203             limits_mask, white, box2, start_angle, end_angle, int(width))
204         pygame.draw.arc(
205             limits_mask, white, box3, start_angle, end_angle, int(width))
206
207         import pygame.locals as pgl
208         raypoly_mask.blit(limits_mask, (0, 0), None, pgl.BLEND_RGBA_MIN)
209         raypoly_mask.set_colorkey(black)
210
211         light_colour = self.COLOURS[self.colour]
212         overlay = surface.copy()
213         overlay.fill(light_colour)
214         raypoly_mask.blit(overlay, (0, 0), None, pgl.BLEND_RGBA_MULT)
215
216         mask2 = surface.copy()
217         mask2.set_alpha(255)
218         mask2.blit(raypoly_mask, (0, 0), None)
219
220         mask2.set_alpha(int(255 * self.intensity))
221
222         surface.blit(mask2, (0, 0), None)
223
224     def render_fitting(self, surface):
225         pygame.draw.circle(
226             surface, (255, 255, 0),
227             pymunk.pygame_util.to_pygame(self.fitting.offset, surface),
228             int(self.fitting.radius))
229
230
231 class SpotLight(BaseLight):
232     def __init__(self, **kw):
233         kw.pop("direction", None)
234         kw.pop("spread", None)
235         super(SpotLight, self).__init__(**kw)