Beispiel #1
0
    def startup(self, *args, **kwargs):
        super().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 = graphics.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 = 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()
        self.exp_bar = ExpBar()

        # 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 = graphics.load_and_scale(filename)

            filename = root + border_type + "_monster_slot_bg.png"
            background = graphics.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 local_session.player.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.client.screen.get_rect()
        rect = Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = graphics.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 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 = graphics.load_image(self.background_filename)

            # load and scale the menu borders
            border = None
            if self.draw_borders:
                border = graphics.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 = graphics.load_and_scale(self.cursor_filename)
        self.arrow = MenuCursor(image)
Beispiel #5
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().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 = graphics.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.client.get_state_by_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().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 #6
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
    unavailable_color = 220, 220, 220  # Font color when the action is unavailable
    background_filename = None  # File to load for image background
    menu_select_sound_filename = "sound_menu_select"
    font_filename = prepare.fetch("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 = True  # 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!
        i = kwargs.get('selected_index', 0)
        self.selected_index = i  # 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()

        self.client.release_controls()

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

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

        :param text_area: TextArea to animate
        :type text_area: tuxemon.core.ui.text.TextArea
        :param callback: called when alert is complete
        :type callback: callable
        :rtype: None
        """
        def next_character():
            try:
                next(text_area)
            except StopIteration:
                if callback:
                    callback()
            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, callback):
        """ Set and animate a text area

        :param text: Test to display
        :type text: basestring
        :param text_area: TextArea to animate
        :type text_area: tuxemon.core.ui.text.TextArea
        :param callback: called when alert is complete
        :type callback: callable
        :rtype: None
        """
        text_area.text = text
        self.start_text_animation(text_area, callback)

    def alert(self, message, callback=None):
        """ 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
        :param callback: called when alert is complete
        :type callback: callable

        :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, callback)

    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 is_valid_entry(self, game_object):
        """ Checked when items are loaded/reloaded.  The return value will enable/disable menu items

        WIP.  The value passed should be Item.game_object

        :param Any game_object: Any object to check
        :return boolean: Becomes the menu item enabled value
        """
        return True

    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)
                item.enabled = self.is_valid_entry(item.game_object)

            if hasattr(self.menu_items, "arrange_menu_items"):
                self.menu_items.arrange_menu_items()

            for index, item in enumerate(self.menu_items):
                if item.enabled:
                    break
                self.selected_index = index

    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: tuxemon.core.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 = audio.load_sound(
            self.menu_select_sound_filename)

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

        :param text: Text to draw
        :param bg:
        :returns:
        """
        color = fg
        if not color:
            color = self.font_color

        top = self.font.render(text, 1, 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 = graphics.load_image(self.background_filename)

            # load and scale the menu borders
            border = None
            if self.draw_borders:
                border = graphics.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 = graphics.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 = self.font_filename

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

        self.line_spacing = tools.scale(line_spacing)

        if prepare.CONFIG.large_gui:
            self.font_size = tools.scale(size + 1)
        else:
            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: Rect
        """
        return self.window.calc_inner_rect(self.rect)

    def process_event(self, event):
        """ Handles player input events. This function is only called when the
        player provides input such as pressing a key or clicking the mouse.

        Since this is part of a chain of event handlers, the return value
        from this method becomes input for the next one.  Returning None
        signifies that this method has dealt with an event and wants it
        exclusively.  Return the event and others can use it as well.

        You should return None if you have handled input here.

        :type event: tuxemon.core.input.PlayerInput
        :rtype: Optional[core.input.PlayerInput]
        """
        handled_event = False

        # close menu
        if event.button in (buttons.B, buttons.BACK, intentions.MENU_CANCEL):
            handled_event = True
            if event.pressed and self.escape_key_exits:
                self.close()

        disabled = True
        if hasattr(self, "menu_items") and event.pressed:
            disabled = all(not i.enabled for i in self.menu_items)
        valid_change = event.pressed and self.state == "normal" and not disabled and self.menu_items

        # confirm selection
        if event.button in (buttons.A, intentions.SELECT):
            handled_event = True
            if valid_change:
                self.menu_select_sound.play()
                self.on_menu_selection(self.get_selected_item())

        # cursor movement
        if event.button in (buttons.UP, buttons.DOWN, buttons.LEFT,
                            buttons.RIGHT):
            handled_event = True
            if valid_change:
                index = self.menu_items.determine_cursor_movement(
                    self.selected_index, event)
                if not self.selected_index == index:
                    self.change_selection(index)

        # mouse/touch selection
        if event.button in (buttons.MOUSELEFT, ):
            handled_event = True
            # TODO: handling of click/drag, miss-click, etc
            # TODO: eventually, maybe move some handling into menuitems
            # TODO: handle screen scaling?
            # TODO: generalized widget system
            if self.touch_aware and valid_change:
                mouse_pos = event.value
                assert mouse_pos is not 0

                try:
                    self.menu_items.update_rect_from_parent()
                except AttributeError:
                    pass
                else:
                    mouse_pos = [
                        a - b for a, b in zip(mouse_pos,
                                              self.menu_items.rect.topleft)
                    ]

                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())

        if not handled_event:
            return event

    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: tuxemon.core.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()

    def close(self):
        if self.state in ["normal", "opening"]:
            self.state = "closing"
            ani = self.animate_close()
            if ani:
                ani.callback = self.client.pop_state
            else:
                self.client.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: 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: 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: tuxemon.core.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: tuxemon.core.animation.Animation
        """
        return None
Beispiel #7
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._exp_bars = dict()  # monster => exp 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.is_trainer_battle = kwargs.get('combat_type') == "trainer"
        self.players = list(self.players)
        self.graphics = kwargs.get('graphics')
        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()
        self.draw_exp_bars()

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

        :returns: None
        """
        for monster, hud in self.hud.items():
            rect = 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 draw_exp_bars(self):
        """ Go through the EXP bars and redraw them

        :returns: None
        """
        for monster, hud in self.hud.items():
            if hud.player:
                rect = Rect(0, 0, tools.scale(70), tools.scale(6))
                rect.right = hud.image.get_width() - tools.scale(8)
                rect.top += tools.scale(31)
                self._exp_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

            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)

            # record the useful properties of the last monster we fought
            monster_record = self.monsters_in_play[self.players[1]][0]
            if monster_record in self.active_monsters:
                self.players[0].game_variables[
                    'battle_last_monster_name'] = monster_record.name
                self.players[0].game_variables[
                    'battle_last_monster_level'] = monster_record.level
                self.players[0].game_variables[
                    'battle_last_monster_type'] = monster_record.slug
                self.players[0].game_variables[
                    'battle_last_monster_category'] = monster_record.category
                self.players[0].game_variables[
                    'battle_last_monster_shape'] = monster_record.shape

        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.players[0].set_party_status()
            self.players[0].game_variables['battle_last_result'] = 'ran'
            self.alert(T.translate('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.client.push_state, "WaitForInputState"), 2)
            self.suppress_phase_change(3)

        elif phase == "draw match":
            self.players[0].set_party_status()
            self.players[0].game_variables['battle_last_result'] = 'draw'

            # it is a draw match; both players were defeated in same round
            self.alert(T.translate('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.client.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
            self.players[0].set_party_status()
            if self.remaining_players[0] == self.players[0]:
                self.players[0].game_variables['battle_last_result'] = 'won'
                self.alert(T.translate('combat_victory'))
            else:
                self.players[0].game_variables['battle_last_result'] = 'lost'
                self.players[0].game_variables['battle_lost_faint'] = 'true'
                self.alert(T.translate('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.client.push_state, "WaitForInputState"), 2)
            self.suppress_phase_change(3)

        elif phase == "end combat":
            self.players[0].set_party_status()
            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_test(action)

        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()
                for tech in monster.moves:
                    tech.recharge()
                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(local_session, [
                    T.format("combat_fainted",
                             parameters={"name": monster.name})
                ])
            elif monster in self.active_monsters:
                tools.open_dialog(local_session, [
                    T.format("combat_isactive",
                             parameters={"name": monster.name})
                ])
                msg = T.translate("combat_replacement_is_fainted")
                tools.open_dialog(local_session, [msg])
            else:
                self.add_monster_into_play(player, monster)
                self.client.pop_state()

        state = self.client.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, T.translate("combat_replacement")), 0)
        state.on_menu_selection = add
        state.escape_key_exits = False

    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
        self.animate_monster_release(player, 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(
                T.format('combat_call_tuxemon',
                         {"name": monster.name.upper()}))
        elif self.is_trainer_battle:
            self.alert(
                T.format('combat_opponent_call_tuxemon', {
                    "name": monster.name.upper(),
                    "user": player.name.upper(),
                }))
        else:
            self.alert(
                T.format('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.client.screen.get_rect()
        rect = Rect(0, 0, w, h // 4)
        rect.bottomright = w, h
        border = graphics.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: tuxemon.core.monster.Monster

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

        state = self.client.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: tuxemon.core.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.use_item:
            # "Monster used move!"
            context = {
                "user": getattr(user, "name", ''),
                "name": technique.name,
                "target": target.name
            }
            message = T.format(technique.use_item, context)
        else:
            message = ''

        try:
            audio.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)

                element_damage_key = MULT_MAP.get(result['element_multiplier'])
                if element_damage_key:
                    m = T.translate(element_damage_key)
                    message += "\n" + m

                for status in result.get("statuses", []):
                    m = T.format(
                        status.use_item, {
                            "name": technique.name,
                            "user": status.link.name if status.link else "",
                            "target": status.carrier.name
                        })
                    message += "\n" + m

            else:  # assume this was an item used

                # handle the capture device
                if result["capture"]:
                    message += "\n" + T.translate('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, T.translate('gotcha')),
                                  action_time)
                        self._animation_in_progress = True
                        return

                # generic handling of anything else
                else:
                    msg_type = 'use_success' if result[
                        'success'] else 'use_failure'
                    template = getattr(technique, msg_type)
                    if template:
                        message += "\n" + T.translate(template)

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

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

        if result["success"] and target_sprite and 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: tuxemon.core.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(
                        T.format('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 check_status(
                        monster, "status_faint"):
                    self.remove_monster_actions_from_queue(monster)
                    self.faint_monster(monster)

                    # If a monster fainted, exp was given, thus the exp bar should be updated
                    # The exp bar must only be animated for the player's monsters
                    # Enemies don't have a bar, doing it for them will cause a crash
                    for monster in self.monsters_in_play[local_session.player]:
                        self.animate_exp(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: tuxemon.core.technique.Technique
        :rtype: tuxemon.core.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: tuxemon.core.sprite.Sprite
        """
        frame_time = .09
        images = list()
        for fn in technique.images:
            image = graphics.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.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
        for player in self.active_players:
            for mon in player.monsters:
                mon.end_combat()

        # clear action queue
        self._action_queue = list()

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

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

        self.client.push_state("FadeOutTransition", caller=self)
Beispiel #8
0
 def load_graphics(self):
     """ Image become class attribute, so is shared.
         Eventually, implement some game-wide image caching
     """
     image = graphics.load_and_scale(self.border_filename)
     ExpBar.border = GraphicBox(image)