Move radius and angle limits into ray 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 import pygame.locals as pgl
9 import pygame.rect
10
11 from .constants import LIGHT_CATEGORY, FITTINGS_CATEGORY
12 from .rays import RayPolyManager
13 from .utils import DetailedTimer
14 from .loader import loader
15 from .transforms import Multiply
16
17 LIGHT_FILTER = pymunk.ShapeFilter(
18     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
19         LIGHT_CATEGORY | FITTINGS_CATEGORY),
20     categories=LIGHT_CATEGORY)
21
22 FITTINGS_FILTER = pymunk.ShapeFilter(
23     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
24         LIGHT_CATEGORY | FITTINGS_CATEGORY),
25     categories=FITTINGS_CATEGORY)
26
27 # Just match lights, nothing else
28 LIT_BY_FILTER = pymunk.ShapeFilter(mask=LIGHT_CATEGORY)
29
30
31 class LightManager(object):
32     """ Manages a set of lights. """
33
34     def __init__(self, space, gamestate):
35         self._space = space
36         self._lights = [
37             BaseLight.load(cfg) for cfg in gamestate.station["lights"]]
38         for light in self._lights:
39             light.add(self._space)
40
41     def toggle_nearest(self, *args, **kw):
42         light = self.nearest(*args, **kw)
43         if light:
44             light.toggle()
45
46     def nearest(self, pos, surfpos=False, max_distance=1.0):
47         if surfpos:
48             surface = pygame.display.get_surface()
49             pos = pymunk.pygame_util.from_pygame(pos, surface)
50         point_info = self._space.point_query_nearest(
51             pos, max_distance, pymunk.ShapeFilter(mask=FITTINGS_CATEGORY))
52         if point_info is not None:
53             return point_info.shape.body.light
54         return None
55
56     def lit_by(self, pos, surfpos=False, max_distance=0.0):
57         if surfpos:
58             surface = pygame.display.get_surface()
59             pos = pymunk.pygame_util.from_pygame(pos, surface)
60         point_info_list = self._space.point_query(
61             pos, max_distance, pymunk.ShapeFilter(mask=LIGHT_CATEGORY))
62         lights = [p.shape.body.light for p in point_info_list]
63         return [light for light in lights if light.on]
64
65     def light_query(self, shape):
66         """Query the lights by shape"""
67         old_filter = shape.filter
68         # We need to restrict matches to only the lights
69         shape.filter = LIT_BY_FILTER
70         shape_info_list = self._space.shape_query(shape)
71         shape.filter = old_filter
72         lights = [p.shape.body.light for p in shape_info_list]
73         return [light for light in lights if light.on]
74
75     def render_light(self, surface):
76         for light in self._lights:
77             light.render_light(surface)
78
79     def render_fittings(self, surface):
80         for light in self._lights:
81             light.render_fitting(surface)
82
83     def tick(self):
84         for light in self._lights:
85             light.tick()
86
87
88 class BaseLight(object):
89     """ Common light functionality. """
90
91     COLOURS = {
92         "red": (255, 0, 0),
93         "green": (0, 255, 0),
94         "blue": (0, 255, 255),
95         "yellow": (255, 255, 0),
96         "white": (255, 255, 255),
97     }
98
99     # defaults
100     RAY_MANAGER = RayPolyManager
101     FITTING_IMG = None
102     FITTING_RADIUS = 10.0
103
104     # cached surfaces
105     _surface_cache = {}
106
107     def __init__(
108             self, colour, position, intensity=1.0, radius_limits=None,
109             angle_limits=None):
110         self.colour = colour
111         self.on = True
112         self.intensity = intensity
113         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
114         self.body.light = self
115         self.ray_manager = self.RAY_MANAGER(
116             self.body, position, ray_filter=LIGHT_FILTER,
117             radius_limits=radius_limits, angle_limits=angle_limits)
118         self.fitting = pymunk.Circle(
119             self.body, self.FITTING_RADIUS, self.ray_manager.position)
120         self.fitting.filter = FITTINGS_FILTER
121         self._image = None
122
123     @property
124     def position(self):
125         return self.ray_manager.position
126
127     @classmethod
128     def load(cls, config):
129         kw = config.copy()
130         light_type = kw.pop("type")
131         [light_class] = [
132             c for c in cls.__subclasses__()
133             if c.__name__.lower() == light_type]
134         return light_class(**kw)
135
136     def add(self, space):
137         if self.body.space is not None:
138             space.remove(self.body, *self.body.shapes)
139         space.add(self.body, self.fitting)
140         self.ray_manager.set_space(space)
141         self.ray_manager.update_shapes()
142
143     def toggle(self):
144         self.on = not self.on
145
146     def _cached_surface(self, name, surface):
147         surf = self._surface_cache.get(name)
148         if surf is None:
149             surf = self._surface_cache[name] = pygame.surface.Surface(
150                 surface.get_size(), pgl.SWSURFACE
151             ).convert_alpha()
152         return surf
153
154     def light_colour(self):
155         light_colour = self.COLOURS[self.colour]
156         intensity = int(255 * self.intensity)
157         return light_colour + (intensity,)
158
159     def render_light(self, surface):
160         if not self.on:
161             return
162
163         dt = DetailedTimer("render_light")
164         dt.start()
165
166         max_radius = self.ray_manager.max_radius
167         min_radius = self.ray_manager.min_radius
168         dest_rect = self.ray_manager.pygame_rect(surface)
169
170         white, black = (255, 255, 255, 255), (0, 0, 0, 0)
171         light_colour = self.light_colour()
172
173         radius_mask = self._cached_surface('radius_mask', surface)
174         radius_mask.set_clip(dest_rect)
175         ray_mask = self._cached_surface('ray_mask', surface)
176         ray_mask.set_clip(dest_rect)
177
178         ray_mask.fill(black)
179         for pygame_poly in self.ray_manager.pygame_polys(surface):
180             pygame.draw.polygon(ray_mask, white, pygame_poly, 0)
181             pygame.draw.polygon(ray_mask, white, pygame_poly, 1)
182         dt.lap("ray mask rendered")
183
184         radius_mask.fill(black)
185         centre = self.ray_manager.pygame_position(surface)
186         pygame.draw.circle(
187             radius_mask, light_colour, centre, int(max_radius), 0)
188         pygame.draw.circle(
189             radius_mask, black, centre, int(min_radius), 0)
190         dt.lap("radius mask rendered")
191
192         ray_mask.blit(radius_mask, dest_rect, dest_rect, pgl.BLEND_RGBA_MULT)
193         dt.lap("blitted radius mask to ray mask")
194
195         surface.blit(ray_mask, dest_rect, dest_rect)
196         dt.lap("blitted surface")
197         dt.end()
198
199     def get_image(self):
200         if self._image is None:
201             fitting_colour = self.COLOURS[self.colour]
202             self._image = loader.load_image(
203                 "64", self.FITTING_IMG,
204                 transform=Multiply(colour=fitting_colour))
205         return self._image
206
207     def render_fitting(self, surface):
208         rx, ry = self.ray_manager.pygame_position(surface)
209         surface.blit(self.get_image(), (rx - 32, ry - 32), None, 0)
210
211     def tick(self):
212         pass
213
214
215 class Lamp(BaseLight):
216     FITTING_IMG = "lamp.png"
217
218     def __init__(self, **kw):
219         kw.pop("direction", None)
220         kw.pop("spread", None)
221         super(Lamp, self).__init__(**kw)
222
223
224 class SpotLight(BaseLight):
225     FITTING_IMG = "spotlight.png"
226
227     def __init__(self, **kw):
228         kw.pop("direction", None)
229         kw.pop("spread", None)
230         self.angular_velocity = kw.pop("angular_velocity", None)
231         super(SpotLight, self).__init__(**kw)
232
233     def tick(self):
234         if self.angular_velocity:
235             self.ray_manager.rotate_degrees(self.angular_velocity)
236             self.ray_manager.update_shapes()