ya2 · news · projects · code · about

housekeeping: modules in pmachines/
authorFlavio Calva <f.calva@gmail.com>
Sat, 24 Dec 2022 07:43:20 +0000 (09:43 +0200)
committerFlavio Calva <f.calva@gmail.com>
Sat, 24 Dec 2022 07:43:20 +0000 (09:43 +0200)
13 files changed:
assets/locale/po/it_IT.po
main.py
pmachines/app.py [deleted file]
pmachines/application/__init__.py [new file with mode: 0644]
pmachines/application/application.py [new file with mode: 0755]
pmachines/application/persistent.py [new file with mode: 0644]
pmachines/gui/menu.py
pmachines/persistent.py [deleted file]
pmachines/scene.py [deleted file]
pmachines/scene/__init__.py [new file with mode: 0644]
pmachines/scene/scene.py [new file with mode: 0644]
setup.py
tests/test_main.py

index 48da421b089507517c9190782423181ca42d08df..8d8ee786e7da575a92174943a1d3eb46c60f6ce1 100644 (file)
@@ -17,34 +17,6 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: pmachines/scene.py:106
-msgid "Scene: "
-msgstr "Scena: "
-
-#: pmachines/scene.py:305 pmachines/gui/menu.py:116
-msgid "Exit"
-msgstr "Esci"
-
-#: pmachines/scene.py:306
-msgid "Instructions"
-msgstr "Istruzioni"
-
-#: pmachines/scene.py:307
-msgid "Run"
-msgstr "Vai"
-
-#: pmachines/scene.py:313
-msgid "Editor"
-msgstr "Editor"
-
-#: pmachines/scene.py:636
-msgid "You win!"
-msgstr "Hai vinto!"
-
-#: pmachines/scene.py:716
-msgid "You have failed!"
-msgstr "Hai perso!"
-
 #: pmachines/editor/augmented_frame.py:48
 msgid "Collapse/Expand"
 msgstr "Nascondi/Mostra"
@@ -251,6 +223,10 @@ msgstr "Opzioni"
 msgid "Credits"
 msgstr "Riconoscimenti"
 
+#: pmachines/gui/menu.py:116 pmachines/scene/scene.py:305
+msgid "Exit"
+msgstr "Esci"
+
 #: pmachines/gui/menu.py:126 pmachines/gui/menu.py:129
 #: pmachines/gui/menu.py:330
 msgid "English"
@@ -322,6 +298,30 @@ msgstr ""
 "  \ 1scale\ 1Luisa Tenuta\ 2\n"
 "  \ 1scale\ 1Damiana Ercolani\ 2"
 
+#: pmachines/scene/scene.py:106
+msgid "Scene: "
+msgstr "Scena: "
+
+#: pmachines/scene/scene.py:306
+msgid "Instructions"
+msgstr "Istruzioni"
+
+#: pmachines/scene/scene.py:307
+msgid "Run"
+msgstr "Vai"
+
+#: pmachines/scene/scene.py:313
+msgid "Editor"
+msgstr "Editor"
+
+#: pmachines/scene/scene.py:636
+msgid "You win!"
+msgstr "Hai vinto!"
+
+#: pmachines/scene/scene.py:716
+msgid "You have failed!"
+msgstr "Hai perso!"
+
 #~ msgid ""
 #~ "Goal: the left box must hit the right box\n"
 #~ "\n"
diff --git a/main.py b/main.py
index 0cfefe0bc303ad4c20d5341eb85c3be6b89ac6c8..cd2889eb748b8df9e9f26b117426b6bffe92b54e 100644 (file)
--- a/main.py
+++ b/main.py
@@ -5,7 +5,7 @@ from ya2.utils.gui import GuiTools
 if '--version' in argv: GuiTools.no_window()
 from os.path import exists
 from p3d_appimage import AppImageBuilder
-from pmachines.app import Pmachines
+from pmachines.application.application import Pmachines
 from traceback import print_exc
 
 
diff --git a/pmachines/app.py b/pmachines/app.py
deleted file mode 100755 (executable)
index b1c29fc..0000000
+++ /dev/null
@@ -1,333 +0,0 @@
-import argparse
-import simplepbr
-#import gltf
-from json import loads
-from sys import platform, exit, argv
-from platform import node
-from logging import info, debug
-from os.path import exists
-from os import makedirs
-from multiprocessing import cpu_count
-from panda3d.core import Filename, load_prc_file_data, AntialiasAttrib, \
-    WindowProperties, LVector2i, TextNode, GraphicsBuffer
-from panda3d.bullet import BulletWorld, BulletDebugNode
-from direct.showbase.ShowBase import ShowBase
-from direct.gui.OnscreenText import OnscreenText
-from direct.fsm.FSM import FSM
-from pmachines.audio.music import MusicMgr
-from pmachines.items.background import Background
-from pmachines.gui.menu import Menu
-from pmachines.scene import Scene
-from pmachines.persistent import Persistent
-from ya2.utils.dictfile import DctFile
-from ya2.utils.logics import LogicsTools
-from ya2.utils.language import LanguageManager
-from ya2.utils.log import WindowedLogManager
-from ya2.utils.functional import FunctionalTest
-from ya2.utils.asserts import Assert
-from ya2.utils.gfx import DirectGuiMixin
-
-
-class MainFsm(FSM):
-
-    def __init__(self, pmachines):
-        super().__init__('Main FSM')
-        self._pmachines = pmachines
-        self.accept('new_scene', self.__on_new_scene)
-
-    def enterMenu(self):
-        self._pmachines.on_menu_enter()
-
-    def exitMenu(self):
-        self._pmachines.on_menu_exit()
-        self.__do_asserts()
-        DirectGuiMixin.clear_tooltips()
-
-    def enterScene(self, cls):
-        self._pmachines.on_scene_enter(cls)
-
-    def exitScene(self):
-        self._pmachines.on_scene_exit()
-        self.__do_asserts()
-        DirectGuiMixin.clear_tooltips()
-
-    def __on_new_scene(self):
-        self.demand('Scene', None)
-
-    def __do_asserts(self):
-        args = self._pmachines._args
-        if not LogicsTools.in_build or args.functional_test or args.functional_ref:
-            Assert.assert_threads()
-            Assert.assert_tasks()
-            Assert.assert_render3d()
-            Assert.assert_render2d()
-            Assert.assert_aspect2d()
-            Assert.assert_events()
-            Assert.assert_buffers()
-
-    def enterOff(self):
-        self.ignore('new_scene')
-
-
-class Pmachines:
-
-    @staticmethod
-    def scenes():
-        with open('assets/scenes/index.json') as f:
-            json = loads(f.read())
-        return json['list']
-
-    def __init__(self):
-        info('platform: %s' % platform)
-        info('exists main.py: %s' % exists('main.py'))
-        self._args = args = self._parse_args()
-        self._configure(args)
-        self.base = ShowBase()
-        self._pipeline = None
-        self.is_update_run = args.update
-        self.is_version_run = args.version
-        self.log_mgr = WindowedLogManager.init_cls()
-        self._pos_mgr = {}
-        self._prepare_window(args)
-        self._fsm = MainFsm(self)
-        self._fsm.demand('Start')  # otherwise it is Off and cleanup in tests won't work
-        if args.update:
-            return
-        if args.functional_test:
-            self._options['settings']['volume'] = 0
-        self._music = MusicMgr(self._options['settings']['volume'])
-        self.lang_mgr = LanguageManager(self._options['settings']['language'],
-                                'pmachines',
-                                'assets/locale/')
-        if args.functional_test or args.functional_ref:
-            FunctionalTest(args.functional_ref, self._pos_mgr, 'pmachines')
-        if not LogicsTools.in_build or args.functional_test or args.functional_ref:
-            self.__fps_lst = []
-            taskMgr.do_method_later(1.0, self.__assert_fps, 'assert_fps')
-
-    def start(self):
-        if self._args.screenshot:
-            #cls = [cls for cls in self.scenes if cls.__name__ == self._args.screenshot][0]
-            scene = Scene(BulletWorld(), None, True, False, lambda: None, self.scenes(), self._pos_mgr, None, None, None, self._args.screenshot, None, None)
-            scene.screenshot()
-            scene.destroy()
-            exit()
-        elif self._options['development']['auto_start']:
-            # mod_name = 'pmachines.scenes.scene_' + self._options['development']['auto_start']
-            # for member in import_module(mod_name).__dict__.values():
-            #     if isclass(member) and issubclass(member, Scene) and \
-            #             member != Scene:
-            #         cls = member
-            self._fsm.demand('Scene', self._options['development']['auto_start'])
-        else:
-            Scene.scenes_done = self.__persistent.scenes_done
-            self._fsm.demand('Menu')
-
-    def on_menu_enter(self):
-        self._menu_bg = Background()
-        self._menu = Menu(
-            self._fsm, self.lang_mgr, self._options, self._music,
-            self._pipeline, self.scenes(), self._args.functional_test or self._args.functional_ref,
-            self._pos_mgr)
-
-    def on_home(self):
-        Scene.scenes_done = self.__persistent.scenes_done
-        self._fsm.demand('Menu')
-
-    def on_menu_exit(self):
-        self._menu_bg.destroy()
-        self._menu.destroy()
-
-    def on_scene_enter(self, scene_name):
-        self._set_physics()
-        self._scene = Scene(
-            self.world, self.on_home,
-            self._options['development']['auto_close_instructions'],
-            self._options['development']['debug_items'],
-            self.reload,
-            self.scenes(),
-            self._pos_mgr,
-            self._args.functional_test or self._args.functional_ref,
-            self._options['development']['mouse_coords'],
-            self.__persistent,
-            scene_name,
-            self._options['development']['editor'],
-            self._options['development']['auto_start_editor'])
-
-    def on_scene_exit(self):
-        self._unset_physics()
-        self._scene.destroy()
-
-    def reload(self, cls):
-        self._fsm.demand('Scene', cls)
-
-    def _configure(self, args):
-        load_prc_file_data('', 'window-title pmachines')
-        load_prc_file_data('', 'framebuffer-srgb true')
-        load_prc_file_data('', 'sync-video true')
-        if args.functional_test or args.functional_ref:
-            load_prc_file_data('', 'win-size 1360 768')
-            # otherwise it is not centered in exwm
-        # load_prc_file_data('', 'threading-model Cull/Draw')
-        # it freezes when you go to the next scene
-        if args.screenshot:
-            load_prc_file_data('', 'window-type offscreen')
-            load_prc_file_data('', 'audio-library-name null')
-
-    def _parse_args(self):
-        parser = argparse.ArgumentParser()
-        parser.add_argument('--update', action='store_true')
-        parser.add_argument('--version', action='store_true')
-        parser.add_argument('--optfile')
-        parser.add_argument('--screenshot')
-        parser.add_argument('--functional-test', action='store_true')
-        parser.add_argument('--functional-ref', action='store_true')
-        cmd_line = [arg for arg in iter(argv[1:]) if not arg.startswith('-psn_')]
-        args = parser.parse_args(cmd_line)
-        return args
-
-    def _prepare_window(self, args):
-        data_path = ''
-        if (platform.startswith('win') or platform.startswith('linux')) and (
-                not exists('main.py') or __file__.startswith('/app/bin/')):
-            # it is the deployed version for windows
-            data_path = str(Filename.get_user_appdata_directory()) + '/pmachines'
-            home = '/home/flavio'  # we must force this for wine
-            if data_path.startswith('/c/users/') and exists(home + '/.wine/'):
-                data_path = home + '/.wine/drive_' + data_path[1:]
-            info('creating dirs: %s' % data_path)
-            makedirs(data_path, exist_ok=True)
-        optfile = args.optfile if args.optfile else 'options.ini'
-        info('data path: %s' % data_path)
-        info('option file: %s' % optfile)
-        info('fixed path: %s' % LogicsTools.platform_specific_path(data_path + '/' + optfile))
-        default_opt = {
-            'settings': {
-                'volume': 1,
-                'language': 'en',
-                'fullscreen': 1,
-                'resolution': '',
-                'antialiasing': 1,
-                'shadows': 1},
-            'save': {
-                'scenes_done': []
-            },
-            'development': {
-                'simplepbr': 1,
-                'verbose_log': 0,
-                'physics_debug': 0,
-                'auto_start': 0,
-                'auto_close_instructions': 0,
-                'show_buffers': 0,
-                'debug_items': 0,
-                'mouse_coords': 0,
-                'fps': 0,
-                'editor': 0,
-                'auto_start_editor': 0}}
-        opt_path = LogicsTools.platform_specific_path(data_path + '/' + optfile) if data_path else optfile
-        opt_exists = exists(opt_path)
-        self._options = DctFile(
-            LogicsTools.platform_specific_path(data_path + '/' + optfile) if data_path else optfile,
-            default_opt)
-        if not opt_exists:
-            self._options.store()
-        self.__persistent = Persistent(self._options['save']['scenes_done'], self._options)
-        Scene.scenes_done = self.__persistent.scenes_done
-        res = self._options['settings']['resolution']
-        if res:
-            res = LVector2i(*[int(_res) for _res in res.split('x')])
-        else:
-            resolutions = []
-            if not self.is_version_run:
-                d_i = base.pipe.get_display_information()
-                def _res(idx):
-                    return d_i.get_display_mode_width(idx), \
-                        d_i.get_display_mode_height(idx)
-                resolutions = [
-                    _res(idx) for idx in range(d_i.get_total_display_modes())]
-                res = sorted(resolutions)[-1]
-        fullscreen = self._options['settings']['fullscreen']
-        props = WindowProperties()
-        if args.functional_test or args.functional_ref:
-            fullscreen = False
-        elif not self.is_version_run:
-            props.set_size(res)
-        props.set_fullscreen(fullscreen)
-        props.set_icon_filename('assets/images/icon/pmachines.ico')
-        if not args.screenshot and not self.is_version_run and base.win and not isinstance(base.win, GraphicsBuffer):
-            base.win.request_properties(props)
-        #gltf.patch_loader(base.loader)
-        if self._options['development']['simplepbr'] and not self.is_version_run and base.win:
-            self._pipeline = simplepbr.init(
-                use_normal_maps=True,
-                use_emission_maps=False,
-                use_occlusion_maps=True,
-                msaa_samples=4 if self._options['settings']['antialiasing'] else 1,
-                enable_shadows=int(self._options['settings']['shadows']))
-            debug(f'msaa: {self._pipeline.msaa_samples}')
-            debug(f'shadows: {self._pipeline.enable_shadows}')
-        render.setAntialias(AntialiasAttrib.MAuto)
-        self.base.set_background_color(0, 0, 0, 1)
-        self.base.disable_mouse()
-        if self._options['development']['show_buffers']:
-            base.bufferViewer.toggleEnable()
-        if self._options['development']['fps']:
-            base.set_frame_rate_meter(True)
-        #self.base.accept('window-event', self._on_win_evt)
-        self.base.accept('aspectRatioChanged', self._on_aspect_ratio_changed)
-        if self._options['development']['mouse_coords']:
-            coords_txt = OnscreenText(
-                '', parent=base.a2dTopRight, scale=0.04,
-                pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
-            def update_coords(task):
-                txt = '%s %s' % (int(base.win.get_pointer(0).x),
-                                 int(base.win.get_pointer(0).y))
-                coords_txt['text'] = txt
-                return task.cont
-            taskMgr.add(update_coords, 'update_coords')
-
-    def _set_physics(self):
-        if self._options['development']['physics_debug']:
-            debug_node = BulletDebugNode('Debug')
-            debug_node.show_wireframe(True)
-            debug_node.show_constraints(True)
-            debug_node.show_bounding_boxes(True)
-            debug_node.show_normals(True)
-            self._debug_np = render.attach_new_node(debug_node)
-            self._debug_np.show()
-        self.world = BulletWorld()
-        self.world.set_gravity((0, 0, -9.81))
-        if self._options['development']['physics_debug']:
-            self.world.set_debug_node(self._debug_np.node())
-        def update(task):
-            dt = globalClock.get_dt()
-            self.world.do_physics(dt, 10, 1/180)
-            return task.cont
-        self._phys_tsk = taskMgr.add(update, 'update')
-
-    def _unset_physics(self):
-        if self._options['development']['physics_debug']:
-            self._debug_np.remove_node()
-        self.world = None
-        taskMgr.remove(self._phys_tsk)
-
-    def _on_aspect_ratio_changed(self):
-        if self._fsm.state == 'Scene':
-            self._scene.on_aspect_ratio_changed()
-
-    def __assert_fps(self, task):
-        if len(self.__fps_lst) > 3:
-            self.__fps_lst.pop(0)
-        self.__fps_lst += [globalClock.average_frame_rate]
-        if len(self.__fps_lst) == 4:
-            fps_threshold = 55 if cpu_count() >= 4 and node() != 'localhost.localdomain' else 10  # i.e. it is the builder machine
-            assert not all(fps < fps_threshold for fps in self.__fps_lst), 'low fps %s' % self.__fps_lst
-        return task.again
-
-    def destroy(self):
-        self._fsm.cleanup()
-        self.base.destroy()
-
-    def run(self):
-        self.start()
-        self.base.run()
diff --git a/pmachines/application/__init__.py b/pmachines/application/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pmachines/application/application.py b/pmachines/application/application.py
new file mode 100755 (executable)
index 0000000..b54c41d
--- /dev/null
@@ -0,0 +1,333 @@
+import argparse
+import simplepbr
+#import gltf
+from json import loads
+from sys import platform, exit, argv
+from platform import node
+from logging import info, debug
+from os.path import exists
+from os import makedirs
+from multiprocessing import cpu_count
+from panda3d.core import Filename, load_prc_file_data, AntialiasAttrib, \
+    WindowProperties, LVector2i, TextNode, GraphicsBuffer
+from panda3d.bullet import BulletWorld, BulletDebugNode
+from direct.showbase.ShowBase import ShowBase
+from direct.gui.OnscreenText import OnscreenText
+from direct.fsm.FSM import FSM
+from pmachines.audio.music import MusicMgr
+from pmachines.items.background import Background
+from pmachines.gui.menu import Menu
+from pmachines.scene.scene import Scene
+from pmachines.application.persistent import Persistent
+from ya2.utils.dictfile import DctFile
+from ya2.utils.logics import LogicsTools
+from ya2.utils.language import LanguageManager
+from ya2.utils.log import WindowedLogManager
+from ya2.utils.functional import FunctionalTest
+from ya2.utils.asserts import Assert
+from ya2.utils.gfx import DirectGuiMixin
+
+
+class MainFsm(FSM):
+
+    def __init__(self, pmachines):
+        super().__init__('Main FSM')
+        self._pmachines = pmachines
+        self.accept('new_scene', self.__on_new_scene)
+
+    def enterMenu(self):
+        self._pmachines.on_menu_enter()
+
+    def exitMenu(self):
+        self._pmachines.on_menu_exit()
+        self.__do_asserts()
+        DirectGuiMixin.clear_tooltips()
+
+    def enterScene(self, cls):
+        self._pmachines.on_scene_enter(cls)
+
+    def exitScene(self):
+        self._pmachines.on_scene_exit()
+        self.__do_asserts()
+        DirectGuiMixin.clear_tooltips()
+
+    def __on_new_scene(self):
+        self.demand('Scene', None)
+
+    def __do_asserts(self):
+        args = self._pmachines._args
+        if not LogicsTools.in_build or args.functional_test or args.functional_ref:
+            Assert.assert_threads()
+            Assert.assert_tasks()
+            Assert.assert_render3d()
+            Assert.assert_render2d()
+            Assert.assert_aspect2d()
+            Assert.assert_events()
+            Assert.assert_buffers()
+
+    def enterOff(self):
+        self.ignore('new_scene')
+
+
+class Pmachines:
+
+    @staticmethod
+    def scenes():
+        with open('assets/scenes/index.json') as f:
+            json = loads(f.read())
+        return json['list']
+
+    def __init__(self):
+        info('platform: %s' % platform)
+        info('exists main.py: %s' % exists('main.py'))
+        self._args = args = self._parse_args()
+        self._configure(args)
+        self.base = ShowBase()
+        self._pipeline = None
+        self.is_update_run = args.update
+        self.is_version_run = args.version
+        self.log_mgr = WindowedLogManager.init_cls()
+        self._pos_mgr = {}
+        self._prepare_window(args)
+        self._fsm = MainFsm(self)
+        self._fsm.demand('Start')  # otherwise it is Off and cleanup in tests won't work
+        if args.update:
+            return
+        if args.functional_test:
+            self._options['settings']['volume'] = 0
+        self._music = MusicMgr(self._options['settings']['volume'])
+        self.lang_mgr = LanguageManager(self._options['settings']['language'],
+                                'pmachines',
+                                'assets/locale/')
+        if args.functional_test or args.functional_ref:
+            FunctionalTest(args.functional_ref, self._pos_mgr, 'pmachines')
+        if not LogicsTools.in_build or args.functional_test or args.functional_ref:
+            self.__fps_lst = []
+            taskMgr.do_method_later(1.0, self.__assert_fps, 'assert_fps')
+
+    def start(self):
+        if self._args.screenshot:
+            #cls = [cls for cls in self.scenes if cls.__name__ == self._args.screenshot][0]
+            scene = Scene(BulletWorld(), None, True, False, lambda: None, self.scenes(), self._pos_mgr, None, None, None, self._args.screenshot, None, None)
+            scene.screenshot()
+            scene.destroy()
+            exit()
+        elif self._options['development']['auto_start']:
+            # mod_name = 'pmachines.scenes.scene_' + self._options['development']['auto_start']
+            # for member in import_module(mod_name).__dict__.values():
+            #     if isclass(member) and issubclass(member, Scene) and \
+            #             member != Scene:
+            #         cls = member
+            self._fsm.demand('Scene', self._options['development']['auto_start'])
+        else:
+            Scene.scenes_done = self.__persistent.scenes_done
+            self._fsm.demand('Menu')
+
+    def on_menu_enter(self):
+        self._menu_bg = Background()
+        self._menu = Menu(
+            self._fsm, self.lang_mgr, self._options, self._music,
+            self._pipeline, self.scenes(), self._args.functional_test or self._args.functional_ref,
+            self._pos_mgr)
+
+    def on_home(self):
+        Scene.scenes_done = self.__persistent.scenes_done
+        self._fsm.demand('Menu')
+
+    def on_menu_exit(self):
+        self._menu_bg.destroy()
+        self._menu.destroy()
+
+    def on_scene_enter(self, scene_name):
+        self._set_physics()
+        self._scene = Scene(
+            self.world, self.on_home,
+            self._options['development']['auto_close_instructions'],
+            self._options['development']['debug_items'],
+            self.reload,
+            self.scenes(),
+            self._pos_mgr,
+            self._args.functional_test or self._args.functional_ref,
+            self._options['development']['mouse_coords'],
+            self.__persistent,
+            scene_name,
+            self._options['development']['editor'],
+            self._options['development']['auto_start_editor'])
+
+    def on_scene_exit(self):
+        self._unset_physics()
+        self._scene.destroy()
+
+    def reload(self, cls):
+        self._fsm.demand('Scene', cls)
+
+    def _configure(self, args):
+        load_prc_file_data('', 'window-title pmachines')
+        load_prc_file_data('', 'framebuffer-srgb true')
+        load_prc_file_data('', 'sync-video true')
+        if args.functional_test or args.functional_ref:
+            load_prc_file_data('', 'win-size 1360 768')
+            # otherwise it is not centered in exwm
+        # load_prc_file_data('', 'threading-model Cull/Draw')
+        # it freezes when you go to the next scene
+        if args.screenshot:
+            load_prc_file_data('', 'window-type offscreen')
+            load_prc_file_data('', 'audio-library-name null')
+
+    def _parse_args(self):
+        parser = argparse.ArgumentParser()
+        parser.add_argument('--update', action='store_true')
+        parser.add_argument('--version', action='store_true')
+        parser.add_argument('--optfile')
+        parser.add_argument('--screenshot')
+        parser.add_argument('--functional-test', action='store_true')
+        parser.add_argument('--functional-ref', action='store_true')
+        cmd_line = [arg for arg in iter(argv[1:]) if not arg.startswith('-psn_')]
+        args = parser.parse_args(cmd_line)
+        return args
+
+    def _prepare_window(self, args):
+        data_path = ''
+        if (platform.startswith('win') or platform.startswith('linux')) and (
+                not exists('main.py') or __file__.startswith('/app/bin/')):
+            # it is the deployed version for windows
+            data_path = str(Filename.get_user_appdata_directory()) + '/pmachines'
+            home = '/home/flavio'  # we must force this for wine
+            if data_path.startswith('/c/users/') and exists(home + '/.wine/'):
+                data_path = home + '/.wine/drive_' + data_path[1:]
+            info('creating dirs: %s' % data_path)
+            makedirs(data_path, exist_ok=True)
+        optfile = args.optfile if args.optfile else 'options.ini'
+        info('data path: %s' % data_path)
+        info('option file: %s' % optfile)
+        info('fixed path: %s' % LogicsTools.platform_specific_path(data_path + '/' + optfile))
+        default_opt = {
+            'settings': {
+                'volume': 1,
+                'language': 'en',
+                'fullscreen': 1,
+                'resolution': '',
+                'antialiasing': 1,
+                'shadows': 1},
+            'save': {
+                'scenes_done': []
+            },
+            'development': {
+                'simplepbr': 1,
+                'verbose_log': 0,
+                'physics_debug': 0,
+                'auto_start': 0,
+                'auto_close_instructions': 0,
+                'show_buffers': 0,
+                'debug_items': 0,
+                'mouse_coords': 0,
+                'fps': 0,
+                'editor': 0,
+                'auto_start_editor': 0}}
+        opt_path = LogicsTools.platform_specific_path(data_path + '/' + optfile) if data_path else optfile
+        opt_exists = exists(opt_path)
+        self._options = DctFile(
+            LogicsTools.platform_specific_path(data_path + '/' + optfile) if data_path else optfile,
+            default_opt)
+        if not opt_exists:
+            self._options.store()
+        self.__persistent = Persistent(self._options['save']['scenes_done'], self._options)
+        Scene.scenes_done = self.__persistent.scenes_done
+        res = self._options['settings']['resolution']
+        if res:
+            res = LVector2i(*[int(_res) for _res in res.split('x')])
+        else:
+            resolutions = []
+            if not self.is_version_run:
+                d_i = base.pipe.get_display_information()
+                def _res(idx):
+                    return d_i.get_display_mode_width(idx), \
+                        d_i.get_display_mode_height(idx)
+                resolutions = [
+                    _res(idx) for idx in range(d_i.get_total_display_modes())]
+                res = sorted(resolutions)[-1]
+        fullscreen = self._options['settings']['fullscreen']
+        props = WindowProperties()
+        if args.functional_test or args.functional_ref:
+            fullscreen = False
+        elif not self.is_version_run:
+            props.set_size(res)
+        props.set_fullscreen(fullscreen)
+        props.set_icon_filename('assets/images/icon/pmachines.ico')
+        if not args.screenshot and not self.is_version_run and base.win and not isinstance(base.win, GraphicsBuffer):
+            base.win.request_properties(props)
+        #gltf.patch_loader(base.loader)
+        if self._options['development']['simplepbr'] and not self.is_version_run and base.win:
+            self._pipeline = simplepbr.init(
+                use_normal_maps=True,
+                use_emission_maps=False,
+                use_occlusion_maps=True,
+                msaa_samples=4 if self._options['settings']['antialiasing'] else 1,
+                enable_shadows=int(self._options['settings']['shadows']))
+            debug(f'msaa: {self._pipeline.msaa_samples}')
+            debug(f'shadows: {self._pipeline.enable_shadows}')
+        render.setAntialias(AntialiasAttrib.MAuto)
+        self.base.set_background_color(0, 0, 0, 1)
+        self.base.disable_mouse()
+        if self._options['development']['show_buffers']:
+            base.bufferViewer.toggleEnable()
+        if self._options['development']['fps']:
+            base.set_frame_rate_meter(True)
+        #self.base.accept('window-event', self._on_win_evt)
+        self.base.accept('aspectRatioChanged', self._on_aspect_ratio_changed)
+        if self._options['development']['mouse_coords']:
+            coords_txt = OnscreenText(
+                '', parent=base.a2dTopRight, scale=0.04,
+                pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
+            def update_coords(task):
+                txt = '%s %s' % (int(base.win.get_pointer(0).x),
+                                 int(base.win.get_pointer(0).y))
+                coords_txt['text'] = txt
+                return task.cont
+            taskMgr.add(update_coords, 'update_coords')
+
+    def _set_physics(self):
+        if self._options['development']['physics_debug']:
+            debug_node = BulletDebugNode('Debug')
+            debug_node.show_wireframe(True)
+            debug_node.show_constraints(True)
+            debug_node.show_bounding_boxes(True)
+            debug_node.show_normals(True)
+            self._debug_np = render.attach_new_node(debug_node)
+            self._debug_np.show()
+        self.world = BulletWorld()
+        self.world.set_gravity((0, 0, -9.81))
+        if self._options['development']['physics_debug']:
+            self.world.set_debug_node(self._debug_np.node())
+        def update(task):
+            dt = globalClock.get_dt()
+            self.world.do_physics(dt, 10, 1/180)
+            return task.cont
+        self._phys_tsk = taskMgr.add(update, 'update')
+
+    def _unset_physics(self):
+        if self._options['development']['physics_debug']:
+            self._debug_np.remove_node()
+        self.world = None
+        taskMgr.remove(self._phys_tsk)
+
+    def _on_aspect_ratio_changed(self):
+        if self._fsm.state == 'Scene':
+            self._scene.on_aspect_ratio_changed()
+
+    def __assert_fps(self, task):
+        if len(self.__fps_lst) > 3:
+            self.__fps_lst.pop(0)
+        self.__fps_lst += [globalClock.average_frame_rate]
+        if len(self.__fps_lst) == 4:
+            fps_threshold = 55 if cpu_count() >= 4 and node() != 'localhost.localdomain' else 10  # i.e. it is the builder machine
+            assert not all(fps < fps_threshold for fps in self.__fps_lst), 'low fps %s' % self.__fps_lst
+        return task.again
+
+    def destroy(self):
+        self._fsm.cleanup()
+        self.base.destroy()
+
+    def run(self):
+        self.start()
+        self.base.run()
diff --git a/pmachines/application/persistent.py b/pmachines/application/persistent.py
new file mode 100644 (file)
index 0000000..81bbc91
--- /dev/null
@@ -0,0 +1,32 @@
+import json
+
+
+class Persistent:
+
+    def __init__(self, scenes_done, opt_file):
+        self.__scenes_done = scenes_done
+        self.__fix_ini_parsing()
+        self.__opt_file = opt_file
+
+    def __fix_ini_parsing(self):
+        #if len(self.__scenes_done) == 1 and not self.__scenes_done[0]:
+        #    self.__scenes_done = []
+        #print(self.__scenes_done)
+        #self.__scenes_done = self.__scenes_done[0]
+        if self.__scenes_done:
+            if not isinstance(self.__scenes_done, list):  # empty list: []
+                self.__scenes_done = self.__scenes_done.strip("'")
+        if self.__scenes_done:
+            if not isinstance(self.__scenes_done, list):
+                self.__scenes_done = json.loads(self.__scenes_done)
+
+    def save_scene(self, name, version):
+        scenes = []
+        scenes = [scene for scene in self.__scenes_done if scene[0] != name]
+        self.__scenes_done = scenes + [(name, version)]
+        self.__opt_file['save']['scenes_done'] = "'%s'" % json.dumps(self.__scenes_done)
+        self.__opt_file.store()
+
+    @property
+    def scenes_done(self):
+        return self.__scenes_done
index 4d004c9f1ae6fe905f70948debdbd275706d4332..4fef839f33df48e058bba825a3ff4f0c724273d2 100644 (file)
@@ -270,7 +270,7 @@ class Menu(DirectObject):
             'frameColor': (1, 1, 1, .8),
             'text_scale': .64}
         left = - (dx := .8) * (min(4, len(self._scenes)) - 1) / 2
-        from pmachines.scene import Scene
+        from pmachines.scene.scene import Scene
         for i, scene_name in enumerate(self._scenes):
             top = .1 if len(self._scenes) < 5 else .6
             row = 0 if i < 4 else 1
diff --git a/pmachines/persistent.py b/pmachines/persistent.py
deleted file mode 100644 (file)
index 81bbc91..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-import json
-
-
-class Persistent:
-
-    def __init__(self, scenes_done, opt_file):
-        self.__scenes_done = scenes_done
-        self.__fix_ini_parsing()
-        self.__opt_file = opt_file
-
-    def __fix_ini_parsing(self):
-        #if len(self.__scenes_done) == 1 and not self.__scenes_done[0]:
-        #    self.__scenes_done = []
-        #print(self.__scenes_done)
-        #self.__scenes_done = self.__scenes_done[0]
-        if self.__scenes_done:
-            if not isinstance(self.__scenes_done, list):  # empty list: []
-                self.__scenes_done = self.__scenes_done.strip("'")
-        if self.__scenes_done:
-            if not isinstance(self.__scenes_done, list):
-                self.__scenes_done = json.loads(self.__scenes_done)
-
-    def save_scene(self, name, version):
-        scenes = []
-        scenes = [scene for scene in self.__scenes_done if scene[0] != name]
-        self.__scenes_done = scenes + [(name, version)]
-        self.__opt_file['save']['scenes_done'] = "'%s'" % json.dumps(self.__scenes_done)
-        self.__opt_file.store()
-
-    @property
-    def scenes_done(self):
-        return self.__scenes_done
diff --git a/pmachines/scene.py b/pmachines/scene.py
deleted file mode 100644 (file)
index 287f84f..0000000
+++ /dev/null
@@ -1,839 +0,0 @@
-from os.path import exists
-from os import makedirs
-from logging import info
-from json import loads
-from collections import namedtuple
-from panda3d.core import AmbientLight, Texture, TextPropertiesManager, \
-    TextNode, Spotlight, PerspectiveLens, BitMask32
-from panda3d.bullet import BulletPlaneShape, BulletGhostNode
-from direct.gui.OnscreenImage import OnscreenImage
-from direct.gui.OnscreenText import OnscreenText
-from direct.gui.DirectGui import DirectButton, DirectFrame
-from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
-from direct.showbase.DirectObject import DirectObject
-from direct.interval.IntervalGlobal import Sequence, Func
-from direct.interval.LerpInterval import LerpFunctionInterval
-from pmachines.items.background import Background
-from pmachines.gui.sidepanel import SidePanel
-from pmachines.items.box import Box, HitStrategy
-from pmachines.items.basketball import Basketball
-from pmachines.items.domino import Domino, DownStrategy
-from pmachines.items.shelf import Shelf
-from pmachines.items.teetertooter import TeeterTooter
-from pmachines.editor.scene import SceneEditor
-from ya2.utils.cursor import MouseCursor
-from ya2.utils.gfx import GfxTools, DirectGuiMixin
-from ya2.utils.gui import GuiTools
-
-
-class Scene(DirectObject):
-
-    json_files = {}
-    scenes_done = []
-
-    def __init__(self, world, exit_cb, auto_close_instr, dbg_items, reload_cb, scenes, pos_mgr, testing, mouse_coords, persistent, json_name, editor, auto_start_editor):
-        super().__init__()
-        self._world = world
-        self._exit_cb = exit_cb
-        self._testing = testing
-        self._mouse_coords = mouse_coords
-        self._dbg_items = dbg_items
-        self._reload_cb = reload_cb
-        self._pos_mgr = pos_mgr
-        self._pos_mgr = {}
-        self._scenes = scenes
-        self._start_evt_time = None
-        self._enforce_result = ''
-        self.__persistent = persistent
-        self.__json_name = json_name
-        self.__editor = editor
-        self.__scene_editor = None
-        self.json = {}
-        self.accept('enforce_result', self.enforce_result)
-        self._set_camera()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01), testing)
-        self.__set_font()
-        self._set_gui()
-        self._set_lights()
-        self._set_input()
-        self._set_mouse_plane()
-        self.__items = []
-        self._test_items = []
-        self.reset()
-        self._state = 'init'
-        self._paused = False
-        self._item_active = None
-        if auto_close_instr:
-            self.__store_state()
-            self.__restore_state()
-        else:
-            self._set_instructions()
-        self._bg = Background()
-        self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.__items)
-        self._scene_tsk = taskMgr.add(self.on_frame, 'scene_on_frame')
-        if auto_start_editor:
-            self._set_editor()
-        self.accept('editor-inspector-delete', self.__on_inspector_delete)
-
-    @classmethod
-    def filename(cls, scene_name):
-        return f'assets/scenes/{scene_name}.json'
-
-    @classmethod
-    def name(cls, scene_name):
-        if not scene_name in cls.json_files:
-            with open(cls.filename(scene_name)) as f:
-                cls.json_files[scene_name] = loads(f.read())
-        return _(cls.json_files[scene_name]['name'])
-
-    @classmethod
-    def version(cls, scene_name):
-        if not scene_name: return ''
-        if not scene_name in cls.json_files:
-            with open(cls.filename(scene_name)) as f:
-                cls.json_files[scene_name] = loads(f.read())
-        return cls.json_files[scene_name]['version']
-
-    @classmethod
-    def is_done(cls, scene_name):
-        if not cls.scenes_done or len(cls.scenes_done) == 1 and not cls.scenes_done[0]:
-            return False
-        return bool([(name, version) for name, version in cls.scenes_done if scene_name == name and cls.version(scene_name) == version])
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name(self.__json_name) + '\n\n'
-        txt += _(self.__process_json_escape(self.__class__.json_files[self.__json_name]['instructions']))
-        return txt
-
-    def __process_json_escape(self, string):
-        return bytes(string, 'utf-8').decode('unicode-escape')
-
-    @property
-    def items(self):
-        items = self.__items[:]
-        if self.__scene_editor:
-            items += self.__scene_editor.test_items
-        return items
-
-    def _set_items(self):
-        if not self.__json_name: return
-        self.__items = []
-        self._test_items = []
-        if not self.json:
-            with open(f'assets/scenes/{self.__json_name}.json') as f:
-                self.json = loads(f.read())
-        for item in self.json['start_items']:
-            args = {
-                'world': self._world,
-                'plane_node': self._mouse_plane_node,
-                'cb_inst': self.cb_inst,
-                'curr_bottom': self.current_bottom,
-                'repos': self.repos,
-                'count': item['count'],
-                'json': item}
-            if 'mass' in item:
-                args['mass'] = item['mass']
-            if 'friction' in item:
-                args['friction'] = item['friction']
-            self.__items += [self.__code2class(item['class'])(**args)]
-        for item in self.json['items']:
-            args = {
-                'world': self._world,
-                'plane_node': self._mouse_plane_node,
-                'cb_inst': self.cb_inst,
-                'curr_bottom': self.current_bottom,
-                'repos': self.repos,
-                'json': item}
-            args['pos'] = tuple(item['position'])
-            if 'mass' in item:
-                args['mass'] = item['mass']
-            if 'friction' in item:
-                args['friction'] = item['friction']
-            if 'roll' in item:
-                args['r'] = item['roll']
-            if 'model_scale' in item:
-                args['model_scale'] = item['model_scale']
-            if 'restitution' in item:
-                args['restitution'] = item['restitution']
-            if 'friction' in item:
-                args['friction'] = item['friction']
-            self.__items += [self.__code2class(item['class'])(**args)]
-            if 'strategy' in item:
-                match item['strategy']:
-                    case 'DownStrategy':
-                        self.__items[-1].set_strategy(self.__code2class(item['strategy'])(self.__items[-1]._np, *item['strategy_args']))
-                    case 'HitStrategy':
-                        self.__items[-1].set_strategy(self.__code2class(item['strategy'])(self.__item_with_id(item['strategy_args'][0]), self.items[-1].node, self.__items[-1]._world))
-
-    def __code2class(self, code):
-        return {
-            'Box': Box,
-            'Basketball': Basketball,
-            'Domino': Domino,
-            'Shelf': Shelf,
-            'TeeterTooter': TeeterTooter,
-            'DownStrategy': DownStrategy,
-            'HitStrategy': HitStrategy
-        }[code]
-
-    def __item_with_id(self, id):
-        for item in self.__items:
-            if 'id' in item.json and item.json['id'] == id:
-                return item
-
-    def screenshot(self, task=None):
-        tex = Texture('screenshot')
-        buffer = base.win.make_texture_buffer('screenshot', 512, 512, tex, True )
-        cam = base.make_camera(buffer)
-        cam.reparent_to(render)
-        cam.node().get_lens().set_fov(base.camLens.get_fov())
-        cam.set_pos(0, -20, 0)
-        cam.look_at(0, 0, 0)
-        import simplepbr
-        simplepbr.init(
-            window=buffer,
-            camera_node=cam,
-            use_normal_maps=True,
-            use_emission_maps=False,
-            use_occlusion_maps=True,
-            msaa_samples=4,
-            enable_shadows=True)
-        base.graphicsEngine.renderFrame()
-        base.graphicsEngine.renderFrame()
-        fname = self.__json_name
-        if not exists('assets/images/scenes'):
-            makedirs('assets/images/scenes')
-        buffer.save_screenshot('assets/images/scenes/%s.png' % fname)
-        # img = DirectButton(
-        #     frameTexture=buffer.get_texture(), relief=FLAT,
-        #     frameSize=(-.2, .2, -.2, .2))
-        return buffer.get_texture()
-
-    def current_bottom(self):
-        curr_bottom = 1
-        for item in self.__items:
-            if item.repos_done:
-                curr_bottom = min(curr_bottom, item.get_bottom())
-        return curr_bottom
-
-    def reset(self):
-        [itm.destroy() for itm in self.__items]
-        [itm.remove_node() for itm in self._test_items]
-        self.__items = []
-        self._test_items = []
-        self._set_items()
-        self._set_test_items()
-        self._state = 'init'
-        self._commands = []
-        self._command_idx = 0
-        self._start_evt_time = None
-        if hasattr(self, '_success_txt'):
-            self._success_txt.destroy()
-            del self._success_txt
-        self.__right_btn['state'] = NORMAL
-
-    def enforce_result(self, val):
-        self._enforce_result = val
-        info('enforce result: ' + val)
-
-    def destroy(self):
-        self.__intro_sequence.finish()
-        self.ignore('enforce_result')
-        self._unset_gui()
-        self._unset_lights()
-        self._unset_input()
-        self._unset_mouse_plane()
-        [itm.destroy() for itm in self.__items]
-        [itm.remove_node() for itm in self._test_items]
-        self._bg.destroy()
-        self._side_panel.destroy()
-        self._cursor.destroy()
-        taskMgr.remove(self._scene_tsk)
-        if hasattr(self, '_success_txt'):
-            self._success_txt.destroy()
-        self.ignore('editor-inspector-delete')
-        if self.__scene_editor: self.__scene_editor.destroy()
-
-    def _set_camera(self):
-        base.camera.set_pos(0, -20, 0)
-        base.camera.look_at(0, 0, 0)
-        def camera_ani(t):
-            start_v = (1, -5, 1)
-            end_v = (0, -20, 0)
-            curr_pos = (
-                start_v[0] + (end_v[0] - start_v[0]) * t,
-                start_v[1] + (end_v[1] - start_v[1]) * t,
-                start_v[2] + (end_v[2] - start_v[2]) * t)
-            base.camera.set_pos(*curr_pos)
-            self.repos()
-        camera_interval = LerpFunctionInterval(
-            camera_ani,
-            1.2,
-            0,
-            1,
-            blendType='easeInOut')
-        self.__intro_sequence = Sequence(
-            camera_interval,
-            Func(self.repos))
-        self.__intro_sequence.start()
-
-    def __load_img_btn(self, path, col):
-        img = OnscreenImage('assets/images/buttons/%s.dds' % path)
-        img.set_transparency(True)
-        img.set_color(col)
-        img.detach_node()
-        return img
-
-    def _set_gui(self):
-        def load_images_btn(path, col):
-            colors = {
-                'gray': [
-                    (.6, .6, .6, 1),  # ready
-                    (1, 1, 1, 1), # press
-                    (.8, .8, .8, 1), # rollover
-                    (.4, .4, .4, .4)],
-                'green': [
-                    (.1, .68, .1, 1),
-                    (.1, 1, .1, 1),
-                    (.1, .84, .1, 1),
-                    (.4, .1, .1, .4)]}[col]
-            return [self.__load_img_btn(path, col) for col in colors]
-        abl, abr = base.a2dBottomLeft, base.a2dBottomRight
-        btn_info = [
-            ('home', self.on_home, NORMAL, abl, 'gray', _('Exit'), 'right'),
-            ('information', self._set_instructions, NORMAL, abl, 'gray', _('Instructions'), 'right'),
-            ('right', self.on_play, NORMAL, abr, 'green', _('Run'), 'left'),
-            #('next', self.on_next, DISABLED, abr, 'gray'),
-            #('previous', self.on_prev, DISABLED, abr, 'gray'),
-            #('rewind', self.reset, NORMAL, abr, 'gray')
-        ]
-        if self.__editor:
-            btn_info.insert(2, ('wrench', self._set_editor, NORMAL, abl, 'gray', _('Editor'), 'right'))
-        num_l = num_r = 0
-        btns = []
-        tooltip_args = self.__font, .05, (.93, .93, .93, 1)
-        for binfo in btn_info:
-            imgs = load_images_btn(binfo[0], binfo[4])
-            if binfo[3] == base.a2dBottomLeft:
-                sign, num = 1, num_l
-                num_l += 1
-            else:
-                sign, num = -1, num_r
-                num_r += 1
-            fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
-            btn = DirectButton(
-                image=imgs, scale=.05, pos=(sign * (.06 + .11 * num), 1, .06),
-                parent=binfo[3], command=binfo[1], state=binfo[2], relief=FLAT,
-                frameColor=fcols[0] if binfo[2] == NORMAL else fcols[1],
-                rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-                clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
-            btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-            btn.set_transparency(True)
-            t = tooltip_args + (binfo[6],)
-            btn.set_tooltip(binfo[5], *t)
-            self._pos_mgr[binfo[0]] = btn.pos_pixel()
-            btns += [btn]
-        if self.__editor:
-            self.__home_btn, self.__info_btn, self.__editor_btn, self.__right_btn = btns
-        else:
-            self.__home_btn, self.__info_btn, self.__right_btn = btns
-        # , self.__next_btn, self.__prev_btn, self.__rewind_btn
-        if self._dbg_items:
-            self._info_txt = OnscreenText(
-                '', parent=base.a2dTopRight, scale=0.04,
-                pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
-        if self._mouse_coords:
-            self._coords_txt = OnscreenText(
-                '', parent=base.a2dTopRight, scale=0.04,
-                pos=(-.03, -.12), fg=(.9, .9, .9, 1), align=TextNode.A_right)
-            def update_coords(task):
-                pos = None
-                for hit in self._get_hits():
-                    if hit.get_node() == self._mouse_plane_node:
-                        pos = hit.get_hit_pos()
-                if pos:
-                    txt = '%s %s' % (round(pos.x, 3),
-                                     round(pos.z, 3))
-                    self._coords_txt['text'] = txt
-                return task.cont
-            self._coords_tsk = taskMgr.add(update_coords, 'update_coords')
-
-    def _unset_gui(self):
-        btns = [
-            self.__home_btn, self.__info_btn, self.__right_btn
-            #self.__next_btn, self.__prev_btn, self.__rewind_btn
-        ]
-        if self.__editor: btns += [self.__editor_btn]
-        [btn.destroy() for btn in btns]
-        if self._dbg_items:
-            self._info_txt.destroy()
-        if self._mouse_coords:
-            taskMgr.remove(self._coords_tsk)
-            self._coords_txt.destroy()
-
-    def _set_spotlight(self, name, pos, look_at, color, shadows=False):
-        light = Spotlight(name)
-        if shadows:
-            light.setLens(PerspectiveLens())
-        light_np = render.attach_new_node(light)
-        light_np.set_pos(pos)
-        light_np.look_at(look_at)
-        light.set_color(color)
-        render.set_light(light_np)
-        return light_np
-
-    def _set_lights(self):
-        alight = AmbientLight('alight')  # for ao
-        alight.set_color((.15, .15, .15, 1))
-        self._alnp = render.attach_new_node(alight)
-        render.set_light(self._alnp)
-        self._key_light = self._set_spotlight(
-            'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
-        self._shadow_light = self._set_spotlight(
-            'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
-        self._shadow_light.node().set_shadow_caster(True, 2048, 2048)
-        self._shadow_light.node().get_lens().set_film_size(2048, 2048)
-        self._shadow_light.node().get_lens().set_near_far(1, 256)
-        self._shadow_light.node().set_camera_mask(BitMask32(0x01))
-
-    def _unset_lights(self):
-        for light in [self._alnp, self._key_light, self._shadow_light]:
-            render.clear_light(light)
-            light.remove_node()
-
-    def _set_input(self):
-        self.accept('mouse1', self.on_click_l)
-        self.accept('mouse1-up', self.on_release)
-        self.accept('mouse3', self.on_click_r)
-        self.accept('mouse3-up', self.on_release)
-
-    def _unset_input(self):
-        for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
-            self.ignore(evt)
-
-    def _set_mouse_plane(self):
-        shape = BulletPlaneShape((0, -1, 0), 0)
-        #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
-        self._mouse_plane_node = BulletGhostNode('mouse plane')
-        self._mouse_plane_node.addShape(shape)
-        #np = render.attachNewNode(self._mouse_plane_node)
-        #self._world.attachRigidBody(self._mouse_plane_node)
-        self._world.attach_ghost(self._mouse_plane_node)
-
-    def _unset_mouse_plane(self):
-        self._world.remove_ghost(self._mouse_plane_node)
-
-    def _get_hits(self):
-        if not base.mouseWatcherNode.has_mouse(): return []
-        p_from, p_to = GuiTools.get_mouse().from_to_points()
-        return self._world.ray_test_all(p_from, p_to).get_hits()
-
-    def _update_info(self, item):
-        txt = ''
-        if item:
-            txt = '%.3f %.3f\n%.3f°' % (
-                item._np.get_x(), item._np.get_z(), item._np.get_r())
-        self._info_txt['text'] = txt
-
-    def _on_click(self, method):
-        if self._paused:
-            return
-        for hit in self._get_hits():
-            if hit.get_node() == self._mouse_plane_node:
-                pos = hit.get_hit_pos()
-        for hit in self._get_hits():
-            for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
-                if not self._item_active:
-                    self._item_active = item
-                if item not in self.__items:
-                    method = 'on_click_l'
-                getattr(item, method)(pos)
-                img = 'move' if method == 'on_click_l' else 'rotate'
-                if not (img == 'rotate' and not item._instantiated):
-                    self._cursor.set_image('assets/images/buttons/%s.dds' % img)
-
-    def on_click_l(self):
-        self._on_click('on_click_l')
-
-    def on_click_r(self):
-        self._on_click('on_click_r')
-
-    def on_release(self):
-        if self._item_active and not self._item_active._first_command:
-            self._commands = self._commands[:self._command_idx]
-            self._commands += [self._item_active]
-            self._command_idx += 1
-            #self.__prev_btn['state'] = NORMAL
-            #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
-            #self.__prev_btn['frameColor'] = fcols[0]
-            #if self._item_active._command_idx == len(self._item_active._commands) - 1:
-            #    self.__next_btn['state'] = DISABLED
-            #    self.__next_btn['frameColor'] = fcols[1]
-        self._item_active = None
-        [item.on_release() for item in self.__items]
-        self._cursor.set_image('assets/images/buttons/arrowUpLeft.dds')
-
-    def repos(self):
-        for item in self.__items:
-            item.repos_done = False
-        self.__items = sorted(self.__items, key=lambda itm: itm.__class__.__name__)
-        [item.on_aspect_ratio_changed() for item in self.__items]
-        self._side_panel.update(self.__items)
-        max_x = -float('inf')
-        for item in self.__items:
-            if not item._instantiated:
-                max_x = max(item._np.get_x(), max_x)
-        for item in self.__items:
-            if not item._instantiated:
-                item.repos_x(max_x)
-
-    def on_aspect_ratio_changed(self):
-        self.repos()
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.__items) and not self._paused
-
-    def _fail_condition(self):
-        return all(itm.fail_condition() for itm in self.__items) and not self._paused and self._state == 'playing'
-
-    def on_frame(self, task):
-        hits = self._get_hits()
-        pos = None
-        for hit in self._get_hits():
-            if hit.get_node() == self._mouse_plane_node:
-                pos = hit.get_hit_pos()
-        hit_nodes = [hit.get_node() for hit in hits]
-        if self._item_active:
-            items_hit = [self._item_active]
-        else:
-            items_hit = [itm for itm in self.items if itm.node in hit_nodes]
-        items_no_hit = [itm for itm in self.items if itm not in items_hit]
-        [itm.on_mouse_on() for itm in items_hit]
-        [itm.on_mouse_off() for itm in items_no_hit]
-        if pos and self._item_active:
-            self._item_active.on_mouse_move(pos)
-        if self._dbg_items:
-            self._update_info(items_hit[0] if items_hit else None)
-        if not self.__scene_editor and self._win_condition():
-            self._start_evt_time = None
-            self._set_fail() if self._enforce_result == 'fail' else self._set_win()
-        elif self._state == 'playing' and self._fail_condition():
-            self._start_evt_time = None
-            self._set_win() if self._enforce_result == 'win' else self._set_fail()
-        elif self._testing and self._start_evt_time and globalClock.getFrameTime() - self._start_evt_time > 5.0:
-            self._start_evt_time = None
-            self._set_win() if self._enforce_result == 'win' else self._set_fail()
-        if any(itm._overlapping for itm in self.items):
-            self._cursor.set_color((.9, .1, .1, 1))
-        else:
-            self._cursor.set_color((.9, .9, .9, 1))
-        return task.cont
-
-    def cb_inst(self, item):
-        self.__items += [item]
-
-    def on_play(self):
-        self._state = 'playing'
-        #self.__prev_btn['state'] = DISABLED
-        #self.__next_btn['state'] = DISABLED
-        self.__right_btn['state'] = DISABLED
-        [itm.play() for itm in self.__items]
-        self._start_evt_time = globalClock.getFrameTime()
-
-    def on_next(self):
-        self._commands[self._command_idx].redo()
-        self._command_idx += 1
-        #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
-        #self.__prev_btn['state'] = NORMAL
-        #self.__prev_btn['frameColor'] = fcols[0]
-        #more_commands = self._command_idx < len(self._commands)
-        #self.__next_btn['state'] = NORMAL if more_commands else DISABLED
-        #self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
-
-    def on_prev(self):
-        self._command_idx -= 1
-        self._commands[self._command_idx].undo()
-        #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
-        #self.__next_btn['state'] = NORMAL
-        #self.__next_btn['frameColor'] = fcols[0]
-        #self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
-        #self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
-
-    def on_home(self):
-        self._exit_cb()
-
-    def __set_font(self):
-        self.__font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
-        self.__font.clear()
-        self.__font.set_pixels_per_unit(60)
-        self.__font.set_minfilter(Texture.FTLinearMipmapLinear)
-        self.__font.set_outline((0, 0, 0, 1), .8, .2)
-
-
-    def _set_instructions(self):
-        self._paused = True
-        self.__store_state()
-        mgr = TextPropertiesManager.get_global_ptr()
-        for name in ['mouse_l', 'mouse_r']:
-            graphic = OnscreenImage('assets/images/buttons/%s.dds' % name)
-            graphic.set_scale(.5)
-            graphic.get_texture().set_minfilter(Texture.FTLinearMipmapLinear)
-            graphic.get_texture().set_anisotropic_degree(2)
-            mgr.set_graphic(name, graphic)
-            graphic.set_z(-.2)
-            graphic.set_transparency(True)
-            graphic.detach_node()
-        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
-                          frameSize=(-.6, .6, -.3, .3))
-        self._txt = OnscreenText(
-            self._instr_txt(), parent=frm, font=self.__font, scale=0.06,
-            fg=(.9, .9, .9, 1), align=TextNode.A_left)
-        u_l = self._txt.textNode.get_upper_left_3d()
-        l_r = self._txt.textNode.get_lower_right_3d()
-        w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
-        btn_scale = .05
-        mar = .06  # margin
-        z = h / 2 - self.__font.get_line_height() * self._txt['scale'][1]
-        z += (btn_scale + 2 * mar) / 2
-        self._txt['pos'] = -w / 2, z
-        u_l = self._txt.textNode.get_upper_left_3d()
-        l_r = self._txt.textNode.get_lower_right_3d()
-        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
-        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
-        frm['frameSize'] = fsz
-        colors = [
-            (.6, .6, .6, 1),  # ready
-            (1, 1, 1, 1), # press
-            (.8, .8, .8, 1), # rollover
-            (.4, .4, .4, .4)]
-        imgs = [self.__load_img_btn('exitRight', col) for col in colors]
-        btn = DirectButton(
-            image=imgs, scale=btn_scale,
-            pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
-            parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
-            relief=FLAT, frameColor=(.6, .6, .6, .08),
-            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
-        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        btn.set_transparency(True)
-        self._pos_mgr['close_instructions'] = btn.pos_pixel()
-
-    def _set_win(self):
-        self.__persistent.save_scene(self.__json_name, self.version(self.__json_name))
-        loader.load_sfx('assets/audio/sfx/success.ogg').play()
-        self._paused = True
-        self.__store_state()
-        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
-                          frameSize=(-.6, .6, -.3, .3))
-        font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
-        font.clear()
-        font.set_pixels_per_unit(60)
-        font.set_minfilter(Texture.FTLinearMipmapLinear)
-        font.set_outline((0, 0, 0, 1), .8, .2)
-        self._txt = OnscreenText(
-            _('You win!'),
-            parent=frm,
-            font=font, scale=0.2,
-            fg=(.9, .9, .9, 1))
-        u_l = self._txt.textNode.get_upper_left_3d()
-        l_r = self._txt.textNode.get_lower_right_3d()
-        #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
-        h = u_l[2] - l_r[2]
-        btn_scale = .05
-        mar = .06  # margin
-        z = h / 2 - font.get_line_height() * self._txt['scale'][1]
-        z += (btn_scale + 2 * mar) / 2
-        self._txt['pos'] = 0, z
-        u_l = self._txt.textNode.get_upper_left_3d()
-        l_r = self._txt.textNode.get_lower_right_3d()
-        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
-        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
-        frm['frameSize'] = fsz
-        colors = [
-            (.6, .6, .6, 1),  # ready
-            (1, 1, 1, 1), # press
-            (.8, .8, .8, 1), # rollover
-            (.4, .4, .4, .4)]
-        imgs = [self.__load_img_btn('home', col) for col in colors]
-        btn = DirectButton(
-            image=imgs, scale=btn_scale,
-            pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
-            parent=frm, command=self._on_end_home, extraArgs=[frm],
-            relief=FLAT, frameColor=(.6, .6, .6, .08),
-            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
-        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        btn.set_transparency(True)
-        self._pos_mgr['home_win'] = btn.pos_pixel()
-        imgs = [self.__load_img_btn('rewind', col) for col in colors]
-        btn = DirectButton(
-            image=imgs, scale=btn_scale,
-            pos=(0, 1, l_r[2] - mar - btn_scale),
-            parent=frm, command=self._on_restart, extraArgs=[frm],
-            relief=FLAT, frameColor=(.6, .6, .6, .08),
-            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
-        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['replay'] = btn.pos_pixel()
-        btn.set_transparency(True)
-        if self.__json_name:
-            enabled = self._scenes.index(self.__json_name) < len(self._scenes) - 1
-            if enabled:
-                next_scene = self._scenes[self._scenes.index(self.__json_name) + 1]
-            else:
-                next_scene = None
-        else:
-            next_scene = None
-            enabled = False
-        imgs = [self.__load_img_btn('right', col) for col in colors]
-        btn = DirectButton(
-            image=imgs, scale=btn_scale,
-            pos=(2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
-            parent=frm, command=self._on_next_scene,
-            extraArgs=[frm, next_scene], relief=FLAT,
-            frameColor=(.6, .6, .6, .08),
-            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
-        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        btn['state'] = NORMAL if enabled else DISABLED
-        self._pos_mgr['next'] = btn.pos_pixel()
-        btn.set_transparency(True)
-
-    def _set_fail(self):
-        loader.load_sfx('assets/audio/sfx/success.ogg').play()
-        self._paused = True
-        self.__store_state()
-        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
-                          frameSize=(-.6, .6, -.3, .3))
-        font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
-        font.clear()
-        font.set_pixels_per_unit(60)
-        font.set_minfilter(Texture.FTLinearMipmapLinear)
-        font.set_outline((0, 0, 0, 1), .8, .2)
-        self._txt = OnscreenText(
-            _('You have failed!'),
-            parent=frm,
-            font=font, scale=0.2,
-            fg=(.9, .9, .9, 1))
-        u_l = self._txt.textNode.get_upper_left_3d()
-        l_r = self._txt.textNode.get_lower_right_3d()
-        #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
-        h = u_l[2] - l_r[2]
-        btn_scale = .05
-        mar = .06  # margin
-        z = h / 2 - font.get_line_height() * self._txt['scale'][1]
-        z += (btn_scale + 2 * mar) / 2
-        self._txt['pos'] = 0, z
-        u_l = self._txt.textNode.get_upper_left_3d()
-        l_r = self._txt.textNode.get_lower_right_3d()
-        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
-        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
-        frm['frameSize'] = fsz
-        colors = [
-            (.6, .6, .6, 1),  # ready
-            (1, 1, 1, 1), # press
-            (.8, .8, .8, 1), # rollover
-            (.4, .4, .4, .4)]
-        imgs = [self.__load_img_btn('home', col) for col in colors]
-        btn = DirectButton(
-            image=imgs, scale=btn_scale,
-            pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
-            parent=frm, command=self._on_end_home, extraArgs=[frm],
-            relief=FLAT, frameColor=(.6, .6, .6, .08),
-            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
-        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['home_win'] = btn.pos_pixel()
-        btn.set_transparency(True)
-        imgs = [self.__load_img_btn('rewind', col) for col in colors]
-        btn = DirectButton(
-            image=imgs, scale=btn_scale,
-            pos=(0, 1, l_r[2] - mar - btn_scale),
-            parent=frm, command=self._on_restart, extraArgs=[frm],
-            relief=FLAT, frameColor=(.6, .6, .6, .08),
-            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
-        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['replay'] = btn.pos_pixel()
-        btn.set_transparency(True)
-
-    def _on_restart(self, frm):
-        self.__on_close_instructions(frm)
-        self.reset()
-
-    def _on_end_home(self, frm):
-        self.__on_close_instructions(frm)
-        self.on_home()
-
-    def _on_next_scene(self, frm, scene):
-        self.__on_close_instructions(frm)
-        self._reload_cb(scene)
-
-    def __store_state(self):
-        btns = [
-            self.__home_btn, self.__info_btn, self.__right_btn,
-            #self.__next_btn, self.__prev_btn, self.__rewind_btn
-        ]
-        if self.__editor: btns += [self.__editor_btn]
-        self.__btn_state = [btn['state'] for btn in btns]
-        for btn in btns:
-            btn['state'] = DISABLED
-        [itm.store_state() for itm in self.__items]
-
-    def __restore_state(self):
-        btns = [
-            self.__home_btn, self.__info_btn, self.__right_btn,
-            #self.__next_btn, self.__prev_btn, self.__rewind_btn
-        ]
-        if self.__editor: btns += [self.__editor_btn]
-        for btn, state in zip(btns, self.__btn_state):
-            btn['state'] = state
-        [itm.restore_state() for itm in self.__items]
-        self._paused = False
-
-    def __on_close_instructions(self, frm):
-        frm.remove_node()
-        self.__restore_state()
-
-    def _set_test_items(self):
-        def frame_after(task):
-            self._define_test_items()
-            for itm in self._test_items:
-                self._pos_mgr[itm.name] = itm.pos2d_pixel()
-        taskMgr.doMethodLater(1.4, frame_after, 'frame after')  # after the intro sequence
-
-    def _define_test_items(self):
-        if not self.__json_name: return
-        if not self.__json_name in self.__class__.json_files:
-            with open(self.__class__.filename(self.__json_name)) as f:
-                self.__class__.json_files[self.__json_name] = loads(f.read())
-        for item in self.__class__.json_files[self.__json_name]['test_items']['pixel_space']:
-            self._pos_mgr[item['id']] = tuple(item['position'])
-        for item in self.__class__.json_files[self.__json_name]['test_items']['world_space']:
-            self._set_test_item(item['id'], tuple(item['position']))
-
-    def _set_test_item(self, name, pos):
-        self._test_items += [GfxTools.build_empty_node(name)]
-        self._test_items[-1].set_pos(pos[0], 0, pos[1])
-
-    def add_item(self, item):
-        self.__items += [item]
-
-    def _set_editor(self):
-        fields = ['world', 'plane_node', 'cb_inst', 'curr_bottom', 'repos', 'json']
-        SceneContext = namedtuple('SceneContext', fields)
-        context = SceneContext(
-            self._world,
-            self._mouse_plane_node,
-            self.cb_inst,
-            self.current_bottom,
-            self.repos,
-            {})
-        self.__scene_editor = SceneEditor(self.json, self.__json_name, context, self.add_item, self.__items, self._world, self._mouse_plane_node)
-
-    def __on_inspector_delete(self, item):
-        self.__items.remove(item)
-        self.json['items'].remove(item.json)
-        item.destroy()
diff --git a/pmachines/scene/__init__.py b/pmachines/scene/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pmachines/scene/scene.py b/pmachines/scene/scene.py
new file mode 100644 (file)
index 0000000..287f84f
--- /dev/null
@@ -0,0 +1,839 @@
+from os.path import exists
+from os import makedirs
+from logging import info
+from json import loads
+from collections import namedtuple
+from panda3d.core import AmbientLight, Texture, TextPropertiesManager, \
+    TextNode, Spotlight, PerspectiveLens, BitMask32
+from panda3d.bullet import BulletPlaneShape, BulletGhostNode
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.OnscreenText import OnscreenText
+from direct.gui.DirectGui import DirectButton, DirectFrame
+from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
+from direct.showbase.DirectObject import DirectObject
+from direct.interval.IntervalGlobal import Sequence, Func
+from direct.interval.LerpInterval import LerpFunctionInterval
+from pmachines.items.background import Background
+from pmachines.gui.sidepanel import SidePanel
+from pmachines.items.box import Box, HitStrategy
+from pmachines.items.basketball import Basketball
+from pmachines.items.domino import Domino, DownStrategy
+from pmachines.items.shelf import Shelf
+from pmachines.items.teetertooter import TeeterTooter
+from pmachines.editor.scene import SceneEditor
+from ya2.utils.cursor import MouseCursor
+from ya2.utils.gfx import GfxTools, DirectGuiMixin
+from ya2.utils.gui import GuiTools
+
+
+class Scene(DirectObject):
+
+    json_files = {}
+    scenes_done = []
+
+    def __init__(self, world, exit_cb, auto_close_instr, dbg_items, reload_cb, scenes, pos_mgr, testing, mouse_coords, persistent, json_name, editor, auto_start_editor):
+        super().__init__()
+        self._world = world
+        self._exit_cb = exit_cb
+        self._testing = testing
+        self._mouse_coords = mouse_coords
+        self._dbg_items = dbg_items
+        self._reload_cb = reload_cb
+        self._pos_mgr = pos_mgr
+        self._pos_mgr = {}
+        self._scenes = scenes
+        self._start_evt_time = None
+        self._enforce_result = ''
+        self.__persistent = persistent
+        self.__json_name = json_name
+        self.__editor = editor
+        self.__scene_editor = None
+        self.json = {}
+        self.accept('enforce_result', self.enforce_result)
+        self._set_camera()
+        self._cursor = MouseCursor(
+            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
+            (.01, .01), testing)
+        self.__set_font()
+        self._set_gui()
+        self._set_lights()
+        self._set_input()
+        self._set_mouse_plane()
+        self.__items = []
+        self._test_items = []
+        self.reset()
+        self._state = 'init'
+        self._paused = False
+        self._item_active = None
+        if auto_close_instr:
+            self.__store_state()
+            self.__restore_state()
+        else:
+            self._set_instructions()
+        self._bg = Background()
+        self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.__items)
+        self._scene_tsk = taskMgr.add(self.on_frame, 'scene_on_frame')
+        if auto_start_editor:
+            self._set_editor()
+        self.accept('editor-inspector-delete', self.__on_inspector_delete)
+
+    @classmethod
+    def filename(cls, scene_name):
+        return f'assets/scenes/{scene_name}.json'
+
+    @classmethod
+    def name(cls, scene_name):
+        if not scene_name in cls.json_files:
+            with open(cls.filename(scene_name)) as f:
+                cls.json_files[scene_name] = loads(f.read())
+        return _(cls.json_files[scene_name]['name'])
+
+    @classmethod
+    def version(cls, scene_name):
+        if not scene_name: return ''
+        if not scene_name in cls.json_files:
+            with open(cls.filename(scene_name)) as f:
+                cls.json_files[scene_name] = loads(f.read())
+        return cls.json_files[scene_name]['version']
+
+    @classmethod
+    def is_done(cls, scene_name):
+        if not cls.scenes_done or len(cls.scenes_done) == 1 and not cls.scenes_done[0]:
+            return False
+        return bool([(name, version) for name, version in cls.scenes_done if scene_name == name and cls.version(scene_name) == version])
+
+    def _instr_txt(self):
+        txt = _('Scene: ') + self.name(self.__json_name) + '\n\n'
+        txt += _(self.__process_json_escape(self.__class__.json_files[self.__json_name]['instructions']))
+        return txt
+
+    def __process_json_escape(self, string):
+        return bytes(string, 'utf-8').decode('unicode-escape')
+
+    @property
+    def items(self):
+        items = self.__items[:]
+        if self.__scene_editor:
+            items += self.__scene_editor.test_items
+        return items
+
+    def _set_items(self):
+        if not self.__json_name: return
+        self.__items = []
+        self._test_items = []
+        if not self.json:
+            with open(f'assets/scenes/{self.__json_name}.json') as f:
+                self.json = loads(f.read())
+        for item in self.json['start_items']:
+            args = {
+                'world': self._world,
+                'plane_node': self._mouse_plane_node,
+                'cb_inst': self.cb_inst,
+                'curr_bottom': self.current_bottom,
+                'repos': self.repos,
+                'count': item['count'],
+                'json': item}
+            if 'mass' in item:
+                args['mass'] = item['mass']
+            if 'friction' in item:
+                args['friction'] = item['friction']
+            self.__items += [self.__code2class(item['class'])(**args)]
+        for item in self.json['items']:
+            args = {
+                'world': self._world,
+                'plane_node': self._mouse_plane_node,
+                'cb_inst': self.cb_inst,
+                'curr_bottom': self.current_bottom,
+                'repos': self.repos,
+                'json': item}
+            args['pos'] = tuple(item['position'])
+            if 'mass' in item:
+                args['mass'] = item['mass']
+            if 'friction' in item:
+                args['friction'] = item['friction']
+            if 'roll' in item:
+                args['r'] = item['roll']
+            if 'model_scale' in item:
+                args['model_scale'] = item['model_scale']
+            if 'restitution' in item:
+                args['restitution'] = item['restitution']
+            if 'friction' in item:
+                args['friction'] = item['friction']
+            self.__items += [self.__code2class(item['class'])(**args)]
+            if 'strategy' in item:
+                match item['strategy']:
+                    case 'DownStrategy':
+                        self.__items[-1].set_strategy(self.__code2class(item['strategy'])(self.__items[-1]._np, *item['strategy_args']))
+                    case 'HitStrategy':
+                        self.__items[-1].set_strategy(self.__code2class(item['strategy'])(self.__item_with_id(item['strategy_args'][0]), self.items[-1].node, self.__items[-1]._world))
+
+    def __code2class(self, code):
+        return {
+            'Box': Box,
+            'Basketball': Basketball,
+            'Domino': Domino,
+            'Shelf': Shelf,
+            'TeeterTooter': TeeterTooter,
+            'DownStrategy': DownStrategy,
+            'HitStrategy': HitStrategy
+        }[code]
+
+    def __item_with_id(self, id):
+        for item in self.__items:
+            if 'id' in item.json and item.json['id'] == id:
+                return item
+
+    def screenshot(self, task=None):
+        tex = Texture('screenshot')
+        buffer = base.win.make_texture_buffer('screenshot', 512, 512, tex, True )
+        cam = base.make_camera(buffer)
+        cam.reparent_to(render)
+        cam.node().get_lens().set_fov(base.camLens.get_fov())
+        cam.set_pos(0, -20, 0)
+        cam.look_at(0, 0, 0)
+        import simplepbr
+        simplepbr.init(
+            window=buffer,
+            camera_node=cam,
+            use_normal_maps=True,
+            use_emission_maps=False,
+            use_occlusion_maps=True,
+            msaa_samples=4,
+            enable_shadows=True)
+        base.graphicsEngine.renderFrame()
+        base.graphicsEngine.renderFrame()
+        fname = self.__json_name
+        if not exists('assets/images/scenes'):
+            makedirs('assets/images/scenes')
+        buffer.save_screenshot('assets/images/scenes/%s.png' % fname)
+        # img = DirectButton(
+        #     frameTexture=buffer.get_texture(), relief=FLAT,
+        #     frameSize=(-.2, .2, -.2, .2))
+        return buffer.get_texture()
+
+    def current_bottom(self):
+        curr_bottom = 1
+        for item in self.__items:
+            if item.repos_done:
+                curr_bottom = min(curr_bottom, item.get_bottom())
+        return curr_bottom
+
+    def reset(self):
+        [itm.destroy() for itm in self.__items]
+        [itm.remove_node() for itm in self._test_items]
+        self.__items = []
+        self._test_items = []
+        self._set_items()
+        self._set_test_items()
+        self._state = 'init'
+        self._commands = []
+        self._command_idx = 0
+        self._start_evt_time = None
+        if hasattr(self, '_success_txt'):
+            self._success_txt.destroy()
+            del self._success_txt
+        self.__right_btn['state'] = NORMAL
+
+    def enforce_result(self, val):
+        self._enforce_result = val
+        info('enforce result: ' + val)
+
+    def destroy(self):
+        self.__intro_sequence.finish()
+        self.ignore('enforce_result')
+        self._unset_gui()
+        self._unset_lights()
+        self._unset_input()
+        self._unset_mouse_plane()
+        [itm.destroy() for itm in self.__items]
+        [itm.remove_node() for itm in self._test_items]
+        self._bg.destroy()
+        self._side_panel.destroy()
+        self._cursor.destroy()
+        taskMgr.remove(self._scene_tsk)
+        if hasattr(self, '_success_txt'):
+            self._success_txt.destroy()
+        self.ignore('editor-inspector-delete')
+        if self.__scene_editor: self.__scene_editor.destroy()
+
+    def _set_camera(self):
+        base.camera.set_pos(0, -20, 0)
+        base.camera.look_at(0, 0, 0)
+        def camera_ani(t):
+            start_v = (1, -5, 1)
+            end_v = (0, -20, 0)
+            curr_pos = (
+                start_v[0] + (end_v[0] - start_v[0]) * t,
+                start_v[1] + (end_v[1] - start_v[1]) * t,
+                start_v[2] + (end_v[2] - start_v[2]) * t)
+            base.camera.set_pos(*curr_pos)
+            self.repos()
+        camera_interval = LerpFunctionInterval(
+            camera_ani,
+            1.2,
+            0,
+            1,
+            blendType='easeInOut')
+        self.__intro_sequence = Sequence(
+            camera_interval,
+            Func(self.repos))
+        self.__intro_sequence.start()
+
+    def __load_img_btn(self, path, col):
+        img = OnscreenImage('assets/images/buttons/%s.dds' % path)
+        img.set_transparency(True)
+        img.set_color(col)
+        img.detach_node()
+        return img
+
+    def _set_gui(self):
+        def load_images_btn(path, col):
+            colors = {
+                'gray': [
+                    (.6, .6, .6, 1),  # ready
+                    (1, 1, 1, 1), # press
+                    (.8, .8, .8, 1), # rollover
+                    (.4, .4, .4, .4)],
+                'green': [
+                    (.1, .68, .1, 1),
+                    (.1, 1, .1, 1),
+                    (.1, .84, .1, 1),
+                    (.4, .1, .1, .4)]}[col]
+            return [self.__load_img_btn(path, col) for col in colors]
+        abl, abr = base.a2dBottomLeft, base.a2dBottomRight
+        btn_info = [
+            ('home', self.on_home, NORMAL, abl, 'gray', _('Exit'), 'right'),
+            ('information', self._set_instructions, NORMAL, abl, 'gray', _('Instructions'), 'right'),
+            ('right', self.on_play, NORMAL, abr, 'green', _('Run'), 'left'),
+            #('next', self.on_next, DISABLED, abr, 'gray'),
+            #('previous', self.on_prev, DISABLED, abr, 'gray'),
+            #('rewind', self.reset, NORMAL, abr, 'gray')
+        ]
+        if self.__editor:
+            btn_info.insert(2, ('wrench', self._set_editor, NORMAL, abl, 'gray', _('Editor'), 'right'))
+        num_l = num_r = 0
+        btns = []
+        tooltip_args = self.__font, .05, (.93, .93, .93, 1)
+        for binfo in btn_info:
+            imgs = load_images_btn(binfo[0], binfo[4])
+            if binfo[3] == base.a2dBottomLeft:
+                sign, num = 1, num_l
+                num_l += 1
+            else:
+                sign, num = -1, num_r
+                num_r += 1
+            fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+            btn = DirectButton(
+                image=imgs, scale=.05, pos=(sign * (.06 + .11 * num), 1, .06),
+                parent=binfo[3], command=binfo[1], state=binfo[2], relief=FLAT,
+                frameColor=fcols[0] if binfo[2] == NORMAL else fcols[1],
+                rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+                clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+            btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+            btn.set_transparency(True)
+            t = tooltip_args + (binfo[6],)
+            btn.set_tooltip(binfo[5], *t)
+            self._pos_mgr[binfo[0]] = btn.pos_pixel()
+            btns += [btn]
+        if self.__editor:
+            self.__home_btn, self.__info_btn, self.__editor_btn, self.__right_btn = btns
+        else:
+            self.__home_btn, self.__info_btn, self.__right_btn = btns
+        # , self.__next_btn, self.__prev_btn, self.__rewind_btn
+        if self._dbg_items:
+            self._info_txt = OnscreenText(
+                '', parent=base.a2dTopRight, scale=0.04,
+                pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
+        if self._mouse_coords:
+            self._coords_txt = OnscreenText(
+                '', parent=base.a2dTopRight, scale=0.04,
+                pos=(-.03, -.12), fg=(.9, .9, .9, 1), align=TextNode.A_right)
+            def update_coords(task):
+                pos = None
+                for hit in self._get_hits():
+                    if hit.get_node() == self._mouse_plane_node:
+                        pos = hit.get_hit_pos()
+                if pos:
+                    txt = '%s %s' % (round(pos.x, 3),
+                                     round(pos.z, 3))
+                    self._coords_txt['text'] = txt
+                return task.cont
+            self._coords_tsk = taskMgr.add(update_coords, 'update_coords')
+
+    def _unset_gui(self):
+        btns = [
+            self.__home_btn, self.__info_btn, self.__right_btn
+            #self.__next_btn, self.__prev_btn, self.__rewind_btn
+        ]
+        if self.__editor: btns += [self.__editor_btn]
+        [btn.destroy() for btn in btns]
+        if self._dbg_items:
+            self._info_txt.destroy()
+        if self._mouse_coords:
+            taskMgr.remove(self._coords_tsk)
+            self._coords_txt.destroy()
+
+    def _set_spotlight(self, name, pos, look_at, color, shadows=False):
+        light = Spotlight(name)
+        if shadows:
+            light.setLens(PerspectiveLens())
+        light_np = render.attach_new_node(light)
+        light_np.set_pos(pos)
+        light_np.look_at(look_at)
+        light.set_color(color)
+        render.set_light(light_np)
+        return light_np
+
+    def _set_lights(self):
+        alight = AmbientLight('alight')  # for ao
+        alight.set_color((.15, .15, .15, 1))
+        self._alnp = render.attach_new_node(alight)
+        render.set_light(self._alnp)
+        self._key_light = self._set_spotlight(
+            'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
+        self._shadow_light = self._set_spotlight(
+            'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
+        self._shadow_light.node().set_shadow_caster(True, 2048, 2048)
+        self._shadow_light.node().get_lens().set_film_size(2048, 2048)
+        self._shadow_light.node().get_lens().set_near_far(1, 256)
+        self._shadow_light.node().set_camera_mask(BitMask32(0x01))
+
+    def _unset_lights(self):
+        for light in [self._alnp, self._key_light, self._shadow_light]:
+            render.clear_light(light)
+            light.remove_node()
+
+    def _set_input(self):
+        self.accept('mouse1', self.on_click_l)
+        self.accept('mouse1-up', self.on_release)
+        self.accept('mouse3', self.on_click_r)
+        self.accept('mouse3-up', self.on_release)
+
+    def _unset_input(self):
+        for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
+            self.ignore(evt)
+
+    def _set_mouse_plane(self):
+        shape = BulletPlaneShape((0, -1, 0), 0)
+        #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
+        self._mouse_plane_node = BulletGhostNode('mouse plane')
+        self._mouse_plane_node.addShape(shape)
+        #np = render.attachNewNode(self._mouse_plane_node)
+        #self._world.attachRigidBody(self._mouse_plane_node)
+        self._world.attach_ghost(self._mouse_plane_node)
+
+    def _unset_mouse_plane(self):
+        self._world.remove_ghost(self._mouse_plane_node)
+
+    def _get_hits(self):
+        if not base.mouseWatcherNode.has_mouse(): return []
+        p_from, p_to = GuiTools.get_mouse().from_to_points()
+        return self._world.ray_test_all(p_from, p_to).get_hits()
+
+    def _update_info(self, item):
+        txt = ''
+        if item:
+            txt = '%.3f %.3f\n%.3f°' % (
+                item._np.get_x(), item._np.get_z(), item._np.get_r())
+        self._info_txt['text'] = txt
+
+    def _on_click(self, method):
+        if self._paused:
+            return
+        for hit in self._get_hits():
+            if hit.get_node() == self._mouse_plane_node:
+                pos = hit.get_hit_pos()
+        for hit in self._get_hits():
+            for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
+                if not self._item_active:
+                    self._item_active = item
+                if item not in self.__items:
+                    method = 'on_click_l'
+                getattr(item, method)(pos)
+                img = 'move' if method == 'on_click_l' else 'rotate'
+                if not (img == 'rotate' and not item._instantiated):
+                    self._cursor.set_image('assets/images/buttons/%s.dds' % img)
+
+    def on_click_l(self):
+        self._on_click('on_click_l')
+
+    def on_click_r(self):
+        self._on_click('on_click_r')
+
+    def on_release(self):
+        if self._item_active and not self._item_active._first_command:
+            self._commands = self._commands[:self._command_idx]
+            self._commands += [self._item_active]
+            self._command_idx += 1
+            #self.__prev_btn['state'] = NORMAL
+            #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+            #self.__prev_btn['frameColor'] = fcols[0]
+            #if self._item_active._command_idx == len(self._item_active._commands) - 1:
+            #    self.__next_btn['state'] = DISABLED
+            #    self.__next_btn['frameColor'] = fcols[1]
+        self._item_active = None
+        [item.on_release() for item in self.__items]
+        self._cursor.set_image('assets/images/buttons/arrowUpLeft.dds')
+
+    def repos(self):
+        for item in self.__items:
+            item.repos_done = False
+        self.__items = sorted(self.__items, key=lambda itm: itm.__class__.__name__)
+        [item.on_aspect_ratio_changed() for item in self.__items]
+        self._side_panel.update(self.__items)
+        max_x = -float('inf')
+        for item in self.__items:
+            if not item._instantiated:
+                max_x = max(item._np.get_x(), max_x)
+        for item in self.__items:
+            if not item._instantiated:
+                item.repos_x(max_x)
+
+    def on_aspect_ratio_changed(self):
+        self.repos()
+
+    def _win_condition(self):
+        return all(itm.strategy.win_condition() for itm in self.__items) and not self._paused
+
+    def _fail_condition(self):
+        return all(itm.fail_condition() for itm in self.__items) and not self._paused and self._state == 'playing'
+
+    def on_frame(self, task):
+        hits = self._get_hits()
+        pos = None
+        for hit in self._get_hits():
+            if hit.get_node() == self._mouse_plane_node:
+                pos = hit.get_hit_pos()
+        hit_nodes = [hit.get_node() for hit in hits]
+        if self._item_active:
+            items_hit = [self._item_active]
+        else:
+            items_hit = [itm for itm in self.items if itm.node in hit_nodes]
+        items_no_hit = [itm for itm in self.items if itm not in items_hit]
+        [itm.on_mouse_on() for itm in items_hit]
+        [itm.on_mouse_off() for itm in items_no_hit]
+        if pos and self._item_active:
+            self._item_active.on_mouse_move(pos)
+        if self._dbg_items:
+            self._update_info(items_hit[0] if items_hit else None)
+        if not self.__scene_editor and self._win_condition():
+            self._start_evt_time = None
+            self._set_fail() if self._enforce_result == 'fail' else self._set_win()
+        elif self._state == 'playing' and self._fail_condition():
+            self._start_evt_time = None
+            self._set_win() if self._enforce_result == 'win' else self._set_fail()
+        elif self._testing and self._start_evt_time and globalClock.getFrameTime() - self._start_evt_time > 5.0:
+            self._start_evt_time = None
+            self._set_win() if self._enforce_result == 'win' else self._set_fail()
+        if any(itm._overlapping for itm in self.items):
+            self._cursor.set_color((.9, .1, .1, 1))
+        else:
+            self._cursor.set_color((.9, .9, .9, 1))
+        return task.cont
+
+    def cb_inst(self, item):
+        self.__items += [item]
+
+    def on_play(self):
+        self._state = 'playing'
+        #self.__prev_btn['state'] = DISABLED
+        #self.__next_btn['state'] = DISABLED
+        self.__right_btn['state'] = DISABLED
+        [itm.play() for itm in self.__items]
+        self._start_evt_time = globalClock.getFrameTime()
+
+    def on_next(self):
+        self._commands[self._command_idx].redo()
+        self._command_idx += 1
+        #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        #self.__prev_btn['state'] = NORMAL
+        #self.__prev_btn['frameColor'] = fcols[0]
+        #more_commands = self._command_idx < len(self._commands)
+        #self.__next_btn['state'] = NORMAL if more_commands else DISABLED
+        #self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
+
+    def on_prev(self):
+        self._command_idx -= 1
+        self._commands[self._command_idx].undo()
+        #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        #self.__next_btn['state'] = NORMAL
+        #self.__next_btn['frameColor'] = fcols[0]
+        #self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
+        #self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
+
+    def on_home(self):
+        self._exit_cb()
+
+    def __set_font(self):
+        self.__font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
+        self.__font.clear()
+        self.__font.set_pixels_per_unit(60)
+        self.__font.set_minfilter(Texture.FTLinearMipmapLinear)
+        self.__font.set_outline((0, 0, 0, 1), .8, .2)
+
+
+    def _set_instructions(self):
+        self._paused = True
+        self.__store_state()
+        mgr = TextPropertiesManager.get_global_ptr()
+        for name in ['mouse_l', 'mouse_r']:
+            graphic = OnscreenImage('assets/images/buttons/%s.dds' % name)
+            graphic.set_scale(.5)
+            graphic.get_texture().set_minfilter(Texture.FTLinearMipmapLinear)
+            graphic.get_texture().set_anisotropic_degree(2)
+            mgr.set_graphic(name, graphic)
+            graphic.set_z(-.2)
+            graphic.set_transparency(True)
+            graphic.detach_node()
+        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                          frameSize=(-.6, .6, -.3, .3))
+        self._txt = OnscreenText(
+            self._instr_txt(), parent=frm, font=self.__font, scale=0.06,
+            fg=(.9, .9, .9, 1), align=TextNode.A_left)
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
+        btn_scale = .05
+        mar = .06  # margin
+        z = h / 2 - self.__font.get_line_height() * self._txt['scale'][1]
+        z += (btn_scale + 2 * mar) / 2
+        self._txt['pos'] = -w / 2, z
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
+        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
+        frm['frameSize'] = fsz
+        colors = [
+            (.6, .6, .6, 1),  # ready
+            (1, 1, 1, 1), # press
+            (.8, .8, .8, 1), # rollover
+            (.4, .4, .4, .4)]
+        imgs = [self.__load_img_btn('exitRight', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        btn.set_transparency(True)
+        self._pos_mgr['close_instructions'] = btn.pos_pixel()
+
+    def _set_win(self):
+        self.__persistent.save_scene(self.__json_name, self.version(self.__json_name))
+        loader.load_sfx('assets/audio/sfx/success.ogg').play()
+        self._paused = True
+        self.__store_state()
+        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                          frameSize=(-.6, .6, -.3, .3))
+        font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
+        font.clear()
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        font.set_outline((0, 0, 0, 1), .8, .2)
+        self._txt = OnscreenText(
+            _('You win!'),
+            parent=frm,
+            font=font, scale=0.2,
+            fg=(.9, .9, .9, 1))
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
+        h = u_l[2] - l_r[2]
+        btn_scale = .05
+        mar = .06  # margin
+        z = h / 2 - font.get_line_height() * self._txt['scale'][1]
+        z += (btn_scale + 2 * mar) / 2
+        self._txt['pos'] = 0, z
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
+        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
+        frm['frameSize'] = fsz
+        colors = [
+            (.6, .6, .6, 1),  # ready
+            (1, 1, 1, 1), # press
+            (.8, .8, .8, 1), # rollover
+            (.4, .4, .4, .4)]
+        imgs = [self.__load_img_btn('home', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_end_home, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        btn.set_transparency(True)
+        self._pos_mgr['home_win'] = btn.pos_pixel()
+        imgs = [self.__load_img_btn('rewind', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(0, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_restart, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._pos_mgr['replay'] = btn.pos_pixel()
+        btn.set_transparency(True)
+        if self.__json_name:
+            enabled = self._scenes.index(self.__json_name) < len(self._scenes) - 1
+            if enabled:
+                next_scene = self._scenes[self._scenes.index(self.__json_name) + 1]
+            else:
+                next_scene = None
+        else:
+            next_scene = None
+            enabled = False
+        imgs = [self.__load_img_btn('right', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_next_scene,
+            extraArgs=[frm, next_scene], relief=FLAT,
+            frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        btn['state'] = NORMAL if enabled else DISABLED
+        self._pos_mgr['next'] = btn.pos_pixel()
+        btn.set_transparency(True)
+
+    def _set_fail(self):
+        loader.load_sfx('assets/audio/sfx/success.ogg').play()
+        self._paused = True
+        self.__store_state()
+        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                          frameSize=(-.6, .6, -.3, .3))
+        font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
+        font.clear()
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        font.set_outline((0, 0, 0, 1), .8, .2)
+        self._txt = OnscreenText(
+            _('You have failed!'),
+            parent=frm,
+            font=font, scale=0.2,
+            fg=(.9, .9, .9, 1))
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
+        h = u_l[2] - l_r[2]
+        btn_scale = .05
+        mar = .06  # margin
+        z = h / 2 - font.get_line_height() * self._txt['scale'][1]
+        z += (btn_scale + 2 * mar) / 2
+        self._txt['pos'] = 0, z
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
+        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
+        frm['frameSize'] = fsz
+        colors = [
+            (.6, .6, .6, 1),  # ready
+            (1, 1, 1, 1), # press
+            (.8, .8, .8, 1), # rollover
+            (.4, .4, .4, .4)]
+        imgs = [self.__load_img_btn('home', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_end_home, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._pos_mgr['home_win'] = btn.pos_pixel()
+        btn.set_transparency(True)
+        imgs = [self.__load_img_btn('rewind', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(0, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_restart, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._pos_mgr['replay'] = btn.pos_pixel()
+        btn.set_transparency(True)
+
+    def _on_restart(self, frm):
+        self.__on_close_instructions(frm)
+        self.reset()
+
+    def _on_end_home(self, frm):
+        self.__on_close_instructions(frm)
+        self.on_home()
+
+    def _on_next_scene(self, frm, scene):
+        self.__on_close_instructions(frm)
+        self._reload_cb(scene)
+
+    def __store_state(self):
+        btns = [
+            self.__home_btn, self.__info_btn, self.__right_btn,
+            #self.__next_btn, self.__prev_btn, self.__rewind_btn
+        ]
+        if self.__editor: btns += [self.__editor_btn]
+        self.__btn_state = [btn['state'] for btn in btns]
+        for btn in btns:
+            btn['state'] = DISABLED
+        [itm.store_state() for itm in self.__items]
+
+    def __restore_state(self):
+        btns = [
+            self.__home_btn, self.__info_btn, self.__right_btn,
+            #self.__next_btn, self.__prev_btn, self.__rewind_btn
+        ]
+        if self.__editor: btns += [self.__editor_btn]
+        for btn, state in zip(btns, self.__btn_state):
+            btn['state'] = state
+        [itm.restore_state() for itm in self.__items]
+        self._paused = False
+
+    def __on_close_instructions(self, frm):
+        frm.remove_node()
+        self.__restore_state()
+
+    def _set_test_items(self):
+        def frame_after(task):
+            self._define_test_items()
+            for itm in self._test_items:
+                self._pos_mgr[itm.name] = itm.pos2d_pixel()
+        taskMgr.doMethodLater(1.4, frame_after, 'frame after')  # after the intro sequence
+
+    def _define_test_items(self):
+        if not self.__json_name: return
+        if not self.__json_name in self.__class__.json_files:
+            with open(self.__class__.filename(self.__json_name)) as f:
+                self.__class__.json_files[self.__json_name] = loads(f.read())
+        for item in self.__class__.json_files[self.__json_name]['test_items']['pixel_space']:
+            self._pos_mgr[item['id']] = tuple(item['position'])
+        for item in self.__class__.json_files[self.__json_name]['test_items']['world_space']:
+            self._set_test_item(item['id'], tuple(item['position']))
+
+    def _set_test_item(self, name, pos):
+        self._test_items += [GfxTools.build_empty_node(name)]
+        self._test_items[-1].set_pos(pos[0], 0, pos[1])
+
+    def add_item(self, item):
+        self.__items += [item]
+
+    def _set_editor(self):
+        fields = ['world', 'plane_node', 'cb_inst', 'curr_bottom', 'repos', 'json']
+        SceneContext = namedtuple('SceneContext', fields)
+        context = SceneContext(
+            self._world,
+            self._mouse_plane_node,
+            self.cb_inst,
+            self.current_bottom,
+            self.repos,
+            {})
+        self.__scene_editor = SceneEditor(self.json, self.__json_name, context, self.add_item, self.__items, self._world, self._mouse_plane_node)
+
+    def __on_inspector_delete(self, item):
+        self.__items.remove(item)
+        self.json['items'].remove(item.json)
+        item.destroy()
index a333a14325ab408731c852faca2ee634df2e09ed..5ab8296f7d4aa4a50585369659942a8b6f1ab206 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@ from ya2.build.lang import LanguageBuilder
 from ya2.build.screenshots import ScreenshotsBuilder
 from ya2.build.images import ImagesBuilder
 from ya2.build.models import ModelsBuilder
-from pmachines.app import Pmachines
+from pmachines.application.application import Pmachines
 
 
 app_name = long_name = 'pmachines'
index 54ea9371bee72e7b17e0534ad4b32548df77d076..ab93e6f9a2b3d130f893fb55fadbabb6443eade7 100644 (file)
@@ -13,7 +13,7 @@ import logging
 from ya2.utils.dictfile import DctFile
 import importlib
 import main
-import pmachines.app
+import pmachines.application.application
 
 
 class MainTests(TestCase):
@@ -44,7 +44,7 @@ class MainTests(TestCase):
 
     def test_update(self):
         with (patch.object(main, 'argv', ['python -m unittest']),
-              patch.object(pmachines.app, 'argv', ['python -m unittest'])):
+              patch.object(pmachines.application.application, 'argv', ['python -m unittest'])):
             _main = Main()
             _main._Main__appimage_builder.update = MagicMock()
             _main._Main__pmachines._fsm.demand = MagicMock()
@@ -53,7 +53,7 @@ class MainTests(TestCase):
             _main._Main__pmachines.destroy()
         _main._Main__appimage_builder.update.assert_not_called()
         with (patch.object(sys, 'argv', ['python -m unittest', '--update']),
-              patch.object(pmachines.app, 'argv', ['python -m unittest', '--update'])):
+              patch.object(pmachines.application.application, 'argv', ['python -m unittest', '--update'])):
             _main = Main()
             _main._Main__appimage_builder.update = MagicMock()
             _main.run()
@@ -62,14 +62,14 @@ class MainTests(TestCase):
 
     def test_version(self):
         with (patch.object(main, 'argv', ['python -m unittest']),
-              patch.object(pmachines.app, 'argv', ['python -m unittest'])):
+              patch.object(pmachines.application.application, 'argv', ['python -m unittest'])):
             _main = Main()
             _main._Main__run_game = MagicMock()
             _main.run()
             _main._Main__pmachines.destroy()
             _main._Main__run_game.assert_called_once()
         with (patch.object(main, 'argv', ['python -m unittest', '--version']),
-              patch.object(pmachines.app, 'argv', ['python -m unittest', '--version'])):
+              patch.object(pmachines.application.application, 'argv', ['python -m unittest', '--version'])):
             _main = Main()
             _main._Main__run_game = MagicMock()
             _main.run()
@@ -77,7 +77,7 @@ class MainTests(TestCase):
             _main._Main__run_game.assert_not_called()
 
     @patch.object(main, 'argv', ['python -m unittest'])
-    @patch.object(pmachines.app, 'argv', ['python -m unittest'])
+    @patch.object(pmachines.application.application, 'argv', ['python -m unittest'])
     def test_run_game(self):
         _main = Main()
         _main._Main__pmachines.run = MagicMock()
@@ -85,7 +85,7 @@ class MainTests(TestCase):
         _main._Main__pmachines.destroy()
         _main._Main__pmachines.run.assert_called_once()
 
-    @patch.object(pmachines.app, 'argv', ['python -m unittest'])
+    @patch.object(pmachines.application.application, 'argv', ['python -m unittest'])
     @patch.object(main, 'argv', ['python -m unittest'])
     @patch.object(main, 'print_exc')
     def test_run_game_exception(self, print_exc_mock):