source: skaapsteker/levelscene.py

Last change on this file was 622:da331c80ec08, checked in by Jeremy Thurgood <firxen@…>, 12 years ago

Clean up sprite inheritance hierarchy a bit.

File size: 14.6 KB
Line 
1"""Scene wrapping a level object."""
2
3from pygame.locals import (KEYDOWN, KEYUP, K_DOWN, K_ESCAPE, K_LEFT, K_RIGHT,
4 K_SEMICOLON, K_UP, K_c, K_j, K_p, K_q, K_x, K_z,
5 K_v, K_k, K_RETURN, K_SPACE, SRCALPHA)
6
7import pygame
8import time
9
10from . import options
11from . import engine
12from . import level
13from . import physics
14from . import constants
15from . import data
16from .sprites import player, base
17from .widgets.text import Text
18from .widgets.bubble import DialogueWidget, NotificationWidget
19from .utils import cadd, csub, cdiv
20
21
22class LevelScene(engine.Scene):
23
24 def __init__(self, game_state, soundsystem, doorway_def=None):
25 super(LevelScene, self).__init__(game_state, soundsystem)
26
27 if doorway_def is not None:
28 fox = self.game_state.world.fox
29 fox.level, fox.doorway = doorway_def.split('.')
30 self._level = level.Level(self.game_state.world.fox.level, soundsystem)
31 self._player_dead = False
32 self._dialogue = None
33
34 self.setup_player()
35
36 self._level_surface = self._level.get_surface()
37 self._clip_rect = None
38 self._world = physics.World(self._level_surface.get_rect())
39 self._paused = False
40
41 # Prepare a Surface for displaying when Dead
42 self._dead_overlay = pygame.Surface(constants.SCREEN, flags=SRCALPHA)
43 self._dead_overlay.fill((255, 255, 255, 128))
44 death_text_pos = cdiv(constants.SCREEN, 2)
45 death_text = Text("You've died.", death_text_pos, size=24)
46 death_text.rect.move_ip(-death_text.rect.width / 2, -death_text.rect.height)
47 death_text.draw(self._dead_overlay)
48 death_text = Text("Press Escape to exit or Return to restart the level.", death_text_pos)
49 death_text.rect.move_ip(-death_text.rect.width / 2, 0)
50 death_text.draw(self._dead_overlay)
51
52 # Helper images for hud
53 self._tofu = data.load_image('icons/tofu.png')
54 self._scroll = data.load_image('icons/haiku-scroll.png')
55 self._tails = {}
56 for tail in constants.FoxHud.TAIL_POSITIONS.iterkeys():
57 image = data.load_image('icons/tails/%s.png' % tail)
58 grey_image = data.load_image('icons/tails/g_%s.png' % tail)
59 self._tails[tail] = (grey_image, image)
60
61 for sprite in self._level.sprites:
62 # XXX: NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO!
63 if isinstance(sprite, base.Monster) or isinstance(sprite, base.CelestialDoorway):
64 sprite.world = game_state.world
65 self._world.add(sprite)
66 npcs_and_items = game_state.create_sprites(self._level.name)
67 self._npcs = dict((s.name, s) for s in npcs_and_items if hasattr(s, 'dsm'))
68 for sprite in npcs_and_items:
69 self._world.add(sprite)
70 self._world.add(self._player)
71
72 self._build_action_map()
73 self._key_sequence = []
74
75 def setup_player(self):
76 doorway = self._level.doorways[self.game_state.world.fox.doorway]
77 self._player = player.Player(self.game_state.world, self._soundsystem)
78 self._player.set_facing(doorway.facing)
79 self._player.set_image()
80 self._player.set_pos(doorway.get_tile_pos())
81
82 # Double tap stuff
83 self._last_keydown_time = None
84 self._last_keyup_time = None
85
86 def _build_action_map(self):
87 action = lambda s: getattr(self._player, 'action_%s' % s)
88
89 self._quit_keys = set([K_q, K_ESCAPE])
90 self._restart_keys = set([K_x, K_z, K_RETURN, K_SPACE])
91
92 self._fast_key_map = {
93 K_LEFT: action('left'),
94 K_RIGHT: action('right'),
95 K_UP: action('up'),
96 }
97 self._fast_keys_down = set()
98
99 self._slow_key_map = {
100 K_DOWN: action('down'),
101 K_ESCAPE: self._quit,
102 K_p: self._toggle_pause,
103 }
104 if options['dvorak']:
105 self._slow_key_map[K_SEMICOLON] = action('fire1')
106 self._slow_key_map[K_q] = action('fire2')
107 self._slow_key_map[K_j] = action('transform')
108 self._slow_key_map[K_k] = action('invisible')
109 else:
110 self._slow_key_map[K_x] = action('fire1')
111 self._slow_key_map[K_z] = action('fire2')
112 self._slow_key_map[K_c] = action('transform')
113 self._slow_key_map[K_v] = action('invisible')
114 self._slow_key_map[K_q] = self._quit
115
116 self._key_tap_map = {
117 (K_LEFT, K_LEFT) : action('double_left'),
118 (K_RIGHT, K_RIGHT) : action('double_right'),
119 (K_UP, K_UP) : action('double_up'),
120 }
121
122 def _quit(self, pause=True):
123 import menuscene # avoid circular import
124 if pause:
125 engine.ChangeScene.post(menuscene.MenuScene(self.game_state, self._soundsystem))
126 else:
127 # FIXME: When starting the game, we shoudl ensure we have sane
128 # states
129 if self._player_dead:
130 self._player.restore()
131 engine.ChangeScene.post(menuscene.MenuScene(self.game_state, self._soundsystem))
132
133 def _restart(self):
134 if self._player_dead:
135 self._player.restore()
136 engine.ChangeScene.post(LevelScene(self.game_state, self._soundsystem))
137
138 def _toggle_pause(self):
139 if self._paused:
140 self._world.thaw()
141 self._soundsystem.unpause()
142 self._paused = False
143 else:
144 self._world.freeze()
145 self._soundsystem.pause()
146 self._paused = True
147
148 def _open_dialogue(self, npc):
149 if isinstance(npc, basestring):
150 npc = self._npcs[npc]
151 if npc.dsm.has_text():
152 if self._dialogue is not None:
153 self._dialogue.close()
154 self._world.freeze()
155 self._dialogue = DialogueWidget(npc)
156 else:
157 self._close_dialogue(npc)
158
159 def _open_notification(self, text):
160 if self._dialogue is not None:
161 self._dialogue.close()
162 self._world.freeze()
163 self._dialogue = NotificationWidget(text)
164
165 def _close_dialogue(self, npc):
166 if isinstance(npc, basestring):
167 npc = self._npcs[npc]
168 # below works for notifications too since they don't have .npc and send None
169 if self._dialogue is not None and getattr(self._dialogue, 'npc', None) is npc:
170 self._world.thaw()
171 self._dialogue.close()
172 self._dialogue = None
173
174 def leave(self):
175 """Freeze the scene, for serialization"""
176 self._world.freeze()
177 self._level.leave()
178
179 def enter(self):
180 """Unfreeze"""
181 self._world.thaw()
182 self._level.enter()
183
184 def draw(self, screen_surface, engine):
185 if self._clip_rect is None:
186 self._clip_rect = pygame.Rect((0, 0), screen_surface.get_size())
187
188 if not self._paused and not self._dialogue:
189 for key in self._fast_keys_down:
190 self._fast_key_map[key]()
191 self._world.update()
192
193 self._update_clip_rect()
194
195 self._level_surface.set_clip(self._clip_rect)
196 self._level.draw(self._level_surface)
197 self._world.draw(self._level_surface)
198 if self._dialogue:
199 self._dialogue.draw(self._level_surface)
200
201 self._draw_fox_status()
202
203 fps_text_pos = cadd(self._clip_rect.topleft, 10)
204 fps_text = Text('FPS: %.1f' % engine.get_fps(), fps_text_pos, shadow='white')
205 fps_text.draw(self._level_surface)
206
207 if self._paused:
208 paused_text_pos = csub(self._clip_rect.center, 10)
209 paused_text = Text('Paused', paused_text_pos)
210 paused_text.draw(self._level_surface)
211
212 if self._player_dead:
213 self._level_surface.blit(self._dead_overlay, self._clip_rect)
214
215 screen_surface.blit(self._level_surface, (0, 0), self._clip_rect)
216
217 def _draw_fox_status(self):
218 """Draw the fox inventory. The tails and the item are drawn on the
219 left side of the screen, a health bar and collected tofu and
220 scroll counts are shown on the right"""
221 # Convenience shortcuts
222 fox = self.game_state.world.fox
223 fox_hud = constants.FoxHud
224
225 # Inventory bg
226 bgsurf = pygame.Surface((fox_hud.INVENTORY_SIZE + 2 * 8,
227 fox_hud.INVENTORY_SIZE + 2 * 8),
228 flags=SRCALPHA)
229 bgsurf.fill((255, 255, 255, fox_hud.BG_ALPHA))
230 self._level_surface.blit(bgsurf, (self._clip_rect.left,
231 self._clip_rect.top + fox_hud.INVENTORY_START - 8))
232
233 # Draw inventory
234 my_item = fox.item
235 if my_item:
236 # Get image and resize it
237 if self._player.inventory_image is None:
238 self._player.make_inventory_image()
239 inv_pos = self._player.inventory_image.get_rect()
240 inv_pos.move_ip(self._clip_rect.left + 8,
241 self._clip_rect.top + fox_hud.INVENTORY_START)
242 if inv_pos.width < fox_hud.INVENTORY_SIZE:
243 inv_pos.left += (fox_hud.INVENTORY_SIZE - inv_pos.width) / 2
244 if inv_pos.height < fox_hud.INVENTORY_SIZE:
245 inv_pos.top += (fox_hud.INVENTORY_SIZE - inv_pos.height) / 2
246 self._level_surface.blit(self._player.inventory_image, inv_pos)
247
248 # Tail bg
249 bgsurf = pygame.Surface((fox_hud.TAILS_WIDTH + 2 * fox_hud.TAILS_BG_MARGIN,
250 fox_hud.TAILS_HEIGHT + 2 * fox_hud.TAILS_BG_MARGIN),
251 flags=SRCALPHA)
252 bgsurf.fill((255, 255, 255, fox_hud.BG_ALPHA))
253 self._level_surface.blit(bgsurf,
254 (self._clip_rect.left,
255 self._clip_rect.top
256 + fox_hud.TAIL_START
257 - fox_hud.TAILS_BG_MARGIN))
258
259 # Draw tails
260 for tail, position in constants.FoxHud.TAIL_POSITIONS.iteritems():
261 has_tail = tail in fox.tails
262 tail_pos = pygame.Rect(self._clip_rect.left + fox_hud.TAILS_BG_MARGIN,
263 self._clip_rect.top + fox_hud.TAIL_POSITIONS[tail],
264 0, 0)
265
266 imgs = self._tails[tail]
267 size = imgs[0].get_size()
268 if has_tail and tail in ('flight', 'invisibility'):
269 area = pygame.Rect(self._player.discharge_level(tail) * size[0], 0, size[0], size[1])
270 self._level_surface.blit(imgs[0], tail_pos)
271 self._level_surface.blit(imgs[1], tail_pos.move(area.left, 0), area)
272 elif has_tail and tail in ('fireball', 'lightning', 'shield'):
273 area = pygame.Rect(0, 0, self._player.recharge_level(tail) * size[0], size[1])
274 self._level_surface.blit(imgs[0], tail_pos)
275 self._level_surface.blit(imgs[1], tail_pos, area)
276 else:
277 self._level_surface.blit(imgs[int(has_tail)], tail_pos)
278
279 # Draw the health bar
280 health_bottom = self._clip_rect.right - 30, self._clip_rect.top + 200
281 bar = pygame.Rect(0, 0, fox_hud.HEALTH_WIDTH, fox_hud.HEALTH_HEIGHT)
282 bar.bottomleft = health_bottom
283 pygame.draw.rect(self._level_surface, fox_hud.HEALTH_BACKGROUND, bar)
284 bar.height = int(fox_hud.HEALTH_HEIGHT * float(max(0, fox.cur_health))/fox.max_health)
285 bar.bottomleft = health_bottom
286 pygame.draw.rect(self._level_surface, fox_hud.HEALTH_FOREGROUND, bar)
287
288 # Draw scroll count
289 pos = self._clip_rect.right - 35, self._clip_rect.top + fox_hud.SCROLL_TOP + 30
290 count = Text("%s/5" % len(fox.scrolls), pos)
291 count.draw(self._level_surface)
292 pos = self._clip_rect.right - 35, self._clip_rect.top + fox_hud.SCROLL_TOP
293 self._level_surface.blit(self._scroll, pos)
294
295 # Draw tofu count
296 pos = self._clip_rect.right - 42, self._clip_rect.top + fox_hud.TOFU_TOP + 28
297 count = Text("% 3s%%" % fox.tofu, pos)
298 count.draw(self._level_surface)
299 pos = self._clip_rect.right - 35, self._clip_rect.top + fox_hud.TOFU_TOP
300 self._level_surface.blit(self._tofu, pos)
301
302
303 def _update_clip_rect(self):
304 cr = self._clip_rect
305 lr = self._level_surface.get_rect()
306 cr.center = self._player.collide_rect.move(0, -level.TILE_SIZE[0]).midbottom
307 cr.clamp_ip(lr)
308
309
310 def _detect_key_sequence(self, ev):
311 if ev.key not in self._fast_key_map:
312 self._key_sequence = []
313 return False
314
315 if ev.type is KEYUP:
316 if (not self._key_sequence
317 or ev.key != self._key_sequence[-1]
318 or (time.time() - self._last_keydown_time) > constants.DOUBLE_TAP_TIME):
319 self._key_sequence = []
320 return False
321 self._last_keyup_time = time.time()
322 return False
323
324 if ev.type is KEYDOWN:
325 if self._last_keyup_time is None or (time.time() - self._last_keyup_time) > constants.DOUBLE_TAP_TIME:
326 self._key_sequence = []
327 self._key_sequence.append(ev.key)
328 self._last_keydown_time = time.time()
329 for seq in self._key_tap_map:
330 if seq == tuple(self._key_sequence[-len(seq):]):
331 self._key_sequence = self._key_sequence[-len(seq):]
332 return True
333 return False
334
335
336 def dispatch(self, ev):
337 if ev.type is KEYDOWN:
338
339 if self._player_dead:
340 if ev.key in self._restart_keys:
341 self._restart()
342 elif ev.key in self._quit_keys:
343 self._quit(False)
344 return
345
346 if self._dialogue:
347 self._dialogue.dispatch(ev)
348 return
349
350 if self._detect_key_sequence(ev):
351 action = self._key_tap_map.get(tuple(self._key_sequence))
352 if action is not None:
353 action()
354 if ev.key in self._fast_key_map:
355 self._fast_keys_down.add(ev.key)
356 action = self._slow_key_map.get(ev.key)
357 if action is not None:
358 action()
359
360 elif ev.type is KEYUP:
361 self._detect_key_sequence(ev)
362
363 if ev.key in self._fast_key_map:
364 self._fast_keys_down.discard(ev.key)
365
366 elif engine.PlayerDied.matches(ev):
367 self._player_dead = True
368 elif engine.OpenDialog.matches(ev):
369 self._open_dialogue(ev.npc)
370 elif engine.OpenNotification.matches(ev):
371 self._open_notification(ev.text)
372 elif engine.CloseDialog.matches(ev):
373 self._close_dialogue(ev.npc)
374 elif engine.AddSpriteEvent.matches(ev):
375 self._world.add(ev.item)
Note: See TracBrowser for help on using the repository browser.