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