Esempio n. 1
0
    def setup(self,
              level: int,
              load_game: bool = False,
              loaded_game_data: dict = None):
        """
        Method that sets up the given level of the game.
        It also calls other setup methods used in the game.
        Send `load_game_data` only if the game is being loaded from the hard disk
        """
        self.character_sprites = SpriteList()
        self.enemy_sprites = SpriteList()

        self.collectibles_to_omit = []
        self.cur_viewport_coords = ()

        self.level = level

        self.setup_characters()

        # We'll only load the game if this is NOT the first time playing it
        if load_game:
            self.load_game_data(loaded_game_data)
            self.setup_correct_viewport()

        self.setup_sprites(self.level)
        self.update_viewport()
        self.setup_physics_engine()
        self.setup_sounds()
        self.setup_images()
Esempio n. 2
0
    def draw(self,
             center_x: float,
             center_y: float,
             width: float,
             height: float,
             angle: float = 0,
             alpha: float = 1,
             transparent: bool = True,
             repeat_count_x=1,
             repeat_count_y=1):

        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite._texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

        self._sprite.center_x = center_x
        self._sprite.center_y = center_y
        self._sprite.width = width
        self._sprite.height = height
        self._sprite.angle = angle

        self._sprite_list.draw()
Esempio n. 3
0
class PreferredSprite(Sprite):
    def __init__(self, *args):
        super().__init__(*args)
        self.animator = None

    def region(self):
        return RectangularRegion([
            Point(self.left, self.bottom),
            Point(self.right, self.bottom),
            Point(self.right, self.top),
            Point(self.left, self.top)
        ])

    def draw(self):
        if ArcadeLegacy.is_arcade_legacy("2.0.5"):
            self.internal_draw()

    def internal_draw(self):
        self._sprite_list = SpriteList()
        self._sprite_list.append(self)
        self._sprite_list.draw()

    def set_position(self, x, y):
        if ArcadeLegacy.is_arcade_legacy():
            super().set_position(x, y)
        else:
            super().set_position(x, y)
Esempio n. 4
0
    def draw(self):
        """ Draw the sprite. """

        if self._sprite_list is None:
            from arcade import SpriteList
            self._sprite_list = SpriteList()
            self._sprite_list.append(self)

        self._sprite_list.draw()
Esempio n. 5
0
    def _create_cached_sprite(self):
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite.texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)
Esempio n. 6
0
class Texture:
    """
    Class that represents a texture.
    Usually created by the ``load_texture`` or ``load_textures`` commands.

    Attributes:
        :id: ID of the texture as assigned by OpenGL
        :width: Width of the texture image in pixels
        :height: Height of the texture image in pixels

    """
    def __init__(self, name, image=None):
        self.name = name
        self.image = image
        self.scale = 1
        if image:
            self.width = image.width
            self.height = image.height
        else:
            self.width = 0
            self.height = 0

        self._sprite = None

    def draw(self,
             center_x: float,
             center_y: float,
             width: float,
             height: float,
             angle: float = 0,
             alpha: float = 1,
             transparent: bool = True,
             repeat_count_x=1,
             repeat_count_y=1):

        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite._texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

        self._sprite.center_x = center_x
        self._sprite.center_y = center_y
        self._sprite.width = width
        self._sprite.height = height
        self._sprite.angle = angle

        self._sprite_list.draw()
Esempio n. 7
0
    def draw(self,
             center_x: float,
             center_y: float,
             width: float,
             height: float,
             angle: float = 0,
             alpha: int = 255,
             transparent: bool = True,
             repeat_count_x=1,
             repeat_count_y=1):
        """

        Args:
            center_x:
            center_y:
            width:
            height:
            angle:
            alpha: Currently unused.
            transparent:  Currently unused.
            repeat_count_x: Currently unused.
            repeat_count_y:  Currently unused.

        Returns:

        """

        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite._texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

        self._sprite.center_x = center_x
        self._sprite.center_y = center_y
        self._sprite.width = width
        self._sprite.height = height
        self._sprite.angle = angle
        self._sprite.alpha = alpha

        self._sprite_list.draw()
Esempio n. 8
0
def load_collectibles(
        collectibles: SpriteList,
        collectibles_to_remove_pos: list[tuple[float, float]]) -> SpriteList:
    """
    Function that will determine which collectibles (coins, flags, etc.) were already collected during previous gameplays.
    After that, it will remove them from the collectibles SpriteList so that they won't be drawn anymore
    and return a new SpriteList containing only the collectibles which are to be loaded
    """
    loaded_collectibles = SpriteList()

    if collectibles_to_remove_pos:
        for collectible in collectibles:
            if collectible.position not in collectibles_to_remove_pos:
                loaded_collectibles.append(collectible)
    else:
        loaded_collectibles = collectibles

    return loaded_collectibles
Esempio n. 9
0
    def draw(self,
             center_x: float,
             center_y: float,
             width: float = None,
             height: float = None,
             angle: float = 0,
             alpha: int = 255):
        """
        Draw the texture

        :param center_x: x location of where to draw the texture
        :param center_y: y location of where to draw the texture
        :param width: width to draw rectangle. If none, calculated from image size and scale
        :param height: height to draw rectangle. If none, calculated from image size and scale
        :param angle: angle to rotate the texture
        :param alpha: transparency of texture. 0-255
        """

        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite._texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

        self._sprite.center_x = center_x
        self._sprite.center_y = center_y
        if width:
            self._sprite.width = width
        else:
            self._sprite.width = self.image.width * self.scale
        if height:
            self._sprite.height = height
        else:
            self._sprite.height = self.image.height * self.scale
        self._sprite.angle = angle
        self._sprite.alpha = alpha

        self._sprite_list.draw()
Esempio n. 10
0
    def draw_textured_rectangle(center_x: float,
                                center_y: float,
                                width: float,
                                height: float,
                                texture: arcade.Texture,
                                angle: float = 0,
                                alpha: float = 255,
                                repeat_count_x=1,
                                repeat_count_y=1):
        _sprite = arcade.Sprite()
        _sprite._texture = texture
        _sprite.textures = [texture]

        _sprite_list = SpriteList()
        _sprite_list.append(_sprite)

        _sprite.center_x = center_x
        _sprite.center_y = center_y
        _sprite.width = width
        _sprite.height = height
        _sprite.angle = angle
        _sprite.alpha = alpha

        _sprite_list.draw()
Esempio n. 11
0
 def internal_draw(self):
     self._sprite_list = SpriteList()
     self._sprite_list.append(self)
     self._sprite_list.draw()
Esempio n. 12
0
class Texture:
    """
    Class that represents a texture.
    Usually created by the ``load_texture`` or ``load_textures`` commands.

    Attributes:
        :name:
        :image:
        :scale:
        :width: Width of the texture image in pixels
        :height: Height of the texture image in pixels

    """
    def __init__(self, name: str, image=None):
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        self.name = name
        self.texture = None
        self.image = image
        # self.scale = 1
        self._sprite: Optional[Sprite] = None
        self._sprite_list: Optional[SpriteList] = None
        self.unscaled_hitbox_points = None

    # @property
    # def scaled_width(self) -> float:
    #     return self.image.width * self.scale
    #
    # @property
    # def scaled_height(self) -> float:
    #     return self.image.height * self.scale

    @property
    def unscaled_width(self) -> int:
        return self.image.width * self.scale

    @property
    def unscaled_height(self) -> int:
        return self.image.height * self.scale

    # noinspection PyUnusedLocal
    def draw(self,
             center_x: float,
             center_y: float,
             width: float = None,
             height: float = None,
             angle: float = 0,
             alpha: int = 255):
        """
        Draw the texture

        :param center_x: x location of where to draw the texture
        :param center_y: y location of where to draw the texture
        :param width: width to draw rectangle. If none, calculated from image size and scale
        :param height: height to draw rectangle. If none, calculated from image size and scale
        :param angle: angle to rotate the texture
        :param alpha: transparency of texture. 0-255
        """

        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite._texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

        self._sprite.center_x = center_x
        self._sprite.center_y = center_y
        if width:
            self._sprite.width = width
        else:
            self._sprite.width = self.image.width * self.scale
        if height:
            self._sprite.height = height
        else:
            self._sprite.height = self.image.height * self.scale
        self._sprite.angle = angle
        self._sprite.alpha = alpha

        self._sprite_list.draw()
Esempio n. 13
0
class Texture:
    """
    Class that represents a texture.
    Usually created by the :class:`load_texture` or :class:`load_textures` commands.

    Attributes:
        :name: Unique name of the texture. Used by load_textures for caching.
               If you are manually creating a texture, you can just set this
               to whatever.
        :image: A :py:class:`PIL.Image.Image` object.
        :width: Width of the texture in pixels.
        :height: Height of the texture in pixels.
    """
    def __init__(self, name: str, image: PIL.Image.Image = None):
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if image:
            assert isinstance(image, PIL.Image.Image)
        self.name = name
        self.image = image
        self._sprite: Optional[Sprite] = None
        self._sprite_list: Optional[SpriteList] = None
        self.hit_box_points = None

    @property
    def width(self) -> int:
        """
        Width of the texture in pixels.
        """
        if self.image:
            return self.image.width
        else:
            return 0

    @property
    def height(self) -> int:
        """
        Height of the texture in pixels.
        """
        if self.image:
            return self.image.height
        else:
            return 0

    def _create_cached_sprite(self):
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite.texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

    def draw_sized(self,
                   center_x: float,
                   center_y: float,
                   width: float,
                   height: float,
                   angle: float = 0,
                   alpha: int = 255):

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = center_x
            self._sprite.center_y = center_y
            self._sprite.height = height
            self._sprite.width = width
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite_list.draw()

    def draw_transformed(self,
                         left: float,
                         bottom: float,
                         width: float,
                         height: float,
                         angle: float = 0,
                         alpha: int = 255,
                         texture_transform: Matrix3x3 = Matrix3x3()):

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = left + width / 2
            self._sprite.center_y = bottom + height / 2
            self._sprite.width = width
            self._sprite.height = height
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite.texture_transform = texture_transform
            self._sprite_list.draw()

    def draw_scaled(self,
                    center_x: float,
                    center_y: float,
                    scale: float = 1.0,
                    angle: float = 0,
                    alpha: int = 255):
        """
        Draw the texture.

        :param float center_x: X location of where to draw the texture.
        :param float center_y: Y location of where to draw the texture.
        :param float scale: Scale to draw rectangle. Defaults to 1.
        :param float angle: Angle to rotate the texture by.
        :param int alpha: The transparency of the texture `(0-255)`.
        """

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = center_x
            self._sprite.center_y = center_y
            self._sprite.scale = scale
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite_list.draw()
Esempio n. 14
0
class Texture:
    """
    Class that represents a texture.
    Usually created by the ``load_texture`` or ``load_textures`` commands.

    Attributes:
        :id: ID of the texture as assigned by OpenGL
        :width: Width of the texture image in pixels
        :height: Height of the texture image in pixels

    """

    def __init__(self, texture_id: int, width: float, height: float, file_name: str):
        """
        Args:
            :texture_id (str): Id of the texture.
            :width (int): Width of the texture.
            :height (int): Height of the texture.
        Raises:
            :ValueError:

        >>> texture_id = Texture(0, 10, -10)
        Traceback (most recent call last):
        ...
        ValueError: Height entered is less than zero. Height must be a positive float.
        >>> texture_id = Texture(0, -10, 10)
        Traceback (most recent call last):
        ...
        ValueError: Width entered is less than zero. Width must be a positive float.
        """
        # Check values before attempting to create Texture object
        if height < 0:
            raise ValueError("Height entered is less than zero. Height must "
                             "be a positive float.")

        if width < 0:
            raise ValueError("Width entered is less than zero. Width must be "
                             "a positive float.")

        # Values seem to be clear, create object
        self.texture_id = texture_id
        self.width = width
        self.height = height
        self.texture_name = file_name
        self._sprite = None
        self._sprite_list = None

    def draw(self, center_x: float, center_y: float, width: float,
             height: float, angle: float=0,
             alpha: float=1, transparent: bool=True,
             repeat_count_x=1, repeat_count_y=1):

        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite == None:
            self._sprite = Sprite()
            self._sprite.texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

        self._sprite.center_x = center_x
        self._sprite.center_y = center_y
        self._sprite.width = width
        self._sprite.height = height
        self._sprite.angle = angle

        self._sprite_list.draw()
Esempio n. 15
0
def draw_text(text: str,
              start_x: float,
              start_y: float,
              color: Color,
              font_size: float = 12,
              width: int = 0,
              align: str = "left",
              font_name=('calibri', 'arial'),
              bold: bool = False,
              italic: bool = False,
              anchor_x: str = "left",
              anchor_y: str = "baseline",
              rotation: float = 0):
    """

    Args:
        text: Text to draw
        start_x:
        start_y:
        color:
        font_size:
        width:
        align:
        font_name:
        bold:
        italic:
        anchor_x:
        anchor_y:
        rotation:

    Returns:

    """

    # Scale the font up, so it matches with the sizes of the old code back
    # when Pyglet drew the text.
    font_size *= 1.25

    # Text isn't anti-aliased, so we'll draw big, and then shrink
    scale_up = 5
    scale_down = 5

    font_size *= scale_up

    # If the cache gets too large, dump it and start over.
    if len(draw_text.cache) > 5000:
        draw_text.cache = {}

    key = f"{text}{color}{font_size}{width}{align}{font_name}{bold}{italic}"
    if key in draw_text.cache:
        label = draw_text.cache[key]
        text_sprite = label.text_sprite_list[0]

        if anchor_x == "left":
            text_sprite.center_x = start_x + text_sprite.width / 2
        elif anchor_x == "center":
            text_sprite.center_x = start_x
        elif anchor_x == "right":
            text_sprite.right = start_x
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
            )

        if anchor_y == "top":
            text_sprite.center_y = start_y - text_sprite.height / 2
        elif anchor_y == "center":
            text_sprite.center_y = start_y
        elif anchor_y == "bottom" or anchor_y == "baseline":
            text_sprite.bottom = start_y
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_y}'"
            )

        text_sprite.angle = rotation

        label.text_sprite_list.update_positions()
    else:
        label = Text()

        # Figure out the font to use
        font = None

        # Font was specified with a string
        if isinstance(font_name, str):
            try:
                font = PIL.ImageFont.truetype(font_name, int(font_size))
            except OSError:
                # print(f"1 Can't find font: {font_name}")
                pass

            if font is None:
                try:
                    temp_font_name = f"{font_name}.ttf"
                    font = PIL.ImageFont.truetype(temp_font_name,
                                                  int(font_size))
                except OSError:
                    # print(f"2 Can't find font: {temp_font_name}")
                    pass

        # We were instead given a list of font names, in order of preference
        else:
            for font_string_name in font_name:
                try:
                    font = PIL.ImageFont.truetype(font_string_name,
                                                  int(font_size))
                    # print(f"3 Found font: {font_string_name}")
                except OSError:
                    # print(f"3 Can't find font: {font_string_name}")
                    pass

                if font is None:
                    try:
                        temp_font_name = f"{font_name}.ttf"
                        font = PIL.ImageFont.truetype(temp_font_name,
                                                      int(font_size))
                    except OSError:
                        # print(f"4 Can't find font: {temp_font_name}")
                        pass

                if font is not None:
                    break

        # Default font if no font
        if font is None:
            font_names = ("arial.ttf",
                          "/usr/share/fonts/truetype/freefont/FreeMono.ttf",
                          '/System/Library/Fonts/SFNSDisplay.ttf')
            for font_string_name in font_names:
                try:
                    font = PIL.ImageFont.truetype(font_string_name,
                                                  int(font_size))
                    break
                except OSError:
                    # print(f"5 Can't find font: {font_string_name}")
                    pass

        # This is stupid. We have to have an image to figure out what size
        # the text will be when we draw it. Of course, we don't know how big
        # to make the image. Catch-22. So we just make a small image we'll trash
        text_image_size = (10, 10)
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)

        # Get size the text will be
        text_image_size = draw.multiline_textsize(text, font=font)

        # Create image of proper size
        text_height = text_image_size[1]
        text_width = text_image_size[0]

        image_start_x = 0
        if width == 0:
            width = text_image_size[0]
        else:
            # Wait! We were given a field width.
            if align == "center":
                # Center text on given field width
                field_width = width * scale_up
                text_image_size = field_width, text_height
                image_start_x = (field_width - text_width) // 2
                width = field_width
            else:
                image_start_x = 0

        # If we draw a y at 0, then the text is drawn with a baseline of 0,
        # cutting off letters that drop below the baseline. This shoves it
        # up a bit.
        image_start_y = -font_size * scale_up * 0.02
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)

        # Convert to tuple if needed, because the multiline_text does not take a
        # list for a color
        if isinstance(color, list):
            color = tuple(color)
        draw.multiline_text((image_start_x, image_start_y),
                            text,
                            color,
                            align=align,
                            font=font)
        image = image.resize((width // scale_down, text_height // scale_down),
                             resample=PIL.Image.LANCZOS)

        text_sprite = Sprite()
        text_sprite._texture = Texture(key)
        text_sprite._texture.image = image

        text_sprite.image = image
        text_sprite.texture_name = key
        text_sprite.width = image.width
        text_sprite.height = image.height

        if anchor_x == "left":
            text_sprite.center_x = start_x + text_sprite.width / 2
        elif anchor_x == "center":
            text_sprite.center_x = start_x
        elif anchor_x == "right":
            text_sprite.right = start_x
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
            )

        if anchor_y == "top":
            text_sprite.center_y = start_y + text_sprite.height / 2
        elif anchor_y == "center":
            text_sprite.center_y = start_y
        elif anchor_y == "bottom" or anchor_y == "baseline":
            text_sprite.bottom = start_y
        else:
            raise ValueError(
                f"anchor_x should be 'top', 'center', 'bottom', or 'baseline'. Not '{anchor_y}'"
            )

        text_sprite.angle = rotation

        from arcade.sprite_list import SpriteList
        label.text_sprite_list = SpriteList()
        label.text_sprite_list.append(text_sprite)

        draw_text.cache[key] = label

    label.text_sprite_list.draw()
class Texture:
    """
    Class that represents a texture.
    Usually created by the :class:`load_texture` or :class:`load_textures` commands.

    Attributes:
        :name: Unique name of the texture. Used by load_textures for caching.
               If you are manually creating a texture, you can just set this
               to whatever.
        :image: A :py:class:`PIL.Image.Image` object.
        :width: Width of the texture in pixels.
        :height: Height of the texture in pixels.
    """
    def __init__(self,
                 name: str,
                 image: PIL.Image.Image = None,
                 hit_box_algorithm: str = "Simple",
                 hit_box_detail: float = 4.5):
        """
        Create a texture, given a PIL Image object.

        :param str name: Name of texture. Used for caching, so must be unique for each texture.
        :param PIL.Image.Image image: Image to use as a texture.
        :param str hit_box_algorithm: One of 'None', 'Simple' or 'Detailed'. \
        Defaults to 'Simple'. Use 'Simple' for the :data:`PhysicsEngineSimple`, \
        :data:`PhysicsEnginePlatformer` \
        and 'Detailed' for the :data:`PymunkPhysicsEngine`.

            .. figure:: images/hit_box_algorithm_none.png
               :width: 40%

               hit_box_algorithm = "None"

            .. figure:: images/hit_box_algorithm_simple.png
               :width: 55%

               hit_box_algorithm = "Simple"

            .. figure:: images/hit_box_algorithm_detailed.png
               :width: 75%

               hit_box_algorithm = "Detailed"
        :param float hit_box_detail: Float, defaults to 4.5. Used with 'Detailed' to hit box

        """
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if image:
            assert isinstance(image, PIL.Image.Image)
        self.name = name
        self.image = image
        self._sprite: Optional[Sprite] = None
        self._sprite_list: Optional[SpriteList] = None
        self._hit_box_points = None

        if hit_box_algorithm != "Simple" and \
           hit_box_algorithm != "Detailed" and \
           hit_box_algorithm != "None":
            raise ValueError(
                "hit_box_algorithm must be 'Simple', 'Detailed', or 'None'.")
        self._hit_box_algorithm = hit_box_algorithm

        self._hit_box_detail = hit_box_detail

    @property
    def width(self) -> int:
        """
        Width of the texture in pixels.
        """
        if self.image:
            return self.image.width
        else:
            return 0

    @property
    def height(self) -> int:
        """
        Height of the texture in pixels.
        """
        if self.image:
            return self.image.height
        else:
            return 0

    @property
    def hit_box_points(self):
        if self._hit_box_points is not None:
            return self._hit_box_points
        else:
            if self._hit_box_algorithm == "Simple":
                self._hit_box_points = calculate_hit_box_points_simple(
                    self.image)
            elif self._hit_box_algorithm == "Detailed":
                self._hit_box_points = calculate_hit_box_points_detailed(
                    self.image, self._hit_box_detail)
            else:
                p1 = (-self.image.width / 2, -self.image.height / 2)
                p2 = (self.image.width / 2, -self.image.height / 2)
                p3 = (self.image.width / 2, self.image.height / 2)
                p4 = (-self.image.width / 2, self.image.height / 2)

                self._hit_box_points = p1, p2, p3, p4

            return self._hit_box_points

    def _create_cached_sprite(self):
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite.texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

    def draw_sized(self,
                   center_x: float,
                   center_y: float,
                   width: float,
                   height: float,
                   angle: float = 0,
                   alpha: int = 255):

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = center_x
            self._sprite.center_y = center_y
            self._sprite.height = height
            self._sprite.width = width
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite_list.draw()

    def draw_transformed(self,
                         left: float,
                         bottom: float,
                         width: float,
                         height: float,
                         angle: float = 0,
                         alpha: int = 255,
                         texture_transform: Matrix3x3 = Matrix3x3()):

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = left + width / 2
            self._sprite.center_y = bottom + height / 2
            self._sprite.width = width
            self._sprite.height = height
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite.texture_transform = texture_transform
            self._sprite_list.draw()

    def draw_scaled(self,
                    center_x: float,
                    center_y: float,
                    scale: float = 1.0,
                    angle: float = 0,
                    alpha: int = 255):
        """
        Draw the texture.

        :param float center_x: X location of where to draw the texture.
        :param float center_y: Y location of where to draw the texture.
        :param float scale: Scale to draw rectangle. Defaults to 1.
        :param float angle: Angle to rotate the texture by.
        :param int alpha: The transparency of the texture `(0-255)`.
        """

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = center_x
            self._sprite.center_y = center_y
            self._sprite.scale = scale
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite_list.draw()
Esempio n. 17
0
def draw_text(text: str,
              start_x: float,
              start_y: float,
              color: Color,
              font_size: float = 12,
              width: int = 0,
              align: str = "left",
              font_name: Union[str, Tuple[str, ...]] = ('calibri', 'arial'),
              bold: bool = False,
              italic: bool = False,
              anchor_x: str = "left",
              anchor_y: str = "baseline",
              rotation: float = 0) -> Sprite:
    """

    Draws text to the screen.

    Internally this works by creating an image, and using the Pillow library to
    draw the text to it. Then use that image to create a sprite. We cache the sprite
    (so we don't have to recreate over and over, which is slow) and use it to
    draw text to the screen.

    This implementation does not support bold/italic like the older Pyglet-based
    implementation of draw_text. However if you specify the 'italic' or 'bold'
    version of the font via the font name, you will get that font. Just the booleans
    do not work.

    :param str text: Text to draw
    :param float start_x: x coordinate of the lower-left point to start drawing text
    :param float start_y: y coordinate of the lower-left point to start drawing text
    :param Color color: Color of the text
    :param float font_size: Size of the text
    :param float width: Width of the text-box for the text to go into. Used with alignment.
    :param str align: Align left, right, center
    :param Union[str, Tuple[str, ...]] font_name: Font name, or list of font names in order of preference
    :param bool bold: Bold the font (currently unsupported)
    :param bool italic: Italicize the font (currently unsupported)
    :param str anchor_x: Anchor the font location, defaults to 'left'
    :param str anchor_y: Anchor the font location, defaults to 'baseline'
    :param float rotation: Rotate the text
    """
    global draw_text_cache

    # If the cache gets too large, dump it and start over.
    if len(draw_text_cache) > 5000:
        draw_text_cache = {}

    r, g, b, alpha = get_four_byte_color(color)
    cache_color = f"{r}{g}{b}"

    key = f"{text}{cache_color}{font_size}{width}{align}{font_name}{bold}{italic}"

    try:
        label = draw_text_cache[key]
    except KeyError:  # doesn't exist, create it

        image = get_text_image(text=text,
                               text_color=color,
                               font_size=font_size,
                               width=width,
                               align=align,
                               font_name=font_name)
        text_sprite = Sprite()
        text_sprite._texture = Texture(key)
        text_sprite.texture.image = image

        text_sprite.width = image.width
        text_sprite.height = image.height

        from arcade.sprite_list import SpriteList

        label = Text()
        label.text_sprite_list = SpriteList()
        label.text_sprite_list.append(text_sprite)

        draw_text_cache[key] = label

    text_sprite = label.text_sprite_list[0]

    if anchor_x == "left":
        text_sprite.center_x = start_x + text_sprite.width / 2
    elif anchor_x == "center":
        text_sprite.center_x = start_x
    elif anchor_x == "right":
        text_sprite.right = start_x
    else:
        raise ValueError(
            f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
        )

    if anchor_y == "top":
        text_sprite.center_y = start_y - text_sprite.height / 2
    elif anchor_y == "center":
        text_sprite.center_y = start_y
    elif anchor_y == "bottom" or anchor_y == "baseline":
        text_sprite.center_y = start_y + text_sprite.height / 2
    else:
        raise ValueError(
            f"anchor_y should be 'top', 'center', 'bottom', or 'baseline'. Not '{anchor_y}'"
        )

    text_sprite.angle = rotation
    text_sprite.alpha = alpha

    label.text_sprite_list.draw()
    return text_sprite
Esempio n. 18
0
class Texture:
    """
    Class that represents a texture.
    Usually created by the ``load_texture`` or ``load_textures`` commands.

    Attributes:
        :name: Unique name of the texture. Used by load_textures for caching.
               If you are manually creating a texture, you can just set this
               to whatever.
        :image: PIL image
        :width: Width of the texture image in pixels
        :height: Height of the texture image in pixels

    """

    def __init__(self, name: str, image: PIL.Image = None):
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        self.name = name
        self.image = image
        self._sprite: Optional[Sprite] = None
        self._sprite_list: Optional[SpriteList] = None
        self.hit_box_points = None

    @property
    def width(self) -> int:
        """
        Width of the texture in pixels
        """
        if self.image:
            return self.image.width
        else:
            return 0

    @property
    def height(self) -> int:
        """
        Height of the texture in pixels
        """
        if self.image:
            return self.image.height
        else:
            return 0

    def _create_cached_sprite(self):
        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite.texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

    def draw_sized(self,
                   center_x: float, center_y: float,
                   width: float,
                   height: float,
                   angle: float = 0,
                   alpha: int = 255):

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = center_x
            self._sprite.center_y = center_y
            self._sprite.height = height
            self._sprite.width = width
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite_list.draw()

    def draw_scaled(self, center_x: float, center_y: float,
                    scale: float = 1.0,
                    angle: float = 0,
                    alpha: int = 255):

        """
        Draw the texture

        :param center_x: x location of where to draw the texture
        :param center_y: y location of where to draw the texture
        :param scale: Scale to draw rectangle. If none, defaults to 1
        :param angle: angle to rotate the texture
        :param alpha: transparency of texture. 0-255
        """

        self._create_cached_sprite()
        if self._sprite and self._sprite_list:
            self._sprite.center_x = center_x
            self._sprite.center_y = center_y
            self._sprite.scale = scale
            self._sprite.angle = angle
            self._sprite.alpha = alpha
            self._sprite_list.draw()
Esempio n. 19
0
def draw_text(text: str,
              start_x: float,
              start_y: float,
              color: Color,
              font_size: float = 12,
              width: int = 0,
              align: str = "left",
              font_name: Union[str, Tuple[str, ...]] = ('calibri', 'arial'),
              bold: bool = False,
              italic: bool = False,
              anchor_x: str = "left",
              anchor_y: str = "baseline",
              rotation: float = 0) -> Sprite:

    # This is a custom version of arcade's draw_text function, because the normal one doesn't work

    global draw_text_cache

    # Scale the font up, so it matches with the sizes of the old code back
    # when Pyglet drew the text.
    font_size *= 1.25

    # Text isn't anti-aliased, so we'll draw big, and then shrink
    scale_up = 2
    scale_down = 2

    font_size *= scale_up

    # If the cache gets too large, dump it and start over.
    if len(draw_text_cache) > 5000:
        draw_text_cache = {}

    r, g, b, alpha = get_four_byte_color(color)
    cache_color = f"{r}{g}{b}"

    key = f"{text}{cache_color}{font_size}{width}{align}{font_name}{bold}{italic}"
    try:
        label = draw_text_cache[key]
    except KeyError:  # doesn't exist, create it
        label = Text()

        # Figure out the font to use
        font = None

        # Font was specified with a string
        if isinstance(font_name, str):
            font_name = font_name,

        font_names = chain(
            *[[font_string_name, f"{font_string_name}.ttf"]
              for font_string_name in font_name], DEFAULT_FONT_NAMES)

        font_found = False
        for font_string_name in font_names:
            try:
                font = ImageFont.truetype(font_string_name,
                                          int(font_size),
                                          layout_engine=ImageFont.LAYOUT_BASIC)
            except OSError:
                continue
            else:
                font_found = True
                break

        if not font_found:
            raise RuntimeError(
                "Unable to find a default font on this system. Please specify an available font."
            )

        # This is stupid. We have to have an image to figure out what size
        # the text will be when we draw it. Of course, we don't know how big
        # to make the image. Catch-22. So we just make a small image we'll trash
        text_image_size = (10, 10)
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)

        # Get size the text will be
        text_image_size = draw.multiline_textsize(text, font=font)
        # Add some extra pixels at the bottom to account for letters that drop below the baseline.
        text_image_size = text_image_size[0], text_image_size[1] + int(
            font_size * 0.25)

        # Create image of proper size
        text_height = text_image_size[1]
        text_width = text_image_size[0]

        image_start_x = 0
        if width == 0:
            width = text_image_size[0]
        else:
            # Wait! We were given a field width.
            if align == "center":
                # Center text on given field width
                field_width = width * scale_up
                text_image_size = field_width, text_height
                image_start_x = (field_width - text_width) // 2
                width = field_width
            else:
                image_start_x = 0

        # Find y of top-left corner
        image_start_y = 0

        # Create image
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)

        # Convert to tuple if needed, because the multiline_text does not take a
        # list for a color
        if isinstance(color, list):
            color = cast(RGBA, tuple(color))
        draw.multiline_text((image_start_x, image_start_y),
                            text,
                            color,
                            align=align,
                            font=font)
        image = image.resize(
            (max(1, width // scale_down), text_height // scale_down),
            resample=PIL.Image.LANCZOS)

        text_sprite = Sprite()
        text_sprite._texture = Texture(key)
        text_sprite.texture.image = image

        text_sprite.width = image.width
        text_sprite.height = image.height

        from arcade.sprite_list import SpriteList
        label.text_sprite_list = SpriteList()
        label.text_sprite_list.append(text_sprite)

        draw_text_cache[key] = label

    text_sprite = label.text_sprite_list[0]

    if anchor_x == "left":
        text_sprite.center_x = start_x + text_sprite.width / 2
    elif anchor_x == "center":
        text_sprite.center_x = start_x
    elif anchor_x == "right":
        text_sprite.right = start_x
    else:
        raise ValueError(
            f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
        )

    if anchor_y == "top":
        text_sprite.center_y = start_y - text_sprite.height / 2
    elif anchor_y == "center":
        text_sprite.center_y = start_y
    elif anchor_y == "bottom" or anchor_y == "baseline":
        text_sprite.bottom = start_y
    else:
        raise ValueError(
            f"anchor_y should be 'top', 'center', 'bottom', or 'baseline'. Not '{anchor_y}'"
        )

    text_sprite.angle = rotation
    text_sprite.alpha = alpha

    label.text_sprite_list.draw()
    return text_sprite
Esempio n. 20
0
    def __init__(self, default_player_speed, level):
        # creates a new Sprite
        super().__init__()

        # set up sprite lists for the backgrounds
        self.bg_sky_list = SpriteList()  # the two skies (back)
        self.bg_skyline_list = SpriteList()  # the two skylines (middle)
        self.bg_building_list = SpriteList()  # the two buildings (front)
        self.bg_all_list = SpriteList()  # all six sprites

        # time of day chosen based on level selected by user
        self.theme = ""
        if level == 1:
            self.theme = "day"
        elif level == 2:
            self.theme = "dusk"
        elif level == 3:
            self.theme = "night"
        else:
            self.theme = "dawn"

        # set up file sources as dictated by previous if-else block
        bg_sky_img = "assets-src/bg_{}_0.png"
        bg_sky_img = bg_sky_img.format(self.theme)
        bg_skyline_img = "assets-src/bg_{}_1.png"
        bg_skyline_img = bg_skyline_img.format(self.theme)
        bg_buildings_img = "assets-src/bg_{}_2.png"
        bg_buildings_img = bg_buildings_img.format(self.theme)

        # set up speeds for each background layer
        self.default_player_speed = default_player_speed
        bg_speed_0 = self.default_player_speed * 0.90  # 90% of player speed
        bg_speed_1 = self.default_player_speed * 0.80  # 80% of player speed
        bg_speed_2 = self.default_player_speed * 0.60  # 60% of player speed

        # set up first sky (back) image
        bg_sprite_0a = arcade.Sprite(bg_sky_img)
        bg_sprite_0a.center_x = 1000
        bg_sprite_0a.center_y = 325
        bg_sprite_0a.change_x = bg_speed_0
        self.bg_sky_list.append(bg_sprite_0a)

        # set up second sky (back) image
        bg_sprite_0b = arcade.Sprite(bg_sky_img)
        bg_sprite_0b.center_x = -1000
        bg_sprite_0b.center_y = 325
        bg_sprite_0b.change_x = bg_speed_0
        self.bg_sky_list.append(bg_sprite_0b)

        # set up first skyline (middle) image
        bg_sprite_1a = arcade.Sprite(bg_skyline_img)
        bg_sprite_1a.center_x = 1000
        bg_sprite_1a.center_y = 325
        bg_sprite_1a.change_x = bg_speed_1
        self.bg_skyline_list.append(bg_sprite_1a)

        # set up second skyline (middle) image
        bg_sprite_1b = arcade.Sprite(bg_skyline_img)
        bg_sprite_1b.center_x = -1000
        bg_sprite_1b.center_y = 325
        bg_sprite_1b.change_x = bg_speed_1
        self.bg_skyline_list.append(bg_sprite_1b)

        # set up first building (front) image
        bg_sprite_2a = arcade.Sprite(bg_buildings_img)
        bg_sprite_2a.center_x = 1000
        bg_sprite_2a.center_y = 325
        bg_sprite_2a.change_x = bg_speed_2
        self.bg_building_list.append(bg_sprite_2a)

        # set up second building (front) image
        bg_sprite_2b = arcade.Sprite(bg_buildings_img)
        bg_sprite_2b.center_x = -1000
        bg_sprite_2b.center_y = 325
        bg_sprite_2b.change_x = bg_speed_2
        self.bg_building_list.append(bg_sprite_2b)

        # combine all backgrounds into one nice list for later
        self.bg_all_list.extend(self.bg_sky_list)
        self.bg_all_list.extend(self.bg_skyline_list)
        self.bg_all_list.extend(self.bg_building_list)
Esempio n. 21
0
class Background(arcade.Sprite):
    """ 
    __init__ - sets up our background sprites
    """
    def __init__(self, default_player_speed, level):
        # creates a new Sprite
        super().__init__()

        # set up sprite lists for the backgrounds
        self.bg_sky_list = SpriteList()  # the two skies (back)
        self.bg_skyline_list = SpriteList()  # the two skylines (middle)
        self.bg_building_list = SpriteList()  # the two buildings (front)
        self.bg_all_list = SpriteList()  # all six sprites

        # time of day chosen based on level selected by user
        self.theme = ""
        if level == 1:
            self.theme = "day"
        elif level == 2:
            self.theme = "dusk"
        elif level == 3:
            self.theme = "night"
        else:
            self.theme = "dawn"

        # set up file sources as dictated by previous if-else block
        bg_sky_img = "assets-src/bg_{}_0.png"
        bg_sky_img = bg_sky_img.format(self.theme)
        bg_skyline_img = "assets-src/bg_{}_1.png"
        bg_skyline_img = bg_skyline_img.format(self.theme)
        bg_buildings_img = "assets-src/bg_{}_2.png"
        bg_buildings_img = bg_buildings_img.format(self.theme)

        # set up speeds for each background layer
        self.default_player_speed = default_player_speed
        bg_speed_0 = self.default_player_speed * 0.90  # 90% of player speed
        bg_speed_1 = self.default_player_speed * 0.80  # 80% of player speed
        bg_speed_2 = self.default_player_speed * 0.60  # 60% of player speed

        # set up first sky (back) image
        bg_sprite_0a = arcade.Sprite(bg_sky_img)
        bg_sprite_0a.center_x = 1000
        bg_sprite_0a.center_y = 325
        bg_sprite_0a.change_x = bg_speed_0
        self.bg_sky_list.append(bg_sprite_0a)

        # set up second sky (back) image
        bg_sprite_0b = arcade.Sprite(bg_sky_img)
        bg_sprite_0b.center_x = -1000
        bg_sprite_0b.center_y = 325
        bg_sprite_0b.change_x = bg_speed_0
        self.bg_sky_list.append(bg_sprite_0b)

        # set up first skyline (middle) image
        bg_sprite_1a = arcade.Sprite(bg_skyline_img)
        bg_sprite_1a.center_x = 1000
        bg_sprite_1a.center_y = 325
        bg_sprite_1a.change_x = bg_speed_1
        self.bg_skyline_list.append(bg_sprite_1a)

        # set up second skyline (middle) image
        bg_sprite_1b = arcade.Sprite(bg_skyline_img)
        bg_sprite_1b.center_x = -1000
        bg_sprite_1b.center_y = 325
        bg_sprite_1b.change_x = bg_speed_1
        self.bg_skyline_list.append(bg_sprite_1b)

        # set up first building (front) image
        bg_sprite_2a = arcade.Sprite(bg_buildings_img)
        bg_sprite_2a.center_x = 1000
        bg_sprite_2a.center_y = 325
        bg_sprite_2a.change_x = bg_speed_2
        self.bg_building_list.append(bg_sprite_2a)

        # set up second building (front) image
        bg_sprite_2b = arcade.Sprite(bg_buildings_img)
        bg_sprite_2b.center_x = -1000
        bg_sprite_2b.center_y = 325
        bg_sprite_2b.change_x = bg_speed_2
        self.bg_building_list.append(bg_sprite_2b)

        # combine all backgrounds into one nice list for later
        self.bg_all_list.extend(self.bg_sky_list)
        self.bg_all_list.extend(self.bg_skyline_list)
        self.bg_all_list.extend(self.bg_building_list)

    """ 
    Update - updates/animates the background sprites frame-by-frame
    """

    def update(self, player_speed, player_location):

        # if the player is moving, all backgrounds can move
        if player_speed == self.default_player_speed:
            for sprite in self.bg_all_list:
                sprite.center_x += sprite.change_x

        # if the player isn't moving (aka dead), only the sky should move
        elif player_speed == 0:
            for sprite in self.bg_sky_list:
                # speed is now -0.15 to simulate leisurely-moving clouds
                # while the camera is not moving
                sprite.center_x += -0.15
        """
        A visual for the background images' unique updating.

        If the player (P) is past the middle of the image (A)...

        [  B  ][  A  ]
                  P
        
        ...then the other image (B) will "hop" in front of (A)...

               [  A  ][  B  ]
                  P

        ...and this pattern will continue on forever. It even works backwards,
        such as when the sky moves backwards once the player is dead.
        """

        # substitues for sky images's positions that are less unwieldy
        sky_0_x = self.bg_sky_list[0].center_x
        sky_1_x = self.bg_sky_list[1].center_x

        # if the player is in the middle of an image, and the other image is
        # behind, then the other "hops" in front by 4000 pixels
        if sky_0_x < player_location and sky_0_x > sky_1_x:
            self.bg_sky_list[1].center_x += 4000
        if sky_1_x < player_location and sky_1_x > sky_0_x:
            self.bg_sky_list[0].center_x += 4000

        # substitues for skyline images' positions that are less unwieldy
        skyline_0_x = self.bg_skyline_list[0].center_x
        skyline_1_x = self.bg_skyline_list[1].center_x

        # if the player is in the middle of an image, and the other image is
        # behind, then the other "hops" in front by 4000 pixels
        if skyline_0_x < player_location and skyline_0_x > skyline_1_x:
            self.bg_skyline_list[1].center_x += 4000
        if skyline_1_x < player_location and skyline_1_x > skyline_0_x:
            self.bg_skyline_list[0].center_x += 4000

        # substitues for buildings images' positions that are less unwieldy
        buildings_0_x = self.bg_building_list[0].center_x
        buildings_1_x = self.bg_building_list[1].center_x

        # if the player is in the middle of an image, and the other image is
        # behind, then the other "hops" in front by 4000 pixels
        if buildings_0_x < player_location and buildings_0_x > buildings_1_x:
            self.bg_building_list[1].center_x += 4000
        if buildings_1_x < player_location and buildings_1_x > buildings_0_x:
            self.bg_building_list[0].center_x += 4000

    """ 
    Draw - allows all six sprites to be drawn in the arcade view. 
    """

    def draw(self):
        # use the list of all sprites to draw them, nice and easy!
        for sprite in self.bg_all_list:
            sprite.draw()
Esempio n. 22
0
def draw_text(text: str,
              start_x: float,
              start_y: float,
              color: Color,
              font_size: float = 12,
              width: int = 0,
              align: str = "left",
              font_name: Union[str, Tuple[str, ...]] = ('calibri', 'arial'),
              bold: bool = False,
              italic: bool = False,
              anchor_x: str = "left",
              anchor_y: str = "baseline",
              rotation: float = 0):
    """

    :param str text: Text to draw
    :param float start_x:
    :param float start_y:
    :param Color color: Color of the text
    :param float font_size: Size of the text
    :param float width:
    :param str align:
    :param Union[str, Tuple[str, ...]] font_name:
    :param bool bold:
    :param bool italic:
    :param str anchor_x:
    :param str anchor_y:
    :param float rotation:
    """

    # Scale the font up, so it matches with the sizes of the old code back
    # when Pyglet drew the text.
    font_size *= 1.25

    # Text isn't anti-aliased, so we'll draw big, and then shrink
    scale_up = 5
    scale_down = 5

    font_size *= scale_up

    # If the cache gets too large, dump it and start over.
    if len(draw_text.cache
           ) > 5000:  # type: ignore # dynamic attribute on function obj
        draw_text.cache = {
        }  # type: ignore # dynamic attribute on function obj

    key = f"{text}{color}{font_size}{width}{align}{font_name}{bold}{italic}"
    if key in draw_text.cache:  # type: ignore # dynamic attribute on function obj
        label = draw_text.cache[
            key]  # type: ignore # dynamic attribute on function obj
        text_sprite = label.text_sprite_list[0]

        if anchor_x == "left":
            text_sprite.center_x = start_x + text_sprite.width / 2
        elif anchor_x == "center":
            text_sprite.center_x = start_x
        elif anchor_x == "right":
            text_sprite.right = start_x
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
            )

        if anchor_y == "top":
            text_sprite.center_y = start_y - text_sprite.height / 2
        elif anchor_y == "center":
            text_sprite.center_y = start_y
        elif anchor_y == "bottom" or anchor_y == "baseline":
            text_sprite.bottom = start_y
        else:
            raise ValueError(
                f"anchor_y should be 'top', 'center', 'bottom', or 'baseline'. Not '{anchor_y}'"
            )

        text_sprite.angle = rotation
    else:
        label = Text()

        # Figure out the font to use
        font = None

        # Font was specified with a string
        if isinstance(font_name, str):
            font_name = [font_name]

        font_names = chain(
            *[[font_string_name, f"{font_string_name}.ttf"]
              for font_string_name in font_name], DEFAULT_FONT_NAMES)

        font_found = False
        for font_string_name in font_names:
            try:
                font = PIL.ImageFont.truetype(font_string_name, int(font_size))
            except OSError:
                continue
            else:
                font_found = True
                break

        if not font_found:
            raise RuntimeError(
                "Unable to find a default font on this system. Please specify an available font."
            )

        # This is stupid. We have to have an image to figure out what size
        # the text will be when we draw it. Of course, we don't know how big
        # to make the image. Catch-22. So we just make a small image we'll trash
        text_image_size = (10, 10)
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)

        # Get size the text will be
        text_image_size = draw.multiline_textsize(text, font=font)

        # Create image of proper size
        text_height = text_image_size[1]
        text_width = text_image_size[0]

        image_start_x = 0
        if width == 0:
            width = text_image_size[0]
        else:
            # Wait! We were given a field width.
            if align == "center":
                # Center text on given field width
                field_width = width * scale_up
                text_image_size = field_width, text_height
                image_start_x = (field_width - text_width) // 2
                width = field_width
            else:
                image_start_x = 0

        # If we draw a y at 0, then the text is drawn with a baseline of 0,
        # cutting off letters that drop below the baseline. This shoves it
        # up a bit.
        image_start_y = -font_size * scale_up * 0.02
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)

        # Convert to tuple if needed, because the multiline_text does not take a
        # list for a color
        if isinstance(color, list):
            color = cast(RGBA, tuple(color))
        draw.multiline_text((image_start_x, image_start_y),
                            text,
                            color,
                            align=align,
                            font=font)
        image = image.resize((width // scale_down, text_height // scale_down),
                             resample=PIL.Image.LANCZOS)

        text_sprite = Sprite()
        text_sprite._texture = Texture(key)
        text_sprite.texture.image = image

        text_sprite.image = image
        text_sprite.texture_name = key
        text_sprite.width = image.width
        text_sprite.height = image.height

        if anchor_x == "left":
            text_sprite.center_x = start_x + text_sprite.width / 2
        elif anchor_x == "center":
            text_sprite.center_x = start_x
        elif anchor_x == "right":
            text_sprite.right = start_x
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
            )

        if anchor_y == "top":
            text_sprite.center_y = start_y + text_sprite.height / 2
        elif anchor_y == "center":
            text_sprite.center_y = start_y
        elif anchor_y == "bottom" or anchor_y == "baseline":
            text_sprite.bottom = start_y
        else:
            raise ValueError(
                f"anchor_y should be 'top', 'center', 'bottom', or 'baseline'. Not '{anchor_y}'"
            )

        text_sprite.angle = rotation

        from arcade.sprite_list import SpriteList
        label.text_sprite_list = SpriteList()
        label.text_sprite_list.append(text_sprite)

        draw_text.cache[
            key] = label  # type: ignore # dynamic attribute on function obj

    label.text_sprite_list.draw()
Esempio n. 23
0
class Sprite:
    """
    Class that represents a 'sprite' on-screen. Most games center around sprites.
    For examples on how to use this class, see:
    http://arcade.academy/examples/index.html#sprites

    Attributes:
        :alpha: Transparency of sprite. 0 is invisible, 255 is opaque.
        :angle: Rotation angle in degrees.
        :radians: Rotation angle in radians.
        :bottom: Set/query the sprite location by using the bottom coordinate. \
        This will be the 'y' of the bottom of the sprite.
        :boundary_left: Used in movement. Left boundary of moving sprite.
        :boundary_right: Used in movement. Right boundary of moving sprite.
        :boundary_top: Used in movement. Top boundary of moving sprite.
        :boundary_bottom: Used in movement. Bottom boundary of moving sprite.
        :center_x: X location of the center of the sprite
        :center_y: Y location of the center of the sprite
        :change_x: Movement vector, in the x direction.
        :change_y: Movement vector, in the y direction.
        :change_angle: Change in rotation.
        :color: Color tint the sprite
        :collision_radius: Used as a fast-check to see if this item is close \
        enough to another item. If this check works, we do a slower more accurate check. \
        You probably don't want to use this field. Instead, set points in the \
        hit box.
        :cur_texture_index: Index of current texture being used.
        :guid: Unique identifier for the sprite. Useful when debugging.
        :height: Height of the sprite.
        :force: Force being applied to the sprite. Useful when used with Pymunk \
        for physics.
        :left: Set/query the sprite location by using the left coordinate. This \
        will be the 'x' of the left of the sprite.
        :points: Points, in relation to the center of the sprite, that are used \
        for collision detection. Arcade defaults to creating points for a rectangle \
        that encompass the image. If you are creating a ramp or making better \
        hit-boxes, you can custom-set these.
        :position: A list with the (x, y) of where the sprite is.
        :repeat_count_x: Unused
        :repeat_count_y: Unused
        :right: Set/query the sprite location by using the right coordinate. \
        This will be the 'y=x' of the right of the sprite.
        :sprite_lists: List of all the sprite lists this sprite is part of.
        :texture: `Texture` class with the current texture.
        :textures: List of textures associated with this sprite.
        :top: Set/query the sprite location by using the top coordinate. This \
        will be the 'y' of the top of the sprite.
        :scale: Scale the image up or down. Scale of 1.0 is original size, 0.5 \
        is 1/2 height and width.
        :velocity: Change in x, y expressed as a list. (0, 0) would be not moving.
        :width: Width of the sprite

    It is common to over-ride the `update` method and provide mechanics on
    movement or other sprite updates.

    """
    def __init__(self,
                 filename: str = None,
                 scale: float = 1,
                 image_x: float = 0,
                 image_y: float = 0,
                 image_width: float = 0,
                 image_height: float = 0,
                 center_x: float = 0,
                 center_y: float = 0,
                 repeat_count_x: int = 1,
                 repeat_count_y: int = 1):
        """
        Create a new sprite.

        Args:
            filename (str): Filename of an image that represents the sprite.
            scale (float): Scale the image up or down. Scale of 1.0 is none.
            image_x (float): X offset to sprite within sprite sheet.
            image_y (float): Y offset to sprite within sprite sheet.
            image_width (float): Width of the sprite
            image_height (float): Height of the sprite
            center_x (float): Location of the sprite
            center_y (float): Location of the sprite

        """

        if image_width < 0:
            raise ValueError("Width of image can't be less than zero.")

        if image_height < 0:
            raise ValueError(
                "Height entered is less than zero. Height must be a positive float."
            )

        if image_width == 0 and image_height != 0:
            raise ValueError("Width can't be zero.")

        if image_height == 0 and image_width != 0:
            raise ValueError("Height can't be zero.")

        self.sprite_lists: List[Any] = []

        self._texture: Optional[Texture]
        if filename is not None:
            try:
                self._texture = load_texture(filename, image_x, image_y,
                                             image_width, image_height)
            except Exception as e:
                print(f"Unable to load {filename} {e}")
                self._texture = None

            if self._texture:
                self.textures = [self._texture]
                # Ignore the texture's scale and use ours
                self._width = self._texture.width * scale
                self._height = self._texture.height * scale
            else:
                self.textures = []
                self._width = 0
                self._height = 0
        else:
            self.textures = []
            self._texture = None
            self._width = 0
            self._height = 0

        self.cur_texture_index = 0

        self._scale = scale
        self._position = (center_x, center_y)
        self._angle = 0.0

        self.velocity = [0.0, 0.0]
        self.change_angle = 0.0

        self.boundary_left = None
        self.boundary_right = None
        self.boundary_top = None
        self.boundary_bottom = None

        self.properties: Dict[str, Any] = {}

        self._alpha = 255
        self._collision_radius: Optional[float] = None
        self._color: RGB = (255, 255, 255)

        self._points: Optional[List[List[float]]] = None

        if self._texture:
            self._points = self._texture.hit_box_points

        self._point_list_cache: Optional[List[List[float]]] = None

        self.force = [0, 0]
        self.guid: Optional[str] = None

        self.repeat_count_x = repeat_count_x
        self.repeat_count_y = repeat_count_y
        self._texture_transform = Matrix3x3()

        # Used if someone insists on doing a sprite.draw()
        self._sprite_list = None

    def append_texture(self, texture: Texture):
        """
        Appends a new texture to the list of textures that can be
        applied to this sprite.

        :param Texture texture: Texture to add ot the list of available textures

        """
        self.textures.append(texture)

    def _get_position(self) -> Tuple[float, float]:
        """
        Get the center x and y coordinates of the sprite.

        Returns:
            (center_x, center_y)
        """
        return self._position

    def _set_position(self, new_value: Tuple[float, float]):
        """
        Set the center x and y coordinates of the sprite.

        Args:
            new_value:

        Returns:

        """
        if new_value[0] != self._position[0] or new_value[1] != self._position[
                1]:
            self.clear_spatial_hashes()
            self._point_list_cache = None
            self._position = new_value
            self.add_spatial_hashes()

            for sprite_list in self.sprite_lists:
                sprite_list.update_location(self)

    position = property(_get_position, _set_position)

    def set_position(self, center_x: float, center_y: float):
        """
        Set a sprite's position

        :param float center_x: New x position of sprite
        :param float center_y: New y position of sprite
        """
        self._set_position((center_x, center_y))

    def set_points(self, points: List[List[float]]):
        """
        Set a sprite's hitbox
        """
        from warnings import warn
        warn('set_points has been deprecated. Use set_hit_box instead.',
             DeprecationWarning)

        self._points = points

    def get_points(self) -> List[List[float]]:
        """
        Get the points that make up the hit box for the rect that makes up the
        sprite, including rotation and scaling.
        """
        from warnings import warn
        warn('get_points has been deprecated. Use get_hit_box instead.',
             DeprecationWarning)

        return self.get_adjusted_hit_box()

    points = property(get_points, set_points)

    def set_hit_box(self, points: List[List[float]]):
        """
        Set a sprite's hit box. Hitbox should be relative to a sprite's center,
        and with a scale of 1.0.
        Points will be scaled with get_adjusted_hit_box.
        """
        self._points = points

    def get_hit_box(self) -> Optional[List[List[float]]]:
        """
        Get a sprite's hit box, unadjusted for translation, rotation, or scale.
        """
        return self._points

    hit_box = property(get_hit_box, set_hit_box)

    def get_adjusted_hit_box(self) -> List[List[float]]:
        """
        Get the points that make up the hit box for the rect that makes up the
        sprite, including rotation and scaling.
        """

        # If we've already calculated the adjusted hit box, use the cached version
        if self._point_list_cache is not None:
            return self._point_list_cache

        # If there is no hitbox, use the width/height to get one
        if self._points is None and self._texture:
            self._points = self._texture.hit_box_points

        if self._points is None and self._width:
            x1, y1 = -self._width / 2, -self._height / 2
            x2, y2 = +self._width / 2, -self._height / 2
            x3, y3 = +self._width / 2, +self._height / 2
            x4, y4 = -self._width / 2, +self._height / 2

            self._points = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]

        if self._points is None and self.texture is not None:
            self._points = self.texture.hit_box_points

        if self._points is None:
            raise ValueError(
                "Error trying to get the hit box of a sprite, when no hit box is set.\nPlease make sure the "
                "Sprite.texture is set to a texture before trying to draw or do collision testing.\n"
                "Alternatively, manually call Sprite.set_hit_box with points for your hitbox."
            )

        # Adjust the hitbox
        point_list = []
        for point in self._points:
            # Get a copy of the point
            point = [point[0], point[1]]

            # Scale the point
            if self.scale != 1:
                point[0] *= self.scale
                point[1] *= self.scale

            # Rotate the point
            if self.angle:
                point = rotate_point(point[0], point[1], 0, 0, self.angle)

            # Offset the point
            point = [point[0] + self.center_x, point[1] + self.center_y]
            point_list.append(point)

        # Cache the results
        self._point_list_cache = point_list

        # if self.texture:
        #     print(self.texture.name, self._point_list_cache)

        return self._point_list_cache

    def forward(self, speed: float = 1.0):
        """
        Set a Sprite's position to speed by its angle
        :param speed: speed factor
        """
        self.change_x += math.cos(self.radians) * speed
        self.change_y += math.sin(self.radians) * speed

    def reverse(self, speed: float = 1.0):
        """
        Set a new speed, but in reverse.
        :param speed: speed factor
        """
        self.forward(-speed)

    def strafe(self, speed: float = 1.0):
        """
        Set a sprites position perpendicular to its angle by speed
        :param speed: speed factor
        """
        self.change_x += -math.sin(self.radians) * speed
        self.change_y += math.cos(self.radians) * speed

    def turn_right(self, theta: float = 90):
        """
        Rotate the sprite right a certain number of degrees.
        :param theta: change in angle
        """
        self.angle -= theta

    def turn_left(self, theta: float = 90):
        """
        Rotate the sprite left a certain number of degrees.
        :param theta: change in angle
        """
        self.angle += theta

    def stop(self):
        """
        Stop the Sprite's motion
        """
        self.change_x = 0
        self.change_y = 0
        self.change_angle = 0

    def _set_collision_radius(self, collision_radius: float):
        """
        Set the collision radius.

        .. note:: Final collision checking is done via geometry that was
            set in the hit_box property. These points are used in the
            check_for_collision function. This collision_radius variable
            is used as a "pre-check." We do a super-fast check with
            collision_radius and see if the sprites are close. If they are,
            then we look at the geometry and figure if they really are colliding.

        :param float collision_radius: Collision radius
        """
        self._collision_radius = collision_radius

    def _get_collision_radius(self):
        """
        Get the collision radius.

        .. note:: Final collision checking is done via geometry that was
            set in get_points/set_points. These points are used in the
            check_for_collision function. This collision_radius variable
            is used as a "pre-check." We do a super-fast check with
            collision_radius and see if the sprites are close. If they are,
            then we look at the geometry and figure if they really are colliding.

        """
        if not self._collision_radius:
            self._collision_radius = max(self.width, self.height)
        return self._collision_radius

    collision_radius = property(_get_collision_radius, _set_collision_radius)

    def __lt__(self, other):
        return self._texture.texture_id.value < other.texture.texture_id.value

    def clear_spatial_hashes(self):
        """
        Search the sprite lists this sprite is a part of, and remove it
        from any spatial hashes it is a part of.

        """
        for sprite_list in self.sprite_lists:
            if sprite_list.use_spatial_hash and sprite_list.spatial_hash is not None:
                try:
                    sprite_list.spatial_hash.remove_object(self)
                except ValueError:
                    print(
                        "Warning, attempt to remove item from spatial hash that doesn't exist in the hash."
                    )

    def add_spatial_hashes(self):
        for sprite_list in self.sprite_lists:
            if sprite_list.use_spatial_hash:
                sprite_list.spatial_hash.insert_object_for_box(self)

    def _get_bottom(self) -> float:
        """
        Return the y coordinate of the bottom of the sprite.
        """
        points = self.get_adjusted_hit_box()
        my_min = points[0][1]
        for point in range(1, len(points)):
            my_min = min(my_min, points[point][1])
        return my_min

    def _set_bottom(self, amount: float):
        """
        Set the location of the sprite based on the bottom y coordinate.
        """
        lowest = self._get_bottom()
        diff = lowest - amount
        self.center_y -= diff

    bottom = property(_get_bottom, _set_bottom)

    def _get_top(self) -> float:
        """
        Return the y coordinate of the top of the sprite.
        """
        points = self.get_adjusted_hit_box()
        my_max = points[0][1]
        for i in range(1, len(points)):
            my_max = max(my_max, points[i][1])
        return my_max

    def _set_top(self, amount: float):
        """ The highest y coordinate. """
        highest = self._get_top()
        diff = highest - amount
        self.center_y -= diff

    top = property(_get_top, _set_top)

    def _get_width(self) -> float:
        """ Get the width of the sprite. """
        return self._width

    def _set_width(self, new_value: float):
        """ Set the width in pixels of the sprite. """
        if new_value != self._width:
            self.clear_spatial_hashes()
            self._point_list_cache = None
            self._width = new_value
            self.add_spatial_hashes()

            for sprite_list in self.sprite_lists:
                sprite_list.update_size(self)

    width = property(_get_width, _set_width)

    def _get_height(self) -> float:
        """ Get the height in pixels of the sprite. """
        return self._height

    def _set_height(self, new_value: float):
        """ Set the center x coordinate of the sprite. """
        if new_value != self._height:
            self.clear_spatial_hashes()
            self._point_list_cache = None
            self._height = new_value
            self.add_spatial_hashes()

            for sprite_list in self.sprite_lists:
                sprite_list.update_height(self)

    height = property(_get_height, _set_height)

    def _get_scale(self) -> float:
        """ Get the scale of the sprite. """
        return self._scale

    def _set_scale(self, new_value: float):
        """ Set the center x coordinate of the sprite. """
        if new_value != self._scale:
            self.clear_spatial_hashes()
            self._point_list_cache = None
            self._scale = new_value
            if self._texture:
                self._width = self._texture.width * self._scale
                self._height = self._texture.height * self._scale
            self.add_spatial_hashes()

            for sprite_list in self.sprite_lists:
                sprite_list.update_size(self)

    scale = property(_get_scale, _set_scale)

    def rescale_relative_to_point(self, point: Point, factor: float) -> None:
        """ Rescale the sprite relative to a different point than its center. """
        self.scale *= factor
        self.center_x = (self.center_x - point[0]) * factor + point[0]
        self.center_y = (self.center_y - point[1]) * factor + point[1]

    def _get_center_x(self) -> float:
        """ Get the center x coordinate of the sprite. """
        return self._position[0]

    def _set_center_x(self, new_value: float):
        """ Set the center x coordinate of the sprite. """
        if new_value != self._position[0]:
            self.clear_spatial_hashes()
            self._point_list_cache = None
            self._position = (new_value, self._position[1])
            self.add_spatial_hashes()

            for sprite_list in self.sprite_lists:
                sprite_list.update_location(self)

    center_x = property(_get_center_x, _set_center_x)

    def _get_center_y(self) -> float:
        """ Get the center y coordinate of the sprite. """
        return self._position[1]

    def _set_center_y(self, new_value: float):
        """ Set the center y coordinate of the sprite. """
        if new_value != self._position[1]:
            self.clear_spatial_hashes()
            self._point_list_cache = None
            self._position = (self._position[0], new_value)
            self.add_spatial_hashes()

            for sprite_list in self.sprite_lists:
                sprite_list.update_location(self)

    center_y = property(_get_center_y, _set_center_y)

    def _get_change_x(self) -> float:
        """ Get the velocity in the x plane of the sprite. """
        return self.velocity[0]

    def _set_change_x(self, new_value: float):
        """ Set the velocity in the x plane of the sprite. """
        self.velocity[0] = new_value

    change_x = property(_get_change_x, _set_change_x)

    def _get_change_y(self) -> float:
        """ Get the velocity in the y plane of the sprite. """
        return self.velocity[1]

    def _set_change_y(self, new_value: float):
        """ Set the velocity in the y plane of the sprite. """
        self.velocity[1] = new_value

    change_y = property(_get_change_y, _set_change_y)

    def _get_angle(self) -> float:
        """ Get the angle of the sprite's rotation. """
        return self._angle

    def _set_angle(self, new_value: float):
        """ Set the angle of the sprite's rotation. """
        if new_value != self._angle:
            self.clear_spatial_hashes()
            self._angle = new_value
            self._point_list_cache = None

            for sprite_list in self.sprite_lists:
                sprite_list.update_angle(self)

            self.add_spatial_hashes()

    angle = property(_get_angle, _set_angle)

    def _to_radians(self) -> float:
        """
        Converts the degrees representation of self.angle into radians.
        :return: float
        """
        return self.angle / 180.0 * math.pi

    def _from_radians(self, new_value: float):
        """
        Converts a radian value into degrees and stores it into angle.
        """
        self.angle = new_value * 180.0 / math.pi

    radians = property(_to_radians, _from_radians)

    def _get_left(self) -> float:
        """
        Return the x coordinate of the left-side of the sprite's hit box.
        """
        points = self.get_adjusted_hit_box()
        my_min = points[0][0]
        for i in range(1, len(points)):
            my_min = min(my_min, points[i][0])
        return my_min

    def _set_left(self, amount: float):
        """ The left most x coordinate. """
        leftmost = self._get_left()
        diff = amount - leftmost
        self.center_x += diff

    left = property(_get_left, _set_left)

    def _get_right(self) -> float:
        """
        Return the x coordinate of the right-side of the sprite's hit box.
        """

        points = self.get_adjusted_hit_box()
        my_max = points[0][0]
        for point in range(1, len(points)):
            my_max = max(my_max, points[point][0])
        return my_max

    def _set_right(self, amount: float):
        """ The right most x coordinate. """
        rightmost = self._get_right()
        diff = rightmost - amount
        self.center_x -= diff

    right = property(_get_right, _set_right)

    def set_texture(self, texture_no: int):
        """
        Sets texture by texture id. Should be renamed because it takes
        a number rather than a texture, but keeping
        this for backwards compatibility.
        """
        if self.textures[texture_no] == self._texture:
            return

        texture = self.textures[texture_no]
        self.clear_spatial_hashes()
        self._point_list_cache = None
        self._texture = texture
        self._width = texture.width * self.scale
        self._height = texture.height * self.scale
        self.add_spatial_hashes()
        for sprite_list in self.sprite_lists:
            sprite_list.update_texture(self)

    def _set_texture2(self, texture: Texture):
        """ Sets texture by texture id. Should be renamed but keeping
        this for backwards compatibility. """
        if texture == self._texture:
            return

        assert (isinstance(texture, Texture))

        self.clear_spatial_hashes()
        self._point_list_cache = None
        self._texture = texture
        self._width = texture.width * self.scale
        self._height = texture.height * self.scale
        self.add_spatial_hashes()
        for sprite_list in self.sprite_lists:
            sprite_list.update_texture(self)

    def _get_texture(self):
        return self._texture

    texture = property(_get_texture, _set_texture2)

    def _get_texture_transform(self) -> Matrix3x3:
        return self._texture_transform

    def _set_texture_transform(self, m: Matrix3x3):
        self._texture_transform = m

    texture_transform = property(_get_texture_transform,
                                 _set_texture_transform)

    def _get_color(self) -> RGB:
        """
        Return the RGB color associated with the sprite.
        """
        return self._color

    def _set_color(self, color: Color):
        """
        Set the current sprite color as a RGB value
        """
        if color is None:
            raise ValueError("Color must be three or four ints from 0-255")
        if len(color) == 3:
            if self._color[0] == color[0] \
                    and self._color[1] == color[1] \
                    and self._color[2] == color[2]:
                return
        elif len(color) == 4:
            color = cast(List, color)  # Prevent typing error
            if self._color[0] == color[0] \
                    and self._color[1] == color[1] \
                    and self._color[2] == color[2]\
                    and self.alpha == color[3]:
                return
            self.alpha = color[3]
        else:
            raise ValueError("Color must be three or four ints from 0-255")

        self._color = color[0], color[1], color[2]

        for sprite_list in self.sprite_lists:
            sprite_list.update_color(self)

    color = property(_get_color, _set_color)

    def _get_alpha(self) -> int:
        """
        Return the alpha associated with the sprite.
        """
        return self._alpha

    def _set_alpha(self, alpha: int):
        """
        Set the current sprite color as a value
        """
        if alpha < 0 or alpha > 255:
            raise ValueError(
                f"Invalid value for alpha. Must be 0 to 255, received {alpha}")

        self._alpha = alpha
        for sprite_list in self.sprite_lists:
            sprite_list.update_color(self)

    alpha = property(_get_alpha, _set_alpha)

    def register_sprite_list(self, new_list):
        """
        Register this sprite as belonging to a list. We will automatically
        remove ourselves from the the list when kill() is called.
        """
        self.sprite_lists.append(new_list)

    def draw(self):
        """ Draw the sprite. """

        if self._sprite_list is None:
            from arcade import SpriteList
            self._sprite_list = SpriteList()
            self._sprite_list.append(self)

        self._sprite_list.draw()

    def draw_hit_box(self, color: Color = BLACK, line_thickness: float = 1):
        """
        Draw a sprite's hit-box. This is slow, but useful for debugging.
        :param color: Color of box
        :param line_thickness: How thick the box should be
        """
        points = self.get_adjusted_hit_box()

        draw_polygon_outline(points, color, line_thickness)

    def update(self):
        """
        Update the sprite.
        """
        self.position = [
            self._position[0] + self.change_x,
            self._position[1] + self.change_y
        ]
        self.angle += self.change_angle

    def on_update(self, delta_time: float = 1 / 60):
        """
        Update the sprite. Similar to update, but also takes a delta-time.
        """
        pass

    def update_animation(self, delta_time: float = 1 / 60):
        """
        Override this to add code that will change
        what image is shown, so the sprite can be
        animated.
        """
        pass

    def remove_from_sprite_lists(self):
        """
        Remove the sprite from all sprite lists.
        """
        if len(self.sprite_lists) > 0:
            # We can't modify a list as we iterate through it, so create a copy.
            sprite_lists = self.sprite_lists.copy()
        else:
            # If the list is a size 1, we don't need to copy
            sprite_lists = self.sprite_lists

        for sprite_list in sprite_lists:
            if self in sprite_list:
                sprite_list.remove(self)
        self.sprite_lists.clear()

    def kill(self):
        """
        Alias of `remove_from_sprite_lists`
        """
        self.remove_from_sprite_lists()

    def collides_with_point(self, point: Point) -> bool:
        """Check if point is within the current sprite.

        Args:
            self: Current sprite
            point: Point to check.

        Returns:
            True if the point is contained within the sprite's boundary.
        """
        from arcade.geometry import is_point_in_polygon

        x, y = point
        return is_point_in_polygon(x, y, self.get_adjusted_hit_box())

    def collides_with_sprite(self, other: 'Sprite') -> bool:
        """Will check if a sprite is overlapping (colliding) another Sprite.

        Args:
            self: Current Sprite.
            other: The other sprite to check against.

        Returns:
            True or False, whether or not they are overlapping.
        """
        from arcade import check_for_collision
        return check_for_collision(self, other)

    def collides_with_list(self, sprite_list: 'SpriteList') -> list:
        """Check if current sprite is overlapping with any other sprite in a list

        Args:
            self: current Sprite
            sprite_list: SpriteList to check against

        Returns:
            SpriteList of all overlapping Sprites from the original SpriteList
        """
        from arcade import check_for_collision_with_list
        # noinspection PyTypeChecker
        return check_for_collision_with_list(self, sprite_list)
Esempio n. 24
0
class ForestKnightView(arcade.View):
    """
    The main View class that runs the actual game code.
    """
    def __init__(self):
        super().__init__()

        # Used to keep track of our scrolling
        self.view_bottom = 0
        self.view_left = 0

        # Sprites
        self.knight = None
        self.character_sprites = None
        self.enemy_sprites = None
        self.platforms = None
        self.foregrounds = None
        self.backgrounds = None
        self.ladders = None
        self.dont_touch = None
        self.collectibles = None
        self.collectibles_to_omit = None

        # Sounds
        self.collectible_sound = None
        self.gameover_sound = None

        # Images
        self.background_image = None

        # Physics Engine
        self.physics_engine = None

        # Level
        self.level = None

        # Other variables
        self.cur_viewport_coords = None

    def setup(self,
              level: int,
              load_game: bool = False,
              loaded_game_data: dict = None):
        """
        Method that sets up the given level of the game.
        It also calls other setup methods used in the game.
        Send `load_game_data` only if the game is being loaded from the hard disk
        """
        self.character_sprites = SpriteList()
        self.enemy_sprites = SpriteList()

        self.collectibles_to_omit = []
        self.cur_viewport_coords = ()

        self.level = level

        self.setup_characters()

        # We'll only load the game if this is NOT the first time playing it
        if load_game:
            self.load_game_data(loaded_game_data)
            self.setup_correct_viewport()

        self.setup_sprites(self.level)
        self.update_viewport()
        self.setup_physics_engine()
        self.setup_sounds()
        self.setup_images()

    def setup_sprites(self, level: int):
        """Method that sets up all the Sprites (except for Knight and other Enemies)"""
        loaded_sprites = level_loader(level, self.collectibles_to_omit)

        self.platforms = loaded_sprites["Platforms"]
        self.foregrounds = loaded_sprites["Foregrounds"]
        self.backgrounds = loaded_sprites["Backgrounds"]
        self.ladders = loaded_sprites["Ladders"]
        self.dont_touch = loaded_sprites["Dont-Touch"]
        self.collectibles = loaded_sprites["Collectibles"]

    def setup_characters(self):
        """Method to set up the Knight and other Enemies"""
        self.knight = Knight(pos_x=KNIGHT_X, pos_y=KNIGHT_Y)
        self.character_sprites.append(self.knight)
        self.character_sprites.preload_textures(self.knight.textures)

        for pos in ZOMBIE_MALE_LEVEL_1_POSITIONS:
            pos_x = pos[0]
            pos_y = pos[1]

            enemy = ZombieMale(pos_x, pos_y)
            self.enemy_sprites.append(enemy)

        for enemy in self.enemy_sprites:
            self.enemy_sprites.preload_textures(enemy.textures)

    def setup_physics_engine(self):
        """Method to set up arcade.PhysicsEnginePlatformer"""
        self.physics_engine = PhysicsEnginePlatformer(self.knight,
                                                      self.platforms,
                                                      gravity_constant=GRAVITY,
                                                      ladders=self.ladders)

    def setup_sounds(self):
        """Method that loads all the sounds required in the game"""
        self.collectible_sound = load_sound(f"{AUDIO_DIR}/coin1.wav")
        self.gameover_sound = load_sound(f"{AUDIO_DIR}/lose1.wav")
        self.background_music = load_sound(f"{AUDIO_DIR}/backgroundMusic2.mp3")

        # We'll play the background music during initial setup
        # self.background_play = Sound.play(self.background_music, volume=0.2)

        self.knight.setup_sounds()

    def setup_images(self, level: int = 1):
        """Method to set up background image of the current level"""
        if level == 1:
            self.background_image = arcade.load_texture(
                f"{IMAGES_DIR}/backgrounds/BG.png")

    def setup_correct_viewport(self):
        """Method that sets up the viewports that was saved"""
        arcade.set_viewport(*self.cur_viewport_coords)

    def delete_all_sprites(self):
        """Deletes all the game sprites when switching to Main Menu to save on resources"""

        self.knight.kill()

    def on_key_press(self, symbol: int, modifiers: int):
        """Method that handles what happens when a key is pressed down"""
        # Knight movement and attack
        if symbol == arcade.key.RIGHT:
            self.knight.change_x = self.knight.speed

        elif symbol == arcade.key.LEFT:
            self.knight.change_x = -(self.knight.speed)

        elif symbol == arcade.key.UP:
            if (self.physics_engine.can_jump()
                    and not self.physics_engine.is_on_ladder()):
                self.knight.change_y = self.knight.jump_speed
                self.knight.jump_sound.play()
            elif self.physics_engine.is_on_ladder():
                self.knight.change_y = self.knight.speed

        elif symbol == arcade.key.DOWN:
            if self.physics_engine.is_on_ladder():
                self.knight.change_y = -(self.knight.speed)

        elif symbol == arcade.key.SPACE:
            self.knight.is_attacking = True

        if symbol in [
                arcade.key.DOWN,
                arcade.key.LEFT,
                arcade.key.RIGHT,
        ]:
            self.knight.is_moving = True

        # Other key-based actions
        if symbol == arcade.key.ESCAPE:
            self.pause()

        if symbol == arcade.key.V:
            print(self.knight.position)

        return super().on_key_press(symbol, modifiers)

    def on_key_release(self, symbol: int, modifiers: int):
        """Method that handles what happens when a key is released"""
        if symbol == arcade.key.RIGHT:
            self.knight.change_x = 0

        elif symbol == arcade.key.LEFT:
            self.knight.change_x = 0

        elif (symbol in [arcade.key.UP, arcade.key.DOWN]
              and self.physics_engine.is_on_ladder()):
            self.knight.change_y = 0

        elif symbol == arcade.key.SPACE:
            self.knight.is_attacking = False

        if symbol in [
                arcade.key.DOWN,
                arcade.key.LEFT,
                arcade.key.RIGHT,
        ]:
            self.knight.is_moving = False

        return super().on_key_release(symbol, modifiers)

    def pause(self):
        """Method that will bring up a screen that pauses the game"""
        pause_view = PauseView(self)
        self.window.show_view(pause_view)

    def load_game_data(self, data: dict):
        """
        Method that takes all the data from the loader function from the game saving utility and correctly sets up the game.
        This method will only be called if the game is NOT being run for the first time
        """
        self.knight.health = data["health"]
        self.knight.position = data["position"]
        self.knight.state = data["knight_state"]
        self.knight.score = data["score"]
        self.knight.texture = data["texture"]
        self.collectibles_to_omit = data["collectibles_to_omit"]
        self.cur_viewport_coords = data["viewport_coords"]

    def update_viewport(self):
        """Method that manages and updates the viewport according to where the Knight is"""

        # Track if we need to change the viewport
        changed = False

        # Scroll left
        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
        if self.knight.left < left_boundary:
            self.view_left -= left_boundary - self.knight.left
            changed = True

        # Scroll right
        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
        if self.knight.right > right_boundary:
            self.view_left += self.knight.right - right_boundary
            changed = True

        # Scroll up
        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
        if self.knight.top > top_boundary:
            self.view_bottom += self.knight.top - top_boundary
            changed = True

        # Scroll down
        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
        if self.knight.bottom < bottom_boundary:
            self.view_bottom -= bottom_boundary - self.knight.bottom
            changed = True

        if changed:
            # Only scroll to integers. Otherwise we end up with pixels that
            # don't line up on the screen
            self.view_bottom = int(self.view_bottom)
            self.view_left = int(self.view_left)

            # Do the scrolling
            arcade.set_viewport(
                self.view_left,
                SCREEN_WIDTH + self.view_left,
                self.view_bottom,
                SCREEN_HEIGHT + self.view_bottom,
            )

    def on_update(self, delta_time: float):
        """Method that is the main game loop and contains most game logic"""
        self.character_sprites.update_animation()
        self.character_sprites.update()
        self.enemy_sprites.update_animation()
        self.enemy_sprites.update()
        self.physics_engine.update()

        if self.knight.is_dying:
            self.knight.die()

        if self.knight.is_attacking:
            self.knight.attack()

        if (not self.knight.is_moving and not self.knight.is_attacking
                and not self.knight.is_dying):
            self.knight.idle_animation()

        # Managing viewport
        self.update_viewport()

        # Collecting coins logic
        coins_collected = check_for_collision_with_list(
            self.knight, self.collectibles)
        for coin in coins_collected:
            self.collectibles_to_omit.append(coin.position)
            coin.kill()
            self.knight.score += 1
            self.collectible_sound.play()

        for enemy in self.enemy_sprites:
            # Enemies will always be on the lookout for the Knight
            enemy.detect_knight(self.knight)

        return super().on_update(delta_time)

    def on_draw(self, draw_stats=True):
        """
        We actually have to draw to the display to show anything on the screen.
        This method handles all the drawing in the game
        """
        start_render()

        # Drawing our loaded background images and setting it
        arcade.draw_texture_rectangle(
            (SCREEN_WIDTH // 2) + self.view_left,
            (SCREEN_HEIGHT // 2) + self.view_bottom,
            SCREEN_WIDTH,
            SCREEN_HEIGHT,
            self.background_image,
        )

        self.platforms.draw()
        self.backgrounds.draw()
        self.ladders.draw()
        self.collectibles.draw()
        self.character_sprites.draw()
        self.enemy_sprites.draw()
        self.dont_touch.draw()
        self.foregrounds.draw()

        # Drawing the Knight's stats
        if draw_stats:
            arcade.draw_text(
                f"Score: {self.knight.score}",
                self.view_left,
                self.view_bottom + 15,
                arcade.color.CHROME_YELLOW,
                15,
            )

            arcade.draw_text(
                f"Health: {self.knight.health}",
                self.view_left,
                self.view_bottom,
                arcade.color.ROSE_RED,
                15,
            )

        return super().on_draw()
Esempio n. 25
0
def draw_text(text: str,
              start_x: float,
              start_y: float,
              color: Color,
              font_size: float = 12,
              width: int = 0,
              align="left",
              font_name=('Calibri', 'Arial'),
              bold: bool = False,
              italic: bool = False,
              anchor_x="left",
              anchor_y="baseline",
              rotation: float = 0):
    """

    Args:
        :text: Text to display.
        :start_x: x coordinate of top left text point.
        :start_y: y coordinate of top left text point.
        :color: color, specified in a list of 3 or 4 bytes in RGB or
         RGBA format.

    Example:

    >>> import arcade
    >>> arcade.open_window(800, 600, "Drawing Example")
    >>> arcade.set_background_color(arcade.color.WHITE)
    >>> arcade.start_render()
    >>> arcade.draw_text("Text Example", 250, 300, arcade.color.BLACK, 10)
    >>> arcade.draw_text("Text Example", 250, 300, (0, 0, 0, 100), 10)
    >>> arcade.finish_render()
    >>> arcade.quick_run(0.25)
    """

    # Scale the font up, so it matches with the sizes of the old code back
    # when pyglet drew the text.
    font_size *= 1.25

    # Text isn't anti-aliased, so we'll draw big, and then shrink
    scale_up = 5
    scale_down = 5

    font_size *= scale_up

    # If the cache gets too large, dump it and start over.
    if len(draw_text.cache) > 5000:
        draw_text.cache = {}

    key = f"{text}{color}{font_size}{width}{align}{font_name}{bold}{italic}"
    if key in draw_text.cache:
        label = draw_text.cache[key]
        text_sprite = label.text_sprite_list[0]

        if anchor_x == "left":
            text_sprite.center_x = start_x + text_sprite.width / 2
        elif anchor_x == "center":
            text_sprite.center_x = start_x
        elif anchor_x == "right":
            text_sprite.right = start_x
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
            )

        if anchor_y == "top":
            text_sprite.center_y = start_y - text_sprite.height / 2
        elif anchor_y == "center":
            text_sprite.center_y = start_y
        elif anchor_y == "bottom" or anchor_y == "baseline":
            text_sprite.bottom = start_y
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_y}'"
            )

        text_sprite.angle = rotation

        label.text_sprite_list.update_positions()
    else:
        label = Text()

        # Figure out the font to use
        font = None

        # Font was specified with a string
        if isinstance(font_name, str):
            try:
                font = PIL.ImageFont.truetype(font_name, int(font_size))
            except OSError:
                pass

            if font is None:
                try:
                    font = PIL.ImageFont.truetype(font_name + ".ttf",
                                                  int(font_size))
                except OSError:
                    pass

        # We were instead given a list of font names, in order of preference
        if font is not None:
            for font_string_name in font_name:
                try:
                    font = PIL.ImageFont.truetype(font_string_name,
                                                  int(font_size))
                except OSError:
                    pass

                if font is None:
                    try:
                        font = PIL.ImageFont.truetype(
                            font_string_name + ".ttf", int(font_size))
                    except OSError:
                        pass

                if font is not None:
                    break

        # Default font if no font
        if font is None:
            font_names = ("arial.ttf",
                          "/usr/share/fonts/truetype/freefont/FreeMono.ttf",
                          '/System/Library/Fonts/SFNSDisplay.ttf')
            for font_string_name in font_names:
                try:
                    font = PIL.ImageFont.truetype(font_string_name,
                                                  int(font_size))
                    break
                except OSError:
                    pass

        # This is stupid. We have to have an image to figure out what size
        # the text will be when we draw it. Of course, we don't know how big
        # to make the image. Catch-22. So we just make a small image we'll trash
        text_image_size = (10, 10)
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)

        # Get size the text will be
        text_image_size = draw.multiline_textsize(text, font=font)

        # Create image of proper size
        text_height = text_image_size[1]
        text_width = text_image_size[0]

        image_start_x = 0
        if width == 0:
            width = text_image_size[0]
        else:
            # Wait! We were given a field width.
            if align == "center":
                # Center text on given field width
                field_width = width * scale_up
                text_image_size = field_width, text_height
                image_start_x = (field_width - text_width) // 2
                width = field_width
            else:
                image_start_x = 0

        # If we draw a y at 0, then the text is drawn with a baseline of 0,
        # cutting off letters that drop below the baseline. This shoves it
        # up a bit.
        image_start_y = -font_size * scale_up * 0.03
        image = PIL.Image.new("RGBA", text_image_size)
        draw = PIL.ImageDraw.Draw(image)
        draw.multiline_text((image_start_x, image_start_y),
                            text,
                            color,
                            align=align,
                            font=font)
        image = image.resize((width // scale_down, text_height // scale_down),
                             resample=PIL.Image.LANCZOS)

        text_sprite = Sprite()
        text_sprite.image = image
        text_sprite.texture_name = key
        text_sprite.width = image.width
        text_sprite.height = image.height

        if anchor_x == "left":
            text_sprite.center_x = start_x + text_sprite.width / 2
        elif anchor_x == "center":
            text_sprite.center_x = start_x
        elif anchor_x == "right":
            text_sprite.right = start_x
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'"
            )

        if anchor_y == "top":
            text_sprite.center_y = start_y + text_sprite.height / 2
        elif anchor_y == "center":
            text_sprite.center_y = start_y
        elif anchor_y == "bottom" or anchor_y == "baseline":
            text_sprite.bottom = start_y
        else:
            raise ValueError(
                f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_y}'"
            )

        text_sprite.angle = rotation

        from arcade.sprite_list import SpriteList
        label.text_sprite_list = SpriteList()
        label.text_sprite_list.append(text_sprite)

        draw_text.cache[key] = label

    label.text_sprite_list.draw()
Esempio n. 26
0
class Texture:
    """
    Class that represents a texture.
    Usually created by the ``load_texture`` or ``load_textures`` commands.

    Attributes:
        :name:
        :image:
        :scale:
        :width: Width of the texture image in pixels
        :height: Height of the texture image in pixels

    """
    def __init__(self, name, image=None):
        self.name = name
        self.texture = None
        self.image = image
        self.scale = 1
        if image:
            self.width = image.width
            self.height = image.height
        else:
            self.width = 0
            self.height = 0

        self._sprite = None
        self._sprite_list = None
        self.unscaled_hitbox_points = None

    # noinspection PyUnusedLocal
    def draw(self,
             center_x: float,
             center_y: float,
             width: float,
             height: float,
             angle: float = 0,
             alpha: int = 255,
             transparent: bool = True,
             repeat_count_x=1,
             repeat_count_y=1):
        """

        Args:
            center_x:
            center_y:
            width:
            height:
            angle:
            alpha: Currently unused.
            transparent:  Currently unused.
            repeat_count_x: Currently unused.
            repeat_count_y:  Currently unused.

        Returns:

        """

        from arcade.sprite import Sprite
        from arcade.sprite_list import SpriteList

        if self._sprite is None:
            self._sprite = Sprite()
            self._sprite._texture = self
            self._sprite.textures = [self]

            self._sprite_list = SpriteList()
            self._sprite_list.append(self._sprite)

        self._sprite.center_x = center_x
        self._sprite.center_y = center_y
        self._sprite.width = width
        self._sprite.height = height
        self._sprite.angle = angle
        self._sprite.alpha = alpha

        self._sprite_list.draw()