widgets.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. from os.path import join
  2. import pygame
  3. from jogai import _globals, settings
  4. from jogai.translations import gettext as _
  5. class Text:
  6. def __init__(
  7. self,
  8. message: str = '',
  9. size: int = 20,
  10. color: tuple = (0, 0, 0),
  11. position: tuple = (0, 0),
  12. font: str = '',
  13. bold: bool = False,
  14. italic: bool = False,
  15. shadow: tuple = (),
  16. ):
  17. # TODO: Implement shadow
  18. if not isinstance(size, int):
  19. raise TypeError(_('The font size must be an integer'))
  20. if size < 1:
  21. raise ValueError(_('The font size must be a positive integer'))
  22. if not isinstance(font, str):
  23. raise TypeError(_('The font must be a string'))
  24. if not isinstance(bold, bool):
  25. raise TypeError(_('The bold argument must be a boolean'))
  26. if not isinstance(italic, bool):
  27. raise TypeError(_('The italic argument must be a boolean'))
  28. if not pygame.font.get_init():
  29. pygame.font.init()
  30. self._message = message
  31. self.color = color
  32. try:
  33. if not font:
  34. raise FileNotFoundError
  35. self.font = pygame.font.Font(join(settings.FONTS_DIR, font), size)
  36. except FileNotFoundError:
  37. self.font = pygame.font.Font(None, size)
  38. self.font.set_bold(bold)
  39. self.font.set_italic(italic)
  40. self.render(position, message)
  41. @property
  42. def x(self):
  43. return self.rect.x
  44. @property
  45. def y(self):
  46. return self.rect.y
  47. def render(self, position: tuple = (), message: str = ''):
  48. if not isinstance(position, tuple) and not isinstance(position, list):
  49. raise TypeError(_('The position must be a tuple or list (x, y).'))
  50. if position and len(position) != 2:
  51. raise ValueError(_('The position has two coordinates (x, y).'))
  52. if not isinstance(message, str):
  53. raise TypeError(_('The message must be a string.'))
  54. if not position:
  55. position = (self.rect.x, self.rect.y)
  56. if message:
  57. self._message = message
  58. self.image = self.font.render(self._message, True, self.color)
  59. self.rect = self.image.get_rect()
  60. self.rect.x, self.rect.y = position
  61. def set_message(self, message: str, key: str = ''):
  62. """
  63. Set a new message for the text.
  64. Parameters:
  65. message: The new message
  66. Examples:
  67. >>> t = Text()
  68. >>> t.set_message('Test')
  69. """
  70. if not key or _get_keys()[utils.eval_key(key)]:
  71. self.render(message=message)
  72. def set_position(self, position: tuple | list, key: str = ''):
  73. """
  74. Set the text position as tuple.
  75. Parameters:
  76. position: A tuple with abscissa and ordinate coordinates
  77. Examples:
  78. >>> t = Text('Test')
  79. >>> t.set_position((200, 200))
  80. """
  81. if not key or _get_keys()[utils.eval_key(key)]:
  82. self.render(position=position)
  83. class Chronometer(Text):
  84. def __init__(
  85. self, initial: int = 0, in_milliseconds: bool = False, **kwargs
  86. ):
  87. if not isinstance(initial, int):
  88. raise TypeError(_('The initial value must be an integer.'))
  89. if initial < 0:
  90. raise ValueError(_('The countdown cannot be negative.'))
  91. if not isinstance(in_milliseconds, bool):
  92. raise TypeError(_('in_milliseconds argument must be a boolean.'))
  93. super().__init__(**kwargs)
  94. self._initial = initial if in_milliseconds else initial * 1000
  95. self._time = 0
  96. self._timestamp = 0
  97. self._onpause = True
  98. self._message = self.format_clock()
  99. if 'position' in kwargs:
  100. self.render(position=kwargs['position'])
  101. else:
  102. self.render()
  103. def __int__(self):
  104. return self.get_milliseconds()
  105. def __repr__(self):
  106. return f'Chronometer <{self.format_clock()}>'
  107. def __str__(self):
  108. return self._message
  109. @property
  110. def time(self):
  111. return self.get_milliseconds()
  112. @property
  113. def onpause(self) -> bool:
  114. return self._onpause
  115. def format_clock(
  116. self,
  117. include_millis: bool = False,
  118. delimiter: str = ':',
  119. delimiter_millis: str = '.',
  120. ) -> str:
  121. """
  122. Create a string with the chronometer time information, formatted as a digital clock.
  123. Parameters:
  124. include_millis: True if milliseconds must be present. False, otherwise.
  125. delimiter: The string delimiter between hours, minutes and seconds.
  126. delimiter_millis: The string delimiter between seconds and milliseconds.
  127. Returns:
  128. A string with the formatted time information.
  129. """
  130. if not isinstance(include_millis, bool):
  131. raise TypeError(_('include_millis must be a boolean.'))
  132. if not isinstance(delimiter, str):
  133. raise TypeError(_('The delimiter must be a string.'))
  134. if not isinstance(delimiter_millis, str):
  135. raise TypeError(
  136. _('The delimiter for milliseconds must be a string.')
  137. )
  138. formatted_time = tuple(
  139. map(lambda x: str(x).zfill(2), self.get_clock())
  140. )
  141. formatted_clock = ':'.join(formatted_time[:-1])
  142. if include_millis:
  143. formatted_clock += f'.{formatted_time[-1]}'
  144. return formatted_clock
  145. def get_clock(self) -> tuple:
  146. """
  147. Get the current chronometer time information.
  148. Returns:
  149. A tuple with the time information (hours, minutes, seconds, milliseconds)
  150. """
  151. millis = self.get_milliseconds()
  152. if millis > 0:
  153. quotient = millis // 1000
  154. millis = millis % 1000
  155. seconds = quotient % 60
  156. quotient = quotient // 60
  157. minutes = quotient % 60
  158. hours = quotient // 60
  159. return hours, minutes, seconds, millis
  160. return 0, 0, 0, 0
  161. def get_milliseconds(self) -> int:
  162. """
  163. Get the current chronometer time information in milliseconds.
  164. Returns:
  165. An integer with the current time in milliseconds.
  166. """
  167. if self._onpause:
  168. current = (
  169. self._initial - self._time if self._initial else self._time
  170. )
  171. else:
  172. if self._initial:
  173. current = self._initial - (
  174. self._time + pygame.time.get_ticks() - self._timestamp
  175. )
  176. else:
  177. current = (
  178. self._time + pygame.time.get_ticks() - self._timestamp
  179. )
  180. current = max(0, current)
  181. return current
  182. def get_seconds(self) -> int:
  183. """
  184. Get the current chronometer time information in seconds.
  185. Returns:
  186. An integer with the current time in seconds.
  187. """
  188. return self.get_milliseconds() // 1000
  189. def start(self):
  190. """
  191. Start the chronometer count.
  192. """
  193. self._time = 0
  194. self._timestamp = pygame.time.get_ticks()
  195. self._onpause = False
  196. def pause(self):
  197. """
  198. Pause the chronometer count.
  199. """
  200. if not self._onpause:
  201. self._onpause = True
  202. self._time = self._time + pygame.time.get_ticks() - self._timestamp
  203. def reset(self):
  204. """
  205. Reset the chronometer count.
  206. """
  207. self._time = 0
  208. def resume(self):
  209. """
  210. Resume the chronometer count.
  211. """
  212. if self._onpause:
  213. self._onpause = False
  214. self._timestamp = pygame.time.get_ticks()
  215. def update(self):
  216. """
  217. Update the text message with the current time information.
  218. """
  219. self._message = self.format_clock()
  220. self.render()
  221. class LevelBar(pygame.Surface):
  222. def __init__(
  223. self,
  224. parts: int,
  225. initial_level: int | None = None,
  226. position: tuple | list = settings.LEVELBARS_DEFAULT_POSITION,
  227. border_color: tuple | list = settings.LEVELBARS_DEFAULT_BORDER_COLOR,
  228. fill_color: tuple | list = settings.LEVELBARS_DEFAULT_FILL_COLOR,
  229. ):
  230. if not isinstance(parts, int):
  231. raise TypeError(_('The amount of parts must be an integer.'))
  232. if not (0 < parts <= settings.LEVELBARS_MAX_PARTS):
  233. raise TypeError(
  234. _(
  235. f'The amount of parts must be between 1 and {settings.LEVELBARS_MAX_PARTS}.'
  236. )
  237. )
  238. super().__init__(
  239. (
  240. settings.LEVELBARS_DEFAULT_WIDTH,
  241. settings.LEVELBARS_DEFAULT_HEIGHT,
  242. )
  243. )
  244. self._parts = parts
  245. if initial_level is None:
  246. initial_level = parts
  247. self.set(initial_level)
  248. self.border_color = border_color
  249. self.fill_color = fill_color
  250. self.x, self.y = position
  251. self._width = settings.LEVELBARS_DEFAULT_WIDTH
  252. self._height = settings.LEVELBARS_DEFAULT_HEIGHT
  253. self._thick = settings.LEVELBARS_DEFAULT_THICK
  254. def __int__(self):
  255. return int(self._level)
  256. def __repr(self):
  257. return f'LevelBar <{self._level}/{self._parts}>'
  258. def __str__(self):
  259. return f'{self._level}/{self._parts}'
  260. def decrease(self, decrease: int):
  261. """
  262. Decrease the level for this bar.
  263. Parameters:
  264. decrease: amount to decrease
  265. Examples:
  266. >>> lifes = LevelBar(5, 3)
  267. >>> lifes.decrease(1)
  268. """
  269. if not isinstance(decrease, int):
  270. raise TypeError(_('The decrease level must be an integer.'))
  271. if self._level - decrease < 0:
  272. raise ValueError(_('The decrease cannot be less than zero.'))
  273. self.set(self._level - decrease)
  274. def increase(self, increase: int):
  275. """
  276. Increase the level for this bar.
  277. Parameters:
  278. increase: amount to increase
  279. Examples:
  280. >>> lifes = LevelBar(5, 2)
  281. >>> lifes.increase(1)
  282. """
  283. if not isinstance(increase, int):
  284. raise TypeError(_('The increase level must be an integer.'))
  285. if self._level + increase > self._parts:
  286. raise ValueError(
  287. _('The increase cannot exceed the maximum level.')
  288. )
  289. self.set(self._level + increase)
  290. def set(self, level):
  291. """
  292. Set the new level for this bar.
  293. Parameters:
  294. level: the new level
  295. Examples:
  296. >>> lifes = LevelBar(5)
  297. >>> lifes.set(3)
  298. """
  299. if not isinstance(level, int):
  300. raise TypeError(_('The level must be an integer.'))
  301. if not (0 <= level <= self._parts):
  302. raise ValueError(
  303. _(
  304. 'The level range must be between zero and the number of parts.'
  305. )
  306. )
  307. self._level = level
  308. def draw(self, position: tuple | list | None = None):
  309. if position is None:
  310. position = (self.x, self.y)
  311. else:
  312. self.x, self.y = position
  313. division = int(self._width / self._parts)
  314. # left border
  315. pygame.draw.line(
  316. _globals._window,
  317. self.border_color,
  318. position,
  319. (position[0], position[1] + self._height),
  320. self._thick,
  321. )
  322. # right border
  323. pygame.draw.line(
  324. _globals._window,
  325. self.border_color,
  326. (position[0] + division * self._parts, position[1]),
  327. (position[0] + division * self._parts, position[1] + self._height),
  328. self._thick,
  329. )
  330. for part in range(self._parts):
  331. # top border
  332. pygame.draw.line(
  333. _globals._window,
  334. self.border_color,
  335. (position[0] + division * part, position[1]),
  336. (position[0] + division * (part + 1), position[1]),
  337. self._thick,
  338. )
  339. # inside
  340. if part < self._level:
  341. pygame.draw.line(
  342. _globals._window,
  343. self.fill_color,
  344. (
  345. position[0] + division * part + self._thick / 2,
  346. position[1] + int(self._height / 2),
  347. ),
  348. (
  349. position[0] + division * (part + 1) - self._thick / 2,
  350. position[1] + int(self._height / 2),
  351. ),
  352. self._height,
  353. )
  354. # bottom border
  355. pygame.draw.line(
  356. _globals._window,
  357. self.border_color,
  358. (position[0] + division * part, position[1] + self._height),
  359. (
  360. position[0] + division * (part + 1),
  361. position[1] + self._height,
  362. ),
  363. self._thick,
  364. )
  365. self.rect = self.get_rect()
  366. class Scoreboard(Text):
  367. def __init__(self, prefix: str, score: int = 0, **kwargs):
  368. if not isinstance(prefix, str):
  369. raise TypeError(_('The prefix text must be a string.'))
  370. super().__init__(**kwargs)
  371. self._prefix = prefix
  372. self.set_score(score)
  373. if 'position' in kwargs:
  374. self.render(position=kwargs['position'])
  375. else:
  376. self.render()
  377. def __int__(self):
  378. return int(self._score)
  379. def __repr__(self):
  380. return f'Scoreboard <{self._message}>'
  381. def __str__(self):
  382. return self._message
  383. @property
  384. def score(self):
  385. return self._score
  386. @property
  387. def message(self):
  388. return self._message
  389. def set_score(self, score: int):
  390. """
  391. Set the character score.
  392. Parameters:
  393. score: the new character score
  394. Examples:
  395. >>> scoreboard = Scoreboard('My character: ')
  396. >>> scoreboard.set_score(100)
  397. """
  398. if not isinstance(score, int):
  399. raise TypeError(_('Score must be an integer.'))
  400. self._score = score
  401. self._message = f'{self._prefix}{self._score}'
  402. self.render()
  403. def add_score(self, score: int):
  404. """
  405. Increment the character score.
  406. Parameters:
  407. score: the character score to be added
  408. Examples:
  409. >>> scoreboard = Scoreboard('My character: ')
  410. >>> scoreboard.add_score(5)
  411. """
  412. if not isinstance(score, int):
  413. raise TypeError(_('Score must be an integer.'))
  414. self._score += score