Browse Source

Fire and kill

Bastien Sevajol 6 years ago
parent
commit
45cf4f9c11

+ 5 - 1
sandbox/tile/config.yaml View File

@@ -1,9 +1,13 @@
1 1
 core:
2 2
     cycle_duration: 0.25
3
-    use_x_cores: 1
3
+    use_x_cores: 2
4 4
 terminals:
5 5
     sync: True
6 6
 game:
7
+    look_around:
8
+        frequency: 1
9
+    engage:
10
+        frequency: 2
7 11
     move:
8 12
         walk_ref_time: 3
9 13
         run_ref_time: 1

+ 2 - 0
sandbox/tile/const.py View File

@@ -7,6 +7,8 @@ FLAG_DE = 'DE'
7 7
 FLAG_URSS = 'URSS'
8 8
 
9 9
 SIDE = 'SIDE'
10
+COMBAT_MODE = 'COMBAT_MODE'
11
+COMBAT_MODE_DEFENSE = 'COMBAT_MODE_DEFENSE'
10 12
 
11 13
 DE_COLOR = (0, 81, 211)
12 14
 URSS_COLOR = (204, 0, 0)

+ 79 - 4
sandbox/tile/gui/base.py View File

@@ -1,9 +1,13 @@
1 1
 # coding: utf-8
2
+import os
3
+import random
2 4
 import typing
3 5
 
6
+import pyglet
4 7
 from pyglet.window import key
5 8
 
6 9
 from cocos.actions import MoveTo as BaseMoveTo
10
+from cocos.audio.pygame.mixer import Sound
7 11
 from sandbox.tile.user_action import UserAction
8 12
 from synergine2.config import Config
9 13
 from synergine2.log import SynergineLogger
@@ -22,6 +26,8 @@ from synergine2_xyz.physics import Physics
22 26
 from synergine2_xyz.utils import get_angle
23 27
 from sandbox.tile.simulation.event import NewVisibleOpponent
24 28
 from sandbox.tile.simulation.event import NoLongerVisibleOpponent
29
+from sandbox.tile.simulation.event import FireEvent
30
+from sandbox.tile.simulation.event import DieEvent
25 31
 
26 32
 
27 33
 class EditLayer(BaseEditLayer):
@@ -44,6 +50,23 @@ class TileLayerManager(LayerManager):
44 50
     edit_layer_class = EditLayer
45 51
 
46 52
 
53
+# TODO: Move into synergine2cocos2d
54
+class AudioLibrary(object):
55
+    sound_file_paths = {
56
+        'gunshot_default': '204010__duckduckpony__homemade-gunshot-2.ogg',
57
+    }
58
+
59
+    def __init__(self, sound_dir_path: str) -> None:
60
+        self._sound_dir_path = sound_dir_path
61
+        self._sounds = {}
62
+
63
+    def get_sound(self, name: str) -> Sound:
64
+        if name not in self._sounds:
65
+            sound_file_name = self.sound_file_paths[name]
66
+            self._sounds[name] = Sound(os.path.join(self._sound_dir_path, sound_file_name))
67
+        return self._sounds[name]
68
+
69
+
47 70
 class Game(TMXGui):
48 71
     layer_manager_class = TileLayerManager
49 72
 
@@ -64,6 +87,7 @@ class Game(TMXGui):
64 87
             read_queue_interval=read_queue_interval,
65 88
             map_dir_path=map_dir_path,
66 89
         )
90
+        self.sound_lib = AudioLibrary('sandbox/tile/sounds/')
67 91
 
68 92
         self.terminal.register_event_handler(
69 93
             FinishMoveEvent,
@@ -85,6 +109,16 @@ class Game(TMXGui):
85 109
             self.no_longer_visible_opponent,
86 110
         )
87 111
 
112
+        self.terminal.register_event_handler(
113
+            FireEvent,
114
+            self.fire_happen,
115
+        )
116
+
117
+        self.terminal.register_event_handler(
118
+            DieEvent,
119
+            self.subject_die,
120
+        )
121
+
88 122
         # configs
89 123
         self.move_duration_ref = float(self.config.resolve('game.move.walk_ref_time'))
90 124
         self.move_fast_duration_ref = float(self.config.resolve('game.move.run_ref_time'))
@@ -147,11 +181,15 @@ class Game(TMXGui):
147 181
             observer_actor = self.layer_manager.subject_layer.subjects_index[event.observer_subject_id]
148 182
             observed_actor = self.layer_manager.subject_layer.subjects_index[event.observed_subject_id]
149 183
 
150
-            observer_pixel_position = self.layer_manager.grid_manager.get_pixel_position_of_grid_position(
151
-                observer_actor.subject.position,
184
+            observer_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
185
+                *self.layer_manager.grid_manager.get_pixel_position_of_grid_position(
186
+                    observer_actor.subject.position,
187
+                )
152 188
             )
153
-            observed_pixel_position = self.layer_manager.grid_manager.get_pixel_position_of_grid_position(
154
-                observed_actor.subject.position,
189
+            observed_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
190
+                *self.layer_manager.grid_manager.get_pixel_position_of_grid_position(
191
+                    observed_actor.subject.position,
192
+                )
155 193
             )
156 194
 
157 195
             def draw_visible_opponent():
@@ -161,4 +199,41 @@ class Game(TMXGui):
161 199
                     line_color,
162 200
                 )
163 201
 
202
+            # TODO: Not in edit layer !
164 203
             self.layer_manager.edit_layer.append_callback(draw_visible_opponent, 1.0)
204
+
205
+    def fire_happen(self, event: FireEvent) -> None:
206
+        # TODO: Not in edit layer !
207
+        shooter_actor = self.layer_manager.subject_layer.subjects_index[event.shooter_subject_id]
208
+        shooter_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
209
+            *self.layer_manager.grid_manager.get_pixel_position_of_grid_position(
210
+                shooter_actor.subject.position,
211
+            )
212
+        )
213
+        fire_to_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
214
+            *self.layer_manager.grid_manager.get_pixel_position_of_grid_position(
215
+                event.target_position,
216
+            )
217
+        )
218
+
219
+        def gunshot_trace():
220
+            draw_line(
221
+                shooter_pixel_position,
222
+                fire_to_pixel_position,
223
+                color=(255, 0, 0),
224
+            )
225
+
226
+        def gunshot_sound():
227
+            self.sound_lib.get_sound('gunshot_default').play()
228
+
229
+        # To avoid all in same time
230
+        delay = random.uniform(0.0, 0.6)
231
+
232
+        self.layer_manager.edit_layer.append_callback(gunshot_trace, duration=0.1, delay=delay)
233
+        self.layer_manager.edit_layer.append_callback(gunshot_sound, duration=0.0, delay=delay)
234
+
235
+    def subject_die(self, event: DieEvent) -> None:
236
+        killed_actor = self.layer_manager.subject_layer.subjects_index[event.shoot_subject_id]
237
+        dead_image = pyglet.resource.image('maps/003/actors/man_d1.png')
238
+        killed_actor.update_image(dead_image)
239
+        killed_actor.freeze()

BIN
sandbox/tile/maps/003/actors/man_d1.png View File


+ 4 - 3
sandbox/tile/run.py View File

@@ -26,8 +26,9 @@ from synergine2.cycle import CycleManager
26 26
 from synergine2.terminals import TerminalManager
27 27
 
28 28
 
29
-def main(map_dir_path: str, seed_value: int=42):
30
-    seed(seed_value)
29
+def main(map_dir_path: str, seed_value: int=None):
30
+    if seed_value is not None:
31
+        seed(seed_value)
31 32
 
32 33
     config = Config()
33 34
     config.load_yaml('sandbox/tile/config.yaml')
@@ -92,7 +93,7 @@ def main(map_dir_path: str, seed_value: int=42):
92 93
 if __name__ == '__main__':
93 94
     parser = argparse.ArgumentParser(description='Run TileStrategy')
94 95
     parser.add_argument('map_dir_path', help='map directory path')
95
-    parser.add_argument('--seed', dest='seed', default=42)
96
+    parser.add_argument('--seed', dest='seed', default=None)
96 97
 
97 98
     args = parser.parse_args()
98 99
 

+ 7 - 1
sandbox/tile/simulation/base.py View File

@@ -1,7 +1,8 @@
1 1
 # coding: utf-8
2
-
2
+from sandbox.tile.const import COLLECTION_ALIVE
3 3
 from sandbox.tile.simulation.physics import TilePhysics
4 4
 from synergine2.config import Config
5
+from synergine2.simulation import SubjectBehaviour
5 6
 from synergine2_xyz.physics import Physics
6 7
 from synergine2_xyz.simulation import XYZSimulation
7 8
 from synergine2_xyz.subjects import XYZSubject
@@ -34,3 +35,8 @@ class TileStrategySubjects(XYZSubjects):
34 35
 
35 36
 class BaseSubject(XYZSubject):
36 37
     pass
38
+
39
+
40
+class AliveSubjectBehaviour(SubjectBehaviour):
41
+    def is_terminated(self) -> bool:
42
+        return COLLECTION_ALIVE not in self.subject.collections

+ 74 - 3
sandbox/tile/simulation/behaviour.py View File

@@ -1,13 +1,18 @@
1 1
 # coding: utf-8
2
+import random
2 3
 import time
4
+import typing
3 5
 
6
+from sandbox.tile.const import COLLECTION_ALIVE
7
+from sandbox.tile.simulation.base import AliveSubjectBehaviour
4 8
 from sandbox.tile.simulation.event import NoLongerVisibleOpponent
9
+from sandbox.tile.simulation.event import FireEvent
10
+from sandbox.tile.simulation.event import DieEvent
5 11
 from sandbox.tile.simulation.event import NewVisibleOpponent
6 12
 from sandbox.tile.simulation.mechanism import OpponentVisibleMechanism
7 13
 from sandbox.tile.user_action import UserAction
8 14
 from synergine2.config import Config
9 15
 from synergine2.simulation import Simulation
10
-from synergine2.simulation import SubjectBehaviour
11 16
 from synergine2.simulation import Event
12 17
 from synergine2.simulation import Subject
13 18
 from synergine2_xyz.move.simulation import MoveToBehaviour as BaseMoveToBehaviour
@@ -25,6 +30,9 @@ class MoveToBehaviour(BaseMoveToBehaviour):
25 30
         self._run_duration = float(self.config.resolve('game.move.run_ref_time'))
26 31
         self._crawl_duration = float(self.config.resolve('game.move.crawl_ref_time'))
27 32
 
33
+    def is_terminated(self) -> bool:
34
+        return COLLECTION_ALIVE not in self.subject.collections
35
+
28 36
     def _can_move_to_next_step(self, move_to_data: dict) -> bool:
29 37
         if move_to_data['gui_action'] == UserAction.ORDER_MOVE:
30 38
             return time.time() - move_to_data['last_intention_time'] >= self._walk_duration
@@ -34,13 +42,20 @@ class MoveToBehaviour(BaseMoveToBehaviour):
34 42
             return time.time() - move_to_data['last_intention_time'] >= self._crawl_duration
35 43
 
36 44
 
37
-class LookAroundBehaviour(SubjectBehaviour):
45
+class LookAroundBehaviour(AliveSubjectBehaviour):
38 46
     """
39 47
     Behaviour who permit to reference visible things like enemies
40 48
     """
41 49
     visible_mechanism = OpponentVisibleMechanism
42 50
     use = [visible_mechanism]
43
-    force_action = True
51
+
52
+    def __init__(self, *args, **kwargs) -> None:
53
+        super().__init__(*args, **kwargs)
54
+        self._seconds_frequency = float(self.config.resolve('game.look_around.frequency'))
55
+
56
+    @property
57
+    def seconds_frequency(self) -> typing.Optional[float]:
58
+        return self._seconds_frequency
44 59
 
45 60
     def action(self, data) -> [Event]:
46 61
         new_visible_subject_events = []
@@ -80,3 +95,59 @@ class LookAroundBehaviour(SubjectBehaviour):
80 95
             'new_visible_subject_ids': new_visible_subject_ids,
81 96
             'no_longer_visible_subject_ids': no_longer_visible_subject_ids,
82 97
         }
98
+
99
+
100
+class EngageOpponent(AliveSubjectBehaviour):
101
+    visible_mechanism = OpponentVisibleMechanism
102
+    use = [visible_mechanism]
103
+
104
+    def __init__(self, *args, **kwargs) -> None:
105
+        super().__init__(*args, **kwargs)
106
+        self._seconds_frequency = float(self.config.resolve('game.engage.frequency'))
107
+
108
+    @property
109
+    def seconds_frequency(self) -> typing.Optional[float]:
110
+        return self._seconds_frequency
111
+
112
+    def action(self, data) -> [Event]:
113
+        kill = data['kill']
114
+        target_subject_id = data['target_subject_id']
115
+        target_subject = self.simulation.subjects.index[target_subject_id]
116
+        target_position = data['target_position']
117
+
118
+        events = list()
119
+        events.append(FireEvent(shooter_subject_id=self.subject.id, target_position=target_position))
120
+
121
+        if kill:
122
+            target_subject.collections.remove(COLLECTION_ALIVE)
123
+            # FIXME: Must be automatic when manipulate subject collections !
124
+            self.simulation.collections[COLLECTION_ALIVE].remove(target_subject_id)
125
+            self.simulation.collections[COLLECTION_ALIVE] = self.simulation.collections[COLLECTION_ALIVE]
126
+            events.append(DieEvent(shooter_subject_id=self.subject.id, shoot_subject_id=target_subject_id))
127
+
128
+        return events
129
+
130
+    def run(self, data):
131
+        visible_subjects = data[self.visible_mechanism]['visible_subjects']
132
+        if not visible_subjects:
133
+            return
134
+        # Manage selected target (can change, better visibility, etc ...)
135
+        # Manage weapon munition to be able to fire
136
+        # Manage fear/under fire ...
137
+        # Manage weapon reload time
138
+
139
+        # For dev fun, don't fire at random
140
+        if random.randint(1, 3) == -1:
141
+            # Executed but decided to fail
142
+            self.last_execution_time = time.time()
143
+            return False
144
+
145
+        target_subject = random.choice(visible_subjects)
146
+        kill = random.randint(0, 100) >= 75
147
+
148
+        # Manage fire miss or touch (visibility, fear, opponent hiding, etc ...)
149
+        return {
150
+            'kill': kill,
151
+            'target_subject_id': target_subject.id,
152
+            'target_position': target_subject.position,
153
+        }

+ 22 - 0
sandbox/tile/simulation/event.py View File

@@ -2,6 +2,8 @@
2 2
 
3 3
 
4 4
 # TODO: Reprendre les events Move, pour les lister tous ici
5
+import typing
6
+
5 7
 from synergine2.simulation import Event
6 8
 
7 9
 
@@ -23,3 +25,23 @@ class NoLongerVisibleOpponent(Event):
23 25
     ) -> None:
24 26
         self.observer_subject_id = observer_subject_id
25 27
         self.observed_subject_id = observed_subject_id
28
+
29
+
30
+class FireEvent(Event):
31
+    def __init__(
32
+        self,
33
+        shooter_subject_id: int,
34
+        target_position: typing.Tuple[int, int],
35
+    ) -> None:
36
+        self.shooter_subject_id = shooter_subject_id
37
+        self.target_position = target_position
38
+
39
+
40
+class DieEvent(Event):
41
+    def __init__(
42
+        self,
43
+        shooter_subject_id: int,
44
+        shoot_subject_id: int,
45
+    ) -> None:
46
+        self.shooter_subject_id = shooter_subject_id
47
+        self.shoot_subject_id = shoot_subject_id

+ 5 - 3
sandbox/tile/simulation/mechanism.py View File

@@ -1,14 +1,16 @@
1 1
 # coding: utf-8
2 2
 import typing
3 3
 
4
-from sandbox.tile.const import SIDE
4
+from sandbox.tile.const import SIDE, COLLECTION_ALIVE
5 5
 from synergine2_xyz.subjects import XYZSubject
6 6
 from synergine2_xyz.visible.simulation import VisibleMechanism
7 7
 
8 8
 
9 9
 class OpponentVisibleMechanism(VisibleMechanism):
10
+    from_collection = COLLECTION_ALIVE
11
+
10 12
     def reduce_subjects(self, subjects: typing.List[XYZSubject]) -> typing.Iterator[XYZSubject]:
11
-        def is_opponent(subject: XYZSubject) -> bool:
13
+        def filter_subject(subject: XYZSubject) -> bool:
12 14
             return self.subject.properties[SIDE] != subject.properties[SIDE]
13 15
 
14
-        return filter(is_opponent, subjects)
16
+        return filter(filter_subject, subjects)

+ 4 - 1
sandbox/tile/simulation/subject.py View File

@@ -1,7 +1,8 @@
1 1
 # coding: utf-8
2
-from sandbox.tile.const import COLLECTION_ALIVE
2
+from sandbox.tile.const import COLLECTION_ALIVE, COMBAT_MODE_DEFENSE
3 3
 from sandbox.tile.simulation.base import BaseSubject
4 4
 from sandbox.tile.simulation.behaviour import MoveToBehaviour
5
+from sandbox.tile.simulation.behaviour import EngageOpponent
5 6
 from sandbox.tile.simulation.behaviour import LookAroundBehaviour
6 7
 from synergine2.share import shared
7 8
 
@@ -13,7 +14,9 @@ class TileSubject(BaseSubject):
13 14
     behaviours_classes = [
14 15
         MoveToBehaviour,
15 16
         LookAroundBehaviour,
17
+        EngageOpponent,
16 18
     ]
17 19
     visible_opponent_ids = shared.create_self('visible_opponent_ids', lambda: [])
20
+    combat_mode = shared.create_self('combat_mode', COMBAT_MODE_DEFENSE)
18 21
     # TODO: implement (copied from engulf)
19 22
     # behaviour_selector_class = CellBehaviourSelector

BIN
sandbox/tile/sounds/204010__duckduckpony__homemade-gunshot-2.ogg View File


+ 4 - 0
sandbox/tile/terminal/base.py View File

@@ -1,5 +1,7 @@
1 1
 # coding: utf-8
2 2
 from sandbox.tile.simulation.event import NewVisibleOpponent
3
+from sandbox.tile.simulation.event import FireEvent
4
+from sandbox.tile.simulation.event import DieEvent
3 5
 from sandbox.tile.simulation.event import NoLongerVisibleOpponent
4 6
 from sandbox.tile.simulation.physics import TilePhysics
5 7
 from sandbox.tile.simulation.subject import TileSubject as ManSubject
@@ -16,6 +18,8 @@ class CocosTerminal(GameTerminal):
16 18
         StartMoveEvent,
17 19
         NewVisibleOpponent,
18 20
         NoLongerVisibleOpponent,
21
+        FireEvent,
22
+        DieEvent,
19 23
     ]
20 24
 
21 25
     def __init__(self, *args, asynchronous: bool, map_dir_path: str, **kwargs):

+ 21 - 9
synergine2/cycle.py View File

@@ -2,6 +2,8 @@
2 2
 import multiprocessing
3 3
 import typing
4 4
 
5
+import time
6
+
5 7
 from synergine2.base import BaseObject
6 8
 from synergine2.config import Config
7 9
 from synergine2.exceptions import SynergineException
@@ -291,23 +293,33 @@ class CycleManager(BaseObject):
291 293
                 str(len(subject_behaviours)),
292 294
             ))
293 295
 
294
-            for behaviour in subject_behaviours.values():
296
+            for behaviour_key, behaviour in list(subject_behaviours.items()):
295 297
                 self.logger.info('Subject {}: run {} behaviour'.format(
296 298
                     str(subject.id),
297 299
                     str(type(behaviour)),
298 300
                 ))
299 301
 
300
-                # We identify behaviour data with it's class to be able to intersect it after subprocess data collect
301
-                with time_it() as elapsed_time:
302
-                    behaviour_data = behaviour.run(mechanisms_data)  # TODO: Behaviours dependencies
302
+                if behaviour.is_terminated():
303
+                    del subject.behaviours[behaviour_key]
303 304
 
304
-                if self.logger.is_debug:
305
-                    self.logger.debug('Subject {}: behaviour {} produce data: {} in {}s'.format(
306
-                        str(type(behaviour)),
305
+                # We identify behaviour data with it's class to be able to intersect it after subprocess data collect
306
+                if behaviour.is_skip(self.current_cycle):
307
+                    behaviour_data = False
308
+                    self.logger.debug('Subject {}: behaviour {} skip'.format(
307 309
                         str(subject.id),
308
-                        str(behaviour_data),
309
-                        elapsed_time.get_final_time(),
310
+                        str(type(behaviour)),
310 311
                     ))
312
+                else:
313
+                    with time_it() as elapsed_time:
314
+                        behaviour.last_execution_time = time.time()
315
+                        behaviour_data = behaviour.run(mechanisms_data)  # TODO: Behaviours dependencies
316
+                        if self.logger.is_debug:
317
+                            self.logger.debug('Subject {}: behaviour {} produce data: {} in {}s'.format(
318
+                                str(type(behaviour)),
319
+                                str(subject.id),
320
+                                str(behaviour_data),
321
+                                elapsed_time.get_final_time(),
322
+                            ))
311 323
 
312 324
                 if behaviour_data:
313 325
                     behaviours_data[behaviour.__class__] = behaviour_data

+ 6 - 2
synergine2/processing.py View File

@@ -1,4 +1,5 @@
1 1
 # coding: utf-8
2
+import random
2 3
 import typing
3 4
 from multiprocessing import Process
4 5
 from multiprocessing.connection import Connection
@@ -41,12 +42,15 @@ class Worker(object):
41 42
             args=(
42 43
                 self.local_write_pipe,
43 44
                 self.process_read_pipe,
44
-            )
45
+            ),
46
+            kwargs={'seed': random.random()}
45 47
         )
46
-        self.db = None  # type: RedisDatabase
48
+        self.db = None  # TODO delete
47 49
         self.process.start()
48 50
 
49 51
     def work(self, *args, **kwargs):
52
+        seed_value = kwargs.pop('seed')
53
+        random.seed(seed_value)
50 54
         while True:
51 55
             args = self.process_read_pipe.recv()
52 56
             if args == STOP:

+ 4 - 0
synergine2/share.py View File

@@ -102,6 +102,10 @@ class TrackedList(list):
102 102
         super().remove(object_)
103 103
         self.shared.set(self.shared_data.get_final_key(self.instance), list(self))
104 104
 
105
+    def extend(self, iterable) -> None:
106
+        super().extend(iterable)
107
+        self.shared.set(self.shared_data.get_final_key(self.instance), list(self))
108
+
105 109
     # TODO: Cover all methods
106 110
 
107 111
 

+ 32 - 3
synergine2/simulation.py View File

@@ -1,6 +1,8 @@
1 1
 # coding: utf-8
2 2
 import typing
3 3
 
4
+import time
5
+
4 6
 from synergine2.base import BaseObject
5 7
 from synergine2.base import IdentifiedObject
6 8
 from synergine2.config import Config
@@ -35,6 +37,7 @@ class Subject(IdentifiedObject):
35 37
     behaviours_classes = []
36 38
     behaviour_selector_class = None  # type: typing.Type[SubjectBehaviourSelector]
37 39
     intention_manager_class = None  # type: typing.Type[IntentionManager]
40
+    collections = shared.create_self('collections', lambda: [])
38 41
 
39 42
     def __init__(
40 43
         self,
@@ -50,7 +53,7 @@ class Subject(IdentifiedObject):
50 53
         """
51 54
         super().__init__()
52 55
         # FIXME: use shared data to permit dynamic start_collections
53
-        self.collections = self.start_collections[:]
56
+        self.collections.extend(self.start_collections[:])
54 57
 
55 58
         self.config = config
56 59
         self._id = id(self)  # We store object id because it's lost between process
@@ -183,7 +186,7 @@ class Simulation(BaseObject):
183 186
     behaviours_classes = []
184 187
 
185 188
     subject_classes = shared.create('subject_classes', {})
186
-    collections = shared.create('start_collections', {})
189
+    collections = shared.create('collections', {})
187 190
 
188 191
     def __init__(
189 192
         self,
@@ -285,7 +288,6 @@ class Behaviour(BaseObject):
285 288
 
286 289
 
287 290
 class SubjectBehaviour(Behaviour):
288
-    frequency = 1
289 291
     use = []  # type: typing.List[typing.Type[SubjectMechanism]]
290 292
 
291 293
     def __init__(
@@ -297,6 +299,33 @@ class SubjectBehaviour(Behaviour):
297 299
         self.config = config
298 300
         self.simulation = simulation
299 301
         self.subject = subject
302
+        self.last_execution_time = 0
303
+
304
+    @property
305
+    def cycle_frequency(self) -> typing.Optional[float]:
306
+        return None
307
+
308
+    @property
309
+    def seconds_frequency(self) -> typing.Optional[float]:
310
+        return None
311
+
312
+    def is_terminated(self) -> bool:
313
+        """
314
+        :return: True if behaviour will no longer exist (can be removed from simulation)
315
+        """
316
+        return False
317
+
318
+    def is_skip(self, cycle_number: int) -> bool:
319
+        """
320
+        :return: True if behaviour have to be skip this time
321
+        """
322
+        if self.cycle_frequency is not None:
323
+            return not bool(cycle_number % self.cycle_frequency)
324
+
325
+        if self.seconds_frequency is not None:
326
+            return time.time() - self.last_execution_time <= self.seconds_frequency
327
+
328
+        return False
300 329
 
301 330
     def run(self, data):
302 331
         """

+ 13 - 0
synergine2_cocos2d/actor.py View File

@@ -44,6 +44,13 @@ class Actor(AnimatedInterface, cocos.sprite.Sprite):
44 44
         self.current_image = image
45 45
         self.need_update_cshape = False
46 46
         self.properties = properties or {}
47
+        self._freeze = False
48
+
49
+    def freeze(self) -> None:
50
+        """
51
+        Set object to freeze mode: No visual modification can be done anymore
52
+        """
53
+        self._freeze = True
47 54
 
48 55
     def stop_actions(self, action_types: typing.Tuple[typing.Type[cocos.actions.Action], ...]) -> None:
49 56
         for action in self.actions:
@@ -59,6 +66,9 @@ class Actor(AnimatedInterface, cocos.sprite.Sprite):
59 66
         self.need_update_cshape = False
60 67
 
61 68
     def update_position(self, new_position: euclid.Vector2) -> None:
69
+        if self._freeze:
70
+            return
71
+
62 72
         self.position = new_position
63 73
         self.cshape.center = new_position  # Note: if remove: strange behaviour: drag change actor position with anomaly
64 74
 
@@ -79,5 +89,8 @@ class Actor(AnimatedInterface, cocos.sprite.Sprite):
79 89
         return self.current_image
80 90
 
81 91
     def update_image(self, new_image: pyglet.image.TextureRegion):
92
+        if self._freeze:
93
+            return
94
+
82 95
         self.image = new_image
83 96
         self.image_anchor = new_image.width // 2, new_image.height // 2

+ 22 - 2
synergine2_cocos2d/gui.py View File

@@ -10,6 +10,7 @@ from pyglet.window import mouse
10 10
 import cocos
11 11
 from cocos import collision_model
12 12
 from cocos import euclid
13
+from cocos.audio.pygame import mixer
13 14
 from cocos.layer import ScrollableLayer
14 15
 from synergine2.config import Config
15 16
 from synergine2.log import SynergineLogger
@@ -122,18 +123,35 @@ class Callback(object):
122 123
         self,
123 124
         func: typing.Callable[[], None],
124 125
         duration: float,
126
+        delay: float=None,
125 127
     ) -> None:
126 128
         self.func = func
127 129
         self.duration = duration
128 130
         # Started timestamp
129 131
         self.started = None  # type: float
132
+        self.require_delay = False
133
+        self.delay = delay
134
+        if delay is not None:
135
+            self.require_delay = True
130 136
 
131 137
     def execute(self) -> None:
138
+        if self.require_delay and not self.started:
139
+            self.started = time.time()
140
+            return
141
+        elif self.require_delay and time.time() - self.started < self.delay:
142
+            return
143
+        elif self.require_delay:
144
+            self.started = None
145
+            self.require_delay = False
146
+
132 147
         if self.started is None:
133 148
             self.started = time.time()
134 149
 
135
-        if time.time() - self.started < self.duration:
150
+        if time.time() - self.started <= self.duration:
151
+            self.func()
152
+        elif not self.duration:
136 153
             self.func()
154
+            raise FinishedCallback()
137 155
         else:
138 156
             raise FinishedCallback()
139 157
 
@@ -231,10 +249,11 @@ class EditLayer(cocos.layer.Layer):
231 249
         # TODO: In top level class: to be available in all layers
232 250
         self.callbacks = []  # type: typing.List[Callback]
233 251
 
234
-    def append_callback(self, callback: typing.Callable[[], None], duration: float) -> None:
252
+    def append_callback(self, callback: typing.Callable[[], None], duration: float, delay: float=None) -> None:
235 253
         self.callbacks.append(Callback(
236 254
             callback,
237 255
             duration,
256
+            delay=delay,
238 257
         ))
239 258
 
240 259
     def set_selectable(self, actor: Actor) -> None:
@@ -719,6 +738,7 @@ class Gui(object):
719 738
             vsync=True,
720 739
             resizable=True,
721 740
         )
741
+        mixer.init()
722 742
 
723 743
         self.interaction_manager = InteractionManager(
724 744
             config=self.config,

+ 12 - 1
synergine2_xyz/visible/simulation.py View File

@@ -8,6 +8,8 @@ from synergine2_xyz.subjects import XYZSubject
8 8
 
9 9
 
10 10
 class VisibleMechanism(SubjectMechanism):
11
+    from_collection = None
12
+
11 13
     def __init__(
12 14
         self,
13 15
         config: Config,
@@ -24,8 +26,17 @@ class VisibleMechanism(SubjectMechanism):
24 26
     def is_visible(self, observed: XYZSubject) -> bool:
25 27
         return self.simulation.physics.subject_see_subject(self.subject, observed)
26 28
 
29
+    def _get_subject_iterable_from_collection(self, collection_name: str) -> typing.Iterator[XYZSubject]:
30
+        for subject_id in self.simulation.collections[collection_name]:
31
+            yield self.simulation.subjects.index[subject_id]
32
+
27 33
     def run(self) -> dict:
28
-        subjects_to_parse = self.reduce_subjects(self.simulation.subjects)
34
+        if self.from_collection is None:
35
+            subjects = self.simulation.subjects
36
+        else:
37
+            subjects = self._get_subject_iterable_from_collection(self.from_collection)
38
+
39
+        subjects_to_parse = self.reduce_subjects(subjects)
29 40
         subjects_visible = list(filter(self.is_visible, subjects_to_parse))
30 41
         return {
31 42
             'visible_subjects': subjects_visible,