pyscratch-pysc 1.0.3__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 (101) hide show
  1. assets/bullet_hell/enemy.py +130 -0
  2. assets/bullet_hell/enemy_bullets.py +230 -0
  3. assets/bullet_hell/main.py +11 -0
  4. assets/bullet_hell/old_verisons/bullet_hell.py +379 -0
  5. assets/bullet_hell/old_verisons/enemy.py +226 -0
  6. assets/bullet_hell/old_verisons/game_start.py +6 -0
  7. assets/bullet_hell/old_verisons/main.py +50 -0
  8. assets/bullet_hell/old_verisons/player.py +76 -0
  9. assets/bullet_hell/player.py +89 -0
  10. assets/bullet_hell/player_bullets.py +34 -0
  11. assets/bullet_hell/setting.py +33 -0
  12. examples/animated_sprite/main.py +7 -0
  13. examples/animated_sprite/my_sprite.py +79 -0
  14. examples/bullet_hell/enemy.py +152 -0
  15. examples/bullet_hell/enemy_bullet.py +88 -0
  16. examples/bullet_hell/main.py +17 -0
  17. examples/bullet_hell/player.py +39 -0
  18. examples/bullet_hell/player_bullet.py +31 -0
  19. examples/doodle_jump/main.py +9 -0
  20. examples/doodle_jump/platforms.py +51 -0
  21. examples/doodle_jump/player.py +52 -0
  22. examples/fish/assets/bullet_hell/enemy.py +130 -0
  23. examples/fish/assets/bullet_hell/enemy_bullets.py +230 -0
  24. examples/fish/assets/bullet_hell/main.py +11 -0
  25. examples/fish/assets/bullet_hell/old_verisons/bullet_hell.py +379 -0
  26. examples/fish/assets/bullet_hell/old_verisons/enemy.py +226 -0
  27. examples/fish/assets/bullet_hell/old_verisons/game_start.py +6 -0
  28. examples/fish/assets/bullet_hell/old_verisons/main.py +50 -0
  29. examples/fish/assets/bullet_hell/old_verisons/player.py +76 -0
  30. examples/fish/assets/bullet_hell/player.py +89 -0
  31. examples/fish/assets/bullet_hell/player_bullets.py +34 -0
  32. examples/fish/assets/bullet_hell/setting.py +33 -0
  33. examples/fish/fish.py +67 -0
  34. examples/fish/main.py +4 -0
  35. examples/getting-started/step 1 - create a sprite/main.py +11 -0
  36. examples/getting-started/step 1 - create a sprite/player.py +5 -0
  37. examples/getting-started/step 2 - control a sprite/main.py +11 -0
  38. examples/getting-started/step 2 - control a sprite/player.py +42 -0
  39. examples/getting-started/step 3 - backdrops/main.py +17 -0
  40. examples/getting-started/step 3 - backdrops/player.py +33 -0
  41. examples/getting-started/step 4 - clone a sprite/enemy.py +53 -0
  42. examples/getting-started/step 4 - clone a sprite/main.py +17 -0
  43. examples/getting-started/step 4 - clone a sprite/player.py +32 -0
  44. examples/getting-started/step 4 - clone a sprite (simple)/enemy.py +42 -0
  45. examples/getting-started/step 4 - clone a sprite (simple)/main.py +17 -0
  46. examples/getting-started/step 4 - clone a sprite (simple)/player.py +32 -0
  47. examples/getting-started/step 5 - local variables/enemy.py +52 -0
  48. examples/getting-started/step 5 - local variables/main.py +17 -0
  49. examples/getting-started/step 5 - local variables/player.py +49 -0
  50. examples/getting-started/step 6 - shared variables/enemy.py +64 -0
  51. examples/getting-started/step 6 - shared variables/main.py +17 -0
  52. examples/getting-started/step 6 - shared variables/player.py +80 -0
  53. examples/getting-started/step 7 - Referencing other sprites/enemy.py +83 -0
  54. examples/getting-started/step 7 - Referencing other sprites/hearts.py +39 -0
  55. examples/getting-started/step 7 - Referencing other sprites/main.py +23 -0
  56. examples/getting-started/step 7 - Referencing other sprites/player.py +59 -0
  57. examples/getting-started/step 8 - sprite variables/enemy.py +98 -0
  58. examples/getting-started/step 8 - sprite variables/hearts.py +39 -0
  59. examples/getting-started/step 8 - sprite variables/main.py +22 -0
  60. examples/getting-started/step 8 - sprite variables/player.py +63 -0
  61. examples/getting-started/step 9 - messages/enemy.py +98 -0
  62. examples/getting-started/step 9 - messages/hearts.py +39 -0
  63. examples/getting-started/step 9 - messages/main.py +23 -0
  64. examples/getting-started/step 9 - messages/player.py +78 -0
  65. examples/perspective_background/main.py +14 -0
  66. examples/perspective_background/player.py +52 -0
  67. examples/perspective_background/trees.py +39 -0
  68. examples/simple_pong/ball.py +72 -0
  69. examples/simple_pong/left_paddle.py +36 -0
  70. examples/simple_pong/main.py +21 -0
  71. examples/simple_pong/right_paddle.py +37 -0
  72. examples/simple_pong/score_display.py +54 -0
  73. pyscratch/__init__.py +48 -0
  74. pyscratch/event.py +263 -0
  75. pyscratch/game_module.py +1589 -0
  76. pyscratch/helper.py +561 -0
  77. pyscratch/sprite.py +1920 -0
  78. pyscratch/tools/sprite_preview/left_panel/frame_preview_card.py +238 -0
  79. pyscratch/tools/sprite_preview/left_panel/frame_preview_panel.py +42 -0
  80. pyscratch/tools/sprite_preview/main.py +18 -0
  81. pyscratch/tools/sprite_preview/main_panel/animation_display.py +77 -0
  82. pyscratch/tools/sprite_preview/main_panel/frame_bin.py +33 -0
  83. pyscratch/tools/sprite_preview/main_panel/play_edit_ui.py +64 -0
  84. pyscratch/tools/sprite_preview/main_panel/set_as_sprite_folder.py +22 -0
  85. pyscratch/tools/sprite_preview/main_panel/sprite_edit_ui.py +174 -0
  86. pyscratch/tools/sprite_preview/main_panel/warning_message.py +25 -0
  87. pyscratch/tools/sprite_preview/right_panel/back_button.py +35 -0
  88. pyscratch/tools/sprite_preview/right_panel/cut_button.py +32 -0
  89. pyscratch/tools/sprite_preview/right_panel/cut_parameter_fitting.py +152 -0
  90. pyscratch/tools/sprite_preview/right_panel/cut_parameters.py +42 -0
  91. pyscratch/tools/sprite_preview/right_panel/file_display.py +84 -0
  92. pyscratch/tools/sprite_preview/right_panel/file_display_area.py +57 -0
  93. pyscratch/tools/sprite_preview/right_panel/spritesheet_view.py +262 -0
  94. pyscratch/tools/sprite_preview/right_panel/ss_select_corner.py +208 -0
  95. pyscratch/tools/sprite_preview/settings.py +14 -0
  96. pyscratch/tools/sprite_preview/utils/input_box.py +235 -0
  97. pyscratch/tools/sprite_preview/utils/render_wrapped_file_name.py +86 -0
  98. pyscratch_pysc-1.0.3.dist-info/METADATA +37 -0
  99. pyscratch_pysc-1.0.3.dist-info/RECORD +101 -0
  100. pyscratch_pysc-1.0.3.dist-info/WHEEL +5 -0
  101. pyscratch_pysc-1.0.3.dist-info/top_level.txt +3 -0
pyscratch/sprite.py ADDED
@@ -0,0 +1,1920 @@
1
+ """
2
+ Everything in this module is directly under the pyscratch namespace.
3
+ For example, instead of doing `pysc.sprite.create_animated_sprite`,
4
+ you can also directly do `pysc.create_animated_sprite`.
5
+ """
6
+
7
+
8
+
9
+ from __future__ import annotations
10
+ from enum import Enum
11
+ from functools import cache
12
+ import inspect
13
+ from typing import Any, Callable, Dict, Hashable, Iterable, List, Optional, ParamSpec, Tuple, Union, cast, override
14
+ from typing_extensions import deprecated
15
+
16
+
17
+ import numpy as np
18
+ import pygame
19
+ import pymunk
20
+
21
+ import pyscratch.game_module
22
+ from .game_module import Game, game
23
+ from .helper import adjust_brightness, set_transparency, create_rect, create_circle, load_frames_from_folder, load_gif_frames, load_frames_from_gif_folder
24
+ from pathlib import Path
25
+
26
+
27
+ def create_sprite_from_gif(path, *args, **kwargs):
28
+
29
+ """
30
+ Create a sprite using a GIF file or GIF files inside a folder.
31
+
32
+ The folder should be organised in the following way:
33
+ ```
34
+ ├─ player/
35
+ ├─ walking.gif
36
+ ├─ idling.gif
37
+ ├─ jumping.gif
38
+ ...
39
+ ```
40
+ **Example**
41
+ ```python
42
+ # takes the path of a gif (single animation sprite)
43
+ my_sprite1 = create_sprite_from_gif("assets/player/walking.gif")
44
+
45
+ # takes the path of the folder (multiple animations)
46
+ my_sprite2 = create_sprite_from_gif("assets/player")
47
+ ```
48
+ """
49
+
50
+
51
+ path = Path(path)
52
+
53
+ if path.is_dir():
54
+ frame_dict = load_frames_from_gif_folder(path)
55
+ else:
56
+ frames = load_gif_frames(path)
57
+ frame_dict = {'always': frames}
58
+ return Sprite(frame_dict, *args, **kwargs)
59
+
60
+
61
+
62
+
63
+ def create_animated_sprite(folder_path, *args, **kwargs):
64
+ """
65
+ Create a sprite using the images inside a folder.
66
+
67
+ The folder should be organised in one of these two ways:
68
+
69
+ **Option 1**: Only one frame mode. *Note that the images must be numbered.*
70
+ ```
71
+ ├─ player/
72
+ ├─ 0.png
73
+ ├─ 1.png
74
+ ├─ 2.png
75
+ ...
76
+ ```
77
+
78
+ **Option 2**: Multiple frame modes. *Note that the images must be numbered.*
79
+ ```
80
+ ├─ player/
81
+ ├─ walking/
82
+ ├─ 0.png
83
+ ├─ 1.png
84
+ ...
85
+ ├─ idling/
86
+ ├─ 0.png
87
+ ├─ 1.png
88
+ ...
89
+ ...
90
+ ```
91
+
92
+
93
+ **Example**
94
+ ```python
95
+ # takes the path of the folder
96
+ my_sprite1 = create_animated_sprite("assets/player")
97
+
98
+ # optionally take in whatever parameters that the `Sprite` constructor take.
99
+ my_sprite2 = create_animated_sprite("assets/player", position=(200, 200))
100
+
101
+ # For option 2 only: optionally set the `starting_animation` parameter in the `Sprite` constructor, but it'd still be fine without it.
102
+ my_sprite3 = create_animated_sprite("assets/player", "idling")
103
+
104
+
105
+ # Event: to animate the sprite
106
+ def animating():
107
+
108
+ # switch to the next frame every 0.2 second
109
+ while True:
110
+ my_sprite1.next_frame() # scratch block: next costume
111
+ yield 0.2
112
+
113
+ my_sprite1.when_game_start().add_handler(animating)
114
+
115
+
116
+ # if doing option 2: use `set_animation` to select the frame
117
+ # there's no equivalent scratch block
118
+ def movement():
119
+ while True:
120
+ if pysc.sensing.is_key_pressed('right'):
121
+ my_sprite1.set_animation('walking') # reference to the folder name
122
+ my_sprite1.x += 1
123
+ else:
124
+ my_sprite1.set_animatione('idling')
125
+
126
+ yield 1/FRAMERATE
127
+
128
+ my_sprite1.when_game_start().add_handler(movement)
129
+
130
+ ```
131
+ Parameters
132
+ ---
133
+ folder_path: str
134
+ The path of the folder that contains the images
135
+
136
+ \\*args & \\*\\*kwargs: Optional
137
+ Whatever the `Sprite` constructor takes, except the `frame_dict` parameter.
138
+ """
139
+ frame_dict = load_frames_from_folder(folder_path)
140
+ return Sprite(frame_dict, *args, **kwargs)
141
+
142
+
143
+ def create_single_costume_sprite(image_path, *args, **kwargs):
144
+ """
145
+ Create a sprite with only one costume, given the path to the image
146
+
147
+ Example
148
+ ```python
149
+ my_sprite1 = create_single_costume_sprite("assets/player.png")
150
+
151
+ # Optionally pass some parameters to the `Sprite` constructor
152
+ my_sprite2 = create_single_costume_sprite("assets/player.png", position=(200, 200))
153
+
154
+ ```
155
+ Parameters
156
+ ---
157
+ image_path: str
158
+ The path of the images
159
+
160
+ \\*args & \\*\\*kwargs: Optional
161
+ Whatever the `Sprite` constructor takes, except `frame_dict` & `starting_animation`.
162
+ """
163
+ img = pygame.image.load(image_path).convert_alpha()
164
+ frame_dict = {"always": [img]}
165
+ return Sprite(frame_dict, "always", *args, **kwargs)
166
+
167
+
168
+ def create_shared_data_display_sprite(key, font, size = (150, 50), bg_colour=(127, 127, 127), text_colour=(255,255,255), position: Optional[Tuple[float, float]]=None, update_period=0.1, **kwargs):
169
+ """
170
+ Create a display for a variable inside shared_data given the dictionary key (i.e. the name of the variable).
171
+ The variable display will update every `update_period` seconds.
172
+ The variable display is created as a sprite.
173
+
174
+ This function is created using only basic pyscratch functions and events.
175
+ If you want a more customised display, you may want to have a look at the source code as an example how it's done.
176
+
177
+ Example
178
+ ```python
179
+
180
+ # if the font is shared by multiple sprites, consider putting it in `settings.py`
181
+ font = pygame.font.SysFont(None, 48) # None = default font, 48 = font size
182
+
183
+ # the shared data
184
+ game.shared_data['hp'] = 10
185
+
186
+ # the variable display is created as a sprite
187
+ health_display = create_shared_data_display_sprite('hp', font, position=(100, 100), update_period=0.5)
188
+
189
+ ```
190
+ Parameters
191
+ ---
192
+ key: str
193
+ The dictionary key of the variable in `game.shared_data` that you want to display.
194
+ font: pygame.font.Font
195
+ The pygame font object. Refer to the website of pygame for more details.
196
+ size: Tuple[float, float]
197
+ The size of the display panel
198
+ bg_colour: Tuple[int, int, int] or Tuple[int, int, int, int]
199
+ The colour of the display panel in RGB or RGBA. Value range: [0-255]
200
+ text_colour: Tuple[int, int, int] or Tuple[int, int, int, int]
201
+ The colour of the text in RGB or RGBA. Value range: [0-255]
202
+ position: Tuple[int, int]
203
+ The position of the display
204
+ update_period: float
205
+ The variable display will update every `update_period` seconds.
206
+ \\*\\*kwargs: Optional
207
+ Whatever the `Sprite` constructor takes, except `frame_dict`,`starting_animation` & `position`
208
+ """
209
+
210
+ w, h = size
211
+ if position is None:
212
+ position = w/2+25, h/2+25
213
+ sprite = create_rect_sprite(bg_colour, w, h, position=position, **kwargs)
214
+
215
+ def update_value():
216
+ while True:
217
+ sprite.write_text(f"{key}: {game.shared_data[key]}", font=font, colour=text_colour, offset=(w/2, h/2))
218
+ yield update_period
219
+
220
+ sprite.when_game_start().add_handler(update_value)
221
+ return sprite
222
+
223
+ def create_circle_sprite(colour, radius:float, *args, **kwargs):
224
+ """
225
+ Create a circle sprite given the colour and radius
226
+ Also optionally takes in any parameters that the `Sprite` constructor takes, except `frame_dict` and `starting_animation`
227
+
228
+ Example
229
+ ```python
230
+ green_transparent = (0, 255, 0, 127)
231
+ my_rect_sprite = create_rect_sprite(green_transparent, radius=10)
232
+ ```
233
+
234
+ Parameters
235
+ ---
236
+ colour: Tuple[int, int, int] or Tuple[int, int, int, int]
237
+ The colour of the rectangle in RGB or RGBA. Value range: [0-255].
238
+ radius: float
239
+ the radius of the cirlce.
240
+ \\*args & \\*\\*kwargs: Optional
241
+ Whatever the `Sprite` constructor takes, except `frame_dict` & `starting_animation`.
242
+ """
243
+
244
+ circle = create_circle(colour, radius)
245
+ return Sprite({"always":[circle]}, "always", *args, **kwargs)
246
+
247
+
248
+ def create_rect_sprite(colour, width, height, *args, **kwargs):
249
+ """
250
+ Create a rectanglar sprite given the colour, width and height
251
+ Also optionally takes in any parameters that the `Sprite` constructor takes, except `frame_dict` and `starting_animation`
252
+
253
+ Example
254
+ ```python
255
+ green_transparent = (0, 255, 0, 127)
256
+ my_rect_sprite = create_rect_sprite(green_transparent, width=10, height=20)
257
+ ```
258
+
259
+ Parameters
260
+ ---
261
+ colour: Tuple[int, int, int] or Tuple[int, int, int, int]
262
+ The colour of the rectangle in RGB or RGBA. Value range: [0-255].
263
+ width: float
264
+ the width (x length) of the rectangle.
265
+ height: float
266
+ the height (y length) of the rectangle.
267
+ \\*args & \\*\\*kwargs: Optional
268
+ Whatever the `Sprite` constructor take, except `frame_dict` & `starting_animation`.
269
+ """
270
+ rect = create_rect(colour, width, height)
271
+ return Sprite({"always":[rect]}, "always", *args, **kwargs)
272
+
273
+
274
+ def create_edge_sprites(edge_colour = (255, 0, 0), thickness=4, collision_type=1):
275
+ """
276
+ A convenience function to create the top, left, bottom and right edges as sprites
277
+
278
+ Usage
279
+ ```python
280
+ # consider putting the edges in settings.py
281
+ top_edge, left_edge, bottom_edge, right_edge = create_edge_sprites()
282
+ ```
283
+ """
284
+
285
+ # TODO: make the edge way thicker to avoid escape due to physics inaccuracy
286
+ # edges
287
+ screen_w, screen_h = game._screen.get_width(), game._screen.get_height()
288
+
289
+ real_thickness = 300 + thickness
290
+
291
+ top_edge = create_rect_sprite(edge_colour, screen_w, real_thickness, (screen_w/2, -real_thickness/2+thickness), body_type= pymunk.Body.STATIC)
292
+
293
+ bottom_edge = create_rect_sprite(edge_colour, screen_w, real_thickness, (screen_w/2, screen_h+real_thickness/2-thickness),body_type= pymunk.Body.STATIC)
294
+
295
+ left_edge = create_rect_sprite(edge_colour, real_thickness, screen_h, (-real_thickness/2+thickness, screen_h/2),body_type= pymunk.Body.STATIC)
296
+
297
+ right_edge = create_rect_sprite(edge_colour, real_thickness, screen_h, (screen_w+real_thickness/2-thickness, screen_h/2),body_type= pymunk.Body.STATIC)
298
+
299
+ top_edge.set_collision_type(collision_type)
300
+ bottom_edge.set_collision_type(collision_type)
301
+ left_edge.set_collision_type(collision_type)
302
+ right_edge.set_collision_type(collision_type)
303
+
304
+ return top_edge, left_edge, bottom_edge, right_edge
305
+
306
+
307
+
308
+ class _RotationStyle(Enum):
309
+ ALL_AROUND = 0
310
+ LEFTRIGHT = 1
311
+ FIXED = 2
312
+
313
+
314
+ _FrameDictType = Dict[str, List[pygame.Surface]]
315
+ class _DrawingManager:
316
+ def __init__(self, frame_dict, starting_animation):
317
+
318
+
319
+ self.frame_dict_original: _FrameDictType = {k: [i.copy() for i in v] for k, v in frame_dict.items()} # never changed
320
+ self.frame_dict: _FrameDictType = {k: [i.copy() for i in v] for k, v in frame_dict.items()} # transformed on the spot
321
+
322
+
323
+ self.animation_name = starting_animation
324
+ self.frames = self.frame_dict[self.animation_name]
325
+ self.frame_idx: int = 0
326
+
327
+ # transforming parameters -> performed during update, but only when the transform is requested
328
+ self.request_transform = False
329
+ self.transparency_factor = 1.0
330
+ self.brightness_factor = 1.0
331
+ self.scale_factor: float = 1.0
332
+ self.rotation_offset: float # TODO: to be implemented
333
+
334
+ def create_blit_surfaces():
335
+ blit_surfaces = {}
336
+ for k in self.frame_dict_original:
337
+ for i in range(len(self.frame_dict_original[k])):
338
+ blit_surfaces[(k, i)] = []
339
+ return blit_surfaces
340
+ self.blit_surfaces: Dict[Tuple[str, int], List[Tuple[pygame.Surface, Tuple[float, float]]]] = create_blit_surfaces()
341
+
342
+
343
+ # rotation and flips -> performed every update on the current frame
344
+ self.rotation_style: _RotationStyle = _RotationStyle.ALL_AROUND
345
+ self.flip_x: bool = False
346
+ self.flip_y: bool = False
347
+ self.mask_threshold: int = 1
348
+
349
+
350
+ def set_mask_threshold(self, value=1):
351
+ self.mask_threshold = value
352
+
353
+ def set_rotation_style(self, flag: _RotationStyle):
354
+ self.rotation_style = flag
355
+
356
+ def flip_horizontal(self, to_flip:bool):
357
+ self.flip_x = to_flip
358
+
359
+ def flip_vertical(self, to_flip:bool):
360
+ self.flip_y = to_flip
361
+
362
+ def set_animation(self, new_mode):
363
+ if new_mode == self.animation_name:
364
+ return
365
+ self.animation_name = new_mode
366
+ self.frames = self.frame_dict[new_mode]
367
+ self.frame_idx = 0
368
+
369
+ def set_frame(self, idx):
370
+ # also allow direct setting of frame_idx
371
+ self.frame_idx = idx
372
+
373
+ def next_frame(self):
374
+ self.frame_idx = (self.frame_idx+1) % len(self.frames)
375
+
376
+ # core transform requests
377
+ def set_scale(self, factor):
378
+ if not self.scale_factor == factor:
379
+ self.request_transform = True
380
+ self.scale_factor = factor
381
+ return True
382
+ else:
383
+ return False
384
+
385
+
386
+ def set_brightness(self, factor):
387
+ self.brightness_factor = factor
388
+ self.request_transform = True
389
+
390
+ def set_transparency(self, factor):
391
+ self.transparency_factor = factor
392
+ #self.request_transform = True
393
+
394
+ def blit_persist(self, surface: pygame.Surface, offset=(0,0), centre=True, reset=True):
395
+ w, h = surface.get_width(), surface.get_height()
396
+ if centre:
397
+ offset = (offset[0]-w/2, offset[1]-h/2)
398
+ if reset:
399
+ self.blit_surfaces[(self.animation_name, self.frame_idx)] = [(surface, offset)]
400
+ else:
401
+ self.blit_surfaces[(self.animation_name, self.frame_idx)].append((surface, offset))
402
+ self.request_transform = True
403
+
404
+ # transform related helper
405
+ def scale_by(self, factor):
406
+ self.set_scale(self.scale_factor*factor)
407
+
408
+ def write_text(self, text: str, font: pygame.font.Font, colour=(255,255,255), offset=(0,0), centre=True, reset=True):
409
+ text_surface = font.render(text, True, colour)
410
+ self.blit_persist(text_surface, offset, centre, reset)
411
+
412
+ # transform
413
+ #@cache
414
+ def transform_frames(self):
415
+ self.request_transform = False
416
+ for k, frames in self.frame_dict_original.items():
417
+ new_frames = []
418
+ for idx, f in enumerate(frames):
419
+ f_new = f.copy()
420
+ for s, o in self.blit_surfaces[(k, idx)]:
421
+ f_new.blit(s, o)
422
+ #f_new = set_transparency(f_new, self.transparency_factor)
423
+ f_new = adjust_brightness(f_new, self.brightness_factor)
424
+ f_new = pygame.transform.scale_by(f_new, self.scale_factor)
425
+ new_frames.append(f_new)
426
+
427
+ self.frame_dict[k] = new_frames
428
+
429
+ self.frames = self.frame_dict[self.animation_name]
430
+
431
+ def on_update(self, x, y, angle) -> Tuple[pygame.Surface, pygame.Rect, pygame.Mask]:
432
+ if self.request_transform:
433
+ self.transform_frames()
434
+
435
+ img = self.frames[self.frame_idx]
436
+
437
+ if self.rotation_style == _RotationStyle.ALL_AROUND:
438
+ img = pygame.transform.rotate(img, -angle)
439
+
440
+ elif self.rotation_style == _RotationStyle.LEFTRIGHT:
441
+ if angle > -90 and angle < 90:
442
+ pass
443
+ else:
444
+ img = pygame.transform.flip(img, True, False)
445
+
446
+ elif self.rotation_style == _RotationStyle.FIXED:
447
+ pass
448
+
449
+ img = pygame.transform.flip(img, self.flip_x, self.flip_y)
450
+
451
+
452
+ img_w, img_h = img.get_width(), img.get_height()
453
+ rect = img.get_rect(
454
+ center=(x, y),
455
+ width=img_w,
456
+ height=img_h,
457
+ )
458
+
459
+
460
+ mask = pygame.mask.from_surface(img, self.mask_threshold)
461
+ img = set_transparency(img, self.transparency_factor)
462
+
463
+ return img, rect, mask
464
+
465
+ class ShapeType(Enum):
466
+ """@private"""
467
+ BOX = 'box'
468
+ CIRCLE = 'circle'
469
+ CIRCLE_WIDTH = 'circle_width'
470
+ CIRCLE_HEIGHT = 'circle_height'
471
+
472
+
473
+ class _PhysicsManager:
474
+ def __init__(self, game, body_type, shape_type, shape_size_factor, position, initial_image):
475
+
476
+ # shape properties that requires shape changes
477
+ self.shape_type: ShapeType = shape_type
478
+ self.collision_type: int = 1
479
+ self.shape_size_factor: float = shape_size_factor
480
+
481
+ # shape properties that does not require shape changes
482
+ self.elasticity: float = 1.0
483
+ self.friction: float = 0
484
+
485
+ # update
486
+ self.__request_shape_update = False
487
+
488
+ # core variables
489
+ self.game = game
490
+ self.space = game._space
491
+
492
+ self.body = pymunk.Body(1, 100, body_type=body_type)
493
+ self.body.position = position
494
+ self.shape = self.create_new_shape(initial_image)
495
+
496
+ self.space.add(self.body, self.shape)
497
+
498
+
499
+
500
+ def request_shape_update(self):
501
+ self.__request_shape_update = True
502
+
503
+ def set_shape_type(self, shape_type: ShapeType):
504
+ if shape_type == self.shape_type:
505
+ return
506
+ self.shape_type = shape_type
507
+ self.__request_shape_update = True
508
+
509
+ def set_shape_size_factor(self, shape_size_factor: float):
510
+ if shape_size_factor == self.shape_size_factor:
511
+ return
512
+ self.shape_size_factor = shape_size_factor
513
+ self.__request_shape_update = True
514
+
515
+
516
+ def set_collision_type(self, collision_type):
517
+ if collision_type == self.collision_type:
518
+ return
519
+ self.collision_type = collision_type
520
+ self.__request_shape_update = True
521
+
522
+ def create_new_shape(self, image: pygame.Surface):
523
+ rect = image.get_rect()
524
+ width = rect.width*self.shape_size_factor
525
+ height = rect.height*self.shape_size_factor
526
+
527
+ if self.shape_type == ShapeType.BOX:
528
+ new_shape = pymunk.Poly.create_box(self.body, (width, height))
529
+
530
+ elif self.shape_type == ShapeType.CIRCLE:
531
+ radius = (width+height)//4
532
+ new_shape = pymunk.Circle(self.body,radius)
533
+
534
+ elif self.shape_type == ShapeType.CIRCLE_WIDTH:
535
+ new_shape = pymunk.Circle(self.body, rect.width//2)
536
+
537
+ elif self.shape_type == ShapeType.CIRCLE_HEIGHT:
538
+ new_shape = pymunk.Circle(self.body, height//2)
539
+ else:
540
+ raise ValueError('invalid shape_type')
541
+
542
+ new_shape.collision_type = self.collision_type
543
+ new_shape.elasticity = self.elasticity
544
+ new_shape.friction = self.friction
545
+
546
+
547
+ return new_shape
548
+
549
+
550
+ def on_update(self, image: pygame.Surface):
551
+
552
+ if self.__request_shape_update:
553
+ self.__request_shape_update = False
554
+
555
+ new_shape = self.create_new_shape(image)
556
+
557
+ game._cleanup_old_shape(self.shape)
558
+ self.space.remove(self.shape)
559
+
560
+ self.shape = new_shape
561
+ self.space.add(self.shape)
562
+
563
+
564
+ # Creating Sprite contruction function outside this file will make the automatic sprite ID assignment much less helpful
565
+ class Sprite(pygame.sprite.Sprite):
566
+ """
567
+ Objects of the Sprite class represents a sprite.
568
+ """
569
+ def __init__(
570
+ self,
571
+ frame_dict: Dict[str, List[pygame.Surface]],
572
+ starting_mode:Optional[str]=None,
573
+ position= (100, 100),
574
+ identifier:Optional[str]=None,
575
+ shape_type = ShapeType.BOX,
576
+ shape_size_factor=1.0,
577
+ body_type=pymunk.Body.KINEMATIC,
578
+ ):
579
+ """
580
+ You might not need to create the sprite from this constructor function.
581
+ **Consider functions like `create_single_costume_sprite` or `create_animated_sprite`
582
+ as they would be easier to work with.**
583
+
584
+ Example:
585
+ ```python
586
+ image1 = helper.load_image("assets/image1.png")
587
+ image2 = helper.load_image("assets/image2.png")
588
+ image3 = helper.load_image("assets/image3.png")
589
+ image4 = helper.load_image("assets/image4.png")
590
+
591
+ frame_dict = {"walking": [image1, image2], "idling": [image3, image4]}
592
+ my_sprite = Sprite(frame_dict, "walking", shape_type="circle", body_type=pymunk.Body.DYNAMIC)
593
+
594
+ # alternative (exactly the same)
595
+ my_sprite = Sprite(frame_dict, "walking", shape_type=ShapeType.CIRCLE, body_type=pymunk.Body.DYNAMIC)
596
+ ```
597
+
598
+ Parameters
599
+ ---
600
+ frame_dict: Dict[str, List[pygame.Surface]]
601
+ A dictionary with different frame modes (str) as the keys
602
+ and lists of images as the values
603
+
604
+ starting_mode:Optional[str]
605
+ The starting frame mode. If not provided,
606
+ any one of the frame mode might be picked
607
+ as the starting frame mode.
608
+
609
+ position: Tuple[float, float]
610
+ The starting position of the sprite.
611
+
612
+ identifier: Optional[str]
613
+ Used for identifying the sprite for loading sprite states (position and direction).
614
+ Each sprite should have unique identifier.
615
+ Put to None for automatic assignment based on the file name and the order of creation.
616
+
617
+ ~~shape_type: ShapeType~~
618
+ *FEATRUE UNDER DEVELOPMENT. LEAVE AS DEFAULT.*
619
+ The collision shape. See `set_shape` for more details.
620
+
621
+ ~~shape_size_factor: float~~
622
+ *FEATRUE UNDER DEVELOPMENT. LEAVE AS DEFAULT.*
623
+
624
+ ~~body_type: int~~
625
+ *FEATRUE UNDER DEVELOPMENT. LEAVE AS DEFAULT.*
626
+ The pymunk body type. Leave out the parameter if unsure.
627
+ Can be `pymunk.Body.KINEMATIC`, `pymunk.Body.DYNAMIC` or `pymunk.Body.STATIC`
628
+ - Use kinematic if you want the sprite to move when when you tell it to.
629
+ - Use dynamic if you want the sprite to be freely moving by physics. Also refer to `set_collision_type` to enable collision.
630
+ - Use static if you do not want it to move at all.
631
+ """
632
+ super().__init__()
633
+
634
+ self.image: pygame.Surface # rotated and flipped every update during self.update
635
+ "@private"
636
+ self.rect: pygame.Rect # depends on the rotated image and thus should be redefined during self.update
637
+ "@private"
638
+
639
+ if starting_mode is None:
640
+ starting_mode = next(iter(frame_dict))
641
+
642
+ self._drawing_manager = _DrawingManager(frame_dict, starting_mode)
643
+ _initial_frame = frame_dict[starting_mode][0]
644
+ self._physics_manager = _PhysicsManager(game, body_type, shape_type, shape_size_factor, position,_initial_frame)
645
+ self.__physics_enabled = False
646
+ self.sprite_data = {}
647
+ """
648
+ A dictionary similar to `game.shared_data`.
649
+
650
+ The access of the items can be done directly through the sprite object.
651
+ For example, `my_sprite['my_data'] = "hello"` is just an alias of `my_sprite.sprite_data['my_data'] = "hello"`
652
+
653
+ You can put any data or variable that should belong to the individuals sprite.
654
+ A good example would be the health point of a charactor.
655
+
656
+ Let say if you have a uncertain number of enemy in game created by cloning or otherwise,
657
+ it would be messy to put the health point of each enemy to `game.shared_data`. In this
658
+ case, putting the health point in the private data is a better choice.
659
+
660
+
661
+ Example:
662
+ ```python
663
+ # same as `my_sprite.sprite_data['hp'] = 10`
664
+ my_sprite['hp'] = 10
665
+
666
+ def on_hit(damage):
667
+ my_sprite['hp'] -= damage
668
+
669
+ print("how much hp I have left: ", my_sprite['hp'])
670
+
671
+ my_sprite.when_received_message('hit').add_handler(on_hit)
672
+
673
+ game.broadcast_message('hit', 2)
674
+ ```
675
+ """
676
+
677
+ self._mouse_selected = False
678
+ self.__is_dragging = False
679
+ self.draggable:bool = False
680
+ """Whether or not this sprite is draggable."""
681
+
682
+
683
+ self.oob_limit: float = 500
684
+ """The sprite will be removed automatically when it is out of the screen for more than `oob_limit` pixel. Default to 500."""
685
+
686
+
687
+ self._lock_to_sprite = None
688
+ self._lock_offset = 0, 0
689
+ self.__x, self.__y = self._physics_manager.body.position[0], self._physics_manager.body.position[1]
690
+
691
+
692
+ self.__direction: pymunk.Vec2d = self._body.rotation_vector
693
+ self.__rotation_style = _RotationStyle.ALL_AROUND
694
+
695
+ self.__removed:bool = False
696
+
697
+
698
+
699
+ # get the caller name
700
+ frame = inspect.currentframe()
701
+ assert frame
702
+
703
+ this_file = frame.f_code.co_filename
704
+
705
+ while True:
706
+ frame = frame.f_back
707
+ if not frame:
708
+ caller_file = "UNKNOWN"
709
+ break
710
+
711
+ caller_file = frame.f_code.co_filename
712
+ #print(caller_file)
713
+ if not caller_file == this_file:
714
+ break
715
+
716
+ count = game._add_sprite(self, caller_file=caller_file)
717
+ self.identifier: str
718
+ """
719
+ An identifier of the sprite for loading sprite states (position and direction).
720
+ Each sprite should have unique identifier.
721
+ Put to None for automatic assignment based on the file name and the order of creation.
722
+
723
+ """
724
+
725
+ if not identifier:
726
+
727
+
728
+ self.identifier = caller_file + ":" + str(count)
729
+ else:
730
+ self.identifier = identifier
731
+
732
+
733
+ #print(self)
734
+
735
+ def __repr__(self):
736
+ return f"Sprite(id='{self.identifier}')"
737
+
738
+
739
+ def __getitem__(self, key):
740
+ return self.sprite_data[key]
741
+
742
+ def __setitem__(self, k, v):
743
+ self.sprite_data[k] = v
744
+
745
+ @property
746
+ def _body(self):
747
+ return self._physics_manager.body
748
+
749
+ @property
750
+ def _shape(self):
751
+ return self._physics_manager.shape
752
+
753
+ @override
754
+ def update(self):
755
+ "@private"
756
+
757
+ if self._lock_to_sprite:
758
+ self._body.position = self._lock_to_sprite._body.position + (self.__x, self.__y) + self._lock_offset
759
+ self._body.velocity = 0, 0
760
+
761
+ x, y = self._body.position
762
+ self.image, self.rect, self.mask = self._drawing_manager.on_update(x, y, self.__direction.angle_degrees)
763
+
764
+ self._physics_manager.on_update(self.image)
765
+
766
+ if self.__is_dragging:
767
+ self._body.velocity=0,0
768
+ # TODO: should be done every physics loop or reset location every frame
769
+ # or can i change it to kinamatic temporarily
770
+
771
+ def _is_mouse_selected(self):
772
+ # TODO: why did i do this
773
+ return self._mouse_selected
774
+
775
+ @property
776
+ def removed(self,) -> bool:
777
+ """Indicates whether or not this sprite has been removed. """
778
+ return self.__removed
779
+
780
+
781
+ def set_draggable(self, draggable):
782
+ """
783
+ Set whether or not this sprite is draggable.
784
+
785
+ Example:
786
+ ```python
787
+ # Make the sprite draggable
788
+ my_sprite.set_draggable(True)
789
+ ```
790
+ """
791
+ self.draggable = draggable
792
+
793
+ def _set_is_dragging(self, is_dragging):
794
+ self.__is_dragging = is_dragging
795
+
796
+
797
+ # START: motions
798
+ @property
799
+ def x(self):
800
+ """
801
+ The x position of the sprite.
802
+ You can change this property to change the x position of the sprite.
803
+
804
+ Remember that the top-left corner is (x=0, y=0),
805
+ and x increases as the sprite goes right.
806
+
807
+ so setting x to 0 sends the sprite to the left edge.
808
+
809
+ Example:
810
+ ```python
811
+ # moves the sprite 10 pixels to the right
812
+ my_sprite.x += 10
813
+ ```
814
+ """
815
+ if self._lock_to_sprite:
816
+ return self.__x
817
+ return self._body.position[0]
818
+
819
+ @property
820
+ def y(self):
821
+ """
822
+ The y position of the sprite.
823
+ You can change this property to change the y position of the sprite.
824
+
825
+ Remember that the top-left corner is (x=0, y=0),
826
+ and y increases as the sprite goes ***down***.
827
+
828
+ so setting y to 0 sends the sprite to the top edge.
829
+
830
+ Example:
831
+ ```python
832
+ # moves the sprite 10 pixels down
833
+ my_sprite.y += 10
834
+ ```
835
+ """
836
+ if self._lock_to_sprite:
837
+ return self.__y
838
+ return self._body.position[1]
839
+
840
+ @property
841
+ def direction(self):
842
+ """
843
+ The direction of movement of the sprite.
844
+ Also rotates the sprite image depending on the rotation style.
845
+ You can change this property to change the direction of movement of the sprite.
846
+
847
+ - 0 degree is pointing to the left
848
+ - 90 degree is pointing ***down***
849
+ - 180 degree is pointing to the right
850
+ - -90 degree or 270 degree is pointing up
851
+
852
+ Therefore, increasing this value turns the sprite clockwise
853
+
854
+ (If you find it strange that 90 degree is pointing down,
855
+ it is because y is positive when going down)
856
+
857
+ Example:
858
+ ```python
859
+ # moves the sprite 10 degrees clockwise
860
+ my_sprite.direction += 10
861
+ ```
862
+ """
863
+
864
+ #self.__direction
865
+ if self.__rotation_style == _RotationStyle.ALL_AROUND:
866
+ return self._body.rotation_vector.angle_degrees
867
+ else:
868
+ return self.__direction.angle_degrees
869
+
870
+ @x.setter
871
+ def x(self, v):
872
+ if self._lock_to_sprite:
873
+ self.__x = v
874
+ else:
875
+ self._body.position = v, self._body.position[1]
876
+
877
+
878
+ @y.setter
879
+ def y(self, v):
880
+ if self._lock_to_sprite:
881
+ self.__y = v
882
+ else:
883
+ self._body.position = self._body.position[0], v
884
+
885
+ @direction.setter
886
+ def direction(self, degree):
887
+ if self.__rotation_style == _RotationStyle.ALL_AROUND:
888
+ self._body.angle = degree/180*np.pi
889
+
890
+ self.__direction = pymunk.Vec2d.from_polar(1, degree/180*np.pi)
891
+ #print(self.__direction)
892
+
893
+
894
+ def move_indir(self, steps: float, offset_degrees=0):
895
+ """
896
+ Moves the sprite forward along `direction + offset_degrees` degrees.
897
+
898
+ Example:
899
+ ```python
900
+ # move along the direction
901
+ my_sprite.move_indir(10)
902
+
903
+ # move along the direction + 90 degrees
904
+ my_sprite.move_indir(10, 90)
905
+ ```
906
+ """
907
+ #self._body.position +=
908
+
909
+ xs, ys = self.__direction.rotated_degrees(offset_degrees)*steps
910
+ self.x += xs
911
+ self.y += ys
912
+
913
+ # def move_across_dir(self, steps: float, offset_degrees=0):
914
+ # """
915
+ # Moves the sprite forward along `direction` + 90 degrees
916
+ # """
917
+ # xs, ys = self.__direction.rotated_degrees(offset_degrees)*steps
918
+ # self.x += xs
919
+ # self.y += ys
920
+
921
+
922
+ def move_xy(self, xy: Tuple[float, float]):
923
+ """
924
+ Increments both x and y.
925
+
926
+ Example:
927
+ ```python
928
+ # increase x by 10 and decrease y by 5
929
+ my_sprite.move_xy((10, -5))
930
+ ```
931
+ """
932
+ self.x += xy[0]
933
+ self.y += xy[1]
934
+
935
+ def set_xy(self, xy: Tuple[float, float]):
936
+ """
937
+ Sets the x and y coordinate.
938
+
939
+ Example:
940
+ ```python
941
+ # put the sprite to the top-left corner
942
+ my_sprite.set_xy((0, 0))
943
+ ```
944
+ """
945
+ self.x, self.y = xy
946
+
947
+
948
+ def distance_to(self, position: Tuple[float, float]) -> float:
949
+ """
950
+ Gets the distance from the centre of this sprite to a location.
951
+
952
+ Example:
953
+ ```python
954
+ # returns the distance to the centre of the screen
955
+ distance_to_centre = my_sprite.distance_to((SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
956
+ ```
957
+ """
958
+ return (position - self._body.position).length
959
+
960
+ def distance_to_sprite(self, sprite: Sprite)-> float:
961
+ """
962
+ Gets the distance between the centres of two sprites.
963
+ Returns one float or a tuple of two floats.
964
+
965
+ Example:
966
+ ```python
967
+ # returns the distance to another sprite
968
+ distance_to_centre = my_sprite.distance_to_sprite(my_sprite2)
969
+ ```
970
+ """
971
+
972
+ return self.distance_to(sprite._body.position)
973
+
974
+
975
+ def point_towards(self, position: Tuple[float, float], offset_degree=0):
976
+ """
977
+ Changes the direction to point to a location.
978
+
979
+ Example:
980
+ ```python
981
+ # point to the centre of the screen
982
+ my_sprite.point_towards((SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
983
+ ```
984
+ """
985
+
986
+
987
+ rot_vec = (position - self._body.position).normalized()
988
+ self.direction = (rot_vec.angle_degrees + offset_degree)
989
+
990
+
991
+ def point_towards_sprite(self, sprite: Sprite, offset_degree=0):
992
+ """
993
+ Changes the direction to point to a sprite.
994
+
995
+ Example:
996
+ ```python
997
+ # point to another sprite
998
+ my_sprite.point_towards_sprite(another_sprite2)
999
+ ```
1000
+ """
1001
+ self.point_towards(sprite._body.position, offset_degree)
1002
+
1003
+ def point_towards_mouse(self, offset_degree=0):
1004
+ """
1005
+ Changes the direction to point to the mouse.
1006
+
1007
+ Example:
1008
+ ```python
1009
+ # point to the mouse
1010
+ my_sprite.point_towards_mouse()
1011
+ ```
1012
+ """
1013
+ self.point_towards(pygame.mouse.get_pos(), offset_degree)
1014
+
1015
+
1016
+ def if_on_edge_bounce(self, bounce_amplitude=5):
1017
+ """
1018
+ Works very similarly to the Scratch counterpart except that it still allows the sprite to go into the edge if you force it to.
1019
+
1020
+ The optional parameter `bounce_amplitude` is used for specifying by how much the sprite should be repelled from the edge.
1021
+ """
1022
+ if self._lock_to_sprite:
1023
+ print("a locked sprite cannot bounce")
1024
+ return
1025
+
1026
+ x, y = self.__direction
1027
+ changed = False
1028
+
1029
+ if self.is_touching(game._top_edge):
1030
+ changed = True
1031
+ self.y += bounce_amplitude
1032
+ y = abs(y)
1033
+
1034
+ elif self.is_touching(game._bottom_edge):
1035
+ changed = True
1036
+ self.y -= bounce_amplitude
1037
+ y = -abs(y)
1038
+
1039
+ if self.is_touching(game._left_edge):
1040
+ changed = True
1041
+ self.x += bounce_amplitude
1042
+ x = abs(x)
1043
+
1044
+ elif self.is_touching(game._right_edge):
1045
+ changed = True
1046
+ self.x -= bounce_amplitude
1047
+ x = -abs(x)
1048
+
1049
+ # is it really gonna make a difference in the performance?
1050
+ if changed:
1051
+ self.direction = pymunk.Vec2d(x, y).angle_degrees
1052
+
1053
+ def retrieve_saved_state(self, not_exist_ok=True) -> bool:
1054
+ """
1055
+ *EXTENDED FEATURE, EXPERIMENTAL*
1056
+
1057
+ Retrieve the states of the sprites saved at the end of the previous run.
1058
+ Only the x,y,direction can be saved.
1059
+ The `identifier` is used to identifier which sprite is which.
1060
+
1061
+ To save the states of the sprites, refer to [`save_sprite_states`](./game_module#Game.save_sprite_states)
1062
+
1063
+ Returns True if the states are loaded and False if not.
1064
+
1065
+ Example:
1066
+ ```python
1067
+ my_sprite = pysc.create_circle_sprite((255,255,255), 5)
1068
+
1069
+ def on_game_start():
1070
+ my_sprite.retrieve_saved_state()
1071
+
1072
+ my_sprite.when_game_start().add_handler(on_game_start)
1073
+ ```
1074
+ """
1075
+
1076
+ result = game._get_saved_state(self.identifier)
1077
+ if result:
1078
+ self.x = result['x']
1079
+ self.y = result['y']
1080
+ self.direction = result['direction']
1081
+ return True
1082
+
1083
+ if not_exist_ok:
1084
+ return False
1085
+
1086
+ raise KeyError(f"No saved stated found for this sprite: {self.identifier}")
1087
+
1088
+ def lock_to(self, sprite: Sprite, offset: Tuple[float, float], reset_xy = False):
1089
+ """
1090
+ *EXTENDED FEATURE, EXPERIMENTAL*
1091
+
1092
+ Locks in the position of this sprite relative to the position of another sprite,
1093
+ so the sprite will always be in the same location relative to the other sprite.
1094
+ This method only need to run once (instead of continuously in a loop)
1095
+
1096
+ KNOWN ISSUE: Dragging a locked sprite will cause the sprite to go unpredictably. (It's unlikely that you would need to do that anyway.)
1097
+
1098
+ Example:
1099
+ ```python
1100
+ # a very rudimentary text bubble
1101
+ text_bubble_sprite = create_rect_sprite(...)
1102
+
1103
+ # lock the position of the text_bubble_sprite relative to the player_sprite.
1104
+ text_bubble_sprite.lock_to(player_sprite, offset=(-100, -100))
1105
+
1106
+ # a very rudimentary implementation that assumes
1107
+ # that you won't have more than one text message within 3 seconds
1108
+ def on_message(data):
1109
+
1110
+ text_bubble_sprite.write_text(data)
1111
+ text_bubble_sprite.show()
1112
+
1113
+ yield 3 # wait for three seconds
1114
+ text_bubble_sprite.hide()
1115
+
1116
+ text_bubble_sprite.when_received_message('dialogue').add_handler(on_message)
1117
+
1118
+ ```
1119
+ """
1120
+ assert self._body.body_type == pymunk.Body.KINEMATIC, "only KINEMATIC object can be locked to another sprite"
1121
+
1122
+ self._lock_to_sprite = sprite
1123
+ self._lock_offset = offset
1124
+ if reset_xy:
1125
+ self.x = 0
1126
+ self.y = 0
1127
+
1128
+ def release_position_lock(self):
1129
+ """
1130
+ *EXTENDED FEATURE, EXPERIMENTAL*
1131
+
1132
+ Release the position lock set by `lock_to`
1133
+ """
1134
+ self._lock_to_sprite = None
1135
+ self._lock_offset = None
1136
+ pass
1137
+
1138
+ # END: motions
1139
+
1140
+
1141
+ # START: drawing related
1142
+ def set_rotation_style_all_around(self):
1143
+ """
1144
+ Same as the block "set rotation style [all around]" in Scratch.
1145
+ Allow the image to rotate all around with `direction`
1146
+ """
1147
+ self._drawing_manager.set_rotation_style(_RotationStyle.ALL_AROUND)
1148
+ self.__rotation_style = _RotationStyle.ALL_AROUND
1149
+
1150
+ def set_rotation_style_left_right(self):
1151
+ """
1152
+ Same as the block "set rotation style [left-right]" in Scratch.
1153
+ Only allows the image to flip left or right depending on the `direction`.
1154
+
1155
+ Does not constrain the direction of movement to only left and right.
1156
+ """
1157
+ self._drawing_manager.set_rotation_style(_RotationStyle.LEFTRIGHT)
1158
+ self.__rotation_style = _RotationStyle.LEFTRIGHT
1159
+
1160
+ def set_rotation_style_no_rotation(self):
1161
+ """
1162
+ Same as the block "set rotation style [don't rotate]" in Scratch.
1163
+ Does not allow the image flip or rotate with `direction`.
1164
+
1165
+ Does not constrain the direction of movement.
1166
+
1167
+ """
1168
+ self._drawing_manager.set_rotation_style(_RotationStyle.FIXED)
1169
+ self.__rotation_style = _RotationStyle.FIXED
1170
+
1171
+
1172
+ def set_frame(self, idx:int):
1173
+ """
1174
+ Same as the block "switch costume to [costume]" in Scratch,
1175
+ except that you are specifying the frame (i.e. the costume) by the index.
1176
+
1177
+ TODO: link to sprite creation
1178
+ """
1179
+ self._drawing_manager.set_frame(idx)
1180
+
1181
+ def next_frame(self):
1182
+ """
1183
+ Same as the block "next costume" in Scratch,
1184
+ """
1185
+ self._drawing_manager.next_frame()
1186
+
1187
+ def set_animation(self, name:str):
1188
+ """
1189
+ *EXTENDED FEATURE*
1190
+
1191
+ Changes the set of frames that is used by `set_frame` and `next_frame`.
1192
+ This is mainly for sprites that have different animations for different actions.
1193
+
1194
+ TODO: update the link
1195
+ See the [guide](https://kwdchan.github.io/pyscratch/) for more details.
1196
+ """
1197
+ self._drawing_manager.set_animation(name)
1198
+
1199
+ @property
1200
+ def frame_idx(self):
1201
+ """
1202
+ In Scratch, this is the costume number.
1203
+
1204
+ To change costume, you will need to call `set_frame`.
1205
+ """
1206
+ return self._drawing_manager.frame_idx
1207
+
1208
+ @property
1209
+ def animation_name(self):
1210
+ """
1211
+ *EXTENDED FEATURE*
1212
+
1213
+ The name of the set of frames that is currently used.
1214
+
1215
+ Set by `set_animation`
1216
+ """
1217
+ return self._drawing_manager.animation_name
1218
+
1219
+ def set_scale(self, factor: float):
1220
+ """
1221
+ Sets the size factor of the sprite.
1222
+
1223
+ For example:
1224
+ - A factor of 1.0 means 100% of the *original* image size
1225
+ - A factor of 1.2 means 120%
1226
+ - A factor of 0.8 means 80%
1227
+ """
1228
+ if self._drawing_manager.set_scale(factor):
1229
+ self._physics_manager.request_shape_update()
1230
+
1231
+ def scale_by(self, factor: float):
1232
+ """
1233
+ Changes the size of the sprite by a factor
1234
+
1235
+ For example:
1236
+ - A factor of 1.2 is a 20% increase of the *current* size (not original size)
1237
+ - A factor of 0.8 makes the sprite 80% of the *current* size
1238
+ """
1239
+ self._drawing_manager.scale_by(factor)
1240
+ self._physics_manager.request_shape_update()
1241
+
1242
+ @property
1243
+ def scale_factor(self):
1244
+ """
1245
+ The scale factor of the sprite size
1246
+ """
1247
+ return self._drawing_manager.scale_factor
1248
+
1249
+ def flip_horizontal(self, to_flip:bool):
1250
+ """
1251
+ Whether or not to flip the image horizontally.
1252
+ Does not affect the direction of movement.
1253
+ """
1254
+ self._drawing_manager.flip_horizontal(to_flip)
1255
+
1256
+ def flip_vertical(self, to_flip:bool):
1257
+ """
1258
+ Whether of not to flip the image vertically.
1259
+ Does not affect the direction of movement.
1260
+ """
1261
+ self._drawing_manager.flip_vertical(to_flip)
1262
+
1263
+ def set_brightness(self, factor):
1264
+ """
1265
+ *EXPERIMENTAL*
1266
+
1267
+ Changes the brightness of the sprite.
1268
+ """
1269
+ self._drawing_manager.set_brightness(factor)
1270
+
1271
+ def set_transparency(self, factor:float):
1272
+ """
1273
+ *EXPERIMENTAL*
1274
+
1275
+ Changes the transparency of the sprite.
1276
+
1277
+ Parameters
1278
+ ---
1279
+ factor : float
1280
+ Transparency level from 0.0 (fully transparent) to 1.0 (fully opaque).
1281
+ """
1282
+ self._drawing_manager.set_transparency(factor)
1283
+
1284
+ def write_text(self, text: str, font: pygame.font.Font, colour=(255,255,255), offset=(0,0), centre=True, reset=True):
1285
+ """
1286
+ *EXTENDED FEATURE, EXPERIMENTAL*
1287
+
1288
+ Writes text on the sprite given a font.
1289
+ ```python
1290
+ # if the font is shared by multiple sprites, consider putting it in `settings.py`
1291
+ font = pygame.font.SysFont(None, 48) # None = default font, 48 = font size
1292
+
1293
+ my_sprite.write_text("hello_world", font)
1294
+
1295
+ ```
1296
+ Parameters
1297
+ ---
1298
+ text: str
1299
+ The text to display.
1300
+
1301
+ font: pygame.font.Font
1302
+ The pygame font object. Refer to the website of pygame for more details.
1303
+
1304
+ colour: Tuple[int, int, int] or Tuple[int, int, int, int]
1305
+ The colour the of text. Takes RGB or RGBA, where A is the transparency. Value range: [0-255]
1306
+
1307
+ offset: Tuple[float, float]
1308
+ The location of the text image relative to the sprite
1309
+
1310
+ centre: bool
1311
+ If False, the top-left corner of the text, instead of the center, would be considered as its location.
1312
+
1313
+ reset: bool
1314
+ Whether or not to clear all the existing drawing (including previous text)
1315
+
1316
+ """
1317
+ text_surface = font.render(text, True, colour)
1318
+ self._drawing_manager.blit_persist(text_surface, offset, centre=centre, reset=reset)
1319
+
1320
+ def draw(self, image: pygame.Surface, offset=(0,0), centre=True, reset=True):
1321
+ """
1322
+ *EXTENDED FEATURE, EXPERIMENTAL*
1323
+
1324
+ Draws an image on the sprite.
1325
+ ```python
1326
+ an_image = pysc.helper.load_image("assets/an_image.png")
1327
+ my_sprite.draw(an_image)
1328
+ ```
1329
+ Parameters
1330
+ ---
1331
+ image: pygame.Surface
1332
+ An image (pygame surface). You can use `helper.load_image` to load the image for you.
1333
+
1334
+ offset: Tuple[float, float]
1335
+ The location of the image relative to the sprite
1336
+
1337
+ centre: bool
1338
+ If False, the top-left corner of the image, instead of the center, would be considered as its location.
1339
+
1340
+ reset: bool
1341
+ Whether or not to clear all the existing drawing (including the text)
1342
+
1343
+ """
1344
+
1345
+ self._drawing_manager.blit_persist(image, offset, centre=centre, reset=reset)
1346
+
1347
+ # END: drawing related
1348
+
1349
+
1350
+ ## other blocks
1351
+ def is_touching(self, other_sprite) -> bool:
1352
+ """
1353
+ Returns whether or not this sprite is touching another sprite.
1354
+ A hidden sprite cannot be touched for the consistency with Scratch.
1355
+
1356
+ This function detects whether there is any overlapping of the pixels of the two sprites that are not *fully transparent*
1357
+
1358
+ Note that the detection is done on the original image before the effect of `set_transparency` is applied.
1359
+ Therefore, the touching can still be detected even if you set the transparency to 1.0.
1360
+
1361
+ ```python
1362
+ if this_sprite.is_touching(another_sprite):
1363
+ print('hi')
1364
+ ```
1365
+ """
1366
+
1367
+ if not self in game._all_sprites_to_show:
1368
+ return False
1369
+
1370
+ if not other_sprite in game._all_sprites_to_show:
1371
+ return False
1372
+
1373
+
1374
+ if not pygame.sprite.collide_rect(self, other_sprite):
1375
+ return False
1376
+
1377
+ return not (pygame.sprite.collide_mask(self, other_sprite) is None)
1378
+
1379
+ def is_touching_mouse(self):
1380
+ """
1381
+ Returns whether or not this sprite is touching the mouse
1382
+ """
1383
+ mos_x, mos_y = pygame.mouse.get_pos()
1384
+
1385
+ if not self.rect.collidepoint((mos_x, mos_y)):
1386
+ return False
1387
+
1388
+ x = mos_x-self.rect.left
1389
+ y = mos_y-self.rect.top
1390
+
1391
+ return self.mask.get_at((x, y))
1392
+
1393
+
1394
+
1395
+ # def is_touching(self, other_sprite) -> bool:
1396
+ # """
1397
+ # Returns whether or not this sprite is touching another sprite.
1398
+ # """
1399
+ # return pyscratch.game_module._is_touching(self, other_sprite)
1400
+
1401
+
1402
+ # def is_touching_mouse(self):
1403
+ # """
1404
+ # Returns whether or not this sprite is touching the mouse
1405
+ # """
1406
+ # return pyscratch.game_module._is_touching_mouse(self)
1407
+
1408
+ def hide(self):
1409
+ """
1410
+ Hides the sprite.
1411
+ The hidden sprite is still in the space but it cannot touch another sprites
1412
+ """
1413
+ game._hide_sprite(self)
1414
+
1415
+ def show(self):
1416
+ """
1417
+ Shows the sprite.
1418
+ """
1419
+ game._show_sprite(self)
1420
+
1421
+ @override
1422
+ def remove(self, *_):
1423
+ """
1424
+ Removes the sprite and all the events and conditions associated to it.
1425
+ Takes no parameter.
1426
+
1427
+ Usage:
1428
+ ```python
1429
+ # remove the sprite.
1430
+ my_sprite.remove()
1431
+ ```
1432
+ """
1433
+ game._remove_sprite(self)
1434
+ self.__removed = True
1435
+
1436
+
1437
+ def create_clone(self):
1438
+ """
1439
+ *EXPERIMENTAL*
1440
+
1441
+ Create a clone of this sprite.
1442
+ Even though is method is provided to align with Scratch,
1443
+ The prefered way to create identitical or similar sprites
1444
+ is to create the sprite within a function or an event.
1445
+
1446
+ ***INCOMPLETE IMPLEMENTATION***:
1447
+ - Transparency and brightness aren't transferred to the clone
1448
+ """
1449
+
1450
+ sprite = type(self)(
1451
+ frame_dict = self._drawing_manager.frame_dict_original,
1452
+ starting_mode = self._drawing_manager.animation_name,
1453
+ position = (self.x, self.y),
1454
+ shape_type = self._physics_manager.shape_type,
1455
+ shape_size_factor = self._physics_manager.shape_size_factor,
1456
+ body_type = self._body.body_type,
1457
+ )
1458
+ if not self in game._all_sprites_to_show:
1459
+ game._hide_sprite(sprite)
1460
+
1461
+ if self.__rotation_style == _RotationStyle.LEFTRIGHT:
1462
+ sprite.set_rotation_style_left_right()
1463
+ elif self.__rotation_style == _RotationStyle.FIXED:
1464
+ sprite.set_rotation_style_no_rotation()
1465
+
1466
+ sprite.direction = self.direction
1467
+ sprite.scale_by(self._drawing_manager.scale_factor)
1468
+ sprite.set_frame(self._drawing_manager.frame_idx)
1469
+ sprite.set_draggable(self.draggable)
1470
+ sprite.elasticity = self.elasticity
1471
+ sprite.friction = self.friction
1472
+
1473
+
1474
+
1475
+ if self._body.body_type == pymunk.Body.DYNAMIC:
1476
+ sprite.mass = self.mass
1477
+ sprite.moment = self.moment
1478
+
1479
+ sprite._drawing_manager.set_rotation_style(self._drawing_manager.rotation_style)
1480
+
1481
+
1482
+ game._clone_event_manager.on_clone(self, sprite)
1483
+ return sprite
1484
+
1485
+
1486
+ # alias of pygame method
1487
+
1488
+ def when_game_start(self, other_associated_sprites: Iterable[Sprite]=[]):
1489
+ """
1490
+ Returns an `Event` that is triggered when you call `game.start`.
1491
+ The event handler does not take in any parameter.
1492
+
1493
+ Also associates the event to the sprite so the event is removed when the sprite is removed.
1494
+
1495
+ Parameters
1496
+ ---
1497
+ other_associated_sprites: List[Sprite]
1498
+ A list of sprites that this event depends on.
1499
+ Removal of any of these sprites leads to the removal of the event.
1500
+ """
1501
+
1502
+
1503
+ associated_sprites = list(other_associated_sprites) + [self]
1504
+ return game.when_game_start(associated_sprites)
1505
+
1506
+ def when_any_key_pressed(self, other_associated_sprites: Iterable[Sprite]=[]):
1507
+ """
1508
+ Returns an `Event` that is triggered when a key is pressed or released.
1509
+ Also associates the event to the sprite so the event is removed when the sprite is removed.
1510
+
1511
+ The event handler have to take two parameters:
1512
+ - **key** (str): The key that is pressed. For example, 'w', 'd', 'left', 'right', 'space'.
1513
+ Uses [pygame.key.key_code](https://www.pygame.org/docs/ref/key.html#pygame.key.key_code) under the hood.
1514
+
1515
+ - **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
1516
+
1517
+ Parameters
1518
+ ---
1519
+ other_associated_sprites: List[Sprite]
1520
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1521
+
1522
+ """
1523
+ associated_sprites = list(other_associated_sprites) + [self]
1524
+ return game.when_any_key_pressed(associated_sprites)
1525
+
1526
+ def when_key_pressed(self, key, other_associated_sprites: Iterable[Sprite]=[]):
1527
+ """
1528
+ Returns an `Event` that is triggered when a specific key is pressed or released.
1529
+ Also associates the event to the sprite so the event is removed when the sprite is removed.
1530
+
1531
+ The event handler have to take one parameter:
1532
+ - **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
1533
+
1534
+ Parameters
1535
+ ---
1536
+ key: str
1537
+ The key that triggers the event. For example, 'w', 'd', 'left', 'right', 'space'.
1538
+ Uses [pygame.key.key_code](https://www.pygame.org/docs/ref/key.html#pygame.key.key_code) under the hood.
1539
+
1540
+ other_associated_sprites: List[Sprite]
1541
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1542
+ """
1543
+
1544
+ associated_sprites = list(other_associated_sprites) + [self]
1545
+ return game.when_key_pressed(key, associated_sprites)
1546
+
1547
+
1548
+
1549
+ def when_this_sprite_clicked(self, other_associated_sprites: Iterable[Sprite]=[]):
1550
+ """
1551
+ Returns an `Event` that is triggered when the given sprite is clicked by mouse.
1552
+ Also associates the event to the sprite so the event is removed when the sprite is removed.
1553
+
1554
+ The event handler does not take in any parameter.
1555
+
1556
+ Parameters
1557
+ ---
1558
+ sprite: Sprite
1559
+ The sprite on which you want the click to be detected. The removal of this sprite will lead to the removal of this event so
1560
+ it does not need to be included in `other_assoicated_sprite`
1561
+
1562
+ other_associated_sprites: List[Sprite]
1563
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1564
+ """
1565
+ return game.when_this_sprite_clicked(self, other_associated_sprites)
1566
+
1567
+ def when_backdrop_switched(self, idx, other_associated_sprites : Iterable[Sprite]=[]):
1568
+ """
1569
+ Returns an `Event` that is triggered when the game is switched to a backdrop at `backdrop_index`.
1570
+ Also associates the event to the sprite so the event is removed when the sprite is removed.
1571
+
1572
+ The event handler does not take in any parameter.
1573
+
1574
+ Parameters
1575
+ ---
1576
+ backdrop_index: int
1577
+ The index of the backdrop
1578
+
1579
+ other_associated_sprites: List[Sprite]
1580
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1581
+ """
1582
+
1583
+ associated_sprites = list(other_associated_sprites) + [self]
1584
+ return game.when_backdrop_switched(idx, associated_sprites)
1585
+
1586
+ def when_any_backdrop_switched(self, other_associated_sprites : Iterable[Sprite]=[]):
1587
+ """
1588
+ Returns an `Event` that is triggered when the backdrop is switched.
1589
+ Also associates the event to the sprite so the event is removed when the sprite is removed.
1590
+
1591
+ The event handler have to take one parameter:
1592
+ - **idx** (int): The index of the new backdrop
1593
+
1594
+ Parameters
1595
+ ---
1596
+ other_associated_sprites: List[Sprite]
1597
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1598
+ """
1599
+ associated_sprites = list(other_associated_sprites) + [self]
1600
+ return game.when_any_backdrop_switched(associated_sprites)
1601
+
1602
+ def when_timer_above(self, t, other_associated_sprites : Iterable[Sprite]=[]):
1603
+ """
1604
+ Returns a `Condition` that is triggered after the game have started for `t` seconds.
1605
+ A `Condition` works the same way an `Event` does.
1606
+
1607
+ Also associates the condition to the sprite so the condition is removed when the sprite is removed.
1608
+
1609
+
1610
+ The event handler have to take one parameter:
1611
+ - **n** (int): This value will always be zero
1612
+
1613
+ Parameters
1614
+ ---
1615
+ other_associated_sprites: List[Sprite]
1616
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1617
+ """
1618
+ associated_sprites = list(other_associated_sprites) + [self]
1619
+ return game.when_timer_above(t, associated_sprites)
1620
+
1621
+ def when_started_as_clone(self, associated_sprites: Iterable[Sprite]=[]):
1622
+ """
1623
+ Returns an `Event` that is triggered after the given sprite is cloned by `Sprite.create_clone`.
1624
+ Cloning of the clone will also trigger the event. Thus the removal of original sprite does not remove the event.
1625
+
1626
+ The event handler have to take one parameter:
1627
+ - **clone_sprite** (Sprite): The newly created clone.
1628
+
1629
+ Parameters
1630
+ ---
1631
+ associated_sprites: List[Sprite]
1632
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1633
+ """
1634
+
1635
+ return game.when_started_as_clone(self, associated_sprites)
1636
+
1637
+ def when_receive_message(self, topic: str, other_associated_sprites : Iterable[Sprite]=[]):
1638
+ """
1639
+ Returns an `Event` that is triggered after a message of the given `topic` is broadcasted.
1640
+ Also associates the event to the sprite so the event is removed when the sprite is removed.
1641
+
1642
+ The event handler have to take one parameter:
1643
+ - **data** (Any): This parameter can be anything passed on by the message.
1644
+
1645
+ Parameters
1646
+ ---
1647
+ topic: str
1648
+ Can be any string. If the topic equals the topic of a broadcast, the event will be triggered.
1649
+ other_associated_sprites: List[Sprite]
1650
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1651
+ """
1652
+
1653
+
1654
+ associated_sprites = list(other_associated_sprites) + [self]
1655
+ return game.when_receive_message(topic, associated_sprites)
1656
+
1657
+ def broadcast_message(self, topic: str, data: Any=None):
1658
+ """
1659
+ Completely the same as `game.broadcast_message`.
1660
+ Just an alias.
1661
+
1662
+ Sends a message of a given `topic` and `data`.
1663
+ Triggers any event that subscribes to the topic.
1664
+ The handlers of the events will receive `data` as the parameter.
1665
+
1666
+ Example:
1667
+ ```python
1668
+ def event_handler(data):
1669
+ print(data) # data will be "hello world!"
1670
+
1671
+ my_sprite.when_receive_message('print_message').add_handler(event_handler)
1672
+ my_sprite2.broadcast_message('print_message', data='hello world!')
1673
+
1674
+ # "hello world!" will be printed out
1675
+ ```
1676
+ Parameters
1677
+ ---
1678
+ topic: str
1679
+ Can be any string. If the topic of an message event equals the topic of the broadcast, the event will be triggered.
1680
+
1681
+ data: Any
1682
+ Any arbitory data that will be passed to the event handler
1683
+
1684
+ """
1685
+ return game.broadcast_message(topic, data)
1686
+
1687
+
1688
+ ## additional events
1689
+ def when_condition_met(self, checker=lambda: False, repeats: Optional[int]=None, other_associated_sprites: Iterable[Sprite]=[]):
1690
+ """
1691
+ *EXTENDED FEATURE*
1692
+
1693
+ For every frame, if a condition is met, the event is triggered. Repeated up to `repeats` times.
1694
+
1695
+ The condition is provided by a function that takes no argument and returns a boolean.
1696
+
1697
+ ```python
1698
+ def slowly_move_sprite_out_of_edge(n):
1699
+ my_sprite.x += 1
1700
+
1701
+ my_sprite.when_condition_met(lambda: (my_sprite.x<0), None).add_handler(slowly_move_sprite_out_of_edge)
1702
+ ```
1703
+
1704
+ The event handler have to take one parameter:
1705
+ - **n** (int): The number of remaining repeats
1706
+
1707
+ Parameters
1708
+ ---
1709
+ checker: Callable[[], bool]
1710
+ A function that takes no argument and returns a boolean.
1711
+ The checker is run one every frame. If it returns true, the handler is called.
1712
+
1713
+ repeats: int or None
1714
+ How many times to repeat. Set to None for infinite repeats.
1715
+
1716
+
1717
+ Parameters
1718
+ ---
1719
+ associated_sprites: List[Sprite]
1720
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1721
+ """
1722
+
1723
+ associated_sprites = list(other_associated_sprites) + [self]
1724
+
1725
+ return game.when_condition_met(checker, repeats, associated_sprites)
1726
+
1727
+
1728
+ def when_timer_reset(self, reset_period: Optional[float]=None, repeats: Optional[int]=None, other_associated_sprites: Iterable[Sprite]=[]):
1729
+ """
1730
+ *EXTENDED FEATURE, EXPERIMENTAL*
1731
+
1732
+ Returns a `Condition` that is triggered after the game have started for `t` seconds.
1733
+ A `Condition` works the same way an `Event` does.
1734
+
1735
+ The event handler have to take one parameter:
1736
+ - **n** (int): This value will always be zero
1737
+
1738
+ Parameters
1739
+ ---
1740
+ associated_sprites: List[Sprite]
1741
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1742
+ """
1743
+ associated_sprites = list(other_associated_sprites) + [self]
1744
+
1745
+ return game.when_timer_reset(reset_period, repeats, associated_sprites)
1746
+
1747
+
1748
+ def start_handler(self, handler:Optional[Callable[[], Any]]=None, other_associated_sprites : Iterable[Sprite]=[]):
1749
+ """
1750
+ Run the event handler immediately. Useful when creating a sprite within a function.
1751
+
1752
+ The handler does not take in any parameters.
1753
+
1754
+ Parameters
1755
+ ---
1756
+ handler: Function
1757
+ A function to run.
1758
+
1759
+ associated_sprites: List[Sprite]
1760
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1761
+
1762
+ """
1763
+
1764
+ associated_sprites = list(other_associated_sprites) + [self]
1765
+ return game.start_handler(handler, associated_sprites)
1766
+
1767
+
1768
+ def create_specific_collision_trigger(self, other_sprite: Sprite, other_associated_sprites: Iterable[Sprite]=[]):
1769
+ """
1770
+ @private
1771
+ *EXTENDED FEATURE, EXPERIMENTAL*
1772
+
1773
+ DOCUMENTATION NOT COMPLETED
1774
+
1775
+ Parameters
1776
+ ---
1777
+ associated_sprites: List[Sprite]
1778
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1779
+ """
1780
+ return game._create_specific_collision_trigger(self, other_sprite, other_associated_sprites)
1781
+
1782
+
1783
+ # START: TODO: physics property getters and setters
1784
+
1785
+ def set_shape(self, shape_type: ShapeType=ShapeType.BOX):
1786
+ """
1787
+ @private
1788
+ *EXTENDED FEATURE, EXPERIMENTAL*
1789
+
1790
+ Sets the collision shape of the sprite. The shape type can be one of the followings
1791
+ - box
1792
+ - circle
1793
+ - circle_height
1794
+ - circle_width
1795
+
1796
+ You can think of the collision shape as the actual shape of the sprite,
1797
+ while the sprite image (the costume) is just like a phantom projection
1798
+ that cannot be touched.
1799
+
1800
+ To see what it means, set `debug_draw` to True when you start the game.
1801
+ ```python
1802
+ game.start(60, debug_draw=True)
1803
+ ```
1804
+ """
1805
+ self._physics_manager.set_shape_type(shape_type)
1806
+
1807
+ def set_shape_size_factor(self, factor=0.8):
1808
+ """
1809
+ @private
1810
+
1811
+ *EXTENDED FEATURE, EXPERIMENTAL*
1812
+
1813
+ Changes the size of the collision shape relative to the size of the image of the sprite.
1814
+ For example:
1815
+ - factor = 1.0 -> same size
1816
+ - factor = 0.8 -> the collision shape is 80% of the sprite image
1817
+ - factor = 1.2 -> the collision shape is 120% of the sprite image
1818
+
1819
+ """
1820
+ self._physics_manager.set_shape_size_factor(factor)
1821
+
1822
+ def set_collision_type(self, value: int=0):
1823
+ """
1824
+ @private
1825
+ *EXTENDED FEATURE, EXPERIMENTAL*
1826
+
1827
+ Set the collision type of the sprite for detection purposes.
1828
+ The collision type can be any integer except that
1829
+ **a sprite with a collision type of 0 (which is the default) will not collide with anything.**
1830
+
1831
+ Note that touching can still be detected.
1832
+ """
1833
+ self._physics_manager.set_collision_type(value)
1834
+
1835
+ @property
1836
+ def mass(self):
1837
+ """
1838
+ @private
1839
+ *EXTENDED FEATURE*
1840
+
1841
+ The mass of the collision shape.
1842
+ Only work for dynamic objects.
1843
+
1844
+ You can make changes to this property.
1845
+ """
1846
+ return self._body.mass
1847
+
1848
+ @property
1849
+ def moment(self):
1850
+ """
1851
+ @private
1852
+ *EXTENDED FEATURE*
1853
+
1854
+ The moment of the collision shape.
1855
+ The lower it is, the more easy it spins.
1856
+ Only work for dynamic objects.
1857
+
1858
+ You can make changes to this property.
1859
+ """
1860
+ return self._body.moment
1861
+
1862
+ @property
1863
+ def elasticity(self):
1864
+ """
1865
+ @private
1866
+ *EXTENDED FEATURE*
1867
+
1868
+ The elasticity of the collision shape.
1869
+ Elasticity of 1 means no energy loss after each collision.
1870
+
1871
+ You can make changes to this property.
1872
+ """
1873
+ return self._shape.elasticity
1874
+
1875
+ @property
1876
+ def friction(self):
1877
+ """
1878
+ @private
1879
+ *EXTENDED FEATURE*
1880
+
1881
+ The friction of the collision shape.
1882
+
1883
+ You can make changes to this property.
1884
+ """
1885
+ return self._shape.friction
1886
+
1887
+ @mass.setter
1888
+ def mass(self, value):
1889
+ self._body.mass = value
1890
+
1891
+ @moment.setter
1892
+ def moment(self, value):
1893
+ self._body.moment = value
1894
+
1895
+ @elasticity.setter
1896
+ def elasticity(self, value):
1897
+ self._physics_manager.elasticity = value
1898
+
1899
+ @friction.setter
1900
+ def friction(self, value):
1901
+ self._physics_manager.friction = value
1902
+
1903
+ # END: physics property
1904
+
1905
+
1906
+
1907
+ def __create_edges_for_on_edge_bounce():
1908
+ t, l, b, r = create_edge_sprites((255, 255, 255, 255), thickness=0, collision_type=0)
1909
+
1910
+ t.identifier = "default_edge_t"
1911
+ l.identifier = "default_edge_l"
1912
+ b.identifier = "default_edge_b"
1913
+ r.identifier = "default_edge_r"
1914
+
1915
+ game._top_edge = t
1916
+ game._left_edge = l
1917
+ game._bottom_edge = b
1918
+ game._right_edge = r
1919
+
1920
+ game.when_game_start().add_handler(__create_edges_for_on_edge_bounce)