| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- import os
- from copy import deepcopy
- from typing import Type
- 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'),
- _('jump'),
- _('up'),
- ]
- _predefined_attributes_allowed = [
- _('height'),
- _('keys'),
- _('position'),
- _('speed'),
- _('width'),
- ]
- 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_filename = image_filename
- self.image = None
- self._original_image = None
- self._original_rect = None
- self.rect = None
- self.name = name
- self.x = 0
- self.y = 0
- self.is_jumping = False
- self.jump_force = settings.DEFAULT_JUMP_FORCE
- self._jump_counter = self.jump_force
- 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
- """
- if data is None:
- data = {}
- if not isinstance(data, dict):
- raise TypeError(_('The data input must be a dict.'))
- 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
- 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.x += x
- self.y += y
- else:
- self.x = x
- self.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
- if not self.is_jumping:
- 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
- if not self.is_jumping:
- self.move(0, -amount, key)
- def jump(self, force: float | None = None, key: str = ''):
- if force is None:
- force = settings.DEFAULT_JUMP_FORCE
- if isinstance(force, int):
- force = float(force)
- if not isinstance(force, float):
- raise TypeError(_('The jump force must be a float number.'))
- if force < 0:
- raise ValueError(_('The jump force must be a non-negative float.'))
- self.jump_force = force
- self.jump_counter = force
- if not self.is_jumping and (
- not key or _get_keys()[utils.eval_key(key)]
- ):
- self.is_jumping = True
- 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):
- decrement = float(decrement)
- 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)]:
- if self.scale_factor + increment < 0:
- self.scale_to(0)
- else:
- 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 non-negative 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
- self.rect = self.image.get_rect()
- 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
- self.image_filename = filename
- def copy(self):
- """
- Create a copy of this character keeping size and position.
- Examples:
- >>> ch = Character('protagonist.png')
- >>> other = ch.copy()
- """
- new_character = Character(self.image_filename)
- new_character.set_height(self.height)
- new_character.set_width(self.width)
- new_character.set_position((self.x, self.y))
- return new_character
- 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_jump(self):
- """
- Calculate and update the character position during the jump.
- """
- if self.is_jumping:
- if self._jump_counter >= -self.jump_force:
- self.move(
- 0,
- -int((self._jump_counter * abs(self._jump_counter)) * 0.5),
- )
- self._jump_counter -= 1
- else:
- self._jump_counter = settings.DEFAULT_JUMP_FORCE
- self.is_jumping = False
- def _update_rect_position(
- self, vg_scene: Type['scene.Scene'], tracking: bool = False
- ):
- """
- Calculate and update the position where the character must be painted.
- Only one character can be tracked.
- Parameters:
- vg_scene: The scene information.
- tracking: True if the character must be followed, False otherwise.
- """
- # TODO: check vg_scene type
- if not isinstance(tracking, bool):
- raise TypeError(_('The tracking parameter must be a boolean.'))
- if not vg_scene:
- return None
- if tracking:
- if self.x < settings.WINDOW_WIDTH // 2:
- self.rect.x = self.x
- elif (
- settings.WINDOW_WIDTH // 2
- < self.x
- < vg_scene.width - settings.WINDOW_WIDTH
- ):
- self.rect.x = settings.WINDOW_WIDTH // 2
- elif self.x > vg_scene.width - settings.WINDOW_WIDTH:
- self.rect.x = settings.WINDOW_WIDTH * 3 / 2 - (
- -self.x % vg_scene.width
- )
- self.rect.y = self.y
- else:
- self.rect.x = self.x + vg_scene.x
- self.rect.y = self.y
- def update(self):
- for action, key in self.predefined_keys.items():
- eval(f'self.{action}(key="{key}")')
- self._update_jump()
|