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)