Browse Source

Start new sandbox project based on tmx map

Bastien Sevajol 6 years ago
parent
commit
d19fb09a5a
40 changed files with 1282 additions and 70 deletions
  1. 1 0
      cocos
  2. 3 2
      sandbox/engulf/gui.py
  3. 1 0
      sandbox/tile/__init__.py
  4. 1 0
      sandbox/tile/gui/__init__.py
  5. 33 0
      sandbox/tile/gui/actor.py
  6. 5 0
      sandbox/tile/gui/animation.py
  7. 29 0
      sandbox/tile/gui/base.py
  8. 17 0
      sandbox/tile/maps/003/003.tmx
  9. BIN
      sandbox/tile/maps/003/actors/man.png
  10. BIN
      sandbox/tile/maps/003/actors/man_c1.png
  11. BIN
      sandbox/tile/maps/003/actors/man_c2.png
  12. BIN
      sandbox/tile/maps/003/actors/man_c3.png
  13. BIN
      sandbox/tile/maps/003/actors/man_c4.png
  14. BIN
      sandbox/tile/maps/003/actors/man_w1.png
  15. BIN
      sandbox/tile/maps/003/actors/man_w10.png
  16. BIN
      sandbox/tile/maps/003/actors/man_w2.png
  17. BIN
      sandbox/tile/maps/003/actors/man_w3.png
  18. BIN
      sandbox/tile/maps/003/actors/man_w4.png
  19. BIN
      sandbox/tile/maps/003/actors/man_w5.png
  20. BIN
      sandbox/tile/maps/003/actors/man_w6.png
  21. BIN
      sandbox/tile/maps/003/actors/man_w7.png
  22. BIN
      sandbox/tile/maps/003/actors/man_w8.png
  23. BIN
      sandbox/tile/maps/003/actors/man_w9.png
  24. BIN
      sandbox/tile/maps/003/background.png
  25. BIN
      sandbox/tile/maps/003/trees_64x64.png
  26. 4 0
      sandbox/tile/maps/003/trees_64x64.tsx
  27. 73 0
      sandbox/tile/run.py
  28. 1 0
      sandbox/tile/simulation/__init__.py
  29. 19 0
      sandbox/tile/simulation/base.py
  30. 6 0
      sandbox/tile/simulation/subject.py
  31. 1 0
      sandbox/tile/terminal/__init__.py
  32. 34 0
      sandbox/tile/terminal/base.py
  33. 1 1
      synergine2/core.py
  34. 73 0
      synergine2_cocos2d/actor.py
  35. 88 0
      synergine2_cocos2d/animation.py
  36. 13 0
      synergine2_cocos2d/exception.py
  37. 669 67
      synergine2_cocos2d/gui.py
  38. 120 0
      synergine2_cocos2d/layer.py
  39. 74 0
      synergine2_cocos2d/middleware.py
  40. 16 0
      synergine2_cocos2d/terminal.py

+ 1 - 0
cocos View File

@@ -0,0 +1 @@
1
+../cocos/cocos

+ 3 - 2
sandbox/engulf/gui.py View File

@@ -6,7 +6,7 @@ from cocos.actions import MoveTo, Repeat, ScaleBy, Reverse, RotateTo
6 6
 from cocos.sprite import Sprite
7 7
 from sandbox.engulf.behaviour import GrassGrownUp, GrassSpawn, MoveTo as MoveToEvent, EatEvent, AttackEvent, EatenEvent
8 8
 from sandbox.engulf.subject import Cell, Grass, PreyCell, PredatorCell
9
-from synergine2.terminals import TerminalPackage
9
+from synergine2.terminals import TerminalPackage, Terminal
10 10
 from synergine2_cocos2d.gui import Gui, GridLayerMixin
11 11
 from synergine2_cocos2d.gui import MainLayer as BaseMainLayer
12 12
 
@@ -85,9 +85,10 @@ class GrassLayer(GridLayerMixin, BaseMainLayer):
85 85
 
86 86
 
87 87
 class MainLayer(GridLayerMixin, BaseMainLayer):
88
-    def __init__(self, game: 'Game', terminal, *args, **kwargs):
88
+    def __init__(self, game: 'Game', terminal: Terminal, *args, **kwargs):
89 89
         super().__init__(terminal, *args, **kwargs)
90 90
         self.game = game
91
+        self.terminal = terminal
91 92
 
92 93
         self.cells = CellsLayer(game=game, terminal=terminal)
93 94
         self.add(self.cells)

+ 1 - 0
sandbox/tile/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 1 - 0
sandbox/tile/gui/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 33 - 0
sandbox/tile/gui/actor.py View File

@@ -0,0 +1,33 @@
1
+# coding: utf-8
2
+import pyglet
3
+
4
+from sandbox.tile.gui.animation import ANIMATION_WALK
5
+from sandbox.tile.gui.animation import ANIMATION_CRAWL
6
+from synergine2_cocos2d.actor import Actor
7
+
8
+
9
+class Man(Actor):
10
+    animation_image_paths = {
11
+        ANIMATION_WALK: [
12
+            'actors/man.png',
13
+            'actors/man_w1.png',
14
+            'actors/man_w2.png',
15
+            'actors/man_w3.png',
16
+            'actors/man_w4.png',
17
+            'actors/man_w5.png',
18
+            'actors/man_w6.png',
19
+            'actors/man_w7.png',
20
+            'actors/man_w8.png',
21
+            'actors/man_w9.png',
22
+            'actors/man_w10.png',
23
+        ],
24
+        ANIMATION_CRAWL: [
25
+            'actors/man_c1.png',
26
+            'actors/man_c2.png',
27
+            'actors/man_c3.png',
28
+            'actors/man_c4.png',
29
+        ]
30
+    }
31
+
32
+    def __init__(self):
33
+        super().__init__(pyglet.resource.image('actors/man.png'))

+ 5 - 0
sandbox/tile/gui/animation.py View File

@@ -0,0 +1,5 @@
1
+# coding: utf-8
2
+
3
+
4
+ANIMATION_WALK = 'WALK'
5
+ANIMATION_CRAWL = 'CRAWL'

+ 29 - 0
sandbox/tile/gui/base.py View File

@@ -0,0 +1,29 @@
1
+# coding: utf-8
2
+import random
3
+
4
+from sandbox.tile.gui.animation import ANIMATION_WALK
5
+from sandbox.tile.gui.animation import ANIMATION_CRAWL
6
+from synergine2_cocos2d.animation import Animate
7
+from synergine2_cocos2d.gui import TMXGui
8
+
9
+
10
+class Game(TMXGui):
11
+    def before_run(self) -> None:
12
+        pass
13
+        # Test
14
+        # from sandbox.tile.gui.actor import Man
15
+        # from cocos import euclid
16
+        #
17
+        # for i in range(10):
18
+        #     x = random.randint(0, 600)
19
+        #     y = random.randint(0, 300)
20
+        #     man = Man()
21
+        #     man.update_position(euclid.Vector2(x, y))
22
+        #     self.layer_manager.add_subject(man)
23
+        #     self.layer_manager.set_selectable(man)
24
+        #     man.scale = 1
25
+        #
26
+        #     if x % 2:
27
+        #         man.do(Animate(ANIMATION_WALK, 10, 4))
28
+        #     else:
29
+        #         man.do(Animate(ANIMATION_CRAWL, 20, 4))

File diff suppressed because it is too large
+ 17 - 0
sandbox/tile/maps/003/003.tmx


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


BIN
sandbox/tile/maps/003/background.png View File


BIN
sandbox/tile/maps/003/trees_64x64.png View File


+ 4 - 0
sandbox/tile/maps/003/trees_64x64.tsx View File

@@ -0,0 +1,4 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<tileset name="trees_64x64" tilewidth="64" tileheight="64" tilecount="9" columns="3">
3
+ <image source="trees_64x64.png" width="192" height="192"/>
4
+</tileset>

+ 73 - 0
sandbox/tile/run.py View File

@@ -0,0 +1,73 @@
1
+# coding: utf-8
2
+import argparse
3
+import os
4
+import sys
5
+import logging
6
+from random import seed
7
+
8
+from sandbox.tile.simulation.subject import Man
9
+
10
+synergine2_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../'))
11
+sys.path.append(synergine2_path)
12
+
13
+from sandbox.tile.simulation.base import TileStrategySimulation
14
+from sandbox.tile.simulation.base import TileStrategySubjects
15
+from synergine2.log import get_default_logger
16
+from synergine2.config import Config
17
+from sandbox.tile.terminal.base import CocosTerminal
18
+from synergine2.core import Core
19
+from synergine2.cycle import CycleManager
20
+from synergine2.terminals import TerminalManager
21
+
22
+
23
+def main(map_dir_path: str, seed_value: int=42):
24
+    seed(seed_value)
25
+
26
+    config = Config()
27
+    config.load_files(['sandbox/engulf/config.yaml'])
28
+    logger = get_default_logger(level=logging.ERROR)
29
+
30
+    simulation = TileStrategySimulation(config)
31
+    subjects = TileStrategySubjects(simulation=simulation)
32
+
33
+    man = Man(
34
+        config=config,
35
+        simulation=simulation,
36
+        position=(0, 0),
37
+    )
38
+    man.position = 0, 0
39
+    subjects.append(man)
40
+
41
+    simulation.subjects = subjects
42
+
43
+    core = Core(
44
+        config=config,
45
+        logger=logger,
46
+        simulation=simulation,
47
+        cycle_manager=CycleManager(
48
+            config=config,
49
+            logger=logger,
50
+            simulation=simulation,
51
+        ),
52
+        terminal_manager=TerminalManager(
53
+            config=config,
54
+            logger=logger,
55
+            terminals=[CocosTerminal(
56
+                config,
57
+                logger,
58
+                asynchronous=False,
59
+                map_dir_path=map_dir_path,
60
+            )]
61
+        ),
62
+        cycles_per_seconds=1 / config.core.cycle_duration,
63
+    )
64
+    core.run()
65
+
66
+if __name__ == '__main__':
67
+    parser = argparse.ArgumentParser(description='Run TileStrategy')
68
+    parser.add_argument('map_dir_path', help='map directory path')
69
+    parser.add_argument('--seed', dest='seed', default=42)
70
+
71
+    args = parser.parse_args()
72
+
73
+    main(args.map_dir_path, seed_value=args.seed)

+ 1 - 0
sandbox/tile/simulation/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 19 - 0
sandbox/tile/simulation/base.py View File

@@ -0,0 +1,19 @@
1
+# coding: utf-8
2
+from synergine2.simulation import Subject
3
+from synergine2.xyz import XYZSimulation
4
+from synergine2.xyz import XYZSubjectMixin
5
+from synergine2.xyz import XYZSubjects
6
+
7
+
8
+class TileStrategySimulation(XYZSimulation):
9
+    behaviours_classes = [
10
+
11
+    ]
12
+
13
+
14
+class TileStrategySubjects(XYZSubjects):
15
+    pass
16
+
17
+
18
+class BaseSubject(XYZSubjectMixin, Subject):
19
+    pass

+ 6 - 0
sandbox/tile/simulation/subject.py View File

@@ -0,0 +1,6 @@
1
+# coding: utf-8
2
+from sandbox.tile.simulation.base import BaseSubject
3
+
4
+
5
+class Man(BaseSubject):
6
+    pass

+ 1 - 0
sandbox/tile/terminal/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

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

@@ -0,0 +1,34 @@
1
+# coding: utf-8
2
+from sandbox.tile.simulation.subject import Man as ManSubject
3
+from sandbox.tile.gui.actor import Man as ManActor
4
+from synergine2_cocos2d.terminal import GameTerminal
5
+
6
+
7
+class CocosTerminal(GameTerminal):
8
+    subscribed_events = [
9
+
10
+    ]
11
+
12
+    def __init__(self, *args, asynchronous: bool, map_dir_path: str, **kwargs):
13
+        super().__init__(*args, **kwargs)
14
+        self.asynchronous = asynchronous
15
+        self.map_dir_path = map_dir_path
16
+
17
+    def run(self):
18
+        from sandbox.tile.gui.base import Game
19
+        from synergine2_cocos2d.gui import SubjectMapper
20
+
21
+        self.gui = Game(
22
+            self.config,
23
+            self.logger,
24
+            self,
25
+            map_dir_path=self.map_dir_path,
26
+        )
27
+
28
+        # TODO: Defind on some other place ?
29
+        self.gui.subject_mapper_factory.register_mapper(
30
+            ManSubject,
31
+            SubjectMapper(ManActor),
32
+        )
33
+
34
+        self.gui.run()

+ 1 - 1
synergine2/core.py View File

@@ -18,7 +18,7 @@ class Core(BaseObject):
18 18
         simulation: Simulation,
19 19
         cycle_manager: CycleManager,
20 20
         terminal_manager: TerminalManager=None,
21
-        cycles_per_seconds: int=1,
21
+        cycles_per_seconds: float=1.0,
22 22
     ):
23 23
         self.config = config
24 24
         self.logger = logger

+ 73 - 0
synergine2_cocos2d/actor.py View File

@@ -0,0 +1,73 @@
1
+# coding: utf-8
2
+import typing
3
+
4
+import pyglet
5
+
6
+import cocos
7
+from cocos import collision_model
8
+from cocos import euclid
9
+from synergine2_cocos2d.animation import AnimatedInterface
10
+
11
+
12
+class Actor(AnimatedInterface, cocos.sprite.Sprite):
13
+    animation_image_paths = {}  # type: typing.Dict[str, typing.List[str]]
14
+    animation_images = {}  # type: typing.Dict[str, typing.List[pyglet.image.TextureRegion]]
15
+
16
+    def __init__(
17
+        self,
18
+        image: pyglet.image.TextureRegion,
19
+        position=(0, 0),
20
+        rotation=0,
21
+        scale=1,
22
+        opacity=255,
23
+        color=(255, 255, 255),
24
+        anchor=None,
25
+        **kwargs
26
+    ):
27
+        super().__init__(
28
+            image,
29
+            position,
30
+            rotation,
31
+            scale,
32
+            opacity,
33
+            color,
34
+            anchor,
35
+            **kwargs
36
+        )
37
+        self.cshape = None  # type: collision_model.AARectShape
38
+        self.update_cshape()
39
+        self.build_animation_images()
40
+        self.current_image = image
41
+        self.need_update_cshape = False
42
+
43
+    def update_cshape(self) -> None:
44
+        self.cshape = collision_model.AARectShape(
45
+            euclid.Vector2(self.position[0], self.position[1]),
46
+            self.width // 2,
47
+            self.height // 2,
48
+        )
49
+        self.need_update_cshape = False
50
+
51
+    def update_position(self, new_position: euclid.Vector2) -> None:
52
+        self.position = new_position
53
+        self.cshape.center = new_position  # Note: if remove: strange behaviour: drag change actor position with anomaly
54
+
55
+    def build_animation_images(self) -> None:
56
+        """
57
+        Fill self.animation_images with self.animation_image_paths
58
+        :return: None
59
+        """
60
+        for animation_name, animation_image_paths in self.animation_image_paths.items():
61
+            self.animation_images[animation_name] = []
62
+            for animation_image_path in animation_image_paths:
63
+                self.animation_images[animation_name].append(pyglet.resource.image(animation_image_path))
64
+
65
+    def get_images_for_animation(self, animation_name: str) -> typing.List[pyglet.image.TextureRegion]:
66
+        return self.animation_images.get(animation_name)
67
+
68
+    def get_inanimate_image(self) -> pyglet.image.TextureRegion:
69
+        return self.current_image
70
+
71
+    def update_image(self, new_image: pyglet.image.TextureRegion):
72
+        self.image = new_image
73
+        self.image_anchor = new_image.width // 2, new_image.height // 2

+ 88 - 0
synergine2_cocos2d/animation.py View File

@@ -0,0 +1,88 @@
1
+# coding: utf-8
2
+import typing
3
+
4
+import pyglet
5
+import cocos
6
+
7
+
8
+class AnimatedInterface(object):
9
+    def get_images_for_animation(self, animation_name: str) -> typing.List[pyglet.image.TextureRegion]:
10
+        raise NotImplementedError()
11
+
12
+    def get_inanimate_image(self) -> pyglet.image.TextureRegion:
13
+        """
14
+        Use this function to specify what image have to be used when animation is finished.
15
+        :return: non inanimate pyglet.image.TextureRegion
16
+        """
17
+        raise NotImplementedError()
18
+
19
+    def update_image(self, new_image: pyglet.image.TextureRegion):
20
+        raise NotImplementedError()
21
+
22
+
23
+# TODO: regarder pyglet.image.Animation
24
+class Animate(cocos.actions.IntervalAction):
25
+    def __init__(
26
+        self,
27
+        animation_name: str,
28
+        duration: float,
29
+        cycle_duration: float,
30
+        direction: int=1,
31
+    ):
32
+        super().__init__()
33
+        self.animation_name = animation_name
34
+        self.duration = duration
35
+        self.animation_images = []  # type: typing.List[pyglet.image.TextureRegion]
36
+        self.last_step_elapsed = 0.0  # type: float
37
+        self.step_interval = None  # type: float
38
+        self.cycle_duration = cycle_duration
39
+        self.current_step = 0  # typ: int
40
+        self.target = typing.cast(AnimatedInterface, self.target)
41
+        self.direction = direction
42
+        self.reshape = False
43
+
44
+    def __reversed__(self):
45
+        return self.__class__(
46
+            self.animation_name,
47
+            self.duration,
48
+            self.cycle_duration,
49
+            self.direction * -1,
50
+        )
51
+
52
+    def set_need_to_reshape(self) -> None:
53
+        # TODO: Maybe inanimate_image is not the more appropriate image to refer ?
54
+        inanimate_image = self.target.get_inanimate_image()
55
+        for position, animation_image in enumerate(self.animation_images):
56
+            if animation_image.width != inanimate_image.width or animation_image.height != inanimate_image.height:
57
+                self.reshape = True
58
+                return
59
+
60
+    def start(self):
61
+        super().start()
62
+        self.animation_images = self.target.get_images_for_animation(self.animation_name)
63
+        self.step_interval = self.cycle_duration / len(self.animation_images)
64
+        self.set_need_to_reshape()
65
+
66
+    def stop(self):
67
+        self.target.update_image(self.target.get_inanimate_image())
68
+        super().stop()
69
+
70
+    def update(self, t):
71
+        if not self.is_time_for_new_image():
72
+            return
73
+
74
+        self.current_step += self.direction
75
+        try:
76
+            new_image = self.animation_images[self.current_step]
77
+        except IndexError:
78
+            self.current_step = 0
79
+            new_image = self.animation_images[0]
80
+
81
+        self.target.update_image(new_image)
82
+        self.last_step_elapsed = self._elapsed
83
+
84
+        if self.reshape:
85
+            self.target.need_update_cshape = True
86
+
87
+    def is_time_for_new_image(self) -> bool:
88
+        return self._elapsed - self.last_step_elapsed >= self.step_interval

+ 13 - 0
synergine2_cocos2d/exception.py View File

@@ -0,0 +1,13 @@
1
+# coding: utf-8
2
+
3
+
4
+class WorldException(Exception):
5
+    pass
6
+
7
+
8
+class PositionException(WorldException):
9
+    pass
10
+
11
+
12
+class OuterWorldPosition(PositionException):
13
+    pass

+ 669 - 67
synergine2_cocos2d/gui.py View File

@@ -1,108 +1,647 @@
1 1
 # coding: utf-8
2
-import cocos
2
+import typing
3
+import weakref
4
+from math import floor
5
+
3 6
 import pyglet
4
-from pyglet.window import key as wkey
7
+from pyglet.window import mouse
8
+
9
+import cocos
10
+from cocos import collision_model
11
+from cocos import euclid
5 12
 from cocos.director import director
6
-from cocos.layer import ScrollableLayer, Layer
13
+from cocos.layer import Layer
14
+from cocos.layer import ScrollableLayer
7 15
 from cocos.sprite import Sprite
8
-
9 16
 from synergine2.config import Config
10 17
 from synergine2.log import SynergineLogger
18
+from synergine2.simulation import Subject
11 19
 from synergine2.terminals import Terminal
12 20
 from synergine2.terminals import TerminalPackage
21
+from synergine2.xyz import XYZSubjectMixin
22
+from synergine2_cocos2d.actor import Actor
23
+from synergine2_cocos2d.exception import OuterWorldPosition
24
+from synergine2_cocos2d.layer import LayerManager
25
+from synergine2_cocos2d.middleware import TMXMiddleware
26
+
27
+
28
+# class GridManager(object):
29
+#     def __init__(
30
+#         self,
31
+#         layer: Layer,
32
+#         square_width: int,
33
+#         border: int=0,
34
+#     ):
35
+#         self.layer = layer
36
+#         self.square_width = square_width
37
+#         self.border = border
38
+#
39
+#     @property
40
+#     def final_width(self):
41
+#         return self.square_width + self.border
42
+#
43
+#     def scale_sprite(self, sprite: Sprite):
44
+#         sprite.scale_x = self.final_width / sprite.image.width
45
+#         sprite.scale_y = self.final_width / sprite.image.height
46
+#
47
+#     def position_sprite(self, sprite: Sprite, grid_position):
48
+#         grid_x = grid_position[0]
49
+#         grid_y = grid_position[1]
50
+#         sprite.position = grid_x * self.final_width, grid_y * self.final_width
51
+#
52
+#     def get_window_position(self, grid_position_x, grid_position_y):
53
+#         grid_x = grid_position_x
54
+#         grid_y = grid_position_y
55
+#         return grid_x * self.final_width, grid_y * self.final_width
56
+#
57
+#     def get_grid_position(self, window_x, window_y, z=0) -> tuple:
58
+#         window_size = director.get_window_size()
59
+#
60
+#         window_center_x = window_size[0] // 2
61
+#         window_center_y = window_size[1] // 2
62
+#
63
+#         window_relative_x = window_x - window_center_x
64
+#         window_relative_y = window_y - window_center_y
65
+#
66
+#         real_width = self.final_width * self.layer.scale
67
+#
68
+#         return int(window_relative_x // real_width),\
69
+#                int(window_relative_y // real_width),\
70
+#                z
71
+#
72
+#
73
+# class GridLayerMixin(object):
74
+#     def __init__(self, *args, **kwargs):
75
+#         square_width = kwargs.pop('square_width', 32)
76
+#         square_border = kwargs.pop('square_border', 2)
77
+#         self.grid_manager = GridManager(
78
+#             self,
79
+#             square_width=square_width,
80
+#             border=square_border,
81
+#         )
82
+#         super().__init__(*args, **kwargs)
13 83
 
14 84
 
15 85
 class GridManager(object):
16 86
     def __init__(
17 87
         self,
18
-        layer: Layer,
19
-        square_width: int,
20
-        border: int=0,
88
+        cell_width: int,
89
+        cell_height: int,
90
+        world_width: int,
91
+        world_height: int,
92
+    ) -> None:
93
+        self.cell_width = cell_width
94
+        self.cell_height = cell_height
95
+        self.world_width = world_width
96
+        self.world_height = world_height
97
+
98
+    def get_grid_position(self, pixel_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
99
+        pixel_x, pixel_y = pixel_position
100
+
101
+        cell_x = int(floor(pixel_x / self.cell_width))
102
+        cell_y = int(floor(pixel_y / self.cell_height))
103
+
104
+        if cell_x > self.world_width or cell_y > self.world_height or cell_x < 0 or cell_y < 0:
105
+            raise OuterWorldPosition('Position "{}" is outer world ({}x{})'.format(
106
+                (cell_x, cell_y),
107
+                self.world_width,
108
+                self.world_height,
109
+            ))
110
+
111
+        return cell_x, cell_y
112
+
113
+    def get_pixel_position_of_grid_position(self, grid_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
114
+        return grid_position[0] * self.cell_width + self.cell_width,\
115
+               grid_position[1] * self.cell_height + self.cell_height
116
+
117
+
118
+class MinMaxRect(cocos.cocosnode.CocosNode):
119
+    def __init__(self, layer_manager: LayerManager):
120
+        super(MinMaxRect, self).__init__()
121
+        self.layer_manager = layer_manager
122
+        self.color3 = (20, 20, 20)
123
+        self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
124
+        self.visible = False
125
+
126
+    def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
127
+        # asumes world to screen preserves order
128
+        sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
129
+        smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
130
+        self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
131
+
132
+    def draw(self):
133
+        if not self.visible:
134
+            return
135
+        pyglet.gl.glLineWidth(1)  # deprecated
136
+        pyglet.gl.glColor3ub(*self.color3)
137
+        pyglet.gl.glBegin(pyglet.gl.GL_LINE_STRIP)
138
+        for v in self.vertexes:
139
+            pyglet.gl.glVertex2f(*v)
140
+        pyglet.gl.glVertex2f(*self.vertexes[0])
141
+        pyglet.gl.glEnd()
142
+
143
+        # rectangle
144
+        pyglet.gl.glColor4f(0, 0, 0, 0.5)
145
+        pyglet.gl.glBegin(pyglet.gl.GL_QUADS)
146
+        pyglet.gl.glVertex3f(self.vertexes[0][0], self.vertexes[0][1], 0)
147
+        pyglet.gl.glVertex3f(self.vertexes[1][0], self.vertexes[1][1], 0)
148
+        pyglet.gl.glVertex3f(self.vertexes[2][0], self.vertexes[2][1], 0)
149
+        pyglet.gl.glVertex3f(self.vertexes[3][0], self.vertexes[3][1], 0)
150
+        pyglet.gl.glEnd()
151
+
152
+    def set_vertexes_from_minmax(self, minx, maxx, miny, maxy):
153
+        self.vertexes = [(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]
154
+
155
+
156
+class EditLayer(cocos.layer.Layer):
157
+    is_event_handler = True
158
+
159
+    def __init__(
160
+        self,
161
+        config: Config,
162
+        logger: SynergineLogger,
163
+        layer_manager: LayerManager,
164
+        grid_manager: GridManager,
165
+        worldview,
166
+        bindings=None,
167
+        fastness=None,
168
+        autoscroll_border=10,
169
+        autoscroll_fastness=None,
170
+        wheel_multiplier=None,
171
+        zoom_min=None,
172
+        zoom_max=None,
173
+        zoom_fastness=None,
174
+        mod_modify_selection=None,
175
+        mod_restricted_mov=None,
21 176
     ):
22
-        self.layer = layer
23
-        self.square_width = square_width
24
-        self.border = border
177
+        # TODO: Clean init params
178
+        super(EditLayer, self).__init__()
179
+
180
+        self.config = config
181
+        self.logger = logger
182
+        self.layer_manager = layer_manager
183
+        self.grid_manager = grid_manager
25 184
 
26
-    @property
27
-    def final_width(self):
28
-        return self.square_width + self.border
185
+        self.bindings = bindings
186
+        buttons = {}
187
+        modifiers = {}
188
+        for k in bindings:
189
+            buttons[bindings[k]] = 0
190
+            modifiers[bindings[k]] = 0
191
+        self.buttons = buttons
192
+        self.modifiers = modifiers
29 193
 
30
-    def scale_sprite(self, sprite: Sprite):
31
-        sprite.scale_x = self.final_width / sprite.image.width
32
-        sprite.scale_y = self.final_width / sprite.image.height
194
+        self.fastness = fastness
195
+        self.autoscroll_border = autoscroll_border
196
+        self.autoscroll_fastness = autoscroll_fastness
197
+        self.wheel_multiplier = wheel_multiplier
198
+        self.zoom_min = zoom_min
199
+        self.zoom_max = zoom_max
200
+        self.zoom_fastness = zoom_fastness
201
+        self.mod_modify_selection = mod_modify_selection
202
+        self.mod_restricted_mov = mod_restricted_mov
33 203
 
34
-    def position_sprite(self, sprite: Sprite, grid_position):
35
-        grid_x = grid_position[0]
36
-        grid_y = grid_position[1]
37
-        sprite.position = grid_x * self.final_width, grid_y * self.final_width
204
+        self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
205
+        self.weak_worldview = weakref.ref(worldview)
206
+        self.wwidth = worldview.width
207
+        self.wheight = worldview.height
38 208
 
39
-    def get_window_position(self, grid_position_x, grid_position_y):
40
-        grid_x = grid_position_x
41
-        grid_y = grid_position_y
42
-        return grid_x * self.final_width, grid_y * self.final_width
209
+        self.autoscrolling = False
210
+        self.drag_selecting = False
211
+        self.drag_moving = False
212
+        self.restricted_mov = False
213
+        self.wheel = 0
214
+        self.dragging = False
215
+        self.keyscrolling = False
216
+        self.keyscrolling_descriptor = (0, 0)
217
+        self.wdrag_start_point = (0, 0)
218
+        self.elastic_box = None  # type: MinMaxRect
219
+        self.elastic_box_wminmax = 0, 0, 0, 0
220
+        self.selection = {}
221
+        self.screen_mouse = (0, 0)
222
+        self.world_mouse = (0, 0)
223
+        self.sleft = None
224
+        self.sright = None
225
+        self.sbottom = None
226
+        self.s_top = None
43 227
 
44
-    def get_grid_position(self, window_x, window_y, z=0) -> tuple:
45
-        window_size = director.get_window_size()
228
+        # opers that change cshape must ensure it goes to False,
229
+        # selection opers must ensure it goes to True
230
+        self.selection_in_collman = True
231
+        # TODO: Hardcoded here, should be obtained from level properties or calc
232
+        # from available actors or current actors in worldview
233
+        gsize = 32 * 1.25
234
+        self.collision_manager = collision_model.CollisionManagerGrid(
235
+            -gsize,
236
+            self.wwidth + gsize,
237
+            -gsize,
238
+            self.wheight + gsize,
239
+            gsize,
240
+            gsize,
241
+        )
46 242
 
47
-        window_center_x = window_size[0] // 2
48
-        window_center_y = window_size[1] // 2
243
+        self.schedule(self.update)
244
+        self.selectable_actors = []
49 245
 
50
-        window_relative_x = window_x - window_center_x
51
-        window_relative_y = window_y - window_center_y
246
+    def set_selectable(self, actor: Actor) -> None:
247
+        self.selectable_actors.append(actor)
248
+        self.collision_manager.add(actor)
52 249
 
53
-        real_width = self.final_width * self.layer.scale
250
+    def unset_selectable(self, actor: Actor) -> None:
251
+        self.selectable_actors.remove(actor)
252
+        self.collision_manager.remove_tricky(actor)
54 253
 
55
-        return int(window_relative_x // real_width),\
56
-               int(window_relative_y // real_width),\
57
-               z
254
+    def draw(self, *args, **kwargs) -> None:
255
+        for actor in self.selectable_actors:
256
+            if actor.need_update_cshape:
257
+                if self.collision_manager.knows(actor):
258
+                    self.collision_manager.remove_tricky(actor)
259
+                    actor.update_cshape()
260
+                    self.collision_manager.add(actor)
58 261
 
262
+    def on_enter(self):
263
+        super(EditLayer, self).on_enter()
264
+        scene = self.get_ancestor(cocos.scene.Scene)
265
+        if self.elastic_box is None:
266
+            self.elastic_box = MinMaxRect(self.layer_manager)
267
+            scene.add(self.elastic_box, z=10)
59 268
 
60
-class GridLayerMixin(object):
61
-    def __init__(self, *args, **kwargs):
62
-        square_width = kwargs.pop('square_width', 32)
63
-        square_border = kwargs.pop('square_border', 2)
64
-        self.grid_manager = GridManager(
65
-            self,
66
-            square_width=square_width,
67
-            border=square_border,
269
+    def update(self, dt):
270
+        mx = self.buttons['right'] - self.buttons['left']
271
+        my = self.buttons['up'] - self.buttons['down']
272
+        dz = self.buttons['zoomin'] - self.buttons['zoomout']
273
+
274
+        # scroll
275
+        if self.autoscrolling:
276
+            self.update_autoscroll(dt)
277
+        else:
278
+            # care for keyscrolling
279
+            new_keyscrolling = ((len(self.selection) == 0) and
280
+                                (mx != 0 or my != 0))
281
+            new_keyscrolling_descriptor = (mx, my)
282
+            if ((new_keyscrolling != self.keyscrolling) or
283
+                (new_keyscrolling_descriptor != self.keyscrolling_descriptor)):
284
+                self.keyscrolling = new_keyscrolling
285
+                self.keyscrolling_descriptor = new_keyscrolling_descriptor
286
+                fastness = 1.0
287
+                if mx != 0 and my != 0:
288
+                    fastness *= 0.707106  # 1/sqrt(2)
289
+                self.autoscrolling_sdelta = (0.5 * fastness * mx, 0.5 * fastness * my)
290
+            if self.keyscrolling:
291
+                self.update_autoscroll(dt)
292
+
293
+        # selection move
294
+        if self.drag_moving:
295
+            # update positions
296
+            wx, wy = self.world_mouse
297
+            dx = wx - self.wdrag_start_point[0]
298
+            dy = wy - self.wdrag_start_point[1]
299
+            if self.restricted_mov:
300
+                if abs(dy) > abs(dx):
301
+                    dx = 0
302
+                else:
303
+                    dy = 0
304
+            dpos = euclid.Vector2(dx, dy)
305
+            for actor in self.selection:
306
+                old_pos = self.selection[actor].center
307
+                new_pos = old_pos + dpos
308
+
309
+                try:
310
+                    grid_pos = self.grid_manager.get_grid_position(new_pos)
311
+                    grid_pixel_pos = self.grid_manager.get_pixel_position_of_grid_position(grid_pos)
312
+                    actor.update_position(grid_pixel_pos)
313
+                except OuterWorldPosition:
314
+                    # don't update position
315
+                    pass
316
+
317
+        scroller = self.weak_scroller()
318
+
319
+        # zoom
320
+        zoom_change = (dz != 0 or self.wheel != 0)
321
+        if zoom_change:
322
+            if self.mouse_into_world():
323
+                wzoom_center = self.world_mouse
324
+                szoom_center = self.screen_mouse
325
+            else:
326
+                # decay to scroller unadorned
327
+                wzoom_center = None
328
+            if self.wheel != 0:
329
+                dt_dz = 0.01666666 * self.wheel
330
+                self.wheel = 0
331
+            else:
332
+                dt_dz = dt * dz
333
+            zoom = scroller.scale + dt_dz * self.zoom_fastness
334
+            if zoom < self.zoom_min:
335
+                zoom = self.zoom_min
336
+            elif zoom > self.zoom_max:
337
+                zoom = self.zoom_max
338
+            scroller.scale = zoom
339
+            if wzoom_center is not None:
340
+                # postprocess toward 'world point under mouse the same before
341
+                # and after zoom' ; other restrictions may prevent fully comply
342
+                wx1, wy1 = self.layer_manager.scrolling_manager.screen_to_world(*szoom_center)
343
+                fx = scroller.restricted_fx + (wzoom_center[0] - wx1)
344
+                fy = scroller.restricted_fy + (wzoom_center[1] - wy1)
345
+                scroller.set_focus(fx, fy)
346
+
347
+    def update_mouse_position(self, sx, sy):
348
+        self.screen_mouse = sx, sy
349
+        self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
350
+        # handle autoscroll
351
+        border = self.autoscroll_border
352
+        if border is not None:
353
+            # sleft and companions includes the border
354
+            scroller = self.weak_scroller()
355
+            self.update_view_bounds()
356
+            sdx = 0.0
357
+            if sx < self.sleft:
358
+                sdx = sx - self.sleft
359
+            elif sx > self.sright:
360
+                sdx = sx - self.sright
361
+            sdy = 0.0
362
+            if sy < self.sbottom:
363
+                sdy = sy - self.sbottom
364
+            elif sy > self.s_top:
365
+                sdy = sy - self.s_top
366
+            self.autoscrolling = sdx != 0.0 or sdy != 0.0
367
+            if self.autoscrolling:
368
+                self.autoscrolling_sdelta = (sdx / border, sdy / border)
369
+
370
+    def update_autoscroll(self, dt):
371
+        fraction_sdx, fraction_sdy = self.autoscrolling_sdelta
372
+        scroller = self.weak_scroller()
373
+        worldview = self.weak_worldview()
374
+        f = self.autoscroll_fastness
375
+        wdx = (fraction_sdx * f * dt) / scroller.scale / worldview.scale
376
+        wdy = (fraction_sdy * f * dt) / scroller.scale / worldview.scale
377
+        # ask scroller to try scroll (wdx, wdy)
378
+        fx = scroller.restricted_fx + wdx
379
+        fy = scroller.restricted_fy + wdy
380
+        scroller.set_focus(fx, fy)
381
+        self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(*self.screen_mouse)
382
+        self.adjust_elastic_box()
383
+        # self.update_view_bounds()
384
+
385
+    def update_view_bounds(self):
386
+        scroller = self.weak_scroller()
387
+        scx, scy = self.layer_manager.scrolling_manager.world_to_screen(
388
+            scroller.restricted_fx,
389
+            scroller.restricted_fy,
68 390
         )
69
-        super().__init__(*args, **kwargs)
391
+        hw = scroller.view_w / 2.0
392
+        hh = scroller.view_h / 2.0
393
+        border = self.autoscroll_border
394
+        self.sleft = scx - hw + border
395
+        self.sright = scx + hw - border
396
+        self.sbottom = scy - hh + border
397
+        self.s_top = scy + hh - border
398
+
399
+    def mouse_into_world(self):
400
+        worldview = self.weak_worldview()
401
+        # TODO: allow lower limits != 0 ?
402
+        return ((0 <= self.world_mouse[0] <= worldview.width) and
403
+               (0 <= self.world_mouse[1] <= worldview.height))
404
+
405
+    def on_key_press(self, k, m):
406
+        binds = self.bindings
407
+        if k in binds:
408
+            self.buttons[binds[k]] = 1
409
+            self.modifiers[binds[k]] = 1
410
+            return True
411
+        return False
412
+
413
+    def on_key_release(self, k, m):
414
+        binds = self.bindings
415
+        if k in binds:
416
+            self.buttons[binds[k]] = 0
417
+            self.modifiers[binds[k]] = 0
418
+            return True
419
+        return False
420
+
421
+    def on_mouse_motion(self, sx, sy, dx, dy):
422
+        self.update_mouse_position(sx, sy)
423
+
424
+    def on_mouse_leave(self, sx, sy):
425
+        self.autoscrolling = False
426
+
427
+    def on_mouse_press(self, x, y, buttons, modifiers):
428
+        if self.logger.is_debug:
429
+            rx, ry = self.layer_manager.scrolling_manager.screen_to_world(x, y)
430
+            self.logger.debug('GUI click: x: {}, y: {}, rx: {}, ry: {}'.format(x, y, rx, ry))
431
+
432
+    def on_mouse_release(self, sx, sy, button, modifiers):
433
+        # should we handle here mod_restricted_mov ?
434
+        wx, wy = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
435
+        modify_selection = modifiers & self.mod_modify_selection
436
+        if self.dragging:
437
+            # ignore all buttons except left button
438
+            if button != mouse.LEFT:
439
+                return
440
+            if self.drag_selecting:
441
+                self.end_drag_selection(wx, wy, modify_selection)
442
+            elif self.drag_moving:
443
+                self.end_drag_move(wx, wy)
444
+            self.dragging = False
445
+        else:
446
+            if button == mouse.LEFT:
447
+                self.end_click_selection(wx, wy, modify_selection)
448
+
449
+    def end_click_selection(self, wx, wy, modify_selection):
450
+        under_mouse_unique = self.single_actor_from_mouse()
451
+        if modify_selection:
452
+            # toggle selected status for unique
453
+            if under_mouse_unique in self.selection:
454
+                self.selection_remove(under_mouse_unique)
455
+            elif under_mouse_unique is not None:
456
+                self.selection_add(under_mouse_unique)
457
+        else:
458
+            # new_selected becomes the current selected
459
+            self.selection.clear()
460
+            if under_mouse_unique is not None:
461
+                self.selection_add(under_mouse_unique)
462
+
463
+    def selection_add(self, actor):
464
+        self.selection[actor] = actor.cshape.copy()
465
+
466
+    def selection_remove(self, actor):
467
+        del self.selection[actor]
468
+
469
+    def end_drag_selection(self, wx, wy, modify_selection):
470
+        new_selection = self.collision_manager.objs_into_box(*self.elastic_box_wminmax)
471
+        if not modify_selection:
472
+            # new_selected becomes the current selected
473
+            self.selection.clear()
474
+        for actor in new_selection:
475
+            self.selection_add(actor)
476
+
477
+        self.elastic_box.visible = False
478
+        self.drag_selecting = False
479
+
480
+    def on_mouse_drag(self, sx, sy, dx, dy, buttons, modifiers):
481
+        # TODO: inhibir esta llamada si estamos fuera de la client area / viewport
482
+        self.update_mouse_position(sx, sy)
483
+        if not buttons & mouse.LEFT:
484
+            # ignore except for left-btn-drag
485
+            return
486
+
487
+        if not self.dragging:
488
+            print("begin drag")
489
+            self.begin_drag()
490
+            return
491
+
492
+        if self.drag_selecting:
493
+            # update elastic box
494
+            self.adjust_elastic_box()
495
+        elif self.drag_moving:
496
+            self.restricted_mov = (modifiers & self.mod_restricted_mov)
497
+
498
+    def adjust_elastic_box(self):
499
+        # when elastic_box visible this method needs to be called any time
500
+        # world_mouse changes or screen_to_world results changes (scroll, etc)
501
+        wx0, wy0 = self.wdrag_start_point
502
+        wx1, wy1 = self.world_mouse
503
+        wminx = min(wx0, wx1)
504
+        wmaxx = max(wx0, wx1)
505
+        wminy = min(wy0, wy1)
506
+        wmaxy = max(wy0, wy1)
507
+        self.elastic_box_wminmax = wminx, wmaxx, wminy, wmaxy
508
+        self.elastic_box.adjust_from_w_minmax(*self.elastic_box_wminmax)
509
+
510
+    def begin_drag(self):
511
+        self.dragging = True
512
+        self.wdrag_start_point = self.world_mouse
513
+        under_mouse_unique = self.single_actor_from_mouse()
514
+        if under_mouse_unique is None:
515
+            # begin drag selection
516
+            self.drag_selecting = True
517
+            self.adjust_elastic_box()
518
+            self.elastic_box.visible = True
519
+            print("begin drag selection: drag_selecting, drag_moving",
520
+                  self.drag_selecting, self.drag_moving)
521
+
522
+        else:
523
+            # want drag move
524
+            if under_mouse_unique in self.selection:
525
+                # want to move current selection
526
+                pass
527
+            else:
528
+                # change selection before moving
529
+                self.selection.clear()
530
+                self.selection_add(under_mouse_unique)
531
+            self.begin_drag_move()
532
+
533
+    def begin_drag_move(self):
534
+        # begin drag move
535
+        self.drag_moving = True
536
+
537
+        # how-to update collman: remove/add vs clear/add all
538
+        # when total number of actors is low anyone will be fine,
539
+        # with high numbers, most probably we move only a small fraction
540
+        # For simplicity I choose remove/add, albeit a hybrid aproach
541
+        # can be implemented later
542
+        self.set_selection_in_collman(False)
543
+#        print "begin drag: drag_selecting, drag_moving", self.drag_selecting, self.drag_moving
544
+
545
+    def end_drag_move(self, wx, wy):
546
+        self.set_selection_in_collman(True)
547
+        for actor in self.selection:
548
+            self.selection[actor] = actor.cshape.copy()
549
+
550
+        self.drag_moving = False
551
+
552
+    def single_actor_from_mouse(self):
553
+        under_mouse = self.collision_manager.objs_touching_point(*self.world_mouse)
554
+        if len(under_mouse) == 0:
555
+            return None
556
+        # return the one with the center most near to mouse, if tie then
557
+        # an arbitrary in the tie
558
+        nearest = None
559
+        near_d = None
560
+        p = euclid.Vector2(*self.world_mouse)
561
+        for actor in under_mouse:
562
+            d = (actor.cshape.center - p).magnitude_squared()
563
+            if nearest is None or (d < near_d):
564
+                nearest = actor
565
+                near_d = d
566
+        return nearest
567
+
568
+    def set_selection_in_collman(self, bool_value):
569
+        if self.selection_in_collman == bool_value:
570
+            return
571
+        self.selection_in_collman = bool_value
572
+        if bool_value:
573
+            for actor in self.selection:
574
+                self.collision_manager.add(actor)
575
+        else:
576
+            for actor in self.selection:
577
+                self.collision_manager.remove_tricky(actor)
578
+
579
+    def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
580
+        # TODO: check if mouse over scroller viewport?
581
+        self.wheel += scroll_y * self.wheel_multiplier
70 582
 
71 583
 
72 584
 class MainLayer(ScrollableLayer):
73 585
     is_event_handler = True
74 586
 
75
-    def __init__(self, terminal: Terminal, scroll_step: int=100):
587
+    def __init__(
588
+        self,
589
+        layer_manager: LayerManager,
590
+        grid_manager: GridManager,
591
+        width: int,
592
+        height: int,
593
+        scroll_step: int=100,
594
+    ) -> None:
76 595
         super().__init__()
77
-
78
-        self.terminal = terminal
596
+        self.layer_manager = layer_manager
79 597
         self.scroll_step = scroll_step
80
-        self.grid_manager = GridManager(self, 32, border=2)
598
+        self.grid_manager = grid_manager
599
+
600
+        self.width = width
601
+        self.height = height
602
+        self.px_width = width
603
+        self.px_height = height
604
+
81 605
 
82
-        # Set scene center on center of screen
83
-        window_size = director.get_window_size()
84
-        self.position = window_size[0] // 2, window_size[1] // 2
606
+class SubjectMapper(object):
607
+    def __init__(
608
+        self,
609
+        actor_class: typing.Type[Actor],
610
+    ) -> None:
611
+        self.actor_class = actor_class
85 612
 
86
-    def on_key_press(self, key, modifiers):
87
-        if key == wkey.LEFT:
88
-            self.position = (self.position[0] + self.scroll_step, self.position[1])
613
+    def append(
614
+        self,
615
+        subject: XYZSubjectMixin,
616
+        layer_manager: LayerManager,
617
+    ) -> None:
618
+        actor = self.actor_class()
619
+        pixel_position = layer_manager.grid_manager.get_pixel_position_of_grid_position((
620
+            subject.position[0],
621
+            subject.position[1],
622
+        ))
623
+        actor.update_position(euclid.Vector2(*pixel_position))
89 624
 
90
-        if key == wkey.RIGHT:
91
-            self.position = (self.position[0] - self.scroll_step, self.position[1])
625
+        # TODO: Selectable nature must be configurable
626
+        layer_manager.add_subject(actor)
627
+        layer_manager.set_selectable(actor)
92 628
 
93
-        if key == wkey.UP:
94
-            self.position = (self.position[0], self.position[1] - self.scroll_step)
95 629
 
96
-        if key == wkey.DOWN:
97
-            self.position = (self.position[0], self.position[1] + self.scroll_step)
630
+class SubjectMapperFactory(object):
631
+    def __init__(self) -> None:
632
+        self.mapping = {}  # type: typing.Dict[typing.Type[XYZSubjectMixin], SubjectMapper]
98 633
 
99
-        if key == wkey.A:
100
-            if self.scale >= 0.3:
101
-                self.scale -= 0.2
634
+    def register_mapper(self, subject_class: typing.Type[XYZSubjectMixin], mapper: SubjectMapper) -> None:
635
+        if subject_class not in self.mapping:
636
+            self.mapping[subject_class] = mapper
637
+        else:
638
+            raise ValueError('subject_class already register with "{}"'.format(str(self.mapping[subject_class])))
102 639
 
103
-        if key == wkey.Z:
104
-            if self.scale <= 4:
105
-                self.scale += 0.2
640
+    def get_subject_mapper(self, subject: XYZSubjectMixin) -> SubjectMapper:
641
+        for subject_class, mapper in self.mapping.items():
642
+            if isinstance(subject, subject_class):
643
+                return mapper
644
+        raise KeyError('No mapper for subject "{}"'.format(str(subject)))
106 645
 
107 646
 
108 647
 class Gui(object):
@@ -114,19 +653,39 @@ class Gui(object):
114 653
             read_queue_interval: float= 1/60.0,
115 654
     ):
116 655
         self.config = config
117
-        self.logger = logger,
656
+        self.logger = logger
118 657
         self._read_queue_interval = read_queue_interval
119 658
         self.terminal = terminal
120 659
         self.cycle_duration = self.config.core.cycle_duration
121
-        cocos.director.director.init()
660
+
661
+        cocos.director.director.init(
662
+            width=640,
663
+            height=480,
664
+            vsync=True,
665
+            resizable=True,
666
+        )
667
+
668
+        # Enable blending
669
+        pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
670
+        pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
671
+
672
+        # Enable transparency
673
+        pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST)
674
+        pyglet.gl.glAlphaFunc(pyglet.gl.GL_GREATER, .1)
675
+
676
+        self.subject_mapper_factory = SubjectMapperFactory()
122 677
 
123 678
     def run(self):
679
+        self.before_run()
124 680
         pyglet.clock.schedule_interval(
125 681
             lambda *_, **__: self.terminal.read(),
126 682
             self._read_queue_interval,
127 683
         )
128 684
         cocos.director.director.run(self.get_main_scene())
129 685
 
686
+    def before_run(self) -> None:
687
+        pass
688
+
130 689
     def get_main_scene(self) -> cocos.cocosnode.CocosNode:
131 690
         raise NotImplementedError()
132 691
 
@@ -135,3 +694,46 @@ class Gui(object):
135 694
 
136 695
     def after_received(self, package: TerminalPackage):
137 696
         pass
697
+
698
+
699
+class TMXGui(Gui):
700
+    def __init__(
701
+        self,
702
+        config: Config,
703
+        logger: SynergineLogger,
704
+        terminal: Terminal,
705
+        read_queue_interval: float = 1 / 60.0,
706
+        map_dir_path: str=None,
707
+    ):
708
+        assert map_dir_path
709
+        super(TMXGui, self).__init__(
710
+            config,
711
+            logger,
712
+            terminal,
713
+            read_queue_interval,
714
+        )
715
+        self.map_dir_path = map_dir_path
716
+        self.layer_manager = LayerManager(
717
+            self.config,
718
+            self.logger,
719
+            middleware=TMXMiddleware(
720
+                self.config,
721
+                self.logger,
722
+                self.map_dir_path,
723
+            ),
724
+        )
725
+        self.layer_manager.init()
726
+        self.layer_manager.center()
727
+
728
+    def get_main_scene(self) -> cocos.cocosnode.CocosNode:
729
+        return self.layer_manager.main_scene
730
+
731
+    def before_received(self, package: TerminalPackage):
732
+        super().before_received(package)
733
+        if package.subjects:  # They are new subjects in the simulation
734
+            for subject in package.subjects:
735
+                self.append_subject(subject)
736
+
737
+    def append_subject(self, subject: XYZSubjectMixin) -> None:
738
+        subject_mapper = self.subject_mapper_factory.get_subject_mapper(subject)
739
+        subject_mapper.append(subject, self.layer_manager)

+ 120 - 0
synergine2_cocos2d/layer.py View File

@@ -0,0 +1,120 @@
1
+# coding: utf-8
2
+from pyglet.window import key
3
+
4
+import cocos
5
+
6
+from synergine2.config import Config
7
+from synergine2.log import SynergineLogger
8
+from synergine2_cocos2d.middleware import MapMiddleware
9
+
10
+if False:
11
+    from synergine2_cocos2d.actor import Actor
12
+    from synergine2_cocos2d.gui import GridManager
13
+
14
+
15
+class LayerManager(object):
16
+    def __init__(
17
+        self,
18
+        config: Config,
19
+        logger: SynergineLogger,
20
+        middleware: MapMiddleware,
21
+    ) -> None:
22
+        self.config = config
23
+        self.logger = logger
24
+        self.middleware = middleware
25
+
26
+        self.grid_manager = None  # type: GridManager
27
+        self.scrolling_manager = None  # type: cocos.layer.ScrollingManager
28
+        self.main_scene = None  # type: cocos.scene.Scene
29
+        self.main_layer = None  # type: cocos.layer.Layer
30
+        self.edit_layer = None  # TODO type
31
+
32
+        self.background_sprite = None  # type: cocos.sprite.Sprite
33
+        self.ground_layer = None  # type: cocos.tiles.RectMapLayer
34
+        self.subject_layer = None  # type: cocos.layer.ScrollableLayer
35
+        self.top_layer = None  # type: cocos.tiles.RectMapLayer
36
+
37
+    def init(self) -> None:
38
+        # TODO: cyclic import
39
+        from synergine2_cocos2d.gui import MainLayer
40
+        from synergine2_cocos2d.gui import EditLayer
41
+        from synergine2_cocos2d.gui import GridManager
42
+
43
+        self.middleware.init()
44
+
45
+        self.grid_manager = GridManager(
46
+            self.middleware.get_cell_width(),
47
+            self.middleware.get_cell_height(),
48
+            self.middleware.get_world_width(),
49
+            self.middleware.get_world_height(),
50
+        )
51
+
52
+        self.main_scene = cocos.scene.Scene()
53
+        self.scrolling_manager = cocos.layer.ScrollingManager()
54
+
55
+        self.main_layer = MainLayer(
56
+            self,
57
+            self.grid_manager,
58
+            **{
59
+                'width': 1200,  # Note: world size
60
+                'height': 1000,  # Note: world size
61
+            }
62
+        )
63
+        self.edit_layer = EditLayer(
64
+            self.config,
65
+            self.logger,
66
+            self,
67
+            self.grid_manager,
68
+            self.main_layer,
69
+            **{
70
+                'bindings': {
71
+                    key.LEFT: 'left',
72
+                    key.RIGHT: 'right',
73
+                    key.UP: 'up',
74
+                    key.DOWN: 'down',
75
+                    key.NUM_ADD: 'zoomin',
76
+                    key.NUM_SUBTRACT: 'zoomout'
77
+                },
78
+                'mod_modify_selection': key.MOD_SHIFT,
79
+                'mod_restricted_mov': key.MOD_ACCEL,
80
+                'fastness': 160.0,
81
+                'autoscroll_border': 20.0,  # in pixels, float; None disables autoscroll
82
+                'autoscroll_fastness': 320.0,
83
+                'wheel_multiplier': 2.5,
84
+                'zoom_min': 0.1,
85
+                'zoom_max': 2.0,
86
+                'zoom_fastness': 1.0
87
+            }
88
+        )
89
+
90
+        self.main_scene.add(self.scrolling_manager)
91
+        self.scrolling_manager.add(self.main_layer, z=0)
92
+        self.main_scene.add(self.edit_layer)
93
+
94
+        self.background_sprite = self.middleware.get_background_sprite()
95
+        self.ground_layer = self.middleware.get_ground_layer()
96
+        self.subject_layer = cocos.layer.ScrollableLayer()
97
+        self.top_layer = self.middleware.get_top_layer()
98
+
99
+        self.main_layer.add(self.background_sprite)
100
+        self.main_layer.add(self.ground_layer)
101
+        self.main_layer.add(self.subject_layer)
102
+        self.main_layer.add(self.top_layer)
103
+
104
+    def center(self):
105
+        self.background_sprite.position = 0 + (self.background_sprite.width/2), 0 + (self.background_sprite.height/2)
106
+        self.ground_layer.set_view(0, 0, self.ground_layer.px_width, self.ground_layer.px_height)
107
+        self.subject_layer.position = 0, 0
108
+        self.top_layer.set_view(0, 0, self.top_layer.px_width, self.top_layer.px_height)
109
+
110
+    def add_subject(self, subject: 'Actor') -> None:
111
+        self.subject_layer.add(subject)
112
+
113
+    def remove_subject(self, subject: 'Actor') -> None:
114
+        self.subject_layer.remove(subject)
115
+
116
+    def set_selectable(self, subject: 'Actor') -> None:
117
+        self.edit_layer.set_selectable(subject)
118
+
119
+    def unset_selectable(self, subject: 'Actor') -> None:
120
+        self.edit_layer.unset_selectable(subject)

+ 74 - 0
synergine2_cocos2d/middleware.py View File

@@ -0,0 +1,74 @@
1
+# coding: utf-8
2
+import os
3
+
4
+import cocos
5
+from synergine2.config import Config
6
+from synergine2.log import SynergineLogger
7
+
8
+
9
+class MapMiddleware(object):
10
+    def __init__(
11
+        self,
12
+        config: Config,
13
+        logger: SynergineLogger,
14
+        map_dir_path: str,
15
+    ) -> None:
16
+        self.config = config
17
+        self.logger = logger
18
+        self.map_dir_path = map_dir_path
19
+        self.tmx = None
20
+
21
+    def init(self) -> None:
22
+        self.tmx = cocos.tiles.load(os.path.join(
23
+            self.map_dir_path,
24
+            '{}.tmx'.format(os.path.basename(self.map_dir_path)),
25
+        ))
26
+
27
+    def get_background_sprite(self) -> cocos.sprite.Sprite:
28
+        raise NotImplementedError()
29
+
30
+    def get_ground_layer(self) -> cocos.tiles.RectMapLayer:
31
+        raise NotImplementedError()
32
+
33
+    def get_top_layer(self) -> cocos.tiles.RectMapLayer:
34
+        raise NotImplementedError()
35
+
36
+    def get_world_height(self) -> int:
37
+        raise NotImplementedError()
38
+
39
+    def get_world_width(self) -> int:
40
+        raise NotImplementedError()
41
+
42
+    def get_cell_height(self) -> int:
43
+        raise NotImplementedError()
44
+
45
+    def get_cell_width(self) -> int:
46
+        raise NotImplementedError()
47
+
48
+
49
+class TMXMiddleware(MapMiddleware):
50
+    def get_background_sprite(self) -> cocos.sprite.Sprite:
51
+        return cocos.sprite.Sprite(os.path.join(
52
+            self.map_dir_path,
53
+            'background.png',
54
+        ))
55
+
56
+    def get_ground_layer(self) -> cocos.tiles.RectMapLayer:
57
+        assert self.tmx
58
+        return self.tmx['ground']
59
+
60
+    def get_top_layer(self) -> cocos.tiles.RectMapLayer:
61
+        assert self.tmx
62
+        return self.tmx['top']
63
+
64
+    def get_world_height(self) -> int:
65
+        return len(self.tmx['ground'].cells[0])
66
+
67
+    def get_world_width(self) -> int:
68
+        return len(self.tmx['ground'].cells)
69
+
70
+    def get_cell_height(self) -> int:
71
+        return self.tmx['ground'].cells[0][0].size[1]
72
+
73
+    def get_cell_width(self) -> int:
74
+        return self.tmx['ground'].cells[0][0].size[0]

+ 16 - 0
synergine2_cocos2d/terminal.py View File

@@ -0,0 +1,16 @@
1
+# coding: utf-8
2
+from synergine2.terminals import Terminal, TerminalPackage
3
+
4
+
5
+class GameTerminal(Terminal):
6
+    def __init__(self, *args, **kwargs):
7
+        super().__init__(*args, **kwargs)
8
+        self.gui = None
9
+
10
+    def receive(self, package: TerminalPackage):
11
+        self.gui.before_received(package)
12
+        super().receive(package)
13
+        self.gui.after_received(package)
14
+
15
+    def run(self):
16
+        raise NotImplementedError()