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)
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.
30 left, right, bottom, top = 0, w, 0, h
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)
42 @debug_timer("lights.calculate_ray_polys")
43 def calculate_ray_polys(space, body, position):
44 position = pymunk.Vec2d(position)
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)
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)
61 vertices = trial_poly.get_vertices() + [point]
63 ray_polys.append(pymunk.Poly(body, vertices))
67 class LightManager(object):
68 """ Manages a set of lights. """
70 def __init__(self, space, gamestate):
73 BaseLight.load(cfg) for cfg in gamestate.station["lights"]]
74 for light in self._lights:
75 light.add(self._space)
77 def toggle_nearest(self, *args, **kw):
78 light = self.nearest(*args, **kw)
82 def nearest(self, pos, surfpos=False, max_distance=1.0):
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
92 def lit_by(self, pos, surfpos=False, max_distance=0.0):
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]
101 def render_light(self, surface):
102 for light in self._lights:
103 light.render_light(surface)
105 def render_fittings(self, surface):
106 for light in self._lights:
107 light.render_fitting(surface)
110 class BaseLight(object):
111 """ Common light functionality. """
115 "green": (0, 255, 0),
116 "blue": (0, 255, 255),
117 "yellow": (255, 255, 0),
118 "white": (255, 255, 255),
122 self, colour, position, intensity=1.0,
123 radius_limits=(None, None), angle_limits=(None, None)):
125 self.position = pymunk.Vec2d(position)
127 self.intensity = intensity
128 self.radius_limits = radius_limits
129 self.angle_limits = angle_limits
130 self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
131 self.fitting = pymunk.Circle(self.body, 10.0, self.position)
132 self.body.light = self
135 def load(cls, config):
137 light_type = kw.pop("type")
139 c for c in cls.__subclasses__()
140 if c.__name__.lower() == light_type]
141 return light_class(**kw)
143 def add(self, space):
144 if self.body.space is not None:
145 space.remove(self.body, *self.body.shapes)
146 shapes = self.shapes_for_ray_polys(
147 calculate_ray_polys(space, self.body, self.position))
149 shape.filter = LIGHT_FILTER
150 self.fitting.filter = FITTINGS_FILTER
151 space.add(self.body, self.fitting, *shapes)
153 def shapes_for_ray_polys(self, ray_polys):
157 self.on = not self.on
159 def render_light(self, surface):
163 raypoly_mask = surface.copy()
164 white, black = (255, 255, 255, 255), (0, 0, 0, 0)
165 raypoly_mask.fill(black)
166 for shape in self.body.shapes:
167 if shape is self.fitting:
170 pymunk.pygame_util.to_pygame(v, surface) for v in
171 shape.get_vertices()]
172 pygame.draw.polygon(raypoly_mask, white, pygame_poly, 0)
173 pygame.draw.aalines(raypoly_mask, white, True, pygame_poly, 1)
175 limits_mask = surface.copy()
176 limits_mask.fill(black)
177 centre = pymunk.pygame_util.to_pygame(self.position, surface)
178 max_radius = self.radius_limits[1] or 50.0
179 box = (centre[0] - max_radius, centre[1] - max_radius,
180 max_radius * 2, max_radius * 2)
181 width = max_radius - (self.radius_limits[0] or 0)
182 box2 = (box[0] + 1,) + tuple(box[1:])
183 box3 = (box[0] + 2,) + tuple(box[1:])
185 start_angle = (self.angle_limits[0] or 0.0) * (math.pi / 180.0)
186 end_angle = (self.angle_limits[1] or 360.0) * (math.pi / 180.0)
188 limits_mask, white, box, start_angle, end_angle, int(width))
190 limits_mask, white, box2, start_angle, end_angle, int(width))
192 limits_mask, white, box3, start_angle, end_angle, int(width))
194 import pygame.locals as pgl
195 raypoly_mask.blit(limits_mask, (0, 0), None, pgl.BLEND_RGBA_MIN)
196 raypoly_mask.set_colorkey(black)
198 light_colour = self.COLOURS[self.colour]
199 overlay = surface.copy()
200 overlay.fill(light_colour)
201 raypoly_mask.blit(overlay, (0, 0), None, pgl.BLEND_RGBA_MULT)
203 mask2 = surface.copy()
205 mask2.blit(raypoly_mask, (0, 0), None)
207 mask2.set_alpha(int(255 * self.intensity))
209 surface.blit(mask2, (0, 0), None)
211 def render_fitting(self, surface):
213 surface, (255, 255, 0),
214 pymunk.pygame_util.to_pygame(self.fitting.offset, surface),
215 int(self.fitting.radius))
218 class SpotLight(BaseLight):
219 def __init__(self, **kw):
220 kw.pop("direction", None)
221 kw.pop("spread", None)
222 super(SpotLight, self).__init__(**kw)