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