Add function that returns whether a position is lit.
[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=False, 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, max_distance, 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 lit_by(self, pos, surfpos=False, max_distance=0.0):
93         if surfpos:
94             surface = pygame.display.get_surface()
95             pos = pymunk.pygame_util.from_pygame(pos, surface)
96         point_info_list = self._space.point_query(
97             pos, max_distance, pymunk.ShapeFilter(mask=LIGHT_CATEGORY))
98         lights = [p.shape.body.light for p in point_info_list]
99         return [light for light in lights if light.on]
100
101     def render_light(self, surface):
102         for light in self._lights:
103             light.render_light(surface)
104
105     def render_fittings(self, surface):
106         for light in self._lights:
107             light.render_fitting(surface)
108
109
110 class BaseLight(object):
111     """ Common light functionality. """
112
113     COLOURS = {
114         "red": (255, 0, 0),
115         "green": (0, 255, 0),
116         "blue": (0, 255, 255),
117         "yellow": (255, 255, 0),
118         "white": (255, 255, 255),
119     }
120
121     def __init__(self, colour, position):
122         self.colour = colour
123         self.position = pymunk.Vec2d(position)
124         self.on = True
125         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
126         self.fitting = pymunk.Circle(self.body, 10.0, self.position)
127         self.body.light = self
128
129     @classmethod
130     def load(cls, config):
131         kw = config.copy()
132         light_type = kw.pop("type")
133         [light_class] = [
134             c for c in cls.__subclasses__()
135             if c.__name__.lower() == light_type]
136         return light_class(**kw)
137
138     def add(self, space):
139         if self.body.space is not None:
140             space.remove(self.body, *self.body.shapes)
141         shapes = self.shapes_for_ray_polys(
142             calculate_ray_polys(space, self.body, self.position))
143         for shape in shapes:
144             shape.filter = LIGHT_FILTER
145         self.fitting.filter = FITTINGS_FILTER
146         space.add(self.body, self.fitting, *shapes)
147
148     def shapes_for_ray_polys(self, ray_polys):
149         return ray_polys
150
151     def toggle(self):
152         self.on = not self.on
153
154     def render_light(self, surface):
155         if not self.on:
156             return
157         subsurface = surface.copy()
158         light_colour = self.COLOURS[self.colour]
159         for shape in self.body.shapes:
160             if shape is self.fitting:
161                 continue
162             pygame_poly = [
163                 pymunk.pygame_util.to_pygame(v, surface) for v in
164                 shape.get_vertices()]
165             pygame.draw.polygon(
166                 subsurface, light_colour, pygame_poly, 0)
167             pygame.draw.aalines(
168                 subsurface, light_colour, True, pygame_poly, 1)
169         subsurface.set_alpha(50)
170         surface.blit(subsurface, (0, 0), None)
171
172     def render_fitting(self, surface):
173         pygame.draw.circle(
174             surface, (255, 255, 0),
175             pymunk.pygame_util.to_pygame(self.fitting.offset, surface),
176             int(self.fitting.radius))
177
178
179 class SpotLight(BaseLight):
180     def __init__(
181             self, colour="white", position=None, direction=90.0, spread=45.0):
182         super(SpotLight, self).__init__(colour, position)
183         self.direction = direction
184         self.spread = spread
185         self.i = 0
186
187
188 class Lamp(BaseLight):
189     def __init__(self, colour="white", position=None, radius=100.0):
190         super(Lamp, self).__init__(colour, position)
191         self.radius = radius