character.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. import os
  2. from typing import Type
  3. import pygame
  4. import jogai
  5. import jogai._globals as _globals
  6. from jogai import logger, settings, utils, widgets
  7. from jogai.translations import gettext as _
  8. from jogai.utils import _get_keys
  9. class Velocity:
  10. x = 0
  11. y = 0
  12. def __repr__(self):
  13. return f'{self.x}, {self.y}'
  14. class Character(pygame.sprite.Sprite):
  15. """
  16. A videogame character with a bunch of actions.
  17. """
  18. TOP_COLLISION = _('top')
  19. BOTTOM_COLLISION = _('bottom')
  20. LEFT_COLLISION = _('left')
  21. RIGHT_COLLISION = _('right')
  22. STATIC_COLLISION = _('static')
  23. _predefined_animations_allowed = [
  24. _('backward'),
  25. _('down'),
  26. _('forward'),
  27. _('stop'),
  28. _('jump'),
  29. _('up'),
  30. ]
  31. _predefined_attributes_allowed = [
  32. _('collidable'),
  33. _('chronometer'),
  34. _('gravitative'),
  35. _('height'),
  36. _('keys'),
  37. _('lives'),
  38. _('position'),
  39. _('score'),
  40. _('speed'),
  41. _('width'),
  42. ]
  43. _predefined_key_actions_allowed = [
  44. _('backward'),
  45. _('decrease'),
  46. _('down'),
  47. _('forward'),
  48. _('increase'),
  49. _('jump'),
  50. _('up'),
  51. ]
  52. def __init__(
  53. self,
  54. image_filename: str,
  55. name: str = '',
  56. animation_filenames: dict = {},
  57. ):
  58. if not isinstance(name, str):
  59. raise TypeError(_('The character name must be a string.'))
  60. self.scale_factor = 1.0
  61. self.speed = 0
  62. self.velocity = Velocity()
  63. self.predefined_keys = {}
  64. self.image_filename = image_filename
  65. self.image = None
  66. self.thumbnail = None
  67. self.thumbnail_rect = None
  68. self.animation_filenames = {}
  69. self._original_image = None
  70. self._original_rect = None
  71. self.rect = None
  72. self.name = name
  73. self.x = 0
  74. self.y = 0
  75. self.gravitative = False
  76. self.is_jumping = False
  77. self.jump_force = settings.CHARACTER_DEFAULT_JUMP_FORCE
  78. self._jump_counter = self.jump_force
  79. self.angle = 0
  80. self.collidable = True
  81. self.flipped = {'horizontal': False, 'vertical': False}
  82. self.score = None
  83. self.lives = None
  84. self.chronometer = None
  85. super().__init__()
  86. self.load(image_filename)
  87. if not animation_filenames and name:
  88. animation_filenames = {
  89. action: f'{name}_{action}.png'
  90. for action in self._predefined_animations_allowed
  91. }
  92. animation_filenames.update({_('stop'): f'{name}.png'})
  93. self.set_animations(animation_filenames)
  94. def __repr__(self):
  95. return f'{_("Character")}: {self.name}'
  96. def _load_data(self, data: dict):
  97. """
  98. Load character attributes from data schema.
  99. Only declarated attributes in predefined_attributes_allowed are allowed.
  100. Parameters:
  101. data: input schema
  102. """
  103. if data is None:
  104. data = {}
  105. if not isinstance(data, dict):
  106. raise TypeError(_('The data input must be a dict.'))
  107. for attribute in data.keys():
  108. # utils._convert_function_arguments_to_string()
  109. if attribute in self._predefined_attributes_allowed:
  110. eval(f'self.set_{attribute}(data["{attribute}"])')
  111. @property
  112. def height(self) -> int:
  113. return self.rect.height
  114. @property
  115. def width(self) -> int:
  116. return self.rect.width
  117. def _eval_predefined_function(
  118. self, function: str, arguments: str | list | dict
  119. ):
  120. if not isinstance(function, str):
  121. raise TypeError(_('The function name must be a string.'))
  122. processed_arguments = utils._convert_function_arguments_to_string(
  123. arguments
  124. )
  125. eval(f'self.{function}({processed_arguments})')
  126. def _get_all_collidables(self):
  127. other_characters = _globals.vg_characters.copy()
  128. other_groups = _globals.vg_groups.copy()
  129. for other_group in other_groups:
  130. other_characters.extend(other_group.sprites())
  131. if self in other_characters:
  132. other_characters.remove(self)
  133. return [other for other in other_characters if other.collidable]
  134. def calculate_collisions(self, position: tuple | list) -> list:
  135. """
  136. Calculate all collisions if the character is moved to a position.
  137. Parameters:
  138. position: A tuple with the x and y coordinates.
  139. Returns:
  140. A list of all other collidable characters that collide with the character.
  141. """
  142. if not isinstance(position, tuple) and not isinstance(position, list):
  143. raise TypeError(
  144. _(
  145. 'The position must be a tuple or a list with the coordinates (x, y).'
  146. )
  147. )
  148. if len(position) != 2 or not all(
  149. map(lambda x: isinstance(x, int), position)
  150. ):
  151. raise ValueError(
  152. _(
  153. 'The position must be a tuple or a list of integers with the coordinates (x, y).'
  154. )
  155. )
  156. tracking = (
  157. True
  158. if self == _globals.character_focus and not position
  159. else False
  160. )
  161. offset_rect = self._update_rect_position(
  162. simulate=position, tracking=tracking
  163. )
  164. collisions = []
  165. for collidable in self._get_all_collidables():
  166. if offset_rect.colliderect(collidable):
  167. collisions.append(collidable)
  168. return collisions
  169. def move(
  170. self,
  171. x: int | None = None,
  172. y: int | None = None,
  173. key: str = '',
  174. relative: bool = True,
  175. ):
  176. """
  177. Move the character the number of pixels indicated in the x and y parameters
  178. Parameters:
  179. x: Number of pixels to move on the abscissa axis
  180. y: Number of pixels to move on the ordinate axis
  181. key: Optional key to perform the action
  182. relative: True for a incremental movement, False for an absolute movement
  183. Examples:
  184. >>> protagonist = Character('protagonist.png')
  185. >>> protagonist.move(5, 3)
  186. >>> protagonist.move()
  187. """
  188. if x is None:
  189. x = self.speed
  190. if y is None:
  191. y = self.speed
  192. if not isinstance(x, int) or not isinstance(y, int):
  193. raise TypeError(_('Amount to move ​​must be an integer.'))
  194. if not isinstance(key, str):
  195. raise TypeError(_('Key pressed must be a string.'))
  196. if not key or _get_keys()[utils.eval_key(key)]:
  197. if relative:
  198. updated_x, updated_y = self.x + x, self.y + y
  199. self.velocity.x, self.velocity.y = x, y
  200. else:
  201. updated_x, updated_y = x, y
  202. collisions = self.calculate_collisions((updated_x, updated_y))
  203. if collisions:
  204. collisions.sort(key=lambda c: c.rect.y)
  205. if self.velocity.y > 0:
  206. self.y = collisions[0].y - self.height
  207. self.velocity.y = 0
  208. elif self.velocity.y < 0:
  209. self.y = collisions[0].y + collisions[0].height
  210. self.velocity.y = 0
  211. collisions.sort(key=lambda c: c.rect.x)
  212. if self.velocity.x > 0:
  213. self.x = collisions[0].x - self.width
  214. self.velocity.x = 0
  215. elif self.velocity.x < 0:
  216. self.x = collisions[0].x + collisions[0].width
  217. self.velocity.x = 0
  218. else:
  219. self.x, self.y = updated_x, updated_y
  220. def move_to(self, x: int, y: int, key: str = ''):
  221. """
  222. Move the character to (x, y) position.
  223. Parameters:
  224. x: coordinate in horizontal axis
  225. y: coordinate in vertical axis
  226. key: Optional key to perform the action
  227. Examples:
  228. >>> protagonist = Character('protagonist.png')
  229. >>> protagonist.move_to(0, 0)
  230. """
  231. self.move(x, y, key, relative=False)
  232. def backward(self, amount: int | None = None, key: str = ''):
  233. """
  234. Move the character in the negative direction of the abscissa.
  235. Parameters:
  236. amount: Number of pixels to move
  237. key: Optional key to perform the action
  238. Examples:
  239. >>> protagonist = Character('protagonist.png')
  240. >>> protagonist.backward(3)
  241. >>> protagonist.backward()
  242. """
  243. if amount is None:
  244. amount = self.speed
  245. self.move(-amount, 0, key)
  246. def down(self, amount: int | None = None, key: str = ''):
  247. """
  248. Move the character in the positive direction of the ordinate.
  249. Parameters:
  250. amount: Number of pixels to move
  251. key: Optional key to perform the action
  252. Examples:
  253. >>> protagonist = Character('protagonist.png')
  254. >>> protagonist.down(3)
  255. >>> protagonist.down()
  256. """
  257. if amount is None:
  258. amount = self.speed
  259. if not self.is_jumping:
  260. self.move(0, amount, key)
  261. def forward(self, amount: int | None = None, key: str = ''):
  262. """
  263. Move the character in the positive direction of the abscissa.
  264. Parameters:
  265. amount: Number of pixels to move
  266. key: Optional key to perform the action
  267. Examples:
  268. >>> protagonist = Character('protagonist.png')
  269. >>> protagonist.forward(3)
  270. >>> protagonist.forward()
  271. """
  272. if amount is None:
  273. amount = self.speed
  274. self.move(amount, 0, key)
  275. def up(self, amount: int | None = None, key: str = ''):
  276. """
  277. Move the character in the negative direction of the ordinate.
  278. Parameters:
  279. amount: Number of pixels to move
  280. key: Optional key to perform the action
  281. Examples:
  282. >>> protagonist = Character('protagonist.png')
  283. >>> protagonist.up(3)
  284. >>> protagonist.up()
  285. """
  286. if amount is None:
  287. amount = self.speed
  288. if not self.is_jumping:
  289. self.move(0, -amount, key)
  290. def jump(self, force: float | None = None, key: str = ''):
  291. """
  292. Jump the character.
  293. Parameters:
  294. force: Jump force
  295. key: Optional key to perform the action
  296. Examples:
  297. >>> protagonist = Character('protagonist.png')
  298. >>> protagonist.jump()
  299. """
  300. if force is None:
  301. force = settings.CHARACTER_DEFAULT_JUMP_FORCE
  302. if isinstance(force, int):
  303. force = float(force)
  304. if not isinstance(force, float):
  305. raise TypeError(_('The jump force must be a float number.'))
  306. if force < 0:
  307. raise ValueError(_('The jump force must be a non-negative float.'))
  308. if not isinstance(key, str):
  309. raise TypeError(_('Key pressed must be a string.'))
  310. self.jump_force = force
  311. self.jump_counter = force
  312. if not self.is_jumping and (
  313. not key or _get_keys()[utils.eval_key(key)]
  314. ):
  315. self.is_jumping = True
  316. if _('jump') in self.animation_filenames.keys():
  317. self.load(
  318. self.animation_filenames[_('jump')], preserve_size=True
  319. )
  320. def rotate(self, increment: int = 1):
  321. """
  322. Rotate the character clockwise.
  323. Parameters:
  324. increment: Amount to be rotated
  325. Examples:
  326. >>> protagonist = Character('protagonist.png')
  327. >>> protagonist.rotate(90)
  328. """
  329. # TODO: rotate from the center
  330. if not isinstance(increment, int):
  331. raise TypeError(_('The rotation increment must be an integer.'))
  332. self.angle -= increment
  333. self.set_angle()
  334. def decrease(self, decrement: float | int = 0.001, key: str = ''):
  335. """
  336. Decreases the size of the character.
  337. Parameters:
  338. decrement: Amount to increase to the scale factor
  339. key: Optional key to perform the action
  340. Examples:
  341. >>> protagonist = Character('protagonist.png')
  342. >>> protagonist.decrease(0.1)
  343. """
  344. if isinstance(decrement, int):
  345. decrement = float(decrement)
  346. if not isinstance(decrement, float):
  347. raise TypeError(_('The decrement must be a float number.'))
  348. self.increase(-decrement, key)
  349. def increase(self, increment: float | int = 0.001, key: str = ''):
  350. """
  351. Increases the size of the character.
  352. Parameters:
  353. increment: Amount to increase to the scale factor
  354. key: Optional key to perform the action
  355. Examples:
  356. >>> protagonist = Character('protagonist.png')
  357. >>> protagonist.increase(0.1)
  358. """
  359. if isinstance(increment, int):
  360. increment = float(increment)
  361. if not isinstance(increment, float):
  362. raise TypeError(_('The increment must be a float number.'))
  363. if not key or _get_keys()[utils.eval_key(key)]:
  364. if self.scale_factor + increment < 0:
  365. self.scale_to(0)
  366. else:
  367. self.scale_to(self.scale_factor + increment)
  368. def scale_to(self, scale_factor: float = 1.0):
  369. """
  370. Scale the character according to the scale factor.
  371. Parameters:
  372. scale_factor: Factor to scale
  373. Examples:
  374. >>> protagonist = Character('protagonist.png')
  375. >>> protagonist.scale_to(2)
  376. """
  377. if not isinstance(scale_factor, float) and not isinstance(
  378. scale_factor, int
  379. ):
  380. raise TypeError(
  381. _('The scale factor must be a positive number (float or int).')
  382. )
  383. if scale_factor < 0:
  384. raise ValueError(
  385. _(
  386. 'The scale factor must be a non-negative number (float or int).'
  387. )
  388. )
  389. self.image = pygame.transform.scale(
  390. self._original_image,
  391. (
  392. int(self._original_rect.width * scale_factor),
  393. int(self._original_rect.height * scale_factor),
  394. ),
  395. )
  396. self.scale_factor = scale_factor
  397. self.rect = self.image.get_rect()
  398. def flip(self, horizontal: bool = True, vertical: bool = True):
  399. """
  400. Flip the character on horizontal and/or vertical axis.
  401. Parameters:
  402. horizontal: Flipped on horizontal axis
  403. vertical: Flipped on vertical axis
  404. Examples:
  405. >>> protagonist = Character('protagonist.png')
  406. >>> protagonist.flip(True, False)
  407. """
  408. if not isinstance(horizontal, bool) or not isinstance(vertical, bool):
  409. raise TypeError(
  410. _('Both the horizontal and vertical axis must be booleans.')
  411. )
  412. self.image = pygame.transform.flip(
  413. self.image,
  414. self.flipped['horizontal'] ^ horizontal,
  415. self.flipped['vertical'] ^ vertical,
  416. )
  417. self.flipped['horizontal'] = horizontal
  418. self.flipped['vertical'] = vertical
  419. def set_angle(self, angle: int | None = None):
  420. """
  421. Set the character rotation.
  422. Parameters:
  423. angle: Angle to be rotated.
  424. Examples:
  425. >>> protagonist = Character('protagonist.png')
  426. >>> protagonist.set_angle(90)
  427. """
  428. # TODO: rotate from the center
  429. if not isinstance(angle, int) and angle is not None:
  430. raise TypeError(_('The rotation angle must be an integer.'))
  431. if angle is not None:
  432. self.angle = 360 - angle
  433. self.image = pygame.transform.rotozoom(
  434. self._original_image, self.angle, self.scale_factor
  435. )
  436. def set_animations(self, animations: dict):
  437. """
  438. Set the image animation filenames for this character.
  439. Parameters:
  440. animations: A dict with pairs {action: filename}
  441. Examples:
  442. >>> protagonist = Character('protagonist.png')
  443. >>> protagonist.set_animations({'jump': 'protagonist_jump.png'})
  444. """
  445. if not isinstance(animations, dict):
  446. raise TypeError(_('The animations must be a dict.'))
  447. filtered_allowed_animations = dict(
  448. filter(
  449. lambda pair: pair[0] in self._predefined_animations_allowed,
  450. animations.items(),
  451. )
  452. )
  453. self.animation_filenames = dict(
  454. filter(
  455. lambda pair: os.path.isfile(
  456. os.path.join(settings.CHARACTERS_DIR, pair[1])
  457. ),
  458. filtered_allowed_animations.items(),
  459. )
  460. )
  461. def set_chronometer(self, time: int):
  462. """
  463. Set a chronometer for this character.
  464. Parameters:
  465. time: Initial time to countdown. If zero, progressive count.
  466. Examples:
  467. >>> protagonist = Character('protagonist.png')
  468. >>> protagonist.set_chronometer(30)
  469. """
  470. if not isinstance(time, int):
  471. raise TypeError(_('Time must be a integer.'))
  472. chronometer_pos = len(_globals.vg_chronometers)
  473. self.chronometer = widgets.Chronometer(
  474. time,
  475. position=(
  476. settings.CHRONOMETERS_DEFAULT_POSITION[0],
  477. settings.CHRONOMETERS_DEFAULT_POSITION[1]
  478. - chronometer_pos * settings.CHRONOMETERS_DEFAULT_HEIGHT,
  479. ),
  480. )
  481. self.chronometer.start()
  482. _globals.vg_chronometers.append(self.chronometer)
  483. def set_collidable(self, collidable: bool = True):
  484. """
  485. Set the character collidable.
  486. Parameters:
  487. collidable: True if it is collidable, False otherwise.
  488. """
  489. if not isinstance(collidable, bool):
  490. raise TypeError(_('The collidable argument must be a boolean.'))
  491. self.collidable = collidable
  492. def set_gravitative(self, gravitative: bool = True):
  493. """
  494. Set the character gravitative.
  495. Parameters:
  496. gravitative: True if it is collidable, False otherwise.
  497. """
  498. if not isinstance(gravitative, bool):
  499. raise TypeError(_('The gravitative argument must be a boolean.'))
  500. self.gravitative = gravitative
  501. def set_height(self, height: int):
  502. """
  503. Set the character height.
  504. Parameters:
  505. height: The character height in pixels
  506. Examples:
  507. >>> protagonist = Character('protagonist.png')
  508. >>> protagonist.set_height(100)
  509. """
  510. if not isinstance(height, int):
  511. raise TypeError(_('Height must be a not negative integer.'))
  512. if height < 1 or self._original_rect.height < 1:
  513. raise ValueError(_('Height must be a not negative integer.'))
  514. self.scale_factor = height / self._original_rect.height
  515. self.image = pygame.transform.scale(
  516. self._original_image,
  517. (int(self._original_rect.width * self.scale_factor), height),
  518. )
  519. self.rect = self.image.get_rect()
  520. def set_lives(self, lives: int):
  521. """
  522. Set the character lives.
  523. Parameters:
  524. lives: The character lives
  525. Examples:
  526. >>> protagonist = Character('protagonist.png')
  527. >>> protagonist.set_lives(5)
  528. """
  529. if self.lives is None:
  530. self.lives = widgets.LevelBar(lives)
  531. _globals.vg_levelbars.append(self.lives)
  532. self.lives.set(lives)
  533. def set_keys(self, predefined_keys: dict):
  534. """
  535. Set the character predefined keys to perform actions.
  536. Only declarated actions in _predefined_key_actions_allowed are allowed.
  537. Parameters:
  538. predefined_keys: The predefined keys with the associated action.
  539. Examples:
  540. >>> protagonist = Character('protagonist.png')
  541. >>> protagonist.set_keys({'forward': 'd'})
  542. """
  543. if not isinstance(predefined_keys, dict):
  544. raise TypeError(
  545. _('The predefined keys must be a dict. No keys are loaded.')
  546. )
  547. filtered_predefined_keys = {}
  548. for action, key in predefined_keys.items():
  549. if action in self._predefined_key_actions_allowed:
  550. filtered_predefined_keys.update({action: key})
  551. self.predefined_keys.update(filtered_predefined_keys)
  552. def set_position(self, position: tuple | list, key: str = ''):
  553. """
  554. Set the character position as tuple. To use as separate coordinates call move_to().
  555. Parameters:
  556. position: A tuple with abscissa and ordinate coordinates
  557. key: Optional key to perform the action
  558. Examples:
  559. >>> protagonist = Character('protagonist.png')
  560. >>> protagonist.set_position((0, 0))
  561. """
  562. if not isinstance(position, tuple) and not isinstance(position, list):
  563. raise TypeError(_('The position must be a tuple or list (x, y).'))
  564. if len(position) != 2:
  565. raise ValueError(_('The position has two coordinates (x, y).'))
  566. self.move(position[0], position[1], key, relative=False)
  567. def set_score(self, score: int):
  568. """
  569. Set the character score and the scoreboard.
  570. Parameters:
  571. score: the new character score
  572. Examples:
  573. >>> protagonist = Character('protagonist.png')
  574. >>> protagonist.set_score(5)
  575. """
  576. if not isinstance(score, int):
  577. raise TypeError(_('The score must be an integer.'))
  578. scoreboard_pos = len(_globals.vg_scoreboards)
  579. if self.score is None:
  580. self.score = widgets.Scoreboard(
  581. f'{self.name} ',
  582. score=score,
  583. position=(
  584. settings.SCOREBOARDS_DEFAULT_POSITION[0],
  585. settings.SCOREBOARDS_DEFAULT_POSITION[1]
  586. + scoreboard_pos * 50,
  587. ),
  588. )
  589. _globals.vg_scoreboards.append(self.score)
  590. else:
  591. self.score.set(score)
  592. def set_speed(self, speed: int):
  593. """
  594. Set the character speed.
  595. Parameters:
  596. speed: The character speed
  597. Examples:
  598. >>> protagonist = Character('protagonist.png')
  599. >>> protagonist.set_speed(10)
  600. """
  601. if not isinstance(speed, int):
  602. raise TypeError(_('The speed must be an integer.'))
  603. self.speed = speed
  604. self.speed = speed
  605. def set_width(self, width: int):
  606. """
  607. Set the character width.
  608. Parameters:
  609. width: the character width in pixels
  610. Examples:
  611. >>> protagonist = Character('protagonist.png')
  612. >>> protagonist.set_width(100)
  613. """
  614. if not isinstance(width, int):
  615. raise TypeError(_('Width must be a not negative integer.'))
  616. if width < 1 or self._original_rect.width < 1:
  617. raise ValueError(_('Width must be a not negative integer.'))
  618. self.scale_factor = width / self._original_rect.width
  619. self.image = pygame.transform.scale(
  620. self._original_image,
  621. (width, int(self._original_rect.height * self.scale_factor)),
  622. )
  623. self.rect = self.image.get_rect()
  624. def touch(
  625. self,
  626. other_characters: Type['Character'] | Type['jogai.group.Group'] | list,
  627. key: str = '',
  628. ) -> str:
  629. """
  630. Detect if one character is touching another(s).
  631. Parameters:
  632. other_characters: character or list of characters to check
  633. Returns:
  634. True, if it is touching; False, otherwise.
  635. Examples:
  636. >>> protagonist = Character('protagonist.png')
  637. >>> secondary = Character('secondary.png')
  638. >>> protagonist.touch(secondary)
  639. 'static'
  640. """
  641. if isinstance(other_characters, Character):
  642. other_characters = [other_characters]
  643. if not isinstance(other_characters, list) and not isinstance(
  644. other_characters, jogai.group.Group
  645. ):
  646. raise TypeError(
  647. _(
  648. 'The instances to be touched must be a character or a list of characters.'
  649. )
  650. )
  651. if isinstance(other_characters, list) and not all(
  652. map(lambda x: isinstance(x, Character), other_characters)
  653. ):
  654. raise ValueError(
  655. _('All instances to be touched must be characters.')
  656. )
  657. if not isinstance(key, str):
  658. raise TypeError(_('Key pressed must be a string.'))
  659. for other_character in other_characters:
  660. if not key or _get_keys()[utils.eval_key(key)]:
  661. if self.rect.colliderect(other_character.rect):
  662. if self.velocity.y > 0:
  663. return self.TOP_COLLISION
  664. elif self.velocity.y < 0:
  665. return self.BOTTOM_COLLISION
  666. if self.velocity.x > 0:
  667. return self.LEFT_COLLISION
  668. elif self.velocity.x < 0:
  669. return self.RIGHT_COLLISION
  670. return self.STATIC_COLLISION
  671. return ''
  672. def generate_thumbnail(
  673. self, height: int = settings.CHARACTER_DEFAULT_THUMBNAIL_SIZE
  674. ):
  675. self.scale_factor = height / self._original_rect.height
  676. self.thumbnail = pygame.transform.scale(
  677. self._original_image,
  678. (int(self._original_rect.width * self.scale_factor), height),
  679. )
  680. self.thumbnail_rect = self.thumbnail.get_rect()
  681. def load(self, filename: str, preserve_size: bool = False):
  682. """
  683. >>> protagonist = Character('protagonist.png')
  684. >>> protagonist.load('secondary.png')
  685. """
  686. self._original_image, self._original_rect = utils._load_image(
  687. settings.CHARACTERS_DIR, filename
  688. )
  689. if preserve_size:
  690. height = self.height
  691. self.image = self._original_image
  692. self.generate_thumbnail()
  693. self._original_rect = self._original_image.get_rect()
  694. self.rect = self.image.get_rect()
  695. self.scale_factor = 1
  696. self.image_filename = filename
  697. if preserve_size:
  698. self.set_height(height)
  699. def copy(self):
  700. """
  701. Create a copy of this character keeping size and position.
  702. Examples:
  703. >>> protagonist = Character('protagonist.png')
  704. >>> other = protagonist.copy()
  705. """
  706. new_character = Character(self.image_filename)
  707. new_character.set_collidable(self.collidable)
  708. new_character.set_gravitative(self.gravitative)
  709. new_character.set_height(self.height)
  710. new_character.set_width(self.width)
  711. new_character.set_position((self.x, self.y))
  712. return new_character
  713. def _update_jump(self):
  714. """
  715. Calculate and update the character position during the jump.
  716. """
  717. if self.is_jumping:
  718. if self._jump_counter >= -self.jump_force:
  719. self.move(
  720. 0,
  721. -int((self._jump_counter * abs(self._jump_counter)) * 0.5),
  722. )
  723. self._jump_counter -= 1
  724. else:
  725. self._jump_counter = settings.CHARACTER_DEFAULT_JUMP_FORCE
  726. self.is_jumping = False
  727. self.load(
  728. self.animation_filenames[_('stop')], preserve_size=True
  729. )
  730. def _update_rect_position(
  731. self,
  732. tracking: bool = False,
  733. simulate: tuple = (),
  734. ) -> Type['pygame.Rect']:
  735. """
  736. Calculate and update the position where the character must be painted.
  737. Only one character can be tracked.
  738. Parameters:
  739. tracking: True if the character must be followed, False otherwise.
  740. Returns:
  741. A rect placed in the videogame window.
  742. """
  743. if not isinstance(tracking, bool):
  744. raise TypeError(_('The tracking parameter must be a boolean.'))
  745. if not _globals.vg_scene:
  746. return None
  747. if simulate:
  748. offset_rect = self.rect.copy()
  749. x, y = simulate
  750. else:
  751. offset_rect = self.rect
  752. x, y = self.x, self.y
  753. if tracking:
  754. if x <= settings.WINDOW_WIDTH // 2 or x > _globals.vg_scene.width:
  755. offset_rect.x = x
  756. elif (
  757. settings.WINDOW_WIDTH // 2
  758. < x
  759. < _globals.vg_scene.width - settings.WINDOW_WIDTH
  760. ):
  761. offset_rect.x = settings.WINDOW_WIDTH // 2
  762. elif x >= _globals.vg_scene.width - settings.WINDOW_WIDTH:
  763. offset_rect.x = settings.WINDOW_WIDTH * 3 // 2 - (
  764. -x % _globals.vg_scene.width
  765. )
  766. else:
  767. offset_rect.x = x + _globals.vg_scene.x
  768. offset_rect.y = y
  769. return offset_rect
  770. def update(self):
  771. for action, key in self.predefined_keys.items():
  772. eval(f'self.{action}(key="{key}")')
  773. if self.gravitative:
  774. self.down(_globals._gravity)
  775. self._update_jump()