|
@@ -0,0 +1,425 @@
|
|
|
|
|
+import os
|
|
|
|
|
+
|
|
|
|
|
+import pygame
|
|
|
|
|
+
|
|
|
|
|
+from jogai import _events, logger, settings, utils
|
|
|
|
|
+from jogai._events import _get_keys
|
|
|
|
|
+from jogai._globals import _window
|
|
|
|
|
+from jogai.translations import gettext as _
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class Character(pygame.sprite.Sprite):
|
|
|
|
|
+ """
|
|
|
|
|
+ A videogame character with a bunch of actions.
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ _predefined_key_actions_allowed = [
|
|
|
|
|
+ _('backward'),
|
|
|
|
|
+ _('decrease'),
|
|
|
|
|
+ _('down'),
|
|
|
|
|
+ _('forward'),
|
|
|
|
|
+ _('increase'),
|
|
|
|
|
+ _('up'),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ _predefined_attributes_allowed = [
|
|
|
|
|
+ _('height'),
|
|
|
|
|
+ _('position'),
|
|
|
|
|
+ _('speed'),
|
|
|
|
|
+ _('width'),
|
|
|
|
|
+ _('keys'),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ def __init__(
|
|
|
|
|
+ self,
|
|
|
|
|
+ image_filename: str,
|
|
|
|
|
+ name: str = '',
|
|
|
|
|
+ ):
|
|
|
|
|
+ if not isinstance(name, str):
|
|
|
|
|
+ raise TypeError(_('The character name must be a string.'))
|
|
|
|
|
+
|
|
|
|
|
+ self.scale_factor = 1.0
|
|
|
|
|
+ self.speed = 0
|
|
|
|
|
+ self.predefined_keys = {}
|
|
|
|
|
+ self._image = None
|
|
|
|
|
+ self._original_image = None
|
|
|
|
|
+ self._original_rect = None
|
|
|
|
|
+ self._rect = None
|
|
|
|
|
+ self.name = name
|
|
|
|
|
+
|
|
|
|
|
+ super().__init__()
|
|
|
|
|
+ self.load(image_filename)
|
|
|
|
|
+
|
|
|
|
|
+ def __repr__(self):
|
|
|
|
|
+ return f'{_("Character")}: {self.name}'
|
|
|
|
|
+
|
|
|
|
|
+ def _load_data(self, data: dict):
|
|
|
|
|
+ """
|
|
|
|
|
+ Load character attributes from data schema.
|
|
|
|
|
+ Only declarated attributes in predefined_attributes_allowed are allowed.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ data: input schema
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ for attribute in data.keys():
|
|
|
|
|
+ # utils._convert_function_arguments_to_string()
|
|
|
|
|
+ if attribute in self._predefined_attributes_allowed:
|
|
|
|
|
+ eval(f'self.set_{attribute}(data["{attribute}"])')
|
|
|
|
|
+
|
|
|
|
|
+ @property
|
|
|
|
|
+ def height(self):
|
|
|
|
|
+ return self._rect.height
|
|
|
|
|
+
|
|
|
|
|
+ @property
|
|
|
|
|
+ def width(self):
|
|
|
|
|
+ return self._rect.width
|
|
|
|
|
+
|
|
|
|
|
+ @property
|
|
|
|
|
+ def x(self):
|
|
|
|
|
+ return self._rect.x
|
|
|
|
|
+
|
|
|
|
|
+ @property
|
|
|
|
|
+ def y(self):
|
|
|
|
|
+ return self._rect.y
|
|
|
|
|
+
|
|
|
|
|
+ def _eval_predefined_function(
|
|
|
|
|
+ self, function: str, arguments: str | list | dict
|
|
|
|
|
+ ):
|
|
|
|
|
+ if not isinstance(function, str):
|
|
|
|
|
+ raise TypeError(_('The function name must be a string.'))
|
|
|
|
|
+
|
|
|
|
|
+ processed_arguments = utils._convert_function_arguments_to_string(
|
|
|
|
|
+ arguments
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ eval(f'self.{function}({processed_arguments})')
|
|
|
|
|
+
|
|
|
|
|
+ def move(
|
|
|
|
|
+ self,
|
|
|
|
|
+ x: int | None = None,
|
|
|
|
|
+ y: int | None = None,
|
|
|
|
|
+ key: str = '',
|
|
|
|
|
+ relative: bool = True,
|
|
|
|
|
+ ):
|
|
|
|
|
+ """
|
|
|
|
|
+ Move the character the number of pixels indicated in the x and y parameters
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ x: Number of pixels to move on the abscissa axis
|
|
|
|
|
+ y: Number of pixels to move on the ordinate axis
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+ relative: True for a incremental movement, False for an absolute movement
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.move(5, 3)
|
|
|
|
|
+ >>> ch.move()
|
|
|
|
|
+ """
|
|
|
|
|
+ if x is None:
|
|
|
|
|
+ x = self.speed
|
|
|
|
|
+ if y is None:
|
|
|
|
|
+ y = self.speed
|
|
|
|
|
+
|
|
|
|
|
+ if not isinstance(x, int) or not isinstance(y, int):
|
|
|
|
|
+ raise TypeError(_('Amount to move must be an integer.'))
|
|
|
|
|
+
|
|
|
|
|
+ if not isinstance(key, str):
|
|
|
|
|
+ raise TypeError(_('Key pressed must be a string.'))
|
|
|
|
|
+
|
|
|
|
|
+ if not key or _get_keys()[utils.eval_key(key)]:
|
|
|
|
|
+ if relative:
|
|
|
|
|
+ self._rect.move_ip(x, y)
|
|
|
|
|
+ else:
|
|
|
|
|
+ self._rect.x = x
|
|
|
|
|
+ self._rect.y = y
|
|
|
|
|
+
|
|
|
|
|
+ def move_to(self, x: int, y: int, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Move the character to (x, y) position.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ x: coordinate in horizontal axis
|
|
|
|
|
+ y: coordinate in vertical axis
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.move_to(0, 0)
|
|
|
|
|
+ """
|
|
|
|
|
+ self.move(x, y, key, relative=False)
|
|
|
|
|
+
|
|
|
|
|
+ def backward(self, amount: int | None = None, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Moves the character in the negative direction of the abscissa.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ amount: Number of pixels to move
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.backward(3)
|
|
|
|
|
+ >>> ch.backward()
|
|
|
|
|
+ """
|
|
|
|
|
+ if amount is None:
|
|
|
|
|
+ amount = self.speed
|
|
|
|
|
+ self.move(-amount, 0, key)
|
|
|
|
|
+
|
|
|
|
|
+ def down(self, amount: int | None = None, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Moves the character in the positive direction of the ordinate.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ amount: Number of pixels to move
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.down(3)
|
|
|
|
|
+ >>> ch.down()
|
|
|
|
|
+ """
|
|
|
|
|
+ if amount is None:
|
|
|
|
|
+ amount = self.speed
|
|
|
|
|
+ self.move(0, amount, key)
|
|
|
|
|
+
|
|
|
|
|
+ def forward(self, amount: int | None = None, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Moves the character in the positive direction of the abscissa.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ amount: Number of pixels to move
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.forward(3)
|
|
|
|
|
+ >>> ch.forward()
|
|
|
|
|
+ """
|
|
|
|
|
+ if amount is None:
|
|
|
|
|
+ amount = self.speed
|
|
|
|
|
+ self.move(amount, 0, key)
|
|
|
|
|
+
|
|
|
|
|
+ def up(self, amount: int | None = None, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Moves the character in the negative direction of the ordinate.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ amount: Number of pixels to move
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.up(3)
|
|
|
|
|
+ >>> ch.up()
|
|
|
|
|
+ """
|
|
|
|
|
+ if amount is None:
|
|
|
|
|
+ amount = self.speed
|
|
|
|
|
+ self.move(0, -amount, key)
|
|
|
|
|
+
|
|
|
|
|
+ def decrease(self, decrement: float | int = 0.001, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Decreases the size of the character.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ decrement: amount to increase to the scale factor
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.decrease(0.1)
|
|
|
|
|
+ """
|
|
|
|
|
+ if isinstance(decrement, int):
|
|
|
|
|
+ increment = float(increment)
|
|
|
|
|
+
|
|
|
|
|
+ if not isinstance(decrement, float):
|
|
|
|
|
+ raise TypeError(_('The decrement must be a float number.'))
|
|
|
|
|
+
|
|
|
|
|
+ self.increase(-decrement, key)
|
|
|
|
|
+
|
|
|
|
|
+ def increase(self, increment: float | int = 0.001, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Increases the size of the character.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ increment: amount to increase to the scale factor
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.increase(0.1)
|
|
|
|
|
+ """
|
|
|
|
|
+ if isinstance(increment, int):
|
|
|
|
|
+ increment = float(increment)
|
|
|
|
|
+
|
|
|
|
|
+ if not isinstance(increment, float):
|
|
|
|
|
+ raise TypeError(_('The increment must be a float number.'))
|
|
|
|
|
+
|
|
|
|
|
+ if not key or _get_keys()[utils.eval_key(key)]:
|
|
|
|
|
+ self.scale_to(self.scale_factor + increment)
|
|
|
|
|
+
|
|
|
|
|
+ def scale_to(self, scale_factor: float = 1.0):
|
|
|
|
|
+ """
|
|
|
|
|
+ Scale the character according to the scale factor.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ scale_factor: factor to scale
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.scale_to(2)
|
|
|
|
|
+ """
|
|
|
|
|
+ if not isinstance(scale_factor, float) and not isinstance(
|
|
|
|
|
+ scale_factor, int
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise TypeError(
|
|
|
|
|
+ _('The scale factor must be a positive number (float or int).')
|
|
|
|
|
+ )
|
|
|
|
|
+ if scale_factor <= 0:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ _('The scale factor must be a positive number (float or int).')
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ self._image = pygame.transform.scale(
|
|
|
|
|
+ self._original_image,
|
|
|
|
|
+ (
|
|
|
|
|
+ int(self._original_rect.width * scale_factor),
|
|
|
|
|
+ int(self._original_rect.height * scale_factor),
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ self.scale_factor = scale_factor
|
|
|
|
|
+ x, y = self._rect.x, self._rect.y
|
|
|
|
|
+ self._rect = self._image.get_rect()
|
|
|
|
|
+ self.move_to(x, y)
|
|
|
|
|
+
|
|
|
|
|
+ def set_height(self, height: int = 0):
|
|
|
|
|
+ """
|
|
|
|
|
+ Set the character height.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ height: the character height in pixels
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.set_height(100)
|
|
|
|
|
+
|
|
|
|
|
+ """
|
|
|
|
|
+ if not isinstance(height, int):
|
|
|
|
|
+ raise TypeError(_('Height must be a not negative integer.'))
|
|
|
|
|
+ if height < 1 or self._original_rect.height < 1:
|
|
|
|
|
+ raise ValueError(_('Height must be a not negative integer.'))
|
|
|
|
|
+
|
|
|
|
|
+ self.scale_factor = height / self._original_rect.height
|
|
|
|
|
+ self._image = pygame.transform.scale(
|
|
|
|
|
+ self._original_image,
|
|
|
|
|
+ (int(self._original_rect.width * self.scale_factor), height),
|
|
|
|
|
+ )
|
|
|
|
|
+ self._rect = self._image.get_rect()
|
|
|
|
|
+
|
|
|
|
|
+ def set_keys(self, predefined_keys: dict):
|
|
|
|
|
+ """
|
|
|
|
|
+ Set the character predefined keys to perform actions.
|
|
|
|
|
+ Only declarated actions in _predefined_key_actions_allowed are allowed.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ predefined_keys: the predefined keys with the associated action.
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.set_keys({'forward': 'd'})
|
|
|
|
|
+ """
|
|
|
|
|
+ if not isinstance(predefined_keys, dict):
|
|
|
|
|
+ raise TypeError(
|
|
|
|
|
+ _('The predefined keys must be a dict. No keys are loaded.')
|
|
|
|
|
+ )
|
|
|
|
|
+ filtered_predefined_keys = {}
|
|
|
|
|
+ for action, key in predefined_keys.items():
|
|
|
|
|
+ if action in self._predefined_key_actions_allowed:
|
|
|
|
|
+ filtered_predefined_keys.update({action: key})
|
|
|
|
|
+ self.predefined_keys.update(filtered_predefined_keys)
|
|
|
|
|
+
|
|
|
|
|
+ def set_position(self, position: tuple | list, key: str = ''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Set the character position as tuple. To use as separate coordinates call move_to().
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ position: a tuple with abscissa and ordinate coordinates
|
|
|
|
|
+ key: Optional key to perform the action
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.set_position((0, 0))
|
|
|
|
|
+ """
|
|
|
|
|
+ if not isinstance(position, tuple) and not isinstance(position, list):
|
|
|
|
|
+ raise TypeError(_('The position must be a tuple or list (x, y).'))
|
|
|
|
|
+
|
|
|
|
|
+ if len(position) != 2:
|
|
|
|
|
+ raise ValueError(_('The position has two coordinates (x, y).'))
|
|
|
|
|
+
|
|
|
|
|
+ self.move(position[0], position[1], key, relative=False)
|
|
|
|
|
+
|
|
|
|
|
+ def set_speed(self, speed: int = 0):
|
|
|
|
|
+ """
|
|
|
|
|
+ Set the character speed.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ speed: the character speed
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.set_speed(10)
|
|
|
|
|
+ """
|
|
|
|
|
+ if isinstance(speed, int):
|
|
|
|
|
+ self.speed = speed
|
|
|
|
|
+
|
|
|
|
|
+ def set_width(self, width: int = 0):
|
|
|
|
|
+ """
|
|
|
|
|
+ Set the character width.
|
|
|
|
|
+
|
|
|
|
|
+ Parameters:
|
|
|
|
|
+ width: the character width in pixels
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.set_width(100)
|
|
|
|
|
+ """
|
|
|
|
|
+ if not isinstance(width, int):
|
|
|
|
|
+ raise TypeError(_('Width must be a not negative integer.'))
|
|
|
|
|
+ if width < 1 or self._original_rect.width < 1:
|
|
|
|
|
+ raise ValueError(_('Width must be a not negative integer.'))
|
|
|
|
|
+
|
|
|
|
|
+ self.scale_factor = width / self._original_rect.width
|
|
|
|
|
+ self._image = pygame.transform.scale(
|
|
|
|
|
+ self._original_image,
|
|
|
|
|
+ (width, int(self._original_rect.height * self.scale_factor)),
|
|
|
|
|
+ )
|
|
|
|
|
+ self._rect = self._image.get_rect()
|
|
|
|
|
+
|
|
|
|
|
+ def load(self, filename: str) -> bool:
|
|
|
|
|
+ """
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> result = ch.load('protagonist.png')
|
|
|
|
|
+ """
|
|
|
|
|
+ self._original_image, self._original_rect = utils._load_image(
|
|
|
|
|
+ settings.CHARACTERS_DIR, filename
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ self._image = self._original_image
|
|
|
|
|
+ self._original_rect = self._original_image.get_rect()
|
|
|
|
|
+ self._rect = self._image.get_rect()
|
|
|
|
|
+ self.scale_factor = 1
|
|
|
|
|
+
|
|
|
|
|
+ def paint(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Paint the character in the main surface (window).
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ >>> ch = Character('protagonist.png')
|
|
|
|
|
+ >>> ch.paint()
|
|
|
|
|
+ """
|
|
|
|
|
+ if self._image:
|
|
|
|
|
+ _window.blit(self._image, self._rect)
|
|
|
|
|
+
|
|
|
|
|
+ def update(self):
|
|
|
|
|
+ for action, key in self.predefined_keys.items():
|
|
|
|
|
+ eval(f'self.{action}(key="{key}")')
|