source: nagslang/level.py@ 488:ae8eb7c0f7bb

Last change on this file since 488:ae8eb7c0f7bb was 488:ae8eb7c0f7bb, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Better wall and bulkhead drawing.

File size: 9.1 KB
Line 
1import os
2
3import pygame
4import pygame.locals as pgl
5import pymunk
6
7from nagslang import collectable
8from nagslang import game_object as go
9from nagslang import enemies
10from nagslang import puzzle
11from nagslang.utils import (
12 tile_surface, points_to_pygame, extend_line, points_to_lines)
13from nagslang.resources import resources
14from nagslang.yamlish import load, dump
15
16POLY_COLORS = {
17 1: pygame.color.THECOLORS['red'],
18 2: pygame.color.THECOLORS['green'],
19 3: pygame.color.THECOLORS['yellow'],
20 4: pygame.color.THECOLORS['blue'],
21 5: pygame.color.THECOLORS['lightblue'],
22 6: pygame.color.THECOLORS['magenta'],
23 7: pygame.color.THECOLORS['lightgreen'],
24 8: pygame.color.THECOLORS['grey'],
25}
26
27
28LINE_COLOR = pygame.color.THECOLORS['orange']
29
30
31class Level(object):
32 _game_starting_point = None
33
34 def __init__(self, name, world):
35 self.name = name
36 # defaults
37 self.x = 800
38 self.y = 600
39 self.polygons = {}
40 self.lines = []
41 self.world = world
42 self.world.level_state.setdefault(name, {})
43 self.basetile = 'tiles/floor.png'
44 self._tile_image = None
45 self._surface = None
46 self._base_surface = None
47 self._exterior = False
48 self._glue = puzzle.PuzzleGlue()
49 self.drawables = []
50 self.overlay_drawables = []
51 self._game_objects = []
52 self._enemies = []
53
54 def _get_data(self):
55 # For overriding in tests.
56 with resources.get_file('levels', self.name) as f:
57 return load(f)
58
59 def _dump_data(self, f):
60 # For manipulation in tests.
61 dump({
62 'size': [self.x, self.y],
63 'base_tile': self.basetile,
64 'polygons': self.polygons,
65 'lines': self.lines,
66 'game_objects': self._game_objects,
67 'enemies': self._enemies,
68 }, f)
69
70 @classmethod
71 def list_levels(cls):
72 dir_ = resources.get_resource_path('levels')
73 for file_ in os.listdir(dir_):
74 if file_ == 'meta':
75 continue
76 yield file_
77
78 @classmethod
79 def game_starting_point(cls):
80 if not cls._game_starting_point:
81 with resources.get_file('levels', 'meta') as f:
82 data = load(f)
83 cls._game_starting_point = (data['starting_level'],
84 tuple(data['starting_position']))
85 return cls._game_starting_point
86
87 def is_starting_level(self):
88 return self.name == self.game_starting_point()[0]
89
90 def load(self, space):
91 data = self._get_data()
92 self.x, self.y = data['size']
93 self.basetile = data['base_tile']
94 for i, points in data['polygons'].iteritems():
95 self.polygons[i] = []
96 for point in points:
97 self.polygons[i].append(tuple(point))
98 self.lines = data.get('lines', [])
99 self._game_objects = data.get('game_objects', [])
100 for game_object_dict in self._game_objects:
101 self._create_game_object(space, **game_object_dict)
102 self._enemies = data.get('enemies', [])
103 for enemy_dict in self._enemies:
104 self._create_enemy(space, **enemy_dict)
105
106 def _create_game_object(self, space, classname, args, name=None):
107 modules = {
108 'collectable': collectable,
109 'game_object': go,
110 'puzzle': puzzle,
111 }
112 if '.' in classname:
113 module, classname = classname.split('.')
114 else:
115 module = 'game_object'
116 cls = getattr(modules[module], classname)
117
118 if module == 'collectable' and name in self.world.inventory:
119 return
120
121 if issubclass(cls, puzzle.Puzzler):
122 gobj = cls(*args)
123 elif issubclass(cls, go.GameObject):
124 gobj = cls(space, *args)
125 level_state = self.world.level_state[self.name]
126 stored_state = level_state.get(name, {})
127 should_save = bool(gobj.set_stored_state_dict(stored_state))
128 if should_save:
129 if name is None:
130 raise Exception(
131 "Unnamed game object wants to save state:" % (gobj,))
132 level_state[name] = stored_state
133 self.drawables.append(gobj)
134 if gobj.overlay:
135 self.overlay_drawables.append(gobj.overlay)
136 else:
137 raise TypeError(
138 "Expected a subclass of Puzzler or GameObject, got %s" % (
139 classname))
140 if name is not None:
141 self._glue.add_component(name, gobj)
142 return gobj
143
144 def _create_enemy(self, space, classname, args, name=None):
145 cls = getattr(enemies, classname)
146 if issubclass(cls, go.GameObject):
147 gobj = cls(space, self.world, *args)
148 self.drawables.append(gobj)
149 else:
150 raise TypeError(
151 "Expected a subclass of GameObject, got %s" % (
152 classname))
153 if name is not None:
154 self._glue.add_component(name, gobj)
155 return gobj
156
157 def all_closed(self):
158 """Check if all the polygons are closed"""
159 closed = True
160 messages = []
161 for index, poly in self.polygons.items():
162 if len(poly) == 0:
163 # We ignore empty polygons
164 continue
165 elif len(poly) == 1:
166 closed = False
167 messages.append("Error: polygon %s too small" % index)
168 elif poly[-1] != poly[0]:
169 closed = False
170 messages.append("Error: polygon %s not closed" % index)
171 return closed, messages
172
173 def save(self):
174 closed, _ = self.all_closed()
175 if not closed:
176 return False
177 with resources.get_file('levels', self.name, mode='w') as f:
178 self._dump_data(f)
179 return True
180
181 def get_size(self):
182 return self.x, self.y
183
184 def set_base_tile(self, new_tile):
185 self.basetile = new_tile
186 self._tile_image = None
187
188 def get_walls(self):
189 walls = self.polygons.values()
190 walls.extend(self.lines)
191 return walls
192
193 def _draw_wall_line(self, points, width, colour, extend):
194 for line in points_to_lines(points):
195 if extend:
196 line = extend_line(
197 pymunk.Vec2d(line[0]), pymunk.Vec2d(line[1]), extend)
198 line = points_to_pygame(self._surface, line)
199 pygame.draw.line(self._surface, colour, line[0], line[1], width)
200
201 def _draw_walls_lines(self, width, colour, extend):
202 for index, polygon in self.polygons.items():
203 self._draw_wall_line(polygon, width, colour, extend)
204 for line in self.lines:
205 self._draw_wall_line(line, width, colour, extend)
206
207 def _draw_walls(self):
208 inner_colour = pygame.color.THECOLORS['red']
209 mid_colour = pygame.color.THECOLORS['orange']
210 outer_colour = pygame.color.THECOLORS['yellow']
211 self._draw_walls_lines(5, outer_colour, 0)
212 self._draw_walls_lines(3, outer_colour, 1)
213 self._draw_walls_lines(3, mid_colour, 0)
214 self._draw_walls_lines(1, inner_colour, 0)
215
216 def get_background(self):
217 if self._surface is None:
218 self._draw_background()
219 self._draw_exterior()
220 # Draw polygons
221 self._draw_walls()
222 return self._surface
223
224 def _draw_exterior(self, force=False):
225 """Fill the exterior of the level with black"""
226 if self._exterior and not force:
227 return
228 white = pygame.color.THECOLORS['white']
229 black = pygame.color.THECOLORS['black']
230 surface = pygame.surface.Surface((self.x, self.y), pgl.SRCALPHA)
231 surface.fill(black)
232 for index, polygon in self.polygons.items():
233 if len(polygon) > 1:
234 pointlist = points_to_pygame(self._surface, polygon)
235 # filled polygons
236 color = white
237 # If a polygon overlaps on of the existing polygons,
238 # it is treated as negative
239 # This is not a complete inversion, since any overlap
240 # triggers this (inversion is easy enough, but the
241 # behaviour doesn't seem useful)
242 # We also only check the vertexes - not breaking this
243 # assumption is left to the level designers
244 surface.lock()
245 for p in pointlist:
246 if surface.get_at(p) == white:
247 color = black
248 surface.unlock()
249 pygame.draw.polygon(surface, color, pointlist, 0)
250 self._surface.blit(surface, (0, 0), special_flags=pgl.BLEND_RGBA_MULT)
251 self._exterior = True
252
253 def _draw_background(self, force=False):
254 if self._tile_image is None:
255 self._tile_image = resources.get_image(self.basetile)
256 if self._surface is not None and not force:
257 # We assume we don't change
258 return self._surface
259 if self._base_surface is None:
260 self._base_surface = tile_surface((self.x, self.y),
261 self._tile_image)
262 self._surface = self._base_surface.copy()
263 return self._surface
Note: See TracBrowser for help on using the repository browser.