batframework 1.0.8a9__py3-none-any.whl → 1.0.8a11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. batFramework/__init__.py +68 -51
  2. batFramework/action.py +126 -99
  3. batFramework/actionContainer.py +53 -9
  4. batFramework/animatedSprite.py +141 -82
  5. batFramework/audioManager.py +69 -26
  6. batFramework/camera.py +259 -69
  7. batFramework/character.py +27 -0
  8. batFramework/constants.py +16 -54
  9. batFramework/cutscene.py +39 -29
  10. batFramework/cutsceneBlocks.py +36 -43
  11. batFramework/dynamicEntity.py +18 -9
  12. batFramework/easingController.py +58 -0
  13. batFramework/entity.py +48 -97
  14. batFramework/enums.py +113 -0
  15. batFramework/fontManager.py +65 -0
  16. batFramework/gui/__init__.py +10 -2
  17. batFramework/gui/button.py +9 -78
  18. batFramework/gui/clickableWidget.py +220 -0
  19. batFramework/gui/constraints/__init__.py +1 -0
  20. batFramework/gui/constraints/constraints.py +815 -0
  21. batFramework/gui/container.py +174 -32
  22. batFramework/gui/debugger.py +131 -43
  23. batFramework/gui/dialogueBox.py +99 -0
  24. batFramework/gui/draggableWidget.py +40 -0
  25. batFramework/gui/image.py +56 -20
  26. batFramework/gui/indicator.py +38 -21
  27. batFramework/gui/interactiveWidget.py +192 -13
  28. batFramework/gui/label.py +309 -74
  29. batFramework/gui/layout.py +231 -63
  30. batFramework/gui/meter.py +74 -0
  31. batFramework/gui/radioButton.py +84 -0
  32. batFramework/gui/root.py +134 -38
  33. batFramework/gui/shape.py +237 -57
  34. batFramework/gui/slider.py +240 -0
  35. batFramework/gui/style.py +10 -0
  36. batFramework/gui/styleManager.py +48 -0
  37. batFramework/gui/textInput.py +247 -0
  38. batFramework/gui/toggle.py +101 -51
  39. batFramework/gui/widget.py +358 -250
  40. batFramework/manager.py +90 -19
  41. batFramework/object.py +123 -0
  42. batFramework/particle.py +115 -0
  43. batFramework/renderGroup.py +67 -0
  44. batFramework/resourceManager.py +100 -0
  45. batFramework/scene.py +281 -123
  46. batFramework/sceneManager.py +178 -116
  47. batFramework/scrollingSprite.py +114 -0
  48. batFramework/sprite.py +51 -0
  49. batFramework/stateMachine.py +11 -8
  50. batFramework/templates/__init__.py +2 -0
  51. batFramework/templates/character.py +44 -0
  52. batFramework/templates/states.py +166 -0
  53. batFramework/tileset.py +46 -0
  54. batFramework/time.py +145 -58
  55. batFramework/transition.py +195 -124
  56. batFramework/triggerZone.py +1 -1
  57. batFramework/utils.py +112 -147
  58. batframework-1.0.8a11.dist-info/LICENCE +21 -0
  59. batframework-1.0.8a11.dist-info/METADATA +43 -0
  60. batframework-1.0.8a11.dist-info/RECORD +62 -0
  61. {batframework-1.0.8a9.dist-info → batframework-1.0.8a11.dist-info}/WHEEL +1 -1
  62. batFramework/debugger.py +0 -48
  63. batFramework/easing.py +0 -71
  64. batFramework/gui/constraints.py +0 -204
  65. batFramework/gui/frame.py +0 -19
  66. batFramework/particles.py +0 -77
  67. batFramework/transitionManager.py +0 -0
  68. batframework-1.0.8a9.dist-info/METADATA +0 -53
  69. batframework-1.0.8a9.dist-info/RECORD +0 -42
  70. {batframework-1.0.8a9.dist-info → batframework-1.0.8a11.dist-info}/top_level.txt +0 -0
batFramework/__init__.py CHANGED
@@ -1,66 +1,83 @@
1
1
  import pygame
2
+ import batFramework as bf
3
+ import sys
2
4
  from .constants import Constants as const
3
- import os
4
- import json
5
- initialized = False
6
-
7
- def init(
8
- resolution:tuple[int,int],
9
- flags:int=0,
10
- vsync:int = 0,
11
- default_text_size=None,
12
- default_font=None,
13
- resource_path:str|None=None,
14
- window_title:str="BatFramework Project",
15
- fps_limit : int = 0
16
- ):
17
- global initialized
18
- if not initialized:
19
- pygame.init()
20
- pygame.display.set_caption(window_title)
21
-
22
- # Initialize display
23
- const.init_screen(resolution,flags,vsync)
24
- const.set_fps_limit(fps_limit)
25
-
26
- # Initialize default text size
27
- if default_text_size: const.set_default_text_size(default_text_size)
28
- # Initialize resource path for game data
29
- if resource_path: const.set_resource_path(resource_path)
30
-
31
- # Initialize default font cache
32
- from .utils import Utils
33
- if default_font is None or isinstance(default_font,str):
34
- Utils.init_font(default_font)
35
- else:
36
- raise ValueError(f"default_font '{default_font}' can be either string or None")
37
-
38
- f = list(Utils.FONTS[None].values())[0]
39
- print(f"Set default font to : {f.name} {'' if default_font is not None else '(default value)'}")
40
- initialized = True
41
-
42
- from .constants import Colors as color
43
- from .constants import Axis as axis
44
5
  from .utils import Singleton
6
+ from .enums import *
7
+ from .resourceManager import ResourceManager
8
+ from .fontManager import FontManager
45
9
  from .utils import Utils as utils
46
- from .time import Time, Timer
47
- from .cutscene import Cutscene,CutsceneManager
10
+ from .tileset import Tileset
11
+ from .time import *
12
+ from .easingController import EasingController
13
+ from .cutscene import Cutscene, CutsceneManager
48
14
  from .cutsceneBlocks import *
49
- from .easing import Easing, EasingAnimation
50
15
  from .audioManager import AudioManager
51
- from .utils import Layout, Alignment, Direction
52
- from .transition import *
16
+ import batFramework.transition as transition
53
17
  from .action import Action
54
- from .actionContainer import ActionContainer
18
+ from .actionContainer import *
55
19
  from .camera import Camera
20
+ from .object import Object
56
21
  from .entity import Entity
22
+ from .renderGroup import RenderGroup
57
23
  from .dynamicEntity import DynamicEntity
58
- from .animatedSprite import AnimatedSprite, AnimState
24
+ from .sprite import Sprite
25
+ from .scrollingSprite import ScrollingSprite
26
+ from .particle import *
27
+ from .animatedSprite import AnimatedSprite, Animation
28
+ from .character import Character
59
29
  from .stateMachine import State, StateMachine
60
- from .particles import Particle, ParticleManager
61
- # from .debugger import Debugger
62
30
  from .scene import Scene
63
- from .gui import *
31
+ from .gui import *
64
32
  from .sceneManager import SceneManager
65
33
  from .manager import Manager
34
+ from .templates import *
35
+ import importlib.metadata
36
+
37
+
38
+ def init_screen(resolution: tuple[int, int], flags: int = 0, vsync: int = 0):
39
+ const.RESOLUTION = resolution
40
+ const.FLAGS = flags
41
+ const.VSYNC = vsync
42
+ const.SCREEN = pygame.display.set_mode(
43
+ const.RESOLUTION, const.FLAGS, vsync=const.VSYNC
44
+ )
45
+ print(
46
+ f"Window : {resolution[0]}x{resolution[1]} [vsync:{pygame.display.is_vsync()}]"
47
+ )
48
+
49
+
50
+
51
+ def print_version():
52
+ package_name = "batFramework"
53
+ try:
54
+ version = importlib.metadata.version(package_name)
55
+ print(f"{package_name} version: {version}")
56
+ except importlib.metadata.PackageNotFoundError:
57
+ print(f"{package_name} is not installed")
58
+
59
+
60
+ def init(
61
+ resolution: tuple[int, int],
62
+ flags: int = 0,
63
+ vsync: int = 0,
64
+ default_text_size=None,
65
+ default_font=None,
66
+ resource_path: str | None = None,
67
+ window_title: str = "BatFramework Project",
68
+ fps_limit: int = 0,
69
+ ):
70
+ print_version()
71
+ pygame.display.set_caption(window_title)
72
+ init_screen(resolution, flags, vsync)
66
73
 
74
+ ResourceManager().set_resource_path(
75
+ resource_path if resource_path is not None else "."
76
+ )
77
+ if resource_path is not None:
78
+ ResourceManager().load_dir(ResourceManager().RESOURCE_PATH)
79
+ if default_text_size is not None:
80
+ FontManager().set_default_text_size(default_text_size)
81
+ FontManager().init_font(default_font)
82
+ const.BF_INITIALIZED = True
83
+ const.set_fps_limit(fps_limit)
batFramework/action.py CHANGED
@@ -1,8 +1,8 @@
1
- from typing import Any
1
+ from typing import Any, Self
2
2
  from enum import Enum
3
3
  import pygame
4
+ from .enums import actionType
4
5
 
5
- ActionType = Enum("type", "INSTANTANEOUS CONTINUOUS HOLDING")
6
6
 
7
7
  class Action:
8
8
  def __init__(self, name: str) -> None:
@@ -12,34 +12,28 @@ class Action:
12
12
  Args:
13
13
  name (str): The name of the action.
14
14
  """
15
- self._name = name
16
- self._active = False
17
- self._type = ActionType.INSTANTANEOUS
18
- self._key_control = set()
19
- self._mouse_control = set()
20
- self._gamepad_button_control = set()
21
- self._gamepad_axis_control = set()
15
+ self.name: str = name
16
+ self.active: bool = False
17
+ self.data: dict = {}
18
+ self.consume_event: bool = False
19
+ self._type: actionType = actionType.INSTANTANEOUS
20
+ self._key_control: set = set()
21
+ self._mouse_control: set = set()
22
+ self._event_control: set = set()
23
+ self._gamepad_button_control: set = set()
24
+ self._gamepad_axis_control: set = set()
22
25
  self._holding = set()
23
- self._unique = True
24
- self.data : Any = None
25
26
 
26
- def set_unique(self, val: bool) -> None:
27
+ def set_consume_event(self, val: bool) -> Self:
27
28
  """
28
29
  Set whether this action is unique (exclusive).
30
+ When in an action Container, unique actions -when active - break the propagation of their event to other actions.
29
31
 
30
32
  Args:
31
33
  val (bool): True if the action is unique, False otherwise.
32
34
  """
33
- self._unique = val
34
-
35
- def is_active(self) -> bool:
36
- """
37
- Check if the action is currently active.
38
-
39
- Returns:
40
- bool: True if the action is active, False otherwise.
41
- """
42
- return self._active
35
+ self.consume_event = val
36
+ return self
43
37
 
44
38
  def set_active(self, value: bool) -> None:
45
39
  """
@@ -48,10 +42,18 @@ class Action:
48
42
  Args:
49
43
  value (bool): True to activate the action, False to deactivate it.
50
44
  """
51
- self._active = value
52
- self._holding = set()
45
+ self.active = value
46
+ # self._holding = set()
53
47
 
54
- def add_key_control(self, *keys) -> 'Action':
48
+ def add_event_control(self, *events) -> Self:
49
+ self._event_control.update(events)
50
+ return self
51
+
52
+ def remove_event_control(self, *events) -> Self:
53
+ self._event_control = self._event_control - events
54
+ return self
55
+
56
+ def add_key_control(self, *keys) -> Self:
55
57
  """
56
58
  Add key controls to the action.
57
59
 
@@ -64,29 +66,51 @@ class Action:
64
66
  self._key_control.update(keys)
65
67
  return self
66
68
 
67
- def add_mouse_control(self, *mouse_buttons) -> 'Action':
69
+ def remove_key_control(self, *keys: int) -> Self:
68
70
  """
69
- Add mouse control to the action.
71
+ Remove key controls to the action.
70
72
 
71
73
  Args:
72
- *mouse_buttons (int): Mouse button codes to control this action.
74
+ *keys (int): Key codes to control this action.
73
75
 
74
76
  Returns:
75
77
  Action: The updated Action object for method chaining.
76
78
  """
77
- self._mouse_control.update(mouse_buttons)
79
+ self._key_control = self._key_control - set(keys)
78
80
  return self
79
81
 
80
- def get_name(self) -> str:
82
+ def replace_key_control(self, key, new_key) -> Self:
83
+ if not key in self._key_control:
84
+ return self
85
+ self.remove_key_control(key)
86
+ self.add_key_control(new_key)
87
+ return self
88
+
89
+ def add_mouse_control(self, *mouse: int) -> Self:
81
90
  """
82
- Get the name of the action.
91
+ Add mouse control to the action.
92
+
93
+ Args:
94
+ *mouse_buttons (int): Mouse button codes to control this action.
83
95
 
84
96
  Returns:
85
- str: The name of the action.
97
+ Action: The updated Action object for method chaining.
86
98
  """
87
- return self._name
99
+ self._mouse_control.update(mouse)
100
+ return self
101
+
102
+ def remove_mouse_control(self, *mouse: int) -> Self:
103
+ self._mouse_control = self._mouse_control - set(mouse)
104
+ return self
88
105
 
89
- def set_continuous(self) -> 'Action':
106
+ def replace_mouse_control(self, mouse, new_mouse) -> Self:
107
+ if not mouse in self._mouse_control:
108
+ return self
109
+ self.remove_mouse_control(mouse)
110
+ self.add_mouse_control(new_mouse)
111
+ return self
112
+
113
+ def set_continuous(self) -> Self:
90
114
  """
91
115
  Set the action type to continuous.
92
116
 
@@ -94,7 +118,7 @@ class Action:
94
118
  Action: The updated Action object for method chaining.
95
119
  """
96
120
  self._holding = set()
97
- self._type = ActionType.CONTINUOUS
121
+ self._type = actionType.CONTINUOUS
98
122
  return self
99
123
 
100
124
  def is_continuous(self) -> bool:
@@ -104,16 +128,16 @@ class Action:
104
128
  Returns:
105
129
  bool: True if the action type is continuous, False otherwise.
106
130
  """
107
- return self._type == ActionType.CONTINUOUS
131
+ return self._type == actionType.CONTINUOUS
108
132
 
109
- def set_instantaneous(self) -> 'Action':
133
+ def set_instantaneous(self) -> Self:
110
134
  """
111
135
  Set the action type to instantaneous.
112
136
 
113
137
  Returns:
114
138
  Action: The updated Action object for method chaining.
115
139
  """
116
- self._type = ActionType.INSTANTANEOUS
140
+ self._type = actionType.INSTANTANEOUS
117
141
  self._holding = set()
118
142
  return self
119
143
 
@@ -124,16 +148,16 @@ class Action:
124
148
  Returns:
125
149
  bool: True if the action type is instantaneous, False otherwise.
126
150
  """
127
- return self._type == ActionType.INSTANTANEOUS
151
+ return self._type == actionType.INSTANTANEOUS
128
152
 
129
- def set_holding(self) -> 'Action':
153
+ def set_holding(self) -> Self:
130
154
  """
131
155
  Set the action type to holding.
132
156
 
133
157
  Returns:
134
158
  Action: The updated Action object for method chaining.
135
159
  """
136
- self._type = ActionType.HOLDING
160
+ self._type = actionType.HOLDING
137
161
  return self
138
162
 
139
163
  def is_holding_type(self) -> bool:
@@ -143,13 +167,17 @@ class Action:
143
167
  Returns:
144
168
  bool: True if the action type is holding, False otherwise.
145
169
  """
146
- return self._type == ActionType.HOLDING
170
+ return self._type == actionType.HOLDING
147
171
 
148
- def process_update(self,event:pygame.Event)->None:
149
- if self.is_active() and event.type == pygame.MOUSEMOTION and self.is_holding_type() and pygame.MOUSEMOTION in self._mouse_control:
150
- self.data = {"pos":event.pos,"rel":event.rel}
172
+ def process_update(self, event: pygame.Event) -> None:
173
+ if (
174
+ event.type == pygame.MOUSEMOTION
175
+ and self._type == actionType.HOLDING
176
+ and pygame.MOUSEMOTION in self._mouse_control
177
+ ) or self._event_control:
178
+ self.data = event.dict
151
179
 
152
- def process_activate(self, event: pygame.event.Event) -> bool:
180
+ def process_activate(self, event: pygame.event.Event):
153
181
  """
154
182
  Process activation of the action based on a pygame event.
155
183
 
@@ -159,94 +187,93 @@ class Action:
159
187
  Returns:
160
188
  bool: True if the action was activated by the event, False otherwise.
161
189
  """
190
+
162
191
  if event.type == pygame.KEYDOWN and event.key in self._key_control:
163
- self._active = True
164
- if self.is_holding_type():
165
- self._holding.add(event.key)
166
- return True
192
+ self._activate_action(event.key)
167
193
 
168
- if event.type == pygame.MOUSEBUTTONDOWN and event.button in self._mouse_control:
169
- self._active = True
170
- if self.is_holding_type():
171
- self._holding.add(event.button)
172
- return True
194
+ elif (
195
+ event.type == pygame.MOUSEBUTTONDOWN and event.button in self._mouse_control
196
+ ):
197
+ self._activate_action(event.button)
173
198
 
174
- if event.type == pygame.MOUSEMOTION and event.type in self._mouse_control:
175
- self._active = True
176
- if self.is_holding_type():
177
- self._holding.add(event.type)
178
- return True
199
+ elif event.type == pygame.MOUSEMOTION and event.type in self._mouse_control:
200
+ self._activate_action(event.type)
179
201
 
180
- return False
202
+ elif event.type in self._event_control:
203
+ self._activate_action(event.type)
204
+ self.data = event.dict
205
+ else:
206
+ return
207
+ if self.consume_event:
208
+ event.consumed = True
181
209
 
182
- def process_deactivate(self, event: pygame.event.Event) -> bool:
210
+ def _activate_action(self, control):
211
+ self.active = True
212
+ if self._type == actionType.HOLDING:
213
+ self._holding.add(control)
214
+
215
+ def process_deactivate(self, event: pygame.event.Event):
183
216
  """
184
217
  Process deactivation of the action based on a pygame event.
185
218
 
186
219
  Args:
187
220
  event (pygame.event.Event): The pygame event to process.
188
221
 
189
- Returns:
190
- bool: True if the action was deactivated by the event, False otherwise.
191
222
  """
192
- if self._type == ActionType.HOLDING:
223
+ if self._type == actionType.HOLDING:
193
224
  if event.type == pygame.KEYUP and event.key in self._key_control:
194
- if event.key in self._holding:
195
- self._holding.remove(event.key)
196
- if not self._holding:
197
- self._active = False
198
- return True
225
+ self._deactivate_action(event.key)
199
226
  elif (
200
227
  event.type == pygame.MOUSEBUTTONUP
201
228
  and event.button in self._mouse_control
202
229
  ):
203
- if event.button in self._holding:
204
- self._holding.remove(event.button)
205
- if not self._holding:
206
- self._active = False
207
- return True
208
- elif (
209
- event.type == pygame.MOUSEMOTION
210
- and event.type in self._mouse_control
211
- ):
212
- self.value = None
213
- if event.type in self._holding:
214
- self._holding.remove(event.type)
215
- if not self._holding:
216
- self._active = False
217
- return True
230
+ self._deactivate_action(event.button)
231
+ elif event.type == pygame.MOUSEMOTION and event.type in self._mouse_control:
232
+ self._deactivate_action(event.type)
233
+ elif event.type in self._event_control:
234
+ self._deactivate_action(event.type)
235
+ else:
236
+ event.consumed = False
237
+
238
+ if self.consume_event:
239
+ event.consumed = True
218
240
 
219
- return False
241
+ def _deactivate_action(self, control) -> bool:
242
+ if control in self._holding:
243
+ self._holding.remove(control)
244
+ if not self._holding:
245
+ self.active = False
220
246
 
221
- def process_event(self, event: pygame.event.Event) -> bool:
247
+ def process_event(self, event: pygame.event.Event):
222
248
  """
223
249
  Process a pygame event and update the action's state.
224
250
 
225
251
  Args:
226
252
  event (pygame.event.Event): The pygame event to process.
227
-
228
- Returns:
229
- bool: True if the action state changed, False otherwise.
230
253
  """
231
- if not self._active:
232
- res = self.process_activate(event)
254
+
255
+ if event.consumed:
256
+ return
257
+ if not self.active:
258
+ self.process_activate(event)
233
259
  else:
234
- res = self.process_deactivate(event)
235
- self.process_update(event)
236
- return res
260
+ self.process_deactivate(event)
261
+ if self.active:
262
+ self.process_update(event)
263
+ return
237
264
 
238
265
  def reset(self) -> None:
239
266
  """
240
267
  Reset the action's state to the default state.
241
268
  """
242
- if self._type in {ActionType.CONTINUOUS, ActionType.HOLDING}:
269
+ if self._type in {actionType.CONTINUOUS, actionType.HOLDING}:
243
270
  return
244
- elif self._type == ActionType.INSTANTANEOUS:
245
- self._active = False
271
+ elif self._type == actionType.INSTANTANEOUS:
272
+ self.active = False
246
273
 
247
274
  def hard_reset(self) -> None:
248
275
  """
249
276
  Hard reset the action, deactivating it and clearing any holding controls.
250
277
  """
251
- self._active = False
278
+ self.active = False
252
279
  self._holding = set()
@@ -1,32 +1,44 @@
1
1
  import batFramework as bf
2
+ import pygame
2
3
 
3
4
 
4
5
  class ActionContainer:
5
6
  def __init__(self, *actions: list[bf.Action]) -> None:
6
7
  self._actions: dict[str, bf.Action] = {}
7
8
  if actions:
8
- self.add_action(*actions)
9
+ self.add_actions(*actions)
10
+
11
+ def __iter__(self):
12
+ return iter(self._actions.values())
9
13
 
10
14
  def clear(self):
11
15
  self._actions = {}
12
16
 
13
- def add_action(self, *actions: bf.Action):
17
+ def add_actions(self, *actions: bf.Action):
14
18
  for action in actions:
15
- self._actions[action.get_name()] = action
19
+ self._actions[action.name] = action
16
20
 
17
- def get(self,name:str)->bf.Action:
21
+ def get(self, name: str) -> bf.Action:
18
22
  return self._actions.get(name)
19
23
 
20
- def has_action(self, name:str):
24
+ def has_action(self, name: str):
21
25
  return name in self._actions
22
26
 
23
- def is_active(self, *names:str)->bool:
24
- return all(self._actions.get(name).is_active() if name in self._actions else False for name in names)
27
+ def get_all(self) -> list[bf.Action]:
28
+ return self._actions
29
+
30
+ def is_active(self, *names: str) -> bool:
31
+ return all(
32
+ self._actions.get(name).active if name in self._actions else False
33
+ for name in names
34
+ )
25
35
 
26
36
  def process_event(self, event):
37
+ if event.consumed:
38
+ return
27
39
  for action in self._actions.values():
28
- a = action.process_event(event)
29
- if a and action._unique:
40
+ action.process_event(event)
41
+ if event.consumed == True:
30
42
  break
31
43
 
32
44
  def reset(self):
@@ -36,3 +48,35 @@ class ActionContainer:
36
48
  def hard_reset(self):
37
49
  for action in self._actions.values():
38
50
  action.hard_reset()
51
+
52
+
53
+ class DirectionalKeyControls(ActionContainer):
54
+ def __init__(self):
55
+ super().__init__(
56
+ bf.Action("up").add_key_control(pygame.K_UP).set_holding(),
57
+ bf.Action("down").add_key_control(pygame.K_DOWN).set_holding(),
58
+ bf.Action("left").add_key_control(pygame.K_LEFT).set_holding(),
59
+ bf.Action("right").add_key_control(pygame.K_RIGHT).set_holding(),
60
+ )
61
+
62
+
63
+ class WASDControls(ActionContainer):
64
+ def __init__(self):
65
+ super().__init__(
66
+ bf.Action("up").add_key_control(pygame.K_w).set_holding(),
67
+ bf.Action("down").add_key_control(pygame.K_s).set_holding(),
68
+ bf.Action("left").add_key_control(pygame.K_a).set_holding(),
69
+ bf.Action("right").add_key_control(pygame.K_d).set_holding(),
70
+ )
71
+
72
+
73
+ class HybridControls(ActionContainer):
74
+ def __init__(self):
75
+ super().__init__(
76
+ bf.Action("up").add_key_control(pygame.K_UP, pygame.K_w).set_holding(),
77
+ bf.Action("down").add_key_control(pygame.K_DOWN, pygame.K_s).set_holding(),
78
+ bf.Action("left").add_key_control(pygame.K_LEFT, pygame.K_a).set_holding(),
79
+ bf.Action("right")
80
+ .add_key_control(pygame.K_RIGHT, pygame.K_r)
81
+ .set_holding(),
82
+ )