Clickable lights.
[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 BaseLight(object):
67     """ Common light functionality. """
68
69     COLOURS = {
70         "red": (255, 0, 0),
71         "green": (0, 255, 0),
72         "blue": (0, 255, 255),
73         "yellow": (255, 255, 0),
74         "white": (255, 255, 255),
75     }
76
77     def __init__(self, colour, position):
78         self.colour = colour
79         self.position = pymunk.Vec2d(position)
80         self.on = True
81         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
82         self.fitting = pymunk.Circle(self.body, 10.0, self.position)
83         self.body.light = self
84
85     @classmethod
86     def load(cls, config):
87         kw = config.copy()
88         light_type = kw.pop("type")
89         [light_class] = [
90             c for c in cls.__subclasses__()
91             if c.__name__.lower() == light_type]
92         return light_class(**kw)
93
94     def add(self, space):
95         if self.body.space is not None:
96             space.remove(self.body, *self.body.shapes)
97         shapes = self.shapes_for_ray_polys(
98             calculate_ray_polys(space, self.body, self.position))
99         for shape in shapes:
100             shape.filter = LIGHT_FILTER
101         self.fitting.filter = FITTINGS_FILTER
102         space.add(self.body, self.fitting, *shapes)
103
104     def shapes_for_ray_polys(self, ray_polys):
105         return ray_polys
106
107     def toggle(self):
108         self.on = not self.on
109
110     def render_light(self, surface):
111         if not self.on:
112             return
113         subsurface = surface.copy()
114         light_colour = self.COLOURS[self.colour]
115         for shape in self.body.shapes:
116             if shape is self.fitting:
117                 continue
118             pygame_poly = [
119                 pymunk.pygame_util.to_pygame(v, surface) for v in
120                 shape.get_vertices()]
121             pygame.draw.polygon(
122                 subsurface, light_colour, pygame_poly, 0)
123             pygame.draw.aalines(
124                 subsurface, light_colour, True, pygame_poly, 1)
125         subsurface.set_alpha(50)
126         surface.blit(subsurface, (0, 0), None)
127
128     def render_fittings(self, surface):
129         pygame.draw.circle(
130             surface, (255, 255, 0),
131             pymunk.pygame_util.to_pygame(self.fitting.offset, surface),
132             int(self.fitting.radius))
133
134
135 class SpotLight(BaseLight):
136     def __init__(
137             self, colour="white", position=None, direction=90.0, spread=45.0):
138         super(SpotLight, self).__init__(colour, position)
139         self.direction = direction
140         self.spread = spread
141         self.i = 0
142
143
144 class Lamp(BaseLight):
145     def __init__(self, colour="white", position=None, radius=100.0):
146         super(Lamp, self).__init__(colour, position)
147         self.radius = radius