Merge branch 'master' of ctpug.org.za:tabakrolletjie
[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
24 def screen_rays(pos):
25     """ An iterable that returns ordered rays from pos to the edge of the
26         screen, starting with the edge point (0, 0) and continuing clockwise
27         in pymunk coordinates.
28     """
29     w, h = SCREEN_SIZE
30     left, right, bottom, top = 0, w, 0, h
31     step = 1
32     for y in range(0, h, step):
33         yield pymunk.Vec2d(left, y)
34     for x in range(0, w, step):
35         yield pymunk.Vec2d(x, top)
36     for y in range(top, -1, -step):
37         yield pymunk.Vec2d(right, y)
38     for x in range(right, -1, -step):
39         yield pymunk.Vec2d(x, bottom)
40
41
42 @debug_timer("lights.calculate_ray_polys")
43 def calculate_ray_polys(space, body, position):
44     position = pymunk.Vec2d(position)
45     vertices = [position]
46     ray_polys = []
47     for ray in screen_rays(position):
48         info = space.segment_query_first(position, ray, 1, LIGHT_FILTER)
49         point = ray if info is None else info.point
50         vertices.append(point)
51         if len(vertices) > 3:
52             trial_poly = pymunk.Poly(None, vertices)
53             trial_poly.update(pymunk.Transform.identity())
54             query_prev = trial_poly.point_query(vertices[-2])
55             query_pos = trial_poly.point_query(position)
56             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
57                 new_poly = pymunk.Poly(body, vertices[:-1])
58                 vertices = [position, vertices[-1]]
59                 ray_polys.append(new_poly)
60             else:
61                 vertices = trial_poly.get_vertices() + [point]
62     if len(vertices) > 2:
63         ray_polys.append(pymunk.Poly(body, vertices))
64     return ray_polys
65
66
67 class LightManager(object):
68     """ Manages a set of lights. """
69
70     def __init__(self, space, gamestate):
71         self._space = space
72         self._lights = [
73             BaseLight.load(cfg) for cfg in gamestate.station["lights"]]
74         for light in self._lights:
75             light.add(self._space)
76
77     def toggle_nearest(self, *args, **kw):
78         light = self.nearest(*args, **kw)
79         if light:
80             light.toggle()
81
82     def nearest(self, pos, surfpos=True, max_distance=1.0):
83         if surfpos:
84             surface = pygame.display.get_surface()
85             pos = pymunk.pygame_util.from_pygame(pos, surface)
86         point_info = self._space.point_query_nearest(
87             pos, 1.0, pymunk.ShapeFilter(mask=FITTINGS_CATEGORY))
88         if point_info is not None:
89             return point_info.shape.body.light
90         return None
91
92     def render_light(self, surface):
93         for light in self._lights:
94             light.render_light(surface)
95
96     def render_fittings(self, surface):
97         for light in self._lights:
98             light.render_fitting(surface)
99
100
101 class BaseLight(object):
102     """ Common light functionality. """
103
104     COLOURS = {
105         "red": (255, 0, 0),
106         "green": (0, 255, 0),
107         "blue": (0, 255, 255),
108         "yellow": (255, 255, 0),
109         "white": (255, 255, 255),
110     }
111
112     def __init__(self, colour, position):
113         self.colour = colour
114         self.position = pymunk.Vec2d(position)
115         self.on = True
116         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
117         self.fitting = pymunk.Circle(self.body, 10.0, self.position)
118         self.body.light = self
119
120     @classmethod
121     def load(cls, config):
122         kw = config.copy()
123         light_type = kw.pop("type")
124         [light_class] = [
125             c for c in cls.__subclasses__()
126             if c.__name__.lower() == light_type]
127         return light_class(**kw)
128
129     def add(self, space):
130         if self.body.space is not None:
131             space.remove(self.body, *self.body.shapes)
132         shapes = self.shapes_for_ray_polys(
133             calculate_ray_polys(space, self.body, self.position))
134         for shape in shapes:
135             shape.filter = LIGHT_FILTER
136         self.fitting.filter = FITTINGS_FILTER
137         space.add(self.body, self.fitting, *shapes)
138
139     def shapes_for_ray_polys(self, ray_polys):
140         return ray_polys
141
142     def toggle(self):
143         self.on = not self.on
144
145     def render_light(self, surface):
146         if not self.on:
147             return
148         subsurface = surface.copy()
149         light_colour = self.COLOURS[self.colour]
150         for shape in self.body.shapes:
151             if shape is self.fitting:
152                 continue
153             pygame_poly = [
154                 pymunk.pygame_util.to_pygame(v, surface) for v in
155                 shape.get_vertices()]
156             pygame.draw.polygon(
157                 subsurface, light_colour, pygame_poly, 0)
158             pygame.draw.aalines(
159                 subsurface, light_colour, True, pygame_poly, 1)
160         subsurface.set_alpha(50)
161         surface.blit(subsurface, (0, 0), None)
162
163     def render_fitting(self, surface):
164         pygame.draw.circle(
165             surface, (255, 255, 0),
166             pymunk.pygame_util.to_pygame(self.fitting.offset, surface),
167             int(self.fitting.radius))
168
169
170 class SpotLight(BaseLight):
171     def __init__(
172             self, colour="white", position=None, direction=90.0, spread=45.0):
173         super(SpotLight, self).__init__(colour, position)
174         self.direction = direction
175         self.spread = spread
176         self.i = 0
177
178
179 class Lamp(BaseLight):
180     def __init__(self, colour="white", position=None, radius=100.0):
181         super(Lamp, self).__init__(colour, position)
182         self.radius = radius