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/game_module.py
ADDED
@@ -0,0 +1,1589 @@
|
|
1
|
+
"""
|
2
|
+
Everything in this module is directly under the pyscratch namespace.
|
3
|
+
For example, instead of doing `pysc.game_module.is_key_pressed`,
|
4
|
+
you can also directly do `pysc.is_key_pressed`.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
from functools import cache
|
9
|
+
from os import PathLike
|
10
|
+
import os
|
11
|
+
from pathlib import Path
|
12
|
+
import threading
|
13
|
+
import time
|
14
|
+
import json, inspect
|
15
|
+
|
16
|
+
import numpy as np
|
17
|
+
import pygame
|
18
|
+
import pymunk
|
19
|
+
from .event import _ConditionInterface, Event, Condition, TimerCondition, _declare_callback_type
|
20
|
+
from pymunk.pygame_util import DrawOptions
|
21
|
+
from typing import Any, Callable, Generic, Iterable, Literal, Optional, List, Dict, ParamSpec, Set, Tuple, TypeVar, Union, cast
|
22
|
+
from typing import TYPE_CHECKING
|
23
|
+
from . import helper
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from .sprite import Sprite
|
27
|
+
|
28
|
+
def _collision_begin(arbiter, space, data):
|
29
|
+
game = cast(Game, data['game'])
|
30
|
+
game._contact_pairs_set.add(arbiter.shapes)
|
31
|
+
|
32
|
+
for e, (a,b) in game._trigger_to_collision_pairs.items():
|
33
|
+
if (a._shape in arbiter.shapes) and (b._shape in arbiter.shapes):
|
34
|
+
e.trigger(arbiter)
|
35
|
+
|
36
|
+
|
37
|
+
colliding_types = arbiter.shapes[0].collision_type, arbiter.shapes[1].collision_type
|
38
|
+
collision_allowed = True
|
39
|
+
for collision_type, (allowed, triggers) in game._collision_type_to_trigger.items():
|
40
|
+
if collision_type in colliding_types:
|
41
|
+
[t.trigger(arbiter) for t in triggers]
|
42
|
+
collision_allowed = collision_allowed and allowed
|
43
|
+
|
44
|
+
|
45
|
+
if (arbiter.shapes[0].collision_type == 0) or (arbiter.shapes[1].collision_type == 0):
|
46
|
+
collision_allowed = False
|
47
|
+
|
48
|
+
|
49
|
+
return collision_allowed
|
50
|
+
|
51
|
+
def _collision_separate(arbiter, space, data):
|
52
|
+
game = cast(Game, data['game'])
|
53
|
+
|
54
|
+
if arbiter.shapes in game._contact_pairs_set:
|
55
|
+
game._contact_pairs_set.remove(arbiter.shapes)
|
56
|
+
|
57
|
+
|
58
|
+
reverse_order = arbiter.shapes[1], arbiter.shapes[0]
|
59
|
+
if reverse_order in game._contact_pairs_set:
|
60
|
+
game._contact_pairs_set.remove(reverse_order)
|
61
|
+
|
62
|
+
|
63
|
+
class _CloneEventManager:
|
64
|
+
|
65
|
+
def __init__(self):
|
66
|
+
# TODO: removed sprites stay here forever
|
67
|
+
self.identical_sprites_and_triggers: List[Tuple[Set[Sprite], List[Event]]] = []
|
68
|
+
|
69
|
+
def new_trigger(self, sprite:Sprite, trigger:Event):
|
70
|
+
new_lineage = True
|
71
|
+
for identical_sprites, triggers in self.identical_sprites_and_triggers:
|
72
|
+
if sprite in identical_sprites:
|
73
|
+
new_lineage = False
|
74
|
+
triggers.append(trigger)
|
75
|
+
|
76
|
+
if new_lineage:
|
77
|
+
self.identical_sprites_and_triggers.append((set([sprite]), [trigger]))
|
78
|
+
|
79
|
+
|
80
|
+
def on_clone(self, old_sprite:Sprite, new_sprite:Sprite):
|
81
|
+
# so that the cloning of the cloned sprite will trigger the same event
|
82
|
+
for identical_sprites, triggers in self.identical_sprites_and_triggers:
|
83
|
+
if not old_sprite in identical_sprites:
|
84
|
+
continue
|
85
|
+
identical_sprites.add(new_sprite)
|
86
|
+
|
87
|
+
for t in triggers:
|
88
|
+
t.trigger(new_sprite)
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
class _SpriteEventDependencyManager:
|
93
|
+
|
94
|
+
def __init__(self):
|
95
|
+
|
96
|
+
self.sprites: Dict[Sprite, List[Union[Event, _ConditionInterface]]] = {}
|
97
|
+
|
98
|
+
def add_event(self, event: Union[_ConditionInterface, Event], sprites: Iterable[Sprite]):
|
99
|
+
"""
|
100
|
+
TODO: if the event is dependent to multiple sprites, the event will not be
|
101
|
+
completely dereferenced until all the sprites on which it depends are removed
|
102
|
+
|
103
|
+
"""
|
104
|
+
for s in sprites:
|
105
|
+
if not s in self.sprites:
|
106
|
+
self.sprites[s] = []
|
107
|
+
self.sprites[s].append(event)
|
108
|
+
|
109
|
+
|
110
|
+
def sprite_removal(self, sprite: Sprite):
|
111
|
+
|
112
|
+
to_remove = self.sprites.get(sprite)
|
113
|
+
if not to_remove:
|
114
|
+
return
|
115
|
+
|
116
|
+
for e in to_remove:
|
117
|
+
e.remove()
|
118
|
+
|
119
|
+
T = TypeVar('T')
|
120
|
+
"""@private"""
|
121
|
+
P = ParamSpec('P')
|
122
|
+
"""@private"""
|
123
|
+
|
124
|
+
class _SpecificEventEmitter(Generic[P]):
|
125
|
+
|
126
|
+
def __init__(self):
|
127
|
+
self.key2triggers: Dict[Any, List[Event[P]]] = {}
|
128
|
+
|
129
|
+
def add_event(self, key, trigger:Event[P]):
|
130
|
+
if not key in self.key2triggers:
|
131
|
+
self.key2triggers[key] = []
|
132
|
+
self.key2triggers[key].append(trigger)
|
133
|
+
|
134
|
+
|
135
|
+
def on_event(self, key, *args: P.args, **kwargs: P.kwargs):
|
136
|
+
if not key in self.key2triggers:
|
137
|
+
return
|
138
|
+
for t in self.key2triggers[key]:
|
139
|
+
t.trigger(*args, **kwargs)
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
class _SavedSpriteStateManager:
|
144
|
+
default_filename = "saved_sprite_states.json"
|
145
|
+
def __init__(self):
|
146
|
+
self.states: Dict[str, Dict[str, Any]] = {}
|
147
|
+
|
148
|
+
|
149
|
+
def save_sprite_states(self, all_sprite: Iterable[Sprite], filename=None):
|
150
|
+
"""
|
151
|
+
Save the x, y & direction of the sprites.
|
152
|
+
|
153
|
+
Usage:
|
154
|
+
```python
|
155
|
+
# main.py
|
156
|
+
from pyscratch import game
|
157
|
+
|
158
|
+
game.start() # when you close the game window, the game.start() function finishes.
|
159
|
+
game.save_sprite_states() # then this function will be run.
|
160
|
+
|
161
|
+
```
|
162
|
+
"""
|
163
|
+
loc = {}
|
164
|
+
for s in all_sprite:
|
165
|
+
loc[s.identifier] = dict(x=s.x, y=s.y, direction=s.direction)
|
166
|
+
|
167
|
+
if not filename:
|
168
|
+
filename = self.default_filename
|
169
|
+
#caller_file = Path(inspect.stack()[-1].filename)
|
170
|
+
|
171
|
+
json.dump(loc, open(filename, "w"))
|
172
|
+
print("Sprite states saved.")
|
173
|
+
|
174
|
+
def load_saved_state(self, filename=None):
|
175
|
+
|
176
|
+
if not filename:
|
177
|
+
filename = self.default_filename
|
178
|
+
|
179
|
+
filename = Path(filename)
|
180
|
+
if filename.exists():
|
181
|
+
self.states= json.load(open(filename, "r"))
|
182
|
+
|
183
|
+
|
184
|
+
def get_state_of(self, sprite_id):
|
185
|
+
|
186
|
+
return self.states.get(sprite_id)
|
187
|
+
|
188
|
+
|
189
|
+
class Game:
|
190
|
+
"""
|
191
|
+
This is the class that the `game` object belongs to. You cannot create another Game object.
|
192
|
+
To exit the game, either close the window, or to press the escape key (esc) by default
|
193
|
+
"""
|
194
|
+
|
195
|
+
_singleton_lock = False
|
196
|
+
def __init__(self):
|
197
|
+
"""@private"""
|
198
|
+
pygame.init()
|
199
|
+
|
200
|
+
assert not Game._singleton_lock, "Already instantiated."
|
201
|
+
Game._singleton_lock = True
|
202
|
+
|
203
|
+
# the screen is needed to load the images.
|
204
|
+
self._screen: pygame.Surface = pygame.display.set_mode((1280, 720), vsync=1)
|
205
|
+
|
206
|
+
self._space: pymunk.Space = pymunk.Space()
|
207
|
+
|
208
|
+
|
209
|
+
self._draw_options = DrawOptions(self._screen)
|
210
|
+
|
211
|
+
# sounds
|
212
|
+
self._mixer = pygame.mixer.init()
|
213
|
+
|
214
|
+
self._sounds = {}
|
215
|
+
|
216
|
+
# shared variables
|
217
|
+
self.shared_data: Dict[Any, Any] = {}
|
218
|
+
"""
|
219
|
+
A dictionary of variables shared across the entire game. You can put anything in it.
|
220
|
+
|
221
|
+
The access of the items can be done directly through the game object.
|
222
|
+
For example, `game['my_data'] = "hello"` is just an alias of `game.shared_data['my_data'] = "hello"`
|
223
|
+
|
224
|
+
Bare in mind that the order of executions of different events and different files is arbitrary.
|
225
|
+
Therefore if variable is defined in one event and accessed in another,
|
226
|
+
a KeyError may be raised because variables are only accessible after the definition.
|
227
|
+
|
228
|
+
Instead, the all the variable should be defined outside the event (before the game start),
|
229
|
+
and variables should be accessed only within events to guarantee its definition.
|
230
|
+
|
231
|
+
|
232
|
+
Example:
|
233
|
+
```python
|
234
|
+
from pyscratch import game
|
235
|
+
|
236
|
+
# same as `game.shared_data['score_left'] = 0`
|
237
|
+
game['score_left'] = 0
|
238
|
+
game['score_right'] = 0
|
239
|
+
|
240
|
+
def on_score(side):
|
241
|
+
if side == "left":
|
242
|
+
game['score_left'] += 1
|
243
|
+
else:
|
244
|
+
game['score_right'] += 1
|
245
|
+
|
246
|
+
print(f"Left score: {game['score_left']}")
|
247
|
+
print(f"Right score: {game['score_right']}")
|
248
|
+
|
249
|
+
game.when_received_message('score').add_handler(on_score)
|
250
|
+
game.broadcast_message('on_score', 'left')
|
251
|
+
```
|
252
|
+
"""
|
253
|
+
|
254
|
+
# sprite event dependency manager
|
255
|
+
self._sprite_event_dependency_manager = _SpriteEventDependencyManager()
|
256
|
+
|
257
|
+
#
|
258
|
+
self._clone_event_manager = _CloneEventManager()
|
259
|
+
"""@private"""
|
260
|
+
|
261
|
+
# collision detection
|
262
|
+
self._trigger_to_collision_pairs: Dict[Event, Tuple[Sprite, Sprite]] = {}
|
263
|
+
|
264
|
+
self._collision_type_pair_to_trigger: Dict[Tuple[int, int], List[Event]] = {}
|
265
|
+
|
266
|
+
self._collision_type_to_trigger: Dict[int, Tuple[bool, List[Event]]] = {}
|
267
|
+
|
268
|
+
self._contact_pairs_set: Set[Tuple[pymunk.Shape, pymunk.Shape]] = set()
|
269
|
+
|
270
|
+
|
271
|
+
self._collision_handler = self._space.add_default_collision_handler()
|
272
|
+
|
273
|
+
self._collision_handler.data['game'] = self
|
274
|
+
self._collision_handler.begin = _collision_begin
|
275
|
+
self._collision_handler.separate = _collision_separate
|
276
|
+
|
277
|
+
# sprites updating and drawing
|
278
|
+
self._all_sprites = pygame.sprite.Group()
|
279
|
+
|
280
|
+
self._all_sprites_to_show = pygame.sprite.LayeredUpdates()
|
281
|
+
|
282
|
+
|
283
|
+
# # scheduled jobs
|
284
|
+
# self.pre_scheduled_jobs = []
|
285
|
+
# self.scheduled_jobs = []
|
286
|
+
|
287
|
+
|
288
|
+
self._all_pygame_events = []
|
289
|
+
|
290
|
+
self._all_triggers: List[Event] = [] # these are to be executed every iteration
|
291
|
+
|
292
|
+
self._all_conditions: List[_ConditionInterface] = [] # these are to be checked every iteration
|
293
|
+
|
294
|
+
#self.all_forever_jobs: List[Callable[[], None]] = []
|
295
|
+
self._all_message_subscriptions: Dict[str, List[Event]] = {}
|
296
|
+
|
297
|
+
# key events
|
298
|
+
key_event = self.create_pygame_event([pygame.KEYDOWN, pygame.KEYUP])
|
299
|
+
key_event.add_handler(self.__key_event_handler)
|
300
|
+
self._all_simple_key_triggers: List[Event] = [] # these are to be triggered by self.__key_event_handler only
|
301
|
+
|
302
|
+
# mouse dragging event
|
303
|
+
self._dragged_sprite = None
|
304
|
+
self._drag_offset = 0, 0
|
305
|
+
self.__clicked_sprite = None
|
306
|
+
self._sprite_click_release_trigger:Dict[Sprite, List[Event]] = {} #TODO: need to be able to destory the trigger here when the sprite is destoryed
|
307
|
+
|
308
|
+
self._sprite_click_trigger:Dict[Sprite, List[Event]] = {} #TODO: need to be able to destory the trigger here when the sprite is destoryed
|
309
|
+
self._mouse_drag_trigger = self.create_pygame_event([pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION])
|
310
|
+
self._mouse_drag_trigger.add_handler(self.__mouse_drag_handler)
|
311
|
+
|
312
|
+
## Backdrops
|
313
|
+
self.backdrops: List[pygame.Surface] = []
|
314
|
+
"""A list of all the loaded backdrop images. You will not need to interact with this property directly."""
|
315
|
+
|
316
|
+
self.__screen_width: int = 0
|
317
|
+
self.__screen_height: int = 0
|
318
|
+
self.__framerate: float = 0
|
319
|
+
|
320
|
+
|
321
|
+
self.__backdrop_index = None
|
322
|
+
self._backdrop_change_triggers: List[Event] = []
|
323
|
+
|
324
|
+
self._top_edge: Sprite
|
325
|
+
self._left_edge: Sprite
|
326
|
+
self._bottom_edge: Sprite
|
327
|
+
self._right_edge: Sprite
|
328
|
+
|
329
|
+
|
330
|
+
## start event
|
331
|
+
self._game_start_triggers: List[Event] = []
|
332
|
+
|
333
|
+
## global timer event
|
334
|
+
self._global_timer_triggers: List[Event] = []
|
335
|
+
|
336
|
+
|
337
|
+
self._current_time_ms: float = 0
|
338
|
+
|
339
|
+
self._specific_key_event_emitter: _SpecificEventEmitter[str] = _SpecificEventEmitter()
|
340
|
+
|
341
|
+
self._specific_backdrop_event_emitter: _SpecificEventEmitter[[]] = _SpecificEventEmitter()
|
342
|
+
|
343
|
+
self.max_number_sprite = 1000
|
344
|
+
"""The maximum number of sprites in the game. Adding more sprites will lead to an error to prevent freezing. Default to 1000."""
|
345
|
+
|
346
|
+
self._sprite_count_per_file: Dict[str, int] = {}
|
347
|
+
|
348
|
+
self.__start = False
|
349
|
+
"""set to false to end the loop"""
|
350
|
+
|
351
|
+
|
352
|
+
self.update_screen_mode()
|
353
|
+
|
354
|
+
self._saved_states_manager = _SavedSpriteStateManager()
|
355
|
+
self.__sprite_last_clicked_for_removal: Optional[Sprite] = None
|
356
|
+
self.__started_interactive = False
|
357
|
+
|
358
|
+
|
359
|
+
def __key_event_handler(self, e):
|
360
|
+
up_or_down = 'down' if e.type == pygame.KEYDOWN else 'up'
|
361
|
+
keyname = pygame.key.name(e.key)
|
362
|
+
|
363
|
+
self._specific_key_event_emitter.on_event(keyname, up_or_down)
|
364
|
+
|
365
|
+
for t in self._all_simple_key_triggers:
|
366
|
+
t.trigger(keyname, up_or_down)
|
367
|
+
|
368
|
+
def __getitem__(self, key):
|
369
|
+
return self.shared_data[key]
|
370
|
+
|
371
|
+
def __setitem__(self, k, v):
|
372
|
+
self.shared_data[k] = v
|
373
|
+
|
374
|
+
|
375
|
+
def __mouse_drag_handler(self, e):
|
376
|
+
|
377
|
+
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
|
378
|
+
|
379
|
+
for s in reversed(list(self._all_sprites_to_show)):
|
380
|
+
if TYPE_CHECKING:
|
381
|
+
s = cast(Sprite, s)
|
382
|
+
|
383
|
+
# click is on the top sprite only
|
384
|
+
if s.is_touching_mouse():
|
385
|
+
self.__clicked_sprite = s
|
386
|
+
self.__sprite_last_clicked_for_removal = s
|
387
|
+
|
388
|
+
for t in self._sprite_click_trigger[s]:
|
389
|
+
t.trigger()
|
390
|
+
|
391
|
+
if not s.draggable:
|
392
|
+
break
|
393
|
+
|
394
|
+
s._set_is_dragging (True)
|
395
|
+
self._dragged_sprite = s
|
396
|
+
offset_x = s._body.position[0] - e.pos[0]
|
397
|
+
offset_y = s._body.position[1] - e.pos[1]
|
398
|
+
self._drag_offset = offset_x, offset_y
|
399
|
+
break
|
400
|
+
|
401
|
+
|
402
|
+
|
403
|
+
elif e.type == pygame.MOUSEBUTTONUP and e.button == 1:
|
404
|
+
if self._dragged_sprite:
|
405
|
+
self._dragged_sprite._set_is_dragging(False)
|
406
|
+
self._dragged_sprite = None
|
407
|
+
|
408
|
+
# TODO: what happens here?? why AND?
|
409
|
+
if self.__clicked_sprite and (temp:= self._sprite_click_release_trigger.get(self.__clicked_sprite)):
|
410
|
+
#temp = self._sprite_click_release_trigger.get(self.__clicked_sprite)
|
411
|
+
#if temp:
|
412
|
+
for t in temp:
|
413
|
+
t.trigger()
|
414
|
+
self.__clicked_sprite = None
|
415
|
+
|
416
|
+
elif e.type == pygame.MOUSEMOTION and self._dragged_sprite:
|
417
|
+
x = e.pos[0] + self._drag_offset[0]
|
418
|
+
y = e.pos[1] + self._drag_offset[1]
|
419
|
+
self._dragged_sprite.set_xy((x,y))
|
420
|
+
|
421
|
+
|
422
|
+
|
423
|
+
def update_screen_mode(self, *arg, **kwargs):
|
424
|
+
"""
|
425
|
+
Update the screen, taking the arguments for
|
426
|
+
[`pygame.display.set_mode`](https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode).
|
427
|
+
|
428
|
+
|
429
|
+
Use this method to change the screen size:
|
430
|
+
|
431
|
+
`game.update_screen_mode((SCREEN_WIDTH, SCREEN_HEIGHT))`
|
432
|
+
"""
|
433
|
+
self.__screen_args = arg
|
434
|
+
self.__screen_kwargs = kwargs
|
435
|
+
|
436
|
+
@property
|
437
|
+
def screen_width(self):
|
438
|
+
"""The width of the screen. Not available until the game is started (should not be referenced outside events)."""
|
439
|
+
return self.__screen_width
|
440
|
+
|
441
|
+
@property
|
442
|
+
def screen_height(self):
|
443
|
+
"""The height of the screen. Not available until the game is started (should not referenced outside events)."""
|
444
|
+
return self.__screen_height
|
445
|
+
|
446
|
+
@property
|
447
|
+
def framerate(self):
|
448
|
+
"""The frame rate of the game. Not available until the game is started"""
|
449
|
+
return self.__framerate
|
450
|
+
|
451
|
+
def _do_autoremove(self):
|
452
|
+
for s in self._all_sprites:
|
453
|
+
if ((s.x < -s.oob_limit) or
|
454
|
+
(s.x > (s.oob_limit + self.__screen_width)) or
|
455
|
+
(s.y < -s.oob_limit) or
|
456
|
+
(s.y > (s.oob_limit + self.__screen_height))
|
457
|
+
):
|
458
|
+
#print(s.x, s.y)
|
459
|
+
#print(s.oob_limit + self.__screen_width, s.oob_limit + self.__screen_height)
|
460
|
+
s.remove()
|
461
|
+
print(f"{s} is removed for going out of boundary above the specified limit.")
|
462
|
+
|
463
|
+
def _check_alive(self):
|
464
|
+
|
465
|
+
last_frame_time = 0
|
466
|
+
while True:
|
467
|
+
|
468
|
+
for i in range(10):
|
469
|
+
time.sleep(.2)
|
470
|
+
if not self.__start:
|
471
|
+
return
|
472
|
+
if not self._current_time_ms > last_frame_time:
|
473
|
+
print('Stucked in the same frame for more than 1 second. This can happen when an error occur or you are in a infinite loop without yielding. ')
|
474
|
+
os._exit(1)
|
475
|
+
|
476
|
+
last_frame_time = self._current_time_ms
|
477
|
+
|
478
|
+
def start_interactive(self, *args, sprite_removal_key='backspace', **kwargs):
|
479
|
+
"""
|
480
|
+
@private
|
481
|
+
Start the game on another thread for jupyter notebook (experimental)
|
482
|
+
"""
|
483
|
+
if self.__started_interactive:
|
484
|
+
raise RuntimeError("The game must not be restarted. Please restart the kernal. ")
|
485
|
+
self.__started_interactive = True
|
486
|
+
|
487
|
+
def remove_sprite(_):
|
488
|
+
if s:=self.__clicked_sprite:
|
489
|
+
s.remove()
|
490
|
+
|
491
|
+
self.when_key_pressed(sprite_removal_key).add_handler(remove_sprite)
|
492
|
+
|
493
|
+
t = threading.Thread(target=self.start, args=args, kwargs=kwargs)
|
494
|
+
t.start()
|
495
|
+
self.__screen_thread = t
|
496
|
+
|
497
|
+
|
498
|
+
return t
|
499
|
+
|
500
|
+
def stop(self):
|
501
|
+
"""
|
502
|
+
@private
|
503
|
+
"""
|
504
|
+
self.__start = False
|
505
|
+
if self.__started_interactive:
|
506
|
+
#self.__started_interactive = False
|
507
|
+
pygame.display.quit()
|
508
|
+
|
509
|
+
|
510
|
+
def start(
|
511
|
+
self,
|
512
|
+
framerate=30,
|
513
|
+
sim_step_min=300,
|
514
|
+
debug_draw=False,
|
515
|
+
event_count=False,
|
516
|
+
show_mouse_position: Optional[bool]=None,
|
517
|
+
exit_key: Optional[str]="escape",
|
518
|
+
saved_state_file=None,
|
519
|
+
print_fps = False,
|
520
|
+
use_frame_time = False
|
521
|
+
):
|
522
|
+
"""
|
523
|
+
Start the game.
|
524
|
+
|
525
|
+
Parameters
|
526
|
+
---
|
527
|
+
framerate : int
|
528
|
+
The number of frames per second
|
529
|
+
|
530
|
+
sim_step_min: int
|
531
|
+
The number of physics steps per second. Increase this value if the physics is unstable and decrease it if the game runs slow.
|
532
|
+
|
533
|
+
debug_draw: bool
|
534
|
+
Whether or not to draw the collision shape for debugging purposes
|
535
|
+
|
536
|
+
event_count: bool
|
537
|
+
Whether or not to print out the number of active events for debugging purposes
|
538
|
+
|
539
|
+
show_mouse_position: bool
|
540
|
+
Whether or not to show the mouse position in the buttom-right corner
|
541
|
+
|
542
|
+
exit_key: Optional[str]
|
543
|
+
Very useful if you are working on a fullscreen game
|
544
|
+
Set to None to disable it.
|
545
|
+
|
546
|
+
saved_state_file: Optional[str]
|
547
|
+
The path of the saved state. Default location will be used if set to None.
|
548
|
+
|
549
|
+
print_fps: bool
|
550
|
+
Whether or not to print the fps
|
551
|
+
|
552
|
+
use_frame_time: bool
|
553
|
+
Use the number of frames to define game time. Note: highly experimental.
|
554
|
+
|
555
|
+
"""
|
556
|
+
|
557
|
+
if not (len(self.__screen_args) or len(self.__screen_kwargs)):
|
558
|
+
self.__screen_kwargs = dict(size=(1280, 720))
|
559
|
+
|
560
|
+
self._screen = pygame.display.set_mode(*self.__screen_args, **self.__screen_kwargs)
|
561
|
+
|
562
|
+
self._saved_states_manager.load_saved_state(saved_state_file)
|
563
|
+
self.__framerate = framerate
|
564
|
+
self.__screen_width = self._screen.get_width()
|
565
|
+
self.__screen_height = self._screen.get_height()
|
566
|
+
|
567
|
+
guide_lines_font = pygame.font.Font(None, 30)
|
568
|
+
|
569
|
+
clock = pygame.time.Clock()
|
570
|
+
|
571
|
+
|
572
|
+
|
573
|
+
draw_every_n_step = sim_step_min//framerate+1
|
574
|
+
|
575
|
+
self._current_time_ms = 0
|
576
|
+
|
577
|
+
if exit_key:
|
578
|
+
self.when_key_pressed(exit_key).add_handler(lambda _: self.stop())
|
579
|
+
|
580
|
+
self.create_pygame_event([pygame.QUIT]).add_handler(lambda _: self.stop())
|
581
|
+
|
582
|
+
for t in self._game_start_triggers:
|
583
|
+
t.trigger()
|
584
|
+
|
585
|
+
cleanup_period = 2*framerate
|
586
|
+
loop_count = 0
|
587
|
+
|
588
|
+
threading.Thread(target=self._check_alive).start()
|
589
|
+
frame_interval = 1000/framerate
|
590
|
+
|
591
|
+
self.__start = True
|
592
|
+
while self.__start:
|
593
|
+
if print_fps:
|
594
|
+
print(f"FPS: {clock.get_fps()}")
|
595
|
+
loop_count += 1
|
596
|
+
|
597
|
+
dt = clock.tick(framerate)
|
598
|
+
self._current_time_ms += frame_interval if use_frame_time else dt
|
599
|
+
for i in range(draw_every_n_step):
|
600
|
+
self._space.step(dt/draw_every_n_step)
|
601
|
+
|
602
|
+
self._all_pygame_events = pygame.event.get()
|
603
|
+
|
604
|
+
|
605
|
+
# check conditions
|
606
|
+
for c in self._all_conditions:
|
607
|
+
c._check()
|
608
|
+
|
609
|
+
# execute
|
610
|
+
for t in self._all_triggers:
|
611
|
+
t._handle_all(self._current_time_ms)
|
612
|
+
# TODO: is it possible to remove t in the self.all_triggers here?
|
613
|
+
t._generators_proceed(self._current_time_ms)
|
614
|
+
|
615
|
+
# clean up
|
616
|
+
self._all_conditions = list(filter(lambda t: t.stay_active, self._all_conditions))
|
617
|
+
self._all_simple_key_triggers = list(filter(lambda t: t.stay_active, self._all_simple_key_triggers))
|
618
|
+
self._all_triggers = list(filter(lambda t: t.stay_active, self._all_triggers))
|
619
|
+
|
620
|
+
|
621
|
+
if event_count:
|
622
|
+
print("all_conditions", len(self._all_conditions))
|
623
|
+
print("all_triggers", len(self._all_triggers))
|
624
|
+
print("all sprite", len(self._all_sprites))
|
625
|
+
# print("all_simple_key_triggers", len(self.all_simple_key_triggers))
|
626
|
+
|
627
|
+
# Drawing
|
628
|
+
|
629
|
+
#self._screen.fill((30, 30, 30))
|
630
|
+
if not (self.__backdrop_index is None):
|
631
|
+
self._screen.blit(self.backdrops[self.__backdrop_index], (0, 0))
|
632
|
+
else:
|
633
|
+
self._screen.fill((255,255,255))
|
634
|
+
helper._draw_guide_lines(self._screen, guide_lines_font, 100, 500)
|
635
|
+
|
636
|
+
if show_mouse_position is None:
|
637
|
+
helper._show_mouse_position(self._screen, guide_lines_font)
|
638
|
+
|
639
|
+
if debug_draw:
|
640
|
+
self._space.debug_draw(self._draw_options)
|
641
|
+
|
642
|
+
self._all_sprites.update()
|
643
|
+
#self._all_sprites_to_show.update()
|
644
|
+
self._all_sprites_to_show.draw(self._screen)
|
645
|
+
|
646
|
+
if show_mouse_position:
|
647
|
+
helper._show_mouse_position(self._screen, guide_lines_font)
|
648
|
+
|
649
|
+
|
650
|
+
pygame.display.flip()
|
651
|
+
if not loop_count % cleanup_period:
|
652
|
+
self._do_autoremove()
|
653
|
+
|
654
|
+
def _get_saved_state(self, sprite_id):
|
655
|
+
return self._saved_states_manager.get_state_of(sprite_id)
|
656
|
+
|
657
|
+
def save_sprite_states(self):
|
658
|
+
"""
|
659
|
+
*EXTENDED FEATURE, EXPERIMENTAL*
|
660
|
+
|
661
|
+
Save the x, y & direction of the sprites.
|
662
|
+
|
663
|
+
To retrieve the states of the sprites, refer to [`retrieve_saved_state`](./sprite#Sprite.retrieve_saved_state)
|
664
|
+
|
665
|
+
|
666
|
+
Usage:
|
667
|
+
```python
|
668
|
+
from pyscratch import game
|
669
|
+
|
670
|
+
# Start the game. The program stays in this line until the game window is closed.
|
671
|
+
game.start(60)
|
672
|
+
|
673
|
+
# The program will get to this line when the game window is closed normally (not due to an error).
|
674
|
+
game.save_sprite_states()
|
675
|
+
```
|
676
|
+
|
677
|
+
"""
|
678
|
+
self._saved_states_manager.save_sprite_states(self._all_sprites)
|
679
|
+
|
680
|
+
def load_sound(self, key: str, path: str) :
|
681
|
+
"""
|
682
|
+
Load the sound given a path, and index it with the key so it can be played later by `play_sound`
|
683
|
+
|
684
|
+
Example:
|
685
|
+
```python
|
686
|
+
game.load_sound('sound1', 'path/to/sound.wav')
|
687
|
+
game.play_sound('sound1', volume=0.5)
|
688
|
+
```
|
689
|
+
"""
|
690
|
+
if key in self._sounds:
|
691
|
+
raise KeyError(f'{key} already loaded. Choose a different key name.')
|
692
|
+
|
693
|
+
self._sounds[key] = pygame.mixer.Sound(path)
|
694
|
+
|
695
|
+
def play_sound(self, key:str, volume=1.0):
|
696
|
+
"""
|
697
|
+
Play the sound given a key.
|
698
|
+
This method does not wait for the sound to finish playing.
|
699
|
+
|
700
|
+
Example:
|
701
|
+
```python
|
702
|
+
game.load_sound('sound1', 'path/to/sound.wav')
|
703
|
+
game.play_sound('sound1', volume=0.5)
|
704
|
+
```
|
705
|
+
"""
|
706
|
+
s = self._sounds[key]
|
707
|
+
s.set_volume(volume)
|
708
|
+
s.play()
|
709
|
+
|
710
|
+
|
711
|
+
def read_timer(self) -> float:
|
712
|
+
"""get the time (in seconds) since the game started."""
|
713
|
+
return self._current_time_ms/1000
|
714
|
+
|
715
|
+
|
716
|
+
def set_gravity(self, xy: Tuple[float, float]):
|
717
|
+
"""
|
718
|
+
@private
|
719
|
+
*EXTENDED FEATURE*
|
720
|
+
|
721
|
+
Change the gravity of the space. Works for sprites with dynamic body type only, which is not the default.
|
722
|
+
It will NOT work unless you explicitly make the sprite to have a dynamic body.
|
723
|
+
"""
|
724
|
+
self._space.gravity = xy
|
725
|
+
|
726
|
+
def _new_sprite_of_file(self, caller_file):
|
727
|
+
if not caller_file in self._sprite_count_per_file:
|
728
|
+
self._sprite_count_per_file[caller_file] = 0
|
729
|
+
else:
|
730
|
+
self._sprite_count_per_file[caller_file] += 1
|
731
|
+
|
732
|
+
return self._sprite_count_per_file[caller_file]
|
733
|
+
|
734
|
+
|
735
|
+
def _add_sprite(self, sprite: Sprite, to_show=True, caller_file=None):
|
736
|
+
|
737
|
+
self._all_sprites.add(sprite)
|
738
|
+
if len(self._all_sprites) > self.max_number_sprite:
|
739
|
+
raise RuntimeError('Reached the maximum number sprite. ')
|
740
|
+
#self._space.add(sprite.body, sprite.shape)
|
741
|
+
self._sprite_click_trigger[sprite] = []
|
742
|
+
if to_show:
|
743
|
+
self._all_sprites_to_show.add(sprite)
|
744
|
+
sprite.update()
|
745
|
+
|
746
|
+
if self.__started_interactive:
|
747
|
+
sprite.set_draggable(True)
|
748
|
+
|
749
|
+
return self._new_sprite_of_file(caller_file)
|
750
|
+
|
751
|
+
def _cleanup_old_shape(self, old_shape):
|
752
|
+
|
753
|
+
remove_list = []
|
754
|
+
for pair in self._contact_pairs_set:
|
755
|
+
if old_shape in pair:
|
756
|
+
remove_list.append(pair)
|
757
|
+
|
758
|
+
for r in remove_list:
|
759
|
+
self._contact_pairs_set.remove(r)
|
760
|
+
|
761
|
+
def _remove_sprite(self, sprite: Sprite):
|
762
|
+
"""
|
763
|
+
Remove the sprite from the game.
|
764
|
+
|
765
|
+
You can use the alias `Sprite.remove()` to do the same.
|
766
|
+
"""
|
767
|
+
|
768
|
+
self._all_sprites.remove(sprite)
|
769
|
+
self._all_sprites_to_show.remove(sprite)
|
770
|
+
|
771
|
+
self._trigger_to_collision_pairs = {k: v for k, v in self._trigger_to_collision_pairs.items() if not sprite in v}
|
772
|
+
|
773
|
+
|
774
|
+
self._cleanup_old_shape(sprite._shape)
|
775
|
+
|
776
|
+
try:
|
777
|
+
self._space.remove(sprite._body, sprite._shape)
|
778
|
+
except:
|
779
|
+
print('removing non-existing shape or body')
|
780
|
+
|
781
|
+
self._sprite_event_dependency_manager.sprite_removal(sprite)
|
782
|
+
|
783
|
+
|
784
|
+
def _show_sprite(self, sprite: Sprite):
|
785
|
+
"""
|
786
|
+
Show the sprite.
|
787
|
+
|
788
|
+
You can use the alias `sprite.show()` to do the same.
|
789
|
+
"""
|
790
|
+
self._all_sprites_to_show.add(sprite)
|
791
|
+
|
792
|
+
def _hide_sprite(self, sprite: Sprite):
|
793
|
+
"""
|
794
|
+
Hide the sprite.
|
795
|
+
|
796
|
+
You can use the alias `sprite.hide()` to do the same.
|
797
|
+
"""
|
798
|
+
self._all_sprites_to_show.remove(sprite)
|
799
|
+
|
800
|
+
def bring_to_front(self, sprite: Sprite):
|
801
|
+
"""
|
802
|
+
Bring the sprite to the front.
|
803
|
+
Analogous to the "go to [front] layer" block in Scratch
|
804
|
+
"""
|
805
|
+
self._all_sprites_to_show.move_to_front(sprite)
|
806
|
+
|
807
|
+
def move_to_back(self, sprite: Sprite):
|
808
|
+
"""
|
809
|
+
Move the sprite to the back.
|
810
|
+
Analogous to the "go to [back] layer" block in Scratch
|
811
|
+
"""
|
812
|
+
self._all_sprites_to_show.move_to_back(sprite)
|
813
|
+
|
814
|
+
def change_layer(self, sprite: Sprite, layer: int):
|
815
|
+
"""
|
816
|
+
Bring the sprite to a specific layer.
|
817
|
+
"""
|
818
|
+
self._all_sprites_to_show.change_layer(sprite, layer)
|
819
|
+
|
820
|
+
|
821
|
+
def change_layer_by(self, sprite: Sprite, by: int):
|
822
|
+
"""
|
823
|
+
Analogous to the "go to [forward/backward] [N] layer" block in Scratch
|
824
|
+
"""
|
825
|
+
layer = self._all_sprites_to_show.get_layer_of_sprite(sprite)
|
826
|
+
self._all_sprites_to_show.change_layer(sprite, layer + by)
|
827
|
+
|
828
|
+
def get_layer_of_sprite(self, sprite: Sprite):
|
829
|
+
"""
|
830
|
+
Returns the layer number of the given sprite
|
831
|
+
"""
|
832
|
+
self._all_sprites_to_show.get_layer_of_sprite(sprite)
|
833
|
+
|
834
|
+
def set_backdrops(self, images: List[pygame.Surface]):
|
835
|
+
"""
|
836
|
+
Set the list of all available backdrops. This function is meant to be run before the game start.
|
837
|
+
|
838
|
+
Example:
|
839
|
+
```python
|
840
|
+
# load the image into python
|
841
|
+
background_image = pysc.load_image('assets/my_background.jpg')
|
842
|
+
background_image2 = pysc.load_image('assets/my_background2.jpg')
|
843
|
+
background_image3 = pysc.load_image('assets/my_background3.jpg')
|
844
|
+
|
845
|
+
# pass in a list of all the available backdrops.
|
846
|
+
pysc.game.set_backdrops([background_image, background_image2, background_image3])
|
847
|
+
|
848
|
+
# choose the backdrop at index 1 (background_image2)
|
849
|
+
pysc.game.switch_backdrop(1)
|
850
|
+
```
|
851
|
+
"""
|
852
|
+
self.backdrops = images
|
853
|
+
|
854
|
+
@property
|
855
|
+
def backdrop_index(self):
|
856
|
+
"""
|
857
|
+
The index of the current backdrops. For example, if you do `game.switch_backdrop(0)`, `game.backdrop_index` would be `0`.
|
858
|
+
"""
|
859
|
+
return self.__backdrop_index
|
860
|
+
|
861
|
+
def switch_backdrop(self, index:Optional[int]=None):
|
862
|
+
"""
|
863
|
+
Change the backdrop by specifying the index of the backdrop.
|
864
|
+
"""
|
865
|
+
|
866
|
+
if index != self.__backdrop_index:
|
867
|
+
self.__backdrop_index = index
|
868
|
+
for t in self._backdrop_change_triggers:
|
869
|
+
t.trigger(index)
|
870
|
+
self._specific_backdrop_event_emitter.on_event(self.__backdrop_index)
|
871
|
+
|
872
|
+
def next_backdrop(self):
|
873
|
+
"""
|
874
|
+
Switch to the next backdrop.
|
875
|
+
"""
|
876
|
+
if not self.__backdrop_index is None:
|
877
|
+
self.switch_backdrop((self.__backdrop_index+1) % len(self.backdrops))
|
878
|
+
|
879
|
+
# all events
|
880
|
+
|
881
|
+
## scratch events
|
882
|
+
|
883
|
+
def when_game_start(self, associated_sprites : Iterable[Sprite]=[])->Event[[]]:
|
884
|
+
"""
|
885
|
+
It is recommended to use the `Sprite.when_game_start` alias instead of this method,
|
886
|
+
so you don't need to specify the `associated_sprites` in every event.
|
887
|
+
|
888
|
+
Returns an `Event` that is triggered when you call `game.start`.
|
889
|
+
The event handler does not take in any parameter.
|
890
|
+
|
891
|
+
Parameters
|
892
|
+
---
|
893
|
+
associated_sprites: List[Sprite]
|
894
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
895
|
+
"""
|
896
|
+
|
897
|
+
|
898
|
+
t = self._create_event(associated_sprites)
|
899
|
+
self._game_start_triggers.append(t)
|
900
|
+
|
901
|
+
if TYPE_CHECKING:
|
902
|
+
def sample_callback()-> Any:
|
903
|
+
return
|
904
|
+
t = _declare_callback_type(t, sample_callback)
|
905
|
+
return t
|
906
|
+
|
907
|
+
|
908
|
+
def when_any_key_pressed(self, associated_sprites : Iterable[Sprite]=[]) -> Event[[str, str]]:
|
909
|
+
"""
|
910
|
+
It is recommended to use the `Sprite.when_any_key_pressed` alias instead of this method,
|
911
|
+
so you don't need to specify the `associated_sprites` in every event.
|
912
|
+
|
913
|
+
Returns an `Event` that is triggered when a key is pressed or released.
|
914
|
+
|
915
|
+
The event handler have to take two parameters:
|
916
|
+
- **key** (str): The key that is pressed. For example, 'w', 'd', 'left', 'right', 'space'.
|
917
|
+
Uses [pygame.key.key_code](https://www.pygame.org/docs/ref/key.html#pygame.key.key_code) under the hood.
|
918
|
+
|
919
|
+
- **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
|
920
|
+
|
921
|
+
Parameters
|
922
|
+
---
|
923
|
+
associated_sprites: List[Sprite]
|
924
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
925
|
+
|
926
|
+
"""
|
927
|
+
t = self._create_event(associated_sprites)
|
928
|
+
self._all_simple_key_triggers.append(t)
|
929
|
+
|
930
|
+
if TYPE_CHECKING:
|
931
|
+
def sample_callback(key:str, updown:str)-> Any:
|
932
|
+
return
|
933
|
+
# this way the naming of the parameters is constrained too
|
934
|
+
t = _declare_callback_type(t, sample_callback)
|
935
|
+
#t = cast(Trigger[[str, str]], t)
|
936
|
+
|
937
|
+
return t
|
938
|
+
|
939
|
+
def when_key_pressed(self, key, associated_sprites : Iterable[Sprite]=[])-> Event[[str]]:
|
940
|
+
"""
|
941
|
+
It is recommended to use the `Sprite.when_key_pressed` alias instead of this method,
|
942
|
+
so you don't need to specify the `associated_sprites` in every event.
|
943
|
+
|
944
|
+
Returns an `Event` that is triggered when a specific key is pressed or released.
|
945
|
+
|
946
|
+
The event handler have to take one parameter:
|
947
|
+
- **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
|
948
|
+
|
949
|
+
Parameters
|
950
|
+
---
|
951
|
+
key: str
|
952
|
+
The key that triggers the event. For example, 'w', 'd', 'left', 'right', 'space'.
|
953
|
+
Uses [pygame.key.key_code](https://www.pygame.org/docs/ref/key.html#pygame.key.key_code) under the hood.
|
954
|
+
|
955
|
+
associated_sprites: List[Sprite]
|
956
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
957
|
+
"""
|
958
|
+
|
959
|
+
t = self._create_event(associated_sprites)
|
960
|
+
|
961
|
+
if TYPE_CHECKING:
|
962
|
+
def sample_callback(updown:str)-> Any:
|
963
|
+
return
|
964
|
+
# this way the naming of the parameters is constrained too
|
965
|
+
t = _declare_callback_type(t, sample_callback)
|
966
|
+
|
967
|
+
self._specific_key_event_emitter.add_event(key, t)
|
968
|
+
return t
|
969
|
+
|
970
|
+
def when_this_sprite_clicked(self, sprite, other_associated_sprites: Iterable[Sprite]=[]) -> Event[[]]:
|
971
|
+
"""
|
972
|
+
It is recommended to use the `Sprite.when_this_sprite_clicked` alias instead of this method.
|
973
|
+
|
974
|
+
Returns an `Event` that is triggered when the given sprite is clicked by mouse.
|
975
|
+
The event handler does not take in any parameter.
|
976
|
+
|
977
|
+
Parameters
|
978
|
+
---
|
979
|
+
sprite: Sprite
|
980
|
+
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
|
981
|
+
it does not need to be included in `other_assoicated_sprite`
|
982
|
+
|
983
|
+
other_associated_sprites: List[Sprite]
|
984
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
985
|
+
"""
|
986
|
+
|
987
|
+
t = self._create_event(set(list(other_associated_sprites)+[sprite]))
|
988
|
+
|
989
|
+
if not sprite in self._sprite_click_trigger:
|
990
|
+
self._sprite_click_trigger[sprite] = []
|
991
|
+
|
992
|
+
self._sprite_click_trigger[sprite].append(t)
|
993
|
+
if TYPE_CHECKING:
|
994
|
+
def sample_callback()-> Any:
|
995
|
+
return
|
996
|
+
t = _declare_callback_type(t, sample_callback)
|
997
|
+
return t
|
998
|
+
|
999
|
+
|
1000
|
+
def when_this_sprite_click_released(self, sprite, other_associated_sprites: Iterable[Sprite]=[]) -> Event[[]]:
|
1001
|
+
"""
|
1002
|
+
The alias `Sprite.when_this_sprite_click_released` is not yet implemented.
|
1003
|
+
|
1004
|
+
Returns an `Event` that is triggered when the mouse click of the given sprite is released.
|
1005
|
+
The event handler does not take in any parameter.
|
1006
|
+
|
1007
|
+
Parameters
|
1008
|
+
---
|
1009
|
+
sprite: Sprite
|
1010
|
+
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
|
1011
|
+
it does not need to be included in `other_assoicated_sprite`
|
1012
|
+
|
1013
|
+
other_associated_sprites: List[Sprite]
|
1014
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1015
|
+
"""
|
1016
|
+
|
1017
|
+
t = self._create_event(set(list(other_associated_sprites)+[sprite]))
|
1018
|
+
|
1019
|
+
if not sprite in self._sprite_click_release_trigger:
|
1020
|
+
self._sprite_click_release_trigger[sprite] = []
|
1021
|
+
|
1022
|
+
self._sprite_click_release_trigger[sprite].append(t)
|
1023
|
+
if TYPE_CHECKING:
|
1024
|
+
def sample_callback()-> Any:
|
1025
|
+
return
|
1026
|
+
t = _declare_callback_type(t, sample_callback)
|
1027
|
+
return t
|
1028
|
+
|
1029
|
+
|
1030
|
+
def when_backdrop_switched(self, backdrop_index, associated_sprites : Iterable[Sprite]=[]) -> Event[[]]:
|
1031
|
+
"""
|
1032
|
+
It is recommended to use the `Sprite.when_backdrop_switched` alias instead of this method,
|
1033
|
+
so you don't need to specify the `associated_sprites` in every event.
|
1034
|
+
|
1035
|
+
Returns an `Event` that is triggered when the game is switched to a backdrop at `backdrop_index`.
|
1036
|
+
|
1037
|
+
The event handler does not take in any parameter.
|
1038
|
+
|
1039
|
+
Parameters
|
1040
|
+
---
|
1041
|
+
backdrop_index: int
|
1042
|
+
The index of the backdrop
|
1043
|
+
|
1044
|
+
associated_sprites: List[Sprite]
|
1045
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1046
|
+
"""
|
1047
|
+
|
1048
|
+
|
1049
|
+
t = self._create_event(associated_sprites)
|
1050
|
+
|
1051
|
+
if TYPE_CHECKING:
|
1052
|
+
def sample_callback()-> Any:
|
1053
|
+
return
|
1054
|
+
t = _declare_callback_type(t, sample_callback)
|
1055
|
+
|
1056
|
+
self._specific_backdrop_event_emitter.add_event(backdrop_index, t)
|
1057
|
+
return t
|
1058
|
+
|
1059
|
+
def when_any_backdrop_switched(self, associated_sprites : Iterable[Sprite]=[]) -> Event[[int]]:
|
1060
|
+
"""
|
1061
|
+
It is recommended to use the `Sprite.when_any_backdrop_switched` alias instead of this method,
|
1062
|
+
so you don't need to specify the `associated_sprites` in every event.
|
1063
|
+
|
1064
|
+
Returns an `Event` that is triggered when the backdrop is switched.
|
1065
|
+
|
1066
|
+
The event handler have to take one parameter:
|
1067
|
+
- **idx** (int): The index of the new backdrop
|
1068
|
+
|
1069
|
+
Parameters
|
1070
|
+
---
|
1071
|
+
associated_sprites: List[Sprite]
|
1072
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1073
|
+
"""
|
1074
|
+
|
1075
|
+
t = self._create_event(associated_sprites)
|
1076
|
+
self._backdrop_change_triggers.append(t)
|
1077
|
+
if TYPE_CHECKING:
|
1078
|
+
def sample_callback(idx: int)-> Any:
|
1079
|
+
return
|
1080
|
+
t = _declare_callback_type(t, sample_callback)
|
1081
|
+
|
1082
|
+
return t
|
1083
|
+
|
1084
|
+
def when_timer_above(self, t, associated_sprites : Iterable[Sprite]=[]) -> Condition:
|
1085
|
+
"""
|
1086
|
+
It is recommended to use the `Sprite.when_timer_above` alias instead of this method,
|
1087
|
+
so you don't need to specify the `associated_sprites` in every event.
|
1088
|
+
|
1089
|
+
Returns a `Condition` that is triggered after the game have started for `t` seconds.
|
1090
|
+
A `Condition` works the same way an `Event` does.
|
1091
|
+
|
1092
|
+
The event handler have to take one parameter:
|
1093
|
+
- **n** (int): This value will always be zero
|
1094
|
+
|
1095
|
+
Parameters
|
1096
|
+
---
|
1097
|
+
associated_sprites: List[Sprite]
|
1098
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1099
|
+
"""
|
1100
|
+
t = t*1000
|
1101
|
+
return self.when_condition_met(lambda:(self._current_time_ms>t), 1, associated_sprites)
|
1102
|
+
|
1103
|
+
def when_started_as_clone(self, sprite, associated_sprites : Iterable[Sprite]=[]) -> Event[[Sprite]]:
|
1104
|
+
"""
|
1105
|
+
Returns an `Event` that is triggered after the given sprite is cloned by `Sprite.create_clone`.
|
1106
|
+
Cloning of the clone will also trigger the event.
|
1107
|
+
|
1108
|
+
The event handler have to take one parameter:
|
1109
|
+
- **clone_sprite** (Sprite): The newly created clone.
|
1110
|
+
|
1111
|
+
Parameters
|
1112
|
+
---
|
1113
|
+
associated_sprites: List[Sprite]
|
1114
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1115
|
+
"""
|
1116
|
+
|
1117
|
+
|
1118
|
+
trigger = self._create_event(associated_sprites)
|
1119
|
+
self._clone_event_manager.new_trigger(sprite, trigger)
|
1120
|
+
if TYPE_CHECKING:
|
1121
|
+
def sample_callback(clone_sprite: Sprite)-> Any:
|
1122
|
+
return
|
1123
|
+
trigger = _declare_callback_type(trigger, sample_callback)
|
1124
|
+
return trigger
|
1125
|
+
|
1126
|
+
def when_receive_message(self, topic: str, associated_sprites : Iterable[Sprite]=[]) -> Event[[Any]]:
|
1127
|
+
"""
|
1128
|
+
It is recommended to use the `Sprite.when_receive_message` alias instead of this method,
|
1129
|
+
so you don't need to specify the `associated_sprites` in every event.
|
1130
|
+
|
1131
|
+
Returns an `Event` that is triggered after a message of the given `topic` is broadcasted.
|
1132
|
+
|
1133
|
+
The event handler have to take one parameter:
|
1134
|
+
- **data** (Any): This parameter can be anything passed on by the message.
|
1135
|
+
|
1136
|
+
Parameters
|
1137
|
+
---
|
1138
|
+
topic: str
|
1139
|
+
Can be any string. If the topic equals the topic of a broadcast, the event will be triggered.
|
1140
|
+
associated_sprites: List[Sprite]
|
1141
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1142
|
+
"""
|
1143
|
+
|
1144
|
+
|
1145
|
+
trigger = self._create_event(associated_sprites)
|
1146
|
+
self.__new_subscription(topic, trigger)
|
1147
|
+
if TYPE_CHECKING:
|
1148
|
+
def sample_callback(data: Any)-> Any:
|
1149
|
+
return
|
1150
|
+
trigger = _declare_callback_type(trigger, sample_callback)
|
1151
|
+
return trigger
|
1152
|
+
|
1153
|
+
|
1154
|
+
def broadcast_message(self, topic: str, data: Any=None):
|
1155
|
+
"""
|
1156
|
+
Sends a message of a given `topic` and `data`.
|
1157
|
+
Triggers any event that subscribes to the topic.
|
1158
|
+
The handlers of the events will receive `data` as the parameter.
|
1159
|
+
|
1160
|
+
Example:
|
1161
|
+
```python
|
1162
|
+
def event_handler(data):
|
1163
|
+
print(data) # data will be "hello world!"
|
1164
|
+
|
1165
|
+
game.when_receive_message('print_message').add_handler(event_handler)
|
1166
|
+
game.broadcast_message('print_message', data='hello world!')
|
1167
|
+
|
1168
|
+
# "hello world!" will be printed out
|
1169
|
+
```
|
1170
|
+
Parameters
|
1171
|
+
---
|
1172
|
+
topic: str
|
1173
|
+
Can be any string. If the topic of an message event equals the topic of the broadcast, the event will be triggered.
|
1174
|
+
|
1175
|
+
data: Any
|
1176
|
+
Any arbitory data that will be passed to the event handler
|
1177
|
+
|
1178
|
+
"""
|
1179
|
+
if not topic in self._all_message_subscriptions:
|
1180
|
+
return
|
1181
|
+
|
1182
|
+
self._all_message_subscriptions[topic] = list(filter(lambda t: t.stay_active, self._all_message_subscriptions[topic]))
|
1183
|
+
for e in self._all_message_subscriptions[topic]:
|
1184
|
+
e.trigger(data)
|
1185
|
+
|
1186
|
+
def __new_subscription(self, topic: str, trigger: Event):
|
1187
|
+
if not (topic in self._all_message_subscriptions):
|
1188
|
+
self._all_message_subscriptions[topic] = []
|
1189
|
+
|
1190
|
+
self._all_message_subscriptions[topic].append(trigger)
|
1191
|
+
|
1192
|
+
def start_handler(self, handler:Optional[Callable[[], Any]]=None, associated_sprites: Iterable[Sprite]=[]):
|
1193
|
+
"""
|
1194
|
+
*EXTENDED FEATURE*
|
1195
|
+
|
1196
|
+
It is recommended to use the `Sprite.start_handler` alias instead of this method,
|
1197
|
+
so you don't need to specify the `associated_sprites` in every event.
|
1198
|
+
|
1199
|
+
Run the event handler immediately. Useful when creating a sprite within a function.
|
1200
|
+
|
1201
|
+
The handler does not take in any parameters.
|
1202
|
+
|
1203
|
+
Parameters
|
1204
|
+
---
|
1205
|
+
handler: Function
|
1206
|
+
A function to run.
|
1207
|
+
|
1208
|
+
associated_sprites: List[Sprite]
|
1209
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1210
|
+
|
1211
|
+
"""
|
1212
|
+
e = self._create_event(associated_sprites)
|
1213
|
+
if handler:
|
1214
|
+
e.add_handler(handler)
|
1215
|
+
e.trigger()
|
1216
|
+
return e
|
1217
|
+
|
1218
|
+
|
1219
|
+
## advance events
|
1220
|
+
def create_pygame_event(self, types: List[int], associated_sprites : Iterable[Sprite]=[]) -> Event[[pygame.event.Event]]:
|
1221
|
+
"""
|
1222
|
+
*EXTENDED FEATURE*
|
1223
|
+
|
1224
|
+
Receives specific types of pygame events when they happen.
|
1225
|
+
|
1226
|
+
See pygame.event for more details: https://www.pygame.org/docs/ref/event.html
|
1227
|
+
|
1228
|
+
```python
|
1229
|
+
def key_press_event(event):
|
1230
|
+
# the event argument is a `pygame.event.Event`.
|
1231
|
+
if event.type == pygame.KEYDOWN and event.key == pygame.K_d:
|
1232
|
+
print("the key d is down")
|
1233
|
+
|
1234
|
+
if event.type == pygame.KEYUP and event.key == pygame.K_d:
|
1235
|
+
print("the key d is up")
|
1236
|
+
|
1237
|
+
pysc.game.create_pygame_event([pygame.KEYDOWN, pygame.KEYUP])
|
1238
|
+
```
|
1239
|
+
|
1240
|
+
The event handler have to take one parameter:
|
1241
|
+
- **event** (pygame.event.Event): An pygame event object.
|
1242
|
+
|
1243
|
+
Parameters
|
1244
|
+
---
|
1245
|
+
types: List[int]
|
1246
|
+
A list of the pygame event flags.
|
1247
|
+
|
1248
|
+
associated_sprites: List[Sprite]
|
1249
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1250
|
+
"""
|
1251
|
+
|
1252
|
+
|
1253
|
+
condition = self.when_condition_met(associated_sprites)
|
1254
|
+
|
1255
|
+
def checker_hijack():
|
1256
|
+
for e in self._all_pygame_events:
|
1257
|
+
if e.type in types:
|
1258
|
+
condition.trigger.trigger(e)
|
1259
|
+
|
1260
|
+
if not condition.trigger.stay_active:
|
1261
|
+
condition.remove()
|
1262
|
+
|
1263
|
+
condition.change_checker(checker_hijack)
|
1264
|
+
|
1265
|
+
return cast(Event[pygame.event.Event], condition.trigger)
|
1266
|
+
|
1267
|
+
|
1268
|
+
def _create_specific_collision_trigger(self, sprite1: Sprite, sprite2: Sprite, other_associated_sprites: Iterable[Sprite]=[]):
|
1269
|
+
"""
|
1270
|
+
*EXTENDED FEATURE, EXPERIMENTAL*
|
1271
|
+
|
1272
|
+
DOCUMENTATION NOT COMPLETED
|
1273
|
+
|
1274
|
+
Parameters
|
1275
|
+
---
|
1276
|
+
associated_sprites: List[Sprite]
|
1277
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1278
|
+
"""
|
1279
|
+
#"""Cannot change the collision type of the object after calling this function"""
|
1280
|
+
trigger = self._create_event(set(list(other_associated_sprites)+[sprite1, sprite2]))
|
1281
|
+
|
1282
|
+
self._trigger_to_collision_pairs[trigger] = sprite1, sprite2
|
1283
|
+
|
1284
|
+
|
1285
|
+
return trigger
|
1286
|
+
|
1287
|
+
def _create_type2type_collision_trigger(self, type_a:int, type_b:int, collision_suppressed=False, associated_sprites: Iterable[Sprite]=[]):
|
1288
|
+
"""
|
1289
|
+
*EXTENDED FEATURE*
|
1290
|
+
|
1291
|
+
DOCUMENTATION NOT COMPLETED
|
1292
|
+
|
1293
|
+
Parameters
|
1294
|
+
---
|
1295
|
+
associated_sprites: List[Sprite]
|
1296
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1297
|
+
"""
|
1298
|
+
|
1299
|
+
pair = (type_a, type_b) if type_a>type_b else (type_b, type_a)
|
1300
|
+
|
1301
|
+
h = self._space.add_collision_handler(*pair)
|
1302
|
+
trigger = self._create_event(associated_sprites)
|
1303
|
+
|
1304
|
+
|
1305
|
+
|
1306
|
+
if not pair in self._collision_type_pair_to_trigger:
|
1307
|
+
self._collision_type_pair_to_trigger[pair] = []
|
1308
|
+
self._collision_type_pair_to_trigger[pair].append(trigger)
|
1309
|
+
|
1310
|
+
collision_allowed = not collision_suppressed
|
1311
|
+
def begin(arbiter, space, data):
|
1312
|
+
game = cast(Game, data['game'])
|
1313
|
+
game._contact_pairs_set.add(arbiter.shapes)
|
1314
|
+
|
1315
|
+
for t in game._collision_type_pair_to_trigger[pair]:
|
1316
|
+
t.trigger(arbiter)
|
1317
|
+
return collision_allowed
|
1318
|
+
|
1319
|
+
h.data['game'] = self
|
1320
|
+
h.begin = begin
|
1321
|
+
h.separate = _collision_separate
|
1322
|
+
|
1323
|
+
|
1324
|
+
return trigger
|
1325
|
+
|
1326
|
+
|
1327
|
+
def _create_type_collision_trigger(self, collision_type:int , collision_suppressed=False, associated_sprites: Iterable[Sprite]=[]):
|
1328
|
+
"""
|
1329
|
+
*EXTENDED FEATURE*
|
1330
|
+
|
1331
|
+
DOCUMENTATION NOT COMPLETED
|
1332
|
+
|
1333
|
+
Parameters
|
1334
|
+
---
|
1335
|
+
associated_sprites: List[Sprite]
|
1336
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1337
|
+
"""
|
1338
|
+
|
1339
|
+
trigger = self._create_event(associated_sprites)
|
1340
|
+
|
1341
|
+
collision_allowed = not collision_suppressed
|
1342
|
+
|
1343
|
+
|
1344
|
+
if not collision_type in self._collision_type_to_trigger:
|
1345
|
+
self._collision_type_to_trigger[collision_type] = collision_allowed, []
|
1346
|
+
|
1347
|
+
|
1348
|
+
self._collision_type_to_trigger[collision_type][1].append(trigger)
|
1349
|
+
|
1350
|
+
return trigger
|
1351
|
+
|
1352
|
+
def _suppress_type_collision(self, collision_type, collision_suppressed=True):
|
1353
|
+
"""
|
1354
|
+
*EXTENDED FEATURE*
|
1355
|
+
|
1356
|
+
DOCUMENTATION NOT COMPLETED
|
1357
|
+
|
1358
|
+
"""
|
1359
|
+
collision_allowed = not collision_suppressed
|
1360
|
+
|
1361
|
+
if not collision_type in self._collision_type_to_trigger:
|
1362
|
+
self._collision_type_to_trigger[collision_type] = collision_allowed, []
|
1363
|
+
else:
|
1364
|
+
t_list = self._collision_type_to_trigger[collision_type][1]
|
1365
|
+
self._collision_type_to_trigger[collision_type] = collision_allowed, t_list
|
1366
|
+
|
1367
|
+
|
1368
|
+
def when_timer_reset(self, reset_period: Optional[float]=None, repeats: Optional[int]=None, associated_sprites: Iterable[Sprite]=[]) -> TimerCondition:
|
1369
|
+
"""
|
1370
|
+
*EXTENDED FEATURE*
|
1371
|
+
|
1372
|
+
Repeats an event for `repeats` time for every `reset_period` seconds.
|
1373
|
+
|
1374
|
+
```python
|
1375
|
+
|
1376
|
+
def print_counts(n):
|
1377
|
+
print(n) # n is the number of remaining repeats
|
1378
|
+
|
1379
|
+
# every one second, for 100 times
|
1380
|
+
pysc.game.when_timer_reset(1, 100).add_handler(print_counts)
|
1381
|
+
|
1382
|
+
# will start printing 99, 98, ..., 0 every 1 second.
|
1383
|
+
```
|
1384
|
+
|
1385
|
+
The event handler have to take one parameter:
|
1386
|
+
- **n** (int): The number of remaining repeats
|
1387
|
+
|
1388
|
+
Parameters
|
1389
|
+
---
|
1390
|
+
reset_period: float
|
1391
|
+
The reset period of the timer. The handlers are triggered on timer reset.
|
1392
|
+
|
1393
|
+
repeats: int or None
|
1394
|
+
How many times to repeat. Set to None for infinite repeats.
|
1395
|
+
|
1396
|
+
associated_sprites: List[Sprite]
|
1397
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1398
|
+
"""
|
1399
|
+
if reset_period is None:
|
1400
|
+
reset_period = np.inf
|
1401
|
+
|
1402
|
+
if repeats is None:
|
1403
|
+
_repeats = np.inf
|
1404
|
+
else:
|
1405
|
+
_repeats = repeats
|
1406
|
+
|
1407
|
+
condition = TimerCondition(reset_period, _repeats)
|
1408
|
+
self._all_conditions.append(condition)
|
1409
|
+
self._all_triggers.append(condition.trigger)
|
1410
|
+
|
1411
|
+
self._sprite_event_dependency_manager.add_event(
|
1412
|
+
condition, associated_sprites
|
1413
|
+
)
|
1414
|
+
return condition
|
1415
|
+
|
1416
|
+
|
1417
|
+
def when_condition_met(self, checker=lambda: False, repeats: Optional[int]=None, associated_sprites: Iterable[Sprite]=[])-> Condition:
|
1418
|
+
"""
|
1419
|
+
*EXTENDED FEATURE*
|
1420
|
+
|
1421
|
+
For every frame, if a condition is met, the event is triggered. Repeated up to `repeats` times.
|
1422
|
+
|
1423
|
+
The condition is provided by a function that takes no argument and returns a boolean.
|
1424
|
+
|
1425
|
+
```python
|
1426
|
+
def slowly_move_sprite_out_of_edge(n):
|
1427
|
+
my_sprite.x += 1
|
1428
|
+
|
1429
|
+
pysc.game.when_condition_met(lambda: (my_sprite.x<0), None).add_handler(slowly_move_sprite_out_of_edge)
|
1430
|
+
```
|
1431
|
+
|
1432
|
+
The event handler have to take one parameter:
|
1433
|
+
- **n** (int): The number of remaining repeats
|
1434
|
+
|
1435
|
+
Parameters
|
1436
|
+
---
|
1437
|
+
checker: Callable[[], bool]
|
1438
|
+
A function that takes no argument and returns a boolean.
|
1439
|
+
The checker is run one every frame. If it returns true, the handler is called.
|
1440
|
+
|
1441
|
+
repeats: int or None
|
1442
|
+
How many times to repeat. Set to None for infinite repeats.
|
1443
|
+
|
1444
|
+
|
1445
|
+
Parameters
|
1446
|
+
---
|
1447
|
+
associated_sprites: List[Sprite]
|
1448
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1449
|
+
"""
|
1450
|
+
|
1451
|
+
if repeats is None:
|
1452
|
+
_repeats = np.inf
|
1453
|
+
else:
|
1454
|
+
_repeats = repeats
|
1455
|
+
|
1456
|
+
condition = Condition(checker, _repeats)
|
1457
|
+
self._all_conditions.append(condition)
|
1458
|
+
self._all_triggers.append(condition.trigger)
|
1459
|
+
|
1460
|
+
self._sprite_event_dependency_manager.add_event(
|
1461
|
+
condition, associated_sprites
|
1462
|
+
)
|
1463
|
+
return condition
|
1464
|
+
|
1465
|
+
def when_mouse_click(self, associated_sprites: Iterable[Sprite]=[] ) -> Event[[Tuple[int, int], int, str]]:
|
1466
|
+
"""
|
1467
|
+
*EXTENDED FEATURE*
|
1468
|
+
|
1469
|
+
Returns an `Event` that is triggered when the mouse is clicked or released.
|
1470
|
+
|
1471
|
+
The event handler have to take three parameters:
|
1472
|
+
- **pos** (Tuple[int, int]): The location of the click
|
1473
|
+
- **button** (int): Indicates which button is clicked. 0 for left, 1 for middle, 2 for right and other numbers for scrolling.
|
1474
|
+
- **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
|
1475
|
+
|
1476
|
+
Parameters
|
1477
|
+
---
|
1478
|
+
associated_sprites: List[Sprite]
|
1479
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1480
|
+
|
1481
|
+
"""
|
1482
|
+
event_internal = self.create_pygame_event([pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN], associated_sprites)
|
1483
|
+
event = self._create_event(associated_sprites)
|
1484
|
+
def handler(e: pygame.event.Event):
|
1485
|
+
updown = "up" if e.type == pygame.MOUSEBUTTONUP else "down"
|
1486
|
+
event.trigger(e.pos, e.button, updown)
|
1487
|
+
event_internal.add_handler(handler)
|
1488
|
+
|
1489
|
+
return event
|
1490
|
+
|
1491
|
+
def when_mouse_scroll(self, associated_sprites: Iterable[Sprite]=[] ) -> Event[[str]]:
|
1492
|
+
"""
|
1493
|
+
*EXTENDED FEATURE*
|
1494
|
+
|
1495
|
+
Returns an `Event` that is triggered when the mouse is scrolled.
|
1496
|
+
|
1497
|
+
The event handler have to take one parameters:
|
1498
|
+
- **updown** (str): Either 'up' or 'down' that indicates whether it is a scroll up or a scroll down.
|
1499
|
+
|
1500
|
+
Parameters
|
1501
|
+
---
|
1502
|
+
associated_sprites: List[Sprite]
|
1503
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1504
|
+
|
1505
|
+
"""
|
1506
|
+
event_internal = self.create_pygame_event([pygame.MOUSEWHEEL], associated_sprites)
|
1507
|
+
event = self._create_event(associated_sprites)
|
1508
|
+
def handler(e):
|
1509
|
+
updown = "up" if e.y > 0 else "down"
|
1510
|
+
event.trigger(updown)
|
1511
|
+
event_internal.add_handler(handler)
|
1512
|
+
|
1513
|
+
return event
|
1514
|
+
|
1515
|
+
|
1516
|
+
def _create_event(self, associated_sprites: Iterable[Sprite]=[]) -> Event:
|
1517
|
+
"""
|
1518
|
+
|
1519
|
+
Parameters
|
1520
|
+
---
|
1521
|
+
associated_sprites: List[Sprite]
|
1522
|
+
A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
|
1523
|
+
"""
|
1524
|
+
|
1525
|
+
trigger = Event()
|
1526
|
+
self._all_triggers.append(trigger)
|
1527
|
+
|
1528
|
+
self._sprite_event_dependency_manager.add_event(
|
1529
|
+
trigger, associated_sprites
|
1530
|
+
)
|
1531
|
+
return trigger
|
1532
|
+
|
1533
|
+
|
1534
|
+
game = Game()
|
1535
|
+
"""
|
1536
|
+
The singleton Game object. This is the object that represent the game.
|
1537
|
+
"""
|
1538
|
+
|
1539
|
+
|
1540
|
+
def is_key_pressed(key: str) -> bool:
|
1541
|
+
"""
|
1542
|
+
Returns a bool(True/False) that indicates if the given key is pressed.
|
1543
|
+
|
1544
|
+
Usage
|
1545
|
+
```python
|
1546
|
+
if is_key_pressed('space'):
|
1547
|
+
print('space pressed')
|
1548
|
+
```
|
1549
|
+
"""
|
1550
|
+
keycode = pygame.key.key_code(key)
|
1551
|
+
result = pygame.key.get_pressed()
|
1552
|
+
return result[keycode]
|
1553
|
+
|
1554
|
+
|
1555
|
+
def get_mouse_pos() -> Tuple[int, int]:
|
1556
|
+
"""
|
1557
|
+
Returns the mouse coordinate.
|
1558
|
+
|
1559
|
+
Usage
|
1560
|
+
```python
|
1561
|
+
mouse_x, mouse_y = get_mouse_pos()
|
1562
|
+
```
|
1563
|
+
"""
|
1564
|
+
return pygame.mouse.get_pos()
|
1565
|
+
|
1566
|
+
|
1567
|
+
def get_mouse_presses() -> Tuple[bool, bool, bool]:
|
1568
|
+
"""
|
1569
|
+
Returns the mouse presses.
|
1570
|
+
```python
|
1571
|
+
is_left_click, is_middle_click, is_right_click = get_mouse_presses()
|
1572
|
+
```
|
1573
|
+
"""
|
1574
|
+
return pygame.mouse.get_pressed(num_buttons=3)
|
1575
|
+
|
1576
|
+
# def _is_touching(sprite_a:Sprite, sprite_b:Sprite):
|
1577
|
+
# """
|
1578
|
+
# pymunk
|
1579
|
+
# """
|
1580
|
+
# for pair in game._contact_pairs_set:
|
1581
|
+
|
1582
|
+
# if (sprite_a._shape in pair) and (sprite_b._shape in pair):
|
1583
|
+
# return True
|
1584
|
+
# return False
|
1585
|
+
|
1586
|
+
|
1587
|
+
# def _is_touching_mouse(sprite: Sprite):
|
1588
|
+
# return sprite._shape.point_query(pygame.mouse.get_pos()).distance <= 0
|
1589
|
+
|