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