1 """ May it be a light for you in dark places, when all other lights go out.
5 import pymunk.pygame_util
9 from .constants import (
10 SCREEN_SIZE, LIGHT_CATEGORY, FITTINGS_CATEGORY)
11 from .utils import debug_timer
13 LIGHT_FILTER = pymunk.ShapeFilter(
14 mask=pymunk.ShapeFilter.ALL_MASKS ^ (
15 LIGHT_CATEGORY | FITTINGS_CATEGORY),
16 categories=LIGHT_CATEGORY)
18 FITTINGS_FILTER = pymunk.ShapeFilter(
19 mask=pymunk.ShapeFilter.ALL_MASKS ^ (
20 LIGHT_CATEGORY | FITTINGS_CATEGORY),
21 categories=FITTINGS_CATEGORY)
23 # Just match lights, nothing else
24 LIT_BY_FILTER = pymunk.ShapeFilter(mask=LIGHT_CATEGORY)
28 """ An iterable that returns ordered rays from pos to the edge of the
29 screen, starting with the edge point (0, 0) and continuing clockwise
30 in pymunk coordinates.
33 left, right, bottom, top = 0, w, 0, h
35 for y in range(0, h, step):
36 yield pymunk.Vec2d(left, y)
37 for x in range(0, w, step):
38 yield pymunk.Vec2d(x, top)
39 for y in range(top, -1, -step):
40 yield pymunk.Vec2d(right, y)
41 for x in range(right, -1, -step):
42 yield pymunk.Vec2d(x, bottom)
45 @debug_timer("lights.calculate_ray_polys")
46 def calculate_ray_polys(space, body, position):
47 position = pymunk.Vec2d(position)
50 for ray in screen_rays(position):
51 info = space.segment_query_first(position, ray, 1, LIGHT_FILTER)
52 point = ray if info is None else info.point
53 vertices.append(point)
55 trial_poly = pymunk.Poly(None, vertices)
56 trial_poly.update(pymunk.Transform.identity())
57 query_prev = trial_poly.point_query(vertices[-2])
58 query_pos = trial_poly.point_query(position)
59 if query_prev.distance < -0.01 or query_pos.distance < -0.01:
60 new_poly = pymunk.Poly(body, vertices[:-1])
61 vertices = [position, vertices[-1]]
62 ray_polys.append(new_poly)
64 vertices = trial_poly.get_vertices() + [point]
66 ray_polys.append(pymunk.Poly(body, vertices))
70 class LightManager(object):
71 """ Manages a set of lights. """
73 def __init__(self, space, gamestate):
76 BaseLight.load(cfg) for cfg in gamestate.station["lights"]]
77 for light in self._lights:
78 light.add(self._space)
80 def toggle_nearest(self, *args, **kw):
81 light = self.nearest(*args, **kw)
85 def nearest(self, pos, surfpos=False, max_distance=1.0):
87 surface = pygame.display.get_surface()
88 pos = pymunk.pygame_util.from_pygame(pos, surface)
89 point_info = self._space.point_query_nearest(
90 pos, max_distance, pymunk.ShapeFilter(mask=FITTINGS_CATEGORY))
91 if point_info is not None:
92 return point_info.shape.body.light
95 def lit_by(self, pos, surfpos=False, max_distance=0.0):
97 surface = pygame.display.get_surface()
98 pos = pymunk.pygame_util.from_pygame(pos, surface)
99 point_info_list = self._space.point_query(
100 pos, max_distance, pymunk.ShapeFilter(mask=LIGHT_CATEGORY))
101 lights = [p.shape.body.light for p in point_info_list]
102 return [light for light in lights if light.on]
104 def light_query(self, shape):
105 """Query the lights by shape"""
106 old_filter = shape.filter
107 # We need to restrict matches to only the lights
108 shape.filter = LIT_BY_FILTER
109 shape_info_list = self._space.shape_query(shape)
110 shape.filter = old_filter
111 lights = [p.shape.body.light for p in shape_info_list]
112 return [light for light in lights if light.on]
114 def render_light(self, surface):
115 for light in self._lights:
116 light.render_light(surface)
118 def render_fittings(self, surface):
119 for light in self._lights:
120 light.render_fitting(surface)
123 class BaseLight(object):
124 """ Common light functionality. """
128 "green": (0, 255, 0),
129 "blue": (0, 255, 255),
130 "yellow": (255, 255, 0),
131 "white": (255, 255, 255),
135 self, colour, position, intensity=1.0,
136 radius_limits=(None, None), angle_limits=(None, None)):
138 self.position = pymunk.Vec2d(position)
140 self.intensity = intensity
141 self.radius_limits = radius_limits
142 self.angle_limits = angle_limits
143 self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
144 self.fitting = pymunk.Circle(self.body, 10.0, self.position)
145 self.body.light = self
148 def load(cls, config):
150 light_type = kw.pop("type")
152 c for c in cls.__subclasses__()
153 if c.__name__.lower() == light_type]
154 return light_class(**kw)
156 def add(self, space):
157 if self.body.space is not None:
158 space.remove(self.body, *self.body.shapes)
159 shapes = self.shapes_for_ray_polys(
160 calculate_ray_polys(space, self.body, self.position))
162 shape.filter = LIGHT_FILTER
163 self.fitting.filter = FITTINGS_FILTER
164 space.add(self.body, self.fitting, *shapes)
166 def shapes_for_ray_polys(self, ray_polys):
170 self.on = not self.on
172 def render_light(self, surface):
176 raypoly_mask = surface.copy()
177 white, black = (255, 255, 255, 255), (0, 0, 0, 0)
178 raypoly_mask.fill(black)
179 for shape in self.body.shapes:
180 if shape is self.fitting:
183 pymunk.pygame_util.to_pygame(v, surface) for v in
184 shape.get_vertices()]
185 pygame.draw.polygon(raypoly_mask, white, pygame_poly, 0)
186 pygame.draw.aalines(raypoly_mask, white, True, pygame_poly, 1)
188 limits_mask = surface.copy()
189 limits_mask.fill(black)
190 centre = pymunk.pygame_util.to_pygame(self.position, surface)
191 max_radius = self.radius_limits[1] or 50.0
192 box = (centre[0] - max_radius, centre[1] - max_radius,
193 max_radius * 2, max_radius * 2)
194 width = max_radius - (self.radius_limits[0] or 0)
195 box2 = (box[0] + 1,) + tuple(box[1:])
196 box3 = (box[0] + 2,) + tuple(box[1:])
198 start_angle = (self.angle_limits[0] or 0.0) * (math.pi / 180.0)
199 end_angle = (self.angle_limits[1] or 360.0) * (math.pi / 180.0)
201 limits_mask, white, box, start_angle, end_angle, int(width))
203 limits_mask, white, box2, start_angle, end_angle, int(width))
205 limits_mask, white, box3, start_angle, end_angle, int(width))
207 import pygame.locals as pgl
208 raypoly_mask.blit(limits_mask, (0, 0), None, pgl.BLEND_RGBA_MIN)
209 raypoly_mask.set_colorkey(black)
211 light_colour = self.COLOURS[self.colour]
212 overlay = surface.copy()
213 overlay.fill(light_colour)
214 raypoly_mask.blit(overlay, (0, 0), None, pgl.BLEND_RGBA_MULT)
216 mask2 = surface.copy()
218 mask2.blit(raypoly_mask, (0, 0), None)
220 mask2.set_alpha(int(255 * self.intensity))
222 surface.blit(mask2, (0, 0), None)
224 def render_fitting(self, surface):
226 surface, (255, 255, 0),
227 pymunk.pygame_util.to_pygame(self.fitting.offset, surface),
228 int(self.fitting.radius))
231 class SpotLight(BaseLight):
232 def __init__(self, **kw):
233 kw.pop("direction", None)
234 kw.pop("spread", None)
235 super(SpotLight, self).__init__(**kw)