Mercurial > skaapsteker
view skaapsteker/levelscene.py @ 632:0675f390653c
Initial port to Python 3 and Pygame 2.
author | Simon Cross <hodgestar@gmail.com> |
---|---|
date | Fri, 20 Jan 2023 20:01:06 +0100 |
parents | da331c80ec08 |
children |
line wrap: on
line source
"""Scene wrapping a level object.""" from pygame.locals import (KEYDOWN, KEYUP, K_DOWN, K_ESCAPE, K_LEFT, K_RIGHT, K_SEMICOLON, K_UP, K_c, K_j, K_p, K_q, K_x, K_z, K_v, K_k, K_RETURN, K_SPACE, SRCALPHA) import pygame import time from . import options from . import engine from . import level from . import physics from . import constants from . import data from .sprites import player, base from .widgets.text import Text from .widgets.bubble import DialogueWidget, NotificationWidget from .utils import cadd, csub, cdiv class LevelScene(engine.Scene): def __init__(self, game_state, soundsystem, doorway_def=None): super(LevelScene, self).__init__(game_state, soundsystem) if doorway_def is not None: fox = self.game_state.world.fox fox.level, fox.doorway = doorway_def.split('.') self._level = level.Level(self.game_state.world.fox.level, soundsystem) self._player_dead = False self._dialogue = None self.setup_player() self._level_surface = self._level.get_surface() self._clip_rect = None self._world = physics.World(self._level_surface.get_rect()) self._paused = False # Prepare a Surface for displaying when Dead self._dead_overlay = pygame.Surface(constants.SCREEN, flags=SRCALPHA) self._dead_overlay.fill((255, 255, 255, 128)) death_text_pos = cdiv(constants.SCREEN, 2) death_text = Text("You've died.", death_text_pos, size=24) death_text.rect.move_ip(-death_text.rect.width / 2, -death_text.rect.height) death_text.draw(self._dead_overlay) death_text = Text("Press Escape to exit or Return to restart the level.", death_text_pos) death_text.rect.move_ip(-death_text.rect.width / 2, 0) death_text.draw(self._dead_overlay) # Helper images for hud self._tofu = data.load_image('icons/tofu.png') self._scroll = data.load_image('icons/haiku-scroll.png') self._tails = {} for tail in constants.FoxHud.TAIL_POSITIONS.keys(): image = data.load_image('icons/tails/%s.png' % tail) grey_image = data.load_image('icons/tails/g_%s.png' % tail) self._tails[tail] = (grey_image, image) for sprite in self._level.sprites: # XXX: NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO! if isinstance(sprite, base.Monster) or isinstance(sprite, base.CelestialDoorway): sprite.world = game_state.world self._world.add(sprite) npcs_and_items = game_state.create_sprites(self._level.name) self._npcs = dict((s.name, s) for s in npcs_and_items if hasattr(s, 'dsm')) for sprite in npcs_and_items: self._world.add(sprite) self._world.add(self._player) self._build_action_map() self._key_sequence = [] def setup_player(self): doorway = self._level.doorways[self.game_state.world.fox.doorway] self._player = player.Player(self.game_state.world, self._soundsystem) self._player.set_facing(doorway.facing) self._player.set_image() self._player.set_pos(doorway.get_tile_pos()) # Double tap stuff self._last_keydown_time = None self._last_keyup_time = None def _build_action_map(self): action = lambda s: getattr(self._player, 'action_%s' % s) self._quit_keys = set([K_q, K_ESCAPE]) self._restart_keys = set([K_x, K_z, K_RETURN, K_SPACE]) self._fast_key_map = { K_LEFT: action('left'), K_RIGHT: action('right'), K_UP: action('up'), } self._fast_keys_down = set() self._slow_key_map = { K_DOWN: action('down'), K_ESCAPE: self._quit, K_p: self._toggle_pause, } if options['dvorak']: self._slow_key_map[K_SEMICOLON] = action('fire1') self._slow_key_map[K_q] = action('fire2') self._slow_key_map[K_j] = action('transform') self._slow_key_map[K_k] = action('invisible') else: self._slow_key_map[K_x] = action('fire1') self._slow_key_map[K_z] = action('fire2') self._slow_key_map[K_c] = action('transform') self._slow_key_map[K_v] = action('invisible') self._slow_key_map[K_q] = self._quit self._key_tap_map = { (K_LEFT, K_LEFT) : action('double_left'), (K_RIGHT, K_RIGHT) : action('double_right'), (K_UP, K_UP) : action('double_up'), } def _quit(self, pause=True): from . import menuscene # avoid circular import if pause: engine.ChangeScene.post(menuscene.MenuScene(self.game_state, self._soundsystem)) else: # FIXME: When starting the game, we shoudl ensure we have sane # states if self._player_dead: self._player.restore() engine.ChangeScene.post(menuscene.MenuScene(self.game_state, self._soundsystem)) def _restart(self): if self._player_dead: self._player.restore() engine.ChangeScene.post(LevelScene(self.game_state, self._soundsystem)) def _toggle_pause(self): if self._paused: self._world.thaw() self._soundsystem.unpause() self._paused = False else: self._world.freeze() self._soundsystem.pause() self._paused = True def _open_dialogue(self, npc): if isinstance(npc, str): npc = self._npcs[npc] if npc.dsm.has_text(): if self._dialogue is not None: self._dialogue.close() self._world.freeze() self._dialogue = DialogueWidget(npc) else: self._close_dialogue(npc) def _open_notification(self, text): if self._dialogue is not None: self._dialogue.close() self._world.freeze() self._dialogue = NotificationWidget(text) def _close_dialogue(self, npc): if isinstance(npc, str): npc = self._npcs[npc] # below works for notifications too since they don't have .npc and send None if self._dialogue is not None and getattr(self._dialogue, 'npc', None) is npc: self._world.thaw() self._dialogue.close() self._dialogue = None def leave(self): """Freeze the scene, for serialization""" self._world.freeze() self._level.leave() def enter(self): """Unfreeze""" self._world.thaw() self._level.enter() def draw(self, screen_surface, engine): if self._clip_rect is None: self._clip_rect = pygame.Rect((0, 0), screen_surface.get_size()) if not self._paused and not self._dialogue: for key in self._fast_keys_down: self._fast_key_map[key]() self._world.update() self._update_clip_rect() self._level_surface.set_clip(self._clip_rect) self._level.draw(self._level_surface) self._world.draw(self._level_surface) if self._dialogue: self._dialogue.draw(self._level_surface) self._draw_fox_status() fps_text_pos = cadd(self._clip_rect.topleft, 10) fps_text = Text('FPS: %.1f' % engine.get_fps(), fps_text_pos, shadow='white') fps_text.draw(self._level_surface) if self._paused: paused_text_pos = csub(self._clip_rect.center, 10) paused_text = Text('Paused', paused_text_pos) paused_text.draw(self._level_surface) if self._player_dead: self._level_surface.blit(self._dead_overlay, self._clip_rect) screen_surface.blit(self._level_surface, (0, 0), self._clip_rect) def _draw_fox_status(self): """Draw the fox inventory. The tails and the item are drawn on the left side of the screen, a health bar and collected tofu and scroll counts are shown on the right""" # Convenience shortcuts fox = self.game_state.world.fox fox_hud = constants.FoxHud # Inventory bg bgsurf = pygame.Surface((fox_hud.INVENTORY_SIZE + 2 * 8, fox_hud.INVENTORY_SIZE + 2 * 8), flags=SRCALPHA) bgsurf.fill((255, 255, 255, fox_hud.BG_ALPHA)) self._level_surface.blit(bgsurf, (self._clip_rect.left, self._clip_rect.top + fox_hud.INVENTORY_START - 8)) # Draw inventory my_item = fox.item if my_item: # Get image and resize it if self._player.inventory_image is None: self._player.make_inventory_image() inv_pos = self._player.inventory_image.get_rect() inv_pos.move_ip(self._clip_rect.left + 8, self._clip_rect.top + fox_hud.INVENTORY_START) if inv_pos.width < fox_hud.INVENTORY_SIZE: inv_pos.left += (fox_hud.INVENTORY_SIZE - inv_pos.width) / 2 if inv_pos.height < fox_hud.INVENTORY_SIZE: inv_pos.top += (fox_hud.INVENTORY_SIZE - inv_pos.height) / 2 self._level_surface.blit(self._player.inventory_image, inv_pos) # Tail bg bgsurf = pygame.Surface((fox_hud.TAILS_WIDTH + 2 * fox_hud.TAILS_BG_MARGIN, fox_hud.TAILS_HEIGHT + 2 * fox_hud.TAILS_BG_MARGIN), flags=SRCALPHA) bgsurf.fill((255, 255, 255, fox_hud.BG_ALPHA)) self._level_surface.blit(bgsurf, (self._clip_rect.left, self._clip_rect.top + fox_hud.TAIL_START - fox_hud.TAILS_BG_MARGIN)) # Draw tails for tail, position in constants.FoxHud.TAIL_POSITIONS.items(): has_tail = tail in fox.tails tail_pos = pygame.Rect(self._clip_rect.left + fox_hud.TAILS_BG_MARGIN, self._clip_rect.top + fox_hud.TAIL_POSITIONS[tail], 0, 0) imgs = self._tails[tail] size = imgs[0].get_size() if has_tail and tail in ('flight', 'invisibility'): area = pygame.Rect(self._player.discharge_level(tail) * size[0], 0, size[0], size[1]) self._level_surface.blit(imgs[0], tail_pos) self._level_surface.blit(imgs[1], tail_pos.move(area.left, 0), area) elif has_tail and tail in ('fireball', 'lightning', 'shield'): area = pygame.Rect(0, 0, self._player.recharge_level(tail) * size[0], size[1]) self._level_surface.blit(imgs[0], tail_pos) self._level_surface.blit(imgs[1], tail_pos, area) else: self._level_surface.blit(imgs[int(has_tail)], tail_pos) # Draw the health bar health_bottom = self._clip_rect.right - 30, self._clip_rect.top + 200 bar = pygame.Rect(0, 0, fox_hud.HEALTH_WIDTH, fox_hud.HEALTH_HEIGHT) bar.bottomleft = health_bottom pygame.draw.rect(self._level_surface, fox_hud.HEALTH_BACKGROUND, bar) bar.height = int(fox_hud.HEALTH_HEIGHT * float(max(0, fox.cur_health))/fox.max_health) bar.bottomleft = health_bottom pygame.draw.rect(self._level_surface, fox_hud.HEALTH_FOREGROUND, bar) # Draw scroll count pos = self._clip_rect.right - 35, self._clip_rect.top + fox_hud.SCROLL_TOP + 30 count = Text("%s/5" % len(fox.scrolls), pos) count.draw(self._level_surface) pos = self._clip_rect.right - 35, self._clip_rect.top + fox_hud.SCROLL_TOP self._level_surface.blit(self._scroll, pos) # Draw tofu count pos = self._clip_rect.right - 42, self._clip_rect.top + fox_hud.TOFU_TOP + 28 count = Text("% 3s%%" % fox.tofu, pos) count.draw(self._level_surface) pos = self._clip_rect.right - 35, self._clip_rect.top + fox_hud.TOFU_TOP self._level_surface.blit(self._tofu, pos) def _update_clip_rect(self): cr = self._clip_rect lr = self._level_surface.get_rect() cr.center = self._player.collide_rect.move(0, -level.TILE_SIZE[0]).midbottom cr.clamp_ip(lr) def _detect_key_sequence(self, ev): if ev.key not in self._fast_key_map: self._key_sequence = [] return False if ev.type == KEYUP: if (not self._key_sequence or ev.key != self._key_sequence[-1] or (time.time() - self._last_keydown_time) > constants.DOUBLE_TAP_TIME): self._key_sequence = [] return False self._last_keyup_time = time.time() return False if ev.type == KEYDOWN: if self._last_keyup_time is None or (time.time() - self._last_keyup_time) > constants.DOUBLE_TAP_TIME: self._key_sequence = [] self._key_sequence.append(ev.key) self._last_keydown_time = time.time() for seq in self._key_tap_map: if seq == tuple(self._key_sequence[-len(seq):]): self._key_sequence = self._key_sequence[-len(seq):] return True return False def dispatch(self, ev): if ev.type == KEYDOWN: if self._player_dead: if ev.key in self._restart_keys: self._restart() elif ev.key in self._quit_keys: self._quit(False) return if self._dialogue: self._dialogue.dispatch(ev) return if self._detect_key_sequence(ev): action = self._key_tap_map.get(tuple(self._key_sequence)) if action is not None: action() if ev.key in self._fast_key_map: self._fast_keys_down.add(ev.key) action = self._slow_key_map.get(ev.key) if action is not None: action() elif ev.type == KEYUP: self._detect_key_sequence(ev) if ev.key in self._fast_key_map: self._fast_keys_down.discard(ev.key) elif engine.PlayerDied.matches(ev): self._player_dead = True elif engine.OpenDialog.matches(ev): self._open_dialogue(ev.npc) elif engine.OpenNotification.matches(ev): self._open_notification(ev.text) elif engine.CloseDialog.matches(ev): self._close_dialogue(ev.npc) elif engine.AddSpriteEvent.matches(ev): self._world.add(ev.item)