character.py 30 KB

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