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.
- assets/bullet_hell/enemy.py +130 -0
- assets/bullet_hell/enemy_bullets.py +230 -0
- assets/bullet_hell/main.py +11 -0
- assets/bullet_hell/old_verisons/bullet_hell.py +379 -0
- assets/bullet_hell/old_verisons/enemy.py +226 -0
- assets/bullet_hell/old_verisons/game_start.py +6 -0
- assets/bullet_hell/old_verisons/main.py +50 -0
- assets/bullet_hell/old_verisons/player.py +76 -0
- assets/bullet_hell/player.py +89 -0
- assets/bullet_hell/player_bullets.py +34 -0
- assets/bullet_hell/setting.py +33 -0
- examples/animated_sprite/main.py +7 -0
- examples/animated_sprite/my_sprite.py +79 -0
- examples/bullet_hell/enemy.py +152 -0
- examples/bullet_hell/enemy_bullet.py +88 -0
- examples/bullet_hell/main.py +17 -0
- examples/bullet_hell/player.py +39 -0
- examples/bullet_hell/player_bullet.py +31 -0
- examples/doodle_jump/main.py +9 -0
- examples/doodle_jump/platforms.py +51 -0
- examples/doodle_jump/player.py +52 -0
- examples/fish/assets/bullet_hell/enemy.py +130 -0
- examples/fish/assets/bullet_hell/enemy_bullets.py +230 -0
- examples/fish/assets/bullet_hell/main.py +11 -0
- examples/fish/assets/bullet_hell/old_verisons/bullet_hell.py +379 -0
- examples/fish/assets/bullet_hell/old_verisons/enemy.py +226 -0
- examples/fish/assets/bullet_hell/old_verisons/game_start.py +6 -0
- examples/fish/assets/bullet_hell/old_verisons/main.py +50 -0
- examples/fish/assets/bullet_hell/old_verisons/player.py +76 -0
- examples/fish/assets/bullet_hell/player.py +89 -0
- examples/fish/assets/bullet_hell/player_bullets.py +34 -0
- examples/fish/assets/bullet_hell/setting.py +33 -0
- examples/fish/fish.py +67 -0
- examples/fish/main.py +4 -0
- examples/getting-started/step 1 - create a sprite/main.py +11 -0
- examples/getting-started/step 1 - create a sprite/player.py +5 -0
- examples/getting-started/step 2 - control a sprite/main.py +11 -0
- examples/getting-started/step 2 - control a sprite/player.py +42 -0
- examples/getting-started/step 3 - backdrops/main.py +17 -0
- examples/getting-started/step 3 - backdrops/player.py +33 -0
- examples/getting-started/step 4 - clone a sprite/enemy.py +53 -0
- examples/getting-started/step 4 - clone a sprite/main.py +17 -0
- examples/getting-started/step 4 - clone a sprite/player.py +32 -0
- examples/getting-started/step 4 - clone a sprite (simple)/enemy.py +42 -0
- examples/getting-started/step 4 - clone a sprite (simple)/main.py +17 -0
- examples/getting-started/step 4 - clone a sprite (simple)/player.py +32 -0
- examples/getting-started/step 5 - local variables/enemy.py +52 -0
- examples/getting-started/step 5 - local variables/main.py +17 -0
- examples/getting-started/step 5 - local variables/player.py +49 -0
- examples/getting-started/step 6 - shared variables/enemy.py +64 -0
- examples/getting-started/step 6 - shared variables/main.py +17 -0
- examples/getting-started/step 6 - shared variables/player.py +80 -0
- examples/getting-started/step 7 - Referencing other sprites/enemy.py +83 -0
- examples/getting-started/step 7 - Referencing other sprites/hearts.py +39 -0
- examples/getting-started/step 7 - Referencing other sprites/main.py +23 -0
- examples/getting-started/step 7 - Referencing other sprites/player.py +59 -0
- examples/getting-started/step 8 - sprite variables/enemy.py +98 -0
- examples/getting-started/step 8 - sprite variables/hearts.py +39 -0
- examples/getting-started/step 8 - sprite variables/main.py +22 -0
- examples/getting-started/step 8 - sprite variables/player.py +63 -0
- examples/getting-started/step 9 - messages/enemy.py +98 -0
- examples/getting-started/step 9 - messages/hearts.py +39 -0
- examples/getting-started/step 9 - messages/main.py +23 -0
- examples/getting-started/step 9 - messages/player.py +78 -0
- examples/perspective_background/main.py +14 -0
- examples/perspective_background/player.py +52 -0
- examples/perspective_background/trees.py +39 -0
- examples/simple_pong/ball.py +72 -0
- examples/simple_pong/left_paddle.py +36 -0
- examples/simple_pong/main.py +21 -0
- examples/simple_pong/right_paddle.py +37 -0
- examples/simple_pong/score_display.py +54 -0
- pyscratch/__init__.py +48 -0
- pyscratch/event.py +263 -0
- pyscratch/game_module.py +1589 -0
- pyscratch/helper.py +561 -0
- pyscratch/sprite.py +1920 -0
- pyscratch/tools/sprite_preview/left_panel/frame_preview_card.py +238 -0
- pyscratch/tools/sprite_preview/left_panel/frame_preview_panel.py +42 -0
- pyscratch/tools/sprite_preview/main.py +18 -0
- pyscratch/tools/sprite_preview/main_panel/animation_display.py +77 -0
- pyscratch/tools/sprite_preview/main_panel/frame_bin.py +33 -0
- pyscratch/tools/sprite_preview/main_panel/play_edit_ui.py +64 -0
- pyscratch/tools/sprite_preview/main_panel/set_as_sprite_folder.py +22 -0
- pyscratch/tools/sprite_preview/main_panel/sprite_edit_ui.py +174 -0
- pyscratch/tools/sprite_preview/main_panel/warning_message.py +25 -0
- pyscratch/tools/sprite_preview/right_panel/back_button.py +35 -0
- pyscratch/tools/sprite_preview/right_panel/cut_button.py +32 -0
- pyscratch/tools/sprite_preview/right_panel/cut_parameter_fitting.py +152 -0
- pyscratch/tools/sprite_preview/right_panel/cut_parameters.py +42 -0
- pyscratch/tools/sprite_preview/right_panel/file_display.py +84 -0
- pyscratch/tools/sprite_preview/right_panel/file_display_area.py +57 -0
- pyscratch/tools/sprite_preview/right_panel/spritesheet_view.py +262 -0
- pyscratch/tools/sprite_preview/right_panel/ss_select_corner.py +208 -0
- pyscratch/tools/sprite_preview/settings.py +14 -0
- pyscratch/tools/sprite_preview/utils/input_box.py +235 -0
- pyscratch/tools/sprite_preview/utils/render_wrapped_file_name.py +86 -0
- pyscratch_pysc-1.0.3.dist-info/METADATA +37 -0
- pyscratch_pysc-1.0.3.dist-info/RECORD +101 -0
- pyscratch_pysc-1.0.3.dist-info/WHEEL +5 -0
- 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)
|