1 """ May it be a light for you in dark places, when all other lights go out.
5 import pymunk.pygame_util
8 import pygame.locals as pgl
10 from .constants import (
11 SCREEN_SIZE, LIGHT_CATEGORY, FITTINGS_CATEGORY)
12 from .utils import debug_timer
14 LIGHT_FILTER = pymunk.ShapeFilter(
15 mask=pymunk.ShapeFilter.ALL_MASKS ^ (
16 LIGHT_CATEGORY | FITTINGS_CATEGORY),
17 categories=LIGHT_CATEGORY)
19 FITTINGS_FILTER = pymunk.ShapeFilter(
20 mask=pymunk.ShapeFilter.ALL_MASKS ^ (
21 LIGHT_CATEGORY | FITTINGS_CATEGORY),
22 categories=FITTINGS_CATEGORY)
24 # Just match lights, nothing else
25 LIT_BY_FILTER = pymunk.ShapeFilter(mask=LIGHT_CATEGORY)
29 """ An iterable that returns ordered rays from pos to the edge of the
30 screen, starting with the edge point (0, 0) and continuing clockwise
31 in pymunk coordinates.
34 left, right, bottom, top = 0, w, 0, h
36 for y in range(0, h, step):
37 yield pymunk.Vec2d(left, y)
38 for x in range(0, w, step):
39 yield pymunk.Vec2d(x, top)
40 for y in range(top, -1, -step):
41 yield pymunk.Vec2d(right, y)
42 for x in range(right, -1, -step):
43 yield pymunk.Vec2d(x, bottom)
46 @debug_timer("lights.calculate_ray_polys")
47 def calculate_ray_polys(space, body, position):
48 position = pymunk.Vec2d(position)
51 for ray in screen_rays(position):
52 info = space.segment_query_first(position, ray, 1, LIGHT_FILTER)
53 point = ray if info is None else info.point
54 vertices.append(point)
56 trial_poly = pymunk.Poly(None, vertices)
57 trial_poly.update(pymunk.Transform.identity())
58 query_prev = trial_poly.point_query(vertices[-2])
59 query_pos = trial_poly.point_query(position)
60 if query_prev.distance < -0.01 or query_pos.distance < -0.01:
61 new_poly = pymunk.Poly(body, vertices[:-1])
62 vertices = [position, vertices[-1]]
63 ray_polys.append(new_poly)
65 vertices = trial_poly.get_vertices() + [point]
67 ray_polys.append(pymunk.Poly(body, vertices))
71 class LightManager(object):
72 """ Manages a set of lights. """
74 def __init__(self, space, gamestate):
77 BaseLight.load(cfg) for cfg in gamestate.station["lights"]]
78 for light in self._lights:
79 light.add(self._space)
81 def toggle_nearest(self, *args, **kw):
82 light = self.nearest(*args, **kw)
86 def nearest(self, pos, surfpos=False, max_distance=1.0):
88 surface = pygame.display.get_surface()
89 pos = pymunk.pygame_util.from_pygame(pos, surface)
90 point_info = self._space.point_query_nearest(
91 pos, max_distance, pymunk.ShapeFilter(mask=FITTINGS_CATEGORY))
92 if point_info is not None:
93 return point_info.shape.body.light
96 def lit_by(self, pos, surfpos=False, max_distance=0.0):
98 surface = pygame.display.get_surface()
99 pos = pymunk.pygame_util.from_pygame(pos, surface)
100 point_info_list = self._space.point_query(
101 pos, max_distance, pymunk.ShapeFilter(mask=LIGHT_CATEGORY))
102 lights = [p.shape.body.light for p in point_info_list]
103 return [light for light in lights if light.on]
105 def light_query(self, shape):
106 """Query the lights by shape"""
107 old_filter = shape.filter
108 # We need to restrict matches to only the lights
109 shape.filter = LIT_BY_FILTER
110 shape_info_list = self._space.shape_query(shape)
111 shape.filter = old_filter
112 lights = [p.shape.body.light for p in shape_info_list]
113 return [light for light in lights if light.on]
115 def render_light(self, surface):
116 for light in self._lights:
117 light.render_light(surface)
119 def render_fittings(self, surface):
120 for light in self._lights:
121 light.render_fitting(surface)
124 class BaseLight(object):
125 """ Common light functionality. """
129 "green": (0, 255, 0),
130 "blue": (0, 255, 255),
131 "yellow": (255, 255, 0),
132 "white": (255, 255, 255),
136 self, colour, position, intensity=1.0,
137 radius_limits=(None, None), angle_limits=(None, None)):
139 self.position = pymunk.Vec2d(position)
141 self.intensity = intensity
142 self.radius_limits = radius_limits
143 self.angle_limits = angle_limits
144 self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
145 self.fitting = pymunk.Circle(self.body, 10.0, self.position)
146 self.body.light = self
149 def load(cls, config):
151 light_type = kw.pop("type")
153 c for c in cls.__subclasses__()
154 if c.__name__.lower() == light_type]
155 return light_class(**kw)
157 def add(self, space):
158 if self.body.space is not None:
159 space.remove(self.body, *self.body.shapes)
160 shapes = self.shapes_for_ray_polys(
161 calculate_ray_polys(space, self.body, self.position))
163 shape.filter = LIGHT_FILTER
164 self.fitting.filter = FITTINGS_FILTER
165 space.add(self.body, self.fitting, *shapes)
167 def shapes_for_ray_polys(self, ray_polys):
171 self.on = not self.on
173 def render_light(self, surface):
177 raypoly_mask = pygame.surface.Surface(surface.get_size(), pgl.SWSURFACE)
178 white, black = (255, 255, 255, 255), (0, 0, 0, 0)
179 raypoly_mask.fill(black)
180 for shape in self.body.shapes:
181 if shape is self.fitting:
184 pymunk.pygame_util.to_pygame(v, surface) for v in
185 shape.get_vertices()]
186 pygame.draw.polygon(raypoly_mask, white, pygame_poly, 0)
187 pygame.draw.aalines(raypoly_mask, white, True, pygame_poly, 1)
189 limits_mask = pygame.surface.Surface(surface.get_size(), pgl.SWSURFACE)
190 limits_mask.fill(black)
191 centre = pymunk.pygame_util.to_pygame(self.position, surface)
192 max_radius = self.radius_limits[1] or 50.0
193 box = (centre[0] - max_radius, centre[1] - max_radius,
194 max_radius * 2, max_radius * 2)
195 width = max_radius - (self.radius_limits[0] or 0)
196 box2 = (box[0] + 1,) + tuple(box[1:])
197 box3 = (box[0] + 2,) + tuple(box[1:])
199 start_angle = (self.angle_limits[0] or 0.0) * (math.pi / 180.0)
200 end_angle = (self.angle_limits[1] or 360.0) * (math.pi / 180.0)
202 limits_mask, white, box, start_angle, end_angle, int(width))
204 limits_mask, white, box2, start_angle, end_angle, int(width))
206 limits_mask, white, box3, start_angle, end_angle, int(width))
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 = pygame.surface.Surface(surface.get_size(), pgl.SWSURFACE)
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)