Avoid surface.copy for the cases where we blow away the contents
[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     def __init__(
136             self, colour, position, intensity=1.0,
137             radius_limits=(None, None), angle_limits=(None, None)):
138         self.colour = colour
139         self.position = pymunk.Vec2d(position)
140         self.on = True
141         self.intensity = intensity
142         self.radius_limits = radius_limits
143         self.angle_limits = angle_limits
144         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
145         self.fitting = pymunk.Circle(self.body, 10.0, self.position)
146         self.body.light = self
147
148     @classmethod
149     def load(cls, config):
150         kw = config.copy()
151         light_type = kw.pop("type")
152         [light_class] = [
153             c for c in cls.__subclasses__()
154             if c.__name__.lower() == light_type]
155         return light_class(**kw)
156
157     def add(self, space):
158         if self.body.space is not None:
159             space.remove(self.body, *self.body.shapes)
160         shapes = self.shapes_for_ray_polys(
161             calculate_ray_polys(space, self.body, self.position))
162         for shape in shapes:
163             shape.filter = LIGHT_FILTER
164         self.fitting.filter = FITTINGS_FILTER
165         space.add(self.body, self.fitting, *shapes)
166
167     def shapes_for_ray_polys(self, ray_polys):
168         return ray_polys
169
170     def toggle(self):
171         self.on = not self.on
172
173     def render_light(self, surface):
174         if not self.on:
175             return
176
177         raypoly_mask = pygame.surface.Surface(surface.get_size(), pgl.SWSURFACE)
178         white, black = (255, 255, 255, 255), (0, 0, 0, 0)
179         raypoly_mask.fill(black)
180         for shape in self.body.shapes:
181             if shape is self.fitting:
182                 continue
183             pygame_poly = [
184                 pymunk.pygame_util.to_pygame(v, surface) for v in
185                 shape.get_vertices()]
186             pygame.draw.polygon(raypoly_mask, white, pygame_poly, 0)
187             pygame.draw.aalines(raypoly_mask, white, True, pygame_poly, 1)
188
189         limits_mask = pygame.surface.Surface(surface.get_size(), pgl.SWSURFACE)
190         limits_mask.fill(black)
191         centre = pymunk.pygame_util.to_pygame(self.position, surface)
192         max_radius = self.radius_limits[1] or 50.0
193         box = (centre[0] - max_radius, centre[1] - max_radius,
194                max_radius * 2, max_radius * 2)
195         width = max_radius - (self.radius_limits[0] or 0)
196         box2 = (box[0] + 1,) + tuple(box[1:])
197         box3 = (box[0] + 2,) + tuple(box[1:])
198         import math
199         start_angle = (self.angle_limits[0] or 0.0) * (math.pi / 180.0)
200         end_angle = (self.angle_limits[1] or 360.0) * (math.pi / 180.0)
201         pygame.draw.arc(
202             limits_mask, white, box, start_angle, end_angle, int(width))
203         pygame.draw.arc(
204             limits_mask, white, box2, start_angle, end_angle, int(width))
205         pygame.draw.arc(
206             limits_mask, white, box3, start_angle, end_angle, int(width))
207
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 = pygame.surface.Surface(surface.get_size(), pgl.SWSURFACE)
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)