Beispiel #1
0
    def startup(self, *args, **kwargs):
        super(CombatTargetMenuState, self).startup(*args, **kwargs)

        # used to determine how player can target techniques
        self.user = kwargs.get("user")
        self.action = kwargs.get("action")
        self.player = kwargs.get("player")

        # load and scale the menu borders
        border = tools.load_and_scale(self.borders_filename)
        self.border = GraphicBox(border, None, None)
Beispiel #2
0
    def startup(self, **kwargs):
        super(MonsterMenuState, self).startup(**kwargs)

        # make a text area to show messages
        self.text_area = TextArea(self.font, self.font_color, (96, 96, 96))
        self.text_area.rect = pygame.Rect(
            tools.scale_sequence([20, 80, 80, 100]))
        self.sprites.add(self.text_area, layer=100)

        # Set up the border images used for the monster slots
        self.monster_slot_border = {}
        self.monster_portrait = pygame.sprite.Sprite()
        self.hp_bar = HpBar()

        # load and scale the monster slot borders
        root = "gfx/ui/monster/"
        border_types = ["empty", "filled", "active"]
        for border_type in border_types:
            filename = root + border_type + "_monster_slot_border.png"
            border = tools.load_and_scale(filename)

            filename = root + border_type + "_monster_slot_bg.png"
            background = tools.load_image(filename)

            window = GraphicBox(border, background, None)
            self.monster_slot_border[border_type] = window

        # TODO: something better than this global, load_sprites stuff
        for monster in self.game.player1.monsters:
            monster.load_sprites()
Beispiel #3
0
    def show_combat_dialog(self):
        """ Create and show the area where battle messages are displayed
        """
        # make the border and area at the bottom of the screen for messages
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = tools.load_and_scale(self.borders_filename)
        self.dialog_box = GraphicBox(border, None, self.background_color)
        self.dialog_box.rect = rect
        self.sprites.add(self.dialog_box, layer=100)

        # make a text area to show messages
        self.text_area = TextArea(self.font, self.font_color)
        self.text_area.rect = self.dialog_box.calc_inner_rect(self.dialog_box.rect)
        self.sprites.add(self.text_area, layer=100)
Beispiel #4
0
    def startup(self, *args, **kwargs):
        super(CombatTargetMenuState, self).startup(*args, **kwargs)

        # used to determine how player can target techniques
        self.user = kwargs.get("user")
        self.action = kwargs.get("action")
        self.player = kwargs.get("player")

        # load and scale the menu borders
        border = tools.load_and_scale(self.borders_filename)
        self.border = GraphicBox(border, None, None)
Beispiel #5
0
    def load_graphics(self):
        """ Loads all the graphical elements of the menu
            Will load some elements from disk, so needs to be called at least once.
        """
        # load and scale the _background
        background = None
        if self.background_filename:
            background = tools.load_image(self.background_filename)

        # load and scale the menu borders
        border = None
        if self.draw_borders:
            border = tools.load_and_scale(self.borders_filename)

        # set the helper to draw the _background
        self.window = GraphicBox(border, background, self.background_color)

        # handle the arrow cursor
        image = tools.load_and_scale(self.cursor_filename)
        self.arrow = MenuCursor(image)
Beispiel #6
0
    def show_combat_dialog(self):
        """ Create and show the area where battle messages are displayed
        """
        # make the border and area at the bottom of the screen for messages
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = tools.load_and_scale(self.borders_filename)
        self.dialog_box = GraphicBox(border, None, self.background_color)
        self.dialog_box.rect = rect
        self.sprites.add(self.dialog_box, layer=100)

        # make a text area to show messages
        self.text_area = TextArea(self.font, self.font_color)
        self.text_area.rect = self.dialog_box.calc_inner_rect(self.dialog_box.rect)
        self.sprites.add(self.text_area, layer=100)
Beispiel #7
0
    def load_graphics(self):
        """ Loads all the graphical elements of the menu
            Will load some elements from disk, so needs to be called at least once.
        """
        # load and scale the _background
        background = None
        if self.background_filename:
            background = tools.load_image(self.background_filename)

        # load and scale the menu borders
        border = None
        if self.draw_borders:
            border = tools.load_and_scale(self.borders_filename)

        # set the helper to draw the _background
        self.window = GraphicBox(border, background, self.background_color)

        # handle the arrow cursor
        image = tools.load_and_scale(self.cursor_filename)
        self.arrow = MenuCursor(image)
Beispiel #8
0
class Menu(state.State):
    """ A class to create menu objects.

    Menus are a type of game state.  Menus that are the top state
    will receive player input and respond to it.  They may be
    stacked, so that menus are nested.

    :background: String

    :ivar rect: The rect of the menu in pixels, defaults to 0, 0, 400, 200.
    :ivar state: An arbitrary state of the menu. E.g. "opening" or "closing".
    :ivar selected_index: The index position of the currently selected menu item.
    :ivar menu_items: A list of available menu items.
    """
    # defaults for the menu
    columns = 1
    min_font_size = 4
    draw_borders = True
    background = None                 # Image used to draw the background
    background_color = 248, 248, 248  # The window's background color
    background_filename = None        # File to load for image background
    menu_select_sound_filename = "sounds/interface/menu-select.ogg"
    font_filename = "resources/font/PressStart2P.ttf"
    borders_filename = "gfx/dialog-borders01.png"
    cursor_filename = "gfx/arrow.png"
    cursor_move_duration = .20
    default_character_delay = 0.05
    shrink_to_items = False    # fit the border to contents
    escape_key_exits = True    # escape key closes menu
    animate_contents = False   # show contents while window opens
    touch_aware = False        # if true, then menu items can be selected with the mouse/touch

    def startup(self, *items, **kwargs):
        self.rect = self.rect.copy()  # do not remove!
        self.selected_index = 0       # track which menu item is selected
        self.state = "closed"         # closed, opening, normal, disabled, closing
        self.window = None            # draws borders, background
        self._show_contents = False   # draw menu items, or not
        self._needs_refresh = False   # refresh layout on next draw
        self._anchors = dict()        # used to position the menu/state
        self.__dict__.update(kwargs)  # may be removed in the future

        # holds sprites representing menu items
        self.create_new_menu_items_group()

        self.set_font()          # load default font
        self.load_graphics()     # load default graphics
        self.reload_sounds()     # load default sounds

    def create_new_menu_items_group(self):
        """ Create a new group for menu items to be contained in

        Override if you need special placement for the menu items.

        :return: None
        """
        # contains the selectable elements of the menu
        self.menu_items = VisualSpriteList(parent=self.calc_menu_items_rect)
        self.menu_items.columns = self.columns

        # generally just for the cursor arrow
        self.menu_sprites = RelativeGroup(parent=self.menu_items)

    def shutdown(self):
        """ Clear objects likely to cause cyclical references

        :returns: None
        """
        self.sprites.empty()
        self.menu_items.empty()
        self.menu_sprites.empty()
        self.animations.empty()

        del self.arrow
        del self.menu_items
        del self.menu_sprites

    def start_text_animation(self, text_area):
        """ Start an animation to show textarea, one character at a time

        :param text_area: TextArea to animate
        :type text_area: core.components.ui.text.TextArea
        :rtype: None
        """

        def next_character():
            try:
                next(text_area)
            except StopIteration:
                pass
            else:
                self.task(next_character, self.character_delay)

        self.character_delay = self.default_character_delay
        self.remove_animations_of(next_character)
        next_character()

    def animate_text(self, text_area, text):
        """ Set and animate a text area

        :param text: Test to display
        :type text: basestring
        :param text_area: TextArea to animate
        :type text_area: core.components.ui.text.TextArea
        :rtype: None
        """
        text_area.text = text
        self.start_text_animation(text_area)

    def alert(self, message):
        """ Write a message to the first available text area

        Generally, a state will have just one, if any, text area.
        The first one found will be use to display the message.
        If no text area is found, a RuntimeError will be raised

        :param message: Something interesting, I hope.
        :type message: basestring

        :returns: None
        """

        def find_textarea():
            for sprite in self.sprites:
                if isinstance(sprite, TextArea):
                    return sprite
            logger.error("attempted to use 'alert' on state without a TextArea", message)
            raise RuntimeError

        self.animate_text(find_textarea(), message)

    def initialize_items(self):
        """ Advanced way to fill in menu items

        For menus that change dynamically, use of this method will
        make changes to the menu easier.

        :return:
        """
        pass

    def reload_items(self):
        """ Empty all items in the menu and re-add them

        Only works if initialize_items is used

        :return: None
        """
        self._needs_refresh = True
        items = self.initialize_items()
        if items:
            self.menu_items.empty()

            for item in items:
                self.add(item)

            number_items = len(self.menu_items)
            if self.menu_items and self.selected_index >= number_items:
                self.change_selection(number_items - 1)

    def build_item(self, label, callback, icon=None):
        """ Create a menu item and add it to the menu

        :param label: Some text
        :param icon: pygame surface (not used yet)
        :param callback: callback to use when selected
        :return: Menu Item
        """
        image = self.shadow_text(label)
        item = MenuItem(image, label, None, callback)
        self.add(item)

    def add(self, item):
        """ Add a menu item

        :type item: core.components.menu.MenuItem
        :return: None
        """
        self.menu_items.add(item)
        self._needs_refresh = True

    def fit_border(self):
        """ Resize the window border to fit the contents of the menu

        :return:
        """
        # get bounding box of menu items and the cursor
        center = self.rect.center
        rect1 = self.menu_items.calc_bounding_rect()
        rect2 = self.menu_sprites.calc_bounding_rect()
        rect1 = rect1.union(rect2)

        # expand the bounding box by the border and some padding
        # TODO: do not hardcode these values
        # border is 12, padding is the rest
        rect1.width += tools.scale(18)
        rect1.height += tools.scale(19)
        rect1.topleft = 0, 0

        # set our rect and adjust the centers to match
        self.rect = rect1
        self.rect.center = center

        # move the bounding box taking account the anchors
        self.position_rect()

    def reload_sounds(self):
        """ Reload sounds

        :returns: None
        """
        self.menu_select_sound = tools.load_sound(self.menu_select_sound_filename)

    def shadow_text(self, text, bg=(192, 192, 192)):
        """ Draw shadowed text

        :param text: Text to draw
        :param bg:
        :returns:
        """
        top = self.font.render(text, 1, self.font_color)
        shadow = self.font.render(text, 1, bg)

        offset = layout((0.5, 0.5))
        size = [int(math.ceil(a + b)) for a, b in zip(offset, top.get_size())]
        image = pygame.Surface(size, pygame.SRCALPHA)

        image.blit(shadow, offset)
        image.blit(top, (0, 0))
        return image

    def load_graphics(self):
        """ Loads all the graphical elements of the menu
            Will load some elements from disk, so needs to be called at least once.
        """
        if not self.transparent:
            # load and scale the _background
            background = None
            if self.background_filename:
                background = tools.load_image(self.background_filename)

            # load and scale the menu borders
            border = None
            if self.draw_borders:
                border = tools.load_and_scale(self.borders_filename)

            # set the helper to draw the _background
            self.window = GraphicBox(border, background, self.background_color)

        # handle the arrow cursor
        image = tools.load_and_scale(self.cursor_filename)
        self.arrow = MenuCursor(image)

    def show_cursor(self):
        """ Show the cursor that indicates the selected object

        :returns: None
        """
        if self.arrow not in self.menu_sprites:
            self.menu_sprites.add(self.arrow)
            self.trigger_cursor_update(False)
            self.get_selected_item().in_focus = True

    def hide_cursor(self):
        """ Hide the cursor that indicates the selected object

        :returns: None
        """
        if self.arrow in self.menu_sprites:
            self.menu_sprites.remove(self.arrow)
            selected = self.get_selected_item()
            if selected is not None:
                selected.in_focus = False

    def refresh_layout(self):
        """ Fit border to contents and hide/show cursor

        :return:
        """
        self.menu_items.expand = not self.shrink_to_items

        # check if we have items, but they are all disabled
        disabled = all(not i.enabled for i in self.menu_items)

        if self.menu_items and not disabled:
            self.show_cursor()
        else:
            self.hide_cursor()

        if self.shrink_to_items:
            self.fit_border()

    def draw(self, surface):
        """ Draws the menu object to a pygame surface.

        :param surface: Surface to draw on
        :type surface: pygame.Surface

        :rtype: None
        :returns: None

        """
        if self._needs_refresh:
            self.refresh_layout()
            self._needs_refresh = False

        if not self.transparent:
            self.window.draw(surface, self.rect)

        if self._show_contents:
            self.menu_items.draw(surface)
            self.menu_sprites.draw(surface)

        self.sprites.draw(surface)

        # debug = show the menu items area
        # surface.fill((255, 0, 0), self.calc_internal_rect(), 2)

    def set_font(self, size=5, font=None, color=(10, 10, 10), line_spacing=10):
        """Set the font properties that the menu uses including font _color, size, typeface,
        and line spacing.

        The size and line_spacing parameters will be adjusted the
        the screen scale.  You should pass the original, unscaled values.

        :param size: The font size in pixels.
        :param font: Path to the typeface file (.ttf)
        :param color: A tuple of the RGB _color values
        :param line_spacing: The spacing in pixels between lines of text

        :type size: Integer
        :type font: String
        :type color: Tuple
        :type line_spacing: Integer

        :rtype: None
        :returns: None

        .. image:: images/menu/set_font.png

        """
        if font is None:
            font = prepare.BASEDIR + self.font_filename

        if size < self.min_font_size:
            size = self.min_font_size

        self.line_spacing = tools.scale(line_spacing)
        self.font_size = tools.scale(size)
        self.font_color = color
        self.font = pygame.font.Font(font, self.font_size)

    def calc_internal_rect(self):
        """ Calculate the area inside the borders, if any.
        If no borders are present, a copy of the menu rect will be returned

        :returns: Rect representing space inside borders, if any
        :rtype: pygame.Rect
        """
        return self.window.calc_inner_rect(self.rect)

    def process_event(self, event):
        """ Process pygame input events

        The menu cursor is handled here, as well as the ESC and ENTER keys.

        This will also toggle the 'in_focus' of the menu item

        :param event: pygame.Event
        :returns: None
        """
        if event.type == pygame.KEYDOWN:

            # TODO: remove this check each time
            # check if we have items, but they are all disabled
            disabled = all(not i.enabled for i in self.menu_items)

            if self.escape_key_exits and event.key == pygame.K_ESCAPE:
                self.close()
                return

            elif self.state == "normal" and not disabled and self.menu_items:

                if event.key == pygame.K_RETURN:
                    self.menu_select_sound.play()
                    self.on_menu_selection(self.get_selected_item())

                else:
                    index = self.menu_items.determine_cursor_movement(self.selected_index, event)
                    if not self.selected_index == index:
                        self.change_selection(index)

        # TODO: handling of click/drag, miss-click, etc
        # TODO: eventually, maybe move some handling into menuitems
        # TODO: handle screen scaling?
        # TODO: generalized widget system
        elif self.touch_aware and event.type == pygame.MOUSEBUTTONDOWN:
            # menu items is (sometimes) a relative group, so their rect will be relative to their parent
            # we need to adjust the point to topleft of the containing rect
            # eventually, a widget system could do this automatically

            # make sure that the rect's position is current
            # a sprite group may not be a relative group... so an attribute error will be raised
            # obvi, a wart, but will be fixed sometime (tm)
            try:
                self.menu_items.update_rect_from_parent()
            except AttributeError:
                # not a relative group, no need to adjust cursor
                mouse_pos = event.pos
            else:
                # move the mouse/touch origin to be relative to the menu_items
                # TODO: a vector type would be niceeee
                mouse_pos = [a - b for a, b in zip(event.pos, self.menu_items.rect.topleft)]

            # loop through all the items here and see if they collide
            # eventually, we should make this more generic...not part of the menu
            for index, item in enumerate([i for i in self.menu_items if i.enabled]):
                if item.rect.collidepoint(mouse_pos):
                    self.change_selection(index)
                    self.on_menu_selection(self.get_selected_item())

    def change_selection(self, index, animate=True):
        """ Force the menu to be evaluated and move cursor and trigger focus changes

        :return: None
        """
        previous = self.get_selected_item()
        if previous is not None:
            previous.in_focus = False              # clear the focus flag of old item, if any
        self.selected_index = index                # update the selection index
        self.menu_select_sound.play()              # play a sound
        self.trigger_cursor_update(animate)        # move cursor and [maybe] animate it
        self.get_selected_item().in_focus = True   # set focus flag of new item
        self.on_menu_selection_change()            # let subclass know menu has changed

    def search_items(self, game_object):
        """ Non-optimised search through menu_items for a particular thing

        TODO: address the confusing name "game object"

        :param game_object:
        :return:
        """
        for menu_item in self.menu_items:
            if game_object == menu_item.game_object:
                return menu_item
        return None

    def trigger_cursor_update(self, animate=True):
        """ Force the menu cursor to move into the correct position

        :param animate: If True, then arrow will move smoothly into position
        :returns: None or Animation
        """
        selected = self.get_selected_item()
        if not selected:
            return

        x, y = selected.rect.midleft
        x -= tools.scale(2)

        if animate:
            self.remove_animations_of(self.arrow.rect)
            return self.animate(self.arrow.rect, right=x, centery=y, duration=self.cursor_move_duration)
        else:
            self.arrow.rect.midright = x, y
            return None

    def get_selected_item(self):
        """ Get the Menu Item that is currently selected

        :rtype: MenuItem
        :rtype: core.components.menu.interface.MenuItem
        """
        try:
            return self.menu_items[self.selected_index]
        except IndexError:
            return None

    def resume(self):
        if self.state == "closed":
            def show_items():
                self.state = "normal"
                self._show_contents = True
                self.on_menu_selection_change()
                self.on_open()

            self.state = "opening"
            self.reload_items()
            self.refresh_layout()

            ani = self.animate_open()
            if ani:
                if self.animate_contents:
                    self._show_contents = True
                    # TODO: make some "dirty" or invalidate layout API
                    # this will make sure items are arranged as menu opens
                    ani.update_callback = partial(setattr, self.menu_items, "_needs_arrange", True)
                ani.callback = show_items
            else:
                self.state = "normal"
                show_items()

        elif self.state == "normal":
            self.reload_items()
            self.refresh_layout()
            self.on_menu_selection_change()

    def close(self):
        if self.state in ["normal", "opening"]:
            self.state = "closing"
            ani = self.animate_close()
            if ani:
                ani.callback = self.game.pop_state
            else:
                self.game.pop_state()

    def anchor(self, attribute, value):
        """ Set an anchor for the menu window

        You can pass any string value that is used in a pygame rect,
        for example: "center", "topleft", and "right".

        When changes are made to the window or it is being opened
        or sized, then these values passed as anchors will override
        others.  The order of which each anchor is applied is not
        necessarily going to match the order they were set, as the
        implementation relies on a dictionary.

        Take care to make sure values do not overlap.

        :param attribute:
        :param value:
        :return:
        """
        if value is None:
            del self._anchors[attribute]
        else:
            self._anchors[attribute] = value

    def position_rect(self):
        """ Reposition rect taking in account the anchors
        """
        for attribute, value in self._anchors.items():
            setattr(self.rect, attribute, value)

    # ============================================================================
    #   The following methods are designed to be monkey patched or overloaded
    # ============================================================================

    def calc_menu_items_rect(self):
        """ Calculate the area inside the internal rect where items are listed

        :rtype: pygame.Rect
        """
        # WARNING: hardcoded values related to menu arrow size
        #          if menu arrow image changes, this should be adjusted
        cursor_margin = -tools.scale(11), -tools.scale(5)
        inner = self.calc_internal_rect()
        menu_rect = inner.inflate(*cursor_margin)
        menu_rect.bottomright = inner.bottomright
        return menu_rect

    def calc_final_rect(self):
        """ Calculate the area in the game window where menu is shown

        This value is the __desired__ location and size, and should not change
        over the lifetime of the menu.  It is used to generate animations
        to open the menu.

        The rect represents the size of the menu after all items are added.

        :rtype: pygame.Rect
        """
        original = self.rect.copy()    # store the original rect
        self.refresh_layout()          # arrange the menu
        rect = self.rect.copy()        # store the final rect
        self.rect = original           # set the original back
        return rect

    def on_open(self):
        """ Hook is called after opening animation has finished

        :return:
        """
        pass

    def on_menu_selection(self, item):
        """ Hook for things to happen when player selects a menu option

        Override in subclass, if you want to

        :return:
        """
        if item.enabled:
            item.game_object()

    def on_menu_selection_change(self):
        """ Hook for things to happen after menu selection changes

        Override in subclass

        :returns: None
        """
        pass

    def animate_open(self):
        """ Called when menu is going to open

        Menu will not receive input during the animation
        Menu will only play this animation once

        Must return either an Animation or Task to attach callback
        Only modify state of the menu Rect
        Do not change important state attributes

        :returns: Animation or Task
        :rtype: core.components.animation.Animation
        """
        return None

    def animate_close(self):
        """ Called when menu is going to open

        Menu will not receive input during the animation
        Menu will play animation only once
        Menu will be popped after animation finished

        Must return either an Animation or Task to attach callback
        Only modify state of the menu Rect
        Do not change important state attributes

        :returns: Animation or Task
        :rtype: core.components.animation.Animation
        """
        return None
Beispiel #9
0
class CombatState(CombatAnimations):
    """ The state-menu responsible for all combat related tasks and functions.
        .. image:: images/combat/monster_drawing01.png

    General description of this class:
        * implements a simple state machine
        * various phases are executed using a queue of actions
        * "decision queue" is used to queue player interactions/menus
        * this class holds mostly logic, though some graphical functions exist
        * most graphical functions are contained in "CombatAnimations" class

    Currently, status icons are implemented as follows:
       each round, all status icons are destroyed
       status icons are created for each status on each monster
       obvs, not ideal, maybe someday make it better? (see transition_phase)
    """
    background_filename = "gfx/ui/combat/battle_bg03.png"
    draw_borders = False
    escape_key_exits = False

    def startup(self, **kwargs):
        self.max_positions = 1  # TODO: make dependant on match type
        self.phase = None
        self.monsters_in_play = defaultdict(list)
        self._damage_map = defaultdict(
            set)  # track damage so experience can be awarded later
        self._technique_cache = dict()  # cache for technique animations
        self._decision_queue = list()  # queue for monsters that need decisions
        self._position_queue = list(
        )  # queue for asking players to add a monster into play (subject to change)
        self._action_queue = list(
        )  # queue for techniques, items, and status effects
        self._status_icons = list()  # list of sprites that are status icons
        self._monster_sprite_map = dict()  # monster => sprite
        self._hp_bars = dict()  # monster => hp bar
        self._layout = dict()  # player => home areas on screen
        self._animation_in_progress = False  # if true, delay phase change
        self._winner = None  # when set, combat ends
        self._round = 0

        super(CombatState, self).startup(**kwargs)
        self.players = list(self.players)
        self.show_combat_dialog()
        self.transition_phase("begin")
        self.task(partial(setattr, self, "phase", "ready"), 3)

    def update(self, time_delta):
        """ Update the combat state.  State machine is checked.

        General operation:
        * determine what phase to execute
        * if new phase, then run transition into new one
        * update the new phase, or the current one
        """
        super(CombatState, self).update(time_delta)
        if not self._animation_in_progress:
            new_phase = self.determine_phase(self.phase)
            if new_phase:
                self.phase = new_phase
                self.transition_phase(new_phase)
            self.update_phase()

    def draw(self, surface):
        super(CombatState, self).draw(surface)
        self.draw_hp_bars()

    def draw_hp_bars(self):
        """ Go through the HP bars and redraw them

        :returns: None
        """
        for monster, hud in self.hud.items():
            rect = pygame.Rect(0, 0, tools.scale(70), tools.scale(8))
            rect.right = hud.image.get_width() - tools.scale(8)
            rect.top += tools.scale(12)
            self._hp_bars[monster].draw(hud.image, rect)

    def determine_phase(self, phase):
        """ Determine the next phase and set it

        Part of state machine
        Only test and set new phase.
        * Do not execute phase actions
        * Try not to modify any values
        * Return a phase name and phase will change
        * Return None and phase will not change

        :returns: None or String
        """
        if phase == "ready":
            return "housekeeping phase"

        elif phase == "housekeeping phase":
            # this will wait for players to fill battleground positions
            for player in self.active_players:
                positions_available = self.max_positions - len(
                    self.monsters_in_play[player])
                if positions_available:
                    return

            return "decision phase"

        elif phase == "decision phase":
            # assume each monster executes one action
            # if number of actions == monsters, then all monsters are ready
            if len(self._action_queue) == len(self.active_monsters):
                return "pre action phase"

            # TODO: change check so that it doesn't change state
            # (state is changed because check_match_status will modify _winner)
            # if a player runs, it will be known here
            self.determine_winner()
            if self._winner:
                return "ran away"

        elif phase == "pre action phase":
            return "action phase"

        if phase == "action phase":
            if not self._action_queue:
                return "post action phase"

        elif phase == "post action phase":
            if not self._action_queue:
                return "resolve match"

        elif phase == "ran away":
            return "end combat"

        elif phase == "has winner":
            return "end combat"

        elif phase == "resolve match":
            if self._winner:
                return "has winner"
            else:
                return "housekeeping phase"

    def transition_phase(self, phase):
        """ Change from one phase from another.

        Part of state machine
        * Will be run just -once- when phase changes.
        * Do not change phase.
        * Execute code only to change into new phase.
        * The phase's update will be executed -after- this

        :param phase:
        :return:
        """
        if phase == "housekeeping phase":
            self._round += 1
            # fill all battlefield positions, but on round 1, don't ask
            self.fill_battlefield_positions(ask=self._round > 1)

        if phase == "decision phase":
            self.reset_status_icons()
            if not self._decision_queue:
                for player in self.human_players:
                    # the decision queue tracks human players who need to choose an
                    # action
                    self._decision_queue.extend(self.monsters_in_play[player])

                for trainer in self.ai_players:
                    for monster in self.monsters_in_play[trainer]:
                        opponents = self.monsters_in_play[self.players[0]]
                        action, target = monster.ai.make_decision(
                            monster, opponents)
                        self.enqueue_action(monster, action, target)

        elif phase == "action phase":

            self._action_queue.sort(key=attrgetter("user.speed"))
            # TODO: Running happens somewhere else, it should be moved here i think.
            # TODO: Sort other items not just healing, Swap/Run?

            #Create a new list for items, possibly running/swap
            #sort items by speed of monster applied to
            #remove items from action_queue and insert them into their new location
            precedent = []
            for action in self._action_queue:
                if action.technique.effect == 'heal':
                    precedent.append(action)
            #sort items by fastest target
            precedent.sort(key=attrgetter("target.speed"))
            for action in precedent:
                self._action_queue.remove(action)
                self._action_queue.insert(0, action)

        elif phase == "post action phase":
            # apply status effects to the monsters
            for monster in self.active_monsters:
                for technique in monster.status:
                    self.enqueue_action(None, technique, monster)

        elif phase == "resolve match":
            self.determine_winner()

        elif phase == "ran away":
            # after 3 seconds, push a state that blocks until enter is pressed
            # after the state is popped, the combat state will clean up and close
            # if you run in PvP, you need "defeated message"
            self.task(partial(self.game.push_state, "WaitForInputState"), 1)
            self.suppress_phase_change(1)

        elif phase == "has winner":
            if self._winner:
                # TODO: proper match check, etc
                if self._winner.name == "Maple":
                    self.alert(trans('combat_defeat'))
                else:
                    self.alert(trans('combat_victory'))

                # after 3 seconds, push a state that blocks until enter is pressed
                # after the state is popped, the combat state will clean up and close
                self.task(partial(self.game.push_state, "WaitForInputState"),
                          1)
                self.suppress_phase_change(1)

        elif phase == "end combat":
            self.end_combat()

    def update_phase(self):
        """ Execute/update phase actions

        Part of state machine
        * Do not change phase.
        * Will be run each iteration phase is active.
        * Do not test conditions to change phase.

        :return: None
        """
        if self.phase == "decision phase":
            # show monster action menu for human players
            if self._decision_queue:
                monster = self._decision_queue.pop()
                self.show_monster_action_menu(monster)

        elif self.phase == "action phase":
            self.handle_action_queue()

        elif self.phase == "post action phase":
            self.handle_action_queue()

    def handle_action_queue(self):
        """ Take one action from the queue and do it

        :return: None
        """
        if self._action_queue:
            action = self._action_queue.pop()
            self.perform_action(*action)
            self.check_party_hp()
            self.task(self.animate_party_status, 3)

    def ask_player_for_monster(self, player):
        """ Open dialog to allow player to choose a TXMN to enter into play

        :param player:
        :return:
        """
        def add(menuitem):
            monster = menuitem.game_object
            if monster.current_hp == 0:
                tools.open_dialog(self.game, [
                    trans("combat_fainted", parameters={"name": monster.name})
                ])
            elif monster in self.active_monsters:
                tools.open_dialog(self.game, [
                    trans("combat_isactive", parameters={"name": monster.name})
                ])
                msg = trans("combat_replacement_is_fainted")
                tools.open_dialog(self.game, [msg])
            else:
                self.add_monster_into_play(player, monster)
                self.game.pop_state()

        state = self.game.push_state("MonsterMenuState")
        # must use a partial because alert relies on a text box that may not exist
        # until after the state hs been startup
        state.task(partial(state.alert, trans("combat_replacement")), 0)
        state.on_menu_selection = add

    def fill_battlefield_positions(self, ask=False):
        """ Check the battlefield for unfilled positions and send out monsters

        :param ask: bool.  if True, then open dialog for human players
        :return:
        """
        # TODO: let work for trainer battles
        humans = list(self.human_players)

        # TODO: integrate some values for different match types
        released = False
        for player in self.active_players:
            positions_available = self.max_positions - len(
                self.monsters_in_play[player])
            if positions_available:
                available = get_awake_monsters(player)
                for i in range(positions_available):
                    released = True
                    if player in humans and ask:
                        self.ask_player_for_monster(player)
                    else:
                        self.add_monster_into_play(player, next(available))

        if released:
            self.suppress_phase_change()

    def add_monster_into_play(self, player, monster):
        """

        :param player:
        :param monster:
        :return:
        """
        # TODO: refactor some into the combat animations
        feet = list(self._layout[player]['home'][0].center)
        feet[1] += tools.scale(11)
        self.animate_monster_release_bottom(feet, monster)
        self.build_hud(self._layout[player]['hud'][0], monster)
        self.monsters_in_play[player].append(monster)

        # TODO: not hardcode
        if player is self.players[0]:
            self.alert(
                trans('combat_call_tuxemon', {"name": monster.name.upper()}))
        else:
            self.alert(
                trans('combat_wild_appeared', {"name": monster.name.upper()}))

    def reset_status_icons(self):
        """ Update/reset status icons for monsters

        TODO: caching, etc
        """
        # remove all status icons
        for s in self._status_icons:
            self.sprites.remove(s)

        # add status icons
        for monster in self.active_monsters:
            for status in monster.status:
                if status.icon:
                    # get the rect of the monster
                    rect = self._monster_sprite_map[monster].rect
                    # load the sprite and add it to the display
                    self.load_sprite(status.icon,
                                     layer=200,
                                     center=rect.topleft)

    def show_combat_dialog(self):
        """ Create and show the area where battle messages are displayed
        """
        # make the border and area at the bottom of the screen for messages
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = tools.load_and_scale(self.borders_filename)
        self.dialog_box = GraphicBox(border, None, self.background_color)
        self.dialog_box.rect = rect
        self.sprites.add(self.dialog_box, layer=100)

        # make a text area to show messages
        self.text_area = TextArea(self.font, self.font_color)
        self.text_area.rect = self.dialog_box.calc_inner_rect(
            self.dialog_box.rect)
        self.sprites.add(self.text_area, layer=100)

    def show_monster_action_menu(self, monster):
        """ Show the main window for choosing player actions

        :param monster: Monster to choose an action for
        :type monster: core.components.monster.Monster

        :returns: None
        """
        message = trans('combat_monster_choice', {"name": monster.name})
        self.alert(message)
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w // 2.5, h // 4)
        rect.bottomright = w, h

        state = self.game.push_state("MainCombatMenuState", columns=2)
        state.monster = monster
        state.rect = rect

    def skip_phase_change(self):
        """ Skip phase change animations

        Useful if player wants to skip a battle animation
        """
        for ani in self.animations:
            ani.finish()

    def enqueue_action(self, user, technique, target=None):
        """ Add some technique or status to the action queue

        :param user:
        :param technique:
        :param target:
        :returns: None
        """
        self._action_queue.append(EnqueuedAction(user, technique, target))

    def remove_monster_actions_from_queue(self, monster):
        """ Remove all queued actions for a particular monster

        This is used mainly for removing actions after monster is fainted

        :type monster: core.components.monster.Monster
        :returns: None
        """
        to_remove = set()
        for action in self._action_queue:
            if action.user is monster or action.target is monster:
                to_remove.add(action)
        [self._action_queue.remove(action) for action in to_remove]

    def suppress_phase_change(self, delay=3):
        """ Prevent the combat phase from changing for a limited time

        Use this function to prevent the phase from changing.  When
        animating elements of the phase, call this to prevent player
        input as well as phase changes.

        :param delay:
        :return:
        """
        if self._animation_in_progress:
            logger.debug("double suppress: bug?")
        else:
            self._animation_in_progress = True
            self.task(partial(setattr, self, "_animation_in_progress", False),
                      delay)

    def perform_action(self, user, technique, target=None):
        """ Do something with the thing: animated

        :param user:
        :param technique: Not a dict: a Technique or Item
        :param target:

        :returns:
        """
        technique.advance_round()

        # This is the time, in seconds, that the animation takes to finish.
        action_time = 3.0
        result = technique.use(user, target)

        try:
            tools.load_sound(technique.sfx).play()
        except AttributeError:
            pass

        # action is performed, so now use sprites to animate it
        # this value will be None if the target is off screen
        target_sprite = self._monster_sprite_map.get(target, None)

        # slightly delay the monster shake, so technique animation
        # is synchronized with the damage shake motion
        hit_delay = 0
        if user:
            message = trans('combat_used_x', {
                "user": user.name,
                "name": technique.name
            })

            # TODO: a real check or some params to test if should tackle, etc
            if result["should_tackle"]:
                hit_delay += .5
                user_sprite = self._monster_sprite_map[user]
                self.animate_sprite_tackle(user_sprite)

                if target_sprite:
                    self.task(
                        partial(self.animate_sprite_take_damage,
                                target_sprite), hit_delay + .2)
                    self.task(partial(self.blink, target_sprite),
                              hit_delay + .6)

                # Track damage
                self._damage_map[target].add(user)

            else:  # assume this was an item used
                # handle the capture device
                if result["name"] == "capture":
                    message += "\n" + trans('attempting_capture')
                    action_time = result["num_shakes"] + 1.8
                    self.animate_capture_monster(result["success"],
                                                 result["num_shakes"], target)
                    if result["success"]:  # end combat right here
                        self.task(self.end_combat, action_time +
                                  0.5)  # Display 'Gotcha!' first.
                        self.task(partial(self.alert, trans('gotcha')),
                                  action_time)
                        self._animation_in_progress = True
                        return

                # generic handling of anything else
                else:
                    if result["success"]:
                        message += "\n" + trans('item_success')
                    else:
                        message += "\n" + trans('item_failure')

            self.alert(message)
            self.suppress_phase_change(action_time)

        else:
            if result["success"]:
                self.suppress_phase_change()
                self.alert(
                    trans('combat_status_damage', {
                        "name": target.name,
                        "status": technique.name
                    }))

        if result["success"] and target_sprite and hasattr(
                technique, "images"):
            tech_sprite = self.get_technique_animation(technique)
            tech_sprite.rect.center = target_sprite.rect.center
            self.task(tech_sprite.image.play, hit_delay)
            self.task(partial(self.sprites.add, tech_sprite, layer=50),
                      hit_delay)
            self.task(tech_sprite.kill, 3)

    def faint_monster(self, monster):
        """ Instantly make the monster faint (will be removed later)

        :type monster: core.components.monster.Monster
        :returns: None
        """
        monster.current_hp = 0
        monster.status = [faint]
        """
        Experience is earned when the target monster is fainted.
        Any monsters who contributed any amount of damage will be awarded
        Experience is distributed evenly to all participants
        """
        if monster in self._damage_map:
            # Award Experience
            awarded_exp = monster.total_experience / monster.level / len(
                self._damage_map[monster])
            for winners in self._damage_map[monster]:
                winners.give_experience(awarded_exp)

            # Remove monster from damage map
            del self._damage_map[monster]

    def animate_party_status(self):
        """ Animate monsters that need to be fainted

        * Animation to remove monster is handled here
        TODO: check for faint status, not HP

        :returns: None
        """
        for player in self.monsters_in_play.keys():
            for monster in self.monsters_in_play[player]:
                if fainted(monster):
                    self.alert(trans('combat_fainted', {"name": monster.name}))
                    self.animate_monster_faint(monster)
                    self.suppress_phase_change(3)

    def check_party_hp(self):
        """ Apply status effects, then check HP, and party status

        * Monsters will be removed from play here

        :returns: None
        """
        for player in self.monsters_in_play.keys():
            for monster in self.monsters_in_play[player]:
                self.animate_hp(monster)
                if monster.current_hp <= 0 and not fainted(monster):
                    self.remove_monster_actions_from_queue(monster)
                    self.faint_monster(monster)

    def get_technique_animation(self, technique):
        """ Return a sprite usable as a technique animation

        TODO: move to some generic animation loading thingy

        :type technique: core.components.technique.Technique
        :rtype: core.components.sprite.Sprite
        """
        try:
            return self._technique_cache[technique]
        except KeyError:
            sprite = self.load_technique_animation(technique)
            self._technique_cache[technique] = sprite
            return sprite

    @staticmethod
    def load_technique_animation(technique):
        """

        TODO: move to some generic animation loading thingy

        :param technique:
        :rtype: core.components.sprite.Sprite
        """
        frame_time = .09
        images = list()
        for fn in technique.images:
            image = tools.load_and_scale(fn)
            images.append((image, frame_time))

        tech = PygAnimation(images, False)
        sprite = Sprite()
        sprite.image = tech
        sprite.rect = tech.get_rect()
        return sprite

    @property
    def active_players(self):
        """ Generator of any non-defeated players/trainers

        :rtype: collections.Iterable[core.components.player.Player]
        """
        for player in self.players:
            if not defeated(player):
                yield player

    @property
    def human_players(self):
        for player in self.players:
            if player.isplayer:
                yield player

    @property
    def ai_players(self):
        for player in set(self.active_players) - set(self.human_players):
            yield player

    @property
    def active_monsters(self):
        """ List of any non-defeated monsters on battlefield

        :rtype: list
        """
        return list(chain.from_iterable(self.monsters_in_play.values()))

    def remove_player(self, player):
        # TODO: non SP things
        self.players.remove(player)
        self.suppress_phase_change()
        self.alert(trans('combat_player_run'))

    def determine_winner(self):
        """ Determine if match should continue or not

        :return:
        """
        if self._winner:
            return

        players = list(self.active_players)
        if len(players) == 1:
            self._winner = players[0]

    def end_combat(self):
        """ End the combat
        """
        # TODO: End combat differently depending on winning or losing

        # clear action queue
        self._action_queue = list()

        contexts = {}
        event_engine = self.game.event_engine
        fadeout_action = namedtuple("action", ["type", "parameters"])
        fadeout_action.type = "fadeout_music"
        fadeout_action.parameters = [1000]
        event_engine.actions["fadeout_music"]["method"](self.game,
                                                        fadeout_action,
                                                        contexts)
        for key in contexts:
            contexts[key].execute(game)

        # remove any menus that may be on top of the combat state
        while self.game.current_state is not self:
            self.game.pop_state()

        self.game.push_state("FadeOutTransition", caller=self)
Beispiel #10
0
class CombatTargetMenuState(Menu):
    """
    Menu for selecting targets of techniques and items

    This special menu draws over the combat screen
    """
    transparent = True

    def create_new_menu_items_group(self):
        # these groups will not automatically position the sprites
        self.menu_items = MenuSpriteGroup()
        self.menu_sprites = SpriteGroup()

    def startup(self, *args, **kwargs):
        super(CombatTargetMenuState, self).startup(*args, **kwargs)

        # used to determine how player can target techniques
        self.user = kwargs.get("user")
        self.action = kwargs.get("action")
        self.player = kwargs.get("player")

        # load and scale the menu borders
        border = tools.load_and_scale(self.borders_filename)
        self.border = GraphicBox(border, None, None)

    def initialize_items(self):
        # get a ref to the combat state
        combat_state = self.game.get_state_name("CombatState")

        # TODO: trainer targeting
        # TODO: cleanup how monster sprites and whatnot are managed
        # TODO: This is going to work fine for simple matches, but controls will be wonky for parties
        # TODO: (cont.) Need better handling of cursor keys for 2d layouts of menu items
        # get all the monster positions

        # this is used to determine who owns what monsters and what not
        # TODO: make less duplication of game data in memory, let combat state have more registers, etc
        self.targeting_map = defaultdict(list)

        for player, monsters in combat_state.monsters_in_play.items():
            for monster in monsters:

                # TODO: more targeting classes
                if player == self.player:
                    targeting_class = "own monster"
                else:
                    targeting_class = "enemy monster"

                self.targeting_map[targeting_class].append(monster)

                # TODO: handle odd cases where a situation creates no valid targets
                # if this target type is not handled by this action, then skip it
                if targeting_class not in self.action.target:
                    continue

                # inspect the monster sprite and make a border image for it
                sprite = combat_state._monster_sprite_map[monster]
                item = MenuItem(None, None, None, monster)
                item.rect = sprite.rect.copy()
                center = item.rect.center
                item.rect.inflate_ip(tools.scale(16), tools.scale(16))
                item.rect.center = center

                yield item

    def refresh_layout(self):
        """ Before refreshing the layout, determine the optimal target

        :return:
        """

        def determine_target():
            for tag in self.action.target:
                for target in self.targeting_map[tag]:
                    menu_item = self.search_items(target)
                    if menu_item.enabled:
                        # TODO: some API for this mess
                        # get the index of the menu_item
                        # change it
                        index = self.menu_items._spritelist.index(menu_item)
                        self.selected_index = index
                        return

        determine_target()
        super(CombatTargetMenuState, self).refresh_layout()

    def on_menu_selection_change(self):
        """ Draw borders around sprites when selection changes

        :return:
        """
        # clear out the old borders
        for sprite in self.menu_items:
            sprite.image = None

        # find the selected item and make a border for it
        item = self.get_selected_item()
        if item:
            item.image = pygame.Surface(item.rect.size, pygame.SRCALPHA)
            self.border.draw(item.image)
Beispiel #11
0
class Menu(state.State):
    """A class to create menu objects.

    :background: String

    :ivar rect: The rect of the menu in pixels, defaults to 0, 0, 400, 200.
    :ivar state: An arbitrary state of the menu. E.g. "opening" or "closing".
    :ivar selected_index: The index position of the currently selected menu item.
    :ivar menu_items: A list of available menu items.
    """
    # defaults for the menu
    columns = 1
    min_font_size = 4
    draw_borders = True
    background = None
    background_color = 248, 248, 248  # The window's background 	olor
    background_filename = None
    menu_select_sound_filename = "sounds/interface/menu-select.ogg"
    font_filename = "resources/font/PressStart2P.ttf"
    borders_filename = "gfx/dialog-borders01.png"
    cursor_filename = "gfx/arrow.png"
    cursor_move_duration = .20
    default_character_delay = 0.05
    shrink_to_items = False
    escape_key_exits = True

    def startup(self, *items, **kwargs):
        self.rect = self.rect.copy()  # do not remove!

        self.__dict__.update(kwargs)

        # used to position the menu/state
        self._anchors = dict()

        # holds sprites representing menu items
        self.menu_items = VisualSpriteList(parent=self.calc_menu_items_rect)
        self.menu_items.columns = self.columns
        if self.shrink_to_items:
            self.menu_items.expand = False

        # generally just for the cursor arrow
        self.menu_sprites = RelativeGroup(parent=self.menu_items)

        self.selected_index = 0  # Used to track which menu item is selected
        self.state = "closed"    # closed, opening, normal, closing

        self.set_font()       # load default font
        self.load_graphics()  # load default graphics
        self.reload_sounds()  # load default sounds

    def shutdown(self):
        """ Clear objects likely to cause cyclical references

        :returns: None
        """
        self.sprites.empty()
        self.menu_items.empty()
        self.menu_sprites.empty()
        self.animations.empty()

        del self.arrow
        del self.menu_items
        del self.menu_sprites

    def start_text_animation(self, text_area):
        """ Start an animation to show textarea, one character at a time

        :param text_area: TextArea to animate
        :type text_area: core.components.ui.text.TextArea
        :rtype: None
        """
        def next_character():
            try:
                next(text_area)
            except StopIteration:
                pass
            else:
                self.task(next_character, self.character_delay)

        self.character_delay = self.default_character_delay
        self.remove_animations_of(next_character)
        next_character()

    def animate_text(self, text_area, text):
        """ Set and animate a text area

        :param text: Test to display
        :type text: basestring
        :param text_area: TextArea to animate
        :type text_area: core.components.ui.text.TextArea
        :rtype: None
        """
        text_area.text = text
        self.start_text_animation(text_area)

    def alert(self, message):
        """ Write a message to the first available text area

        Generally, a state will have just one, if any, text area.
        The first one found will be use to display the message.
        If no text area is found, a RuntimeError will be raised

        :param message: Something interesting, I hope.
        :type message: basestring

        :returns: None
        """
        def find_textarea():
            for sprite in self.sprites:
                if isinstance(sprite, TextArea):
                    return sprite
            print("attempted to use 'alert' on state without a TextArea", message)
            raise RuntimeError

        self.animate_text(find_textarea(), message)

    def _initialize_items(self):
        """ Internal use only.  Will reset the items in the menu

        Reset the menu items and get new updated ones.

        :rtype: collections.Iterable[MenuItem]
        """
        self.selected_index = 0
        self.menu_items.empty()
        for item in self.initialize_items():
            self.menu_items.add(item)
        if self.menu_items:
            self.show_cursor()

        # call item selection change to trigger callback for first time
        self.on_menu_selection_change()

        if self.shrink_to_items:
            center = self.rect.center
            rect1 = self.menu_items.calc_bounding_rect()
            rect2 = self.menu_sprites.calc_bounding_rect()
            rect1 = rect1.union(rect2)

            # TODO: do not hardcode these values
            # border is 12, padding is the rest
            rect1.width += tools.scale(18)
            rect1.height += tools.scale(19)
            rect1.topleft = 0, 0

            # self.rect = rect1.union(rect2)
            # self.rect.width += tools.scale(20)
            # self.rect.topleft = 0, 0
            self.rect = rect1
            self.rect.center = center
            self.position_rect()

    def reload_sounds(self):
        """ Reload sounds

        :returns: None
        """
        self.menu_select_sound = tools.load_sound(self.menu_select_sound_filename)

    def shadow_text(self, text, bg=(192, 192, 192)):
        """ Draw shadowed text

        :param text: Text to draw
        :param bg:
        :returns:
        """
        top = self.font.render(text, 1, self.font_color)
        shadow = self.font.render(text, 1, bg)

        offset = layout((0.5, 0.5))
        size = [int(math.ceil(a + b)) for a, b in zip(offset, top.get_size())]
        image = pygame.Surface(size, pygame.SRCALPHA)

        image.blit(shadow, offset)
        image.blit(top, (0, 0))
        return image

    def load_graphics(self):
        """ Loads all the graphical elements of the menu
            Will load some elements from disk, so needs to be called at least once.
        """
        # load and scale the _background
        background = None
        if self.background_filename:
            background = tools.load_image(self.background_filename)

        # load and scale the menu borders
        border = None
        if self.draw_borders:
            border = tools.load_and_scale(self.borders_filename)

        # set the helper to draw the _background
        self.window = GraphicBox(border, background, self.background_color)

        # handle the arrow cursor
        image = tools.load_and_scale(self.cursor_filename)
        self.arrow = MenuCursor(image)

    def show_cursor(self):
        """ Show the cursor that indicates the selected object

        :returns: None
        """
        if self.arrow not in self.menu_sprites:
            self.menu_sprites.add(self.arrow)
            self.trigger_cursor_update(False)
            self.get_selected_item().in_focus = True

    def hide_cursor(self):
        """ Hide the cursor that indicates the selected object

        :returns: None
        """
        self.menu_sprites.remove(self.arrow)
        self.get_selected_item().in_focus = False

    def draw(self, surface):
        """ Draws the menu object to a pygame surface.

        :param surface: Surface to draw on
        :type surface: pygame.Surface

        :rtype: None
        :returns: None

        """
        self.window.draw(surface, self.rect)

        if self.state == "normal" and self.menu_items:
            self.menu_items.draw(surface)
            self.menu_sprites.draw(surface)

        self.sprites.draw(surface)

        # debug = show the menu items area
        # surface.fill((255, 0, 0), self.calc_internal_rect(), 2)

    def set_font(self, size=5, font=None, color=(10, 10, 10), line_spacing=10):
        """Set the font properties that the menu uses including font _color, size, typeface,
        and line spacing.

        The size and line_spacing parameters will be adjusted the
        the screen scale.  You should pass the original, unscaled values.

        :param size: The font size in pixels.
        :param font: Path to the typeface file (.ttf)
        :param color: A tuple of the RGB _color values
        :param line_spacing: The spacing in pixels between lines of text

        :type size: Integer
        :type font: String
        :type color: Tuple
        :type line_spacing: Integer

        :rtype: None
        :returns: None

        .. image:: images/menu/set_font.png

        """
        if font is None:
            font = prepare.BASEDIR + self.font_filename

        if size < self.min_font_size:
            size = self.min_font_size

        self.line_spacing = tools.scale(line_spacing)
        self.font_size = tools.scale(size)
        self.font_color = color
        self.font = pygame.font.Font(font, self.font_size)

    def calc_internal_rect(self):
        """ Calculate the area inside the borders, if any.
        If no borders are present, a copy of the menu rect will be returned

        :returns: Rect representing space inside borders, if any
        :rtype: pygame.Rect
        """
        return self.window.calc_inner_rect(self.rect)

    def process_event(self, event):
        """ Process pygame input events

        The menu cursor is handled here, as well as the ESC and ENTER keys.

        This will also toggle the 'in_focus' of the menu item

        :param event: pygame.Event
        :returns: None
        """
        if event.type == pygame.KEYDOWN:
            if self.escape_key_exits and event.key == pygame.K_ESCAPE:
                self.close()
                return

            elif self.state == "normal" and self.menu_items and event.key == pygame.K_RETURN:
                self.menu_select_sound.play()
                self.on_menu_selection(self.get_selected_item())
                return

        # check if cursor has changed
        index = self.menu_items.determine_cursor_movement(self.selected_index, event)
        if not self.selected_index == index:
            self.get_selected_item().in_focus = False  # clear the focus flag of old item
            self.selected_index = index                # update the selection index
            self.menu_select_sound.play()
            self.trigger_cursor_update()               # move cursor and animate it
            self.get_selected_item().in_focus = True   # set focus flag of new item
            self.on_menu_selection_change()            # let subclass know menu has changed

    def trigger_cursor_update(self, animate=True):
        """ Force the menu cursor to move into the correct position

        :param animate: If True, then arrow will move smoothly into position
        :returns: None or Animation
        """
        selected = self.get_selected_item()
        if not selected:
            return

        x, y = selected.rect.midleft
        x -= tools.scale(2)

        if animate:
            self.remove_animations_of(self.arrow.rect)
            return self.animate(self.arrow.rect, right=x, centery=y, duration=self.cursor_move_duration)
        else:
            self.arrow.rect.midright = x, y
            return None

    def get_selected_item(self):
        """ Get the Menu Item that is currently selected

        :rtype: MenuItem
        :rtype: core.components.menu.interface.MenuItem
        """
        try:
            return self.menu_items[self.selected_index]
        except IndexError:
            return None

    def resume(self):
        if self.state == "closed":
            def show_items():
                self.state = "normal"
                self.on_open()

            self._initialize_items()
            self.state = "opening"
            self.position_rect()
            ani = self.animate_open()
            if ani:
                ani.callback = show_items
            else:
                show_items()

    def close(self):
        if self.state in ["normal", "opening"]:
            self.state = "closing"
            ani = self.animate_close()
            if ani:
                ani.callback = self.game.pop_state
            else:
                self.game.pop_state()

    def anchor(self, attribute, value):
        """ Set an anchor for the menu window

        You can pass any string value that is used in a pygame rect,
        for example: "center", "topleft", and "right.

        When changes are made to the window, when it is being opened
        or sized, then these values passed as anchors will override
        others.  The order of which each anchor is applied is not
        necessarily going to match the order they were set, as the
        implementation relies on a dictionary.

        Take care to make sure values do not overlap,

        :param attribute:
        :param value:
        :return:
        """
        if value is None:
            del self._anchors[attribute]
        else:
            self._anchors[attribute] = value

    def position_rect(self):
        """ Reposition rect taking in account the anchors
        """
        for attribute, value in self._anchors.items():
            setattr(self.rect, attribute, value)

    def update(self, time_delta):
        self.animations.update(time_delta)

    # ============================================================================
    #   The following methods are designed to be monkey patched or overloaded
    # ============================================================================

    def calc_menu_items_rect(self):
        """ Calculate the area inside the internal rect where items are listed

        :rtype: pygame.Rect
        """
        # WARNING: hardcoded values related to menu arrow size
        #          if menu arrow image changes, this should be adjusted
        cursor_margin = -tools.scale(11), -tools.scale(5)
        inner = self.calc_internal_rect()
        menu_rect = inner.inflate(*cursor_margin)
        menu_rect.bottomright = inner.bottomright
        return menu_rect

    def calc_final_rect(self):
        """ Calculate the area in the game window where menu is shown

        This value is the __desired__ location, and should not change
        over the lifetime of the menu.  It is used to generate animations
        to open the menu.

        By default, this will be the entire screen

        :rtype: pygame.Rect
        """
        return self.rect

    def on_open(self):
        """ Hook is called after opening animation has finished

        :return:
        """
        pass

    def on_menu_selection(self, item):
        """ Hook for things to happen when player selects a menu option

        Override in subclass

        :return:
        """
        pass

    def on_menu_selection_change(self):
        """ Hook for things to happen after menu selection changes

        Override in subclass

        :returns: None
        """
        pass

    def initialize_items(self):
        """ Hook for adding items to menu when menu is created

        Override with a generator

        :returns: Generator of MenuItems
        :rtype: collections.Iterable[MenuItem]
        """
        return iter(())

    def animate_open(self):
        """ Called when menu is going to open

        Menu will not receive input during the animation
        Menu will only play this animation once

        Must return either an Animation or Task to attach callback
        Only modify state of the menu Rect
        Do not change important state attributes

        :returns: Animation or Task
        :rtype: core.components.animation.Animation
        """
        return None

    def animate_close(self):
        """ Called when menu is going to open

        Menu will not receive input during the animation
        Menu will play animation only once
        Menu will be popped after animation finished

        Must return either an Animation or Task to attach callback
        Only modify state of the menu Rect
        Do not change important state attributes

        :returns: Animation or Task
        :rtype: core.components.animation.Animation
        """
        return None
Beispiel #12
0
 def load_graphics(self):
     """ Image become class attribute, so is shared.
         Eventually, implement some game-wide image caching
     """
     image = tools.load_and_scale(self.border_filename)
     HpBar.border = GraphicBox(image)
Beispiel #13
0
class CombatState(CombatAnimations):
    """ The state-menu responsible for all combat related tasks and functions.
        .. image:: images/combat/monster_drawing01.png

    General description of this class:
        * implements a simple state machine
        * various phases are executed using a queue of actions
        * "decision queue" is used to queue player interactions/menus
        * this class holds mostly logic, though some graphical functions exist
        * most graphical functions are contained in "CombatAnimations" class

    Currently, status icons are implemented as follows:
       each round, all status icons are destroyed
       status icons are created for each status on each monster
       obvs, not ideal, maybe someday make it better? (see transition_phase)
    """
    background_filename = "gfx/ui/combat/battle_bg03.png"
    draw_borders = False
    escape_key_exits = False

    def startup(self, **kwargs):
        self.max_positions = 1  # TODO: make dependant on match type
        self.phase = None
        self.monsters_in_play = defaultdict(list)
        self._damage_map = defaultdict(set)  # track damage so experience can be awarded later
        self._technique_cache = dict()  # cache for technique animations
        self._decision_queue = list()  # queue for monsters that need decisions
        self._position_queue = list()  # queue for asking players to add a monster into play (subject to change)
        self._action_queue = list()  # queue for techniques, items, and status effects
        self._status_icons = list()  # list of sprites that are status icons
        self._monster_sprite_map = dict()  # monster => sprite
        self._hp_bars = dict()  # monster => hp bar
        self._layout = dict()  # player => home areas on screen
        self._animation_in_progress = False  # if true, delay phase change
        self._round = 0

        super(CombatState, self).startup(**kwargs)
        self.players = list(self.players)
        self.show_combat_dialog()
        self.transition_phase("begin")
        self.task(partial(setattr, self, "phase", "ready"), 3)

    def update(self, time_delta):
        """ Update the combat state.  State machine is checked.

        General operation:
        * determine what phase to update
        * if new phase, then run transition into new one
        * update the new phase, or the current one
        """
        super(CombatState, self).update(time_delta)
        if not self._animation_in_progress:
            new_phase = self.determine_phase(self.phase)
            if new_phase:
                self.phase = new_phase
                self.transition_phase(new_phase)
            self.update_phase()

    def draw(self, surface):
        super(CombatState, self).draw(surface)
        self.draw_hp_bars()

    def draw_hp_bars(self):
        """ Go through the HP bars and redraw them

        :returns: None
        """
        for monster, hud in self.hud.items():
            rect = pygame.Rect(0, 0, tools.scale(70), tools.scale(8))
            rect.right = hud.image.get_width() - tools.scale(8)
            rect.top += tools.scale(12)
            self._hp_bars[monster].draw(hud.image, rect)

    def determine_phase(self, phase):
        """ Determine the next phase and set it

        Part of state machine
        Only test and set new phase.
        * Do not update phase actions
        * Try not to modify any values
        * Return a phase name and phase will change
        * Return None and phase will not change

        :returns: None or String
        """
        if phase == "ready":
            return "housekeeping phase"

        elif phase == "housekeeping phase":
            # this will wait for players to fill battleground positions
            for player in self.active_players:
                positions_available = self.max_positions - len(self.monsters_in_play[player])
                if positions_available:
                    return

            # DO NOT REMOVE THIS CODE
            # enable it to test for draw matches
            if 0:
                t = Technique("technique_poison_sting")
                for p, m in self.monsters_in_play.items():
                    for m in m:
                        m.current_hp = min(m.current_hp, 1)
                        t.use(m, m)

            # enable to test for defeat in matches
            if 0:
                [setattr(m, 'current_hp', 1) for m in self.players[0].monsters]

            return "decision phase"

        elif phase == "decision phase":
            # TODO: only works for single player and if player runs
            if len(self.remaining_players) == 1:
                return "ran away"

            # assume each monster executes one action
            # if number of actions == monsters, then all monsters are ready
            if len(self._action_queue) == len(self.active_monsters):
                return "pre action phase"

        elif phase == "pre action phase":
            return "action phase"

        if phase == "action phase":
            if not self._action_queue:
                return "post action phase"

        elif phase == "post action phase":
            if not self._action_queue:
                return "resolve match"

        elif phase == "resolve match":
            remaining = len(self.remaining_players)

            if remaining == 0:
                return "draw match"
            elif remaining == 1:
                return "has winner"
            else:
                return "housekeeping phase"

        elif phase == "ran away":
            return "end combat"

        elif phase == "draw match":
            return "end combat"

        elif phase == "has winner":
            return "end combat"

    def transition_phase(self, phase):
        """ Change from one phase from another.

        Part of state machine
        * Will be run just -once- when phase changes.
        * Do not change phase.
        * Execute code only to change into new phase.
        * The phase's update will be executed -after- this

        :param phase:
        :return:
        """
        if phase == "housekeeping phase":
            self._round += 1
            # fill all battlefield positions, but on round 1, don't ask
            self.fill_battlefield_positions(ask=self._round > 1)

        if phase == "decision phase":
            self.reset_status_icons()
            if not self._decision_queue:
                for player in self.human_players:
                    # the decision queue tracks human players who need to choose an
                    # action
                    self._decision_queue.extend(self.monsters_in_play[player])

                for trainer in self.ai_players:
                    for monster in self.monsters_in_play[trainer]:
                        action = self.get_combat_decision_from_ai(monster)
                        self._action_queue.append(action)

        elif phase == "action phase":
            self.sort_action_queue()

        elif phase == "post action phase":
            # apply status effects to the monsters
            for monster in self.active_monsters:
                for technique in monster.status:
                    self.enqueue_action(None, technique, monster)

        elif phase == "resolve match":
            pass

        elif phase == "ran away":
            self.alert(trans('combat_player_run'))

            # after 3 seconds, push a state that blocks until enter is pressed
            # after the state is popped, the combat state will clean up and close
            # if you run in PvP, you need "defeated message"
            self.task(partial(self.game.push_state, "WaitForInputState"), 2)
            self.suppress_phase_change(3)

        elif phase == "draw match":
            # it is a draw match; both players were defeated in same round
            self.alert(trans('combat_draw'))

            # after 3 seconds, push a state that blocks until enter is pressed
            # after the state is popped, the combat state will clean up and close
            self.task(partial(self.game.push_state, "WaitForInputState"), 2)
            self.suppress_phase_change(3)

        elif phase == "has winner":
            # TODO: proper match check, etc
            # This assumes that player[0] is the human playing in single player
            if self.remaining_players[0] == self.players[0]:
                self.alert(trans('combat_victory'))
            else:
                self.alert(trans('combat_defeat'))

            # after 3 seconds, push a state that blocks until enter is pressed
            # after the state is popped, the combat state will clean up and close
            self.task(partial(self.game.push_state, "WaitForInputState"), 2)
            self.suppress_phase_change(3)

        elif phase == "end combat":
            self.end_combat()

    def get_combat_decision_from_ai(self, monster):
        """ Get ai action from a monster and enqueue it

        :param monster:
        :param opponents:
        :return:
        """
        # TODO: parties/teams/etc to choose opponents
        opponents = self.monsters_in_play[self.players[0]]
        technique, target = monster.ai.make_decision(monster, opponents)
        return EnqueuedAction(monster, technique, target)

    def sort_action_queue(self):
        """ Sort actions in the queue according to game rules

        * Swap actions are always first
        * Techniques that damage are sorted by monster speed
        * Items are sorted by trainer speed

        :return:
        """

        def rank_action(action):
            sort = action.technique.sort
            try:
                primary_order = sort_order.index(sort)
            except IndexError:
                logger.error("unsortable action: ", action)
                primary_order = -1

            if sort == 'meta':
                # all meta items sorted together
                # use of 0 leads to undefined sort/probably random
                return primary_order, 0

            else:
                # TODO: determine the secondary sort element, monster speed, trainer speed, etc
                return primary_order, action.user.speed

        sort_order = ['meta', 'item', 'utility', 'potion', 'food', 'heal', 'damage']

        # TODO: Running happens somewhere else, it should be moved here i think.
        # TODO: Eventually make an action queue class?
        self._action_queue.sort(key=rank_action, reverse=True)

    def update_phase(self):
        """ Execute/update phase actions

        Part of state machine
        * Do not change phase.
        * Will be run each iteration phase is active.
        * Do not test conditions to change phase.

        :return: None
        """
        if self.phase == "decision phase":
            # show monster action menu for human players
            if self._decision_queue:
                monster = self._decision_queue.pop()
                self.show_monster_action_menu(monster)

        elif self.phase == "action phase":
            self.handle_action_queue()

        elif self.phase == "post action phase":
            self.handle_action_queue()

    def handle_action_queue(self):
        """ Take one action from the queue and do it

        :return: None
        """
        if self._action_queue:
            action = self._action_queue.pop()
            self.perform_action(*action)
            self.check_party_hp()
            self.task(self.animate_party_status, 3)

    def ask_player_for_monster(self, player):
        """ Open dialog to allow player to choose a TXMN to enter into play

        :param player:
        :return:
        """

        def add(menuitem):
            monster = menuitem.game_object
            if monster.current_hp == 0:
                tools.open_dialog(self.game, [trans("combat_fainted", parameters={"name": monster.name})])
            elif monster in self.active_monsters:
                tools.open_dialog(self.game, [trans("combat_isactive", parameters={"name": monster.name})])
                msg = trans("combat_replacement_is_fainted")
                tools.open_dialog(self.game, [msg])
            else:
                self.add_monster_into_play(player, monster)
                self.game.pop_state()

        state = self.game.push_state("MonsterMenuState")
        # must use a partial because alert relies on a text box that may not exist
        # until after the state hs been startup
        state.task(partial(state.alert, trans("combat_replacement")), 0)
        state.on_menu_selection = add

    def fill_battlefield_positions(self, ask=False):
        """ Check the battlefield for unfilled positions and send out monsters

        :param ask: bool.  if True, then open dialog for human players
        :return:
        """
        # TODO: let work for trainer battles
        humans = list(self.human_players)

        # TODO: integrate some values for different match types
        released = False
        for player in self.active_players:
            positions_available = self.max_positions - len(self.monsters_in_play[player])
            if positions_available:
                available = get_awake_monsters(player)
                for i in range(positions_available):
                    released = True
                    if player in humans and ask:
                        self.ask_player_for_monster(player)
                    else:
                        self.add_monster_into_play(player, next(available))

        if released:
            self.suppress_phase_change()

    def add_monster_into_play(self, player, monster):
        """

        :param player:
        :param monster:
        :return:
        """
        # TODO: refactor some into the combat animations
        feet = list(self._layout[player]['home'][0].center)
        feet[1] += tools.scale(11)
        self.animate_monster_release_bottom(feet, monster)
        self.build_hud(self._layout[player]['hud'][0], monster)
        self.monsters_in_play[player].append(monster)

        # TODO: not hardcode
        if player is self.players[0]:
            self.alert(trans('combat_call_tuxemon', {"name": monster.name.upper()}))
        else:
            self.alert(trans('combat_wild_appeared', {"name": monster.name.upper()}))

    def reset_status_icons(self):
        """ Update/reset status icons for monsters

        TODO: caching, etc
        """
        # remove all status icons
        for s in self._status_icons:
            self.sprites.remove(s)

        # add status icons
        for monster in self.active_monsters:
            for status in monster.status:
                if status.icon:
                    # get the rect of the monster
                    rect = self._monster_sprite_map[monster].rect
                    # load the sprite and add it to the display
                    self.load_sprite(status.icon, layer=200, center=rect.topleft)

    def show_combat_dialog(self):
        """ Create and show the area where battle messages are displayed
        """
        # make the border and area at the bottom of the screen for messages
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = tools.load_and_scale(self.borders_filename)
        self.dialog_box = GraphicBox(border, None, self.background_color)
        self.dialog_box.rect = rect
        self.sprites.add(self.dialog_box, layer=100)

        # make a text area to show messages
        self.text_area = TextArea(self.font, self.font_color)
        self.text_area.rect = self.dialog_box.calc_inner_rect(self.dialog_box.rect)
        self.sprites.add(self.text_area, layer=100)

    def show_monster_action_menu(self, monster):
        """ Show the main window for choosing player actions

        :param monster: Monster to choose an action for
        :type monster: core.components.monster.Monster

        :returns: None
        """
        message = trans('combat_monster_choice', {"name": monster.name})
        self.alert(message)
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w // 2.5, h // 4)
        rect.bottomright = w, h

        state = self.game.push_state("MainCombatMenuState", columns=2)
        state.monster = monster
        state.rect = rect

    def skip_phase_change(self):
        """ Skip phase change animations

        Useful if player wants to skip a battle animation
        """
        for ani in self.animations:
            ani.finish()

    def enqueue_action(self, user, technique, target=None):
        """ Add some technique or status to the action queue

        :param user:
        :param technique:
        :param target:
        :returns: None
        """
        self._action_queue.append(EnqueuedAction(user, technique, target))

    def rewrite_action_queue_target(self, original, new):
        """ Used for swapping monsters

        :param original:
        :param new:
        :return:
        """
        # rewrite actions in the queue to target the new monster
        for index, action in enumerate(self._action_queue):
            if action.target is original:
                new_action = EnqueuedAction(action.user, action.technique, new)
                self._action_queue[index] = new_action

    def remove_monster_from_play(self, trainer, monster):
        """ Remove monster from play without fainting it

        * If another monster has targeted this monster, it can change action
        * Will remove actions as well
        * currently for 'swap' technique

        :param monster:
        :return:
        """
        self.remove_monster_actions_from_queue(monster)
        self.animate_monster_faint(monster)

    def remove_monster_actions_from_queue(self, monster):
        """ Remove all queued actions for a particular monster

        This is used mainly for removing actions after monster is fainted

        :type monster: core.components.monster.Monster
        :returns: None
        """
        to_remove = set()
        for action in self._action_queue:
            if action.user is monster or action.target is monster:
                to_remove.add(action)
        [self._action_queue.remove(action) for action in to_remove]

    def suppress_phase_change(self, delay=3.0):
        """ Prevent the combat phase from changing for a limited time

        Use this function to prevent the phase from changing.  When
        animating elements of the phase, call this to prevent player
        input as well as phase changes.

        :param delay:
        :return:
        """
        if self._animation_in_progress:
            logger.debug("double suppress: bug?")
            return

        self._animation_in_progress = True
        return self.task(partial(setattr, self, "_animation_in_progress", False), delay)

    def perform_action(self, user, technique, target=None):
        """ Do something with the thing: animated

        :param user:
        :param technique: Not a dict: a Technique or Item
        :param target:

        :returns:
        """
        technique.advance_round()

        # This is the time, in seconds, that the animation takes to finish.
        action_time = 3.0
        result = technique.use(user, target)

        if technique.execute_trans:
            context = {"user": getattr(user, "name", ''),
                       "name": technique.name,
                       "target": target.name}
            message = trans(technique.execute_trans, context)
        else:
            message = ''

        try:
            tools.load_sound(technique.sfx).play()
        except AttributeError:
            pass

        # action is performed, so now use sprites to animate it
        # this value will be None if the target is off screen
        target_sprite = self._monster_sprite_map.get(target, None)

        # slightly delay the monster shake, so technique animation
        # is synchronized with the damage shake motion
        hit_delay = 0
        if user:

            # TODO: a real check or some params to test if should tackle, etc
            if result["should_tackle"]:
                hit_delay += .5
                user_sprite = self._monster_sprite_map[user]
                self.animate_sprite_tackle(user_sprite)

                if target_sprite:
                    self.task(partial(self.animate_sprite_take_damage, target_sprite), hit_delay + .2)
                    self.task(partial(self.blink, target_sprite), hit_delay + .6)

                # TODO: track total damage
                # Track damage
                self._damage_map[target].add(user)

            else:  # assume this was an item used

                # handle the capture device
                if result["capture"]:
                    message += "\n" + trans('attempting_capture')
                    action_time = result["num_shakes"] + 1.8
                    self.animate_capture_monster(result["success"], result["num_shakes"], target)

                    # TODO: Don't end combat right away; only works with SP, and 1 member parties
                    # end combat right here
                    if result["success"]:
                        self.task(self.end_combat, action_time + 0.5)  # Display 'Gotcha!' first.
                        self.task(partial(self.alert, trans('gotcha')), action_time)
                        self._animation_in_progress = True
                        return

                # generic handling of anything else
                else:
                    msg_type = 'success_trans' if result['success'] else 'failure_trans'
                    template = getattr(technique, msg_type)
                    if template:
                        message += "\n" + trans(template)

            self.alert(message)
            self.suppress_phase_change(action_time)

        else:
            if result["success"]:
                self.suppress_phase_change()
                self.alert(trans('combat_status_damage', {"name": target.name, "status": technique.name}))

        if result["success"] and target_sprite and hasattr(technique, "images"):
            tech_sprite = self.get_technique_animation(technique)
            tech_sprite.rect.center = target_sprite.rect.center
            self.task(tech_sprite.image.play, hit_delay)
            self.task(partial(self.sprites.add, tech_sprite, layer=50), hit_delay)
            self.task(tech_sprite.kill, 3)

    def faint_monster(self, monster):
        """ Instantly make the monster faint (will be removed later)

        :type monster: core.components.monster.Monster
        :returns: None
        """
        monster.current_hp = 0
        monster.status = [faint]

        """
        Experience is earned when the target monster is fainted.
        Any monsters who contributed any amount of damage will be awarded
        Experience is distributed evenly to all participants
        """
        if monster in self._damage_map:
            # Award Experience
            awarded_exp = monster.total_experience / monster.level / len(self._damage_map[monster])
            for winners in self._damage_map[monster]:
                winners.give_experience(awarded_exp)

            # Remove monster from damage map
            del self._damage_map[monster]

    def animate_party_status(self):
        """ Animate monsters that need to be fainted

        * Animation to remove monster is handled here
        TODO: check for faint status, not HP

        :returns: None
        """
        for player, party in self.monsters_in_play.items():
            for monster in party:
                if fainted(monster):
                    self.alert(trans('combat_fainted', {"name": monster.name}))
                    self.animate_monster_faint(monster)
                    self.suppress_phase_change(3)

    def check_party_hp(self):
        """ Apply status effects, then check HP, and party status

        * Monsters will be removed from play here

        :returns: None
        """
        for player, party in self.monsters_in_play.items():
            for monster in party:
                self.animate_hp(monster)
                if monster.current_hp <= 0 and not fainted(monster):
                    self.remove_monster_actions_from_queue(monster)
                    self.faint_monster(monster)

    def get_technique_animation(self, technique):
        """ Return a sprite usable as a technique animation

        TODO: move to some generic animation loading thingy

        :type technique: core.components.technique.Technique
        :rtype: core.components.sprite.Sprite
        """
        try:
            return self._technique_cache[technique]
        except KeyError:
            sprite = self.load_technique_animation(technique)
            self._technique_cache[technique] = sprite
            return sprite

    @staticmethod
    def load_technique_animation(technique):
        """

        TODO: move to some generic animation loading thingy

        :param technique:
        :rtype: core.components.sprite.Sprite
        """
        frame_time = .09
        images = list()
        for fn in technique.images:
            image = tools.load_and_scale(fn)
            images.append((image, frame_time))

        tech = PygAnimation(images, False)
        sprite = Sprite()
        sprite.image = tech
        sprite.rect = tech.get_rect()
        return sprite

    @property
    def active_players(self):
        """ Generator of any non-defeated players/trainers

        :rtype: collections.Iterable[core.components.player.Player]
        """
        for player in self.players:
            if not defeated(player):
                yield player

    @property
    def human_players(self):
        for player in self.players:
            if player.isplayer:
                yield player

    @property
    def ai_players(self):
        for player in set(self.active_players) - set(self.human_players):
            yield player

    @property
    def active_monsters(self):
        """ List of any non-defeated monsters on battlefield

        :rtype: list
        """
        return list(chain.from_iterable(self.monsters_in_play.values()))

    @property
    def remaining_players(self):
        """ List of non-defeated players/trainers.  WIP

        right now, this is similar to Combat.active_players, but it may change in the future.
        For implementing teams, this would need to be different than active_players

        Use to check for match winner

        :return: list
        """
        # TODO: perhaps change this to remaining "parties", or "teams", instead of player/trainer
        return [p for p in self.players if not defeated(p)]

    def trigger_player_run(self, player):
        """ WIP.  make player run from battle

        This is a temporary fix for now.  Expected to be called by the command menu.

        :param player:
        :return:
        """
        # TODO: non SP things
        del self.monsters_in_play[player]
        self.players.remove(player)

    def end_combat(self):
        """ End the combat
        """
        # TODO: End combat differently depending on winning or losing

        # clear action queue
        self._action_queue = list()

        # fade music out
        self.game.event_engine.execute_action("fadeout_music", [1000])

        # remove any menus that may be on top of the combat state
        while self.game.current_state is not self:
            self.game.pop_state()

        self.game.push_state("FadeOutTransition", caller=self)
Beispiel #14
0
class CombatTargetMenuState(Menu):
    """
    Menu for selecting targets of techniques and items

    This special menu draws over the combat screen
    """
    transparent = True

    def create_new_menu_items_group(self):
        # these groups will not automatically position the sprites
        self.menu_items = MenuSpriteGroup()
        self.menu_sprites = SpriteGroup()

    def startup(self, *args, **kwargs):
        super(CombatTargetMenuState, self).startup(*args, **kwargs)

        # used to determine how player can target techniques
        self.user = kwargs.get("user")
        self.action = kwargs.get("action")
        self.player = kwargs.get("player")

        # load and scale the menu borders
        border = tools.load_and_scale(self.borders_filename)
        self.border = GraphicBox(border, None, None)

    def initialize_items(self):
        # get a ref to the combat state
        combat_state = self.game.get_state_name("CombatState")

        # TODO: trainer targeting
        # TODO: cleanup how monster sprites and whatnot are managed
        # TODO: This is going to work fine for simple matches, but controls will be wonky for parties
        # TODO: (cont.) Need better handling of cursor keys for 2d layouts of menu items
        # get all the monster positions

        # this is used to determine who owns what monsters and what not
        # TODO: make less duplication of game data in memory, let combat state have more registers, etc
        self.targeting_map = defaultdict(list)

        for player, monsters in combat_state.monsters_in_play.items():
            for monster in monsters:

                # TODO: more targeting classes
                if player == self.player:
                    targeting_class = "own monster"
                else:
                    targeting_class = "enemy monster"

                self.targeting_map[targeting_class].append(monster)

                # TODO: handle odd cases where a situation creates no valid targets
                # if this target type is not handled by this action, then skip it
                if targeting_class not in self.action.target:
                    continue

                # inspect the monster sprite and make a border image for it
                sprite = combat_state._monster_sprite_map[monster]
                item = MenuItem(None, None, None, monster)
                item.rect = sprite.rect.copy()
                center = item.rect.center
                item.rect.inflate_ip(tools.scale(16), tools.scale(16))
                item.rect.center = center

                yield item

    def refresh_layout(self):
        """ Before refreshing the layout, determine the optimal target

        :return:
        """
        def determine_target():
            for tag in self.action.target:
                for target in self.targeting_map[tag]:
                    menu_item = self.search_items(target)
                    if menu_item.enabled:
                        # TODO: some API for this mess
                        # get the index of the menu_item
                        # change it
                        index = self.menu_items._spritelist.index(menu_item)
                        self.selected_index = index
                        return

        determine_target()
        super(CombatTargetMenuState, self).refresh_layout()

    def on_menu_selection_change(self):
        """ Draw borders around sprites when selection changes

        :return:
        """
        # clear out the old borders
        for sprite in self.menu_items:
            sprite.image = None

        # find the selected item and make a border for it
        item = self.get_selected_item()
        if item:
            item.image = pygame.Surface(item.rect.size, pygame.SRCALPHA)
            self.border.draw(item.image)
Beispiel #15
0
class CombatState(CombatAnimations):
    """ The state-menu responsible for all combat related tasks and functions.
        .. image:: images/combat/monster_drawing01.png
    """
    background_filename = "gfx/ui/combat/battle_bg03.png"
    draw_borders = False
    escape_key_exits = False

    def startup(self, **kwargs):
        self.max_positions = 1  # TODO: make dependant on match type
        self.phase = None
        self.monsters_in_play = defaultdict(list)
        self._experience_tracking = defaultdict(list)
        self._technique_cache = dict()  # cache for technique animations
        self._decision_queue = list()  # queue for monsters that need decisions
        self._position_queue = list(
        )  # queue for asking players to add a monster into play (subject to change)
        self._action_queue = list(
        )  # queue for techniques, items, and status effects
        self._monster_sprite_map = dict()  # monster => sprite
        self._hp_bars = dict()  # monster => hp bar
        self._layout = dict()  # player => home areas on screen
        self._animation_in_progress = False  # if true, delay phase change
        self._winner = None  # when set, combat ends
        self._round = 0

        super(CombatState, self).startup(**kwargs)
        self.players = list(self.players)
        self.show_combat_dialog()
        self.transition_phase("begin")
        self.task(partial(setattr, self, "phase", "ready"), 3)

    def update(self, time_delta):
        super(CombatState, self).update(time_delta)
        if not self._animation_in_progress:
            new_phase = self.determine_phase(self.phase)
            if new_phase:
                self.phase = new_phase
                self.transition_phase(new_phase)
            self.update_phase()

    def draw(self, surface):
        super(CombatState, self).draw(surface)
        self.draw_hp_bars()

    def draw_hp_bars(self):
        """ Go through the HP bars and redraw them

        :returns: None
        """
        for monster, hud in self.hud.items():
            rect = pygame.Rect(0, 0, tools.scale(70), tools.scale(8))
            rect.right = hud.image.get_width() - tools.scale(8)
            rect.top += tools.scale(12)
            self._hp_bars[monster].draw(hud.image, rect)

    def determine_phase(self, phase):
        """ Determine the next phase and set it

        Only test and set new phase.  Do not execute phase actions.

        :returns: None
        """
        if phase == "ready":
            return "housekeeping phase"

        elif phase == "housekeeping phase":
            return "decision phase"

        elif phase == "decision phase":
            if len(self._action_queue) == len(list(self.active_monsters)):
                return "pre action phase"

            # if a player runs, it will be known here
            self.check_match_status()
            if self._winner:
                return "resolve match"

        elif phase == "pre action phase":
            return "action phase"

        if phase == "action phase":
            if not self._action_queue:
                return "post action phase"

        elif phase == "post action phase":
            if not self._action_queue:
                return "ready"

        elif phase == "resolve match":
            if not self._winner:
                return "housekeeping phase"

    def transition_phase(self, phase):
        """ Change from one phase from another.

        This will be run just once when phase changes.
        Do not change phase.  Just runs actions for new phase.

        :param phase:
        :return:
        """
        if phase == "housekeeping phase":
            self._round += 1
            self.fill_battlefield_positions(ask=self._round > 1)

        if phase == "decision phase":
            if not self._decision_queue:
                for player in self.human_players:
                    self._decision_queue.extend(self.monsters_in_play[player])

                for trainer in self.ai_players:
                    for monster in self.monsters_in_play[trainer]:
                        # TODO: real ai...
                        target = choice(self.monsters_in_play[self.players[0]])
                        self.enqueue_action(monster, choice(monster.moves),
                                            target)

        elif phase == "action phase":
            self._action_queue.sort(key=attrgetter("user.speed"))

        elif phase == "post action phase":
            for monster in self.active_monsters:
                for technique in monster.status:
                    self.enqueue_action(None, technique, monster)

        elif phase == "resolve match":
            self.check_match_status()
            if self._winner:
                self.end_combat()

    def update_phase(self):
        """ Execute/update phase actions

        Do not change phase.  This will be run each iteration phase is active.
        Do not test conditions to change phase.  Only do actions.

        :return: None
        """
        if self.phase == "decision phase":
            if self._decision_queue:
                monster = self._decision_queue.pop()
                self.show_monster_action_menu(monster)

        elif self.phase == "action phase":
            self.handle_action_queue()

        elif self.phase == "post action phase":
            self.handle_action_queue()

    def handle_action_queue(self):
        """ Take one action from the queue and do it

        :return: None
        """
        if self._action_queue:
            action = self._action_queue.pop()
            self.perform_action(*action)
            self.check_party_hp()
            self.task(self.animate_party_status, 3)

    def ask_player_for_monster(self, player):
        """ Open dialog to allow player to choose a TXMN to enter into play

        :param player:
        :return:
        """
        def add(menuitem):
            monster = menuitem.game_object

            if monster.current_hp == 0:
                tools.open_dialog(self.game,
                                  ["Cannot choose because is fainted"])
            else:
                self.game.pop_state()
                self.add_monster_into_play(player, monster)

        state = self.game.push_state("MonsterMenuState")
        # must use a partial because alert relies on a text box that may not exist
        # until after the state hs been startup
        state.task(partial(state.alert, "Choose a replacement!"), 0)
        state.on_menu_selection = add

    def fill_battlefield_positions(self, ask=False):
        """ Check the battlefield for unfilled positions and send out monsters

        :param ask: bool.  if True, then open dialog for human players
        :return:
        """
        # TODO: let work for trainer battles
        humans = list(self.human_players)

        released = False
        for player in self.active_players:
            positions_available = self.max_positions - len(
                self.monsters_in_play[player])
            if positions_available:
                available = get_awake_monsters(player)
                for i in range(positions_available):
                    released = True
                    if player in humans and ask:
                        self.ask_player_for_monster(player)
                    else:
                        self.add_monster_into_play(player, next(available))

        if released:
            self.suppress_phase_change()

    def add_monster_into_play(self, player, monster):
        feet = list(self._layout[player]['home'][0].center)
        feet[1] += tools.scale(11)
        self.animate_monster_release_bottom(feet, monster)
        self.build_hud(self._layout[player]['hud'][0], monster)
        self.monsters_in_play[player].append(monster)

        # TODO: not hardcode
        if player is self.players[0]:
            self.alert('Go %s!' % monster.name.upper())
        else:
            self.alert('A wild %s appeared!' % monster.name.upper())

    def show_combat_dialog(self):
        """ Create and show the area where battle messages are displayed
        """
        # make the border and area at the bottom of the screen for messages
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = tools.load_and_scale(self.borders_filename)
        self.dialog_box = GraphicBox(border, None, self.background_color)
        self.dialog_box.rect = rect
        self.sprites.add(self.dialog_box, layer=100)

        # make a text area to show messages
        self.text_area = TextArea(self.font, self.font_color)
        self.text_area.rect = self.dialog_box.calc_inner_rect(
            self.dialog_box.rect)
        self.sprites.add(self.text_area, layer=100)

    def show_monster_action_menu(self, monster):
        """ Show the main window for choosing player actions

        :param monster: Monster to choose an action for
        :type monster: core.components.monster.Monster

        :returns: None
        """
        message = 'What will %s do?' % monster.name
        self.alert(message)
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w // 2.5, h // 4)
        rect.bottomright = w, h

        state = self.game.push_state("MainCombatMenuState", columns=2)
        state.monster = monster
        state.rect = rect

    def skip_phase_change(self):
        """ Skip phase change animations
        """
        for ani in self.animations:
            ani.finish()

    def enqueue_action(self, user, technique, target=None):
        self._action_queue.append(EnqueuedAction(user, technique, target))

    def remove_monster_actions_from_queue(self, monster):
        """ Remove all queued actions for a particular monster

        This is used mainly for removing actions after monster is fainted

        :type monster: core.components.monster.Monster
        :returns: None
        """
        to_remove = set()
        for action in self._action_queue:
            if action.user is monster or action.target is monster:
                to_remove.add(action)
        [self._action_queue.remove(action) for action in to_remove]

    def suppress_phase_change(self, delay=3):
        """ Prevent the combat phase from changing for a limited time

        Use this function to prevent the phase from changing.  When
        animating elements of the phase, call this to prevent player
        input as well as phase changes.

        :param delay:
        :return:
        """
        if self._animation_in_progress:
            logger.debug("double suppress: bug?")
        else:
            self._animation_in_progress = True
            self.task(partial(setattr, self, "_animation_in_progress", False),
                      delay)

    def perform_action(self, user, technique, target=None):
        """ Do something with the thing: animated

        :param user:
        :param technique: Not a dict: a Technique or Item
        :param target:

        :returns:
        """
        result = technique.use(user, target)

        try:
            tools.load_sound(technique.sfx).play()
        except AttributeError:
            pass

        # action is performed, so now use sprites to animate it
        target_sprite = self._monster_sprite_map[target]

        hit_delay = 0
        if user:
            message = "%s used %s!" % (user.name, technique.name)

            # TODO: a real check or some params to test if should tackle, etc
            if technique in user.moves:
                hit_delay += .5
                user_sprite = self._monster_sprite_map[user]
                self.animate_sprite_tackle(user_sprite)
                self.task(
                    partial(self.animate_sprite_take_damage, target_sprite),
                    hit_delay + .2)
                self.task(partial(self.blink, target_sprite), hit_delay + .6)

            else:  # assume this was an item used
                if result:
                    message += "\nIt worked!"
                else:
                    message += "\nIt failed!"

            self.alert(message)
            self.suppress_phase_change()

        else:
            if result:
                self.suppress_phase_change()
                self.alert("{0.name} took {1.name} damage!".format(
                    target, technique))

        if result and hasattr(technique, "images"):
            tech_sprite = self.get_technique_animation(technique)
            tech_sprite.rect.center = target_sprite.rect.center
            self.task(tech_sprite.image.play, hit_delay)
            self.task(partial(self.sprites.add, tech_sprite, layer=50),
                      hit_delay)
            self.task(tech_sprite.kill, 3)

    def faint_monster(self, monster):
        """ Instantly make the monster faint (will be removed later)

        :type monster: core.components.monster.Monster
        :returns: None
        """
        monster.current_hp = 0
        monster.status = [faint]
        # TODO: award experience

    def animate_party_status(self):
        """ Animate monsters that need to be fainted

        * Animation to remove monster is handled here
        TODO: check for faint status, not HP

        :returns: None
        """
        for player in self.monsters_in_play.keys():
            for monster in self.monsters_in_play[player]:
                if fainted(monster):
                    self.alert("{0.name} fainted!".format(monster))
                    self.animate_monster_faint(monster)
                    self.suppress_phase_change(3)

    def check_party_hp(self):
        """ Apply status effects, then check HP, and party status

        * Monsters will be removed from play here

        :returns: None
        """
        for player in self.monsters_in_play.keys():
            for monster in self.monsters_in_play[player]:
                self.animate_hp(monster)
                if monster.current_hp <= 0:
                    self.remove_monster_actions_from_queue(monster)
                    self.faint_monster(monster)

    def get_technique_animation(self, technique):
        """ Return a sprite usable as a technique animation

        TODO: move to some generic animation loading thingy

        :type technique: core.components.monster.Technique
        :rtype: core.components.sprite.Sprite
        """
        try:
            return self._technique_cache[technique]
        except KeyError:
            sprite = self.load_technique_animation(technique)
            self._technique_cache[technique] = sprite
            return sprite

    @staticmethod
    def load_technique_animation(technique):
        """

        TODO: move to some generic animation loading thingy

        :param technique:
        :rtype: core.components.sprite.Sprite
        """
        frame_time = .09
        images = list()
        for fn in technique.images:
            image = tools.load_and_scale(fn)
            images.append((image, frame_time))

        tech = PygAnimation(images, False)
        sprite = Sprite()
        sprite.image = tech
        sprite.rect = tech.get_rect()
        return sprite

    @property
    def active_players(self):
        """ Generator of any non-defeated players/trainers

        :rtype: collections.Iterable[core.components.player.Player]
        """
        for player in self.players:
            if not defeated(player):
                yield player

    @property
    def human_players(self):
        # TODO: this.
        yield self.players[0]

    @property
    def ai_players(self):
        for player in set(self.active_players) - set(self.human_players):
            yield player

    @property
    def active_monsters(self):
        """ Generator of any non-defeated monsters on battlefield

        :rtype: collections.Iterable[core.components.monster.Monster]
        """
        for monsters in self.monsters_in_play.values():
            for monster in monsters:
                yield monster

    def remove_player(self, player):
        # TODO: non SP things
        self.players.remove(player)
        self.suppress_phase_change()
        self.alert("You have run away!")

    def check_match_status(self):
        """ Determine if match should continue or not

        :return:
        """
        if self._winner:
            return

        players = list(self.active_players)
        if len(players) == 1:
            self._winner = players[0]

            # TODO: proper match check, etc
            if self._winner.name == "Maple":
                self.alert("You've been defeated!")
            else:
                self.alert("You have won!")

    def end_combat(self):
        """ End the combat
        """
        # TODO: End combat differently depending on winning or losing

        # clear action queue
        self._action_queue = list()

        event_engine = self.game.event_engine
        fadeout_action = namedtuple("action", ["type", "parameters"])
        fadeout_action.type = "fadeout_music"
        fadeout_action.parameters = [1000]
        event_engine.actions["fadeout_music"]["method"](self.game,
                                                        fadeout_action)

        # remove any menus that may be on top of the combat state
        while self.game.current_state is not self:
            self.game.pop_state()

        self.game.push_state("FadeOutTransition", caller=self)
Beispiel #16
0
class CombatState(CombatAnimations):
    """ The state-menu responsible for all combat related tasks and functions.
        .. image:: images/combat/monster_drawing01.png
    """

    background_filename = "gfx/ui/combat/battle_bg03.png"
    draw_borders = False
    escape_key_exits = False

    def startup(self, **kwargs):
        self.max_positions = 1  # TODO: make dependant on match type
        self.phase = None
        self.monsters_in_play = defaultdict(list)
        self._experience_tracking = defaultdict(list)
        self._technique_cache = dict()  # cache for technique animations
        self._decision_queue = list()  # queue for monsters that need decisions
        self._position_queue = list()  # queue for asking players to add a monster into play (subject to change)
        self._action_queue = list()  # queue for techniques, items, and status effects
        self._monster_sprite_map = dict()  # monster => sprite
        self._hp_bars = dict()  # monster => hp bar
        self._layout = dict()  # player => home areas on screen
        self._animation_in_progress = False  # if true, delay phase change
        self._winner = None  # when set, combat ends
        self._round = 0

        super(CombatState, self).startup(**kwargs)
        self.players = list(self.players)
        self.show_combat_dialog()
        self.transition_phase("begin")
        self.task(partial(setattr, self, "phase", "ready"), 3)

    def update(self, time_delta):
        super(CombatState, self).update(time_delta)
        if not self._animation_in_progress:
            new_phase = self.determine_phase(self.phase)
            if new_phase:
                self.phase = new_phase
                self.transition_phase(new_phase)
            self.update_phase()

    def draw(self, surface):
        super(CombatState, self).draw(surface)
        self.draw_hp_bars()

    def draw_hp_bars(self):
        """ Go through the HP bars and redraw them

        :returns: None
        """
        for monster, hud in self.hud.items():
            rect = pygame.Rect(0, 0, tools.scale(70), tools.scale(8))
            rect.right = hud.image.get_width() - tools.scale(8)
            rect.top += tools.scale(12)
            self._hp_bars[monster].draw(hud.image, rect)

    def determine_phase(self, phase):
        """ Determine the next phase and set it

        Only test and set new phase.  Do not execute phase actions.

        :returns: None
        """
        if phase == "ready":
            return "housekeeping phase"

        elif phase == "housekeeping phase":
            return "decision phase"

        elif phase == "decision phase":
            if len(self._action_queue) == len(list(self.active_monsters)):
                return "pre action phase"

            # if a player runs, it will be known here
            self.check_match_status()
            if self._winner:
                return "resolve match"

        elif phase == "pre action phase":
            return "action phase"

        if phase == "action phase":
            if not self._action_queue:
                return "post action phase"

        elif phase == "post action phase":
            if not self._action_queue:
                return "ready"

        elif phase == "resolve match":
            if not self._winner:
                return "housekeeping phase"

    def transition_phase(self, phase):
        """ Change from one phase from another.

        This will be run just once when phase changes.
        Do not change phase.  Just runs actions for new phase.

        :param phase:
        :return:
        """
        if phase == "housekeeping phase":
            self._round += 1
            self.fill_battlefield_positions(ask=self._round > 1)

        if phase == "decision phase":
            if not self._decision_queue:
                for player in self.human_players:
                    self._decision_queue.extend(self.monsters_in_play[player])

                for trainer in self.ai_players:
                    for monster in self.monsters_in_play[trainer]:
                        # TODO: real ai...
                        target = choice(self.monsters_in_play[self.players[0]])
                        self.enqueue_action(monster, choice(monster.moves), target)

        elif phase == "action phase":
            self._action_queue.sort(key=attrgetter("user.speed"))

        elif phase == "post action phase":
            for monster in self.active_monsters:
                for technique in monster.status:
                    self.enqueue_action(None, technique, monster)

        elif phase == "resolve match":
            self.check_match_status()
            if self._winner:
                self.end_combat()

    def update_phase(self):
        """ Execute/update phase actions

        Do not change phase.  This will be run each iteration phase is active.
        Do not test conditions to change phase.  Only do actions.

        :return: None
        """
        if self.phase == "decision phase":
            if self._decision_queue:
                monster = self._decision_queue.pop()
                self.show_monster_action_menu(monster)

        elif self.phase == "action phase":
            self.handle_action_queue()

        elif self.phase == "post action phase":
            self.handle_action_queue()

    def handle_action_queue(self):
        """ Take one action from the queue and do it

        :return: None
        """
        if self._action_queue:
            action = self._action_queue.pop()
            self.perform_action(*action)
            self.check_party_hp()
            self.task(self.animate_party_status, 3)

    def ask_player_for_monster(self, player):
        """ Open dialog to allow player to choose a TXMN to enter into play

        :param player:
        :return:
        """

        def add(menuitem):
            monster = menuitem.game_object

            if monster.current_hp == 0:
                tools.open_dialog(self.game, ["Cannot choose because is fainted"])
            else:
                self.game.pop_state()
                self.add_monster_into_play(player, monster)

        state = self.game.push_state("MonsterMenuState")
        # must use a partial because alert relies on a text box that may not exist
        # until after the state hs been startup
        state.task(partial(state.alert, "Choose a replacement!"), 0)
        state.on_menu_selection = add

    def fill_battlefield_positions(self, ask=False):
        """ Check the battlefield for unfilled positions and send out monsters

        :param ask: bool.  if True, then open dialog for human players
        :return:
        """
        # TODO: let work for trainer battles
        humans = list(self.human_players)

        released = False
        for player in self.active_players:
            positions_available = self.max_positions - len(self.monsters_in_play[player])
            if positions_available:
                available = get_awake_monsters(player)
                for i in range(positions_available):
                    released = True
                    if player in humans and ask:
                        self.ask_player_for_monster(player)
                    else:
                        self.add_monster_into_play(player, next(available))

        if released:
            self.suppress_phase_change()

    def add_monster_into_play(self, player, monster):
        feet = list(self._layout[player]["home"][0].center)
        feet[1] += tools.scale(11)
        self.animate_monster_release_bottom(feet, monster)
        self.build_hud(self._layout[player]["hud"][0], monster)
        self.monsters_in_play[player].append(monster)

        # TODO: not hardcode
        if player is self.players[0]:
            self.alert("Go %s!" % monster.name.upper())
        else:
            self.alert("A wild %s appeared!" % monster.name.upper())

    def show_combat_dialog(self):
        """ Create and show the area where battle messages are displayed
        """
        # make the border and area at the bottom of the screen for messages
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = tools.load_and_scale(self.borders_filename)
        self.dialog_box = GraphicBox(border, None, self.background_color)
        self.dialog_box.rect = rect
        self.sprites.add(self.dialog_box, layer=100)

        # make a text area to show messages
        self.text_area = TextArea(self.font, self.font_color)
        self.text_area.rect = self.dialog_box.calc_inner_rect(self.dialog_box.rect)
        self.sprites.add(self.text_area, layer=100)

    def show_monster_action_menu(self, monster):
        """ Show the main window for choosing player actions

        :param monster: Monster to choose an action for
        :type monster: core.components.monster.Monster

        :returns: None
        """
        message = "What will %s do?" % monster.name
        self.alert(message)
        x, y, w, h = self.game.screen.get_rect()
        rect = pygame.Rect(0, 0, w // 2.5, h // 4)
        rect.bottomright = w, h

        state = self.game.push_state("MainCombatMenuState", columns=2)
        state.monster = monster
        state.rect = rect

    def skip_phase_change(self):
        """ Skip phase change animations
        """
        for ani in self.animations:
            ani.finish()

    def enqueue_action(self, user, technique, target=None):
        self._action_queue.append(EnqueuedAction(user, technique, target))

    def remove_monster_actions_from_queue(self, monster):
        """ Remove all queued actions for a particular monster

        This is used mainly for removing actions after monster is fainted

        :type monster: core.components.monster.Monster
        :returns: None
        """
        to_remove = set()
        for action in self._action_queue:
            if action.user is monster or action.target is monster:
                to_remove.add(action)
        [self._action_queue.remove(action) for action in to_remove]

    def suppress_phase_change(self, delay=3):
        """ Prevent the combat phase from changing for a limited time

        Use this function to prevent the phase from changing.  When
        animating elements of the phase, call this to prevent player
        input as well as phase changes.

        :param delay:
        :return:
        """
        if self._animation_in_progress:
            logger.debug("double suppress: bug?")
        else:
            self._animation_in_progress = True
            self.task(partial(setattr, self, "_animation_in_progress", False), delay)

    def perform_action(self, user, technique, target=None):
        """ Do something with the thing: animated

        :param user:
        :param technique: Not a dict: a Technique or Item
        :param target:

        :returns:
        """
        result = technique.use(user, target)

        try:
            tools.load_sound(technique.sfx).play()
        except AttributeError:
            pass

        # action is performed, so now use sprites to animate it
        target_sprite = self._monster_sprite_map[target]

        hit_delay = 0
        if user:
            message = "%s used %s!" % (user.name, technique.name)

            # TODO: a real check or some params to test if should tackle, etc
            if technique in user.moves:
                hit_delay += 0.5
                user_sprite = self._monster_sprite_map[user]
                self.animate_sprite_tackle(user_sprite)
                self.task(partial(self.animate_sprite_take_damage, target_sprite), hit_delay + 0.2)
                self.task(partial(self.blink, target_sprite), hit_delay + 0.6)

            else:  # assume this was an item used
                if result:
                    message += "\nIt worked!"
                else:
                    message += "\nIt failed!"

            self.alert(message)
            self.suppress_phase_change()

        else:
            if result:
                self.suppress_phase_change()
                self.alert("{0.name} took {1.name} damage!".format(target, technique))

        if result and hasattr(technique, "images"):
            tech_sprite = self.get_technique_animation(technique)
            tech_sprite.rect.center = target_sprite.rect.center
            self.task(tech_sprite.image.play, hit_delay)
            self.task(partial(self.sprites.add, tech_sprite, layer=50), hit_delay)
            self.task(tech_sprite.kill, 3)

    def faint_monster(self, monster):
        """ Instantly make the monster faint (will be removed later)

        :type monster: core.components.monster.Monster
        :returns: None
        """
        monster.current_hp = 0
        monster.status = [faint]
        # TODO: award experience

    def animate_party_status(self):
        """ Animate monsters that need to be fainted

        * Animation to remove monster is handled here
        TODO: check for faint status, not HP

        :returns: None
        """
        for player in self.monsters_in_play.keys():
            for monster in self.monsters_in_play[player]:
                if fainted(monster):
                    self.alert("{0.name} fainted!".format(monster))
                    self.animate_monster_faint(monster)
                    self.suppress_phase_change(3)

    def check_party_hp(self):
        """ Apply status effects, then check HP, and party status

        * Monsters will be removed from play here

        :returns: None
        """
        for player in self.monsters_in_play.keys():
            for monster in self.monsters_in_play[player]:
                self.animate_hp(monster)
                if monster.current_hp <= 0:
                    self.remove_monster_actions_from_queue(monster)
                    self.faint_monster(monster)

    def get_technique_animation(self, technique):
        """ Return a sprite usable as a technique animation

        TODO: move to some generic animation loading thingy

        :type technique: core.components.monster.Technique
        :rtype: core.components.sprite.Sprite
        """
        try:
            return self._technique_cache[technique]
        except KeyError:
            sprite = self.load_technique_animation(technique)
            self._technique_cache[technique] = sprite
            return sprite

    @staticmethod
    def load_technique_animation(technique):
        """

        TODO: move to some generic animation loading thingy

        :param technique:
        :rtype: core.components.sprite.Sprite
        """
        frame_time = 0.09
        images = list()
        for fn in technique.images:
            image = tools.load_and_scale(fn)
            images.append((image, frame_time))

        tech = PygAnimation(images, False)
        sprite = Sprite()
        sprite.image = tech
        sprite.rect = tech.get_rect()
        return sprite

    @property
    def active_players(self):
        """ Generator of any non-defeated players/trainers

        :rtype: collections.Iterable[core.components.player.Player]
        """
        for player in self.players:
            if not defeated(player):
                yield player

    @property
    def human_players(self):
        # TODO: this.
        yield self.players[0]

    @property
    def ai_players(self):
        for player in set(self.active_players) - set(self.human_players):
            yield player

    @property
    def active_monsters(self):
        """ Generator of any non-defeated monsters on battlefield

        :rtype: collections.Iterable[core.components.monster.Monster]
        """
        for monsters in self.monsters_in_play.values():
            for monster in monsters:
                yield monster

    def remove_player(self, player):
        # TODO: non SP things
        self.players.remove(player)
        self.suppress_phase_change()
        self.alert("You have run away!")

    def check_match_status(self):
        """ Determine if match should continue or not

        :return:
        """
        if self._winner:
            return

        players = list(self.active_players)
        if len(players) == 1:
            self._winner = players[0]

            # TODO: proper match check, etc
            if self._winner.name == "Maple":
                self.alert("You've been defeated!")
            else:
                self.alert("You have won!")

    def end_combat(self):
        """ End the combat
        """
        # TODO: End combat differently depending on winning or losing

        # clear action queue
        self._action_queue = list()

        event_engine = self.game.event_engine
        fadeout_action = namedtuple("action", ["type", "parameters"])
        fadeout_action.type = "fadeout_music"
        fadeout_action.parameters = [1000]
        event_engine.actions["fadeout_music"]["method"](self.game, fadeout_action)

        # remove any menus that may be on top of the combat state
        while self.game.current_state is not self:
            self.game.pop_state()

        self.game.push_state("FadeOutTransition", caller=self)
Beispiel #17
0
class Menu(state.State):
    """A class to create menu objects.

    :background: String

    :ivar rect: The rect of the menu in pixels, defaults to 0, 0, 400, 200.
    :ivar state: An arbitrary state of the menu. E.g. "opening" or "closing".
    :ivar selected_index: The index position of the currently selected menu item.
    :ivar menu_items: A list of available menu items.
    """
    # defaults for the menu
    columns = 1
    min_font_size = 4
    draw_borders = True
    background = None
    background_color = 248, 248, 248  # The window's background 	olor
    background_filename = None
    menu_select_sound_filename = "sounds/interface/menu-select.ogg"
    font_filename = "resources/font/PressStart2P.ttf"
    borders_filename = "gfx/dialog-borders01.png"
    cursor_filename = "gfx/arrow.png"
    cursor_move_duration = .20
    default_character_delay = 0.05
    shrink_to_items = False
    escape_key_exits = True

    def startup(self, *items, **kwargs):
        self.rect = self.rect.copy()  # do not remove!

        self.__dict__.update(kwargs)

        # used to position the menu/state
        self._anchors = dict()

        # holds sprites representing menu items
        self.menu_items = VisualSpriteList(parent=self.calc_menu_items_rect)
        self.menu_items.columns = self.columns
        if self.shrink_to_items:
            self.menu_items.expand = False

        # generally just for the cursor arrow
        self.menu_sprites = RelativeGroup(parent=self.menu_items)

        self.selected_index = 0  # Used to track which menu item is selected
        self.state = "closed"  # closed, opening, normal, closing

        self.set_font()  # load default font
        self.load_graphics()  # load default graphics
        self.reload_sounds()  # load default sounds

    def shutdown(self):
        """ Clear objects likely to cause cyclical references

        :returns: None
        """
        self.sprites.empty()
        self.menu_items.empty()
        self.menu_sprites.empty()
        self.animations.empty()

        del self.arrow
        del self.menu_items
        del self.menu_sprites

    def start_text_animation(self, text_area):
        """ Start an animation to show textarea, one character at a time

        :param text_area: TextArea to animate
        :type text_area: core.components.ui.text.TextArea
        :rtype: None
        """
        def next_character():
            try:
                next(text_area)
            except StopIteration:
                pass
            else:
                self.task(next_character, self.character_delay)

        self.character_delay = self.default_character_delay
        self.remove_animations_of(next_character)
        next_character()

    def animate_text(self, text_area, text):
        """ Set and animate a text area

        :param text: Test to display
        :type text: basestring
        :param text_area: TextArea to animate
        :type text_area: core.components.ui.text.TextArea
        :rtype: None
        """
        text_area.text = text
        self.start_text_animation(text_area)

    def alert(self, message):
        """ Write a message to the first available text area

        Generally, a state will have just one, if any, text area.
        The first one found will be use to display the message.
        If no text area is found, a RuntimeError will be raised

        :param message: Something interesting, I hope.
        :type message: basestring

        :returns: None
        """
        def find_textarea():
            for sprite in self.sprites:
                if isinstance(sprite, TextArea):
                    return sprite
            print("attempted to use 'alert' on state without a TextArea",
                  message)
            raise RuntimeError

        self.animate_text(find_textarea(), message)

    def _initialize_items(self):
        """ Internal use only.  Will reset the items in the menu

        Reset the menu items and get new updated ones.

        :rtype: collections.Iterable[MenuItem]
        """
        self.selected_index = 0
        self.menu_items.empty()
        for item in self.initialize_items():
            self.menu_items.add(item)
        if self.menu_items:
            self.show_cursor()

        # call item selection change to trigger callback for first time
        self.on_menu_selection_change()

        if self.shrink_to_items:
            center = self.rect.center
            rect1 = self.menu_items.calc_bounding_rect()
            rect2 = self.menu_sprites.calc_bounding_rect()
            rect1 = rect1.union(rect2)

            # TODO: do not hardcode these values
            # border is 12, padding is the rest
            rect1.width += tools.scale(18)
            rect1.height += tools.scale(19)
            rect1.topleft = 0, 0

            # self.rect = rect1.union(rect2)
            # self.rect.width += tools.scale(20)
            # self.rect.topleft = 0, 0
            self.rect = rect1
            self.rect.center = center
            self.position_rect()

    def reload_sounds(self):
        """ Reload sounds

        :returns: None
        """
        self.menu_select_sound = tools.load_sound(
            self.menu_select_sound_filename)

    def shadow_text(self, text, bg=(192, 192, 192)):
        """ Draw shadowed text

        :param text: Text to draw
        :param bg:
        :returns:
        """
        top = self.font.render(text, 1, self.font_color)
        shadow = self.font.render(text, 1, bg)

        offset = layout((0.5, 0.5))
        size = [int(math.ceil(a + b)) for a, b in zip(offset, top.get_size())]
        image = pygame.Surface(size, pygame.SRCALPHA)

        image.blit(shadow, offset)
        image.blit(top, (0, 0))
        return image

    def load_graphics(self):
        """ Loads all the graphical elements of the menu
            Will load some elements from disk, so needs to be called at least once.
        """
        # load and scale the _background
        background = None
        if self.background_filename:
            background = tools.load_image(self.background_filename)

        # load and scale the menu borders
        border = None
        if self.draw_borders:
            border = tools.load_and_scale(self.borders_filename)

        # set the helper to draw the _background
        self.window = GraphicBox(border, background, self.background_color)

        # handle the arrow cursor
        image = tools.load_and_scale(self.cursor_filename)
        self.arrow = MenuCursor(image)

    def show_cursor(self):
        """ Show the cursor that indicates the selected object

        :returns: None
        """
        if self.arrow not in self.menu_sprites:
            self.menu_sprites.add(self.arrow)
            self.trigger_cursor_update(False)
            self.get_selected_item().in_focus = True

    def hide_cursor(self):
        """ Hide the cursor that indicates the selected object

        :returns: None
        """
        self.menu_sprites.remove(self.arrow)
        self.get_selected_item().in_focus = False

    def draw(self, surface):
        """ Draws the menu object to a pygame surface.

        :param surface: Surface to draw on
        :type surface: pygame.Surface

        :rtype: None
        :returns: None

        """
        self.window.draw(surface, self.rect)

        if self.state == "normal" and self.menu_items:
            self.menu_items.draw(surface)
            self.menu_sprites.draw(surface)

        self.sprites.draw(surface)

        # debug = show the menu items area
        # surface.fill((255, 0, 0), self.calc_internal_rect(), 2)

    def set_font(self, size=5, font=None, color=(10, 10, 10), line_spacing=10):
        """Set the font properties that the menu uses including font _color, size, typeface,
        and line spacing.

        The size and line_spacing parameters will be adjusted the
        the screen scale.  You should pass the original, unscaled values.

        :param size: The font size in pixels.
        :param font: Path to the typeface file (.ttf)
        :param color: A tuple of the RGB _color values
        :param line_spacing: The spacing in pixels between lines of text

        :type size: Integer
        :type font: String
        :type color: Tuple
        :type line_spacing: Integer

        :rtype: None
        :returns: None

        .. image:: images/menu/set_font.png

        """
        if font is None:
            font = prepare.BASEDIR + self.font_filename

        if size < self.min_font_size:
            size = self.min_font_size

        self.line_spacing = tools.scale(line_spacing)
        self.font_size = tools.scale(size)
        self.font_color = color
        self.font = pygame.font.Font(font, self.font_size)

    def calc_internal_rect(self):
        """ Calculate the area inside the borders, if any.
        If no borders are present, a copy of the menu rect will be returned

        :returns: Rect representing space inside borders, if any
        :rtype: pygame.Rect
        """
        return self.window.calc_inner_rect(self.rect)

    def process_event(self, event):
        """ Process pygame input events

        The menu cursor is handled here, as well as the ESC and ENTER keys.

        This will also toggle the 'in_focus' of the menu item

        :param event: pygame.Event
        :returns: None
        """
        if event.type == pygame.KEYDOWN:
            if self.escape_key_exits and event.key == pygame.K_ESCAPE:
                self.close()
                return

            elif self.state == "normal" and self.menu_items and event.key == pygame.K_RETURN:
                self.menu_select_sound.play()
                self.on_menu_selection(self.get_selected_item())
                return

        # check if cursor has changed
        index = self.menu_items.determine_cursor_movement(
            self.selected_index, event)
        if not self.selected_index == index:
            self.get_selected_item(
            ).in_focus = False  # clear the focus flag of old item
            self.selected_index = index  # update the selection index
            self.menu_select_sound.play()
            self.trigger_cursor_update()  # move cursor and animate it
            self.get_selected_item(
            ).in_focus = True  # set focus flag of new item
            self.on_menu_selection_change(
            )  # let subclass know menu has changed

    def trigger_cursor_update(self, animate=True):
        """ Force the menu cursor to move into the correct position

        :param animate: If True, then arrow will move smoothly into position
        :returns: None or Animation
        """
        selected = self.get_selected_item()
        if not selected:
            return

        x, y = selected.rect.midleft
        x -= tools.scale(2)

        if animate:
            self.remove_animations_of(self.arrow.rect)
            return self.animate(self.arrow.rect,
                                right=x,
                                centery=y,
                                duration=self.cursor_move_duration)
        else:
            self.arrow.rect.midright = x, y
            return None

    def get_selected_item(self):
        """ Get the Menu Item that is currently selected

        :rtype: MenuItem
        :rtype: core.components.menu.interface.MenuItem
        """
        try:
            return self.menu_items[self.selected_index]
        except IndexError:
            return None

    def resume(self):
        if self.state == "closed":

            def show_items():
                self.state = "normal"
                self.on_open()

            self._initialize_items()
            self.state = "opening"
            self.position_rect()
            ani = self.animate_open()
            if ani:
                ani.callback = show_items
            else:
                show_items()

    def close(self):
        if self.state in ["normal", "opening"]:
            self.state = "closing"
            ani = self.animate_close()
            if ani:
                ani.callback = self.game.pop_state
            else:
                self.game.pop_state()

    def anchor(self, attribute, value):
        """ Set an anchor for the menu window

        You can pass any string value that is used in a pygame rect,
        for example: "center", "topleft", and "right.

        When changes are made to the window, when it is being opened
        or sized, then these values passed as anchors will override
        others.  The order of which each anchor is applied is not
        necessarily going to match the order they were set, as the
        implementation relies on a dictionary.

        Take care to make sure values do not overlap,

        :param attribute:
        :param value:
        :return:
        """
        if value is None:
            del self._anchors[attribute]
        else:
            self._anchors[attribute] = value

    def position_rect(self):
        """ Reposition rect taking in account the anchors
        """
        for attribute, value in self._anchors.items():
            setattr(self.rect, attribute, value)

    def update(self, time_delta):
        self.animations.update(time_delta)

    # ============================================================================
    #   The following methods are designed to be monkey patched or overloaded
    # ============================================================================

    def calc_menu_items_rect(self):
        """ Calculate the area inside the internal rect where items are listed

        :rtype: pygame.Rect
        """
        # WARNING: hardcoded values related to menu arrow size
        #          if menu arrow image changes, this should be adjusted
        cursor_margin = -tools.scale(11), -tools.scale(5)
        inner = self.calc_internal_rect()
        menu_rect = inner.inflate(*cursor_margin)
        menu_rect.bottomright = inner.bottomright
        return menu_rect

    def calc_final_rect(self):
        """ Calculate the area in the game window where menu is shown

        This value is the __desired__ location, and should not change
        over the lifetime of the menu.  It is used to generate animations
        to open the menu.

        By default, this will be the entire screen

        :rtype: pygame.Rect
        """
        return self.rect

    def on_open(self):
        """ Hook is called after opening animation has finished

        :return:
        """
        pass

    def on_menu_selection(self, item):
        """ Hook for things to happen when player selects a menu option

        Override in subclass

        :return:
        """
        pass

    def on_menu_selection_change(self):
        """ Hook for things to happen after menu selection changes

        Override in subclass

        :returns: None
        """
        pass

    def initialize_items(self):
        """ Hook for adding items to menu when menu is created

        Override with a generator

        :returns: Generator of MenuItems
        :rtype: collections.Iterable[MenuItem]
        """
        return iter(())

    def animate_open(self):
        """ Called when menu is going to open

        Menu will not receive input during the animation
        Menu will only play this animation once

        Must return either an Animation or Task to attach callback
        Only modify state of the menu Rect
        Do not change important state attributes

        :returns: Animation or Task
        :rtype: core.components.animation.Animation
        """
        return None

    def animate_close(self):
        """ Called when menu is going to open

        Menu will not receive input during the animation
        Menu will play animation only once
        Menu will be popped after animation finished

        Must return either an Animation or Task to attach callback
        Only modify state of the menu Rect
        Do not change important state attributes

        :returns: Animation or Task
        :rtype: core.components.animation.Animation
        """
        return None