character.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import os
  2. from copy import deepcopy
  3. from typing import Type
  4. import pygame
  5. from jogai import _events, logger, settings, utils
  6. from jogai._events import _get_keys
  7. from jogai._globals import _window
  8. from jogai.translations import gettext as _
  9. class Character(pygame.sprite.Sprite):
  10. """
  11. A videogame character with a bunch of actions.
  12. """
  13. _predefined_key_actions_allowed = [
  14. _('backward'),
  15. _('decrease'),
  16. _('down'),
  17. _('forward'),
  18. _('increase'),
  19. _('jump'),
  20. _('up'),
  21. ]
  22. _predefined_attributes_allowed = [
  23. _('height'),
  24. _('keys'),
  25. _('position'),
  26. _('speed'),
  27. _('width'),
  28. ]
  29. def __init__(
  30. self,
  31. image_filename: str,
  32. name: str = '',
  33. ):
  34. if not isinstance(name, str):
  35. raise TypeError(_('The character name must be a string.'))
  36. self.scale_factor = 1.0
  37. self.speed = 0
  38. self.predefined_keys = {}
  39. self.image_filename = image_filename
  40. self.image = None
  41. self._original_image = None
  42. self._original_rect = None
  43. self.rect = None
  44. self.name = name
  45. self.x = 0
  46. self.y = 0
  47. self.is_jumping = False
  48. self.jump_force = settings.DEFAULT_JUMP_FORCE
  49. self._jump_counter = self.jump_force
  50. super().__init__()
  51. self.load(image_filename)
  52. def __repr__(self):
  53. return f'{_("Character")}: {self.name}'
  54. def _load_data(self, data: dict):
  55. """
  56. Load character attributes from data schema.
  57. Only declarated attributes in predefined_attributes_allowed are allowed.
  58. Parameters:
  59. data: input schema
  60. """
  61. if data is None:
  62. data = {}
  63. if not isinstance(data, dict):
  64. raise TypeError(_('The data input must be a dict.'))
  65. for attribute in data.keys():
  66. # utils._convert_function_arguments_to_string()
  67. if attribute in self._predefined_attributes_allowed:
  68. eval(f'self.set_{attribute}(data["{attribute}"])')
  69. @property
  70. def height(self):
  71. return self.rect.height
  72. @property
  73. def width(self):
  74. return self.rect.width
  75. def _eval_predefined_function(
  76. self, function: str, arguments: str | list | dict
  77. ):
  78. if not isinstance(function, str):
  79. raise TypeError(_('The function name must be a string.'))
  80. processed_arguments = utils._convert_function_arguments_to_string(
  81. arguments
  82. )
  83. eval(f'self.{function}({processed_arguments})')
  84. def move(
  85. self,
  86. x: int | None = None,
  87. y: int | None = None,
  88. key: str = '',
  89. relative: bool = True,
  90. ):
  91. """
  92. Move the character the number of pixels indicated in the x and y parameters
  93. Parameters:
  94. x: Number of pixels to move on the abscissa axis
  95. y: Number of pixels to move on the ordinate axis
  96. key: Optional key to perform the action
  97. relative: True for a incremental movement, False for an absolute movement
  98. Examples:
  99. >>> ch = Character('protagonist.png')
  100. >>> ch.move(5, 3)
  101. >>> ch.move()
  102. """
  103. if x is None:
  104. x = self.speed
  105. if y is None:
  106. y = self.speed
  107. if not isinstance(x, int) or not isinstance(y, int):
  108. raise TypeError(_('Amount to move ​​must be an integer.'))
  109. if not isinstance(key, str):
  110. raise TypeError(_('Key pressed must be a string.'))
  111. if not key or _get_keys()[utils.eval_key(key)]:
  112. if relative:
  113. self.x += x
  114. self.y += y
  115. else:
  116. self.x = x
  117. self.y = y
  118. def move_to(self, x: int, y: int, key: str = ''):
  119. """
  120. Move the character to (x, y) position.
  121. Parameters:
  122. x: coordinate in horizontal axis
  123. y: coordinate in vertical axis
  124. key: Optional key to perform the action
  125. Examples:
  126. >>> ch = Character('protagonist.png')
  127. >>> ch.move_to(0, 0)
  128. """
  129. self.move(x, y, key, relative=False)
  130. def backward(self, amount: int | None = None, key: str = ''):
  131. """
  132. Moves the character in the negative direction of the abscissa.
  133. Parameters:
  134. amount: Number of pixels to move
  135. key: Optional key to perform the action
  136. Examples:
  137. >>> ch = Character('protagonist.png')
  138. >>> ch.backward(3)
  139. >>> ch.backward()
  140. """
  141. if amount is None:
  142. amount = self.speed
  143. self.move(-amount, 0, key)
  144. def down(self, amount: int | None = None, key: str = ''):
  145. """
  146. Moves the character in the positive direction of the ordinate.
  147. Parameters:
  148. amount: Number of pixels to move
  149. key: Optional key to perform the action
  150. Examples:
  151. >>> ch = Character('protagonist.png')
  152. >>> ch.down(3)
  153. >>> ch.down()
  154. """
  155. if amount is None:
  156. amount = self.speed
  157. if not self.is_jumping:
  158. self.move(0, amount, key)
  159. def forward(self, amount: int | None = None, key: str = ''):
  160. """
  161. Moves the character in the positive direction of the abscissa.
  162. Parameters:
  163. amount: Number of pixels to move
  164. key: Optional key to perform the action
  165. Examples:
  166. >>> ch = Character('protagonist.png')
  167. >>> ch.forward(3)
  168. >>> ch.forward()
  169. """
  170. if amount is None:
  171. amount = self.speed
  172. self.move(amount, 0, key)
  173. def up(self, amount: int | None = None, key: str = ''):
  174. """
  175. Moves the character in the negative direction of the ordinate.
  176. Parameters:
  177. amount: Number of pixels to move
  178. key: Optional key to perform the action
  179. Examples:
  180. >>> ch = Character('protagonist.png')
  181. >>> ch.up(3)
  182. >>> ch.up()
  183. """
  184. if amount is None:
  185. amount = self.speed
  186. if not self.is_jumping:
  187. self.move(0, -amount, key)
  188. def jump(self, force: float | None = None, key: str = ''):
  189. if force is None:
  190. force = settings.DEFAULT_JUMP_FORCE
  191. if isinstance(force, int):
  192. force = float(force)
  193. if not isinstance(force, float):
  194. raise TypeError(_('The jump force must be a float number.'))
  195. if force < 0:
  196. raise ValueError(_('The jump force must be a non-negative float.'))
  197. self.jump_force = force
  198. self.jump_counter = force
  199. if not self.is_jumping and (
  200. not key or _get_keys()[utils.eval_key(key)]
  201. ):
  202. self.is_jumping = True
  203. def decrease(self, decrement: float | int = 0.001, key: str = ''):
  204. """
  205. Decreases the size of the character.
  206. Parameters:
  207. decrement: amount to increase to the scale factor
  208. key: Optional key to perform the action
  209. Examples:
  210. >>> ch = Character('protagonist.png')
  211. >>> ch.decrease(0.1)
  212. """
  213. if isinstance(decrement, int):
  214. decrement = float(decrement)
  215. if not isinstance(decrement, float):
  216. raise TypeError(_('The decrement must be a float number.'))
  217. self.increase(-decrement, key)
  218. def increase(self, increment: float | int = 0.001, key: str = ''):
  219. """
  220. Increases the size of the character.
  221. Parameters:
  222. increment: amount to increase to the scale factor
  223. key: Optional key to perform the action
  224. Examples:
  225. >>> ch = Character('protagonist.png')
  226. >>> ch.increase(0.1)
  227. """
  228. if isinstance(increment, int):
  229. increment = float(increment)
  230. if not isinstance(increment, float):
  231. raise TypeError(_('The increment must be a float number.'))
  232. if not key or _get_keys()[utils.eval_key(key)]:
  233. if self.scale_factor + increment < 0:
  234. self.scale_to(0)
  235. else:
  236. self.scale_to(self.scale_factor + increment)
  237. def scale_to(self, scale_factor: float = 1.0):
  238. """
  239. Scale the character according to the scale factor.
  240. Parameters:
  241. scale_factor: factor to scale
  242. Examples:
  243. >>> ch = Character('protagonist.png')
  244. >>> ch.scale_to(2)
  245. """
  246. if not isinstance(scale_factor, float) and not isinstance(
  247. scale_factor, int
  248. ):
  249. raise TypeError(
  250. _('The scale factor must be a positive number (float or int).')
  251. )
  252. if scale_factor < 0:
  253. raise ValueError(
  254. _(
  255. 'The scale factor must be a non-negative number (float or int).'
  256. )
  257. )
  258. self.image = pygame.transform.scale(
  259. self._original_image,
  260. (
  261. int(self._original_rect.width * scale_factor),
  262. int(self._original_rect.height * scale_factor),
  263. ),
  264. )
  265. self.scale_factor = scale_factor
  266. self.rect = self.image.get_rect()
  267. def set_height(self, height: int = 0):
  268. """
  269. Set the character height.
  270. Parameters:
  271. height: the character height in pixels
  272. Examples:
  273. >>> ch = Character('protagonist.png')
  274. >>> ch.set_height(100)
  275. """
  276. if not isinstance(height, int):
  277. raise TypeError(_('Height must be a not negative integer.'))
  278. if height < 1 or self._original_rect.height < 1:
  279. raise ValueError(_('Height must be a not negative integer.'))
  280. self.scale_factor = height / self._original_rect.height
  281. self.image = pygame.transform.scale(
  282. self._original_image,
  283. (int(self._original_rect.width * self.scale_factor), height),
  284. )
  285. self.rect = self.image.get_rect()
  286. def set_keys(self, predefined_keys: dict):
  287. """
  288. Set the character predefined keys to perform actions.
  289. Only declarated actions in _predefined_key_actions_allowed are allowed.
  290. Parameters:
  291. predefined_keys: The predefined keys with the associated action.
  292. Examples:
  293. >>> ch = Character('protagonist.png')
  294. >>> ch.set_keys({'forward': 'd'})
  295. """
  296. if not isinstance(predefined_keys, dict):
  297. raise TypeError(
  298. _('The predefined keys must be a dict. No keys are loaded.')
  299. )
  300. filtered_predefined_keys = {}
  301. for action, key in predefined_keys.items():
  302. if action in self._predefined_key_actions_allowed:
  303. filtered_predefined_keys.update({action: key})
  304. self.predefined_keys.update(filtered_predefined_keys)
  305. def set_position(self, position: tuple | list, key: str = ''):
  306. """
  307. Set the character position as tuple. To use as separate coordinates call move_to().
  308. Parameters:
  309. position: A tuple with abscissa and ordinate coordinates
  310. key: Optional key to perform the action
  311. Examples:
  312. >>> ch = Character('protagonist.png')
  313. >>> ch.set_position((0, 0))
  314. """
  315. if not isinstance(position, tuple) and not isinstance(position, list):
  316. raise TypeError(_('The position must be a tuple or list (x, y).'))
  317. if len(position) != 2:
  318. raise ValueError(_('The position has two coordinates (x, y).'))
  319. self.move(position[0], position[1], key, relative=False)
  320. def set_speed(self, speed: int = 0):
  321. """
  322. Set the character speed.
  323. Parameters:
  324. speed: The character speed
  325. Examples:
  326. >>> ch = Character('protagonist.png')
  327. >>> ch.set_speed(10)
  328. """
  329. if isinstance(speed, int):
  330. self.speed = speed
  331. def set_width(self, width: int = 0):
  332. """
  333. Set the character width.
  334. Parameters:
  335. width: the character width in pixels
  336. Examples:
  337. >>> ch = Character('protagonist.png')
  338. >>> ch.set_width(100)
  339. """
  340. if not isinstance(width, int):
  341. raise TypeError(_('Width must be a not negative integer.'))
  342. if width < 1 or self._original_rect.width < 1:
  343. raise ValueError(_('Width must be a not negative integer.'))
  344. self.scale_factor = width / self._original_rect.width
  345. self.image = pygame.transform.scale(
  346. self._original_image,
  347. (width, int(self._original_rect.height * self.scale_factor)),
  348. )
  349. self.rect = self.image.get_rect()
  350. def load(self, filename: str) -> bool:
  351. """
  352. >>> ch = Character('protagonist.png')
  353. >>> result = ch.load('protagonist.png')
  354. """
  355. self._original_image, self._original_rect = utils._load_image(
  356. settings.CHARACTERS_DIR, filename
  357. )
  358. self.image = self._original_image
  359. self._original_rect = self._original_image.get_rect()
  360. self.rect = self.image.get_rect()
  361. self.scale_factor = 1
  362. self.image_filename = filename
  363. def copy(self):
  364. """
  365. Create a copy of this character keeping size and position.
  366. Examples:
  367. >>> ch = Character('protagonist.png')
  368. >>> other = ch.copy()
  369. """
  370. new_character = Character(self.image_filename)
  371. new_character.set_height(self.height)
  372. new_character.set_width(self.width)
  373. new_character.set_position((self.x, self.y))
  374. return new_character
  375. def paint(self):
  376. """
  377. Paint the character in the main surface (window).
  378. Examples:
  379. >>> ch = Character('protagonist.png')
  380. >>> ch.paint()
  381. """
  382. if self.image:
  383. _window.blit(self.image, self.rect)
  384. def _update_jump(self):
  385. """
  386. Calculate and update the character position during the jump.
  387. """
  388. if self.is_jumping:
  389. if self._jump_counter >= -self.jump_force:
  390. self.move(
  391. 0,
  392. -int((self._jump_counter * abs(self._jump_counter)) * 0.5),
  393. )
  394. self._jump_counter -= 1
  395. else:
  396. self._jump_counter = settings.DEFAULT_JUMP_FORCE
  397. self.is_jumping = False
  398. def _update_rect_position(
  399. self, vg_scene: Type['scene.Scene'], tracking: bool = False
  400. ):
  401. """
  402. Calculate and update the position where the character must be painted.
  403. Only one character can be tracked.
  404. Parameters:
  405. vg_scene: The scene information.
  406. tracking: True if the character must be followed, False otherwise.
  407. """
  408. # TODO: check vg_scene type
  409. if not isinstance(tracking, bool):
  410. raise TypeError(_('The tracking parameter must be a boolean.'))
  411. if not vg_scene:
  412. return None
  413. if tracking:
  414. if self.x < settings.WINDOW_WIDTH // 2:
  415. self.rect.x = self.x
  416. elif (
  417. settings.WINDOW_WIDTH // 2
  418. < self.x
  419. < vg_scene.width - settings.WINDOW_WIDTH
  420. ):
  421. self.rect.x = settings.WINDOW_WIDTH // 2
  422. elif self.x > vg_scene.width - settings.WINDOW_WIDTH:
  423. self.rect.x = settings.WINDOW_WIDTH * 3 / 2 - (
  424. -self.x % vg_scene.width
  425. )
  426. self.rect.y = self.y
  427. else:
  428. self.rect.x = self.x + vg_scene.x
  429. self.rect.y = self.y
  430. def update(self):
  431. for action, key in self.predefined_keys.items():
  432. eval(f'self.{action}(key="{key}")')
  433. self._update_jump()