batframework 1.0.9a7__py3-none-any.whl → 1.0.9a8__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 (61) hide show
  1. batFramework/__init__.py +20 -11
  2. batFramework/action.py +1 -1
  3. batFramework/animatedSprite.py +47 -116
  4. batFramework/animation.py +30 -5
  5. batFramework/audioManager.py +8 -5
  6. batFramework/baseScene.py +240 -0
  7. batFramework/camera.py +4 -0
  8. batFramework/constants.py +6 -1
  9. batFramework/cutscene.py +221 -21
  10. batFramework/cutsceneManager.py +5 -2
  11. batFramework/drawable.py +7 -5
  12. batFramework/easingController.py +10 -11
  13. batFramework/entity.py +21 -2
  14. batFramework/enums.py +48 -33
  15. batFramework/gui/__init__.py +3 -1
  16. batFramework/gui/animatedLabel.py +10 -2
  17. batFramework/gui/button.py +4 -31
  18. batFramework/gui/clickableWidget.py +42 -30
  19. batFramework/gui/constraints/constraints.py +212 -136
  20. batFramework/gui/container.py +72 -48
  21. batFramework/gui/debugger.py +12 -17
  22. batFramework/gui/draggableWidget.py +8 -11
  23. batFramework/gui/image.py +3 -10
  24. batFramework/gui/indicator.py +73 -1
  25. batFramework/gui/interactiveWidget.py +117 -100
  26. batFramework/gui/label.py +73 -63
  27. batFramework/gui/layout.py +221 -452
  28. batFramework/gui/meter.py +21 -7
  29. batFramework/gui/radioButton.py +0 -1
  30. batFramework/gui/root.py +99 -29
  31. batFramework/gui/selector.py +257 -0
  32. batFramework/gui/shape.py +13 -5
  33. batFramework/gui/slider.py +260 -93
  34. batFramework/gui/textInput.py +45 -21
  35. batFramework/gui/toggle.py +70 -52
  36. batFramework/gui/tooltip.py +30 -0
  37. batFramework/gui/widget.py +203 -125
  38. batFramework/manager.py +7 -8
  39. batFramework/particle.py +4 -1
  40. batFramework/propertyEaser.py +79 -0
  41. batFramework/renderGroup.py +17 -50
  42. batFramework/resourceManager.py +43 -13
  43. batFramework/scene.py +15 -335
  44. batFramework/sceneLayer.py +138 -0
  45. batFramework/sceneManager.py +31 -36
  46. batFramework/scrollingSprite.py +8 -3
  47. batFramework/sprite.py +1 -1
  48. batFramework/templates/__init__.py +1 -2
  49. batFramework/templates/controller.py +97 -0
  50. batFramework/timeManager.py +76 -22
  51. batFramework/transition.py +37 -103
  52. batFramework/utils.py +121 -3
  53. {batframework-1.0.9a7.dist-info → batframework-1.0.9a8.dist-info}/METADATA +24 -3
  54. batframework-1.0.9a8.dist-info/RECORD +66 -0
  55. {batframework-1.0.9a7.dist-info → batframework-1.0.9a8.dist-info}/WHEEL +1 -1
  56. batFramework/character.py +0 -27
  57. batFramework/templates/character.py +0 -43
  58. batFramework/templates/states.py +0 -166
  59. batframework-1.0.9a7.dist-info/RECORD +0 -63
  60. /batframework-1.0.9a7.dist-info/LICENCE → /batframework-1.0.9a8.dist-info/LICENSE +0 -0
  61. {batframework-1.0.9a7.dist-info → batframework-1.0.9a8.dist-info}/top_level.txt +0 -0
batFramework/gui/meter.py CHANGED
@@ -12,6 +12,7 @@ def custom_top_at(self, x, y):
12
12
  class Meter(Shape):
13
13
  def __init__(self, min_value: float = 0, max_value: float = 1, step: float = 0.1):
14
14
  super().__init__()
15
+ self.axis: bf.axis = bf.axis.HORIZONTAL
15
16
  self.min_value, self.max_value = min_value, max_value
16
17
  self.step = step
17
18
  self.snap: bool = False
@@ -29,6 +30,11 @@ class Meter(Shape):
29
30
  def __str__(self) -> str:
30
31
  return "Meter"
31
32
 
33
+ def set_axis(self,axis:bf.axis)->Self:
34
+ self.axis = axis
35
+ self.dirty_shape = True
36
+ return self
37
+
32
38
  def set_step(self, step: float) -> Self:
33
39
  self.step = step
34
40
  self.set_value(self.value)
@@ -36,9 +42,6 @@ class Meter(Shape):
36
42
 
37
43
  def set_range(self, range_min: float, range_max: float) -> Self:
38
44
  if range_min >= range_max:
39
- print(
40
- f"[Warning] : minimum value {range_min} is greater than or equal to maximum value {range_max}"
41
- )
42
45
  return self
43
46
  self.min_value = range_min
44
47
  self.max_value = range_max
@@ -65,10 +68,21 @@ class Meter(Shape):
65
68
  return (self.value-self.min_value) / (self.max_value - self.min_value)
66
69
 
67
70
  def _build_content(self) -> None:
68
- width = self.get_padded_width() * self.get_ratio()
69
- self.content.set_size((width, self.get_padded_height()))
70
- self.content.rect.topleft = self.get_padded_rect().topleft
71
+ padded = self.get_inner_rect()
72
+ ratio = self.get_ratio()
73
+
74
+ if self.axis == bf.axis.HORIZONTAL:
75
+ width = padded.width * ratio
76
+ self.content.set_size((width, padded.height))
77
+ self.content.rect.topleft = padded.topleft
78
+
79
+ else: # VERTICAL
80
+ height = padded.height * ratio
81
+ self.content.set_size((padded.width, height))
82
+ # Content grows from bottom up
83
+ self.content.rect.bottomleft = padded.bottomleft
84
+
71
85
 
72
86
  def build(self) -> None:
73
- super().build()
74
87
  self._build_content()
88
+ super().build()
@@ -3,7 +3,6 @@ from typing import Self, Any, Callable
3
3
  from .toggle import Toggle
4
4
 
5
5
 
6
- class RadioVariable: ...
7
6
 
8
7
 
9
8
  class RadioButton(Toggle):
batFramework/gui/root.py CHANGED
@@ -15,15 +15,28 @@ class Root(InteractiveWidget):
15
15
  self.rect.size = pygame.display.get_surface().get_size()
16
16
  self.focused: InteractiveWidget | None = self
17
17
  self.hovered: Widget | None = self
18
- self.set_debug_color("yellow")
19
- self.set_render_order(sys.maxsize)
20
18
  self.clip_children = False
19
+ self.set_debug_color("yellow")
20
+
21
+ self.show_tooltip : bool = True
22
+ self.tooltip = bf.gui.ToolTip("").set_visible(False)
23
+ self.add(self.tooltip)
21
24
 
25
+ def toggle_tooltip(self,value:bool)->Self:
26
+ self.show_tooltip = value
27
+ return self
28
+
22
29
  def __str__(self) -> str:
23
30
  return "Root"
31
+
32
+ def to_ascii_tree(self) -> str:
33
+ def f(w, depth):
34
+ prefix = " " * (depth * 4) + ("└── " if depth > 0 else "")
35
+ children = "\n".join(f(c, depth + 1) for c in w.children) if w.children else ""
36
+ return f"{prefix}{str(w)}\n{children}"
37
+ return f(self, 0)
24
38
 
25
39
  def set_parent_scene(self, parent_scene: bf.Scene) -> Self:
26
- bf.StyleManager().register_widget(self)
27
40
  return super().set_parent_scene(parent_scene)
28
41
 
29
42
  def get_focused(self) -> Widget | None:
@@ -91,28 +104,36 @@ class Root(InteractiveWidget):
91
104
  if not force:
92
105
  return self
93
106
  self.rect.size = size
107
+ self.dirty_shape = True
108
+ self.dirty_size_constraints = True
94
109
  return self
95
110
 
96
- def do_handle_event(self, event):
111
+ def process_event(self,event):
112
+ if event.consumed : return
113
+ self.do_handle_event_early(event)
114
+ if event.consumed : return
115
+ super().process_event(event)
116
+
117
+ def do_handle_event_early(self, event):
118
+ if event.type == pygame.VIDEORESIZE:
119
+ self.set_size((event.w,event.h),force=True)
97
120
  if self.focused:
98
121
  if event.type == pygame.KEYDOWN:
99
- if self.focused.on_key_down(event.key):
100
- event.consumed = True
122
+ event.consumed = self.focused.on_key_down(event.key)
123
+ if not event.consumed :
124
+ event.consumed = self._handle_alt_tab(event.key)
101
125
  elif event.type == pygame.KEYUP:
102
- if self.focused.on_key_up(event.key):
103
- event.consumed = True
126
+ event.consumed = self.focused.on_key_up(event.key)
104
127
 
105
128
  if not self.hovered or (not isinstance(self.hovered, InteractiveWidget)):
106
129
  return
107
130
 
108
131
  if event.type == pygame.MOUSEBUTTONDOWN:
109
- if self.hovered.on_click_down(event.button):
110
- event.consumed = True
132
+ event.consumed = self.hovered.on_click_down(event.button)
111
133
 
112
134
  elif event.type == pygame.MOUSEBUTTONUP:
113
- if self.hovered.on_click_up(event.button):
114
- event.consumed = True
115
-
135
+ event.consumed = self.hovered.on_click_up(event.button)
136
+
116
137
  def do_on_click_down(self, button: int) -> None:
117
138
  if button == 1:
118
139
  self.clear_focused()
@@ -127,25 +148,68 @@ class Root(InteractiveWidget):
127
148
 
128
149
  def update(self, dt: float) -> None:
129
150
  super().update(dt)
130
- old = self.hovered
131
- transposed = self.drawing_camera.screen_to_world(pygame.mouse.get_pos())
132
- self.hovered = self.top_at(*transposed) if self.top_at(*transposed) else None
133
- if old == self.hovered and isinstance(self.hovered, InteractiveWidget):
134
- self.hovered.on_mouse_motion(*transposed)
151
+ self.update_tree()
152
+
153
+ mouse_screen = pygame.mouse.get_pos()
154
+ mouse_world = self.drawing_camera.screen_to_world(mouse_screen)
155
+ prev_hovered = self.hovered
156
+ self.hovered = self.top_at(*mouse_world) or None
157
+
158
+ # Tooltip logic
159
+ if self.hovered and self.hovered.tooltip_text:
160
+ self.tooltip.set_text(self.hovered.tooltip_text)
161
+
162
+ tooltip_size = self.tooltip.get_min_required_size()
163
+ screen_w, screen_h = self.drawing_camera.rect.size
164
+ tooltip_x, tooltip_y = self.drawing_camera.world_to_screen_point(mouse_world)
165
+
166
+ tooltip_x = min(tooltip_x, screen_w - tooltip_size[0])
167
+ tooltip_y = min(tooltip_y, screen_h - tooltip_size[1])
168
+ tooltip_x = max(0, tooltip_x)
169
+ tooltip_y = max(0, tooltip_y)
170
+
171
+ self.tooltip.set_position(tooltip_x, tooltip_y)
172
+ self.tooltip.fade_in()
173
+ else:
174
+ self.tooltip.fade_out()
175
+
176
+ if self.hovered == prev_hovered:
177
+ if isinstance(self.hovered, InteractiveWidget):
178
+ self.hovered.on_mouse_motion(*mouse_world)
135
179
  return
136
- if old and isinstance(old, InteractiveWidget):
137
- old.on_exit()
138
- if self.hovered and isinstance(self.hovered, InteractiveWidget):
180
+
181
+ if isinstance(prev_hovered, InteractiveWidget):
182
+ prev_hovered.on_exit()
183
+ if isinstance(self.hovered, InteractiveWidget):
139
184
  self.hovered.on_enter()
140
185
 
141
- def apply_updates(self):
142
- if any(child.dirty_shape for child in self.children):
143
- self.dirty_shape = True # Mark layout as dirty if any child changed size
144
186
 
145
- if self.dirty_shape:
146
- for child in self.children:
147
- child.apply_updates()
148
- self.dirty_shape = False
187
+ def _handle_alt_tab(self,key):
188
+ if self.focused is None:
189
+ return False
190
+ if key != pygame.K_TAB:
191
+ return False
192
+ keys = pygame.key.get_pressed()
193
+ if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
194
+ self.focused.focus_prev_tab(self.focused)
195
+ else:
196
+ self.focused.focus_next_tab(self.focused)
197
+ return True
198
+
199
+ def update_tree(self):
200
+ # print("START updating tree")
201
+ self.apply_updates("pre")
202
+ self.apply_updates("post")
203
+ self.apply_updates("pre")
204
+ self.apply_updates("post")
205
+
206
+ # print("END updating tree")
207
+
208
+ def apply_pre_updates(self):
209
+ return
210
+
211
+ def apply_post_updates(self, skip_draw = False):
212
+ return
149
213
 
150
214
  def draw(self, camera: bf.Camera) -> None:
151
215
  super().draw(camera)
@@ -155,4 +219,10 @@ class Root(InteractiveWidget):
155
219
  and self.focused
156
220
  and self.focused != self
157
221
  ):
158
- self.focused.draw_focused(camera)
222
+ clip:bool =self.focused.parent and self.focused.parent.clip_children
223
+ if clip:
224
+ old_clip = camera.surface.get_clip()
225
+ camera.surface.set_clip(camera.world_to_screen(self.focused.parent.rect))
226
+ if clip:
227
+ camera.surface.set_clip(old_clip)
228
+ self.focused.draw_focused(camera)
@@ -0,0 +1,257 @@
1
+ import batFramework as bf
2
+ import pygame
3
+ from typing import Self,Callable,Any
4
+ from .button import Button
5
+ from .indicator import ArrowIndicator
6
+ from .clickableWidget import ClickableWidget
7
+ from .widget import Widget
8
+
9
+ class MyArrow(ArrowIndicator,ClickableWidget):
10
+ def top_at(self,x,y):
11
+ return Widget.top_at(self,x,y)
12
+
13
+ def get_focus(self):
14
+ return self.parent.get_focus()
15
+
16
+ class Selector(Button):
17
+ def __init__(self,options:list[str]=None,default_value:str=None,allow_cycle:bool=False):
18
+ super().__init__('')
19
+ self.allow_cycle = allow_cycle
20
+ self.current_index = 0
21
+ self.on_modify_callback : Callable[[str,int],Any] = None
22
+ self.options = options if options else []
23
+ self.gap : int = 2
24
+ self.indicator_height = 0
25
+ text_value = ""
26
+
27
+
28
+ if not (default_value is not None and default_value in self.options):
29
+ default_value = options[0]
30
+
31
+ text_value = default_value
32
+ self.current_index = self.options.index(default_value)
33
+
34
+ self.left_indicator : MyArrow = (MyArrow(bf.direction.LEFT)
35
+ .set_draw_stem(False)
36
+ .set_color((0,0,0,0)).set_arrow_color(self.text_color)
37
+ .set_callback(lambda : self.set_by_index(self.get_current_index()-1))
38
+ )
39
+
40
+ self.right_indicator:MyArrow =(MyArrow(bf.direction.RIGHT)
41
+ .set_draw_stem(False)
42
+ .set_color((0,0,0,0)).set_arrow_color(self.text_color)
43
+ .set_callback(lambda : self.set_by_index(self.get_current_index()+1))
44
+ )
45
+
46
+ self.add(self.left_indicator,self.right_indicator)
47
+ self.set_clip_children(False)
48
+ self.set_text(text_value)
49
+
50
+ def __str__(self):
51
+ return f"Selector[{self.options[self.current_index]}]"
52
+
53
+ def set_gap(self,value:int)->Self:
54
+ self.gap = value
55
+ self.dirty_shape = True
56
+ return self
57
+
58
+ def disable(self):
59
+ super().disable()
60
+ self.left_indicator.disable()
61
+ self.right_indicator.disable()
62
+ return self
63
+
64
+ def enable(self):
65
+ super().enable()
66
+ self.right_indicator.enable()
67
+ self.left_indicator.enable()
68
+ index = self.current_index
69
+ if not self.allow_cycle:
70
+ if index == 0:
71
+ self.left_indicator.disable()
72
+ else:
73
+ self.left_indicator.enable()
74
+ if index == len(self.options)-1:
75
+ self.right_indicator.disable()
76
+ else:
77
+ self.right_indicator.enable()
78
+
79
+
80
+ return self
81
+
82
+ def set_tooltip_text(self, text):
83
+ self.left_indicator.set_tooltip_text(text)
84
+ self.right_indicator.set_tooltip_text(text)
85
+ return super().set_tooltip_text(text)
86
+
87
+ def get_min_required_size(self) -> tuple[float, float]:
88
+ """
89
+ Calculates the minimum size required for the selector, including text and indicators.
90
+ """
91
+
92
+ # Calculate the maximum size needed for the text
93
+ old_text = self.text
94
+ max_text_size = (0, 0)
95
+ for option in self.options:
96
+ self.text = option
97
+ text_size = self._get_text_rect_required_size()
98
+ max_text_size = max(max_text_size[0], text_size[0]), max(max_text_size[1], text_size[1])
99
+ self.text = old_text
100
+
101
+ # Ensure total_height is always an odd integer
102
+ total_height = max(8, max_text_size[1] * 1.5)
103
+ total_height += self.unpressed_relief
104
+ total_height += max(self.right_indicator.outline_width,self.left_indicator.outline_width)
105
+ self.indicator_height = total_height
106
+
107
+ # Calculate total width and height
108
+ total_width = (
109
+ self.indicator_height*2+
110
+ max_text_size[0] + # Text width
111
+ self.gap * 2 # Gaps between text and indicators
112
+ )
113
+ # Inflate by padding at the very end
114
+ final_size = self.expand_rect_with_padding((0, 0, total_width, total_height)).size
115
+
116
+ return final_size
117
+
118
+
119
+
120
+ def _align_content(self):
121
+ """
122
+ Builds the selector layout (places and resizes the indicators)
123
+ """
124
+
125
+ # return
126
+ # Step 1: Calculate the padded area for positioning
127
+ padded = self.get_inner_rect()
128
+
129
+
130
+ # left_size = self.left_indicator.rect.size
131
+ right_size = self.right_indicator.rect.size
132
+
133
+ indicator_height = self.indicator_height
134
+
135
+ self.left_indicator.set_size((indicator_height,indicator_height))
136
+ self.right_indicator.set_size((indicator_height,indicator_height))
137
+
138
+
139
+ # Step 3: Position indicators
140
+ self.left_indicator.set_position(padded.left, None)
141
+ self.left_indicator.set_center(None, padded.centery)
142
+
143
+ self.right_indicator.set_position(padded.right - right_size[0], None)
144
+ self.right_indicator.set_center(None, padded.centery)
145
+
146
+
147
+
148
+ def build(self):
149
+
150
+ super().build()
151
+ self._align_content()
152
+
153
+ def get_current_index(self)->int:
154
+ return self.current_index
155
+
156
+ def set_allow_cycle(self,value:bool)->Self:
157
+ if value == self.allow_cycle: return self
158
+ self.allow_cycle = value
159
+ self.dirty_surface = True
160
+ return self
161
+
162
+ def set_text_color(self,color)->Self:
163
+ super().set_text_color(color)
164
+ self.left_indicator.set_arrow_color(color)
165
+ self.right_indicator.set_arrow_color(color)
166
+ return self
167
+
168
+ def set_modify_callback(self,function:Callable[[str,int],Any]):
169
+ """
170
+ the function will receive the value and the index
171
+ """
172
+ self.on_modify_callback = function
173
+ return self
174
+
175
+
176
+
177
+ def set_by_index(self,index:int)->Self:
178
+ if self.allow_cycle:
179
+ index = index%len(self.options)
180
+ else:
181
+ index = max(min(len(self.options)-1,index),0)
182
+
183
+ if index == self.current_index:
184
+ return self
185
+
186
+
187
+ self.current_index = index
188
+ self.set_text(self.options[self.current_index])
189
+ if self.on_modify_callback:
190
+ self.on_modify_callback(self.options[self.current_index],self.current_index)
191
+ if not self.allow_cycle:
192
+ if index == 0:
193
+ self.left_indicator.disable()
194
+ else:
195
+ self.left_indicator.enable()
196
+ if index == len(self.options)-1:
197
+ self.right_indicator.disable()
198
+ else:
199
+ self.right_indicator.enable()
200
+ return self
201
+
202
+ def paint(self):
203
+ super().paint()
204
+ self.left_indicator.show()
205
+ self.right_indicator.show()
206
+ if not self.allow_cycle and self.current_index == 0:
207
+ self.left_indicator.hide()
208
+ elif not self.allow_cycle and self.current_index== len(self.options)-1:
209
+ self.right_indicator.hide()
210
+
211
+ def set_by_value(self,value:str)->Self:
212
+ if not self.enabled : return
213
+ if value not in self.options : return self
214
+ index = self.options.index(value)
215
+ self.set_by_index(index)
216
+ return self
217
+
218
+ def on_key_down(self, key: int) -> bool:
219
+ if not self.enabled:
220
+ return False
221
+
222
+ key_actions = {
223
+ pygame.K_RIGHT: self.right_indicator,
224
+ pygame.K_SPACE: self.right_indicator,
225
+ pygame.K_LEFT: self.left_indicator
226
+ }
227
+
228
+ indicator = key_actions.get(key)
229
+ if indicator and indicator.visible and indicator.enabled:
230
+ indicator.on_click_down(1)
231
+ return True
232
+
233
+ return False
234
+
235
+ def on_key_up(self, key: int) -> bool:
236
+ if not self.enabled:
237
+ return False
238
+
239
+ key_actions = {
240
+ pygame.K_RIGHT: self.right_indicator,
241
+ pygame.K_SPACE: self.right_indicator,
242
+ pygame.K_LEFT: self.left_indicator
243
+ }
244
+
245
+ indicator = key_actions.get(key)
246
+ if indicator and indicator.visible and indicator.enabled:
247
+ indicator.on_click_up(1)
248
+ return True
249
+
250
+ return False
251
+
252
+ def do_on_click_down(self, button) -> None:
253
+ if self.enabled and button == 1:
254
+ if not self.get_focus():
255
+ return True
256
+ return False
257
+
batFramework/gui/shape.py CHANGED
@@ -18,16 +18,24 @@ class Shape(Widget):
18
18
  self.shadow_color: tuple[int, int, int] | str = (0, 0, 0, 255)
19
19
  self.draw_mode = bf.drawMode.SOLID
20
20
 
21
- def get_padded_bottom(self) -> float:
21
+ def get_inner_bottom(self) -> float:
22
22
  return self.rect.bottom - self.padding[3] - self.relief
23
23
 
24
- def get_padded_height(self) -> float:
24
+ def get_inner_height(self) -> float:
25
25
  return self.rect.h - self.padding[1] - self.padding[3] - self.relief
26
26
 
27
- def get_padded_top(self) -> float:
28
- return self.rect.y + self.relief
27
+ def get_inner_top(self) -> float:
28
+ return self.rect.y + self.padding[1]
29
29
 
30
- def get_padded_rect(self) -> pygame.FRect:
30
+ def get_local_padded_rect(self)->pygame.FRect:
31
+ return pygame.FRect(
32
+ self.padding[0],
33
+ self.padding[1],
34
+ self.rect.w - self.padding[2] - self.padding[0],
35
+ self.rect.h - self.padding[1] - self.padding[3] - self.relief,
36
+ )
37
+
38
+ def get_inner_rect(self) -> pygame.FRect:
31
39
  return pygame.FRect(
32
40
  self.rect.x + self.padding[0],
33
41
  self.rect.y + self.padding[1],