ya2 · news · projects · code · about

gettext: --no-location master
authorFlavio Calva <f.calva@gmail.com>
Sat, 10 Jun 2023 06:38:24 +0000 (08:38 +0200)
committerFlavio Calva <f.calva@gmail.com>
Sat, 10 Jun 2023 06:38:24 +0000 (08:38 +0200)
187 files changed:
.gitignore
ChangeLog [deleted file]
LICENSE
README
assets/images/buttons/down.png [new file with mode: 0644]
assets/images/buttons/menuList.png [new file with mode: 0644]
assets/images/buttons/plus.png [new file with mode: 0644]
assets/images/buttons/save.png [new file with mode: 0644]
assets/images/buttons/start_items.png [new file with mode: 0644]
assets/images/buttons/trashcan.png [new file with mode: 0644]
assets/images/buttons/up.png [new file with mode: 0644]
assets/images/buttons/wrench.png [new file with mode: 0644]
assets/locale/po/it_IT.po
assets/locale/po/pmachines.pot
assets/models/blend/background/ao.png [deleted file]
assets/models/blend/background/ao_roughness_metal.png [deleted file]
assets/models/blend/background/background.blend [deleted file]
assets/models/blend/background/base.png [deleted file]
assets/models/blend/background/bump.png [deleted file]
assets/models/blend/background/bump_frame.png [deleted file]
assets/models/blend/background/bump_pattern.png [deleted file]
assets/models/blend/background/metal.png [deleted file]
assets/models/blend/background/normal.png [deleted file]
assets/models/blend/background/roughness.png [deleted file]
assets/models/blend/backgrounds/metal/ao.png [new file with mode: 0644]
assets/models/blend/backgrounds/metal/ao_roughness_metal.png [new file with mode: 0644]
assets/models/blend/backgrounds/metal/background.blend [new file with mode: 0644]
assets/models/blend/backgrounds/metal/base.png [new file with mode: 0644]
assets/models/blend/backgrounds/metal/metal.png [new file with mode: 0644]
assets/models/blend/backgrounds/metal/normal.png [new file with mode: 0644]
assets/models/blend/backgrounds/metal/roughness.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/ao.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/ao_roughness_metal.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/background.blend [new file with mode: 0644]
assets/models/blend/backgrounds/wood/base.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/bump.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/bump_frame.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/bump_pattern.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/metal.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/normal.png [new file with mode: 0644]
assets/models/blend/backgrounds/wood/roughness.png [new file with mode: 0644]
assets/models/blend/test_handle/albedo.png [new file with mode: 0644]
assets/models/blend/test_handle/test_handle.blend [new file with mode: 0644]
assets/scenes/basketball.json [new file with mode: 0644]
assets/scenes/box.json [new file with mode: 0644]
assets/scenes/domino.json [new file with mode: 0644]
assets/scenes/domino_box.json [new file with mode: 0644]
assets/scenes/domino_box_basketball.json [new file with mode: 0644]
assets/scenes/index.json [new file with mode: 0644]
assets/scenes/teeter_domino_box_basketball.json [new file with mode: 0644]
assets/scenes/teeter_tooter.json [new file with mode: 0644]
audio/__init__.py [deleted file]
audio/music.py [deleted file]
gui/__init__.py [deleted file]
gui/menu.py [deleted file]
gui/sidepanel.py [deleted file]
licenses/0bsd.txt [new file with mode: 0644]
licenses/bsd.txt [deleted file]
licenses/gpl.txt [new file with mode: 0644]
logics/__init__.py [deleted file]
logics/app.py [deleted file]
logics/items/__init__.py [deleted file]
logics/items/background.py [deleted file]
logics/items/basketball.py [deleted file]
logics/items/box.py [deleted file]
logics/items/domino.py [deleted file]
logics/items/item.py [deleted file]
logics/items/shelf.py [deleted file]
logics/items/teetertooter.py [deleted file]
logics/posmgr.py [deleted file]
logics/scene.py [deleted file]
logics/scenes/__init__.py [deleted file]
logics/scenes/scene_basketball.py [deleted file]
logics/scenes/scene_box.py [deleted file]
logics/scenes/scene_domino.py [deleted file]
logics/scenes/scene_domino_box.py [deleted file]
logics/scenes/scene_domino_box_basketball.py [deleted file]
logics/scenes/scene_teeter_domino_box_basketball.py [deleted file]
logics/scenes/scene_teeter_tooter.py [deleted file]
main.py
pmachines/__init__.py [new file with mode: 0644]
pmachines/application/__init__.py [new file with mode: 0644]
pmachines/application/application.py [new file with mode: 0755]
pmachines/application/persistency.py [new file with mode: 0644]
pmachines/audio/__init__.py [new file with mode: 0644]
pmachines/audio/music.py [new file with mode: 0644]
pmachines/editor/__init__.py [new file with mode: 0644]
pmachines/editor/augmented_frame.py [new file with mode: 0644]
pmachines/editor/inspector.py [new file with mode: 0644]
pmachines/editor/scene.py [new file with mode: 0644]
pmachines/editor/scene_list.py [new file with mode: 0644]
pmachines/editor/start_items.py [new file with mode: 0644]
pmachines/gui/__init__.py [new file with mode: 0644]
pmachines/gui/credits_page.py [new file with mode: 0644]
pmachines/gui/main_page.py [new file with mode: 0644]
pmachines/gui/menu.py [new file with mode: 0644]
pmachines/gui/options_page.py [new file with mode: 0644]
pmachines/gui/play_page.py [new file with mode: 0644]
pmachines/gui/sidepanel.py [new file with mode: 0644]
pmachines/items/__init__.py [new file with mode: 0644]
pmachines/items/background.py [new file with mode: 0644]
pmachines/items/basketball.py [new file with mode: 0644]
pmachines/items/box.py [new file with mode: 0644]
pmachines/items/domino.py [new file with mode: 0644]
pmachines/items/item.py [new file with mode: 0644]
pmachines/items/shelf.py [new file with mode: 0644]
pmachines/items/teetertooter.py [new file with mode: 0644]
pmachines/items/test_item.py [new file with mode: 0644]
pmachines/scene/__init__.py [new file with mode: 0644]
pmachines/scene/scene.py [new file with mode: 0644]
prj.org
requirements.txt
setup.py
tests/assets/images/icon16.png [new file with mode: 0644]
tests/assets/images/icon32.png [new file with mode: 0644]
tests/assets/locale/po/it_IT.po [new file with mode: 0644]
tests/assets/locale/po/test.pot [new file with mode: 0644]
tests/assets/models/blend/cube/cube.blend [deleted file]
tests/assets/models/blend/cube/diffuse.png [deleted file]
tests/assets/models/blend/cube1/cube.blend [new file with mode: 0644]
tests/assets/models/blend/cube1/diffuse.png [new file with mode: 0644]
tests/assets/models/blend/cube2/cube.blend [new file with mode: 0644]
tests/assets/models/blend/cube2/diffuse.png [new file with mode: 0644]
tests/functional_test.py
tests/pmachines/__init__.py [new file with mode: 0644]
tests/pmachines/audio/__init__.py [new file with mode: 0644]
tests/pmachines/audio/test_music.py [new file with mode: 0644]
tests/test_functional.py
tests/test_l10n.py [new file with mode: 0644]
tests/test_lint.py [new file with mode: 0644]
tests/test_main.py [new file with mode: 0644]
tests/test_setup.py
tests/test_system.py [new file with mode: 0644]
tests/ya2/build/test_blend2gltf.py [new file with mode: 0644]
tests/ya2/build/test_build.py
tests/ya2/build/test_images.py [new file with mode: 0644]
tests/ya2/build/test_lang.py
tests/ya2/build/test_models.py
tests/ya2/build/test_mtprocesser.py [deleted file]
tests/ya2/build/test_screenshots.py [new file with mode: 0644]
tests/ya2/patterns/__init__.py [deleted file]
tests/ya2/utils/gui/__init__.py [new file with mode: 0644]
tests/ya2/utils/gui/test_cursor.py [new file with mode: 0644]
tests/ya2/utils/gui/test_gui.py [new file with mode: 0644]
tests/ya2/utils/test_asserts.py [new file with mode: 0644]
tests/ya2/utils/test_audio.py [new file with mode: 0644]
tests/ya2/utils/test_decorator.py [new file with mode: 0644]
tests/ya2/utils/test_functional.py [new file with mode: 0644]
tests/ya2/utils/test_gameobject.py [deleted file]
tests/ya2/utils/test_gfx.py [new file with mode: 0644]
tests/ya2/utils/test_language.py [new file with mode: 0644]
tests/ya2/utils/test_log.py [new file with mode: 0644]
tests/ya2/utils/test_logics.py [new file with mode: 0644]
tests/ya2/utils/test_observer.py [deleted file]
ya2/__init__.py
ya2/build/blend2gltf.py
ya2/build/build.py
ya2/build/images.py
ya2/build/lang.py
ya2/build/models.py
ya2/build/mtprocesser.py [deleted file]
ya2/build/screenshots.py
ya2/p3d/__init__.py [deleted file]
ya2/p3d/gfx.py [deleted file]
ya2/p3d/gui.py [deleted file]
ya2/p3d/p3d.py [deleted file]
ya2/patterns/__init__.py [deleted file]
ya2/patterns/gameobject.py [deleted file]
ya2/patterns/observer.py [deleted file]
ya2/tools/pdfsingle.py [deleted file]
ya2/utils/asserts.py [new file with mode: 0644]
ya2/utils/audio.py [new file with mode: 0644]
ya2/utils/build_metal_texture.py [new file with mode: 0644]
ya2/utils/cursor.py [deleted file]
ya2/utils/decorator.py [new file with mode: 0644]
ya2/utils/dictfile.py
ya2/utils/functional.py
ya2/utils/gfx.py [new file with mode: 0755]
ya2/utils/gui/__init__.py [new file with mode: 0644]
ya2/utils/gui/base_page.py [new file with mode: 0644]
ya2/utils/gui/cursor.py [new file with mode: 0644]
ya2/utils/gui/gui.py [new file with mode: 0644]
ya2/utils/gui/menu.py [new file with mode: 0644]
ya2/utils/lang.py [deleted file]
ya2/utils/language.py [new file with mode: 0644]
ya2/utils/log.py
ya2/utils/logics.py [new file with mode: 0755]

index bcfb59f1bb7b4c7e5358a8fe86e25db938378d67..aead0fd91e25edd73180758ce5ea9a4d9a519a6c 100644 (file)
@@ -1,17 +1,17 @@
 TAGS
 TAGS
+*.pyc
+*.mo
+*.blend1
 *.bam
 *.txo
 *.dds
 *.bam
 *.txo
 *.dds
-*.mo
-*.pyc
-*.blend1
-/hash_cache.txt
-/assets/bld_version.txt
+/models_manifest.txt
 /options.ini
 /options.ini
+/assets/build_version.txt
+/assets/images/scenes/
+/assets/locale/it_IT/
 /assets/models/gltf/
 /assets/models/gltf/
-/tests/assets/models/gltf/
 /build/
 /dist/
 /build/
 /dist/
-/assets/locale/
-/assets/images/scenes/
+/tests/assets/models/gltf/
 /uml/*.png
 /uml/*.png
diff --git a/ChangeLog b/ChangeLog
deleted file mode 100644 (file)
index 30a7fb5..0000000
--- a/ChangeLog
+++ /dev/null
@@ -1,10 +0,0 @@
-* itch.io
-* flatpak
-* windows build
-* appimage
-* functional tests
-* first commit
-       * current rc *
-       * current *
-       * previous rc *
-       * previous *
diff --git a/LICENSE b/LICENSE
index 78e4364b931179857dd58eb1cfe44f7e5ade46ad..0d8ac03760d6e7ae16a3c5624aaa2968e2d84c3c 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,12 @@
 The source code (*.py, *.glsl and generally every text file) is licensed under
 The source code (*.py, *.glsl and generally every text file) is licensed under
-3-clause BSD License (see the file licenses/bsd.txt for details).
+0BSD License (see the file licenses/0bsd.txt for details) with the
+following exception.
+
+The package pmachines/ is licensed under GPLv3 (see the file
+licenses/gpl-3.0.txt for details). Please note that this covers *only* the
+gameplay code specific for this project. You are free to use the more general
+code (that you should find more useful for your cases) as said in the previous
+paragraph.
 
 Several assets are from OpenGameArt.com, they are licensed under several forms
 of CC, look at licenses/licenses.txt for details.
 
 Several assets are from OpenGameArt.com, they are licensed under several forms
 of CC, look at licenses/licenses.txt for details.
diff --git a/README b/README
index 0c431cabac19c29e167303c101c31d6c1e604d48..8010972e727582ca975c9f417bc4cb68e61b3ab5 100644 (file)
--- a/README
+++ b/README
@@ -1,12 +1,12 @@
 pmachines
 
 pmachines is an open source puzzle game based on Rube Goldberg devices
 pmachines
 
 pmachines is an open source puzzle game based on Rube Goldberg devices
-developed by Ya2 using Panda3D (http://www.panda3d.org) for Windows, and Linux.
-More information can be found on http://www.ya2.it/pages/pmachines.html.
+developed by Ya2 using Panda3D (https://www.panda3d.org) for Windows, and Linux.
+More information can be found on https://www.ya2.it/pages/pmachines.html.
 
 
-It requires Python 3.x.
+It requires Python 3.10 or higher.
 
 
-To run it you should create assets:
+To run it, you should create the assets:
 
 * python setup.py images lang models
 
 
 * python setup.py images lang models
 
@@ -17,7 +17,7 @@ To create the builds, you can use the awesome Panda3D's deployment tools:
 Here's a short guide about installing and preparing your environment for pmachines.
 
 * clone the repository:
 Here's a short guide about installing and preparing your environment for pmachines.
 
 * clone the repository:
-  git clone http://www.ya2tech.it/git/pmachines.git
+  git clone https://www.ya2.it/git/pmachines.git
 * go into the directory: cd pmachines
 * create a python3 virtualenv: python3 -m venv venv
 * activate the virtualenv: . ./venv/bin/activate
 * go into the directory: cd pmachines
 * create a python3 virtualenv: python3 -m venv venv
 * activate the virtualenv: . ./venv/bin/activate
diff --git a/assets/images/buttons/down.png b/assets/images/buttons/down.png
new file mode 100644 (file)
index 0000000..50cba60
Binary files /dev/null and b/assets/images/buttons/down.png differ
diff --git a/assets/images/buttons/menuList.png b/assets/images/buttons/menuList.png
new file mode 100644 (file)
index 0000000..92e1528
Binary files /dev/null and b/assets/images/buttons/menuList.png differ
diff --git a/assets/images/buttons/plus.png b/assets/images/buttons/plus.png
new file mode 100644 (file)
index 0000000..3f5cf37
Binary files /dev/null and b/assets/images/buttons/plus.png differ
diff --git a/assets/images/buttons/save.png b/assets/images/buttons/save.png
new file mode 100644 (file)
index 0000000..a40de9f
Binary files /dev/null and b/assets/images/buttons/save.png differ
diff --git a/assets/images/buttons/start_items.png b/assets/images/buttons/start_items.png
new file mode 100644 (file)
index 0000000..3db9e99
Binary files /dev/null and b/assets/images/buttons/start_items.png differ
diff --git a/assets/images/buttons/trashcan.png b/assets/images/buttons/trashcan.png
new file mode 100644 (file)
index 0000000..cbd352d
Binary files /dev/null and b/assets/images/buttons/trashcan.png differ
diff --git a/assets/images/buttons/up.png b/assets/images/buttons/up.png
new file mode 100644 (file)
index 0000000..d1b642c
Binary files /dev/null and b/assets/images/buttons/up.png differ
diff --git a/assets/images/buttons/wrench.png b/assets/images/buttons/wrench.png
new file mode 100644 (file)
index 0000000..5f74641
Binary files /dev/null and b/assets/images/buttons/wrench.png differ
index d70369e25bc708656c6f78f02a530f6e9a382649..643a5d8381342db34793461bd6edc43f5dc010af 100644 (file)
@@ -9,7 +9,7 @@ msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"PO-Revision-Date: 2022-01-19 17:36+0100\n"
+"PO-Revision-Date: 2023-02-06 17:47+0100\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "Language: \n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "Language: \n"
@@ -17,182 +17,280 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: lib/engine/gui/page.py:188
-msgid "Quit"
-msgstr "Esci"
+msgid "Collapse/Expand"
+msgstr "Nascondi/Mostra"
 
 
-#: lib/engine/gui/page.py:188 game/menu.py:147 game/menu.py:169
-#: game/menu.py:197
-msgid "Back"
-msgstr "Indietro"
+msgid "Write the file names (without the extension), one file for each line"
+msgstr "Scrivi i nomi dei file (senza estensione), uno per riga."
+
+msgid "the list of the scenes in the proper order"
+msgstr "la lista della scene nell'ordine"
+
+msgid "Close the scene editor"
+msgstr "Chiudi l'editor della scena"
+
+msgid "Save the scene list"
+msgstr "Salva la lista delle scene"
+
+msgid "You have unsaved changes. Really quit?"
+msgstr "Hai modifiche non salvate, vuoi veramente uscire?"
+
+msgid "class"
+msgstr "classe"
+
+msgid "class of the item"
+msgstr "classe dell'oggetto"
+
+msgid "count"
+msgstr "quantità"
+
+msgid "number of the items"
+msgstr "numero degli oggetti"
+
+msgid "scale"
+msgstr "scala"
+
+msgid "scale (e.g. 1.2)"
+msgstr "scala (e.g. 1.2)"
+
+msgid "mass"
+msgstr "massa"
+
+msgid "mass (default 1)"
+msgstr "massa (default 1)"
+
+msgid "restitution"
+msgstr "rimbalzo"
+
+msgid "restitution (default 0.5)"
+msgstr "rimbalzo (default 0.5)"
+
+msgid "friction"
+msgstr "frizione"
+
+msgid "friction (default 0.5)"
+msgstr "frizione (default 0.5)"
+
+msgid "id"
+msgstr "id"
+
+msgid "strategy"
+msgstr "strategia"
+
+msgid "the strategy of the item"
+msgstr "la strategia dell'oggetto"
+
+msgid "strategy_args"
+msgstr "argomenti strategia"
+
+msgid "the arguments of the strategy"
+msgstr "gli argomenti della strategia"
+
+msgid "Close"
+msgstr "Chiudi"
+
+msgid "Save"
+msgstr "Salva"
+
+msgid "Delete"
+msgstr "Cancella"
+
+msgid "New"
+msgstr "Nuovo"
+
+msgid "Next"
+msgstr "Prossimo"
+
+msgid "There are errors in the strategy args."
+msgstr "Ci sono errori negli argomenti della strategia."
+
+msgid "position"
+msgstr "posizione"
+
+msgid "position (e.g. 0.1 2.3 4.5)"
+msgstr "posizione (e.g. 0.1 2.3 4.5)"
+
+msgid "roll"
+msgstr "roll"
+
+msgid "roll (e.g. 90)"
+msgstr "roll (e.g. 90)"
+
+msgid "mass (default 1; 0 if fixed)"
+msgstr "massa (default 1; 0 se fisso)"
+
+msgid "id of the item (for the strategies)"
+msgstr "id dell'oggetto (per le strategie)"
+
+msgid "Delete the item"
+msgstr "Cancella l'oggetto"
 
 
-#: lib/engine/gui/mainpage.py:34
-msgid "version: "
-msgstr "versione: "
+msgid "Filename"
+msgstr "Filename"
+
+msgid "The name of the file without the extension"
+msgstr "Il nome del file senza estensione"
+
+msgid "Name"
+msgstr "Nome"
+
+msgid "The title of the scene"
+msgstr "Il titolo della scena"
+
+msgid "Background"
+msgstr "Sfondo"
+
+msgid "The name of the background"
+msgstr "Il nome dello sfondo"
+
+msgid "Description"
+msgstr "Descrizione"
+
+msgid "The description of the scene"
+msgstr "La descrizione della scena"
+
+msgid "Save the scene"
+msgstr "Salva la scena"
+
+msgid "Set the sorting of the scenes"
+msgstr "Imposta l'ordinamento delle scene"
+
+msgid "new item"
+msgstr "nuovo oggetto"
+
+msgid "Create a new item"
+msgstr "Crea un nuovo oggetto"
+
+msgid "Initial items"
+msgstr "Oggetti iniziali"
+
+msgid "New scene"
+msgstr "Nuova scena"
+
+msgid ""
+"Code and gfx\n"
+"  \ 1scale\ 1Flavio Calva\ 2\n"
+"\n"
+"\n"
+"Music\n"
+"  \ 1scale\ 1Stefan Grossmann\ 2"
+msgstr ""
+"Code and gfx\n"
+"  \ 1scale\ 1Flavio Calva\ 2\n"
+"\n"
+"\n"
+"Music\n"
+"  \ 1scale\ 1Stefan Grossmann\ 2"
+
+msgid "Website"
+msgstr "Sito web"
+
+msgid ""
+"Special thanks to:\n"
+"  \ 1scale\ 1rdb\ 2\n"
+"  \ 1scale\ 1Luisa Tenuta\ 2\n"
+"  \ 1scale\ 1Damiana Ercolani\ 2"
+msgstr ""
+"Ringraziamenti speciali a:\n"
+"  \ 1scale\ 1rdb\ 2\n"
+"  \ 1scale\ 1Luisa Tenuta\ 2\n"
+"  \ 1scale\ 1Damiana Ercolani\ 2"
 
 
-#: lib/engine/gui/mainpage.py:41
-msgid "made with heart with panda3d, panda3d-simplepbr, panda3d-gltf"
-msgstr "fatto col cuore con panda3d, panda3d-simplepbr, panda3d-gltf"
+msgid "Back"
+msgstr "Indietro"
 
 
-#: game/menu.py:72
 msgid "Play"
 msgstr "Gioca"
 
 msgid "Play"
 msgstr "Gioca"
 
-#: game/menu.py:75
 msgid "Options"
 msgstr "Opzioni"
 
 msgid "Options"
 msgstr "Opzioni"
 
-#: game/menu.py:78
 msgid "Credits"
 msgstr "Riconoscimenti"
 
 msgid "Credits"
 msgstr "Riconoscimenti"
 
-#: game/menu.py:81
 msgid "Exit"
 msgstr "Esci"
 
 msgid "Exit"
 msgstr "Esci"
 
-#: game/menu.py:87 game/menu.py:90 game/menu.py:230
 msgid "English"
 msgstr "Inglese"
 
 msgid "English"
 msgstr "Inglese"
 
-#: game/menu.py:87 game/menu.py:91 game/menu.py:231
 msgid "Italian"
 msgstr "Italiano"
 
 msgid "Italian"
 msgstr "Italiano"
 
-#: game/menu.py:94
 msgid "Language"
 msgstr "Linguaggio"
 
 msgid "Language"
 msgstr "Linguaggio"
 
-#: game/menu.py:100
 msgid "Volume"
 msgid "Volume"
-msgstr ""
+msgstr "Volume"
 
 
-#: game/menu.py:110
 msgid "Fullscreen"
 msgid "Fullscreen"
-msgstr ""
+msgstr "Schermo intero"
 
 
-#: game/menu.py:129
 msgid "Resolution"
 msgid "Resolution"
-msgstr ""
+msgstr "Risoluzione"
 
 
-#: game/menu.py:135
-#, fuzzy
 msgid "Antialiasing"
 msgid "Antialiasing"
-msgstr "Italiano"
+msgstr "Antialiasing"
 
 
-#: game/menu.py:141
 msgid "Shadows"
 msgid "Shadows"
-msgstr ""
+msgstr "Ombre"
 
 
-#: game/menu.py:156
-msgid ""
-"Code and gfx\n"
-"  \ 1scale\ 1Flavio Calva\ 2\n"
-"\n"
-"\n"
-"Music\n"
-"  \ 1scale\ 1Stefan Grossmann\ 2"
-msgstr ""
+msgid "Scene: "
+msgstr "Scena: "
 
 
-#: game/menu.py:161
-msgid "Website"
-msgstr ""
+msgid "Instructions"
+msgstr "Istruzioni"
 
 
-#: game/menu.py:164
-msgid ""
-"Special thanks to:\n"
-"  \ 1scale\ 1rdb\ 2\n"
-"  \ 1scale\ 1Luisa Tenuta\ 2\n"
-"  \ 1scale\ 1Damiana Ercolani\ 2"
-msgstr ""
+msgid "Run"
+msgstr "Vai"
+
+msgid "Editor"
+msgstr "Editor"
 
 
-#: game/scene.py:433
 msgid "You win!"
 msgid "You win!"
-msgstr ""
+msgstr "Hai vinto!"
 
 
-#: game/scene.py:502
 msgid "You have failed!"
 msgid "You have failed!"
-msgstr ""
-
-#: game/scenes/scene_domino.py:13
-msgid "Domino"
-msgstr ""
-
-#: game/scenes/scene_domino.py:36 game/scenes/scene_box.py:36
-#: game/scenes/scene_domino_box.py:49 game/scenes/scene_basketball.py:52
-#: game/scenes/scene_domino_box_basketball.py:38
-#: game/scenes/scene_teeter_tooter.py:39
-#: game/scenes/scene_teeter_domino_box_basketball.py:48
-msgid "Scene: "
-msgstr ""
+msgstr "Hai perso!"
 
 
-#: game/scenes/scene_domino.py:37
-msgid ""
-"Goal: every domino piece must fall\n"
-"\n"
-msgstr ""
+msgid "Teeter tooter, domino, box and basket ball"
+msgstr "Altalena, domino, scatola e palla da basket"
 
 
-#: game/scenes/scene_domino.py:38 game/scenes/scene_box.py:38
-#: game/scenes/scene_domino_box.py:51 game/scenes/scene_basketball.py:54
-#: game/scenes/scene_domino_box_basketball.py:40
-#: game/scenes/scene_teeter_tooter.py:41
-#: game/scenes/scene_teeter_domino_box_basketball.py:50
-msgid ""
-"keep \ 5mouse_l\ 5 pressed to drag an item\n"
-"\n"
-"keep \ 5mouse_r\ 5 pressed to rotate an item"
-msgstr ""
-"tieni premuto \ 5mouse_l\ 5 per trascinare un oggetto\n"
-"\n"
-"tieni premuto \ 5mouse_r\ 5 per ruotare un oggetto"
+msgid "Goal: every domino piece must be hit"
+msgstr "Obiettivo: ogni pezzo del domino deve essere colpito"
 
 
-#: game/scenes/scene_box.py:13
-msgid "Box"
-msgstr ""
+msgid "Goal: you must hit every domino piece"
+msgstr "Obiettivo: devi colpire tutti i pezzi del domino"
 
 
-#: game/scenes/scene_box.py:37
-msgid ""
-"Goal: the left box must hit the right box\n"
-"\n"
-msgstr ""
+msgid "Domino, box and basket ball"
+msgstr "Domino, scatola e palla da basket"
 
 
-#: game/scenes/scene_domino_box.py:13
-msgid "Domino and box"
-msgstr ""
+msgid "Domino"
+msgstr "Domino"
 
 
-#: game/scenes/scene_domino_box.py:50
-msgid ""
-"Goal: only the last piece of each row must be up\n"
-"\n"
-msgstr ""
+msgid "keep \ 5mouse_l\ 5 pressed to drag an item"
+msgstr "tieni \\5mouse_l\\5 per muovere un oggetto"
 
 
-#: game/scenes/scene_basketball.py:13
 msgid "Basket ball"
 msgid "Basket ball"
-msgstr ""
+msgstr "Palla da basket"
 
 
-#: game/scenes/scene_basketball.py:53 game/scenes/scene_teeter_tooter.py:40
-msgid ""
-"Goal: you must hit every domino piece\n"
-"\n"
-msgstr ""
+msgid "Teeter tooter"
+msgstr "Altalena"
 
 
-#: game/scenes/scene_domino_box_basketball.py:13
-msgid "Domino, box and basket ball"
-msgstr ""
+msgid "Box"
+msgstr "Scatola"
 
 
-#: game/scenes/scene_domino_box_basketball.py:39
-#: game/scenes/scene_teeter_domino_box_basketball.py:49
-msgid ""
-"Goal: every domino piece must be hit\n"
-"\n"
-msgstr ""
+msgid "Domino and box"
+msgstr "Domino e scatola"
 
 
-#: game/scenes/scene_teeter_tooter.py:13
-msgid "Teeter tooter"
-msgstr ""
+msgid "keep \ 5mouse_r\ 5 pressed to rotate an item"
+msgstr "tieni \\5mouse_r\\5 per ruotare un oggetto"
 
 
-#: game/scenes/scene_teeter_domino_box_basketball.py:13
-msgid "Teeter tooter, domino, box and basket ball"
-msgstr ""
+msgid "Goal: the left box must hit the right box"
+msgstr "Obiettivo: la scatola a sinistra deve colpire la scatola a destra"
+
+msgid "Goal: every domino piece must fall"
+msgstr "Obiettivo: ogni pezzo del domino deve cadere"
 
 
-#~ msgid "Support us"
-#~ msgstr "Supportaci"
+msgid "Goal: only the last piece of each row must be up"
+msgstr "Obiettivo: l'ultimo pezzo di ogni riga deve rimanere in piedi"
index 07d9e499dca3dc6bac2846bc9c2faca75d09ac34..a4e41812fecdfb30e696f4b1dfcd72969458a79b 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-03-11 10:04+0100\n"
+"POT-Creation-Date: 2023-06-14 16:41+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,60 +17,156 @@ msgstr ""
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: pmachines/menu.py:71
-msgid "Play"
+msgid "Collapse/Expand"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:74
-msgid "Options"
+msgid "Write the file names (without the extension), one file for each line"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:77
-msgid "Credits"
+msgid "the list of the scenes in the proper order"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:80
-msgid "Exit"
+msgid "Close the scene editor"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:86 pmachines/menu.py:89 pmachines/menu.py:237
-msgid "English"
+msgid "Save the scene list"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:86 pmachines/menu.py:90 pmachines/menu.py:238
-msgid "Italian"
+msgid "You have unsaved changes. Really quit?"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:93
-msgid "Language"
+msgid "class"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:99
-msgid "Volume"
+msgid "class of the item"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:109
-msgid "Fullscreen"
+msgid "count"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:128
-msgid "Resolution"
+msgid "number of the items"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:134
-msgid "Antialiasing"
+msgid "scale"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:140
-msgid "Shadows"
+msgid "scale (e.g. 1.2)"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:146 pmachines/menu.py:168 pmachines/menu.py:204
-#: lib/engine/gui/page.py:188
-msgid "Back"
+msgid "mass"
+msgstr ""
+
+msgid "mass (default 1)"
+msgstr ""
+
+msgid "restitution"
+msgstr ""
+
+msgid "restitution (default 0.5)"
+msgstr ""
+
+msgid "friction"
+msgstr ""
+
+msgid "friction (default 0.5)"
+msgstr ""
+
+msgid "id"
+msgstr ""
+
+msgid "strategy"
+msgstr ""
+
+msgid "the strategy of the item"
+msgstr ""
+
+msgid "strategy_args"
+msgstr ""
+
+msgid "the arguments of the strategy"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
+msgid "Delete"
+msgstr ""
+
+msgid "New"
+msgstr ""
+
+msgid "Next"
+msgstr ""
+
+msgid "There are errors in the strategy args."
+msgstr ""
+
+msgid "position"
+msgstr ""
+
+msgid "position (e.g. 0.1 2.3 4.5)"
+msgstr ""
+
+msgid "roll"
+msgstr ""
+
+msgid "roll (e.g. 90)"
+msgstr ""
+
+msgid "mass (default 1; 0 if fixed)"
+msgstr ""
+
+msgid "id of the item (for the strategies)"
+msgstr ""
+
+msgid "Delete the item"
+msgstr ""
+
+msgid "Filename"
+msgstr ""
+
+msgid "The name of the file without the extension"
+msgstr ""
+
+msgid "Name"
+msgstr ""
+
+msgid "The title of the scene"
+msgstr ""
+
+msgid "Background"
+msgstr ""
+
+msgid "The name of the background"
+msgstr ""
+
+msgid "Description"
+msgstr ""
+
+msgid "The description of the scene"
+msgstr ""
+
+msgid "Save the scene"
+msgstr ""
+
+msgid "Set the sorting of the scenes"
+msgstr ""
+
+msgid "new item"
+msgstr ""
+
+msgid "Create a new item"
+msgstr ""
+
+msgid "Initial items"
+msgstr ""
+
+msgid "New scene"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/menu.py:155
 msgid ""
 "Code and gfx\n"
 "  \ 1scale\ 1Flavio Calva\ 2\n"
 msgid ""
 "Code and gfx\n"
 "  \ 1scale\ 1Flavio Calva\ 2\n"
@@ -80,11 +176,9 @@ msgid ""
 "  \ 1scale\ 1Stefan Grossmann\ 2"
 msgstr ""
 
 "  \ 1scale\ 1Stefan Grossmann\ 2"
 msgstr ""
 
-#: pmachines/menu.py:160
 msgid "Website"
 msgstr ""
 
 msgid "Website"
 msgstr ""
 
-#: pmachines/menu.py:163
 msgid ""
 "Special thanks to:\n"
 "  \ 1scale\ 1rdb\ 2\n"
 msgid ""
 "Special thanks to:\n"
 "  \ 1scale\ 1rdb\ 2\n"
@@ -92,103 +186,101 @@ msgid ""
 "  \ 1scale\ 1Damiana Ercolani\ 2"
 msgstr ""
 
 "  \ 1scale\ 1Damiana Ercolani\ 2"
 msgstr ""
 
-#: pmachines/scene.py:429
-msgid "You win!"
+msgid "Back"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scene.py:506
-msgid "You have failed!"
+msgid "Play"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_domino.py:15
-msgid "Domino"
+msgid "Options"
+msgstr ""
+
+msgid "Credits"
+msgstr ""
+
+msgid "Exit"
+msgstr ""
+
+msgid "English"
+msgstr ""
+
+msgid "Italian"
+msgstr ""
+
+msgid "Language"
+msgstr ""
+
+msgid "Volume"
+msgstr ""
+
+msgid "Fullscreen"
+msgstr ""
+
+msgid "Resolution"
+msgstr ""
+
+msgid "Antialiasing"
+msgstr ""
+
+msgid "Shadows"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_domino.py:38 pmachines/scenes/scene_box.py:38
-#: pmachines/scenes/scene_domino_box.py:51
-#: pmachines/scenes/scene_basketball.py:54
-#: pmachines/scenes/scene_domino_box_basketball.py:40
-#: pmachines/scenes/scene_teeter_tooter.py:41
-#: pmachines/scenes/scene_teeter_domino_box_basketball.py:50
 msgid "Scene: "
 msgstr ""
 
 msgid "Scene: "
 msgstr ""
 
-#: pmachines/scenes/scene_domino.py:39
-msgid ""
-"Goal: every domino piece must fall\n"
-"\n"
+msgid "Instructions"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_domino.py:40 pmachines/scenes/scene_box.py:40
-#: pmachines/scenes/scene_domino_box.py:53
-#: pmachines/scenes/scene_basketball.py:56
-#: pmachines/scenes/scene_domino_box_basketball.py:42
-#: pmachines/scenes/scene_teeter_tooter.py:43
-#: pmachines/scenes/scene_teeter_domino_box_basketball.py:52
-msgid ""
-"keep \ 5mouse_l\ 5 pressed to drag an item\n"
-"\n"
-"keep \ 5mouse_r\ 5 pressed to rotate an item"
+msgid "Run"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_box.py:15
-msgid "Box"
+msgid "Editor"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_box.py:39
-msgid ""
-"Goal: the left box must hit the right box\n"
-"\n"
+msgid "You win!"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_domino_box.py:15
-msgid "Domino and box"
+msgid "You have failed!"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_domino_box.py:52
-msgid ""
-"Goal: only the last piece of each row must be up\n"
-"\n"
+msgid "Teeter tooter, domino, box and basket ball"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_basketball.py:15
-msgid "Basket ball"
+msgid "Goal: every domino piece must be hit"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_basketball.py:55
-#: pmachines/scenes/scene_teeter_tooter.py:42
-msgid ""
-"Goal: you must hit every domino piece\n"
-"\n"
+msgid "Goal: you must hit every domino piece"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_domino_box_basketball.py:15
 msgid "Domino, box and basket ball"
 msgstr ""
 
 msgid "Domino, box and basket ball"
 msgstr ""
 
-#: pmachines/scenes/scene_domino_box_basketball.py:41
-#: pmachines/scenes/scene_teeter_domino_box_basketball.py:51
-msgid ""
-"Goal: every domino piece must be hit\n"
-"\n"
+msgid "Domino"
+msgstr ""
+
+msgid "keep \ 5mouse_l\ 5 pressed to drag an item"
+msgstr ""
+
+msgid "Basket ball"
 msgstr ""
 
 msgstr ""
 
-#: pmachines/scenes/scene_teeter_tooter.py:15
 msgid "Teeter tooter"
 msgstr ""
 
 msgid "Teeter tooter"
 msgstr ""
 
-#: pmachines/scenes/scene_teeter_domino_box_basketball.py:15
-msgid "Teeter tooter, domino, box and basket ball"
+msgid "Box"
+msgstr ""
+
+msgid "Domino and box"
+msgstr ""
+
+msgid "keep \ 5mouse_r\ 5 pressed to rotate an item"
 msgstr ""
 
 msgstr ""
 
-#: lib/engine/gui/page.py:188
-msgid "Quit"
+msgid "Goal: the left box must hit the right box"
 msgstr ""
 
 msgstr ""
 
-#: lib/engine/gui/mainpage.py:34
-msgid "version: "
+msgid "Goal: every domino piece must fall"
 msgstr ""
 
 msgstr ""
 
-#: lib/engine/gui/mainpage.py:41
-msgid "made with heart with panda3d, panda3d-simplepbr, panda3d-gltf"
+msgid "Goal: only the last piece of each row must be up"
 msgstr ""
 msgstr ""
diff --git a/assets/models/blend/background/ao.png b/assets/models/blend/background/ao.png
deleted file mode 100644 (file)
index a3a90b5..0000000
Binary files a/assets/models/blend/background/ao.png and /dev/null differ
diff --git a/assets/models/blend/background/ao_roughness_metal.png b/assets/models/blend/background/ao_roughness_metal.png
deleted file mode 100644 (file)
index d191abf..0000000
Binary files a/assets/models/blend/background/ao_roughness_metal.png and /dev/null differ
diff --git a/assets/models/blend/background/background.blend b/assets/models/blend/background/background.blend
deleted file mode 100644 (file)
index 4808dd2..0000000
Binary files a/assets/models/blend/background/background.blend and /dev/null differ
diff --git a/assets/models/blend/background/base.png b/assets/models/blend/background/base.png
deleted file mode 100644 (file)
index b6f8c50..0000000
Binary files a/assets/models/blend/background/base.png and /dev/null differ
diff --git a/assets/models/blend/background/bump.png b/assets/models/blend/background/bump.png
deleted file mode 100644 (file)
index 8d88791..0000000
Binary files a/assets/models/blend/background/bump.png and /dev/null differ
diff --git a/assets/models/blend/background/bump_frame.png b/assets/models/blend/background/bump_frame.png
deleted file mode 100644 (file)
index 162db11..0000000
Binary files a/assets/models/blend/background/bump_frame.png and /dev/null differ
diff --git a/assets/models/blend/background/bump_pattern.png b/assets/models/blend/background/bump_pattern.png
deleted file mode 100644 (file)
index 5dfc360..0000000
Binary files a/assets/models/blend/background/bump_pattern.png and /dev/null differ
diff --git a/assets/models/blend/background/metal.png b/assets/models/blend/background/metal.png
deleted file mode 100644 (file)
index 1258f72..0000000
Binary files a/assets/models/blend/background/metal.png and /dev/null differ
diff --git a/assets/models/blend/background/normal.png b/assets/models/blend/background/normal.png
deleted file mode 100644 (file)
index 597185d..0000000
Binary files a/assets/models/blend/background/normal.png and /dev/null differ
diff --git a/assets/models/blend/background/roughness.png b/assets/models/blend/background/roughness.png
deleted file mode 100644 (file)
index 324ea17..0000000
Binary files a/assets/models/blend/background/roughness.png and /dev/null differ
diff --git a/assets/models/blend/backgrounds/metal/ao.png b/assets/models/blend/backgrounds/metal/ao.png
new file mode 100644 (file)
index 0000000..35388dc
Binary files /dev/null and b/assets/models/blend/backgrounds/metal/ao.png differ
diff --git a/assets/models/blend/backgrounds/metal/ao_roughness_metal.png b/assets/models/blend/backgrounds/metal/ao_roughness_metal.png
new file mode 100644 (file)
index 0000000..6f852b3
Binary files /dev/null and b/assets/models/blend/backgrounds/metal/ao_roughness_metal.png differ
diff --git a/assets/models/blend/backgrounds/metal/background.blend b/assets/models/blend/backgrounds/metal/background.blend
new file mode 100644 (file)
index 0000000..375fd9a
Binary files /dev/null and b/assets/models/blend/backgrounds/metal/background.blend differ
diff --git a/assets/models/blend/backgrounds/metal/base.png b/assets/models/blend/backgrounds/metal/base.png
new file mode 100644 (file)
index 0000000..cdd6c7e
Binary files /dev/null and b/assets/models/blend/backgrounds/metal/base.png differ
diff --git a/assets/models/blend/backgrounds/metal/metal.png b/assets/models/blend/backgrounds/metal/metal.png
new file mode 100644 (file)
index 0000000..f0f0fd0
Binary files /dev/null and b/assets/models/blend/backgrounds/metal/metal.png differ
diff --git a/assets/models/blend/backgrounds/metal/normal.png b/assets/models/blend/backgrounds/metal/normal.png
new file mode 100644 (file)
index 0000000..c533778
Binary files /dev/null and b/assets/models/blend/backgrounds/metal/normal.png differ
diff --git a/assets/models/blend/backgrounds/metal/roughness.png b/assets/models/blend/backgrounds/metal/roughness.png
new file mode 100644 (file)
index 0000000..2d325e4
Binary files /dev/null and b/assets/models/blend/backgrounds/metal/roughness.png differ
diff --git a/assets/models/blend/backgrounds/wood/ao.png b/assets/models/blend/backgrounds/wood/ao.png
new file mode 100644 (file)
index 0000000..a3a90b5
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/ao.png differ
diff --git a/assets/models/blend/backgrounds/wood/ao_roughness_metal.png b/assets/models/blend/backgrounds/wood/ao_roughness_metal.png
new file mode 100644 (file)
index 0000000..d191abf
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/ao_roughness_metal.png differ
diff --git a/assets/models/blend/backgrounds/wood/background.blend b/assets/models/blend/backgrounds/wood/background.blend
new file mode 100644 (file)
index 0000000..4808dd2
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/background.blend differ
diff --git a/assets/models/blend/backgrounds/wood/base.png b/assets/models/blend/backgrounds/wood/base.png
new file mode 100644 (file)
index 0000000..b6f8c50
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/base.png differ
diff --git a/assets/models/blend/backgrounds/wood/bump.png b/assets/models/blend/backgrounds/wood/bump.png
new file mode 100644 (file)
index 0000000..8d88791
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/bump.png differ
diff --git a/assets/models/blend/backgrounds/wood/bump_frame.png b/assets/models/blend/backgrounds/wood/bump_frame.png
new file mode 100644 (file)
index 0000000..162db11
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/bump_frame.png differ
diff --git a/assets/models/blend/backgrounds/wood/bump_pattern.png b/assets/models/blend/backgrounds/wood/bump_pattern.png
new file mode 100644 (file)
index 0000000..5dfc360
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/bump_pattern.png differ
diff --git a/assets/models/blend/backgrounds/wood/metal.png b/assets/models/blend/backgrounds/wood/metal.png
new file mode 100644 (file)
index 0000000..1258f72
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/metal.png differ
diff --git a/assets/models/blend/backgrounds/wood/normal.png b/assets/models/blend/backgrounds/wood/normal.png
new file mode 100644 (file)
index 0000000..597185d
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/normal.png differ
diff --git a/assets/models/blend/backgrounds/wood/roughness.png b/assets/models/blend/backgrounds/wood/roughness.png
new file mode 100644 (file)
index 0000000..324ea17
Binary files /dev/null and b/assets/models/blend/backgrounds/wood/roughness.png differ
diff --git a/assets/models/blend/test_handle/albedo.png b/assets/models/blend/test_handle/albedo.png
new file mode 100644 (file)
index 0000000..e7e35f8
Binary files /dev/null and b/assets/models/blend/test_handle/albedo.png differ
diff --git a/assets/models/blend/test_handle/test_handle.blend b/assets/models/blend/test_handle/test_handle.blend
new file mode 100644 (file)
index 0000000..28d7a3a
Binary files /dev/null and b/assets/models/blend/test_handle/test_handle.blend differ
diff --git a/assets/scenes/basketball.json b/assets/scenes/basketball.json
new file mode 100644 (file)
index 0000000..510bb1a
--- /dev/null
@@ -0,0 +1,153 @@
+{
+  "background": "metal",
+  "items": [
+    {
+      "class": "Shelf",
+      "position": [
+        -0.56,
+        0,
+        0.21
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        1.67,
+        0,
+        0.21
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        -0.56,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        1.67,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        -4.45,
+        0,
+        -3.18
+      ],
+      "mass": 0,
+      "restitution": 1,
+      "roll": 27
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.61,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.06,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        0.91,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        1.73,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        2.57,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        30
+      ]
+    }
+  ],
+  "instructions": "Goal: you must hit every domino piece\n\nkeep \\5mouse_l\\5 pressed to drag an item\n\nkeep \\5mouse_r\\5 pressed to rotate an item",
+  "name": "Basket ball",
+  "start_items": [
+    {
+      "class": "Basketball",
+      "count": 1
+    }
+  ],
+  "test_items": {
+    "pixel_space": [
+      {
+        "id": "drag_start_0",
+        "position": [
+          55,
+          50
+        ]
+      }
+    ],
+    "world_space": [
+      {
+        "id": "drag_stop_0",
+        "position": [
+          -0.42,
+          1.03
+        ]
+      },
+      {
+        "id": "drag_stop_1",
+        "position": [
+          -4.19,
+          4.66
+        ]
+      }
+    ]
+  },
+  "version": "d6a06890225b"
+}
diff --git a/assets/scenes/box.json b/assets/scenes/box.json
new file mode 100644 (file)
index 0000000..da44c2e
--- /dev/null
@@ -0,0 +1,112 @@
+{
+  "background": "metal",
+  "instructions": "Goal: the left box must hit the right box\n\nkeep \\5mouse_l\\5 pressed to drag an item\n\nkeep \\5mouse_r\\5 pressed to rotate an item",
+  "items": [
+    {
+      "class": "Shelf",
+      "mass": 0,
+      "position": [
+        0.46,
+        0,
+        -3.95
+      ]
+    },
+    {
+      "class": "Shelf",
+      "mass": 0,
+      "position": [
+        4.43,
+        0,
+        -3.95
+      ]
+    },
+    {
+      "class": "Shelf",
+      "mass": 0,
+      "position": [
+        -1.29,
+        0,
+        0.26
+      ],
+      "roll": 28.45
+    },
+    {
+      "class": "Shelf",
+      "mass": 0,
+      "position": [
+        2.15,
+        0,
+        -1.49
+      ],
+      "roll": 28.45
+    },
+    {
+      "class": "Box",
+      "friction": 0.4,
+      "id": "left_box",
+      "mass": 1,
+      "position": [
+        -1.5499999523162842,
+        0.0,
+        1.2300000190734863
+      ],
+      "roll": 0.0
+    },
+    {
+      "class": "Box",
+      "mass": 1,
+      "position": [
+        4.380000114440918,
+        0.0,
+        -3.3499999046325684
+      ],
+      "roll": 0.0,
+      "strategy": "HitStrategy",
+      "strategy_args": [
+        "left_box"
+      ]
+    }
+  ],
+  "name": "Box",
+  "start_items": [
+    {
+      "class": "Box",
+      "count": 3
+    }
+  ],
+  "test_items": {
+    "pixel_space": [
+      {
+        "id": "drag_start_0",
+        "position": [
+          65,
+          60
+        ]
+      }
+    ],
+    "world_space": [
+      {
+        "id": "drag_stop_0",
+        "position": [
+          0.42,
+          -3.29
+        ]
+      },
+      {
+        "id": "drag_stop_1",
+        "position": [
+          0.42,
+          -2.18
+        ]
+      },
+      {
+        "id": "drag_stop_2",
+        "position": [
+          0.35,
+          -1.06
+        ]
+      }
+    ]
+  },
+  "version": "8f6333998c31"
+}
diff --git a/assets/scenes/domino.json b/assets/scenes/domino.json
new file mode 100644 (file)
index 0000000..0821407
--- /dev/null
@@ -0,0 +1,146 @@
+{
+  "background": "wood",
+  "instructions": "Goal: every domino piece must fall\n\nkeep \\5mouse_l\\5 pressed to drag an item\n\nkeep \\5mouse_r\\5 pressed to rotate an item",
+  "items": [
+    {
+      "class": "Shelf",
+      "mass": 0,
+      "position": [
+        -1.2,
+        0,
+        -0.6
+      ]
+    },
+    {
+      "class": "Shelf",
+      "mass": 0,
+      "position": [
+        1.2,
+        0,
+        -0.6
+      ]
+    },
+    {
+      "class": "Domino",
+      "mass": 1,
+      "position": [
+        -1.14,
+        0,
+        -0.04
+      ],
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        60
+      ]
+    },
+    {
+      "class": "Domino",
+      "id": "test_piece",
+      "mass": 1,
+      "position": [
+        -0.49,
+        0,
+        -0.04
+      ],
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        60
+      ]
+    },
+    {
+      "class": "Domino",
+      "mass": 1,
+      "position": [
+        0.94,
+        0,
+        -0.04
+      ],
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        60
+      ]
+    },
+    {
+      "class": "Domino",
+      "mass": 1,
+      "position": [
+        1.55,
+        0,
+        -0.04
+      ],
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        60
+      ]
+    },
+    {
+      "class": "Domino",
+      "mass": 1,
+      "position": [
+        2.09,
+        0,
+        -0.04
+      ],
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        88
+      ]
+    }
+  ],
+  "name": "Domino",
+  "start_items": [
+    {
+      "class": "Domino",
+      "count": 2
+    }
+  ],
+  "test_items": {
+    "pixel_space": [
+      {
+        "id": "drag_start_0",
+        "position": [
+          35,
+          60
+        ]
+      }
+    ],
+    "world_space": [
+      {
+        "id": "drag_stop_0",
+        "position": [
+          -1.82,
+          0.06
+        ]
+      },
+      {
+        "id": "drag_stop_1",
+        "position": [
+          0.49,
+          0.06
+        ]
+      },
+      {
+        "id": "drag_stop_2",
+        "position": [
+          -1.54,
+          0.06
+        ]
+      },
+      {
+        "id": "drag_start_1",
+        "position": [
+          -1.54,
+          0.4
+        ]
+      },
+      {
+        "id": "drag_stop_3",
+        "position": [
+          -1.05,
+          0.4
+        ]
+      }
+    ]
+  },
+  "version": "51b44031bbce"
+}
diff --git a/assets/scenes/domino_box.json b/assets/scenes/domino_box.json
new file mode 100644 (file)
index 0000000..7e4fd25
--- /dev/null
@@ -0,0 +1,226 @@
+{
+  "background": "wood",
+  "items": [
+    {
+      "class": "Shelf",
+      "position": [
+        -0.56,
+        0,
+        0.21
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        1.67,
+        0,
+        0.21
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        -0.56,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        1.67,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        3.78,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.61,
+        0,
+        -0.94
+      ],
+      "mass": 1,
+      "roll": 37,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.06,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        0.91,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        1.73,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        2.57,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "UpStrategy",
+      "strategy_args": [
+        30
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.61,
+        0,
+        0.73
+      ],
+      "mass": 1,
+      "roll": 37,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.06,
+        0,
+        0.78
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        0.91,
+        0,
+        0.78
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        1.73,
+        0,
+        0.78
+      ],
+      "mass": 1,
+      "strategy": "UpStrategy",
+      "strategy_args": [
+        30
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        2.57,
+        0,
+        0.78
+      ],
+      "mass": 1,
+      "strategy": "UpStrategy",
+      "strategy_args": [
+        30
+      ]
+    }
+  ],
+  "instructions": "Goal: only the last piece of each row must be up\n\nkeep \\5mouse_l\\5 pressed to drag an item\n\nkeep \\5mouse_r\\5 pressed to rotate an item",
+  "name": "Domino and box",
+  "start_items": [
+    {
+      "class": "Box",
+      "count": 2,
+      "mass": 5
+    }
+  ],
+  "test_items": {
+    "pixel_space": [
+      {
+        "id": "drag_start_0",
+        "position": [
+          65,
+          60
+        ]
+      }
+    ],
+    "world_space": [
+      {
+        "id": "drag_stop_0",
+        "position": [
+          3.21,
+          -0.78
+        ]
+      },
+      {
+        "id": "drag_stop_1",
+        "position": [
+          3.21,
+          0.33
+        ]
+      },
+      {
+        "id": "drag_stop_2",
+        "position": [
+          2.16,
+          1.87
+        ]
+      }
+    ]
+  },
+  "version": "621f7e5cc76e"
+}
diff --git a/assets/scenes/domino_box_basketball.json b/assets/scenes/domino_box_basketball.json
new file mode 100644 (file)
index 0000000..f2f7792
--- /dev/null
@@ -0,0 +1,217 @@
+{
+  "background": "wood",
+  "items": [
+    {
+      "class": "Shelf",
+      "position": [
+        -0.56,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        1.67,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        3.78,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        1.48,
+        0,
+        0.38
+      ],
+      "mass": 0,
+      "roll": -90
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        2.62,
+        0,
+        0.05
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        4.88,
+        0,
+        0.05
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Basketball",
+      "position": [
+        -0.3,
+        1,
+        2.5
+      ],
+      "mass": 1
+    },
+    {
+      "class": "Domino",
+      "position": [
+        1.68,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        2.35,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        3.08,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        3.78,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        4.53,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    }
+  ],
+  "instructions": "Goal: every domino piece must be hit\n\nkeep \\5mouse_l\\5 pressed to drag an item\n\nkeep \\5mouse_r\\5 pressed to rotate an item",
+  "name": "Domino, box and basket ball",
+  "start_items": [
+    {
+      "class": "Box",
+      "count": 1,
+      "mass": 5
+    },
+    {
+      "class": "Domino",
+      "count": 1
+    }
+  ],
+  "test_items": {
+    "pixel_space": [
+      {
+        "id": "drag_start_0",
+        "position": [
+          65,
+          60
+        ]
+      },
+      {
+        "id": "drag_start_1",
+        "position": [
+          30,
+          60
+        ]
+      }
+    ],
+    "world_space": [
+      {
+        "id": "drag_stop_0",
+        "position": [
+          -1.4,
+          -0.78
+        ]
+      },
+      {
+        "id": "drag_stop_1",
+        "position": [
+          -1.26,
+          0.2
+        ]
+      },
+      {
+        "id": "drag_stop_2",
+        "position": [
+          -0.28,
+          -0.78
+        ]
+      },
+      {
+        "id": "drag_start_2",
+        "position": [
+          -0.28,
+          -0.57
+        ]
+      },
+      {
+        "id": "drag_stop_3",
+        "position": [
+          -0.77,
+          -0.57
+        ]
+      },
+      {
+        "id": "drag_start_3",
+        "position": [
+          -0.28,
+          -0.85
+        ]
+      },
+      {
+        "id": "drag_stop_4",
+        "position": [
+          -0.42,
+          -0.85
+        ]
+      }
+    ]
+  },
+  "version": "ecb8be6d8330"
+}
diff --git a/assets/scenes/index.json b/assets/scenes/index.json
new file mode 100644 (file)
index 0000000..128401a
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "list": [
+    "domino",
+    "box",
+    "domino_box",
+    "basketball",
+    "domino_box_basketball",
+    "teeter_tooter",
+    "teeter_domino_box_basketball"
+  ]
+}
\ No newline at end of file
diff --git a/assets/scenes/teeter_domino_box_basketball.json b/assets/scenes/teeter_domino_box_basketball.json
new file mode 100644 (file)
index 0000000..5e5694d
--- /dev/null
@@ -0,0 +1,292 @@
+{
+  "background": "wood",
+  "items": [
+    {
+      "class": "Shelf",
+      "position": [
+        -6.24,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "TeeterTooter",
+      "position": [
+        -6.24,
+        0,
+        -1.2
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        5.37,
+        0,
+        -0.78
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        7.48,
+        0,
+        -0.78
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        4.74,
+        0,
+        -1.95
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        6.88,
+        0,
+        -1.95
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        0.53,
+        0,
+        -1.95
+      ],
+      "mass": 0,
+      "restitution": 0.95
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        2.63,
+        0,
+        -1.95
+      ],
+      "mass": 0,
+      "restitution": 0.95
+    },
+    {
+      "class": "Shelf",
+      "friction": 0,
+      "position": [
+        -3.65,
+        0,
+        1.05
+      ],
+      "mass": 0,
+      "roll": 28
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        -1.27,
+        0,
+        1.72
+      ],
+      "mass": 0,
+      "restitution": 0.95
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        0.88,
+        0,
+        1.72
+      ],
+      "mass": 0,
+      "restitution": 0.95
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        -1.67,
+        0,
+        0.55
+      ],
+      "mass": 0,
+      "restitution": 0.95
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        0.52,
+        0,
+        0.55
+      ],
+      "mass": 0,
+      "restitution": 0.95
+    },
+    {
+      "class": "Basketball",
+      "position": [
+        0.98,
+        1,
+        1.02
+      ],
+      "mass": 1
+    },
+    {
+      "class": "Shelf",
+      "friction": 1,
+      "mass": 1,
+      "position": [
+        -6.15,
+        0,
+        -0.93
+      ],
+      "roll": 24.6
+    },
+    {
+      "class": "Box",
+      "friction": 1,
+      "mass": 0.3,
+      "model_scale": 0.5,
+      "position": [
+        -5.38,
+        0,
+        -0.93
+      ],
+      "roll": 24.6
+    },
+    {
+      "class": "Domino",
+      "position": [
+        4.83,
+        0,
+        -1.39
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        5.67,
+        0,
+        -1.39
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        6.59,
+        0,
+        -1.39
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -1.73,
+        0,
+        1.11
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.97,
+        0,
+        1.11
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        -0.1,
+        0,
+        1.11
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    }
+  ],
+  "instructions": "Goal: every domino piece must be hit\n\nkeep \\5mouse_l\\5 pressed to drag an item\n\nkeep \\5mouse_r\\5 pressed to rotate an item",
+  "name": "Teeter tooter, domino, box and basket ball",
+  "start_items": [
+    {
+      "class": "Box",
+      "count": 2,
+      "friction": 1,
+      "mass": 3
+    }
+  ],
+  "test_items": {
+    "pixel_space": [
+      {
+        "id": "drag_start_0",
+        "position": [
+          60,
+          60
+        ]
+      }
+    ],
+    "world_space": [
+      {
+        "id": "drag_stop_0",
+        "position": [
+          -7.33,
+          4.24
+        ]
+      },
+      {
+        "id": "drag_stop_1",
+        "position": [
+          -7.12,
+          4.24
+        ]
+      },
+      {
+        "id": "drag_start_1",
+        "position": [
+          -6.77,
+          4.66
+        ]
+      },
+      {
+        "id": "drag_stop_2",
+        "position": [
+          -6.77,
+          4.24
+        ]
+      }
+    ]
+  },
+  "version": "3796993dea9b"
+}
diff --git a/assets/scenes/teeter_tooter.json b/assets/scenes/teeter_tooter.json
new file mode 100644 (file)
index 0000000..049543f
--- /dev/null
@@ -0,0 +1,181 @@
+{
+  "background": "metal",
+  "items": [
+    {
+      "class": "Shelf",
+      "position": [
+        -2.76,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        -0.56,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        2.27,
+        0,
+        -0.28
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        4.38,
+        0,
+        -0.28
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        1.67,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        3.78,
+        0,
+        -1.45
+      ],
+      "mass": 0
+    },
+    {
+      "class": "TeeterTooter",
+      "position": [
+        -2.74,
+        0,
+        -1.2
+      ],
+      "mass": 0
+    },
+    {
+      "class": "Shelf",
+      "position": [
+        -0.25,
+        0,
+        -0.57
+      ],
+      "mass": 0,
+      "roll": 52
+    },
+    {
+      "class": "Box",
+      "friction": 1,
+      "mass": 0.2,
+      "model_scale": 0.5,
+      "position": [
+        -3.61,
+        0,
+        -0.99
+      ],
+      "roll": -25.3
+    },
+    {
+      "class": "Domino",
+      "position": [
+        1.73,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        2.57,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    },
+    {
+      "class": "Domino",
+      "position": [
+        3.5,
+        0,
+        -0.89
+      ],
+      "mass": 1,
+      "strategy": "DownStrategy",
+      "strategy_args": [
+        35
+      ]
+    }
+  ],
+  "instructions": "Goal: you must hit every domino piece\n\nkeep \\5mouse_l\\5 pressed to drag an item\n\nkeep \\5mouse_r\\5 pressed to rotate an item",
+  "name": "Teeter tooter",
+  "start_items": [
+    {
+      "class": "Box",
+      "count": 1,
+      "friction": 1,
+      "mass": 3
+    }
+  ],
+  "test_items": {
+    "pixel_space": [
+      {
+        "id": "drag_start_0",
+        "position": [
+          60,
+          60
+        ]
+      }
+    ],
+    "world_space": [
+      {
+        "id": "drag_stop_0",
+        "position": [
+          -2.65,
+          1.18
+        ]
+      },
+      {
+        "id": "drag_stop_1",
+        "position": [
+          -2.65,
+          3.27
+        ]
+      },
+      {
+        "id": "drag_start_1",
+        "position": [
+          -2.3,
+          3.75
+        ]
+      },
+      {
+        "id": "drag_stop_2",
+        "position": [
+          -2.5,
+          3.66
+        ]
+      }
+    ]
+  },
+  "version": "5efa110617cc"
+}
diff --git a/audio/__init__.py b/audio/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/audio/music.py b/audio/music.py
deleted file mode 100644 (file)
index cf6c0c9..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-from os.path import dirname, exists, basename
-from platform import system
-from glob import glob
-from pathlib import Path
-from random import choice
-from logging import info
-from panda3d.core import AudioSound, Filename
-
-
-class MusicMgr:
-
-    def __init__(self, volume):
-        files = self.curr_path + 'assets/audio/music/*.ogg'
-        self._start_music(glob(files))
-        base.musicManager.setVolume(.8 * volume)
-        base.sfxManagerList[0].setVolume(volume)
-        taskMgr.add(self._on_frame, 'on frame music')
-
-    @property
-    def is_appimage(self):
-        par_path = str(Path(__file__).parent.absolute())
-        is_appimage = par_path.startswith('/tmp/.mount_Pmachi')
-        return is_appimage and par_path.endswith('/usr/bin')
-
-    @property
-    def curr_path(self):
-        if system() == 'Windows':
-            return ''
-        if exists('main.py'):
-            return ''
-        else:
-            par_path = str(Path(__file__).parent.absolute())
-        if self.is_appimage:
-            par_path = str(Path(par_path).absolute())
-        par_path += '/'
-        return par_path
-
-    def _start_music(self, files):
-        self._music = loader.load_music(choice(files))
-        info('playing music ' + self._music.get_name())
-        self._music.play()
-
-    def set_volume(self, volume):
-        base.musicManager.setVolume(.8 * volume)
-        base.sfxManagerList[0].setVolume(volume)
-
-    def _on_frame(self, task):
-        if self._music.status() == AudioSound.READY:
-            oggs = Filename(self.curr_path + 'assets/audio/music/*.ogg').to_os_specific()
-            files = glob(oggs)
-            rm_music = Filename(self.curr_path + 'assets/audio/music/' + basename(self._music.get_name())).to_os_specific()
-            # basename is needed in windows
-            files.remove(rm_music)
-            self._start_music(files)
-        return task.cont
diff --git a/gui/__init__.py b/gui/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/gui/menu.py b/gui/menu.py
deleted file mode 100644 (file)
index 335fade..0000000
+++ /dev/null
@@ -1,340 +0,0 @@
-from logging import info, debug
-from sys import platform, exit
-from os import environ, system
-from glob import glob
-from importlib import import_module
-from inspect import isclass
-from webbrowser import open_new_tab
-from xmlrpc.client import ServerProxy
-from panda3d.core import Texture, TextNode, WindowProperties, LVector2i, \
-    TextProperties, TextPropertiesManager, NodePath
-from direct.gui.DirectGui import DirectButton, DirectCheckButton, \
-    DirectOptionMenu, DirectSlider, DirectCheckButton
-from direct.gui.DirectGuiGlobals import FLAT
-from direct.gui.OnscreenText import OnscreenText
-from direct.showbase.DirectObject import DirectObject
-from ya2.utils.cursor import MouseCursor
-from ya2.p3d.p3d import LibP3d
-from logics.scene import Scene
-from panda3d.bullet import BulletWorld
-
-
-class Menu(DirectObject):
-
-    def __init__(self, fsm, lang_mgr, opt_file, music, pipeline, scenes, fun_test, pos_mgr):
-        super().__init__()
-        self._fsm = fsm
-        self._lang_mgr = lang_mgr
-        self._opt_file = opt_file
-        self._music = music
-        self._pipeline = pipeline
-        self._scenes = scenes
-        self._fun_test = fun_test
-        self._pos_mgr = pos_mgr
-        self._enforced_res = ''
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01))
-        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)
-        self._widgets = []
-        self._common = {
-            'scale': .12,
-            'text_font': self._font,
-            'text_fg': (.9, .9, .9, 1),
-            'relief': FLAT,
-            'frameColor': (.4, .4, .4, .14),
-            'rolloverSound': loader.load_sfx('assets/audio/sfx/rollover.ogg'),
-            'clickSound': loader.load_sfx('assets/audio/sfx/click.ogg')}
-        self._common_btn = {'frameSize': (-4.8, 4.8, -.6, 1.2)} | self._common
-        hlc = self._common_btn['frameColor']
-        hlc = (hlc[0] + .2, hlc[1] + .2, hlc[2] + .2, hlc[3] + .2)
-        self._common_opt = {
-            'item_frameColor': self._common_btn['frameColor'],
-            'popupMarker_frameColor': self._common_btn['frameColor'],
-            'item_relief': self._common_btn['relief'],
-            'item_text_font': self._common_btn['text_font'],
-            'item_text_fg': self._common_btn['text_fg'],
-            'textMayChange': 1,
-            'highlightColor': hlc,
-            'text_align': TextNode.A_center,
-        } | self._common_btn
-        f_s = self._common_opt['frameSize']
-        self._common_opt['frameSize'] = f_s[0], f_s[1] - .56, f_s[2], f_s[3]
-        self._common_slider = self._common | {
-            'range': (0, 1),
-            'thumb_frameColor': (.4, .4, .4, .4),
-            'thumb_scale': 1.6,
-            'scale': .4}
-        del self._common_slider['rolloverSound']
-        del self._common_slider['clickSound']
-        self._set_main()
-
-    def enforce_res(self, val):
-        self._enforced_res = val
-        info('enforced resolution: ' + val)
-
-    def _set_main(self):
-        self._pos_mgr.reset()
-        self._widgets = []
-        self._widgets += [DirectButton(
-            text=_('Play'), pos=(0, 1, .6), command=self.on_play,
-            **self._common_btn)]
-        self._pos_mgr.register('play', LibP3d.wdg_pos(self._widgets[-1]))
-        self._widgets += [DirectButton(
-            text=_('Options'), pos=(0, 1, .2), command=self.on_options,
-            **self._common_btn)]
-        self._pos_mgr.register('options', LibP3d.wdg_pos(self._widgets[-1]))
-        self._widgets += [DirectButton(
-            text=_('Credits'), pos=(0, 1, -.2), command=self.on_credits,
-            **self._common_btn)]
-        self._pos_mgr.register('credits', LibP3d.wdg_pos(self._widgets[-1]))
-        def btn_exit():
-            if self._fun_test:
-                ServerProxy('http://localhost:6000').destroy()
-            exit()
-        self._widgets += [DirectButton(
-            text=_('Exit'), pos=(0, 1, -.6), command=lambda: btn_exit(),
-            **self._common_btn)]
-        self._pos_mgr.register('exit', LibP3d.wdg_pos(self._widgets[-1]))
-        self._rearrange_width()
-        self.accept('enforce_resolution', self.enforce_res)
-
-    def _set_options(self):
-        self._pos_mgr.reset()
-        self._widgets = []
-        self._lang_funcs = [lambda: _('English'), lambda: _('Italian')]
-        items = [fnc() for fnc in self._lang_funcs]
-        inititem = {
-            'en': _('English'),
-            'it': _('Italian')
-        }[self._opt_file['settings']['language']]
-        btn = DirectOptionMenu(
-            text=_('Language'), items=items, initialitem=inititem,
-            pos=(0, 1, .8), command=self.on_language, **self._common_opt)
-        btn.popupMenu['frameColor'] = self._common_btn['frameColor']
-        btn.popupMenu['relief'] = self._common_btn['relief']
-        self._widgets += [btn]
-        pos_lang = LibP3d.wdg_pos(self._widgets[-1])
-        self._pos_mgr.register('languages', pos_lang)
-        pos_eng = pos_lang[0] + 300, pos_lang[1] - 57
-        pos_it = pos_lang[0] + 300, pos_lang[1] + 33
-        self._pos_mgr.register('english', pos_eng)
-        self._pos_mgr.register('italian', pos_it)
-        self._widgets += [OnscreenText(
-            _('Volume'), pos=(-.1, .55), font=self._common['text_font'],
-            scale=self._common['scale'], fg=self._common['text_fg'],
-            align=TextNode.A_right)]
-        self._widgets += [DirectSlider(
-            pos=(.5, 1, .57),
-            value=self._opt_file['settings']['volume'],
-            command=self.on_volume,
-            **self._common_slider)]
-        vol_pos = LibP3d.wdg_pos(self._widgets[-1])
-        self._pos_mgr.register('volume', vol_pos)
-        self._pos_mgr.register('volume_0', [vol_pos[0] - 153, vol_pos[1]])
-        self._slider = self._widgets[-1]
-        self._widgets += [DirectCheckButton(
-            text=_('Fullscreen'), pos=(0, 1, .3), command=self.on_fullscreen,
-            indicator_frameColor=self._common_opt['highlightColor'],
-            indicator_relief=self._common_btn['relief'],
-            indicatorValue=self._opt_file['settings']['fullscreen'],
-            **self._common_btn)]
-        self._pos_mgr.register('fullscreen', LibP3d.wdg_pos(self._widgets[-1]))
-        res = self._opt_file['settings']['resolution']
-        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())]
-        resolutions = list(set(resolutions))
-        resolutions = sorted(resolutions)
-        resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
-        resolutions = ['x'.join(_res) for _res in resolutions]
-        if not res:
-            res = resolutions[-1]
-        btn = DirectOptionMenu(
-            text=_('Resolution'), items=resolutions, initialitem=res,
-            pos=(0, 1, .05), command=self.on_resolution, **self._common_opt)
-        btn.popupMenu['frameColor'] = self._common_btn['frameColor']
-        btn.popupMenu['relief'] = self._common_btn['relief']
-        self._widgets += [btn]
-        pos_res = LibP3d.wdg_pos(self._widgets[-1])
-        self._pos_mgr.register('resolutions', pos_res)  # 680 365
-        self._pos_mgr.register('res_1440x900', [pos_res[0] + 320, pos_res[1] + 75])
-        self._pos_mgr.register('res_1360x768', [pos_res[0] + 430, pos_res[1] -285])
-        self._widgets += [DirectCheckButton(
-            text=_('Antialiasing'), pos=(0, 1, -.2), command=self.on_aa,
-            indicator_frameColor=self._common_opt['highlightColor'],
-            indicator_relief=self._common_btn['relief'],
-            indicatorValue=self._opt_file['settings']['antialiasing'],
-            **self._common_btn)]
-        self._pos_mgr.register('aa', LibP3d.wdg_pos(self._widgets[-1]))
-        self._widgets += [DirectCheckButton(
-            text=_('Shadows'), pos=(0, 1, -.45), command=self.on_shadows,
-            indicator_frameColor=self._common_opt['highlightColor'],
-            indicator_relief=self._common_btn['relief'],
-            indicatorValue=self._opt_file['settings']['shadows'],
-            **self._common_btn)]
-        self._pos_mgr.register('shadows', LibP3d.wdg_pos(self._widgets[-1]))
-        self._widgets += [DirectButton(
-            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
-            **self._common_btn)]
-        self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
-        self.accept('enforce_resolution', self.enforce_res)
-
-    def _set_credits(self):
-        self._pos_mgr.reset()
-        self._widgets = []
-        tp_scale = TextProperties()
-        tp_scale.set_text_scale(.64)
-        TextPropertiesManager.getGlobalPtr().setProperties('scale', tp_scale)
-        self._widgets += [OnscreenText(
-            _('Code and gfx\n  \1scale\1Flavio Calva\2\n\n\nMusic\n  \1scale\1Stefan Grossmann\2'),
-            pos=(-.9, .55), font=self._common['text_font'],
-            scale=self._common['scale'], fg=self._common['text_fg'],
-            align=TextNode.A_left)]
-        self._widgets += [DirectButton(
-            text=_('Website'), pos=(-.6, 1, .29), command=self.on_website,
-            **self._common_btn | {'scale': .08})]
-        self._widgets += [OnscreenText(
-            _('Special thanks to:\n  \1scale\1rdb\2\n  \1scale\1Luisa Tenuta\2\n  \1scale\1Damiana Ercolani\2'),
-            pos=(.1, .55), font=self._common['text_font'],
-            scale=self._common['scale'], fg=self._common['text_fg'],
-            align=TextNode.A_left)]
-        self._widgets += [DirectButton(
-            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
-            **self._common_btn)]
-        self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
-        self.accept('enforce_resolution', self.enforce_res)
-
-    def on_play(self):
-        self._pos_mgr.reset()
-        self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01))
-        self._widgets = []
-        cmn = self._common_btn.copy() | {
-            'frameSize': (-2.4, 2.4, -2.4, 2.4),
-            'frameColor': (1, 1, 1, .8),
-            'text_scale': .64}
-        left = - (dx := .8) * (min(4, len(self._scenes)) - 1) / 2
-        for i, cls in enumerate(self._scenes):
-            top = .1 if len(self._scenes) < 5 else .6
-            row = 0 if i < 4 else 1
-            self._widgets += [DirectButton(
-                text=cls.name(), pos=(left + dx * (i % 4), 1, top - dx * row),
-                command=self.start, extraArgs=[cls], text_wordwrap=6,
-                frameTexture='assets/images/scenes/%s.dds' % cls.__name__,
-                **cmn)]
-            name = cls.__name__[5:].lower()
-            self._pos_mgr.register(name, LibP3d.wdg_pos(self._widgets[-1]))
-            for j in range(4):
-                tnode = self._widgets[-1].component('text%s' % j).textNode
-                height = - tnode.getLineHeight() / 2
-                height += (tnode.get_height() - tnode.get_line_height()) / 2
-                self._widgets[-1].component('text%s' % j).set_pos(0, 0, height)
-        self._widgets += [DirectButton(
-            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
-            **self._common_btn)]
-        self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
-
-    def start(self, cls):
-        self._fsm.demand('Scene', cls)
-
-    def on_options(self):
-        self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01))
-        self._set_options()
-
-    def on_credits(self):
-        self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01))
-        self._set_credits()
-
-    def _rearrange_width(self):
-        max_width = 0
-        for wdg in self._widgets:
-            t_n = wdg.component('text0')
-            u_l = t_n.textNode.get_upper_left_3d()
-            l_r = t_n.textNode.get_lower_right_3d()
-            max_width = max(l_r[0] - u_l[0], max_width)
-        for wdg in self._widgets:
-            m_w = max_width / 2 + .8
-            wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3]
-
-    def on_language(self, arg):
-        lang_code = {
-            _('English'): 'en_EN',
-            _('Italian'): 'it_IT'}[arg]
-        self._lang_mgr.set_lang(lang_code)
-        self._opt_file['settings']['language'] = lang_code[:2]
-        self._opt_file.store()
-        self.on_options()
-
-    def on_volume(self):
-        self._opt_file['settings']['volume'] = self._slider['value']
-        self._music.set_volume(self._slider['value'])
-
-    def on_fullscreen(self, arg):
-        props = WindowProperties()
-        props.set_fullscreen(arg)
-        if not self._fun_test:
-            base.win.request_properties(props)
-            # if we actually switch to fullscreen during the tests then
-            # exwm inside qemu can't restore the correct resolution
-            # i may re-enable this if/when i run the tests onto a
-            # physical machine
-        self._opt_file['settings']['fullscreen'] = int(arg)
-        self._opt_file.store()
-
-    def on_resolution(self, arg):
-        info('on resolution: %s (%s)' % (arg, self._enforced_res))
-        arg = self._enforced_res or arg
-        info('set resolution: %s' % arg)
-        props = WindowProperties()
-        props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
-        base.win.request_properties(props)
-        self._opt_file['settings']['resolution'] = arg
-        self._opt_file.store()
-
-    def on_aa(self, arg):
-        self._pipeline.msaa_samples = 4 if arg else 1
-        debug(f'msaa: {self._pipeline.msaa_samples}')
-        self._opt_file['settings']['antialiasing'] = int(arg)
-        self._opt_file.store()
-
-    def on_shadows(self, arg):
-        self._pipeline.enable_shadows = int(arg)
-        debug(f'shadows: {self._pipeline.enable_shadows}')
-        self._opt_file['settings']['shadows'] = int(arg)
-        self._opt_file.store()
-
-    def on_website(self):
-        if platform.startswith('linux'):
-            environ['LD_LIBRARY_PATH'] = ''
-            system('xdg-open https://www.ya2.it')
-        else:
-            open_new_tab('https://www.ya2.it')
-
-    def on_back(self):
-        self._opt_file.store()
-        self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01))
-        self._set_main()
-
-    def destroy(self):
-        [wdg.destroy() for wdg in self._widgets]
-        self._cursor.destroy()
-        self.ignore('enforce_resolution')
diff --git a/gui/sidepanel.py b/gui/sidepanel.py
deleted file mode 100644 (file)
index 39afaa4..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-from textwrap import dedent
-from panda3d.core import GeomVertexData, GeomVertexFormat, Geom, \
-    GeomVertexWriter, GeomTriangles, GeomNode, Shader, Point3, Plane, Vec3
-from ya2.p3d.gfx import P3dGfxMgr
-
-
-class SidePanel:
-
-    def __init__(self, world, plane_node, top_l, bottom_r, y, items):
-        self._world = world
-        self._plane_node = plane_node
-        self._set((-1, 1), y)
-        self.update(items)
-
-    def update(self, items):
-        p_from, p_to = P3dGfxMgr.world_from_to((-1, 1))
-        for hit in self._world.ray_test_all(p_from, p_to).get_hits():
-            if hit.get_node() == self._plane_node:
-                pos = hit.get_hit_pos()
-        y = 0
-        corner = -20, 20
-        for item in items:
-            if not item._instantiated:
-                bounds = item._np.get_tight_bounds()
-                if bounds[1][1] > y:
-                    y = bounds[1][1]
-                icorner = item.get_corner()
-                icorner = P3dGfxMgr.screen_coord(icorner)
-                if icorner[0] > corner[0]:
-                    corner = icorner[0], corner[1]
-                if icorner[1] < corner[1]:
-                    corner = corner[0], icorner[1]
-        self._set((pos[0], pos[2]), y)
-        bounds = self._np.get_tight_bounds()
-        corner3d = bounds[1][0], bounds[1][1], bounds[0][2]
-        corner2d = P3dGfxMgr.screen_coord(corner3d)
-        plane = Plane(Vec3(0, 1, 0), y)
-        pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
-        ar, margin = base.get_aspect_ratio(), .04
-        x = corner[0] / ar if ar >= 1 else corner[0]
-        z = corner[1] * ar if ar < 1 else corner[1]
-        x += margin / ar if ar >= 1 else margin
-        z -= margin * ar if ar < 1 else margin
-        base.camLens.extrude((x, z), near_pt, far_pt)
-        plane.intersects_line(
-            pos3d, render.get_relative_point(base.camera, near_pt),
-            render.get_relative_point(base.camera, far_pt))
-        corner_pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
-        base.camLens.extrude((-1, 1), near_pt, far_pt)
-        plane.intersects_line(
-            corner_pos3d, render.get_relative_point(base.camera, near_pt),
-            render.get_relative_point(base.camera, far_pt))
-        self._np.set_pos((pos3d + corner_pos3d) / 2)
-        scale = Vec3(pos3d[0] - corner_pos3d[0], 1, corner_pos3d[2] - pos3d[2])
-        self._np.set_scale(scale)
-
-    def _set(self, pos, y):
-        if hasattr(self, '_np'):
-            self._np.remove_node()
-        vdata = GeomVertexData('quad', GeomVertexFormat.get_v3(), Geom.UHStatic)
-        vdata.setNumRows(2)
-        vertex = GeomVertexWriter(vdata, 'vertex')
-        vertex.add_data3(.5, 0, -.5)
-        vertex.add_data3(.5, 0, .5)
-        vertex.add_data3(-.5, 0, .5)
-        vertex.add_data3(-.5, 0, -.5)
-        prim = GeomTriangles(Geom.UHStatic)
-        prim.add_vertices(0, 1, 2)
-        prim.add_vertices(0, 2, 3)
-        prim.close_primitive()
-        geom = Geom(vdata)
-        geom.add_primitive(prim)
-        node = GeomNode('gnode')
-        node.add_geom(geom)
-        self._np = render.attach_new_node(node)
-        self._np.setTransparency(True)
-        self._np.set_pos(pos[0], y, pos[1])
-        vert = '''\
-            #version 130
-            uniform mat4 p3d_ModelViewProjectionMatrix;
-            in vec4 p3d_Vertex;
-            void main() {
-                gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }'''
-        frag = '''\
-            #version 130
-            out vec4 p3d_FragColor;
-            void main() {
-                p3d_FragColor = vec4(.04, .04, .04, .08); }'''
-        self._np.set_shader(Shader.make(Shader.SL_GLSL, dedent(vert), dedent(frag)))
-        # mat = Material()
-        # mat.set_base_color((1, 1, 1, 1))
-        # mat.set_emission((1, 1, 1, 1))
-        # mat.set_metallic(.5)
-        # mat.set_roughness(.5)
-        # np.set_material(mat)
-        # texture_sz = 64
-        # base_color_pnm = PNMImage(texture_sz, texture_sz)
-        # base_color_pnm.fill(.1, .1, .1)
-        # base_color_pnm.add_alpha()
-        # base_color_pnm.alpha_fill(.04)
-        # base_color_tex = Texture('base color')
-        # base_color_tex.load(base_color_pnm)
-        # ts = TextureStage('base color')
-        # ts.set_mode(TextureStage.M_modulate)
-        # np.set_texture(ts, base_color_tex)
-        # emission_pnm = PNMImage(texture_sz, texture_sz)
-        # emission_pnm.fill(0, 0, 0)
-        # emission_tex = Texture('emission')
-        # emission_tex.load(emission_pnm)
-        # ts = TextureStage('emission')
-        # ts.set_mode(TextureStage.M_emission)
-        # np.set_texture(ts, emission_tex)
-        # metal_rough_pnm = PNMImage(texture_sz, texture_sz)
-        # ambient_occlusion = 1
-        # roughness = .5
-        # metallicity = .5
-        # metal_rough_pnm.fill(ambient_occlusion, roughness, metallicity)
-        # metal_rough_tex = Texture('ao metal roughness')
-        # metal_rough_tex.load(metal_rough_pnm)
-        # ts = TextureStage('ao metal roughness')
-        # ts.set_mode(TextureStage.M_selector)
-        # np.set_texture(ts, metal_rough_tex)
-        # normal_pnm = PNMImage(texture_sz, texture_sz)
-        # normal_pnm.fill(.5, .5, .1)
-        # normal_tex = Texture('normals')
-        # normal_tex.load(normal_pnm)
-        # ts = TextureStage('normals')
-        # ts.set_mode(TextureStage.M_normal)
-        # np.set_texture(ts, normal_tex)
-
-    def destroy(self):
-        self._np.remove_node()
diff --git a/licenses/0bsd.txt b/licenses/0bsd.txt
new file mode 100644 (file)
index 0000000..2f01942
--- /dev/null
@@ -0,0 +1,10 @@
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/licenses/bsd.txt b/licenses/bsd.txt
deleted file mode 100644 (file)
index 1dc32a6..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-Copyright (c) 2020, Ya2
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
-* Neither the name of the Ya2 nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL YA2 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/licenses/gpl.txt b/licenses/gpl.txt
new file mode 100644 (file)
index 0000000..f288702
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/logics/__init__.py b/logics/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/logics/app.py b/logics/app.py
deleted file mode 100755 (executable)
index 0ee87cc..0000000
+++ /dev/null
@@ -1,281 +0,0 @@
-import argparse
-import simplepbr
-#import gltf
-from glob import glob
-from importlib import import_module
-from inspect import isclass
-from sys import platform, argv, exit
-from logging import info, debug
-from os.path import exists
-from os import makedirs
-from panda3d.core import Filename, load_prc_file_data, AntialiasAttrib, \
-    Texture, WindowProperties, LVector2i, TextNode
-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 audio.music import MusicMgr
-from logics.items.background import Background
-from gui.menu import Menu
-from logics.scene import Scene
-from logics.scenes.scene_basketball import SceneBasketBall
-from logics.scenes.scene_box import SceneBox
-from logics.scenes.scene_domino_box_basketball import SceneDominoBoxBasketball
-from logics.scenes.scene_domino_box import SceneDominoBox
-from logics.scenes.scene_domino import SceneDomino
-from logics.scenes.scene_teeter_domino_box_basketball import SceneTeeterDominoBoxBasketball
-from logics.scenes.scene_teeter_tooter import SceneTeeterTooter
-from logics.posmgr import PositionMgr
-from ya2.utils.dictfile import DctFile
-from ya2.p3d.p3d import LibP3d
-from ya2.utils.lang import LangMgr
-from ya2.utils.log import LogMgr
-from ya2.utils.functional import FunctionalTest
-
-
-class MainFsm(FSM):
-
-    def __init__(self, pmachines):
-        super().__init__('Main FSM')
-        self._pmachines = pmachines
-
-    def enterMenu(self):
-        self._pmachines.on_menu_enter()
-
-    def exitMenu(self):
-        self._pmachines.on_menu_exit()
-
-    def enterScene(self, cls):
-        self._pmachines.on_scene_enter(cls)
-
-    def exitScene(self):
-        self._pmachines.on_scene_exit()
-
-
-class PmachinesApp:
-
-    scenes = [
-        SceneDomino,
-        SceneBox,
-        SceneDominoBox,
-        SceneBasketBall,
-        SceneDominoBoxBasketball,
-        SceneTeeterTooter,
-        SceneTeeterDominoBoxBasketball]
-
-    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.updating = args.update
-        self.version = args.version
-        self.log_mgr = LogMgr.init_cls()(self)
-        self._pos_mgr = PositionMgr()
-        self._prepare_window(args)
-        if args.update:
-            return
-        if args.functional_test:
-            self._options['settings']['volume'] = 0
-        self._music = MusicMgr(self._options['settings']['volume'])
-        self.lang_mgr = LangMgr(self._options['settings']['language'],
-                                'pmachines',
-                                'assets/locale/')
-        self._fsm = MainFsm(self)
-        if args.screenshot:
-            cls = [cls for cls in self.scenes if cls.__name__ == args.screenshot][0]
-            scene = cls(BulletWorld(), None, True, False, lambda: None, self.scenes)
-            scene.screenshot()
-            scene.destroy()
-            exit()
-        elif self._options['development']['auto_start']:
-            mod_name = 'logics.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', cls)
-        else:
-            self._fsm.demand('Menu')
-        if args.functional_test or args.functional_ref:
-            FunctionalTest(args.functional_ref, self._pos_mgr)
-
-    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):
-        self._fsm.demand('Menu')
-
-    def on_menu_exit(self):
-        self._menu_bg.destroy()
-        self._menu.destroy()
-
-    def on_scene_enter(self, cls):
-        self._set_physics()
-        self._scene = cls(
-            self.world, self.on_home,
-            self._options['development']['auto_close_instructions'],
-            self._options['development']['debug_items'],
-            self.reload,
-            self.scenes,
-            self._pos_mgr)
-
-    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' % LibP3d.fixpath(data_path + '/' + optfile))
-        default_opt = {
-            'settings': {
-                'volume': 1,
-                'language': 'en',
-                'fullscreen': 1,
-                'resolution': '',
-                'antialiasing': 1,
-                'shadows': 1},
-            '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}}
-        opt_path = LibP3d.fixpath(data_path + '/' + optfile) if data_path else optfile
-        opt_exists = exists(opt_path)
-        self._options = DctFile(
-            LibP3d.fixpath(data_path + '/' + optfile) if data_path else optfile,
-            default_opt)
-        if not opt_exists:
-            self._options.store()
-        res = self._options['settings']['resolution']
-        if res:
-            res = LVector2i(*[int(_res) for _res in res.split('x')])
-        else:
-            resolutions = []
-            if not self.version:
-                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
-        else:
-            props.set_size(res)
-        props.set_fullscreen(fullscreen)
-        props.set_icon_filename('assets/images/icon/pmachines.ico')
-        if not args.screenshot and not self.version:
-            base.win.request_properties(props)
-        #gltf.patch_loader(base.loader)
-        if self._options['development']['simplepbr'] and not self.version:
-            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()
diff --git a/logics/items/__init__.py b/logics/items/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/logics/items/background.py b/logics/items/background.py
deleted file mode 100644 (file)
index 5203202..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-from itertools import product
-from panda3d.core import NodePath
-from ya2.p3d.gfx import set_srgb
-
-
-class Background:
-
-    def __init__(self):
-        self._root = NodePath('background_root')
-        self._root.reparent_to(render)
-        ncols, nrows = 16, 8
-        start_size, end_size = 5, 2.5
-        offset = 5
-        for col, row in product(range(ncols), range(nrows)):
-            model = loader.load_model('assets/models/bam/background/background.bam')
-            model.set_scale(end_size / start_size)
-            model.reparent_to(self._root)
-            total_width, total_height = end_size * ncols, end_size * nrows
-            left, bottom = -total_width/2, -total_height/2
-            model.set_pos(left + end_size * col, offset, bottom + end_size * row)
-        self._root.clear_model_nodes()
-        self._root.flatten_strong()
-        set_srgb(self._root)
-
-    def destroy(self):
-        self._root.remove_node()
diff --git a/logics/items/basketball.py b/logics/items/basketball.py
deleted file mode 100644 (file)
index 7ab294f..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-from panda3d.bullet import BulletSphereShape, BulletRigidBodyNode, BulletGhostNode
-from logics.items.item import Item
-
-
-class Basketball(Item):
-
-    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.92, friction=.6):
-        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/basketball/basketball.bam', .4, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
-
-    def _set_shape(self, apply_scale=True):
-        self.node.add_shape(BulletSphereShape(1))
diff --git a/logics/items/box.py b/logics/items/box.py
deleted file mode 100644 (file)
index d3ee01d..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-from panda3d.bullet import BulletBoxShape, BulletRigidBodyNode, BulletGhostNode
-from logics.items.item import Item
-
-
-class Box(Item):
-
-    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.8, model_scale=1):
-        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/box/box.bam', mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction, model_scale=model_scale)
-
-    def _set_shape(self, apply_scale=True):
-        self.node.add_shape(BulletBoxShape((.5, .5, .5)))
-
-
-class HitStrategy:
-
-    def __init__(self, hit_by, node, world):
-        self._hit_by = hit_by
-        self._node = node
-        self._world = world
-
-    def win_condition(self):
-        for contact in self._world.contact_test(self._node).get_contacts():
-            other = contact.get_node1() if contact.get_node0() == self._node else contact.get_node0()
-            if other == self._hit_by.node:
-                return True
diff --git a/logics/items/domino.py b/logics/items/domino.py
deleted file mode 100644 (file)
index 8071e69..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-from panda3d.bullet import BulletBoxShape, BulletRigidBodyNode, BulletGhostNode
-from logics.items.item import Item, StillStrategy
-
-
-class Domino(Item):
-
-    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.6):
-        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/domino/domino.bam', mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
-
-    def _set_shape(self, apply_scale=True):
-        self.node.add_shape(BulletBoxShape((.1, .25, .5)))
-
-
-class UpStrategy(StillStrategy):
-
-    def __init__(self, np, tgt_degrees):
-        super().__init__(np)
-        self._tgt_degrees = tgt_degrees
-        self._np = np
-
-    def win_condition(self):
-        return super().win_condition() and abs(self._np.get_r()) <= self._tgt_degrees
-
-
-class DownStrategy(StillStrategy):
-
-    def __init__(self, np, tgt_degrees):
-        super().__init__(np)
-        self._tgt_degrees = tgt_degrees
-        self._np = np
-
-    def win_condition(self):
-        return self._np.get_z() < -6 or super().win_condition() and abs(self._np.get_r()) >= self._tgt_degrees
diff --git a/logics/items/item.py b/logics/items/item.py
deleted file mode 100644 (file)
index 9f2bf5d..0000000
+++ /dev/null
@@ -1,339 +0,0 @@
-from panda3d.core import CullFaceAttrib, Point3, NodePath, Point2, Texture, \
-    Plane, Vec3, BitMask32
-from panda3d.bullet import BulletBoxShape, BulletRigidBodyNode, BulletGhostNode
-from direct.gui.OnscreenText import OnscreenText
-from ya2.p3d.gfx import P3dGfxMgr, set_srgb
-
-
-class Command:
-
-    def __init__(self, pos, rot):
-        self.pos = pos
-        self.rot = rot
-
-
-class FixedStrategy:
-
-    def win_condition(self):
-        return True
-
-
-class StillStrategy:
-
-    def __init__(self, np):
-        self._np = np
-        self._positions = []
-        self._rotations = []
-
-    def win_condition(self):
-        self._positions += [self._np.get_pos()]
-        self._rotations += [self._np.get_hpr()]
-        if len(self._positions) > 30:
-            self._positions.pop(0)
-        if len(self._rotations) > 30:
-            self._rotations.pop(0)
-        if len(self._positions) < 28:
-            return
-        avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
-        avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
-        avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
-        avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
-        avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
-        avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
-        avg_pos = Point3(avg_x, avg_y, avg_z)
-        avg_rot = Point3(avg_h, avg_p, avg_r)
-        return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
-            all((rot - avg_rot).length() < 1 for rot in self._rotations)
-
-
-class Item:
-
-    def __init__(self, world, plane_node, cb_inst, curr_bottom, scene_repos, model_path, model_scale=1, exp_num_contacts=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.5):
-        self._world = world
-        self._plane_node = plane_node
-        self._count = count
-        self._cb_inst = cb_inst
-        self._paused = False
-        self._overlapping = False
-        self._first_command = True
-        self.repos_done = False
-        self._mass = mass
-        self._pos = pos
-        self._r = r
-        self.strategy = FixedStrategy()
-        self._exp_num_contacts = exp_num_contacts
-        self._curr_bottom = curr_bottom
-        self._scene_repos = scene_repos
-        self._model_scale = model_scale
-        self._model_path = model_path
-        self._commands = []
-        self._command_idx = -1
-        self._restitution = restitution
-        self._friction = friction
-        self._positions = []
-        self._rotations = []
-        if count:
-            self.node = BulletGhostNode(self.__class__.__name__)
-        else:
-            self.node = BulletRigidBodyNode(self.__class__.__name__)
-        self._set_shape(count)
-        self._np = render.attach_new_node(self.node)
-        if count:
-            world.attach_ghost(self.node)
-        else:
-            world.attach_rigid_body(self.node)
-        self._model = loader.load_model(model_path)
-        set_srgb(self._model)
-        self._model.flatten_light()
-        self._model.reparent_to(self._np)
-        self._np.set_scale(model_scale)
-        self._np.flatten_strong()
-        if count:
-            self._set_outline_model()
-            set_srgb(self._outline_model)
-            self._model.hide(BitMask32(0x01))
-            self._outline_model.hide(BitMask32(0x01))
-        self._start_drag_pos = None
-        self._prev_rot_info = None
-        self._last_nonoverlapping_pos = None
-        self._last_nonoverlapping_rot = None
-        self._instantiated = not count
-        self.interactable = count
-        self._box_tsk = taskMgr.add(self.on_frame, 'on_frame')
-        if count:
-            self._repos()
-        else:
-            self._np.set_pos(pos)
-            self._np.set_r(r)
-
-    def _set_shape(self, apply_scale=True):
-        pass
-
-    def set_strategy(self, strategy):
-        self.strategy = strategy
-
-    def _repos(self):
-        p_from, p_to = P3dGfxMgr.world_from_to((-1, 1))
-        for hit in self._world.ray_test_all(p_from, p_to).get_hits():
-            if hit.get_node() == self._plane_node:
-                pos = hit.get_hit_pos()
-        corner = P3dGfxMgr.screen_coord(pos)
-        bounds = self._np.get_tight_bounds()
-        bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
-        self._np.set_pos(pos)
-        plane = Plane(Vec3(0, 1, 0), bounds[0][1])
-        pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
-        margin, ar = .04, base.get_aspect_ratio()
-        margin_x = margin / ar if ar >= 1 else margin
-        margin_z = margin * ar if ar < 1 else margin
-        top = self._curr_bottom()
-        base.camLens.extrude((-1 + margin_x, top - margin_z), near_pt, far_pt)
-        plane.intersects_line(
-            pos3d, render.get_relative_point(base.camera, near_pt),
-            render.get_relative_point(base.camera, far_pt))
-        delta = Vec3(bounds[1][0], bounds[1][1], bounds[0][2])
-        self._np.set_pos(pos3d + delta)
-        if not hasattr(self, '_txt'):
-            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(
-                str(self._count), font=font, scale=0.06, fg=(.9, .9, .9, 1))
-        pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
-        p2d = P3dGfxMgr.screen_coord(pos)
-        self._txt['pos'] = p2d
-        self.repos_done = True
-
-    def repos_x(self, x):
-        self._np.set_x(x)
-        bounds = self._np.get_tight_bounds()
-        bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
-        pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
-        p2d = P3dGfxMgr.screen_coord(pos)
-        self._txt['pos'] = p2d
-
-    def get_bottom(self):
-        bounds = self._np.get_tight_bounds()
-        bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
-        pos = self._np.get_pos() + (bounds[1][0], bounds[1][1], bounds[0][2])
-        p2d = P3dGfxMgr.screen_coord(pos)
-        ar = base.get_aspect_ratio()
-        return p2d[1] if ar >= 1 else (p2d[1] * ar)
-
-    def get_corner(self):
-        bounds = self._np.get_tight_bounds()
-        return bounds[1][0], bounds[1][1], bounds[0][2]
-
-    def _set_outline_model(self):
-        self._outline_model = loader.load_model(self._model_path)
-        #clockw = CullFaceAttrib.MCullClockwise
-        #self._outline_model.set_attrib(CullFaceAttrib.make(clockw))
-        self._outline_model.set_attrib(CullFaceAttrib.make_reverse())
-        self._outline_model.reparent_to(self._np)
-        self._outline_model.set_scale(1.08)
-        self._outline_model.set_light_off()
-        self._outline_model.set_color(.4, .4, .4, 1)
-        self._outline_model.set_color_scale(.4, .4, .4, 1)
-        self._outline_model.hide()
-
-    def on_frame(self, task):
-        self._np.set_y(0)
-        return task.cont
-
-    def undo(self):
-        self._command_idx -= 1
-        self._np.set_pos(self._commands[self._command_idx].pos)
-        self._np.set_hpr(self._commands[self._command_idx].rot)
-
-    def redo(self):
-        self._command_idx += 1
-        self._np.set_pos(self._commands[self._command_idx].pos)
-        self._np.set_hpr(self._commands[self._command_idx].rot)
-
-    def play(self):
-        if not self._instantiated:
-            return
-        self._world.remove_rigid_body(self.node)
-        self.node.set_mass(self._mass)
-        self._world.attach_rigid_body(self.node)
-        self.node.set_restitution(self._restitution)
-        self.node.set_friction(self._friction)
-
-    def on_click_l(self, pos):
-        if self._paused: return
-        self._start_drag_pos = pos, self._np.get_pos()
-        loader.load_sfx('assets/audio/sfx/grab.ogg').play()
-        if not self._instantiated:
-            self._world.remove_ghost(self.node)
-            pos = self._np.get_pos()
-            self._np.remove_node()
-            self.node = BulletRigidBodyNode('box')
-            self._set_shape()
-            self._np = render.attach_new_node(self.node)
-            self._world.attach_rigid_body(self.node)
-            self._model.reparent_to(self._np)
-            self._np.set_pos(pos)
-            self._set_outline_model()
-            self._np.set_scale(self._model_scale)
-            self._model.show(BitMask32(0x01))
-            self._outline_model.hide(BitMask32(0x01))
-            self._instantiated = True
-            self._txt.destroy()
-            self._count -= 1
-            if self._count:
-                item = self.__class__(self._world, self._plane_node, self._cb_inst, self._curr_bottom, self._scene_repos, count=self._count, mass=self._mass, pos=self._pos, r=self._r)  # pylint: disable=no-value-for-parameter
-                self._cb_inst(item)
-            self._scene_repos()
-
-    def on_click_r(self, pos):
-        if self._paused or not self._instantiated: return
-        self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
-        loader.load_sfx('assets/audio/sfx/grab.ogg').play()
-
-    def on_release(self):
-        if self._start_drag_pos or self._prev_rot_info:
-            loader.load_sfx('assets/audio/sfx/release.ogg').play()
-            self._command_idx += 1
-            self._commands = self._commands[:self._command_idx]
-            self._commands += [Command(self._np.get_pos(), self._np.get_hpr())]
-            self._first_command = False
-        self._start_drag_pos = self._prev_rot_info = None
-        if self._overlapping:
-            self._np.set_pos(self._last_nonoverlapping_pos)
-            self._np.set_hpr(self._last_nonoverlapping_rot)
-            self._outline_model.set_color(.4, .4, .4, 1)
-            self._outline_model.set_color_scale(.4, .4, .4, 1)
-            self._overlapping = False
-
-    def on_mouse_on(self):
-        if not self._paused and self.interactable:
-            self._outline_model.show()
-
-    def on_mouse_off(self):
-        if self._start_drag_pos or self._prev_rot_info: return
-        if self.interactable:
-            self._outline_model.hide()
-
-    def on_mouse_move(self, pos):
-        if self._start_drag_pos:
-            d_pos =  pos - self._start_drag_pos[0]
-            self._np.set_pos(self._start_drag_pos[1] + d_pos)
-        if self._prev_rot_info:
-            start_vec = self._prev_rot_info[0] - self._prev_rot_info[1]
-            curr_vec = pos - self._prev_rot_info[1]
-            d_angle = curr_vec.signed_angle_deg(start_vec, (0, -1, 0))
-            self._np.set_r(self._prev_rot_info[2] + d_angle)
-            self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
-        if self._start_drag_pos or self._prev_rot_info:
-            res = self._world.contact_test(self.node)
-            nres = res.get_num_contacts()
-            if nres <= self._exp_num_contacts:
-                self._overlapping = False
-                self._outline_model.set_color(.4, .4, .4, 1)
-                self._outline_model.set_color_scale(.4, .4, .4, 1)
-            if nres > self._exp_num_contacts and not self._overlapping:
-                actual_nres = 0
-                for contact in res.get_contacts():
-                    for node in [contact.get_node0(), contact.get_node1()]:
-                        if isinstance(node, BulletRigidBodyNode) and \
-                                node != self.node:
-                            actual_nres += 1
-                if actual_nres >= 1:
-                    self._overlapping = True
-                    loader.load_sfx('assets/audio/sfx/overlap.ogg').play()
-                    self._outline_model.set_color(.9, .1, .1, 1)
-                    self._outline_model.set_color_scale(.9, .1, .1, 1)
-        if not self._overlapping:
-            self._last_nonoverlapping_pos = self._np.get_pos()
-            self._last_nonoverlapping_rot = self._np.get_hpr()
-
-    def on_aspect_ratio_changed(self):
-        if not self._instantiated:
-            self._repos()
-
-    def store_state(self):
-        self._paused = True
-        self._model.set_transparency(True)
-        self._model.set_alpha_scale(.3)
-        if hasattr(self, '_txt') and not self._txt.is_empty():
-            self._txt.set_alpha_scale(.3)
-
-    def restore_state(self):
-        self._paused = False
-        self._model.set_alpha_scale(1)
-        if hasattr(self, '_txt') and not self._txt.is_empty():
-            self._txt.set_alpha_scale(1)
-
-    def fail_condition(self):
-        if self._np.get_z() < -6:
-            return True
-        self._positions += [self._np.get_pos()]
-        self._rotations += [self._np.get_hpr()]
-        if len(self._positions) > 30:
-            self._positions.pop(0)
-        if len(self._rotations) > 30:
-            self._rotations.pop(0)
-        if len(self._positions) < 28:
-            return
-        avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
-        avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
-        avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
-        avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
-        avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
-        avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
-        avg_pos = Point3(avg_x, avg_y, avg_z)
-        avg_rot = Point3(avg_h, avg_p, avg_r)
-        return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
-            all((rot - avg_rot).length() < 1 for rot in self._rotations)
-
-    def destroy(self):
-        self._np.remove_node()
-        taskMgr.remove(self._box_tsk)
-        if hasattr(self, '_txt'):
-            self._txt.destroy()
-        if not self._instantiated:
-            self._world.remove_ghost(self.node)
-        else:
-            self._world.remove_rigid_body(self.node)
diff --git a/logics/items/shelf.py b/logics/items/shelf.py
deleted file mode 100644 (file)
index b88ecd0..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-from panda3d.bullet import BulletBoxShape, BulletRigidBodyNode, BulletGhostNode
-from logics.items.item import Item
-
-
-class Shelf(Item):
-
-    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.6):
-        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/shelf/shelf.bam', mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
-
-    def _set_shape(self, apply_scale=True):
-        self.node.add_shape(BulletBoxShape((1, .5, .05)))
diff --git a/logics/items/teetertooter.py b/logics/items/teetertooter.py
deleted file mode 100644 (file)
index 7dc3349..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-from panda3d.core import TransformState
-from panda3d.bullet import BulletCylinderShape, BulletRigidBodyNode, BulletGhostNode, YUp, ZUp
-from logics.items.item import Item
-
-
-class TeeterTooter(Item):
-
-    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.5):
-        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/teeter_tooter/teeter_tooter.bam', exp_num_contacts=2, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction, model_scale=.5)
-
-    def _set_shape(self, apply_scale=True):
-        scale = self._model_scale if apply_scale else 1
-        self.node.add_shape(
-            BulletCylinderShape(.1, 1.6, YUp),
-            TransformState.makePos((0, 0, scale * .36)))
-        self.node.add_shape(
-            BulletCylinderShape(.1, .7, ZUp),
-            TransformState.makePos((0, scale * .8, scale * -.1)))
-        self.node.add_shape(
-            BulletCylinderShape(.1, .7, ZUp),
-            TransformState.makePos((0, scale * -.8, scale * -.1)))
diff --git a/logics/posmgr.py b/logics/posmgr.py
deleted file mode 100644 (file)
index 980d43c..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-class PositionMgr:
-
-    def __init__(self):
-        self._pos = {}
-
-    def reset(self):
-        self._pos = {}
-
-    def register(self, wdg, pos):
-        print('register', wdg, pos)
-        self._pos[wdg] = pos
-
-    def get(self, tgt):
-        return self._pos[tgt]
diff --git a/logics/scene.py b/logics/scene.py
deleted file mode 100644 (file)
index ae168b3..0000000
+++ /dev/null
@@ -1,597 +0,0 @@
-from os.path import exists
-from os import makedirs
-from glob import glob
-from logging import debug, info
-from importlib import import_module
-from inspect import isclass
-from panda3d.core import AmbientLight, DirectionalLight, Point3, 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 logics.items.background import Background
-from gui.sidepanel import SidePanel
-from ya2.utils.cursor import MouseCursor
-from ya2.p3d.gfx import P3dGfxMgr
-from ya2.p3d.p3d import LibP3d
-
-
-class Scene(DirectObject):
-
-    def __init__(self, world, exit_cb, auto_close_instr, dbg_items, reload_cb, scenes, pos_mgr):
-        super().__init__()
-        self._world = world
-        self._exit_cb = exit_cb
-        self._dbg_items = dbg_items
-        self._reload_cb = reload_cb
-        self._pos_mgr = pos_mgr
-        self._pos_mgr.reset()
-        self._scenes = scenes
-        self._enforce_res = ''
-        self.accept('enforce_res', self.enforce_res)
-        self._set_camera()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01))
-        self._set_gui()
-        self._set_lights()
-        self._set_input()
-        self._set_mouse_plane()
-        self.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, 'on_frame')
-
-    @staticmethod
-    def name():
-        return ''
-
-    def _instr_txt(self):
-        return ''
-
-    def _set_items(self):
-        self.items = []
-
-    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.__class__.__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]
-        self._set_items()
-        self._state = 'init'
-        self._commands = []
-        self._command_idx = 0
-        if hasattr(self, '_success_txt'):
-            self._success_txt.destroy()
-            del self._success_txt
-        self.__right_btn['state'] = NORMAL
-
-    def enforce_res(self, val):
-        self._enforce_res = val
-        info('enforce res: ' + val)
-
-    def destroy(self):
-        self.ignore('enforce_res')
-        self._unset_gui()
-        self._unset_lights()
-        self._unset_input()
-        self._unset_mouse_plane()
-        [itm.destroy() for itm in self.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()
-
-    def _set_camera(self):
-        base.camera.set_pos(0, -20, 0)
-        base.camera.look_at(0, 0, 0)
-
-    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'),
-            ('information', self._set_instructions, NORMAL, abl, 'gray'),
-            ('right', self.on_play, NORMAL, abr, 'green'),
-            #('next', self.on_next, DISABLED, abr, 'gray'),
-            #('previous', self.on_prev, DISABLED, abr, 'gray'),
-            #('rewind', self.reset, NORMAL, abr, 'gray')
-        ]
-        num_l = num_r = 0
-        btns = []
-        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.set_transparency(True)
-            self._pos_mgr.register(binfo[0], LibP3d.wdg_pos(btn))
-            btns += [btn]
-        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)
-
-    def _unset_gui(self):
-        btns = [
-            self.__home_btn, self.__info_btn, self.__right_btn,
-            #self.__next_btn, self.__prev_btn, self.__rewind_btn
-        ]
-        [btn.destroy() for btn in btns]
-        if self._dbg_items:
-            self._info_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 = P3dGfxMgr.world_from_to(base.mouseWatcherNode.get_mouse())
-        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
-                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):
-        pass
-
-    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 self._win_condition():
-            self._set_fail() if self._enforce_res == 'fail' else self._set_win()
-        elif self._state == 'playing' and self._fail_condition():
-            self._set_win() if self._enforce_res == 'win' else self._set_fail()
-        if any(itm._overlapping for itm in self.items):
-            self._cursor.cursor_img.img.set_color(.9, .1, .1, 1)
-        else:
-            self._cursor.cursor_img.img.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]
-
-    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_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))
-        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(
-            self._instr_txt(), parent=frm, font=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 - 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.set_transparency(True)
-        self._pos_mgr.register('close_instructions', LibP3d.wdg_pos(btn))
-
-    def _set_win(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 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]
-        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.set_transparency(True)
-        self._pos_mgr.register('home_win', LibP3d.wdg_pos(btn))
-        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'))
-        self._pos_mgr.register('replay', LibP3d.wdg_pos(btn))
-        btn.set_transparency(True)
-        enabled = self._scenes.index(self.__class__) < len(self._scenes) - 1
-        if enabled:
-            next_scene = self._scenes[self._scenes.index(self.__class__) + 1]
-        else:
-            next_scene = None
-        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['state'] = NORMAL if enabled else DISABLED
-        self._pos_mgr.register('next', LibP3d.wdg_pos(btn))
-        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]
-        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'))
-        self._pos_mgr.register('home_win', LibP3d.wdg_pos(btn))
-        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'))
-        self._pos_mgr.register('replay', LibP3d.wdg_pos(btn))
-        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
-        ]
-        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
-        ]
-        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()
diff --git a/logics/scenes/__init__.py b/logics/scenes/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/logics/scenes/scene_basketball.py b/logics/scenes/scene_basketball.py
deleted file mode 100644 (file)
index f90e683..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-from logics.scene import Scene
-from logics.items.box import Box
-from logics.items.shelf import Shelf
-from logics.items.domino import Domino, UpStrategy, DownStrategy
-from logics.items.basketball import Basketball
-from logics.items.teetertooter import TeeterTooter
-
-
-class SceneBasketBall(Scene):
-
-    @staticmethod
-    def name():
-        return _('Basket ball')
-
-    def _set_items(self):
-        self.items = []
-        #self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=5, count=2)]
-        #self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=9)]
-        self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=1)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-.56, 0, .21))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.67, 0, .21))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-.56, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.67, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-4.45, 0, -3.18), r=27, restitution=1)]
-        #self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-5.45, 0, -3.18), restitution=1)]
-        #self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(3.78, 0, -1.45))]
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=9)]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.61, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.06, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(0.91, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.73, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.57, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 30))
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.61, 0, .73), r=37)]
-        #self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.06, 0, .78))]
-        #self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(0.91, 0, .78))]
-        #self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.73, 0, .78))]
-        #self.items[-1].set_strategy(UpStrategy(self.items[-1]._np, 30))
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.57, 0, .78))]
-        #self.items[-1].set_strategy(UpStrategy(self.items[-1]._np, 30))
-        #self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name() + '\n\n'
-        txt += _('Goal: you must hit every domino piece\n\n')
-        txt += _('keep \5mouse_l\5 pressed to drag an item\n\n'
-                'keep \5mouse_r\5 pressed to rotate an item')
-        return txt
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
diff --git a/logics/scenes/scene_box.py b/logics/scenes/scene_box.py
deleted file mode 100644 (file)
index aed3b73..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-from logics.scene import Scene
-from logics.items.box import Box, HitStrategy
-from logics.items.shelf import Shelf
-from logics.items.domino import Domino
-from logics.items.basketball import Basketball
-from logics.items.teetertooter import TeeterTooter
-
-
-class SceneBox(Scene):
-
-    @staticmethod
-    def name():
-        return _('Box')
-
-    def _set_items(self):
-        self.items = []
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(.46, 0, -3.95))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(4.43, 0, -3.95))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-1.29, 0, .26), r=28.45)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(2.15, 0, -1.49), r=28.45)]
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-1.55, 0, 1.23), friction=.4)]
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(4.38, 0, -3.35))]
-        self.items[-1].set_strategy(HitStrategy(self.items[-2], self.items[-1].node, self.items[-1]._world))
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=2)]
-        #self.items += [TargetDomino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-1.14, 0, -.04), tgt_degrees=60)]
-        #self.items += [TargetDomino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.49, 0, -.04), tgt_degrees=60)]
-        #self.items += [TargetDomino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(0.94, 0, -.04), tgt_degrees=60)]
-        #self.items += [TargetDomino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.55, 0, -.04), tgt_degrees=60)]
-        #self.items += [TargetDomino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.09, 0, -.04), tgt_degrees=88)]
-        #self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name() + '\n\n'
-        txt += _('Goal: the left box must hit the right box\n\n')
-        txt += _('keep \5mouse_l\5 pressed to drag an item\n\n'
-                'keep \5mouse_r\5 pressed to rotate an item')
-        return txt
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
diff --git a/logics/scenes/scene_domino.py b/logics/scenes/scene_domino.py
deleted file mode 100644 (file)
index fa14aa5..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-from logics.scene import Scene
-from logics.items.box import Box
-from logics.items.shelf import Shelf
-from logics.items.domino import Domino, DownStrategy
-from logics.items.basketball import Basketball
-from logics.items.teetertooter import TeeterTooter
-
-
-class SceneDomino(Scene):
-
-    @staticmethod
-    def name():
-        return _('Domino')
-
-    def _set_items(self):
-        self.items = []
-        #self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-1.2, 0, -.6))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.2, 0, -.6))]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=2)]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-1.14, 0, -.04))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 60))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.49, 0, -.04))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 60))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(0.94, 0, -.04))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 60))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.55, 0, -.04))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 60))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.09, 0, -.04))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 88))
-        #self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name() + '\n\n'
-        txt += _('Goal: every domino piece must fall\n\n')
-        txt += _('keep \5mouse_l\5 pressed to drag an item\n\n'
-                'keep \5mouse_r\5 pressed to rotate an item')
-        return txt
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
diff --git a/logics/scenes/scene_domino_box.py b/logics/scenes/scene_domino_box.py
deleted file mode 100644 (file)
index bbdbb26..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-from logics.scene import Scene
-from logics.items.box import Box
-from logics.items.shelf import Shelf
-from logics.items.domino import Domino, UpStrategy, DownStrategy
-from logics.items.basketball import Basketball
-from logics.items.teetertooter import TeeterTooter
-
-
-class SceneDominoBox(Scene):
-
-    @staticmethod
-    def name():
-        return _('Domino and box')
-
-    def _set_items(self):
-        self.items = []
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=5, count=2)]
-        #self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=9)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-.56, 0, .21))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.67, 0, .21))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-.56, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.67, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(3.78, 0, -1.45))]
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=9)]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.61, 0, -.94), r=37)]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.06, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(0.91, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.73, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.57, 0, -.89))]
-        self.items[-1].set_strategy(UpStrategy(self.items[-1]._np, 30))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.61, 0, .73), r=37)]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.06, 0, .78))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(0.91, 0, .78))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.73, 0, .78))]
-        self.items[-1].set_strategy(UpStrategy(self.items[-1]._np, 30))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.57, 0, .78))]
-        self.items[-1].set_strategy(UpStrategy(self.items[-1]._np, 30))
-        #self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name() + '\n\n'
-        txt += _('Goal: only the last piece of each row must be up\n\n')
-        txt += _('keep \5mouse_l\5 pressed to drag an item\n\n'
-                'keep \5mouse_r\5 pressed to rotate an item')
-        return txt
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
diff --git a/logics/scenes/scene_domino_box_basketball.py b/logics/scenes/scene_domino_box_basketball.py
deleted file mode 100644 (file)
index 6181478..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-from logics.scene import Scene
-from logics.items.box import Box
-from logics.items.shelf import Shelf
-from logics.items.domino import Domino, UpStrategy, DownStrategy
-from logics.items.basketball import Basketball
-from logics.items.teetertooter import TeeterTooter
-
-
-class SceneDominoBoxBasketball(Scene):
-
-    @staticmethod
-    def name():
-        return _('Domino, box and basket ball')
-
-    def _set_items(self):
-        self.items = []
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=1, mass=5)]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=1)]
-        self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.3, 1, 2.5))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-.56, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.67, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(3.78, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.48, 0, .38), r=-90)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(2.62, 0, .05))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(4.88, 0, .05))]
-        #self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=9)]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.68, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.35, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(3.08, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(3.78, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(4.53, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        #self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name() + '\n\n'
-        txt += _('Goal: every domino piece must be hit\n\n')
-        txt += _('keep \5mouse_l\5 pressed to drag an item\n\n'
-                'keep \5mouse_r\5 pressed to rotate an item')
-        return txt
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
diff --git a/logics/scenes/scene_teeter_domino_box_basketball.py b/logics/scenes/scene_teeter_domino_box_basketball.py
deleted file mode 100644 (file)
index 75d361c..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-from logics.scene import Scene
-from logics.items.box import Box
-from logics.items.shelf import Shelf
-from logics.items.domino import Domino, UpStrategy, DownStrategy
-from logics.items.basketball import Basketball
-from logics.items.teetertooter import TeeterTooter
-
-
-class SceneTeeterDominoBoxBasketball(Scene):
-
-    @staticmethod
-    def name():
-        return _('Teeter tooter, domino, box and basket ball')
-
-    def _set_items(self):
-        self.items = []
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=3, count=2, friction=1)]
-        self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(.98, 1, 1.02))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-6.24, 0, -1.45))]
-        self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-6.24, 0, -1.20))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=1, r=24.60, friction=1, pos=(-6.15, 0, -.93))]
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=.3, friction=1, model_scale=.5, pos=(-5.38, 0, -.93), r=24.60)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(5.37, 0, -.78))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(7.48, 0, -.78))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(4.74, 0, -1.95))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(6.88, 0, -1.95))]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=1, pos=(4.83, 0, -1.39))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=1, pos=(5.67, 0, -1.39))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=1, pos=(6.59, 0, -1.39))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(.53, 0, -1.95), restitution=.95)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(2.63, 0, -1.95), restitution=.95)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-3.65, 0, 1.05), r=28, friction=0)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-1.27, 0, 1.72), restitution=.95)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(.88, 0, 1.72), restitution=.95)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-1.67, 0, .55), restitution=.95)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(.52, 0, .55), restitution=.95)]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=.5, pos=(-1.73, 0, 1.11))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=.5, pos=(-.97, 0, 1.11))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=.5, pos=(-.1, 0, 1.11))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name() + '\n\n'
-        txt += _('Goal: every domino piece must be hit\n\n')
-        txt += _('keep \5mouse_l\5 pressed to drag an item\n\n'
-                'keep \5mouse_r\5 pressed to rotate an item')
-        return txt
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
diff --git a/logics/scenes/scene_teeter_tooter.py b/logics/scenes/scene_teeter_tooter.py
deleted file mode 100644 (file)
index 3a748ea..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-from logics.scene import Scene
-from logics.items.box import Box
-from logics.items.shelf import Shelf
-from logics.items.domino import Domino, UpStrategy, DownStrategy
-from logics.items.basketball import Basketball
-from logics.items.teetertooter import TeeterTooter
-
-
-class SceneTeeterTooter(Scene):
-
-    @staticmethod
-    def name():
-        return _('Teeter tooter')
-
-    def _set_items(self):
-        self.items = []
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=3, count=1, friction=1)]
-        #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=5, count=2)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-2.76, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-.56, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(2.27, 0, -.28))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(4.38, 0, -.28))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.67, 0, -1.45))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(3.78, 0, -1.45))]
-        self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-2.74, 0, -1.20))]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=1, r=-25.30, friction=1, pos=(-2.78, 0, -.93))]
-        self.items += [Box(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=.2, friction=1, model_scale=.5, pos=(-3.61, 0, -.99), r=-25.30)]
-        self.items += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(-.25, 0, -.57), r=52)]
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(1.73, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(2.57, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(3.50, 0, -.89))]
-        self.items[-1].set_strategy(DownStrategy(self.items[-1]._np, 35))
-        #self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-        #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
-
-    def _instr_txt(self):
-        txt = _('Scene: ') + self.name() + '\n\n'
-        txt += _('Goal: you must hit every domino piece\n\n')
-        txt += _('keep \5mouse_l\5 pressed to drag an item\n\n'
-                'keep \5mouse_r\5 pressed to rotate an item')
-        return txt
-
-    def _win_condition(self):
-        return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
diff --git a/main.py b/main.py
index 00d9b9cc67c1e45ea1a98c8d5eb5d6daba5c2d8a..f7a5c7954d1b14e54b0b1de5c1a8ee15d231af55 100644 (file)
--- a/main.py
+++ b/main.py
@@ -1,19 +1,28 @@
-'''This is the main file. This launches the application.'''
-import ya2.utils.log  # so logging's info/debug are logged
+from ya2.utils.log import LogManager
+LogManager.before_init_setup('pmachines')
 from sys import argv
 from sys import argv
-from panda3d.core import load_prc_file_data
-if '--version' in argv:
-    load_prc_file_data('', 'window-type none')
+from ya2.utils.gui.gui import GuiTools
+if '--version' in argv: GuiTools.no_window()
 from os.path import exists
 from os.path import exists
-from traceback import print_exc
-from logics.app import PmachinesApp
 from p3d_appimage import AppImageBuilder
 from p3d_appimage import AppImageBuilder
+from pmachines.application.application import Pmachines
+from traceback import print_exc
+
+
+class Main:
+
+    def __init__(self):
+        self.__pmachines = Pmachines()
+        self.__appimage_builder = AppImageBuilder(None, 'pmachines')
+
+    def run(self):
+        if self.__pmachines.is_update_run: self.__appimage_builder.update()
+        elif not self.__pmachines.is_version_run: self.__run_game()
+
+    def __run_game(self):
+        try: self.__pmachines.run()
+        except Exception: print_exc()
+
 
 if __name__ == '__main__' or exists('main.pyo'):
 
 if __name__ == '__main__' or exists('main.pyo'):
-    pmachines = PmachinesApp()
-    if pmachines.updating:
-        AppImageBuilder('pmachines').update()
-    elif not pmachines.version:
-        try: pmachines.base.run()
-        except Exception:
-            print_exc()
+    Main().run()
diff --git a/pmachines/__init__.py b/pmachines/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
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..ece8537
--- /dev/null
@@ -0,0 +1,341 @@
+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, shutdown
+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 MusicManager
+from pmachines.items.background import Background
+from pmachines.gui.menu import Menu
+from pmachines.scene.scene import Scene
+from pmachines.application.persistency import Persistency
+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
+from ya2.utils.gui.gui import GuiTools
+
+
+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, scene_name):
+        self._pmachines.on_scene_enter(scene_name)
+
+    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 or args.system_test:
+            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)
+        GuiTools.init()
+        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 or args.system_test:
+            self._options['settings']['volume'] = 0
+        self._music = MusicManager(self._options['settings']['volume'], 'pmachines')
+        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 or args.system_test:
+            self.__fps_lst = []
+            taskMgr.do_method_later(1.0, self.__assert_fps, 'assert_fps')
+
+    def start(self):
+        if self._args.system_test:
+            info('the application has been started correctly')
+            shutdown()
+            exit()
+        elif 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('wood')
+        self._menu = Menu(
+            lambda scene_name: self._fsm.demand('Scene', scene_name),
+            self.lang_mgr.set_language, self._options,
+            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 or args.system_test:
+            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')
+        parser.add_argument('--system-test', 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(f'{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': 1,
+                '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 = Persistency(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 = 25 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/persistency.py b/pmachines/application/persistency.py
new file mode 100644 (file)
index 0000000..f728ead
--- /dev/null
@@ -0,0 +1,34 @@
+from json import loads, dumps
+
+
+class Persistency:
+
+    def __init__(self, scenes_done, option_file):
+        self.__scenes_done = scenes_done
+        self.__option_file = option_file
+        self.__fix_ini_parsing()
+
+    def __fix_ini_parsing(self):
+        if len(self.__scenes_done) == 1 and not self.__scenes_done[0]:
+            self.__scenes_done = []
+        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 = loads(self.__scenes_done)
+
+    def save_scene(self, name, version):
+        self.__compute_scenes_done(name, version)
+        self.__store_scenes()
+
+    def __compute_scenes_done(self, name, version):
+        other_scenes = [s for s in self.__scenes_done if s[0] != name]
+        self.__scenes_done = other_scenes + [(name, version)]
+
+    def __store_scenes(self):
+        self.__option_file['save']['scenes_done'] = f"'{dumps(self.__scenes_done)}'"
+        self.__option_file.store()
+
+    @property
+    def scenes_done(self): return self.__scenes_done
diff --git a/pmachines/audio/__init__.py b/pmachines/audio/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pmachines/audio/music.py b/pmachines/audio/music.py
new file mode 100644 (file)
index 0000000..e0d18bb
--- /dev/null
@@ -0,0 +1,36 @@
+from os.path import basename
+from glob import glob
+from random import choice
+from logging import info
+from panda3d.core import AudioSound, Filename
+from ya2.utils.audio import AudioTools
+from ya2.utils.logics import LogicsTools
+
+
+class MusicManager:
+
+    def __init__(self, volume, app_name):
+        self.__current_path = LogicsTools.current_path(app_name)
+        musics = self.__current_path + 'assets/audio/music/*.ogg'
+        self.__start_music(glob(musics))
+        AudioTools.set_volume(volume)
+        taskMgr.add(self.__on_frame, 'on frame music')
+
+    def __start_music(self, file_names):
+        self.__music = loader.load_music(choice(file_names))
+        info('playing music ' + self.__music.get_name())
+        self.__music.play()
+
+    def __on_frame(self, task):
+        if self.__music.status() == AudioSound.READY: self.__restart_music()
+        return task.cont
+
+    def __restart_music(self):
+        ogg_files = Filename(self.__current_path + 'assets/audio/music/*.ogg').to_os_specific()
+        file_names = glob(ogg_files)
+        music_to_remove = Filename(
+            self.__current_path + 'assets/audio/music/' +
+            basename(self.__music.get_name())).to_os_specific()
+        # basename is needed in windows
+        file_names.remove(music_to_remove)
+        self.__start_music(file_names)
diff --git a/pmachines/editor/__init__.py b/pmachines/editor/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pmachines/editor/augmented_frame.py b/pmachines/editor/augmented_frame.py
new file mode 100644 (file)
index 0000000..4480c01
--- /dev/null
@@ -0,0 +1,116 @@
+from panda3d.core import LPoint3f, Texture
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.DirectGui import DirectFrame, DirectButton
+from direct.gui.DirectGuiGlobals import NORMAL, B1PRESS, B1RELEASE, FLAT
+from ya2.utils.gfx import DirectGuiMixin
+
+
+class AugmentedDirectFrame(DirectFrame):
+
+    def __init__(self, *args, **kwargs):
+        self.__delta_drag = kwargs['delta_drag']
+        del kwargs['delta_drag']
+        collapse_pos = kwargs['collapse_pos']
+        del kwargs['collapse_pos']
+        pos_mgr = kwargs['pos_mgr']
+        del kwargs['pos_mgr']
+        frame_name = kwargs['frame_name']
+        del kwargs['frame_name']
+        super().__init__(*args, **kwargs)
+        self.initialiseoptions(AugmentedDirectFrame)
+        self['state'] = NORMAL
+        self.bind(B1PRESS, self.__drag)
+        self.bind(B1RELEASE, self.__drop)
+        self.__start_drag_pos = None
+        self.__update_dd_task = taskMgr.add(self.__update_dd, 'update_dd', sort=-50)
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        self.__collapse_btn = DirectButton(
+            image=self.__load_images_btn('up', 'gray'), scale=.05,
+            pos=collapse_pos,
+            parent=self, command=self.__on_collapse, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        self.__collapse_btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr[f'collapse_button_{frame_name}'] = self.__collapse_btn.pos_pixel()
+        _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)
+        _common = {
+            'scale': .046,
+            'text_font': _font,
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': loader.load_sfx(
+                'assets/audio/sfx/rollover.ogg'),
+            'clickSound': loader.load_sfx(
+                'assets/audio/sfx/click.ogg')}
+        tooltip_args = _common['text_font'], _common['scale'], _common['text_fg']
+        self.__collapse_btn.set_tooltip(_('Collapse/Expand'), *tooltip_args)
+        self.__collapsed = False
+
+    def __drag(self, mouse_pos=None):
+        m = mouse_pos.get_mouse()
+        m = LPoint3f(m[0], 0, m[1])
+        from_origin = m - self.parent.get_pos()
+        self.__start_drag_pos = self.get_pos(), from_origin
+
+    def __drop(self, mouse_pos=None):
+        self.__start_drag_pos = None
+
+    def __update_dd(self, task):
+        if base.mouseWatcherNode.has_mouse() and self.__start_drag_pos:
+            m = base.mouseWatcherNode.get_mouse()
+            m = LPoint3f(m[0], 0, m[1])
+            delta = m - self.__start_drag_pos[1]
+            new_pos = self.__start_drag_pos[0] + delta + self.__delta_drag
+            self.set_pos(new_pos)
+        return task.again
+
+    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 __load_images_btn(self, 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]
+
+    def __on_collapse(self):
+        self.__expand() if self.__collapsed else self.__collapse()
+
+    def __collapse(self):
+        self.__collapsed = True
+        for c in self.children:
+            c.hide()
+        self.__collapse_btn.show()
+        self.__collapse_btn['image'] = self.__load_images_btn('down', 'gray')
+        self.__prev_fs = self['frameSize']
+        self['frameSize'] = (0, 0, 0, 0)
+
+    def __expand(self):
+        self.__collapsed = False
+        for c in self.children:
+            c.show()
+        self.__collapse_btn['image'] = self.__load_images_btn('up', 'gray')
+        self['frameSize'] = self.__prev_fs
+
+    def destroy(self):
+        super().destroy()
+        taskMgr.remove(self.__update_dd_task)
diff --git a/pmachines/editor/inspector.py b/pmachines/editor/inspector.py
new file mode 100644 (file)
index 0000000..2b6d872
--- /dev/null
@@ -0,0 +1,577 @@
+from collections import namedtuple
+from logging import info
+from panda3d.core import Texture, TextNode, LPoint3f
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.DirectGui import DirectButton, DirectFrame, DirectEntry, DirectOptionMenu, OkDialog
+from direct.gui.DirectGuiGlobals import FLAT, NORMAL
+from direct.gui.OnscreenText import OnscreenText
+from direct.showbase.DirectObject import DirectObject
+from pmachines.items.item import FixedStrategy, StillStrategy
+from pmachines.items.box import HitStrategy
+from pmachines.items.domino import DownStrategy, UpStrategy
+from pmachines.editor.augmented_frame import AugmentedDirectFrame
+from ya2.utils.gui.base_page import DirectOptionMenuTestable
+from ya2.utils.gfx import DirectGuiMixin
+
+
+class Inspector(DirectObject):
+
+    def __init__(self, item, all_items, pos_mgr, strategy_items):
+        super().__init__()
+        self.__item = item
+        self.__all_items = all_items
+        self.__pos_mgr = pos_mgr
+        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)
+        self._common = {
+            'scale': .046,
+            'text_font': self._font,
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': loader.load_sfx(
+                'assets/audio/sfx/rollover.ogg'),
+            'clickSound': loader.load_sfx(
+                'assets/audio/sfx/click.ogg')}
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        w, h = .8, 1.04
+        self._frm = AugmentedDirectFrame(frameColor=(.4, .4, .4, .06),
+                                         frameSize=(0, w, -h, 0),
+                                         parent=base.a2dTopRight,
+                                         pos=(-w, 0, 0),
+                                         delta_drag=LPoint3f(-w, 0, -h),
+                                         collapse_pos=(w - .06, 1, -h + .06),
+                                         pos_mgr=pos_mgr,
+                                         frame_name='inspector')
+        self.__z = -.08
+        p = self.__item._np.get_pos()
+        r = self.__item._np.get_r()
+        s = self.__item._np.get_scale()[0]
+        m = self.__item._mass
+        restitution = self.__item._restitution
+        f = self.__item._friction
+        _id = ''
+        if 'id' in self.__item.json:
+            _id = self.__item.json['id']
+        _strategy = ''
+        if 'strategy' in self.__item.json:
+            _strategy = self.__item.json['strategy']
+        _strategy_args = ''
+        if 'strategy_args' in self.__item.json:
+            _strategy_args = ' '.join(map(str, self.__item.json['strategy_args']))
+        t, pos_entry = self.__add_row('position', _('position'), f'{round(p.x, 3)} {round(p.z, 3)}', self.on_edit_position, _('position (e.g. 0.1 2.3 4.5)'))
+        t, rot_entry = self.__add_row('roll', _('roll'), f'{round(r, 3)}', self.on_edit_roll, _('roll (e.g. 90)'))
+        t, scale_entry = self.__add_row('scale', _('scale'), f'{round(s, 3)}', self.on_edit_scale, _('scale (e.g. 1.2)'))
+        t, mass_entry = self.__add_row('mass', _('mass'), f'{round(m, 3)}', self.on_edit_mass, _('mass (default 1; 0 if fixed)'))
+        t, restitution_entry = self.__add_row('restitution', _('restitution'), f'{round(restitution, 3)}', self.on_edit_restitution, _('restitution (default 0.5)'))
+        t, friction_entry = self.__add_row('friction', _('friction'), f'{round(f, 3)}', self.on_edit_friction, _('friction (default 0.5)'))
+        t, id_entry = self.__add_row('id', _('id'), _id, self.on_edit_id, _('id of the item (for the strategies)'))
+        # item_modules = glob('pmachines/items/*.py')
+        # item_modules = [basename(i)[:-3] for i in item_modules]
+        # strategy_items = ['']
+        # for item_module in item_modules:
+        #     mod_name = 'pmachines.items.' + item_module
+        #     for member in import_module(mod_name).__dict__.values():
+        #         if isclass(member) and issubclass(member, ItemStrategy) and \
+        #                 member != ItemStrategy:
+        #             strategy_items = list(set(strategy_items + [member.__name__]))
+        strategy_names = [s.__name__ for s in strategy_items]
+        t, strategy_entry = self.__add_row_option(_('strategy'), _strategy, strategy_names, self.on_edit_strategy, _('the strategy of the item'))
+
+        def strategy_set(comps):
+            strategy_labels = [f'inspector_strategy_{i.lower()}' for i in strategy_names]
+            for i in strategy_labels:
+                if i in self.__pos_mgr:
+                    del self.__pos_mgr[i]
+            for l, b in zip(strategy_labels, comps):
+                b.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
+                p = b.pos_pixel()
+                self.__pos_mgr[l] = (p[0] + 5, p[1] + 10)
+        strategy_entry._show_cb = strategy_set
+        p = strategy_entry.pos_pixel()
+        self.__pos_mgr['editor_inspector_strategy'] = (p[0] + 5, p[1])
+
+        t, strategy_args_entry = self.__add_row('strategy_args', _('strategy_args'), _strategy_args, self.on_edit_strategy_args, _('the arguments of the strategy'))
+        fields = ['position', 'roll', 'scale', 'mass', 'restitution', 'friction', 'id', 'strategy', 'strategy_args']
+        Entries = namedtuple('Entries', fields)
+        self.__entries = Entries(pos_entry, rot_entry, scale_entry, mass_entry, restitution_entry, friction_entry, id_entry, strategy_entry, strategy_args_entry)
+        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]
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        b = DirectButton(
+            image=load_images_btn('exitRight', 'gray'), scale=.05,
+            pos=(.06, 1, -h + .06),
+            parent=self._frm, command=self.destroy, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_inspector_close'] = b.pos_pixel()
+        b.set_tooltip(_('Close'), *tooltip_args)
+        self.accept('item-rototranslated', self.__on_item_rototranslated)
+        b = DirectButton(
+            image=load_images_btn('trashcan', 'gray'), scale=.05,
+            pos=(.18, 1, -h + .06),
+            parent=self._frm, command=self.__delete_item, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_inspector_delete'] = b.pos_pixel()
+        b.set_tooltip(_('Delete the item'), *tooltip_args)
+
+    def __add_row(self, id_, label, text, callback, tooltip):
+        tw = 10
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        t = OnscreenText(
+            label,
+            pos=(.03, self.__z), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            wordwrap=20, align=TextNode.ALeft)
+        e = DirectEntry(
+            scale=self._common['scale'],
+            pos=(.30, 1, self.__z),
+            entryFont=self._font,
+            width=tw,
+            cursorKeys=True,
+            frameColor=self._common['frameColor'],
+            initialText=text,
+            parent=self._frm,
+            text_fg=self._common['text_fg'],
+            command=callback)
+        e.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        e.set_tooltip(tooltip, *tooltip_args)
+        self.__pos_mgr[f'editor_inspector_{id_}'] = e.pos_pixel()
+        self.__z -= .1
+        return t, e
+
+    def __add_row_option(self, label, text, items, callback, tooltip):
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        t = OnscreenText(
+            label,
+            pos=(.03, self.__z), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            wordwrap=20, align=TextNode.ALeft)
+        e = DirectOptionMenuTestable(
+            scale=self._common['scale'],
+            initialitem=text,
+            pos=(.30, 1, self.__z),
+            items=items,
+            parent=self._frm,
+            command=callback,
+            state=NORMAL,
+            relief=FLAT,
+            item_relief=FLAT,
+            frameColor=self._common['frameColor'],
+            item_frameColor=self._common['frameColor'],
+            popupMenu_frameColor=self._common['frameColor'],
+            popupMarker_frameColor=self._common['frameColor'],
+            text_font=self._font,
+            text_fg=self._common['text_fg'],
+            highlightColor=(.9, .9, .9, .9),
+            item_text_font=self._font,
+            item_text_fg=self._common['text_fg'],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        e.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
+        e.set_tooltip(tooltip, *tooltip_args)
+        self.__z -= .1
+        return t, e
+
+    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 __on_item_rototranslated(self, np):
+        pos = np.get_pos()
+        r = np.get_r()
+        self.__entries.position.set('%s %s' % (str(round(pos.x, 3)), str(round(pos.z, 3))))
+        self.__entries.roll.set('%s' % str(round(r, 3)))
+        self.__item.json['position'] = list(pos)
+        self.__item.json['roll'] = round(r, 3)
+
+    def __delete_item(self):
+        messenger.send('editor-inspector-delete', [self.__item])
+        self.destroy()
+
+    @property
+    def item(self):
+        return self.__item
+
+    def on_edit_position(self, txt):
+        x, z = map(float, txt.split())
+        self.__item.position = [x, 0, z]
+
+    def on_edit_roll(self, txt):
+        self.__item.roll = float(txt)
+
+    def on_edit_scale(self, txt):
+        self.__item.scale = float(txt)
+
+    def on_edit_mass(self, txt):
+        self.__item.mass = float(txt)
+
+    def on_edit_restitution(self, txt):
+        self.__item.restitution = float(txt)
+
+    def on_edit_friction(self, txt):
+        self.__item.friction = float(txt)
+
+    def on_edit_id(self, txt):
+        self.__item.id = txt
+
+    def on_edit_strategy(self, txt):
+        if not txt:
+            self.__entries.strategy_args.set('')
+            return
+        name2class = {
+            'StillStrategy': StillStrategy,
+            'UpStrategy': UpStrategy,
+            'HitStrategy': HitStrategy,
+            'DownStrategy': DownStrategy,
+            'FixedStrategy': FixedStrategy}
+        class_ = name2class[txt]
+        args = []
+        error = False
+        if txt == 'StillStrategy':
+            args += [self.__item._np]
+        if txt in ['UpStrategy', 'DownStrategy']:
+            args += [self.__item._np]
+            try:
+                args += [float(self.__entries.strategy_args.get())]
+            except ValueError:
+                error = True
+        if txt == 'HitStrategy':
+            for item in self.__all_items:
+                if item.id == self.__entries.strategy_args.get():
+                    args += [item]
+            args += [self.__item.node]
+            args += [self.__item._world]
+        if not error:
+            self.__item.strategy = class_(*args)
+            self.__item.strategy_json = txt
+        else:
+            self.__show_error_popup()
+
+    def on_edit_strategy_args(self, txt):
+        self.__item.strategy_args_json = txt
+
+    def __show_error_popup(self):
+            self.__dialog = OkDialog(dialogName='Strategy args errors',
+                                        text=_('There are errors in the strategy args.'),
+                                        command=self.__actually_close)
+            self.__dialog['frameColor'] = (.4, .4, .4, .14)
+            self.__dialog['relief'] = FLAT
+            self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
+            self.__dialog.component('text0')['font'] = self._font
+            for b in self.__dialog.buttonList:
+                b['frameColor'] = (.4, .4, .4, .14)
+                b.component('text0')['fg'] = (.9, .9, .9, 1)
+                b.component('text0')['font'] = self._font
+                b.component('text1')['fg'] = (.9, .1, .1, 1)
+                b.component('text1')['font'] = self._font
+                b.component('text2')['fg'] = (.9, .9, .1, 1)
+                b.component('text2')['font'] = self._font
+
+    def __actually_close(self, arg):
+        self.__entries.strategy.set('')
+        self.__entries.strategy_args.set('')
+        self.__dialog.cleanup()
+
+    def destroy(self):
+        self._frm.destroy()
+        self.ignore('item-rototranslated')
+        messenger.send('editor-inspector-destroy')
+
+
+class PixelSpaceInspector(DirectObject):
+
+    def __init__(self, item, all_items):
+        info('PixelSpaceInspector')
+        super().__init__()
+        self.__item = item
+        self.__all_items = all_items
+        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)
+        self._common = {
+            'scale': .046,
+            'text_font': self._font,
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': loader.load_sfx(
+                'assets/audio/sfx/rollover.ogg'),
+            'clickSound': loader.load_sfx(
+                'assets/audio/sfx/click.ogg')}
+        w, h = .8, .36
+        self._frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                                frameSize=(0, w, -h, 0),
+                                parent=base.a2dTopRight,
+                                pos=(-w, 0, 0))
+        self.__z = -.08
+        p = self.__item._np.get_pos()
+        _id = ''
+        if 'id' in self.__item.json:
+            _id = self.__item.json['id']
+        t, pos_entry = self.__add_row(_('position'), f'{round(p.x, 3)}, {round(p.z, 3)}', self.on_edit_position)
+        t, id_entry = self.__add_row(_('id'), _id, self.on_edit_id)
+        fields = ['position', 'id']
+        Entries = namedtuple('Entries', fields)
+        self.__entries = Entries(pos_entry, id_entry)
+        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]
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        DirectButton(
+            image=load_images_btn('exitRight', 'gray'), scale=.05,
+            pos=(.06, 1, -h + .06),
+            parent=self._frm, command=self.destroy, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        self.accept('item-rototranslated', self.__on_item_rototranslated)
+        DirectButton(
+            image=load_images_btn('trashcan', 'gray'), scale=.05,
+            pos=(.18, 1, -h + .06),
+            parent=self._frm, command=self.__delete_item, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+
+    def __add_row(self, label, text, callback):
+        tw = 10
+        t = OnscreenText(
+            label,
+            pos=(.03, self.__z), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            wordwrap=20, align=TextNode.ALeft)
+        e = DirectEntry(
+            scale=self._common['scale'],
+            pos=(.30, 1, self.__z),
+            entryFont=self._font,
+            width=tw,
+            cursorKeys=True,
+            frameColor=self._common['frameColor'],
+            initialText=text,
+            parent=self._frm,
+            text_fg=self._common['text_fg'],
+            command=callback)
+        self.__pos_mgr[f'editor_inspector_test_{label}'] = e.pos_pixel()
+        self.__z -= .1
+        return t, e
+
+    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 __on_item_rototranslated(self, np):
+        pos = np.pos2d_pixel()
+        self.__entries.position.set('%s %s' % (str(round(pos[0], 3)), str(round(pos[1], 3))))
+        self.__item.json['position'] = list(pos)
+
+    def __delete_item(self):
+        messenger.send('editor-inspector-delete', [self.__item])
+        self.destroy()
+
+    @property
+    def item(self):
+        return self.__item
+
+    def on_edit_position(self, txt):
+        x, z = map(float, txt.split())
+        self.__item.position = [x, 0, z]
+
+    def on_edit_id(self, txt):
+        self.__item.id = txt
+
+    def __actually_close(self, arg):
+        self.__entries.strategy.set('')
+        self.__entries.strategy_args.set('')
+        self.__dialog.cleanup()
+
+    def destroy(self):
+        self._frm.destroy()
+        self.ignore('item-rototranslated')
+        messenger.send('editor-inspector-destroy')
+
+
+class WorldSpaceInspector(DirectObject):
+
+    def __init__(self, item, all_items, pos_mgr):
+        info('WorldSpaceInspector')
+        super().__init__()
+        self.__item = item
+        self.__all_items = all_items
+        self.__pos_mgr = pos_mgr
+        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)
+        self._common = {
+            'scale': .046,
+            'text_font': self._font,
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': loader.load_sfx(
+                'assets/audio/sfx/rollover.ogg'),
+            'clickSound': loader.load_sfx(
+                'assets/audio/sfx/click.ogg')}
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        w, h = .8, .36
+        self._frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                                frameSize=(0, w, -h, 0),
+                                parent=base.a2dTopRight,
+                                pos=(-w, 0, 0))
+        self.__z = -.08
+        p = self.__item._np.get_pos()
+        _id = ''
+        if 'id' in self.__item.json:
+            _id = self.__item.json['id']
+        t, pos_entry = self.__add_row('position', _('position'), f'{round(p.x, 3)} {round(p.z, 3)}', self.on_edit_position, _('position (e.g. 0.1 2.3 4.5)'))
+        t, id_entry = self.__add_row('id', _('id'), _id, self.on_edit_id, _('id of the item (for the strategies)'))
+        fields = ['position', 'id']
+        Entries = namedtuple('Entries', fields)
+        self.__entries = Entries(pos_entry, id_entry)
+        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]
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        b = DirectButton(
+            image=load_images_btn('exitRight', 'gray'), scale=.05,
+            pos=(.06, 1, -h + .06),
+            parent=self._frm, command=self.destroy, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_inspector_test_close'] = b.pos_pixel()
+        b.set_tooltip(_('Close'), *tooltip_args)
+        self.accept('item-rototranslated', self.__on_item_rototranslated)
+        b = DirectButton(
+            image=load_images_btn('trashcan', 'gray'), scale=.05,
+            pos=(.18, 1, -h + .06),
+            parent=self._frm, command=self.__delete_item, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_inspector_test_delete'] = b.pos_pixel()
+        b.set_tooltip(_('Delete the item'), *tooltip_args)
+
+    def __add_row(self, id_, label, text, callback, tooltip):
+        tw = 10
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        t = OnscreenText(
+            label,
+            pos=(.03, self.__z), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            wordwrap=20, align=TextNode.ALeft)
+        e = DirectEntry(
+            scale=self._common['scale'],
+            pos=(.30, 1, self.__z),
+            entryFont=self._font,
+            width=tw,
+            cursorKeys=True,
+            frameColor=self._common['frameColor'],
+            initialText=text,
+            parent=self._frm,
+            text_fg=self._common['text_fg'],
+            command=callback)
+        e.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        e.set_tooltip(tooltip, *tooltip_args)
+        self.__pos_mgr[f'editor_inspector_test_{id_}'] = e.pos_pixel()
+        self.__z -= .1
+        return t, e
+
+    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 __on_item_rototranslated(self, np):
+        pos = np.get_pos()
+        self.__entries.position.set('%s %s' % (str(round(pos.x, 3)), str(round(pos.z, 3))))
+        self.__item.json['position'] = list(pos)
+
+    def __delete_item(self):
+        messenger.send('editor-inspector-delete', [self.__item])
+        self.destroy()
+
+    @property
+    def item(self):
+        return self.__item
+
+    def on_edit_position(self, txt):
+        x, z = map(float, txt.split())
+        self.__item.position = [x, 0, z]
+
+    def on_edit_id(self, txt):
+        self.__item.id = txt
+
+    def __actually_close(self, arg):
+        self.__entries.strategy.set('')
+        self.__entries.strategy_args.set('')
+        self.__dialog.cleanup()
+
+    def destroy(self):
+        self._frm.destroy()
+        self.ignore('item-rototranslated')
+        messenger.send('editor-inspector-destroy')
diff --git a/pmachines/editor/scene.py b/pmachines/editor/scene.py
new file mode 100644 (file)
index 0000000..fff5613
--- /dev/null
@@ -0,0 +1,441 @@
+from copy import deepcopy
+from json import dumps
+from logging import info
+import hashlib
+from panda3d.core import Texture, TextNode, LPoint3f
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.DirectGui import DirectButton, DirectEntry, \
+    YesNoDialog, DirectOptionMenu, DirectFrame
+from direct.gui.DirectGuiGlobals import FLAT, NORMAL
+from direct.gui.OnscreenText import OnscreenText
+from direct.showbase.DirectObject import DirectObject
+from pmachines.items.test_item import PixelSpaceTestItem, WorldSpaceTestItem
+from pmachines.editor.scene_list import SceneList
+from pmachines.editor.inspector import Inspector, PixelSpaceInspector, WorldSpaceInspector
+from pmachines.editor.start_items import StartItems
+from ya2.utils.gfx import Point, DirectGuiMixin
+from pmachines.editor.augmented_frame import AugmentedDirectFrame
+from ya2.utils.gui.base_page import DirectOptionMenuTestable
+from pmachines.items.basketball import Basketball
+from pmachines.items.box import Box
+from pmachines.items.domino import Domino
+from pmachines.items.shelf import Shelf
+from pmachines.items.teetertooter import TeeterTooter
+from pmachines.items.item import FixedStrategy, StillStrategy
+from pmachines.items.domino import UpStrategy, DownStrategy
+from pmachines.items.box import HitStrategy
+
+
+class SceneEditor(DirectObject):
+
+    def __init__(self, json, json_name, context, add_item, items, world, mouse_plane_node, pos_mgr):
+        super().__init__()
+        self.__items = items
+        self.__json = json
+        self.__world = world
+        self.__mouse_plane_node = mouse_plane_node
+        self.__pos_mgr = pos_mgr
+        self.__dialog = None
+        if not json_name:
+            self.__json = json = {
+                'name': '',
+                'instructions': '',
+                'version': '',
+                'items': [],
+                'start_items': [],
+                'test_items': {
+                    'pixel_space': [],
+                    'world_space': []}}
+        self.__inspector = None
+        self.__context = context
+        self.__add_item = add_item
+        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)
+        self.__item_classes = [Basketball, Box, Domino, Shelf, TeeterTooter]
+        self.__item_strategy_classes = [FixedStrategy, StillStrategy, HitStrategy, UpStrategy, DownStrategy]
+        self._common = {
+            'scale': .046,
+            'text_font': self._font,
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': loader.load_sfx(
+                'assets/audio/sfx/rollover.ogg'),
+            'clickSound': loader.load_sfx(
+                'assets/audio/sfx/click.ogg')}
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        w, h, tw, l = 1.8, 1.1, 30, .36
+        self._frm = AugmentedDirectFrame(frameColor=(.4, .4, .4, .06),
+                                         frameSize=(0, w, 0, h),
+                                         parent=base.a2dBottomCenter,
+                                         pos=(-w/2, 0, 0),
+                                         delta_drag=LPoint3f(0, 0, h),
+                                         collapse_pos=(.06, 1, .94),
+                                         pos_mgr=self.__pos_mgr,
+                                         frame_name='scene')
+        OnscreenText(
+            _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            align=TextNode.A_right)
+        self.__filenamename_entry = DirectEntry(
+            scale=self._common['scale'],
+            pos=(l, 1, h - .1),
+            entryFont=self._font,
+            width=tw,
+            frameColor=self._common['frameColor'],
+            initialText=json_name,
+            parent=self._frm,
+            text_fg=self._common['text_fg'])
+        self.__filenamename_entry['text_fg'] = (1, 0, 0, 1)
+        self.__filenamename_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        self.__filenamename_entry.set_tooltip(_('The name of the file without the extension'), *tooltip_args)
+        self.__pos_mgr['editor_scene_filename'] = self.__filenamename_entry.pos_pixel()
+        OnscreenText(
+            _('Name'), pos=(l - .03, h - .2), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            align=TextNode.A_right)
+        self.__name_entry = DirectEntry(
+            scale=self._common['scale'],
+            pos=(l, 1, h - .2),
+            entryFont=self._font,
+            width=tw,
+            frameColor=self._common['frameColor'],
+            initialText=json['name'],
+            parent=self._frm,
+            text_fg=self._common['text_fg'])
+        self.__name_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        self.__name_entry.set_tooltip(_('The title of the scene'), *tooltip_args)
+        self.__pos_mgr['editor_scene_name'] = self.__name_entry.pos_pixel()
+        OnscreenText(
+            _('Background'), pos=(l - .03, h - .3), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            align=TextNode.A_right)
+        self.__background_entry = DirectEntry(
+            scale=self._common['scale'],
+            pos=(l, 1, h - .3),
+            entryFont=self._font,
+            width=tw,
+            frameColor=self._common['frameColor'],
+            initialText=json['background'],
+            parent=self._frm,
+            text_fg=self._common['text_fg'])
+        self.__background_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        self.__background_entry.set_tooltip(_('The name of the background'), *tooltip_args)
+        self.__pos_mgr['editor_scene_background'] = self.__background_entry.pos_pixel()
+        OnscreenText(
+            _('Description'), pos=(l - .03, h - .4), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            align=TextNode.A_right)
+        def add_line_break(txt, entry):
+            curpos = entry.node().getCursorPosition()
+            entry.set(txt[:curpos]+ "\n" + txt[curpos:])
+            entry.node().setCursorPosition(curpos+1)
+            entry['focus']=1
+        self.__instructions_entry = DirectEntry(
+            scale=self._common['scale'],
+            pos=(l, 1, h - .4),
+            entryFont=self._font,
+            width=tw,
+            numLines=12,
+            cursorKeys=True,
+            frameColor=self._common['frameColor'],
+            initialText=json['instructions'].replace('\\n', '\n'),
+            parent=self._frm,
+            text_fg=self._common['text_fg'])
+        self.__instructions_entry['command'] = add_line_break
+        self.__instructions_entry['extraArgs'] = [self.__instructions_entry]
+        self.__instructions_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        self.__instructions_entry.set_tooltip(_('The description of the scene'), *tooltip_args)
+        self.__pos_mgr['editor_scene_instructions'] = self.__instructions_entry.pos_pixel()
+        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]
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        b = DirectButton(
+            image=load_images_btn('exitRight', 'gray'), scale=.05,
+            pos=(.06, 1, .06),
+            parent=self._frm, command=self.__on_close, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self.__pos_mgr['editor_close'] = b.pos_pixel()
+        b.set_tooltip(_('Close the scene editor'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('save', 'gray'), scale=.05,
+            pos=(.06, 1, .18),
+            parent=self._frm, command=self.__on_save, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self.__pos_mgr['editor_save'] = b.pos_pixel()
+        b.set_tooltip(_('Save the scene'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('menuList', 'gray'), scale=.05,
+            pos=(.06, 1, .30),
+            parent=self._frm, command=self.__on_scene_list, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self.__pos_mgr['editor_sorting'] = b.pos_pixel()
+        b.set_tooltip(_('Set the sorting of the scenes'), *tooltip_args)
+        # item_modules = glob('pmachines/items/*.py')
+        # item_modules = [basename(i)[:-3] for i in item_modules]
+        self.__new_items = {}
+        # for item_module in item_modules:
+        #     mod_name = 'pmachines.items.' + item_module
+        #     for member in import_module(mod_name).__dict__.values():
+        #         if isclass(member) and issubclass(member, Item) and \
+        #                 member != Item:
+        #             self.__new_items[member.__name__] = member
+        for i in self.__item_classes: self.__new_items[i.__name__] = i
+        OnscreenText(
+            _('new item'), pos=(.02, .46), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            align=TextNode.A_left)
+        items = list(self.__new_items.keys())
+        def new_item_test_set(comps):
+            item_labels = [f'new_item_{i.lower()}' for i in items]
+            for i in item_labels:
+                if i in self.__pos_mgr:
+                    del self.__pos_mgr[i]
+            for l, b in zip(item_labels, comps):
+                b.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
+                p = b.pos_pixel()
+                self.__pos_mgr[l] = (p[0] + 5, p[1])
+        b = DirectOptionMenuTestable(
+            scale=.05,
+            text=_('new item'), pos=(.02, 1, .4), items=items,
+            parent=self._frm, command=self.__on_new_item, state=NORMAL,
+            relief=FLAT, item_relief=FLAT,
+            frameColor=fcols[0], item_frameColor=fcols[0],
+            popupMenu_frameColor=fcols[0],
+            popupMarker_frameColor=fcols[0],
+            text_font=self._font, text_fg=(.9, .9, .9, 1),
+            highlightColor=(.9, .9, .9, .9),
+            item_text_font=self._font, item_text_fg=(.9, .9, .9, 1),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
+        b._show_cb = new_item_test_set
+        b.set_tooltip(_('Create a new item'), *tooltip_args)
+        self.__pos_mgr['editor_new_item'] = b.pos_pixel()
+        b = DirectButton(
+            image=load_images_btn('start_items', 'gray'), scale=.05,
+            pos=(.06, 1, .58),
+            parent=self._frm, command=self.__on_start_items, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self.__pos_mgr['editor_start'] = b.pos_pixel()
+        b.set_tooltip(_('Initial items'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('plus', 'gray'), scale=.05,
+            pos=(.06, 1, .7),
+            parent=self._frm, command=self.__on_new_scene, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self.__pos_mgr['editor_new'] = b.pos_pixel()
+        b.set_tooltip(_('New scene'), *tooltip_args)
+        self.__test_items = []
+        self.__set_test_items()
+        self.__start_items = self.__scene_list = None
+        messenger.send('editor-start')
+        self.accept('editor-item-click', self.__on_item_click)
+        self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
+        self.accept('editor-start-items-save', self.__on_start_items_save)
+        self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
+
+    def __set_test_items(self):
+        for pixel_space_item in self.__json['test_items']['pixel_space']:
+            print(pixel_space_item['id'], pixel_space_item['position'])
+            pos_pixel = pixel_space_item['position']
+            win_res = base.win.getXSize(), base.win.getYSize()
+            pos_win = (pos_pixel[0] / win_res[0] * 2 - 1,
+                       1 - pos_pixel[1] / win_res[1] * 2)
+            p_from, p_to = Point(pos_win).from_to_points()
+            for hit in self.__world.ray_test_all(p_from, p_to).get_hits():
+                if hit.get_node() == self.__mouse_plane_node:
+                    pos = hit.get_hit_pos()
+            self.__set_test_item(pos, pixel_space_item, PixelSpaceTestItem)
+        for world_space_item in self.__json['test_items']['world_space']:
+            print(world_space_item['id'], world_space_item['position'])
+            self.__set_test_item(world_space_item['position'], world_space_item, WorldSpaceTestItem)
+
+    def __set_test_item(self, pos, json, item_class):
+        test_item = item_class(
+            self.__context.world,
+            self.__context.plane_node,
+            self.__context.cb_inst,
+            self.__context.curr_bottom,
+            self.__context.repos,
+            json,
+            pos=(pos[0], 0, pos[-1]),
+            model_scale=.2)
+        self.__test_items += [test_item]
+
+    def __on_close(self):
+        self.__json['name'] = self.__name_entry.get()
+        self.__json['background'] = self.__background_entry.get()
+        self.__json['instructions'] = self.__instructions_entry.get()
+        if self.__compute_hash() == self.__json['version']:
+            self.destroy()
+        else:
+            self.__dialog = YesNoDialog(dialogName='Unsaved changes',
+                                        text=_('You have unsaved changes. Really quit?'),
+                                        command=self.__actually_close)
+            self.__dialog['frameColor'] = (.4, .4, .4, .14)
+            self.__dialog['relief'] = FLAT
+            self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
+            self.__dialog.component('text0')['font'] = self._font
+            for i, b in enumerate(self.__dialog.buttonList):
+                b['frameColor'] = (.4, .4, .4, .14)
+                b.component('text0')['fg'] = (.9, .9, .9, 1)
+                b.component('text0')['font'] = self._font
+                b.component('text1')['fg'] = (.9, .1, .1, 1)
+                b.component('text1')['font'] = self._font
+                b.component('text2')['fg'] = (.9, .9, .1, 1)
+                b.component('text2')['font'] = self._font
+                b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+                yes_no = 'no' if i else 'yes'
+                self.__pos_mgr[f'editor_save_dialog_{yes_no}'] = b.pos_pixel()
+
+    def __on_new_item(self, item):
+        info(f'new {item}')
+        item_json = {}
+        scale = 1
+        if item in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
+            scale = .2
+        _item = self.__new_items[item](
+            self.__context.world,
+            self.__context.plane_node,
+            self.__context.cb_inst,
+            self.__context.curr_bottom,
+            self.__context.repos,
+            item_json,
+            model_scale=scale)
+        _item._Item__editing = True
+        self.__add_item(_item)
+        item_json['class'] = _item.__class__.__name__
+        item_json['position'] = list(_item._np.get_pos())
+        if item == 'PixelSpaceTestItem':
+            del item_json['class']
+            self.__json['test_items']['pixel_space'] += [item_json]
+        elif item == 'WorldSpaceTestItem':
+            del item_json['class']
+            self.__json['test_items']['world_space'] += [item_json]
+        else:
+            self.__json['items'] += [item_json]
+
+    def __actually_close(self, arg):
+        if arg:
+            self.destroy()
+            messenger.send('editor-stop')
+        if self.__dialog:
+            self.__dialog.cleanup()
+        self.__dialog = None
+
+    def __on_save(self):
+        self.__json['name'] = self.__name_entry.get()
+        self.__json['background'] = self.__background_entry.get()
+        self.__json['instructions'] = self.__instructions_entry.get()
+        self.__json['version'] = self.__compute_hash()
+        json_name = self.__filenamename_entry.get()
+        with open('assets/scenes/%s.json' % json_name, 'w') as f:
+            f.write(dumps(self.__json, indent=2, sort_keys=True))
+
+    def __on_scene_list(self):
+        self.__scene_list = SceneList(self.__pos_mgr)
+
+    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 __compute_hash(self):
+        new_dict = deepcopy(self.__json)
+        del new_dict['version']
+        new_dict_str = str(dumps(new_dict, indent=2, sort_keys=True))
+        h = hashlib.new('sha256')
+        h.update(new_dict_str.encode())
+        return h.hexdigest()[:12]
+
+    def __on_item_click(self, item):
+        if self.__inspector and self.__inspector.item == item: return
+        if self.__inspector:
+            self.__inspector.destroy()
+        if item.__class__ == PixelSpaceTestItem:
+            self.__inspector = PixelSpaceInspector(item, self.__items)
+        elif item.__class__ == WorldSpaceTestItem:
+            self.__inspector = WorldSpaceInspector(item, self.__items, self.__pos_mgr)
+        else:
+            self.__inspector = Inspector(item, self.__items, self.__pos_mgr, self.__item_strategy_classes)
+
+    def __on_inspector_destroy(self):
+        self.__inspector = None
+
+    def __on_new_scene(self):
+        self.destroy()
+        messenger.send('editor-stop')
+        messenger.send('new_scene')
+
+    def __on_start_items(self):
+        self.__start_items = StartItems(self.__json['start_items'], self.__pos_mgr, self.__item_classes, self.__item_strategy_classes)
+
+    def __on_start_items_save(self, start_items):
+        self.__json['start_items'] = start_items
+        self.__on_save()
+
+    def __on_start_items_destroy(self):
+        self.__start_items = None
+
+    @property
+    def test_items(self):
+        return self.__test_items
+
+    def destroy(self):
+        self._frm.destroy()
+        if self.__inspector:
+            self.__inspector.destroy()
+        self.ignore('editor-item-click')
+        self.ignore('editor-inspector-destroy')
+        self.ignore('editor-start-items-save')
+        self.ignore('editor-start-items-destroy')
+        if self.__dialog: self.__actually_close(False)
+        for t in self.__test_items: t.destroy()
+        self.__test_items = []
+        if self.__start_items:
+            self.__start_items.destroy()
+        if self.__scene_list:
+            self.__scene_list.destroy()
diff --git a/pmachines/editor/scene_list.py b/pmachines/editor/scene_list.py
new file mode 100644 (file)
index 0000000..26f95af
--- /dev/null
@@ -0,0 +1,147 @@
+from json import dumps, loads
+from panda3d.core import Texture, LPoint3f
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.DirectGui import DirectButton, DirectEntry, YesNoDialog
+from direct.gui.DirectGuiGlobals import FLAT, NORMAL
+from direct.gui.OnscreenText import OnscreenText
+from pmachines.editor.augmented_frame import AugmentedDirectFrame
+from ya2.utils.gfx import DirectGuiMixin
+
+
+class SceneList:
+
+    def __init__(self, pos_mgr):
+        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)
+        self._common = {
+            'scale': .046,
+            'text_font': self._font,
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': loader.load_sfx(
+                'assets/audio/sfx/rollover.ogg'),
+            'clickSound': loader.load_sfx(
+                'assets/audio/sfx/click.ogg')}
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        w, h, tw = .8, 1.05, 16
+        self._frm = AugmentedDirectFrame(frameColor=(.4, .4, .4, .06),
+                                         frameSize=(0, w, -h, 0),
+                                         parent=base.a2dTopCenter,
+                                         pos=(-w/2, 0, 0),
+                                         delta_drag=LPoint3f(0, 0, -h),
+                                         collapse_pos=(w - .06, 1, -h + .06),
+                                         pos_mgr=pos_mgr,
+                                         frame_name='sorting')
+        OnscreenText(
+            _('Write the file names (without the extension), one file for each line'),
+            pos=(w/2, -.08), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            wordwrap=20)
+        def add_line_break(txt, entry):
+            curpos = entry.node().getCursorPosition()
+            entry.set(txt[:curpos]+ "\n" + txt[curpos:])
+            entry.node().setCursorPosition(curpos+1)
+            entry['focus']=1
+        with open('assets/scenes/index.json') as f:
+            self.__json = loads(f.read())
+        self.__list_entry = DirectEntry(
+            scale=self._common['scale'],
+            pos=(.03, 1, -.24),
+            entryFont=self._font,
+            width=tw,
+            numLines=14,
+            cursorKeys=True,
+            frameColor=self._common['frameColor'],
+            initialText='\n'.join(self.__json['list']),
+            parent=self._frm,
+            text_fg=self._common['text_fg'])
+        self.__list_entry['command'] = add_line_break
+        self.__list_entry['extraArgs'] = [self.__list_entry]
+        self.__list_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        self.__list_entry.set_tooltip(_('the list of the scenes in the proper order'), *tooltip_args)
+        pos_mgr['editor_sorting_text'] = self.__list_entry.pos_pixel()
+        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]
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        b = DirectButton(
+            image=load_images_btn('exitRight', 'gray'), scale=.05,
+            pos=(.19, 1, -.99),
+            parent=self._frm, command=self.__on_close, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_sorting_close'] = b.pos_pixel()
+        b.set_tooltip(_('Close the scene editor'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('save', 'gray'), scale=.05,
+            pos=(.06, 1, -.99),
+            parent=self._frm, command=self.__on_save, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_sorting_save'] = b.pos_pixel()
+        b.set_tooltip(_('Save the scene list'), *tooltip_args)
+
+    def __on_close(self):
+        new_list = self.__list_entry.get().split('\n')
+        new_list = [e for e in new_list if e]
+        if new_list == self.__json['list']:
+            self._frm.destroy()
+        else:
+            self.__dialog = YesNoDialog(dialogName='Unsaved changes',
+                                        text=_('You have unsaved changes. Really quit?'),
+                                        command=self.__actually_close)
+            self.__dialog['frameColor'] = (.4, .4, .4, .14)
+            self.__dialog['relief'] = FLAT
+            self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
+            self.__dialog.component('text0')['font'] = self._font
+            for b in self.__dialog.buttonList:
+                b['frameColor'] = (.4, .4, .4, .14)
+                b.component('text0')['fg'] = (.9, .9, .9, 1)
+                b.component('text0')['font'] = self._font
+                b.component('text1')['fg'] = (.9, .1, .1, 1)
+                b.component('text1')['font'] = self._font
+                b.component('text2')['fg'] = (.9, .9, .1, 1)
+                b.component('text2')['font'] = self._font
+
+    def __actually_close(self, arg):
+        if arg:
+            self._frm.destroy()
+        self.__dialog.cleanup()
+
+    def __on_save(self):
+        new_list = self.__list_entry.get().split('\n')
+        new_list = [e for e in new_list if e]
+        self.__json['list'] = new_list
+        with open('assets/scenes/index.json', 'w') as f:
+            f.write(dumps(self.__json, indent=2, sort_keys=True))
+
+    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 destroy(self):
+        self._frm.destroy()
diff --git a/pmachines/editor/start_items.py b/pmachines/editor/start_items.py
new file mode 100644 (file)
index 0000000..52cbd1c
--- /dev/null
@@ -0,0 +1,384 @@
+from collections import namedtuple
+from panda3d.core import Texture, TextNode, LPoint3f
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.DirectGui import DirectButton, DirectEntry, DirectOptionMenu, OkDialog, DirectFrame
+from direct.gui.DirectGuiGlobals import FLAT, NORMAL
+from direct.gui.OnscreenText import OnscreenText
+from direct.showbase.DirectObject import DirectObject
+from ya2.utils.gfx import DirectGuiMixin
+from pmachines.editor.augmented_frame import AugmentedDirectFrame
+from ya2.utils.gui.base_page import DirectOptionMenuTestable
+
+
+class StartItems(DirectObject):
+
+    def __init__(self, json_items, pos_mgr, item_classes, strategy_items):
+        super().__init__()
+        self.__items = json_items
+        self.__json = self.__items[0]
+        self.__pos_mgr = pos_mgr
+        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)
+        self._common = {
+            'scale': .046,
+            'text_font': self._font,
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': loader.load_sfx(
+                'assets/audio/sfx/rollover.ogg'),
+            'clickSound': loader.load_sfx(
+                'assets/audio/sfx/click.ogg')}
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        w, h = .8, 1.04
+        self._frm = AugmentedDirectFrame(frameColor=(.4, .4, .4, .06),
+                                         frameSize=(0, w, -h, 0),
+                                         parent=base.a2dTopLeft,
+                                         pos=(0, 0, 0),
+                                         delta_drag=LPoint3f(w, 0, -h),
+                                         collapse_pos=(w - .06, 1, -h + .06),
+                                         pos_mgr=pos_mgr,
+                                         frame_name='start')
+        self.__z = -.08
+        # item_modules = glob('pmachines/items/*.py')
+        # item_modules = [basename(i)[:-3] for i in item_modules]
+        # new_items = ['']
+        # for item_module in item_modules:
+        #     mod_name = 'pmachines.items.' + item_module
+        #     for member in import_module(mod_name).__dict__.values():
+        #         if isclass(member) and issubclass(member, Item) and \
+        #                 member != Item:
+        #             new_items = list(set(new_items + [member.__name__]))
+        item_names = [i.__name__ for i in item_classes]
+        t, item_class_entry = self.__add_row_option(_('class'), '', item_names, self.on_edit_class, _('class of the item'))
+
+        def item_class_set(comps):
+            class_labels = [f'start_class_{i.lower()}' for i in item_names]
+            for i in class_labels:
+                if i in self.__pos_mgr:
+                    del self.__pos_mgr[i]
+            for l, b in zip(class_labels, comps):
+                b.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
+                p = b.pos_pixel()
+                self.__pos_mgr[l] = (p[0] + 5, p[1])
+        item_class_entry._show_cb = item_class_set
+        self.__pos_mgr['editor_start_class'] = item_class_entry.pos_pixel()
+
+        t, count_entry = self.__add_row('count', _('count'), '', self.on_edit_count, _('number of the items'))
+        t, scale_entry = self.__add_row('scale', _('scale'), '', self.on_edit_scale, _('scale (e.g. 1.2)'))
+        t, mass_entry = self.__add_row('mass', _('mass'), '', self.on_edit_mass, _('mass (default 1)'))
+        t, restitution_entry = self.__add_row('restitution', _('restitution'), '', self.on_edit_restitution, _('restitution (default 0.5)'))
+        t, friction_entry = self.__add_row('friction', _('friction'), '', self.on_edit_friction, _('friction (default 0.5)'))
+        t, id_entry = self.__add_row('id', _('id'), '', self.on_edit_id, _('id'))
+        # strategy_items = ['']
+        # for item_module in item_modules:
+        #     mod_name = 'pmachines.items.' + item_module
+        #     for member in import_module(mod_name).__dict__.values():
+        #         if isclass(member) and issubclass(member, ItemStrategy) and \
+        #                 member != ItemStrategy:
+        #             strategy_items = list(set(strategy_items + [member.__name__]))
+        strategy_names = [s.__name__ for s in strategy_items]
+        t, strategy_entry = self.__add_row_option(_('strategy'), '', strategy_names, self.on_edit_strategy, _('the strategy of the item'))
+
+        def strategy_set(comps):
+            strategy_labels = [f'start_strategy_{i.lower()}' for i in strategy_names]
+            for i in strategy_labels:
+                if i in self.__pos_mgr:
+                    del self.__pos_mgr[i]
+            for l, b in zip(strategy_labels, comps):
+                b.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
+                p = b.pos_pixel()
+                self.__pos_mgr[l] = (p[0] + 5, p[1])
+        strategy_entry._show_cb = strategy_set
+        p = strategy_entry.pos_pixel()
+        self.__pos_mgr['editor_start_strategy'] = (p[0] + 5, p[1])
+
+        t, strategy_args_entry = self.__add_row('strategy_args', _('strategy_args'), '', self.on_edit_strategy_args, _('the arguments of the strategy'))
+        fields = ['scale', 'mass', 'restitution', 'friction', 'id', 'strategy', 'strategy_args', 'item_class', 'count']
+        Entries = namedtuple('Entries', fields)
+        self.__entries = Entries(scale_entry, mass_entry, restitution_entry, friction_entry, id_entry, strategy_entry, strategy_args_entry, item_class_entry, count_entry)
+        self.__set(self.__json)
+        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]
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        b = DirectButton(
+            image=load_images_btn('exitRight', 'gray'), scale=.05,
+            pos=(.54, 1, -h + .06),
+            parent=self._frm, command=self.destroy, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_start_close'] = b.pos_pixel()
+        b.set_tooltip(_('Close'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('save', 'gray'), scale=.05,
+            pos=(.42, 1, -h + .06),
+            parent=self._frm, command=self.__save, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_start_save'] = b.pos_pixel()
+        b.set_tooltip(_('Save'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('trashcan', 'gray'), scale=.05,
+            pos=(.3, 1, -h + .06),
+            parent=self._frm, command=self.__delete_item, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_start_delete'] = b.pos_pixel()
+        b.set_tooltip(_('Delete'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('plus', 'gray'), scale=.05,
+            pos=(.06, 1, -h + .06),
+            parent=self._frm, command=self.__new_item, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_start_new'] = b.pos_pixel()
+        b.set_tooltip(_('New'), *tooltip_args)
+        b = DirectButton(
+            image=load_images_btn('right', 'gray'), scale=.05,
+            pos=(.18, 1, -h + .06),
+            parent=self._frm, command=self.__next_item, state=NORMAL, relief=FLAT,
+            frameColor=fcols[0],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos_mgr['editor_start_next'] = b.pos_pixel()
+        b.set_tooltip(_('Next'), *tooltip_args)
+
+    def __add_row(self, id_, label, text, callback, tooltip):
+        tw = 10
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        t = OnscreenText(
+            label,
+            pos=(.03, self.__z), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            wordwrap=20, align=TextNode.ALeft)
+        e = DirectEntry(
+            scale=self._common['scale'],
+            pos=(.30, 1, self.__z),
+            entryFont=self._font,
+            width=tw,
+            cursorKeys=True,
+            frameColor=self._common['frameColor'],
+            initialText=text,
+            parent=self._frm,
+            text_fg=self._common['text_fg'],
+            command=callback)
+        e.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
+        e.set_tooltip(tooltip, *tooltip_args)
+        self.__pos_mgr[f'editor_start_{id_}'] = e.pos_pixel()
+        self.__z -= .1
+        return t, e
+
+    def __add_row_option(self, label, text, items, callback, tooltip):
+        tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
+        t = OnscreenText(
+            label,
+            pos=(.03, self.__z), parent=self._frm,
+            font=self._common['text_font'],
+            scale=self._common['scale'],
+            fg=self._common['text_fg'],
+            wordwrap=20, align=TextNode.ALeft)
+        e = DirectOptionMenuTestable(
+            scale=self._common['scale'],
+            initialitem=text,
+            pos=(.30, 1, self.__z),
+            items=items,
+            parent=self._frm,
+            command=callback,
+            state=NORMAL,
+            relief=FLAT,
+            item_relief=FLAT,
+            frameColor=self._common['frameColor'],
+            item_frameColor=self._common['frameColor'],
+            popupMenu_frameColor=self._common['frameColor'],
+            popupMarker_frameColor=self._common['frameColor'],
+            text_font=self._font,
+            text_fg=self._common['text_fg'],
+            highlightColor=(.9, .9, .9, .9),
+            item_text_font=self._font,
+            item_text_fg=self._common['text_fg'],
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        e.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
+        e.set_tooltip(tooltip, *tooltip_args)
+        self.__z -= .1
+        return t, e
+
+    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 on_edit_scale(self, txt):
+        if txt:
+            self.__json['model_scale'] = float(txt)
+        else:
+            del self.__json['model_scale']
+
+    def on_edit_mass(self, txt):
+        if txt:
+            self.__json['mass'] = float(txt)
+        else:
+            del self.__json['mass']
+
+    def on_edit_restitution(self, txt):
+        if txt:
+            self.__json['restitution'] = float(txt)
+        else:
+            del self.__json['restitution']
+
+    def on_edit_friction(self, txt):
+        if txt:
+            self.__json['friction'] = float(txt)
+        else:
+            del self.__json['friction']
+
+    def on_edit_id(self, txt):
+        if txt:
+            self.__json['id'] = txt
+        else:
+            del self.__json['id']
+
+    def on_edit_strategy(self, txt):
+        if txt:
+            self.__json['strategy'] = txt
+            self.__entries.strategy_args.set('')
+
+    def on_edit_strategy_args(self, txt):
+        if txt:
+            self.__json['strategy_args'] = txt
+        else:
+            del self.__json['strategy_args']
+
+    def on_edit_class(self, txt):
+        if txt:
+            self.__json['class'] = txt
+
+    def on_edit_count(self, txt):
+        if txt:
+            self.__json['count'] = int(txt)
+        else:
+            del self.__json['count']
+
+    def __new_item(self):
+        curr_index = self.__items.index(self.__json)
+        self.__json = {}
+        curr_index = (curr_index + 1) % len(self.__items)
+        self.__items.insert(curr_index, self.__json)
+        self.__set(self.__json)
+        import pprint
+        pprint.pprint(self.__items)
+
+    def __next_item(self):
+        curr_index = self.__items.index(self.__json)
+        self.__json = self.__items[(curr_index + 1) % len(self.__items)]
+        self.__set(self.__json)
+        import pprint
+        pprint.pprint(self.__items)
+
+    def __delete_item(self):
+        curr_index = self.__items.index(self.__json)
+        if curr_index == len(self.__items): curr_index = 0
+        self.__items.remove(self.__json)
+        if not self.__items:
+            self.__items = [{}]
+        self.__json = self.__items[curr_index]
+        self.__set(self.__json)
+        import pprint
+        pprint.pprint(self.__items)
+
+    def __set(self, json):
+        if 'model_scale' in json:
+            self.__entries.scale.set(str(json['model_scale']))
+        else:
+            self.__entries.scale.set('')
+        if 'mass' in json:
+            self.__entries.mass.set(str(json['mass']))
+        else:
+            self.__entries.mass.set('')
+        if 'restitution' in json:
+            self.__entries.restitution.set(str(json['restitution']))
+        else:
+            self.__entries.restitution.set('')
+        if 'friction' in json:
+            self.__entries.friction.set(str(json['friction']))
+        else:
+            self.__entries.friction.set('')
+        if 'id' in json:
+            self.__entries.id.set(str(json['id']))
+        else:
+            self.__entries.id.set('')
+        if 'strategy' in json:
+            self.__entries.strategy.set(str(json['strategy']))
+        else:
+            self.__entries.strategy.set('')
+        if 'strategy_args' in json:
+            self.__entries.strategy_args.set(str(json['strategy_args']))
+        else:
+            self.__entries.strategy_args.set('')
+        if 'class' in json:
+            self.__entries.item_class.set(str(json['class']))
+        else:
+            self.__entries.item_class.set('')
+        if 'count' in json:
+            self.__entries.count.set(str(json['count']))
+        else:
+            self.__entries.count.set('')
+        import pprint
+        pprint.pprint(self.__items)
+
+    def __save(self):
+        messenger.send('editor-start-items-save', [self.__items])
+
+    def __show_error_popup(self):
+        self.__dialog = OkDialog(dialogName='Strategy args errors',
+                                    text=_('There are errors in the strategy args.'),
+                                    command=self.__actually_close)
+        self.__dialog['frameColor'] = (.4, .4, .4, .14)
+        self.__dialog['relief'] = FLAT
+        self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
+        self.__dialog.component('text0')['font'] = self._font
+        for b in self.__dialog.buttonList:
+            b['frameColor'] = (.4, .4, .4, .14)
+            b.component('text0')['fg'] = (.9, .9, .9, 1)
+            b.component('text0')['font'] = self._font
+            b.component('text1')['fg'] = (.9, .1, .1, 1)
+            b.component('text1')['font'] = self._font
+            b.component('text2')['fg'] = (.9, .9, .1, 1)
+            b.component('text2')['font'] = self._font
+
+    def __actually_close(self, arg):
+        self.__entries.strategy.set('')
+        self.__entries.strategy_args.set('')
+        self.__dialog.cleanup()
+
+    def destroy(self):
+        self._frm.destroy()
+        messenger.send('editor-start-items-destroy')
diff --git a/pmachines/gui/__init__.py b/pmachines/gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pmachines/gui/credits_page.py b/pmachines/gui/credits_page.py
new file mode 100644 (file)
index 0000000..0d1d730
--- /dev/null
@@ -0,0 +1,40 @@
+from sys import platform
+from os import environ, system
+from webbrowser import open_new_tab
+from ya2.utils.gui.base_page import BasePage, TextArgs, ButtonArgs
+
+
+class CreditsPage(BasePage):
+
+    def _build_widgets(self):
+        text_dev = TextArgs(
+            text=_('Code and gfx\n  \1scale\1Flavio Calva\2\n\n\nMusic\n  \1scale\1Stefan Grossmann\2'),
+            pos=(-.9, .55),
+            align='left')
+        self._add_text(text_dev)
+        website_button = ButtonArgs(
+            id='website',
+            text=_('Website'),
+            pos=(-.6, .29),
+            command=self.__on_website,
+            scale=.08)
+        self._add_button(website_button)
+        thanks_text = TextArgs(
+            text=_('Special thanks to:\n  \1scale\1rdb\2\n  \1scale\1Luisa Tenuta\2\n  \1scale\1Damiana Ercolani\2'),
+            pos=(.1, .55),
+            align='left')
+        self._add_text(thanks_text)
+        back_button = ButtonArgs(
+            id='back',
+            text=_('Back'),
+            pos=(0, -.8),
+            command=self._set_page,
+            command_args=['main'])
+        self._add_button(back_button)
+
+    def __on_website(self):
+        if platform.startswith('linux'):
+            environ['LD_LIBRARY_PATH'] = ''
+            system('xdg-open https://www.ya2.it')
+        else:
+            open_new_tab('https://www.ya2.it')
diff --git a/pmachines/gui/main_page.py b/pmachines/gui/main_page.py
new file mode 100644 (file)
index 0000000..318e456
--- /dev/null
@@ -0,0 +1,41 @@
+from sys import exit
+from xmlrpc.client import ServerProxy
+from ya2.utils.gui.base_page import BasePage, ButtonArgs
+
+
+class MainPage(BasePage):
+
+    def _build_widgets(self):
+        play_args = ButtonArgs(
+            id='play',
+            text=_('Play'),
+            pos=(0, .6),
+            command=self._set_page,
+            command_args=['play'])
+        self._add_button(play_args)
+        option_args = ButtonArgs(
+            id='options',
+            text=_('Options'),
+            pos=(0, .2),
+            command=self._set_page,
+            command_args=['options'])
+        self._add_button(option_args)
+        credits_args = ButtonArgs(
+            id='credits',
+            text=_('Credits'),
+            pos=(0, -.2),
+            command=self._set_page,
+            command_args=['credits'])
+        self._add_button(credits_args)
+        exit_args = ButtonArgs(
+            id='exit',
+            text=_('Exit'),
+            pos=(0, -.6),
+            command=self.__on_exit)
+        self._add_button(exit_args)
+        self._rearrange_buttons_width()
+
+    def __on_exit(self):
+        if self._page_info.running_functional_tests:
+            ServerProxy('http://localhost:7000').destroy()
+        exit()
diff --git a/pmachines/gui/menu.py b/pmachines/gui/menu.py
new file mode 100644 (file)
index 0000000..08f06c5
--- /dev/null
@@ -0,0 +1,34 @@
+from pmachines.gui.main_page import MainPage
+from pmachines.gui.play_page import PlayPage
+from pmachines.gui.options_page import OptionsPage
+from pmachines.gui.credits_page import CreditsPage
+from ya2.utils.gui.menu import BaseMenu
+from ya2.utils.gui.cursor import MouseCursorArgs
+
+
+class Menu(BaseMenu):
+
+    def __init__(self, start_scene, set_language, option_file,
+                 gfx_pipeline, scenes,
+                 running_functional_tests, test_positions):
+        c = MouseCursorArgs(
+            'assets/images/buttons/arrowUpLeft.dds',
+            running_functional_tests,
+            (.04, 1, .04),
+            (.5, .5, .5, 1),
+            (.01, .01))
+        super().__init__(c, running_functional_tests, test_positions)
+        self.__start_scene = start_scene
+        self.__set_language = set_language
+        self.__option_file = option_file
+        self.__gfx_pipeline = gfx_pipeline
+        self.__scenes = scenes
+        self.set_page('main')
+
+    def _set_page(self, page_name):
+        match page_name:
+            case 'main': p = MainPage(self._page_info)
+            case 'credits': p = CreditsPage(self._page_info)
+            case 'options': p = OptionsPage(self._page_info, self.__option_file, self.__set_language, self.__gfx_pipeline)
+            case 'play': p = PlayPage(self._page_info, self.__scenes, self.__start_scene)
+        return p
diff --git a/pmachines/gui/options_page.py b/pmachines/gui/options_page.py
new file mode 100644 (file)
index 0000000..8ad16a9
--- /dev/null
@@ -0,0 +1,196 @@
+from logging import info, debug
+from panda3d.core import WindowProperties, LVector2i
+from direct.showbase.DirectObject import DirectObject
+from direct.gui.DirectGui import DirectFrame
+from ya2.utils.gfx import DirectGuiMixin, pos_pixel
+from ya2.utils.audio import AudioTools
+from ya2.utils.gui.base_page import BasePage, OptionMenuArgs, TextArgs, SliderArgs, CheckButtonArgs, ButtonArgs
+
+
+class OptionsPage(BasePage, DirectObject):
+
+    def __init__(self, page_info, option_file, set_language, gfx_pipeline):
+        DirectObject.__init__(self)
+        self.__option_file = option_file
+        self.__set_language = set_language
+        self.__gfx_pipeline = gfx_pipeline
+        self.__enforced_resolution = ''
+        super().__init__(page_info)
+
+    def _build_widgets(self):
+        self.__build_language()
+        self.__build_volume()
+        self.__build_fullscreen()
+        self.__build_resolution()
+        self.__build_aa()
+        self.__build_shadows()
+        self.__build_back()
+
+    def __build_language(self):
+        self._lang_funcs = [lambda: _('English'), lambda: _('Italian')]
+        items = [fnc() for fnc in self._lang_funcs]
+        inititem = {
+            'en': _('English'),
+            'it': _('Italian')
+        }[self.__option_file['settings']['language']]
+
+        def lang_cb(comps):
+            for element in ['english', 'italian']:
+                if element in self._page_info.test_positions:
+                    del self._page_info.test_positions[element]
+            for lng, btn in zip(['english', 'italian'], comps):
+                btn.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
+                pos = btn.pos_pixel()
+                self._page_info.test_positions[lng] = (pos[0] + 5, pos[1])
+        language_menu = OptionMenuArgs(
+            id='languages',
+            text=_('Language'),
+            items=items,
+            initial_item=inititem,
+            pos=(0, .8),
+            command=self.on_language)
+        self._add_option_menu(language_menu, lang_cb)
+
+    def __build_volume(self):
+        t = TextArgs(
+            text=_('Volume'),
+            pos=(-.56, .55),
+            align='left')
+        self._add_text(t)
+        s = SliderArgs(
+            pos=(.3, 1, .57),
+            value=self.__option_file['settings']['volume'],
+            scale=(.28, 1, .5),
+            command=self.on_volume)
+        self._slider = self._add_slider(s)
+
+    def __build_fullscreen(self):
+        c = CheckButtonArgs(
+            id='fullscreen',
+            text=_('Fullscreen'),
+            pos=(0, .3),
+            command=self.on_fullscreen,
+            value=self.__option_file['settings']['fullscreen'])
+        self._add_check_button(c)
+
+    def __build_resolution(self):
+        res = self.__option_file['settings']['resolution']
+        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())]
+        resolutions = list(set(resolutions))
+        resolutions = sorted(resolutions)
+        resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
+        resolutions = ['x'.join(_res) for _res in resolutions]
+        if not res:
+            res = resolutions[-1]
+        def res_cb(comps):
+            for element in ['res_1440x900', 'res_1360x768']:
+                if element in self._page_info.test_positions:
+                    del self._page_info.test_positions[element]
+            for tgt_res in ['1440x900', '1360x768']:
+                for btn in comps:
+                    if btn['text'] == tgt_res:
+                        pos = pos_pixel(btn)
+                        self._page_info.test_positions['res_' + tgt_res] = (pos[0] + 5, pos[1])
+        r = OptionMenuArgs(
+            id='resolutions',
+            text=_('Resolution'),
+            items=resolutions,
+            initial_item=res,
+            pos=(0, .05),
+            command=self.on_resolution)
+        o = self._add_option_menu(r, res_cb)
+        pos_res = o.pos_pixel()
+        self._page_info.test_positions['res_1440x900'] = [pos_res[0] + 320, pos_res[1] + 75]
+        self._page_info.test_positions['res_1360x768'] = [pos_res[0] + 430, pos_res[1] -285]
+
+    def __build_aa(self):
+        c = CheckButtonArgs(
+            id='aa',
+            text=_('Antialiasing'),
+            pos=(0, -.2),
+            command=self.on_aa,
+            value=self.__option_file['settings']['antialiasing'])
+        self._add_check_button(c)
+
+    def __build_shadows(self):
+        c = CheckButtonArgs(
+            id='shadows',
+            text=_('Shadows'),
+            pos=(0, -.45),
+            command=self.on_shadows,
+            value=self.__option_file['settings']['shadows'])
+        self._add_check_button(c)
+
+    def __build_back(self):
+        back_button = ButtonArgs(
+            id='back',
+            text=_('Back'),
+            pos=(0, -.8),
+            command=self._set_page,
+            command_args=['main'])
+        self._add_button(back_button)
+        self.accept('enforce_resolution', self.enforce_resolution)
+
+    def enforce_resolution(self, resolution):
+        self.__enforced_resolution = resolution
+        info('enforced resolution: ' + resolution)
+
+    def on_language(self, arg):
+        lang_code = {
+            _('English'): 'en_EN',
+            _('Italian'): 'it_IT'}[arg]
+        self.__set_language(lang_code)
+        self.__option_file['settings']['language'] = lang_code[:2]
+        self.__option_file.store()
+        self._set_page('options')
+
+    def on_volume(self):
+        self.__option_file['settings']['volume'] = self._slider['value']
+        AudioTools.set_volume(self._slider['value'])
+
+    def on_fullscreen(self, arg):
+        props = WindowProperties()
+        props.set_fullscreen(arg)
+        if not self._page_info.running_functional_tests:
+            base.win.request_properties(props)
+            # if we actually switch to fullscreen during the tests then
+            # exwm inside qemu can't restore the correct resolution
+            # i may re-enable this if/when i run the tests onto a
+            # physical machine
+        self.__option_file['settings']['fullscreen'] = int(arg)
+        self.__option_file.store()
+
+    def on_resolution(self, arg):
+        info('on resolution: %s (%s)' % (arg, self.__enforced_resolution))
+        arg = self.__enforced_resolution or arg
+        info('set resolution: %s' % arg)
+        props = WindowProperties()
+        props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
+        base.win.request_properties(props)
+        self.__option_file['settings']['resolution'] = arg
+        self.__option_file.store()
+
+    def on_aa(self, arg):
+        self.__gfx_pipeline.msaa_samples = 4 if arg else 1
+        debug(f'msaa: {self.__gfx_pipeline.msaa_samples}')
+        self.__option_file['settings']['antialiasing'] = int(arg)
+        self.__option_file.store()
+
+    def on_shadows(self, arg):
+        self.__gfx_pipeline.enable_shadows = int(arg)
+        debug(f'shadows: {self.__gfx_pipeline.enable_shadows}')
+        self.__option_file['settings']['shadows'] = int(arg)
+        self.__option_file.store()
+
+    def on_back(self):
+        self.__option_file.store()
+        super().on_back()
+
+    def destroy(self):
+        super().destroy()
+        self.ignore('enforce_resolution')
diff --git a/pmachines/gui/play_page.py b/pmachines/gui/play_page.py
new file mode 100644 (file)
index 0000000..a38ab7e
--- /dev/null
@@ -0,0 +1,36 @@
+from ya2.utils.gui.base_page import BasePage, ButtonArgs
+
+
+class PlayPage(BasePage):
+
+    def __init__(self, page_info, scenes, start_scene):
+        self.__scenes = scenes
+        self.__start_scene = start_scene
+        super().__init__(page_info)
+
+    def _build_widgets(self):
+        left = - (dx := .8) * (min(4, len(self.__scenes)) - 1) / 2
+        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
+            i = ButtonArgs(
+                id=scene_name.lower(),
+                text=Scene.name(scene_name),
+                pos=(left + dx * (i % 4), top - dx * row),
+                command=self.__start_scene,
+                command_args=[scene_name],
+                wordwrap=6,
+                texture='assets/images/scenes/%s.dds' % scene_name,
+                size=(-2.4, 2.4, -2.4, 2.4),
+                text_scale=.64,
+                color=(1, 1, 1, .4) if Scene.is_done(scene_name) else (1, 1, 1, .8),
+                text_color=(.9, .9, .9, .4) if Scene.is_done(scene_name) else (.9, .9, .9, 1))
+            self._add_button(i, True)
+        back_button = ButtonArgs(
+            id='back',
+            text=_('Back'),
+            pos=(0, -.8),
+            command=self._set_page,
+            command_args=['main'])
+        self._add_button(back_button)
diff --git a/pmachines/gui/sidepanel.py b/pmachines/gui/sidepanel.py
new file mode 100644 (file)
index 0000000..70e4c89
--- /dev/null
@@ -0,0 +1,132 @@
+from textwrap import dedent
+from panda3d.core import GeomVertexData, GeomVertexFormat, Geom, \
+    GeomVertexWriter, GeomTriangles, GeomNode, Shader, Point3, Plane, Vec3
+from ya2.utils.gfx import Point
+
+
+class SidePanel:
+
+    def __init__(self, world, plane_node, top_l, bottom_r, y, items):
+        self._world = world
+        self._plane_node = plane_node
+        self._set((-1, 1), y)
+        self.update(items)
+
+    def update(self, items):
+        p_from, p_to = Point((-1, 1)).from_to_points()
+        for hit in self._world.ray_test_all(p_from, p_to).get_hits():
+            if hit.get_node() == self._plane_node:
+                pos = hit.get_hit_pos()
+        y = 0
+        corner = -20, 20
+        for item in items:
+            if not item._instantiated:
+                bounds = item._np.get_tight_bounds()
+                if bounds[1][1] > y:
+                    y = bounds[1][1]
+                icorner = Point(item.get_corner())
+                icorner = icorner.screen_coord()
+                if icorner[0] > corner[0]:
+                    corner = icorner[0], corner[1]
+                if icorner[1] < corner[1]:
+                    corner = corner[0], icorner[1]
+        self._set((pos[0], pos[2]), y)
+        bounds = self._np.get_tight_bounds()
+        #corner3d = bounds[1][0], bounds[1][1], bounds[0][2]
+        #corner2d = P3dGfxMgr.screen_coord(corner3d)
+        plane = Plane(Vec3(0, 1, 0), y)
+        pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
+        ar, margin = base.get_aspect_ratio(), .04
+        x = corner[0] / ar if ar >= 1 else corner[0]
+        z = corner[1] * ar if ar < 1 else corner[1]
+        x += margin / ar if ar >= 1 else margin
+        z -= margin * ar if ar < 1 else margin
+        base.camLens.extrude((x, z), near_pt, far_pt)
+        plane.intersects_line(
+            pos3d, render.get_relative_point(base.camera, near_pt),
+            render.get_relative_point(base.camera, far_pt))
+        corner_pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
+        base.camLens.extrude((-1, 1), near_pt, far_pt)
+        plane.intersects_line(
+            corner_pos3d, render.get_relative_point(base.camera, near_pt),
+            render.get_relative_point(base.camera, far_pt))
+        self._np.set_pos((pos3d + corner_pos3d) / 2)
+        scale = Vec3(pos3d[0] - corner_pos3d[0], 1, corner_pos3d[2] - pos3d[2])
+        self._np.set_scale(scale)
+
+    def _set(self, pos, y):
+        if hasattr(self, '_np'):
+            self._np.remove_node()
+        vdata = GeomVertexData('quad', GeomVertexFormat.get_v3(), Geom.UHStatic)
+        vdata.setNumRows(2)
+        vertex = GeomVertexWriter(vdata, 'vertex')
+        vertex.add_data3(.5, 0, -.5)
+        vertex.add_data3(.5, 0, .5)
+        vertex.add_data3(-.5, 0, .5)
+        vertex.add_data3(-.5, 0, -.5)
+        prim = GeomTriangles(Geom.UHStatic)
+        prim.add_vertices(0, 1, 2)
+        prim.add_vertices(0, 2, 3)
+        prim.close_primitive()
+        geom = Geom(vdata)
+        geom.add_primitive(prim)
+        node = GeomNode('gnode')
+        node.add_geom(geom)
+        self._np = render.attach_new_node(node)
+        self._np.setTransparency(True)
+        self._np.set_pos(pos[0], y, pos[1])
+        vert = '''\
+            #version 130
+            uniform mat4 p3d_ModelViewProjectionMatrix;
+            in vec4 p3d_Vertex;
+            void main() {
+                gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }'''
+        frag = '''\
+            #version 130
+            out vec4 p3d_FragColor;
+            void main() {
+                p3d_FragColor = vec4(.04, .04, .04, .08); }'''
+        self._np.set_shader(Shader.make(Shader.SL_GLSL, dedent(vert), dedent(frag)))
+        # mat = Material()
+        # mat.set_base_color((1, 1, 1, 1))
+        # mat.set_emission((1, 1, 1, 1))
+        # mat.set_metallic(.5)
+        # mat.set_roughness(.5)
+        # np.set_material(mat)
+        # texture_sz = 64
+        # base_color_pnm = PNMImage(texture_sz, texture_sz)
+        # base_color_pnm.fill(.1, .1, .1)
+        # base_color_pnm.add_alpha()
+        # base_color_pnm.alpha_fill(.04)
+        # base_color_tex = Texture('base color')
+        # base_color_tex.load(base_color_pnm)
+        # ts = TextureStage('base color')
+        # ts.set_mode(TextureStage.M_modulate)
+        # np.set_texture(ts, base_color_tex)
+        # emission_pnm = PNMImage(texture_sz, texture_sz)
+        # emission_pnm.fill(0, 0, 0)
+        # emission_tex = Texture('emission')
+        # emission_tex.load(emission_pnm)
+        # ts = TextureStage('emission')
+        # ts.set_mode(TextureStage.M_emission)
+        # np.set_texture(ts, emission_tex)
+        # metal_rough_pnm = PNMImage(texture_sz, texture_sz)
+        # ambient_occlusion = 1
+        # roughness = .5
+        # metallicity = .5
+        # metal_rough_pnm.fill(ambient_occlusion, roughness, metallicity)
+        # metal_rough_tex = Texture('ao metal roughness')
+        # metal_rough_tex.load(metal_rough_pnm)
+        # ts = TextureStage('ao metal roughness')
+        # ts.set_mode(TextureStage.M_selector)
+        # np.set_texture(ts, metal_rough_tex)
+        # normal_pnm = PNMImage(texture_sz, texture_sz)
+        # normal_pnm.fill(.5, .5, .1)
+        # normal_tex = Texture('normals')
+        # normal_tex.load(normal_pnm)
+        # ts = TextureStage('normals')
+        # ts.set_mode(TextureStage.M_normal)
+        # np.set_texture(ts, normal_tex)
+
+    def destroy(self):
+        self._np.remove_node()
diff --git a/pmachines/items/__init__.py b/pmachines/items/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pmachines/items/background.py b/pmachines/items/background.py
new file mode 100644 (file)
index 0000000..d641496
--- /dev/null
@@ -0,0 +1,25 @@
+from itertools import product
+from ya2.utils.gfx import GfxTools
+
+
+class Background:
+
+    def __init__(self, name):
+        self._root = GfxTools.build_empty_node('background_root')
+        self._root.reparent_to(render)
+        ncols, nrows = 16, 8
+        start_size, end_size = 5, 2.5
+        offset = 5
+        for col, row in product(range(ncols), range(nrows)):
+            model = GfxTools.build_model(f'assets/models/bam/backgrounds/{name}/background.bam')
+            model.set_scale(end_size / start_size)
+            model.reparent_to(self._root)
+            total_width, total_height = end_size * ncols, end_size * nrows
+            left, bottom = -total_width/2, -total_height/2
+            model.set_pos(left + end_size * col, offset, bottom + end_size * row)
+        self._root.clear_model_nodes()
+        self._root.flatten_strong()
+        self._root.set_srgb_textures()
+
+    def destroy(self):
+        self._root.remove_node()
diff --git a/pmachines/items/basketball.py b/pmachines/items/basketball.py
new file mode 100644 (file)
index 0000000..adba49e
--- /dev/null
@@ -0,0 +1,11 @@
+from panda3d.bullet import BulletSphereShape
+from pmachines.items.item import Item
+
+
+class Basketball(Item):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, model_scale=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.92, friction=.6):
+        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/basketball/basketball.bam', json, .4, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
+
+    def _set_shape(self, apply_scale=True):
+        self.node.add_shape(BulletSphereShape(1))
diff --git a/pmachines/items/box.py b/pmachines/items/box.py
new file mode 100644 (file)
index 0000000..28217f0
--- /dev/null
@@ -0,0 +1,25 @@
+from panda3d.bullet import BulletBoxShape
+from pmachines.items.item import Item, ItemStrategy
+
+
+class Box(Item):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, model_scale=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.8):
+        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/box/box.bam', json, model_scale=model_scale, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
+
+    def _set_shape(self, apply_scale=True):
+        self.node.add_shape(BulletBoxShape((.5, .5, .5)))
+
+
+class HitStrategy(ItemStrategy):
+
+    def __init__(self, hit_by, node, world):
+        self._hit_by = hit_by
+        self._node = node
+        self._world = world
+
+    def win_condition(self):
+        for contact in self._world.contact_test(self._node).get_contacts():
+            other = contact.get_node1() if contact.get_node0() == self._node else contact.get_node0()
+            if other == self._hit_by.node:
+                return True
diff --git a/pmachines/items/domino.py b/pmachines/items/domino.py
new file mode 100644 (file)
index 0000000..1f862d8
--- /dev/null
@@ -0,0 +1,33 @@
+from panda3d.bullet import BulletBoxShape
+from pmachines.items.item import Item, StillStrategy
+
+
+class Domino(Item):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, model_scale=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.6):
+        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/domino/domino.bam', json, model_scale=model_scale, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
+
+    def _set_shape(self, apply_scale=True):
+        self.node.add_shape(BulletBoxShape((.1, .25, .5)))
+
+
+class UpStrategy(StillStrategy):
+
+    def __init__(self, np, tgt_degrees):
+        super().__init__(np)
+        self._tgt_degrees = tgt_degrees
+        self._np = np
+
+    def win_condition(self):
+        return super().win_condition() and abs(self._np.get_r()) <= self._tgt_degrees
+
+
+class DownStrategy(StillStrategy):
+
+    def __init__(self, np, tgt_degrees):
+        super().__init__(np)
+        self._tgt_degrees = tgt_degrees
+        self._np = np
+
+    def win_condition(self):
+        return self._np.get_z() < -6 or super().win_condition() and abs(self._np.get_r()) >= self._tgt_degrees
diff --git a/pmachines/items/item.py b/pmachines/items/item.py
new file mode 100644 (file)
index 0000000..a0dd204
--- /dev/null
@@ -0,0 +1,444 @@
+from panda3d.core import CullFaceAttrib, Point3, Texture, \
+    Plane, Vec3, BitMask32
+from panda3d.bullet import BulletRigidBodyNode, BulletGhostNode
+from direct.gui.OnscreenText import OnscreenText
+from direct.showbase.DirectObject import DirectObject
+from ya2.utils.gfx import GfxTools, Point
+
+
+class Command:
+
+    def __init__(self, pos, rot):
+        self.pos = pos
+        self.rot = rot
+
+
+class ItemStrategy: pass
+
+
+class FixedStrategy(ItemStrategy):
+
+    def win_condition(self):
+        return True
+
+
+class StillStrategy(ItemStrategy):
+
+    def __init__(self, np):
+        self._np = np
+        self._positions = []
+        self._rotations = []
+
+    def win_condition(self):
+        self._positions += [self._np.get_pos()]
+        self._rotations += [self._np.get_hpr()]
+        if len(self._positions) > 30:
+            self._positions.pop(0)
+        if len(self._rotations) > 30:
+            self._rotations.pop(0)
+        if len(self._positions) < 28:
+            return
+        avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
+        avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
+        avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
+        avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
+        avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
+        avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
+        avg_pos = Point3(avg_x, avg_y, avg_z)
+        avg_rot = Point3(avg_h, avg_p, avg_r)
+        return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
+            all((rot - avg_rot).length() < 1 for rot in self._rotations)
+
+
+class Item(DirectObject):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, scene_repos, model_path, json, model_scale=1, exp_num_contacts=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.5):
+        super().__init__()
+        self._world = world
+        self._plane_node = plane_node
+        self._count = count
+        self._cb_inst = cb_inst
+        self._paused = False
+        self._overlapping = False
+        self._first_command = True
+        self.repos_done = False
+        self._mass = mass
+        self._pos = pos
+        self._r = r
+        self.json = json
+        self.strategy = FixedStrategy()
+        self._exp_num_contacts = exp_num_contacts
+        self._curr_bottom = curr_bottom
+        self._scene_repos = scene_repos
+        self._model_scale = model_scale
+        self._model_path = model_path
+        self._commands = []
+        self._command_idx = -1
+        self._restitution = restitution
+        self._friction = friction
+        self._positions = []
+        self._rotations = []
+        if count:
+            self.node = BulletGhostNode(self.__class__.__name__)
+        else:
+            self.node = BulletRigidBodyNode(self.__class__.__name__)
+        self._set_shape(count)
+        self._np = GfxTools.build_node_from_physics(self.node)
+        if count:
+            world.attach_ghost(self.node)
+        else:
+            world.attach_rigid_body(self.node)
+        self._model = GfxTools.build_model(model_path)
+        self._model.set_srgb_textures()
+        self._model.flatten_light()
+        self._model.reparent_to(self._np)
+        self._np.set_scale(model_scale)
+        self._np.flatten_strong()
+        self._set_outline_model()
+        self._outline_model.set_srgb_textures()
+        self._model.hide(BitMask32(0x01))
+        self._outline_model.hide(BitMask32(0x01))
+        self._start_drag_pos = None
+        self._prev_rot_info = None
+        self._last_nonoverlapping_pos = (0, 0, 0)
+        self._last_nonoverlapping_rot = 0
+        self._instantiated = not count
+        self._box_tsk = taskMgr.add(self.on_frame, 'item_on_frame')
+        if count:
+            self._repos()
+        else:
+            self._np.set_pos(pos)
+            self._np.set_r(r)
+        self.__editing = False
+        self.__interactable = count
+        self.accept('editor-start', self.__editor_start)
+        self.accept('editor-stop', self.__editor_stop)
+
+    def _set_shape(self, apply_scale=True):
+        pass
+
+    def __editor_start(self):
+        self.__editing = True
+
+    def __editor_stop(self):
+        self.__editing = False
+
+    @property
+    def interactable(self):
+        return self.__editing or self.__interactable
+
+    def set_strategy(self, strategy):
+        self.strategy = strategy
+
+    def _repos(self):
+        p_from, p_to = Point((-1, 1)).from_to_points()
+        for hit in self._world.ray_test_all(p_from, p_to).get_hits():
+            if hit.get_node() == self._plane_node:
+                pos = hit.get_hit_pos()
+        #corner = P3dGfxMgr.screen_coord(pos)
+        bounds = self._np.get_tight_bounds()
+        bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
+        self._np.set_pos(pos)
+        plane = Plane(Vec3(0, 1, 0), bounds[0][1])
+        pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
+        margin, ar = .04, base.get_aspect_ratio()
+        margin_x = margin / ar if ar >= 1 else margin
+        margin_z = margin * ar if ar < 1 else margin
+        top = self._curr_bottom()
+        base.camLens.extrude((-1 + margin_x, top - margin_z), near_pt, far_pt)
+        plane.intersects_line(
+            pos3d, render.get_relative_point(base.camera, near_pt),
+            render.get_relative_point(base.camera, far_pt))
+        delta = Vec3(bounds[1][0], bounds[1][1], bounds[0][2])
+        self._np.set_pos(pos3d + delta)
+        if not hasattr(self, '_txt'):
+            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(
+                str(self._count), font=font, scale=0.06, fg=(.9, .9, .9, 1))
+        pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
+        p2d = Point(pos).screen_coord()
+        self._txt['pos'] = p2d
+        self.repos_done = True
+
+    def repos_x(self, x):
+        self._np.set_x(x)
+        bounds = self._np.get_tight_bounds()
+        bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
+        pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
+        p2d = Point(pos).screen_coord()
+        self._txt['pos'] = p2d
+
+    def get_bottom(self):
+        bounds = self._np.get_tight_bounds()
+        bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
+        pos = self._np.get_pos() + (bounds[1][0], bounds[1][1], bounds[0][2])
+        p2d = Point(pos).screen_coord()
+        ar = base.get_aspect_ratio()
+        return p2d[1] if ar >= 1 else (p2d[1] * ar)
+
+    def get_corner(self):
+        bounds = self._np.get_tight_bounds()
+        return bounds[1][0], bounds[1][1], bounds[0][2]
+
+    def _set_outline_model(self):
+        self._outline_model = GfxTools.build_model(self._model_path)
+        #clockw = CullFaceAttrib.MCullClockwise
+        #self._outline_model.set_attrib(CullFaceAttrib.make(clockw))
+        self._outline_model.set_attrib(CullFaceAttrib.make_reverse())
+        self._outline_model.reparent_to(self._np)
+        self._outline_model.set_scale(1.08)
+        self._outline_model.set_light_off()
+        self._outline_model.set_color(.4, .4, .4, 1)
+        self._outline_model.set_color_scale(.4, .4, .4, 1)
+        self._outline_model.hide()
+
+    def on_frame(self, task):
+        self._np.set_y(0)
+        return task.cont
+
+    def undo(self):
+        self._command_idx -= 1
+        self._np.set_pos(self._commands[self._command_idx].pos)
+        self._np.set_hpr(self._commands[self._command_idx].rot)
+
+    def redo(self):
+        self._command_idx += 1
+        self._np.set_pos(self._commands[self._command_idx].pos)
+        self._np.set_hpr(self._commands[self._command_idx].rot)
+
+    def play(self):
+        if not self._instantiated:
+            return
+        self._world.remove_rigid_body(self.node)
+        self.node.set_mass(self._mass)
+        self._world.attach_rigid_body(self.node)
+        self.node.set_restitution(self._restitution)
+        self.node.set_friction(self._friction)
+
+    def on_click_l(self, pos):
+        if self._paused: return
+        if self.__editing:
+            messenger.send('editor-item-click', [self])
+        self._start_drag_pos = pos, self._np.get_pos()
+        loader.load_sfx('assets/audio/sfx/grab.ogg').play()
+        if not self._instantiated:
+            self._world.remove_ghost(self.node)
+            pos = self._np.get_pos()
+            self._np.remove_node()
+            self.node = BulletRigidBodyNode('box')
+            self._set_shape()
+            self._np = render.attach_new_node(self.node)
+            self._world.attach_rigid_body(self.node)
+            self._model.reparent_to(self._np)
+            self._np.set_pos(pos)
+            self._set_outline_model()
+            self._np.set_scale(self._model_scale)
+            self._model.show(BitMask32(0x01))
+            self._outline_model.hide(BitMask32(0x01))
+            self._instantiated = True
+            self._txt.destroy()
+            self._count -= 1
+            if self._count:
+                item = self.__class__(self._world, self._plane_node, self._cb_inst, self._curr_bottom, self._scene_repos, None, count=self._count, mass=self._mass, pos=self._pos, r=self._r)  # pylint: disable=no-value-for-parameter
+                self._cb_inst(item)
+            self._scene_repos()
+
+    def on_click_r(self, pos):
+        if self._paused or not self._instantiated: return
+        if self.__editing:
+            messenger.send('editor-item-click', [self])
+        self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
+        loader.load_sfx('assets/audio/sfx/grab.ogg').play()
+
+    def on_release(self):
+        if self._start_drag_pos or self._prev_rot_info:
+            loader.load_sfx('assets/audio/sfx/release.ogg').play()
+            self._command_idx += 1
+            self._commands = self._commands[:self._command_idx]
+            self._commands += [Command(self._np.get_pos(), self._np.get_hpr())]
+            self._first_command = False
+        self._start_drag_pos = self._prev_rot_info = None
+        if self._overlapping:
+            self._np.set_pos(self._last_nonoverlapping_pos)
+            self._np.set_hpr(self._last_nonoverlapping_rot)
+            self._outline_model.set_color(.4, .4, .4, 1)
+            self._outline_model.set_color_scale(.4, .4, .4, 1)
+            self._overlapping = False
+
+    def on_mouse_on(self):
+        if not self._paused and self.interactable:
+            self._outline_model.show()
+
+    def on_mouse_off(self):
+        if self._start_drag_pos or self._prev_rot_info: return
+        if self.interactable:
+            self._outline_model.hide()
+
+    def on_mouse_move(self, pos):
+        if self._start_drag_pos:
+            d_pos =  pos - self._start_drag_pos[0]
+            self._np.set_pos(self._start_drag_pos[1] + d_pos)
+        if self._prev_rot_info:
+            start_vec = self._prev_rot_info[0] - self._prev_rot_info[1]
+            curr_vec = pos - self._prev_rot_info[1]
+            d_angle = curr_vec.signed_angle_deg(start_vec, (0, -1, 0))
+            self._np.set_r(self._prev_rot_info[2] + d_angle)
+            self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
+        if self._start_drag_pos or self._prev_rot_info:
+            res = self._world.contact_test(self.node)
+            nres = res.get_num_contacts()
+            if nres <= self._exp_num_contacts:
+                self._overlapping = False
+                self._outline_model.set_color(.4, .4, .4, 1)
+                self._outline_model.set_color_scale(.4, .4, .4, 1)
+            if nres > self._exp_num_contacts and not self._overlapping:
+                actual_nres = 0
+                for contact in res.get_contacts():
+                    for node in [contact.get_node0(), contact.get_node1()]:
+                        if isinstance(node, BulletRigidBodyNode) and \
+                                node != self.node:
+                            actual_nres += 1
+                if actual_nres >= 1:
+                    self._overlapping = True
+                    loader.load_sfx('assets/audio/sfx/overlap.ogg').play()
+                    self._outline_model.set_color(.9, .1, .1, 1)
+                    self._outline_model.set_color_scale(.9, .1, .1, 1)
+        if not self._overlapping:
+            self._last_nonoverlapping_pos = self._np.get_pos()
+            self._last_nonoverlapping_rot = self._np.get_hpr()
+        messenger.send('item-rototranslated', [self._np])
+
+    def on_aspect_ratio_changed(self):
+        if not self._instantiated:
+            self._repos()
+
+    def store_state(self):
+        self._paused = True
+        self._model.set_transparency(True)
+        self._model.set_alpha_scale(.3)
+        if hasattr(self, '_txt') and not self._txt.is_empty():
+            self._txt.set_alpha_scale(.3)
+
+    def restore_state(self):
+        self._paused = False
+        self._model.set_alpha_scale(1)
+        if hasattr(self, '_txt') and not self._txt.is_empty():
+            self._txt.set_alpha_scale(1)
+
+    def fail_condition(self):
+        if self._np.get_z() < -6:
+            return True
+        self._positions += [self._np.get_pos()]
+        self._rotations += [self._np.get_hpr()]
+        if len(self._positions) > 30:
+            self._positions.pop(0)
+        if len(self._rotations) > 30:
+            self._rotations.pop(0)
+        if len(self._positions) < 28:
+            return
+        avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
+        avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
+        avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
+        avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
+        avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
+        avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
+        avg_pos = Point3(avg_x, avg_y, avg_z)
+        avg_rot = Point3(avg_h, avg_p, avg_r)
+        return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
+            all((rot - avg_rot).length() < 1 for rot in self._rotations)
+
+    @property
+    def mass(self):
+        return self._mass
+
+    @mass.setter
+    def mass(self, val):
+        self._mass = val
+        self.json['mass'] = val
+
+    @property
+    def restitution(self):
+        return self._restitution
+
+    @restitution.setter
+    def restitution(self, val):
+        self._restitution = val
+        self.json['restitution'] = val
+
+    @property
+    def friction(self):
+        return self._friction
+
+    @friction.setter
+    def friction(self, val):
+        self._friction = val
+        self.json['friction'] = val
+
+    @property
+    def position(self):
+        return self._np.get_pos()
+
+    @position.setter
+    def position(self, val):
+        self._np.set_pos(*val)
+        self.json['position'] = val
+
+    @property
+    def roll(self):
+        return self._np.get_r()
+
+    @roll.setter
+    def roll(self, val):
+        self._np.set_r(val)
+        self.json['roll'] = val
+
+    @property
+    def scale(self):
+        return self._np.get_scale()[0]
+
+    @scale.setter
+    def scale(self, val):
+        self._np.set_scale(val)
+        self.json['model_scale'] = val
+
+    @property
+    def id(self):
+        if 'id' in self.json:
+            return self.json['id']
+        return None
+
+    @id.setter
+    def id(self, val):
+        self.json['id'] = val
+
+    @property
+    def strategy_json(self):
+        return self.json['strategy']
+
+    @strategy_json.setter
+    def strategy_json(self, val):
+        self.json['strategy'] = val
+
+    @property
+    def strategy_args_json(self):
+        return self.json['strategy_args']
+
+    @strategy_args_json.setter
+    def strategy_args_json(self, val):
+        self.json['strategy_args'] = val.split(' ')
+
+    def destroy(self):
+        self._np.remove_node()
+        taskMgr.remove(self._box_tsk)
+        if hasattr(self, '_txt'):
+            self._txt.destroy()
+        if not self._instantiated:
+            self._world.remove_ghost(self.node)
+        else:
+            self._world.remove_rigid_body(self.node)
+        self.ignore('editor-start')
+        self.ignore('editor-stop')
diff --git a/pmachines/items/shelf.py b/pmachines/items/shelf.py
new file mode 100644 (file)
index 0000000..905e127
--- /dev/null
@@ -0,0 +1,11 @@
+from panda3d.bullet import BulletBoxShape
+from pmachines.items.item import Item
+
+
+class Shelf(Item):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, model_scale=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.6):
+        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/shelf/shelf.bam', json, model_scale=model_scale, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
+
+    def _set_shape(self, apply_scale=True):
+        self.node.add_shape(BulletBoxShape((1, .5, .05)))
diff --git a/pmachines/items/teetertooter.py b/pmachines/items/teetertooter.py
new file mode 100644 (file)
index 0000000..bf0a17e
--- /dev/null
@@ -0,0 +1,51 @@
+from panda3d.core import TransformState, Point3, Vec3
+from panda3d.bullet import BulletCylinderShape, YUp, ZUp, BulletHingeConstraint, BulletBoxShape
+from pmachines.items.item import Item
+
+
+class TeeterTooterShelf(Item):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, model_scale=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.6):
+        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/shelf/shelf.bam', json, model_scale=model_scale, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
+
+    def _set_shape(self, apply_scale=True):
+        self.node.add_shape(BulletBoxShape((1, .5, .05)))
+        self.node.add_shape(
+            BulletBoxShape((.05, .5, .05)),
+            TransformState.makePos((-1.05, 0, .1)))
+
+
+class TeeterTooter(Item):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, model_scale=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.5):
+        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/teeter_tooter/teeter_tooter.bam', json, model_scale=.5, exp_num_contacts=2, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
+        p = pos[0] - .04, pos[1], pos[2] + .27
+        self.__teeter_tooter_shelf = TeeterTooterShelf(world, plane_node, cb_inst, curr_bottom, repos, {}, pos=p, friction=1, r=-25.3, mass=.1)
+        self.set_constraint()
+
+    def _set_shape(self, apply_scale=True):
+        scale = self._model_scale if apply_scale else 1
+        self.node.add_shape(
+            BulletCylinderShape(.1, 1.6, YUp),
+            TransformState.makePos((0, 0, scale * .36)))
+        self.node.add_shape(
+            BulletCylinderShape(.1, .7, ZUp),
+            TransformState.makePos((0, scale * .8, scale * -.1)))
+        self.node.add_shape(
+            BulletCylinderShape(.1, .7, ZUp),
+            TransformState.makePos((0, scale * -.8, scale * -.1)))
+
+    def play(self):
+        super().play()
+        self.__teeter_tooter_shelf.play()
+
+    def set_constraint(self):
+        cs = BulletHingeConstraint(
+           self.__teeter_tooter_shelf.node,
+           Point3(0, 0, .1),
+           Vec3(0, 1, 0))
+        self._world.attach_constraint(cs)
+
+    def destroy(self):
+        self.__teeter_tooter_shelf.destroy()
+        super().destroy()
diff --git a/pmachines/items/test_item.py b/pmachines/items/test_item.py
new file mode 100644 (file)
index 0000000..4275ea6
--- /dev/null
@@ -0,0 +1,21 @@
+from panda3d.bullet import BulletBoxShape
+from pmachines.items.item import Item
+
+
+class TestItem(Item):
+
+    def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, model_scale=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.8):
+        super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/test_handle/test_handle.bam', json, model_scale=model_scale, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction)
+        self.id = json['id']
+        self._np.set_bin('fixed', 0)
+        self._np.set_depth_test(False)
+        self._np.set_depth_write(False)
+
+    def _set_shape(self, apply_scale=True):
+        self.node.add_shape(BulletBoxShape((.5, .5, .5)))
+
+
+class PixelSpaceTestItem(TestItem): pass
+
+
+class WorldSpaceTestItem(TestItem): pass
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..53e38f4
--- /dev/null
@@ -0,0 +1,860 @@
+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.items.test_item import PixelSpaceTestItem, WorldSpaceTestItem
+from pmachines.editor.scene import SceneEditor
+from ya2.utils.gui.cursor import MouseCursor, MouseCursorArgs
+from ya2.utils.gfx import GfxTools, DirectGuiMixin
+from ya2.utils.gui.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
+        for k in list(self._pos_mgr.keys()): del self._pos_mgr[k]
+        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()
+        c = MouseCursorArgs(
+            'assets/images/buttons/arrowUpLeft.dds', testing, (.04, 1, .04), (.5, .5, .5, 1),
+            (.01, .01))
+        self._cursor = MouseCursor(c)
+        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()
+        elif self.__json_name:
+            self._set_instructions()
+        if not self.__json_name:
+            self.json = {'background': 'wood'}
+        self._bg = Background(self.json['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):
+        info(f'{self.__json_name=}')
+        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())
+                info(f'{self.json=}')
+        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')
+        ]
+        info(f'{self.__editor=}')
+        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.__json_name 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.items:
+                if itm.id:
+                    self._pos_mgr[itm.id] = itm._model.pos2d_pixel()
+            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, self._pos_mgr)
+
+    def __on_inspector_delete(self, item):
+        if item.__class__ in [WorldSpaceTestItem, PixelSpaceTestItem]:
+            for i in self._test_items:
+                if item.id == i.name:
+                    r = i
+            self._test_items.remove(r)
+        else:
+            self.__items.remove(item)
+        j = self.json['items']
+        if item.__class__ == PixelSpaceTestItem:
+            j = self.json['test_items']['pixel_space']
+        if item.__class__ == WorldSpaceTestItem:
+            j = self.json['test_items']['world_space']
+        j.remove(item.json)
+        item.destroy()
diff --git a/prj.org b/prj.org
index 97eef1a7162f61445f2e7846a46732025b15117b..aab8600a5658b1199a5d729c4645455bcafdd77d 100644 (file)
--- a/prj.org
+++ b/prj.org
@@ -1,32 +1,17 @@
 #+STARTUP: indent
 #+STARTUP: indent
-#+TODO: INBOX BACKLOG READY RED CODE L10N REFACTOR LINT GREEN BUILD CHANGELOG BLOG | DONE
+#+TODO: BACKLOG READY TEST DOING REFACTORING BUILD BLOG | DONE
 #+CATEGORY: pmachines
 #+TAGS: bug(b) calendar(c) waiting(w)
 
 #+CATEGORY: pmachines
 #+TAGS: bug(b) calendar(c) waiting(w)
 
-* RED functional tests for performance (frame rate)
-* READY functional tests for "cleaning" i.e. at the end of the states verify:
-- [ ] active threads
-- [ ] active tasks
-- [ ] current nodepaths (render3d)
-- [ ] current nodepaths (render2d)
-- [ ] current nodepaths (render3d)
-- [ ] current accepting events
-- [ ] current buffers
-* BACKLOG intro animation (from target item to start position)
-* BACKLOG buttons of the scenes enabled sequentially
-- [ ] each scene has a version
-- [ ] when you win save the id + version
-- [ ] put an "update" if id is saved and versions are different
-* BACKLOG actions: rewind, prev, next
-* BACKLOG teeter-tooter with constraints (real teeter tooter), magnet, road cone, bucket
-* BACKLOG (when panda3d provides it) android build (test with the emulator of android studio)
-* BACKLOG (when itch.io's client waiting works with wine) functional tests for windows-itch.io
+* remove functional tests
+* bucket, magnet, road cone
+* (when panda3d provides it) android build (test with the emulator of android studio)
 * calendar                                                         :calendar:
 ** publish post q1; reschedule
 * calendar                                                         :calendar:
 ** publish post q1; reschedule
-SCHEDULED: <2023-03-18 Sat +1y>
+SCHEDULED: <2024-03-18 Mon +1y>
 ** publish post q2; reschedule
 ** publish post q2; reschedule
-SCHEDULED: <2022-06-18 Sat +1y>
+SCHEDULED: <2023-06-19 Mon +1y>
 ** publish post q3; reschedule
 ** publish post q3; reschedule
-SCHEDULED: <2022-09-16 Fri +1y>
+SCHEDULED: <2023-09-18 Mon +1y>
 ** publish post q4; reschedule
 ** publish post q4; reschedule
-SCHEDULED: <2022-12-16 Fri +1y>
+SCHEDULED: <2023-12-18 Mon +1y>
index c80bfb08fdc38ea0a10de3d26757a2e05a1e91ad..5645ca5c878188639b8befebddff8af5b362acf4 100644 (file)
@@ -1,4 +1,4 @@
 panda3d
 panda3d-simplepbr
 panda3d
 panda3d-simplepbr
-#panda3d-gltf
-#psutil
+panda3d-gltf
+panda3d-appimage
index 24a302b020ba50e64961959c9a891f6a12498cbd..a7107545ee2fbd401de3689fe320a4d3c230bfe4 100644 (file)
--- a/setup.py
+++ b/setup.py
-'''Setuptools' configuration file
-e.g. python setup.py models --cores=1
-e.g. python setup.py bdist_apps --nowin=1'''
+'''e.g. python setup.py bdist_apps --cores=1 --no-windows=1'''
 
 
 
 
-from os import system, getcwd, chdir
-from sys import argv, executable
-from collections import namedtuple
+from ya2.utils.log import LogManager
+LogManager.before_init_setup('pmachines')
+import sys, os
+from sys import argv
+from setuptools import setup
 from subprocess import Popen
 from subprocess import Popen
-from setuptools import setup, Command
+from setuptools import Command
 from setuptools.command.develop import develop
 from multiprocessing import cpu_count
 from setuptools.command.develop import develop
 from multiprocessing import cpu_count
+from textwrap import dedent
 from direct.dist.commands import bdist_apps
 from direct.dist.commands import bdist_apps
-from ya2.build.build import branch, files, ver, files, bld_dpath
-#from ya2.build.docs import bld_docs
-from ya2.build.models import ModelsBuilder
-from ya2.build.images import bld_images
-from ya2.build.screenshots import bld_screenshots
-from ya2.build.lang import LanguageBuilder
 from p3d_appimage import AppImageBuilder
 from p3d_appimage import AppImageBuilder
-from p3d_flatpak import FlatpakBuilder
-import ya2.utils.log  # so logging's info/debug are logged
-from logics.app import PmachinesApp
-
-
-appname = longname = 'pmachines'
-
+from ya2.build.build import _branch, find_file_names, _version, FindFileNamesArgs
+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.application.application import Pmachines
 
 
 
 
-msg = '''NOTE: please be sure that you've already created the assets with:
-    * python setup.py images models lang'''
+app_name = long_name = 'pmachines'
 
 
 
 
-class DevelopPyCmd(develop):
-    '''Command for setting up the development.'''
+class SetupDevelopmentCommand(develop):
 
     def run(self):
 
     def run(self):
-        '''Prepare the development environment.'''
-        develop.run(self)
-        Popen([executable, __file__, 'lang']).communicate()
-        Popen([executable, __file__, 'models']).communicate()
+        super().run(self)
+        for argument in ['lang', 'models', 'images']:
+            p = Popen([sys.executable, __file__, argument])
+            p.communicate()
 
 
 
 
-class AbsCmd(Command):
-    '''Common functionalities for commands.'''
+class BaseCommand(Command):
 
     user_options = [('cores=', None, '#cores')]
     cores = cpu_count()
 
     def initialize_options(self):
 
     user_options = [('cores=', None, '#cores')]
     cores = cpu_count()
 
     def initialize_options(self):
-        for arg in argv[:]:
-            if arg.startswith('--cores='):
-                AbsCmd.cores = int(arg.split('=')[1])
-
-    def finalize_options(self):  # must override
-        pass
-
+        BaseCommand.cores = cpu_count()
+        for a in argv[:]: self.__process_argument(a)
 
 
-#class DocsCmd(AbsCmd):
-#    '''Command for building the docs.'''
+    def __process_argument(self, argument):
+        if argument.startswith('--cores='):
+            BaseCommand.cores = int(argument.split('=')[1])
 
 
-#    def run(self):
-#        '''Builds the docs.'''
-#        bld_docs()
+    def finalize_options(self): pass  # must override
 
 
 
 
-class ModelsCmd(AbsCmd):
-    '''Command for building the models.'''
+class ModelsCommand(BaseCommand):
 
     def run(self):
 
     def run(self):
-        '''Builds the models.'''
-        ModelsBuilder().build('assets/models', int(AbsCmd.cores))
+        m = ModelsBuilder('assets/models', int(BaseCommand.cores))
+        m.build()
 
 
 
 
-class ImagesCmd(AbsCmd):
-    '''Command for building the models.'''
+class ImagesCommand(BaseCommand):
 
     def run(self):
 
     def run(self):
-        '''Builds the images.'''
-        bld_screenshots(PmachinesApp.scenes)
-        bld_images(
-            files(['jpg', 'png'], ['models', 'gltf', 'bam'], ['_png.png']), int(AbsCmd.cores))
+        s = ScreenshotsBuilder(Pmachines.scenes())
+        s.build()
+        find_info = FindFileNamesArgs(
+            ['jpg', 'png'],
+            ['models', 'gltf', 'bam', 'build'],
+            ['_png.png'])
+        images = find_file_names(find_info)
+        i = ImagesBuilder(images, int(BaseCommand.cores))
+        i.build()
 
 
 
 
-class LangCmd(AbsCmd):
-    '''Command for building po, pot and mo files.'''
-
-    lang_path = 'assets/locale/'
-
-    def _process_lang(self, lang_code):
-        '''Processes a single language.'''
-        poname = 'assets/locale/po/%s.po' % lang_code
-        LanguageBuilder.merge(lang_code, 'assets/locale/po/', self.lang_path, appname)
-        mo_tmpl = '%s%s/LC_MESSAGES/%s.mo'
-        moname = mo_tmpl % (self.lang_path, lang_code, appname)
-        LanguageBuilder.mo(moname, self.lang_path, appname)
+class L10nCommand(BaseCommand):
 
     def run(self):
 
     def run(self):
-        '''Builds the language files.'''
-        LanguageBuilder.pot(appname, 'assets/locale/po/')
-        list(map(self._process_lang, ['it_IT']))
+        l = LanguageBuilder(
+            app_name, 'assets/locale/po/', 'assets/scenes/', 'assets/locale/', '.')
+        l.build()
+        # l = LanguageBuilder(
+        #     'test', 'assets/locale/po/', 'assets/scenes/', 'assets/locale/', 'tests')
+        # l.build()
 
 
 
 
-class BDistAppsCmd(bdist_apps):
-    '''Customization of BDistApps.'''
+class BDistAppsCommand(bdist_apps):
 
     user_options = bdist_apps.user_options + [
         ('cores', None, '#cores'),
 
     user_options = bdist_apps.user_options + [
         ('cores', None, '#cores'),
-        ('nowin=', None, "don't build for windows"),
+        ('nowindows=', None, "don't build for windows"),  # letters, numbers and hypens only
         ('nolinux=', None, "don't build for linux")]
 
     def initialize_options(self):
         ('nolinux=', None, "don't build for linux")]
 
     def initialize_options(self):
-        '''Default values.'''
-        bdist_apps.initialize_options(self)
-        self.nowin, self.nolinux = None, None
+        super().initialize_options()
+        self.nowindows, self.nolinux = None, None
+        for a in argv[:]: self.__process_argument(a)
 
 
-    #def finalize_options(self):  # must override
-    #    bdist_apps.finalize_options(self)
+    def __process_argument(self, argument):
+        if argument.startswith('--no-windows='):
+            self.nowindows = int(argument.split('=')[1])
+        if argument.startswith('--no-linux='):
+            self.nolinux = int(argument.split('=')[1])
 
     def run(self):
 
     def run(self):
-        '''Our bdist_apps' customization.'''
-        print(msg)
-        cmd = 'patch --forward ' + \
+        assets_creation_message = dedent('''NOTE: please be sure that you've already created the assets with:
+            * python setup.py images models lang''')
+        print(assets_creation_message)
+        self.__patch_commands_py()
+        super().run()
+        self.__eventually_build_appimage()
+
+    def __patch_commands_py(self):
+        command = 'patch --forward ' + \
               '../venv/lib/python3.7/site-packages/direct/dist/commands.py' + \
               ' ya2/build/commands.patch'
               '../venv/lib/python3.7/site-packages/direct/dist/commands.py' + \
               ' ya2/build/commands.patch'
-        system(cmd)
-        bdist_apps.run(self)
-        if not self.nolinux:
-            hbranch = {'master': 'alpha', 'rc': 'rc', 'stable': ''}[branch]
-            AppImageBuilder(self).build(longname, hbranch,
-                                        'https://www.ya2.it/downloads/')
-            fbranch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[branch]
-            bld = FlatpakBuilder(
-                self,
-                'it.ya2.Pmachines',
-                '/home/flavio/builders/pmachines_builder/flatpak',
-                'D43B6D401912B520B6805FCC8E019E6340E3BAB5',
-                '/home/flavio/builders/pmachines_builder/gpg',
-                'https://www.ya2.it/flatpak',
-                ['options*.ini'],
-                fbranch,
-                ['assets'])
-            bld.build()
+        os.system(command)
 
 
-
-if __name__ == '__main__':
-    platform_lst, installers_dct = [], {}
-    if all('--nowin' not in arg for arg in argv):
-        platform_lst += ['win_amd64']
-        installers_dct['win_amd64'] = ['nsis']
-    if all('--nolinux' not in arg for arg in argv):
-        platform_lst += ['manylinux2010_x86_64']
-        installers_dct['manylinux2010_x86_64'] = []
-    log_path = '$USER_APPDATA/pmachines/logs/%Y/%B/%d/%H_%M_%S.log'
-    package_data_dirs = {'simplepbr': [('simplepbr/shaders*', '', {})]}
-    setup(
-        name=appname,
-        version=ver,
-        cmdclass={
-            'develop': DevelopPyCmd,
-            # 'docs': DocsCmd,
-            'models': ModelsCmd,
-            'images': ImagesCmd,
-            'lang': LangCmd,
-            'bdist_apps': BDistAppsCmd},
-        install_requires=['panda3d'],
-        options={
+    def __eventually_build_appimage(self):
+        if not self.nolinux:
+            a = AppImageBuilder(self)
+            filename_branch = {'master': 'alpha', 'rc': 'rc', 'stable': ''}
+            branch = filename_branch[_branch()]
+            a.build(long_name, branch, 'https://www.ya2.it/downloads/')
+
+
+def _build_setup_arguments():
+    d = {
+        'name': app_name,
+        'version': _version(),
+        'packages': ['pmachines', 'ya2', 'assets', 'licenses'],
+        'cmdclass': {
+            'develop': SetupDevelopmentCommand,
+            'models': ModelsCommand,
+            'images': ImagesCommand,
+            'lang': L10nCommand,
+            'bdist_apps': BDistAppsCommand},
+        'install_requires': ['panda3d'],
+        'options': {
             'build_apps': {
                 'exclude_patterns': [
                     'build/*', 'built/*', 'setup.py', 'requirements.txt',
             'build_apps': {
                 'exclude_patterns': [
                     'build/*', 'built/*', 'setup.py', 'requirements.txt',
@@ -173,19 +142,12 @@ if __name__ == '__main__':
                     'assets/models/**/models/*.png',
                     'assets/models/**/models/*.jpg'],
                 'log_filename_strftime': True,
                     'assets/models/**/models/*.png',
                     'assets/models/**/models/*.jpg'],
                 'log_filename_strftime': True,
-                'log_filename': log_path,
+                'log_filename': '$USER_APPDATA/pmachines/logs/%Y/%B/%d/%H_%M_%S.log',
                 'plugins': ['pandagl', 'p3openal_audio', 'pandadx9'],
                 'plugins': ['pandagl', 'p3openal_audio', 'pandadx9'],
-                'gui_apps': {appname: 'main.py'},
-                'package_data_dirs': package_data_dirs,
-                'icons': {
-                    appname: [
-                        'assets/images/icon/icon256_png.png',
-                        'assets/images/icon/icon128_png.png',
-                        'assets/images/icon/icon48_png.png',
-                        'assets/images/icon/icon32_png.png',
-                        'assets/images/icon/icon16_png.png']},
+                'gui_apps': {app_name: 'main.py'},
+                'package_data_dirs': {'simplepbr': [('simplepbr/shaders*', '', {})]},
                 'icons': {
                 'icons': {
-                    appname: [
+                    app_name: [
                         'assets/images/icon/icon256.png',
                         'assets/images/icon/icon128.png',
                         'assets/images/icon/icon48.png',
                         'assets/images/icon/icon256.png',
                         'assets/images/icon/icon128.png',
                         'assets/images/icon/icon48.png',
@@ -209,6 +171,27 @@ if __name__ == '__main__':
                     '**/*.ogg',
                     '**/*.wav',
                     '**/*.mo'],
                     '**/*.ogg',
                     '**/*.wav',
                     '**/*.mo'],
-                'platforms': platform_lst,
-                'include_modules': {'*': ['encodings.hex_codec']}},
-            'bdist_apps': {'installers': installers_dct}})
+                'platforms': __platform_list(),
+                'include_modules': {'*': ['encodings.hex_codec']},
+                'prefer_discrete_gpu': True},
+            'bdist_apps': {'installers': __installers_dictionary()}}}
+    return d
+
+def __platform_list():
+    platforms = []
+    if all('--no-windows' not in a for a in argv):
+        platforms += ['win_amd64']
+    if all('--no-linux' not in a for a in argv):
+        platforms += ['manylinux2010_x86_64']
+    return platforms
+
+def __installers_dictionary():
+    installers = {}
+    if all('--no-windows' not in a for a in argv):
+        installers['win_amd64'] = ['nsis']
+    if all('--no-linux' not in a for a in argv):
+        installers['manylinux2010_x86_64'] = []
+    return installers
+
+if __name__ == '__main__':
+    setup(**_build_setup_arguments())
diff --git a/tests/assets/images/icon16.png b/tests/assets/images/icon16.png
new file mode 100644 (file)
index 0000000..cb9cf89
Binary files /dev/null and b/tests/assets/images/icon16.png differ
diff --git a/tests/assets/images/icon32.png b/tests/assets/images/icon32.png
new file mode 100644 (file)
index 0000000..18c07c3
Binary files /dev/null and b/tests/assets/images/icon32.png differ
diff --git a/tests/assets/locale/po/it_IT.po b/tests/assets/locale/po/it_IT.po
new file mode 100644 (file)
index 0000000..7e55476
--- /dev/null
@@ -0,0 +1,21 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: 2022-12-14 08:00+0100\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "test string"
+msgstr "stringa di test"
diff --git a/tests/assets/locale/po/test.pot b/tests/assets/locale/po/test.pot
new file mode 100644 (file)
index 0000000..8d71626
--- /dev/null
@@ -0,0 +1,21 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-06-14 16:46+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "test string"
+msgstr ""
diff --git a/tests/assets/models/blend/cube/cube.blend b/tests/assets/models/blend/cube/cube.blend
deleted file mode 100644 (file)
index 159f71d..0000000
Binary files a/tests/assets/models/blend/cube/cube.blend and /dev/null differ
diff --git a/tests/assets/models/blend/cube/diffuse.png b/tests/assets/models/blend/cube/diffuse.png
deleted file mode 100644 (file)
index fc20070..0000000
Binary files a/tests/assets/models/blend/cube/diffuse.png and /dev/null differ
diff --git a/tests/assets/models/blend/cube1/cube.blend b/tests/assets/models/blend/cube1/cube.blend
new file mode 100644 (file)
index 0000000..159f71d
Binary files /dev/null and b/tests/assets/models/blend/cube1/cube.blend differ
diff --git a/tests/assets/models/blend/cube1/diffuse.png b/tests/assets/models/blend/cube1/diffuse.png
new file mode 100644 (file)
index 0000000..fc20070
Binary files /dev/null and b/tests/assets/models/blend/cube1/diffuse.png differ
diff --git a/tests/assets/models/blend/cube2/cube.blend b/tests/assets/models/blend/cube2/cube.blend
new file mode 100644 (file)
index 0000000..159f71d
Binary files /dev/null and b/tests/assets/models/blend/cube2/cube.blend differ
diff --git a/tests/assets/models/blend/cube2/diffuse.png b/tests/assets/models/blend/cube2/diffuse.png
new file mode 100644 (file)
index 0000000..fc20070
Binary files /dev/null and b/tests/assets/models/blend/cube2/diffuse.png differ
index 9aa1d8ac045095e9721da214449c8d8404fa3df0..338eebb96742e4bc61bfa5bc767215a7e676cee5 100644 (file)
-'''Create ref:
-* M-x fla-set-fun-test
-* rm options.ini
-* python main.py --functional-test --functional-ref & python -m ya2.tools.functional_test.py 1
-* python main.py --functional-test --functional-ref & python -m ya2.tools.functional_test.py 2
-* M-x fla-unset-fun-test'''
 from panda3d.core import load_prc_file_data
 load_prc_file_data('', 'window-type none')
 from panda3d.core import load_prc_file_data
 load_prc_file_data('', 'window-type none')
-import datetime
+import sys
 from time import sleep
 from time import sleep
-from os import getcwd, system
+from os import system
 from xmlrpc.client import ServerProxy
 from xmlrpc.client import ServerProxy
-from logging import debug, info
-from pathlib import Path
-from shutil import rmtree
-from os import makedirs
-from os.path import join, exists
-from glob import glob
 from sys import exit, argv
 from sys import exit, argv
-from panda3d.core import Filename
 from direct.showbase.ShowBase import ShowBase
 from direct.showbase.ShowBase import ShowBase
-from direct.gui.OnscreenText import OnscreenText
-from ya2.patterns.gameobject import GameObject
-from ya2.build.build import _branch
+import asyncio
+import xmlrpc
+from logging import basicConfig, debug, info, DEBUG, getLogger, StreamHandler
 
 
 
 
-class FunctionalTest(GameObject):
+basicConfig(level=DEBUG, format='(evt-test) %(asctime)s.%(msecs)03d %(message)s', datefmt='%H:%M:%S')
+getLogger().setLevel(DEBUG)  # it doesn't work otherwise
+getLogger().addHandler(StreamHandler(sys.stdout))
 
 
-    screenshot_time = 1.2
-    evt_time = 1.0
-    drag_time = 1.0
+
+class FunctionalTest:
+
+    screenshot_time = 2.0
+    evt_time = 2.0
+    drag_time = 2.0
     start_time = 2
 
     def __init__(self, idx, offset):
     start_time = 2
 
     def __init__(self, idx, offset):
+        debug('creating FunctionalTest (%s)' % id(self))
         super().__init__()
         super().__init__()
+
+        # timestamp = datetime.datetime.now().strftime('%y%m%d%H%M%S')
+        # logFormatter = Formatter('%(asctime)s.%(msecs)03d %(message)s', datefmt='%H:%M:%S')
+        # path = str(Path.home()) + '/.local/share/pmachines/logs/evt/'
+        # if not exists(path):
+        #     makedirs(path)
+        # lpath = path + 'evt_%s.log' % timestamp
+        # fileHandler = FileHandler(lpath)
+        # fileHandler.setFormatter(logFormatter)
+        # getLogger().addHandler(fileHandler)
+
         info('test idx: %s' % idx)
         self._offset = offset
         info('test idx: %s' % idx)
         self._offset = offset
-        sleep(5)
-        self._proxy = ServerProxy('http://localhost:6000')
+        info(str(sys.argv))
+        sleep(int(sys.argv[2]) if len(sys.argv) >= 3 else 12)
+        self._proxy = ServerProxy('http://localhost:7000')
         self._curr_time = 0
         self._tasks = []
         self._prev_time = 0
         self._curr_time = 0
         self._tasks = []
         self._prev_time = 0
-        taskMgr.add(self.on_frame_unpausable, 'on-frame-unpausable')
-        self._proxy.set_idx(idx)
+        #taskMgr.add(self.on_frame_unpausable, 'on-frame-unpausable')
+        self._proxy.set_index(idx)
         self._do_screenshots(idx)
 
     def _do_screenshot(self, name):
         self._do_screenshots(idx)
 
     def _do_screenshot(self, name):
-        time = datetime.datetime.now().strftime('%y%m%d%H%M%S')
+        time = datetime.datetime.now().strftime('%y%m%d%H%M%S')
         name = name + '.png'
         self._proxy.screenshot(name)
         info('screenshot %s' % name)
 
         name = name + '.png'
         self._proxy.screenshot(name)
         info('screenshot %s' % name)
 
-    def _screenshot(self, time, name):
-        self._tasks += [(
-            self._curr_time + time,
-            lambda: self._do_screenshot(name),
-            'screenshot: %s' % name)]
-        self._curr_time += time
+    async def _screenshot(self, time, name):
+        #self._tasks += [(
+        #    self._curr_time + time,
+        #    lambda: self._do_screenshot(name),
+        #    'screenshot: %s' % name)]
+        #self._curr_time += time
+        await asyncio.sleep(time)
+        self._do_screenshot(name)
 
 
-    def __mouse_click(self, tgt, btn):
+    async def __mouse_click(self, tgt, btn):
+        await asyncio.sleep(.5)
         offset_x = int((1920 - 1360) / 2) #+ 1  # xfce decorations
         offset_y = int((1080 - 768) / 2) #+ 24 + self._offset  # xfce decorations
         btn = 3 if btn == 'right' else 1
         offset_x = int((1920 - 1360) / 2) #+ 1  # xfce decorations
         offset_y = int((1080 - 768) / 2) #+ 24 + self._offset  # xfce decorations
         btn = 3 if btn == 'right' else 1
-        pos = self._proxy.get_pos(tgt)
-        print(tgt, pos)
+        pos = self._proxy.get_position(tgt)
         system('xdotool mousemove %s %s' % (offset_x + pos[0], offset_y + pos[1]))
         system('xdotool mousemove %s %s' % (offset_x + pos[0], offset_y + pos[1]))
-        def click(task):
-            system('xdotool click %s' % btn)
-        taskMgr.do_method_later(.28, click, 'click')
+        #def click(task):
+        #    system('xdotool click %s' % tgt)
+        #    print('CLICK %s' % str(btn))
+        #taskMgr.do_method_later(.28, click, 'click')
+        await asyncio.sleep(.5)
+        system('xdotool click %s' % btn)
 
 
-    def __mouse_drag(self, start, end, btn):
+    async def __mouse_drag(self, start, end, btn):
+        await asyncio.sleep(.5)
         offset_x = int((1920 - 1360) / 2) #+ 1  # xfce decorations
         offset_y = int((1080 - 768) / 2) #+ 24 + self._offset  # xfce decorations
         btn = 3 if btn == 'right' else 1
         offset_x = int((1920 - 1360) / 2) #+ 1  # xfce decorations
         offset_y = int((1080 - 768) / 2) #+ 24 + self._offset  # xfce decorations
         btn = 3 if btn == 'right' else 1
+        start = self._proxy.get_position(start)
+        end = self._proxy.get_position(end)
         system('xdotool mousemove %s %s' % (offset_x + start[0], offset_y + start[1]))
         system('xdotool mousemove %s %s' % (offset_x + start[0], offset_y + start[1]))
-        def mousedown(task):
-            system('xdotool mousedown %s' % btn)
-            def mousemove(task):
-                system('xdotool mousemove %s %s' % (offset_x + end[0], offset_y + end[1]))
-                def mouseup(task):
-                    system('xdotool mouseup %s' % btn)
-                taskMgr.do_method_later(.28, mouseup, 'mouseup')
-            taskMgr.do_method_later(.28, mousemove, 'mousemove')
-        taskMgr.do_method_later(.28, mousedown, 'mousedown')
-
-    def _event(self, time, evt, mouse_args=None):
+        await asyncio.sleep(.5)
+        system('xdotool mousedown %s' % btn)
+        await asyncio.sleep(.5)
+        system('xdotool mousemove %s %s' % (offset_x + end[0], offset_y + end[1]))
+        await asyncio.sleep(.5)
+        system('xdotool mouseup %s' % btn)
+        # def mousedown(task):
+        #     system('xdotool mousedown %s' % btn)
+        #     def mousemove(task):
+        #         system('xdotool mousemove %s %s' % (offset_x + end[0], offset_y + end[1]))
+        #         def mouseup(task):
+        #             system('xdotool mouseup %s' % btn)
+        #         taskMgr.do_method_later(.28, mouseup, 'mouseup')
+        #     taskMgr.do_method_later(.28, mousemove, 'mousemove')
+        # taskMgr.do_method_later(.28, mousedown, 'mousedown')
+
+    async def __emulate_keys(self, keys):
+        await asyncio.sleep(.4)
+        for k in keys:
+            await asyncio.sleep(.3)
+            system(f'xdotool key {k}')
+
+    async def _event(self, time, evt, mouse_args=None):
+        await asyncio.sleep(time)
         if evt == 'mouseclick':
         if evt == 'mouseclick':
-            cback = lambda: self.__mouse_click(*mouse_args)
+            #cback = lambda: self.__mouse_click(*mouse_args)
+            await self.__mouse_click(*mouse_args)
         elif evt == 'mousedrag':
         elif evt == 'mousedrag':
-            cback = lambda: self.__mouse_drag(*mouse_args)
-        self._tasks += [(
-            self._curr_time + time,
-            cback,
-            'event: %s' % evt)]
-        self._curr_time += time
-
-    def _enforce_res(self, time, res):
-        def cback():
-            self._proxy.enforce_res(res)
-            info('enforce_res %s' % res)
-        self._tasks += [(
-            self._curr_time + time,
-            cback,
-            'enforce res: %s' % res)]
-        self._curr_time += time
-
-    def _enforce_resolution(self, time, res):
-        def cback():
-            self._proxy.enforce_resolution(res)
-            info('enforce_resolution %s (send)' % res)
-        self._tasks += [(
-            self._curr_time + time,
-            cback,
-            'enforce resolution: %s' % res)]
-        self._curr_time += time
-
-    def _verify(self):
-        def __verify():
-            self._proxy.verify()
-            info('verify')
-        self._tasks += [(
-            self._curr_time + 3,
-            lambda: __verify(),
-            'verify')]
-        self._curr_time += 3
-
-    def _exit(self):
-        def do_exit():
+            #cback = lambda: self.__mouse_drag(*mouse_args)
+            await self.__mouse_drag(*mouse_args)
+        elif evt == 'keyboard':
+            #cback = lambda: self.__mouse_drag(*mouse_args)
+            await self.__emulate_keys(mouse_args)
+        #self._tasks += [(
+        #    self._curr_time + time,
+        #    cback,
+        #    'event: %s' % evt)]
+        #self._curr_time += time
+
+    async def _enforce_result(self, time, res):
+        await asyncio.sleep(time)
+        self._proxy.enforce_result(res)
+        #def cback():
+        #    self._proxy.enforce_result(res)
+        #    info('enforce_result %s' % res)
+        #self._tasks += [(
+        #    self._curr_time + time,
+        #    cback,
+        #    'enforce res: %s' % res)]
+        #self._curr_time += time
+
+    async def _enforce_resolution(self, time, res):
+        await asyncio.sleep(time)
+        self._proxy.enforce_resolution(res)
+        #def cback():
+        #    self._proxy.enforce_resolution(res)
+        #    info('enforce_resolution %s (send)' % res)
+        #self._tasks += [(
+        #    self._curr_time + time,
+        #    cback,
+        #    'enforce resolution: %s' % res)]
+        #self._curr_time += time
+
+    async def _verify(self, time):
+        await asyncio.sleep(time)
+        self._proxy.verify()
+        info('verify')
+        #def __verify():
+        #    self._proxy.verify()
+        #    info('verify')
+        #self._tasks += [(
+        #    self._curr_time + 3,
+        #    lambda: __verify(),
+        #    'verify')]
+        #self._curr_time += 3
+
+    async def _exit(self, time):
+        await asyncio.sleep(time)
+        try:
             self._proxy.close()
             self._proxy.close()
-            exit()
-        self._tasks += [(
-            self._curr_time + 3,
-            lambda: do_exit(),
-            'exit')]
-
-    def on_frame_unpausable(self, task):
-        for tsk in self._tasks:
-            #if self._prev_time <= tsk[0] < self.eng.event.unpaused_time:
-            if self._prev_time <= tsk[0] < globalClock.getFrameTime():
-                debug('%s %s' % (tsk[0], tsk[2]))
-                tsk[1]()
-        self._prev_time = globalClock.getFrameTime()  # self.eng.event.unpaused_time
-        return task.cont
-
-    def _do_screenshots_1(self):
+        except (ConnectionRefusedError, xmlrpc.client.Fault) as e:  # the other part has been closed with the exit button
+            debug('already closed (%s)' % e)
+        debug('destroying FunctionalTest (%s)' % id(self))
+        exit()
+        #def do_exit():
+        #    try:
+        #        self._proxy.close()
+        #    except (ConnectionRefusedError, xmlrpc.client.Fault) as e:  # the other part has been closed with the exit button
+        #        debug('already closed (%s)' % e)
+        #    debug('destroying FunctionalTest (%s)' % id(self))
+        #    exit()
+        #self._tasks += [(
+        #    self._curr_time + 3,
+        #    lambda: do_exit(),
+        #    'exit')]
+
+    # def on_frame_unpausable(self, task):
+    #     for tsk in self._tasks:
+    #         #if self._prev_time <= tsk[0] < self.eng.event.unpaused_time:
+    #         if self._prev_time <= tsk[0] < globalClock.getFrameTime():
+    #             debug('%s %s' % (tsk[0], tsk[2]))
+    #             tsk[1]()
+    #     self._prev_time = globalClock.getFrameTime()  # self.eng.event.unpaused_time
+    #     return task.cont
+
+    async def _do_screenshots_1(self):
         info('_do_screenshots_1')
         info('_do_screenshots_1')
-        self._screenshot(FunctionalTest.start_time, 'main_menu')
-        self._do_screenshots_credits()
-        self._do_screenshots_options()
-        self._do_screenshots_exit()
-
-    def _do_screenshots_credits(self):
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['credits', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'credits_menu')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'main_menu_back_from_credits')
-
-    def _do_screenshots_options(self):
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 300), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['options', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'options_menu')
+        await self._screenshot(FunctionalTest.start_time, 'main_menu')
+        await self._do_screenshots_credits()
+        await self._do_screenshots_options()
+        await self._do_screenshots_exit()
+
+    async def _do_screenshots_credits(self):
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['credits', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'credits_menu')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'main_menu_back_from_credits')
+
+    async def _do_screenshots_options(self):
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 300), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['options', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'options_menu')
         # languages
         # languages
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 60), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['languages', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'open_languages')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(980, 120), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['italian', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'options_menu_italian')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 60), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['languages', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'open_languages')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(980, 120), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['italian', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'options_menu_italian')
         # volume
         # volume
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(740, 163), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['volume', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'options_menu_drag_1')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(740, 163), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['volume', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'options_menu_drag_1')
         # antialiasing
         # antialiasing
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 440), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['aa', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'antialiasing_no')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 440), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['aa', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'antialiasing_no')
         # shadows
         # shadows
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 540), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['shadows', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'shadows_no')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 540), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['shadows', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'shadows_no')
         # test aa and shadows
         # test aa and shadows
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])  # back
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])  # back
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])  # play
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])  # domino
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(900, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])  # close instructions
-        self._screenshot(FunctionalTest.screenshot_time, 'aa_no_shadows_no')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(25, 740), 'left'])  # home
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['home', 'left'])  # home
-
-    def _do_screenshots_restore_options(self):
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 300), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['options', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'options_menu_restored')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])  # back
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])  # back
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])  # play
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])  # domino
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(900, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])  # close instructions
+        await self._screenshot(FunctionalTest.screenshot_time, 'aa_no_shadows_no')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(25, 740), 'left'])  # home
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['home', 'left'])  # home
+
+    async def _do_screenshots_restore_options(self):
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 300), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['options', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'options_menu_restored')
         # languages
         # languages
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 60), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['languages', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'open_languages_restored')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(980, 20), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['english', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'options_menu_english')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 60), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['languages', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'open_languages_restored')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(980, 20), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['english', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'options_menu_english')
         # volume
         # volume
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(719, 163), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['volume_0', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'options_menu_drag_2')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(719, 163), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['volume_0', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'options_menu_drag_2')
         # fullscreen
         # the first one is because of the windowed mode in test
         # fullscreen
         # the first one is because of the windowed mode in test
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 250), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['fullscreen', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'fullscreen')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 250), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['fullscreen', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'fullscreen')
-        #self._event(8 + FunctionalTest.evt_time, 'mouseclick', [(440, 120), 'left'])
-        #self._event(8 + FunctionalTest.evt_time, 'mouseclick', [(680, 250), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['fullscreen', 'left'])
-        self._screenshot(8 + FunctionalTest.screenshot_time, 'back_from_fullscreen')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 250), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['fullscreen', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'fullscreen')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 250), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['fullscreen', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'fullscreen')
+        #await self._event(8 + FunctionalTest.evt_time, 'mouseclick', [(440, 120), 'left'])
+        #await self._event(8 + FunctionalTest.evt_time, 'mouseclick', [(680, 250), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['fullscreen', 'left'])
+        await self._screenshot(8 + FunctionalTest.screenshot_time, 'back_from_fullscreen')
         # resolution
         # resolution
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 340), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['resolutions', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'resolutions')
-        self._enforce_resolution(FunctionalTest.evt_time, '1440x900')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1000, 440), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['res_1440x900', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, '1440x900')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(740, 400), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['resolutions', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'resolutions_2')
-        self._enforce_resolution(FunctionalTest.evt_time, '1360x768')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1110, 80), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['res_1360x768', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, '1360x768')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 340), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['resolutions', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'resolutions')
+        await self._enforce_resolution(FunctionalTest.evt_time, '1440x900')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1000, 440), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['res_1440x900', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, '1440x900')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(740, 400), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['resolutions', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'resolutions_2')
+        await self._enforce_resolution(FunctionalTest.evt_time, '1360x768')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1110, 80), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['res_1360x768', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, '1360x768')
         # antialiasing
         # antialiasing
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 440), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['aa', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'antialiasing_yes')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 440), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['aa', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'antialiasing_yes')
         # shadows
         # shadows
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 540), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['shadows', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'shadows_yes')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])  # back
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])
-
-    def _do_screenshots_play(self):
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'play_menu')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])  # back
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'back_from_play')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino scene
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_domino_instructions')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_domino')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(25, 740), 'left'])  # home
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['home', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'home_back_from_scene')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(70, 740), 'left'])  # info
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['information', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'info')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        # self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (430, 280), 'left'])  # drag a piece
-        # self._screenshot(FunctionalTest.screenshot_time, 'domino_dragged')
-        # self._event(FunctionalTest.evt_time, 'mouseclick', [(1220, 740), 'left'])  # rewind
-        # self._screenshot(FunctionalTest.screenshot_time, 'rewind')
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (550, 380), 'left'])  # drag a piece
-        # self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (715, 380), 'left'])  # drag a piece
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])  # play
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(630, 450), 'left'])  # home
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['home_win', 'left'])  # home
-        self._screenshot(FunctionalTest.screenshot_time, 'home_back_from_fail')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])  # play
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (550, 380), 'left'])  # drag a piece
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (715, 380), 'left'])  # drag a piece
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])  # play
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino_2')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])  # play
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (570, 380), 'left'])  # drag a piece
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(570, 355), (605, 355), 'right'])  # rotate the piece
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (715, 380), 'left'])  # drag a piece
-        self._enforce_res(FunctionalTest.evt_time, 'win')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])  # play
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'win_domino')
-        self._enforce_res(FunctionalTest.evt_time, '')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])  # play
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_box')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 540), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['shadows', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'shadows_yes')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])  # back
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])
+
+    async def _do_screenshots_play(self):
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'play_menu')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 680), 'left'])  # back
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['back', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'back_from_play')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino scene
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_domino_instructions')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_domino')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(25, 740), 'left'])  # home
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['home', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'home_back_from_scene')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(70, 740), 'left'])  # info
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['information', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'info')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        # await self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (430, 280), 'left'])  # drag a piece
+        # await self._screenshot(FunctionalTest.screenshot_time, 'domino_dragged')
+        # await self._event(FunctionalTest.evt_time, 'mouseclick', [(1220, 740), 'left'])  # rewind
+        # await self._screenshot(FunctionalTest.screenshot_time, 'rewind')
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (550, 380), 'left'])  # drag a piece
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a piece
+        # await self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (715, 380), 'left'])  # drag a piece
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])  # play
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(630, 450), 'left'])  # home
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['home_win', 'left'])  # home
+        await self._screenshot(FunctionalTest.screenshot_time, 'home_back_from_fail')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 140), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])  # play
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(230, 160), 'left'])  # domino
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(850, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (550, 380), 'left'])  # drag a piece
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a piece
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (715, 380), 'left'])  # drag a piece
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_1', 'left'])  # drag a piece  .49 .06
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])  # play
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino_2')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])  # play
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (570, 380), 'left'])  # drag a piece -1.54 .06
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_2', 'left'])  # drag a piece -1.54 .06
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(570, 355), (605, 355), 'right'])  # rotate the piece -1.05 .4
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_1', 'drag_stop_3', 'right'])  # rotate the piece -1.54 .4 -1.05 .4
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(35, 60), (715, 380), 'left'])  # drag a piece
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a piece
+        await self._enforce_result(FunctionalTest.evt_time, 'win')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])  # play
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'win_domino')
+        await self._enforce_result(FunctionalTest.evt_time, '')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])  # play
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_box')
         # scene 2
         # scene 2
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(880, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 620), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 540), 'left'])  # drag a box
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_box')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 620), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 540), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (705, 460), 'left'])  # drag a box
-        self._enforce_res(FunctionalTest.evt_time, 'win')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'win_box')
-        self._enforce_res(FunctionalTest.evt_time, '')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_box_domino')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(880, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 620), 'left'])  # drag a box .42 -3.29
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box .42 -3.29
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 540), 'left'])  # drag a box .42 -2.18
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_1', 'left'])  # drag a box .42 -2.18
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_box')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 620), 'left'])  # drag a box
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box
+       # await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (710, 540), 'left'])  # drag a box
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_1', 'left'])  # drag a box
+       # await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (705, 460), 'left'])  # drag a box .35 -1.06
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_2', 'left'])  # drag a box .35 -1.06
+        await self._enforce_result(FunctionalTest.evt_time, 'win')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'win_box')
+        await self._enforce_result(FunctionalTest.evt_time, '')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_box_domino')
         # scene 3
         # scene 3
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(930, 485), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (910, 440), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (910, 360), 'left'])  # drag a box
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_box_domino')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (910, 440), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (835, 250), 'left'])  # drag a box
-        self._enforce_res(FunctionalTest.evt_time, 'win')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'win_box_domino')
-        self._enforce_res(FunctionalTest.evt_time, '')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_basketball')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(930, 485), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (910, 440), 'left'])  # drag a box 3.21 -.78
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box 3.21 -.78
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (910, 360), 'left'])  # drag a box 3.21 .33
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_1', 'left'])  # drag a box 3.21 .33
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_box_domino')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (910, 440), 'left'])  # drag a box
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (835, 250), 'left'])  # drag a box 2.16 1.87
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_2', 'left'])  # drag a box 2.16 1.87
+        await self._enforce_result(FunctionalTest.evt_time, 'win')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'win_box_domino')
+        await self._enforce_result(FunctionalTest.evt_time, '')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_basketball')
         # scene 4
         # scene 4
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(870, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(55, 50), (650, 310), 'left'])  # drag a ball
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_basketball')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(55, 50), (380, 50), 'left'])  # drag a ball
-        self._enforce_res(FunctionalTest.evt_time, 'win')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'win_basketball')
-        self._enforce_res(FunctionalTest.evt_time, '')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_domino_box_basketball')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(870, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(55, 50), (650, 310), 'left'])  # drag a ball -.42 1.03
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a ball -.42 1.03
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_basketball')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(55, 50), (380, 50), 'left'])  # drag a ball -4.19 4.66
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_1', 'left'])  # drag a ball -4.19 4.66
+        await self._enforce_result(FunctionalTest.evt_time, 'win')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'win_basketball')
+        await self._enforce_result(FunctionalTest.evt_time, '')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_domino_box_basketball')
         # scene 5
         # scene 5
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(865, 490), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (580, 440), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(30, 60), (590, 370), 'left'])  # drag a piece
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino_box_basketball')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (580, 440), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(30, 60), (660, 440), 'left'])  # drag a piece
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(660, 425), (625, 425), 'right'])  # rotate a piece
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(660, 435), (650, 445), 'left'])  # drag a piece
-        self._enforce_res(FunctionalTest.evt_time, 'win')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'win_domino_box_basketball')
-        self._enforce_res(FunctionalTest.evt_time, '')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_teeter_tooter')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(865, 490), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (580, 440), 'left'])  # drag a box -1.4 -.78
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box -1.4 -.78
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(30, 60), (590, 370), 'left'])  # drag a piece -1.26 .2
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_1', 'drag_stop_1', 'left'])  # drag a piece -1.26 .2
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino_box_basketball')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(65, 60), (580, 440), 'left'])  # drag a box
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(30, 60), (660, 440), 'left'])  # drag a piece -.28 -.78
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_1', 'drag_stop_2', 'left'])  # drag a piece -.28 -.78
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(660, 425), (625, 425), 'right'])  # rotate a piece -.28 -.57 -.77 -.57
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_2', 'drag_stop_3', 'right'])  # rotate a piece -.28 -.57 -.77 -.57
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(660, 435), (650, 445), 'left'])  # drag a piece -.28 -.85 -.42 -.85
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_3', 'drag_stop_4', 'left'])  # drag a piece -.28 -.85 -.42 -.85
+        await self._enforce_result(FunctionalTest.evt_time, 'win')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'win_domino_box_basketball')
+        await self._enforce_result(FunctionalTest.evt_time, '')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_teeter_tooter')
         # scene 6
         # scene 6
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(870, 485), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (490, 300), 'left'])  # drag a box
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_teeter_tooter')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (490, 150), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(515, 115), (515, 122), 'right'])  # rotate a box
-        self._enforce_res(FunctionalTest.evt_time, 'win')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'win_teeter_tooter')
-        self._enforce_res(FunctionalTest.evt_time, '')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'scene_teeter_domino_box_basketball')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(870, 485), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (490, 300), 'left'])  # drag a box -2.65 1.18
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box -2.65 1.18
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_teeter_tooter')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (490, 150), 'left'])  # drag a box -2.65 3.27
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_1', 'left'])  # drag a box -2.65 3.27
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(515, 115), (515, 122), 'right'])  # rotate a box -2.3 3.75 -2.5 3.66
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_1', 'drag_stop_2', 'right'])  # rotate a box -2.3 3.75 -2.5 3.66
+        await self._enforce_result(FunctionalTest.evt_time, 'win')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'win_teeter_tooter')
+        await self._enforce_result(FunctionalTest.evt_time, '')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(735, 450), 'left'])  # next
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['next', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'scene_teeter_domino_box_basketball')
         # scene 7
         # scene 7
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(930, 485), 'left'])  # close instructions
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (155, 180), 'left'])  # drag a box
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_teeter_domino_box_basketball')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (170, 80), 'left'])  # drag a box
-        self._event(FunctionalTest.drag_time, 'mousedrag', [(195, 50), (195, 80), 'right'])  # rotate a box
-        self._enforce_res(FunctionalTest.evt_time, 'win')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
-        self._screenshot(16 + FunctionalTest.screenshot_time, 'win_teeter_domino_box_basketball')
-        self._enforce_res(FunctionalTest.evt_time, '')
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(630, 450), 'left'])  # home
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['home_win', 'left'])
-        self._screenshot(FunctionalTest.screenshot_time, 'home_from_play')
-
-    def _exit(self):
-        self._tasks += [(
-            self._curr_time + 3,
-            lambda: exit(),
-            'exit')]
-
-    def _do_screenshots_exit(self):
-        self._verify()
-        #self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 600), 'left'])
-        self._event(FunctionalTest.evt_time, 'mouseclick', ['exit', 'left'])
-        self._exit()
-
-    def _do_screenshots_2(self):
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(930, 485), 'left'])  # close instructions
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (155, 180), 'left'])  # drag a box -7.33 4.24
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_0', 'left'])  # drag a box -7.33 4.24
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_teeter_domino_box_basketball')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 450), 'left'])  # replay
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['replay', 'left'])
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(60, 60), (170, 80), 'left'])  # drag a box -7.12 4.24
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_0', 'drag_stop_1', 'left'])  # drag a box -7.12 4.24
+        #await self._event(FunctionalTest.drag_time, 'mousedrag', [(195, 50), (195, 80), 'right'])  # rotate a box -6.77 4.66 -6.77 4.24
+        await self._event(FunctionalTest.drag_time, 'mousedrag', ['drag_start_1', 'drag_stop_2', 'right'])  # rotate a box -6.77 4.66 -6.77 4.24
+        await self._enforce_result(FunctionalTest.evt_time, 'win')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(1340, 740), 'left'])  # play
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['right', 'left'])
+        await self._screenshot(16 + FunctionalTest.screenshot_time, 'win_teeter_domino_box_basketball')
+        await self._enforce_result(FunctionalTest.evt_time, '')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(630, 450), 'left'])  # home
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['home_win', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'home_from_play')
+
+    async def _do_screenshots_exit(self):
+        await self._verify(FunctionalTest.evt_time)
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', [(680, 600), 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['exit', 'left'])
+        await self._exit(FunctionalTest.evt_time)
+
+    async def _do_screenshots_2(self):
         info('_do_screenshots_2')
         info('_do_screenshots_2')
-        self._screenshot(FunctionalTest.start_time, 'main_menu_2')
-        self._do_screenshots_restore_options()
-        self._do_screenshots_play()
-        self._do_screenshots_exit()
+        await self._screenshot(FunctionalTest.start_time, 'main_menu_2')
+        await self._do_screenshots_restore_options()
+        await self._do_screenshots_play()
+        await self._do_screenshots_exit()
+
+    async def _do_screenshots_editor(self):
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['play', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['domino', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['close_instructions', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['wrench', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_scene', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_collapsed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_scene', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_expanded')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_save', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_save')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_scene_filename', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_filename_changed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_scene_filename', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_filename_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_scene_name', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_name_changed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_scene_name', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_name_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_scene_background', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_background_changed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_scene_background', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_background_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_scene_instructions', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return', 'BackSpace', 'BackSpace', 'BackSpace', 'BackSpace'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_sorting', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_sorting')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_scene', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_sorting_text', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_sorting_changed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_sorting_text', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'BackSpace'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_sorting_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_sorting', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_sorting_collapsed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_sorting', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_sorting_expanded')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_sorting_save', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_sorting_save')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_sorting_close', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_sorting_closed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_scene', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_class', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_start_class')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['start_class_box', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_start_class_box')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_strategy', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_start_strategy')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['start_strategy_upstrategy', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_start_strategy_up')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_count', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['0', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_count_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_count', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_count_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_scale', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_scale_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_scale', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_scale_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_mass', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_mass_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_mass', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_mass_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_restitution', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_restitution_modifed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_restitution', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_restitution_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_friction', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_friction_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_friction', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_friction_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_id', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_id_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_id', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_id_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_strategy_args', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_strategy_args_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_strategy_args', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_strategy_args_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_save', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_save')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_next', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_next')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_new', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_new')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_delete', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_delete')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_start_close', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_start_close')
+        #await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_new', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_scene', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['test_piece', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_strategy', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_inspector_strategy')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['inspector_strategy_upstrategy', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_inspector_strategy_up')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_position', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_position_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_position', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_position_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_roll', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_roll_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_roll', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_roll_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_scale', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_scale_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_scale', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_scale_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_mass', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_mass_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_mass', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_mass_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_restitution', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_restitution_modifed')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_restitution', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_restitution_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_friction', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_friction_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_friction', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_friction_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_id', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_id_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_id', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_id_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_strategy_args', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_strategy_args_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_strategy_args', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_strategy_args_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_delete', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_delete')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_close', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_close')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['drag_stop_0', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_test_position', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['1', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_test_position_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_test_position', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_test_position_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_test_id', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['a', 'b', 'c', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_test_id_modified')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_test_id', 'left'])
+        await self._event(FunctionalTest.evt_time, 'keyboard', ['BackSpace', 'BackSpace', 'BackSpace', 'Return'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_test_id_restored')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_test_delete', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_test_delete')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_inspector_test_close', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_inspector_test_close')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['collapse_button_scene', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_new_item', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_new_item')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['new_item_box', 'left'])
+        await self._screenshot(FunctionalTest.screenshot_time, 'editor_new_item')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_close', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_save_dialog_yes', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_close')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['wrench', 'left'])
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['editor_new', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'editor_scene_new')
+        await self._event(FunctionalTest.evt_time, 'mouseclick', ['home', 'left'])
+        await self._screenshot(FunctionalTest.start_time, 'home_from_editor')
+
+    async def _do_screenshots_3(self):
+        info('_do_screenshots_3')
+        await self._screenshot(FunctionalTest.start_time, 'main_menu_3')
+        await self._do_screenshots_editor()
+        await self._do_screenshots_exit()
 
     def _do_screenshots(self, idx):
 
     def _do_screenshots(self, idx):
-        [self._do_screenshots_1, self._do_screenshots_2][int(idx) - 1]()
+        asyncio.run(
+            [self._do_screenshots_1, self._do_screenshots_2, self._do_screenshots_3][int(idx) - 1]()
+        )
 
 
 class TestApp(ShowBase):
 
 
 class TestApp(ShowBase):
@@ -459,7 +741,7 @@ class TestApp(ShowBase):
     def __init__(self):
         ShowBase.__init__(self)
         offset = int(argv[2]) if len(argv) >= 3 else 0
     def __init__(self):
         ShowBase.__init__(self)
         offset = int(argv[2]) if len(argv) >= 3 else 0
-        fun_test = FunctionalTest(int(argv[1]), offset)
+        FunctionalTest(int(argv[1]), offset)
 
 
 TestApp().run()
 
 
 TestApp().run()
diff --git a/tests/pmachines/__init__.py b/tests/pmachines/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/pmachines/audio/__init__.py b/tests/pmachines/audio/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/pmachines/audio/test_music.py b/tests/pmachines/audio/test_music.py
new file mode 100644 (file)
index 0000000..38abcd6
--- /dev/null
@@ -0,0 +1,43 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from os.path import basename
+from glob import glob
+from unittest import TestCase
+from unittest.mock import patch
+from direct.showbase.ShowBase import ShowBase
+from panda3d.core import AudioSound
+from pmachines.audio.music import MusicManager
+from pmachines.audio import music
+
+
+class MusicTests(TestCase):
+
+    def setUp(self):
+        self.__app = ShowBase()
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    @patch.object(music, 'AudioTools', autospec=True)
+    def test_music(self, a):
+        m = MusicManager(.8, 'pmachines')
+        a.set_volume.assert_called_once()
+        a_args = a.set_volume.call_args_list[0].args
+        self.assertAlmostEqual(a_args[0], .8, delta=.01)
+        self.assertEqual(len(a_args), 1)
+        tasks = [t.name for t in taskMgr.getTasks() + taskMgr.getDoLaters()]
+        self.assertIn('on frame music', tasks)
+        _music = m._MusicManager__music
+        musics = list(map(basename, glob('assets/audio/music/*.ogg')))
+        self.assertIn(_music.get_name(), musics)
+        self.assertEqual(_music.status(), AudioSound.PLAYING)
+        prev_music_name = _music.get_name()
+        m._MusicManager__restart_music()
+        _music = m._MusicManager__music
+        self.assertIn(_music.get_name(), musics)
+        self.assertEqual(_music.status(), AudioSound.PLAYING)
+        self.assertNotEqual(_music.get_name(), prev_music_name)
index fbd89e0341bef4eebdc8a26c60480ad21492088f..911a50e567096b913dfa4e14a3a3da3d039e3ed3 100644 (file)
@@ -1,22 +1,27 @@
 from pathlib import Path
 from pathlib import Path
-from datetime import datetime
+#from datetime import datetime
 from itertools import product
 from logging import info
 import sys
 if '' in sys.path: sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent))
 from unittest import TestCase
 from itertools import product
 from logging import info
 import sys
 if '' in sys.path: sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent))
 from unittest import TestCase
-from shutil import rmtree, copy
+from shutil import rmtree#, copy
 from time import sleep
 from os import system, remove, environ
 from time import sleep
 from os import system, remove, environ
-from os.path import exists, basename, join
+from os.path import exists, join  # basename
 from glob import glob
 from glob import glob
+#from subprocess import Popen, STDOUT
 from panda3d.core import Filename
 from panda3d.core import Filename
-from ya2.build.build import exec_cmd, _branch, ver
+from ya2.build.build import exec_cmd, _branch, _version
 
 
 class FunctionalTests(TestCase):
 
 
 
 class FunctionalTests(TestCase):
 
+    def __init__(self, *args, **kwargs):
+        super(FunctionalTests, self).__init__(*args, **kwargs)
+        self.__itchio_updated = False
+
     def __clean(self):
         paths = [
             'tests/functional',
     def __clean(self):
         paths = [
             'tests/functional',
@@ -31,7 +36,7 @@ class FunctionalTests(TestCase):
             '/home/flavio/.wine/drive_c/users/flavio/AppData/Local']
         files = [
             'pmachines/options.ini',
             '/home/flavio/.wine/drive_c/users/flavio/AppData/Local']
         files = [
             'pmachines/options.ini',
-            'pmachines/obs_version.txt']
+            'pmachines/observed_version.txt']
         for dir_file in product(dirs, files):
             _file = join(*dir_file)
             if exists(_file):
         for dir_file in product(dirs, files):
             _file = join(*dir_file)
             if exists(_file):
@@ -61,80 +66,126 @@ class FunctionalTests(TestCase):
 
     def __similar_images(self, ref_img, tst_img):
         cmd = 'magick compare -metric NCC %s %s diff.png' % (ref_img, tst_img)
 
     def __similar_images(self, ref_img, tst_img):
         cmd = 'magick compare -metric NCC %s %s diff.png' % (ref_img, tst_img)
-        res = exec_cmd(cmd)
+        res = exec_cmd(cmd, False)
         if exists('diff.png'): remove('diff.png')
         print('compare %s %s: %s' % (ref_img, tst_img, res))
         return float(res) > .8
 
         if exists('diff.png'): remove('diff.png')
         print('compare %s %s: %s' % (ref_img, tst_img, res))
         return float(res) > .8
 
-    def __test_template(self, cmd, path):
-        if environ.get('FUNCTIONAL') != '1':
+    def __test_template(self, app_cmd, path):
+        if True:  #we're doing system tests  # environ.get('FUNCTIONAL') != '1':
             self.skipTest('skipped functional tests')
         self.__clean()
             self.skipTest('skipped functional tests')
         self.__clean()
+
+        # previous_logs = glob(path + '/*.log')
+        # info('launching ' + app_cmd)
+        # print('launching ' + app_cmd)
+        # p_app = Popen(app_cmd, shell=True, stderr=STDOUT, universal_newlines=True)
+        # sleep(8)
+        # p_app_out, p_app_err = p_app.communicate()
+        # info('output (%s): %s' % (app_cmd, p_app_out))
+        # info('error (%s): %s' % (app_cmd, p_app_err))
+        # print('output (%s): %s' % (app_cmd, p_app_out))
+        # print('error (%s): %s' % (app_cmd, p_app_err))
+        # self.assertEqual(p_app.returncode, 0, 'error while executing ' + app_cmd)
+        # current_logs = glob(path + '/*.log')
+        # new_logs = [l for l in current_logs if l not in previous_logs]
+        # self.assertEqual(len(new_logs), 1)
+        # with open(new_logs[0]) as f: new_log = f.read()
+        # self.assertContains('the application has been started correctly', new_log)
+
         self.__awake()
         system('amixer sset Master 0%')
         self.__awake()
         system('amixer sset Master 0%')
-        ret = system(cmd)
-        self.assertEqual(ret, 0, 'error while executing ' + cmd)
-        files = glob(str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/*.png' % _branch())
-        self.assertGreater(len(files), 1)
-        for fname in files:
-            self.assertTrue(exists(path + basename(fname)), '%s does not exist' % (path + basename(fname)))
-            similar = self.__similar_images(
-                str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/' % _branch() + basename(fname),
-                path + basename(fname)),
-            'error while comparing %s and %s' % (
-                str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/' % _branch() + basename(fname),
-                path + basename(fname))
-            if not similar:
-                timestamp = datetime.now().strftime('%y%m%d%H%M%S%f')
-                copy(path + basename(fname), '~/Desktop/' + basename(fname)[:-4] + timestamp + '.png')
-            self.assertTrue(similar)
+        #ret = system(cmd)
+        #proc = run(cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=check)
+        #return proc.stdout.strip() + proc.stderr.strip()
+        # for test_cmd in test_cmds:
+        #     sleep(8)
+        #     info('launching ' + app_cmd)
+        #     print('launching ' + app_cmd)
+        #     p_app = Popen(app_cmd, shell=True, stderr=STDOUT, universal_newlines=True)
+        #     sleep(30)
+        #     info('launching ' + test_cmd)
+        #     print('launching ' + test_cmd)
+        #     p_test = Popen(test_cmd, shell=True, stderr=STDOUT, universal_newlines=True)
+        #     p_app_out, p_app_err = p_app.communicate()
+        #     t_out, t_err = p_test.communicate()
+        #     info('output (%s): %s' % (app_cmd, p_app_out))
+        #     info('error (%s): %s' % (app_cmd, p_app_err))
+        #     info('output (%s): %s' % (test_cmd, t_out))
+        #     info('error (%s): %s' % (test_cmd, t_err))
+        #     print('output (%s): %s' % (app_cmd, p_app_out))
+        #     print('error (%s): %s' % (app_cmd, p_app_err))
+        #     print('output (%s): %s' % (test_cmd, t_out))
+        #     print('error (%s): %s' % (test_cmd, t_err))
+        #     self.assertEqual(p_app.returncode, 0, 'error while executing ' + app_cmd)
+        #     self.assertEqual(p_test.returncode, 0, 'error while executing ' + test_cmd)
+        # files = glob(str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/*.png' % _branch())
+        # self.assertGreater(len(files), 1)
+        # for fname in files:
+        #     self.assertTrue(exists(path + basename(fname)), '%s does not exist' % (path + basename(fname)))
+        #     similar = self.__similar_images(
+        #         str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/' % _branch() + basename(fname),
+        #         path + basename(fname)),
+        #     'error while comparing %s and %s' % (
+        #         str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/' % _branch() + basename(fname),
+        #         path + basename(fname))
+        #     if not similar:
+        #         timestamp = datetime.now().strftime('%y%m%d%H%M%S%f')
+        #         copy(path + basename(fname), '~/Desktop/' + basename(fname)[:-4] + timestamp + '.png')
+        #     self.assertTrue(similar)
 
     def test_code(self):
         info('test_code')
 
     def test_code(self):
         info('test_code')
+        if environ.get('FUNCTIONALPOST') == '1':
+            self.skipTest('skipped functional-post test: test_code')
         self.__test_template(
         self.__test_template(
-            'timeout 720s ~/venv/bin/python main.py --functional-test & '
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 1; sleep 5; '
-            'timeout 720s ~/venv/bin/python main.py --functional-test & '
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 2',
+            '/home/flavio/venv/bin/python main.py --functional-test',
             str(Path.home()) + '/.local/share/pmachines/tests/functional/')
 
     def test_appimage(self):
         info('test_appimage')
             str(Path.home()) + '/.local/share/pmachines/tests/functional/')
 
     def test_appimage(self):
         info('test_appimage')
+        if environ.get('FUNCTIONALPOST') == '1':
+            self.skipTest('skipped functional-post test: test_appimage')
         bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
         bld_branch = '' if bld_branch == 'stable' else ('-' + bld_branch)
         self.__test_template(
         bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
         bld_branch = '' if bld_branch == 'stable' else ('-' + bld_branch)
         self.__test_template(
-            'timeout 720s ./dist/Pmachines%s-x86_64.AppImage --functional-test & '
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 1; sleep 5; '
-            'timeout 720s ./dist/Pmachines%s-x86_64.AppImage --functional-test & ' % (bld_branch, bld_branch) +
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 2',
+            'timeout 7200s ./dist/Pmachines%s-x86_64.AppImage --functional-test' % bld_branch,
+            #['timeout 7200s ~/venv/bin/python -m tests.functional_test.py 1',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 2',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 3'],
             str(Path.home()) + '/.local/share/pmachines/tests/functional/')
 
             str(Path.home()) + '/.local/share/pmachines/tests/functional/')
 
-    def test_flatpak(self):
-        info('test_flatpak')
-        if environ.get('FUNCTIONALPOST') != '1':
-            self.skipTest('skipped functional-post tests')
-        bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
-        cmd = 'flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch
-        info('executing: %s' % cmd)
-        #system(cmd)
-        fout = exec_cmd(cmd)
-        info('executed: %s' % cmd)
-        info(fout)
-        self.__test_template(
-            'timeout 720s flatpak run it.ya2.Pmachines//%s --functional-test & '
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 1; sleep 5; '
-            'timeout 720s flatpak run it.ya2.Pmachines//%s --functional-test & ' % (bld_branch, bld_branch) +
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 2',
-            str(Path.home()) + '/.var/app/it.ya2.Pmachines/data/pmachines/tests/functional/')
+    def test_flatpak(self):
+        info('test_flatpak')
+        if environ.get('FUNCTIONALPOST') != '1':
+            self.skipTest('skipped functional-post tests')
+        bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+        cmd = 'flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch
+        info('executing: %s' % cmd)
+        #system(cmd)
+        fout = exec_cmd(cmd)
+        info('executed: %s' % cmd)
+        info(fout)
+        self.__test_template(
+            'timeout 720s flatpak run it.ya2.Pmachines//%s --functional-test & '
+            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 1; sleep 5; '
+            'timeout 720s flatpak run it.ya2.Pmachines//%s --functional-test & ' % (bld_branch, bld_branch) +
+            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 2',
+            str(Path.home()) + '/.var/app/it.ya2.Pmachines/data/pmachines/tests/functional/')
 
     def __update_itchio(self):
 
     def __update_itchio(self):
+        info('update_itchio::itchio_updated::cond: %s' % str(self.__itchio_updated))
+        if self.__itchio_updated:
+            return
         system('/home/flavio/.itch/itch')
         system('/home/flavio/.itch/itch')
-        sleep(5)
+        sleep(120)
         system('xdotool mousemove 860 620')
         system('xdotool mousemove 860 620')
-        sleep(1)
+        sleep(3)
         system('xdotool click 1')
         sleep(300)
         system('killall itch')
         system('xdotool click 1')
         sleep(300)
         system('killall itch')
+        self.__itchio_updated = True
+        info('update_itchio::itchio_updated::done: %s' % str(self.__itchio_updated))
 
     def test_itchio(self):
         info('test_itchio')
 
     def test_itchio(self):
         info('test_itchio')
@@ -144,53 +195,57 @@ class FunctionalTests(TestCase):
             return
         self.__update_itchio()
         self.__test_template(
             return
         self.__update_itchio()
         self.__test_template(
-            'timeout 720s /home/flavio/.config/itch/apps/pmachines/pmachines --functional-test & '
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 1; sleep 5; '
-            'timeout 720s /home/flavio/.config/itch/apps/pmachines/pmachines --functional-test & '
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 2',
+            'timeout 7200s /home/flavio/.config/itch/apps/pmachines/pmachines --functional-test',
+            ['timeout 7200s ~/venv/bin/python -m tests.functional_test.py 1',
+             'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 2',
+             'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 3'],
             str(Path.home()) + '/.local/share/pmachines/tests/functional/')
 
     def test_windows(self):
         info('test_windows')
             str(Path.home()) + '/.local/share/pmachines/tests/functional/')
 
     def test_windows(self):
         info('test_windows')
+        if environ.get('FUNCTIONALPOST') == '1':
+            self.skipTest('skipped functional-post test: test_windows')
         system('pkill -f "pmachines.exe"')
         abspath = str(Path(__file__).parent.parent) + '/build/win_amd64/pmachines.exe'
         self.__test_template(
         system('pkill -f "pmachines.exe"')
         abspath = str(Path(__file__).parent.parent) + '/build/win_amd64/pmachines.exe'
         self.__test_template(
-            'timeout 720s wine %s --functional-test & ' % abspath +
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 1; sleep 5; '
-            'timeout 720s wine %s --functional-test & ' % abspath +
-            'timeout 720s ~/venv/bin/python -m tests.functional_test.py 2',
+            'timeout 7200s wine %s --functional-test' % abspath,
+            #['timeout 7200s ~/venv/bin/python -m tests.functional_test.py 1',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 2',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 3'],
             str(Path.home()) + '/.wine/drive_c/users/flavio/AppData/Local/pmachines/tests/functional/')
 
     def test_versions(self):
         info('test_versions')
         if environ.get('FUNCTIONAL') != '1':
             self.skipTest('skipped functional tests')
             str(Path.home()) + '/.wine/drive_c/users/flavio/AppData/Local/pmachines/tests/functional/')
 
     def test_versions(self):
         info('test_versions')
         if environ.get('FUNCTIONAL') != '1':
             self.skipTest('skipped functional tests')
-        bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
-        with open('/home/flavio/builders/pmachines_builder/last_bld.txt') as f:
+        #bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+        with open('/home/flavio/builders/pmachines/last_bld.txt') as f:
             lines = f.readlines()
         for line in lines:
             if line.strip().split()[0] == _branch():
                 commit = line.strip().split()[1][:7]
             lines = f.readlines()
         for line in lines:
             if line.strip().split()[0] == _branch():
                 commit = line.strip().split()[1][:7]
-        _ver = ver
+        __ver = _version()
         if _branch() == 'stable':
             with open('/home/flavio/builders/pmachines_builder/pmachines/assets/version.txt') as fver:
         if _branch() == 'stable':
             with open('/home/flavio/builders/pmachines_builder/pmachines/assets/version.txt') as fver:
-                _ver = fver.read().strip() + '-'
-        exp = '%s-%s' % (_ver, commit)
+                __ver = fver.read().strip() + '-'
+        exp = '%s-%s' % (__ver, commit)
+        appimage_branch = '' if _branch() == 'stable' else ('-' + _branch())
+        if appimage_branch == '-master': appimage_branch = '-alpha'
         cmds = [
         cmds = [
-            ('timeout 720s ./build/manylinux2010_x86_64/pmachines --version', str(Filename.get_user_appdata_directory()) + '/pmachines/obs_version.txt'),
-            ('timeout 720s ./dist/Pmachines-%s-x86_64.AppImage --version' % bld_branch, str(Filename.get_user_appdata_directory()) + '/pmachines/obs_version.txt'),
-            ('timeout 720s wine ./build/win_amd64/pmachines.exe --version', '/home/flavio/.wine/drive_c/users/flavio/AppData/Local/pmachines/obs_version.txt')
+            ('timeout 7200s ./build/manylinux2010_x86_64/pmachines --version', str(Filename.get_user_appdata_directory()) + '/pmachines/observed_version.txt'),
+            ('timeout 7200s ./dist/Pmachines%s-x86_64.AppImage --version' % appimage_branch, str(Filename.get_user_appdata_directory()) + '/pmachines/observed_version.txt'),
+            ('timeout 7200s wine ./build/win_amd64/pmachines.exe --version', '/home/flavio/.wine/drive_c/users/flavio/AppData/Local/pmachines/observed_version.txt')
         ]
         if environ.get('FUNCTIONALPOST') == '1':
             if _branch() == 'master':
                 self.__update_itchio()
         ]
         if environ.get('FUNCTIONALPOST') == '1':
             if _branch() == 'master':
                 self.__update_itchio()
-                cmds += [('timeout 720s /home/flavio/.config/itch/apps/pmachines/pmachines --version', str(Filename.get_user_appdata_directory()) + '/pmachines/obs_version.txt')]
-            cmds += [('timeout 720s flatpak run it.ya2.Pmachines//%s --version' % bld_branch, '/home/flavio/.var/app/it.ya2.Pmachines/data/pmachines/obs_version.txt')]
-        info('executing flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch)
-        #system('flatpak update -y --user it.ya2.Pmachines//%s' % bld_branch)
-        fout = exec_cmd('flatpak update -y --user it.ya2.Pmachines//%s' % bld_branch)
-        info('executed flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch)
-        info(fout)
+                cmds += [('timeout 7200s /home/flavio/.config/itch/apps/pmachines/pmachines --version', str(Filename.get_user_appdata_directory()) + '/pmachines/observed_version.txt')]
+            # cmds += [('timeout 7200s flatpak run it.ya2.Pmachines//%s --version' % bld_branch, '/home/flavio/.var/app/it.ya2.Pmachines/data/pmachines/observed_version.txt')]
+        info('executing flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch)
+        # #system('flatpak update -y --user it.ya2.Pmachines//%s' % bld_branch)
+        fout = exec_cmd('flatpak update -y --user it.ya2.Pmachines//%s' % bld_branch)
+        info('executed flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch)
+        info(fout)
         for cmd in cmds:
             if exists(cmd[1]):
                 remove(cmd[1])
         for cmd in cmds:
             if exists(cmd[1]):
                 remove(cmd[1])
diff --git a/tests/test_l10n.py b/tests/test_l10n.py
new file mode 100644 (file)
index 0000000..a4d636a
--- /dev/null
@@ -0,0 +1,18 @@
+from unittest import TestCase
+from glob import glob
+import polib
+
+
+class L10nTests(TestCase):
+
+    def test_l10n_complete(self):
+        language_files = glob('assets/locale/po/*.po')
+        for language_file in language_files: self.__test_language_file(language_file)
+
+    def __test_language_file(self, language_file):
+        pofile = polib.pofile(language_file)
+        for entry in pofile: self.__test_entry(entry)
+
+    def __test_entry(self, entry):
+        self.assertTrue(entry.msgstr, 'empty: ' + entry.msgid)
+        self.assertFalse(entry.fuzzy, 'fuzzy: ' + entry.msgid)
diff --git a/tests/test_lint.py b/tests/test_lint.py
new file mode 100644 (file)
index 0000000..9a07766
--- /dev/null
@@ -0,0 +1,21 @@
+from unittest import TestCase
+import multiprocessing
+from os import walk, system, environ
+
+
+def _test_py_file(py_file):
+    environ['PYFLAKES_BUILTINS'] = '_,base,taskMgr,messenger,loader,pixel2d,globalClock,render,render2d,aspect2d'
+    command = '/home/flavio/venv/bin/pyflakes ' + py_file
+    return system(command)
+
+
+class LintTests(TestCase):
+
+    def test_lint(self):
+        fnames = [root + '/' + fname for root, _, _fnames in walk('.') for fname in _fnames if fname.endswith('.py')]
+        with multiprocessing.Pool() as pool:
+            results = pool.map(_test_py_file, fnames)
+        fnames = [(fname, res) for fname, res in zip(fnames, results) if res]
+        fnames = [fname[0] for fname in fnames]
+        fnames = ', '.join(fnames)
+        self.assertTrue(all(not res for res in results), 'pyflakes: ' + fnames)
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644 (file)
index 0000000..ab93e6f
--- /dev/null
@@ -0,0 +1,97 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from os import makedirs
+from shutil import rmtree
+from unittest import TestCase
+from unittest.mock import patch, MagicMock
+from main import Main
+import logging
+from ya2.utils.dictfile import DctFile
+import importlib
+import main
+import pmachines.application.application
+
+
+class MainTests(TestCase):
+
+    def setUp(self):
+        self.__optfile = DctFile('options.ini')
+        self.__verbose_val = self.__optfile['development']['verbose_log']
+        makedirs('/tmp/.mount_Pmachines_unit_test/usr/bin', exist_ok=True)
+        with open('/tmp/.mount_Pmachines_unit_test/usr/bin/appimage_version.txt', 'w') as f:
+            f.write('version_test')
+
+    def tearDown(self):
+        self.__optfile['development']['verbose_log'] = self.__verbose_val
+        self.__optfile.store()
+        rmtree('/tmp/.mount_Pmachines_unit_test')
+
+    def test_logging(self):
+        self.assertEqual(logging.root.handlers[0].formatter._fmt, '%(asctime)s %(message)s')
+        self.assertEqual(logging.root.handlers[0].formatter.datefmt, '%H:%M:%S')
+        self.__optfile['development']['verbose_log'] = 0
+        self.__optfile.store()
+        importlib.reload(main)
+        self.assertEqual(logging.root.level, logging.INFO)
+        self.__optfile['development']['verbose_log'] = 1
+        self.__optfile.store()
+        importlib.reload(main)
+        self.assertEqual(logging.root.level, logging.DEBUG)
+
+    def test_update(self):
+        with (patch.object(main, '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()
+            _main._Main__pmachines.run = MagicMock()
+            _main.run()
+            _main._Main__pmachines.destroy()
+        _main._Main__appimage_builder.update.assert_not_called()
+        with (patch.object(sys, '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()
+            _main._Main__pmachines.destroy()
+        _main._Main__appimage_builder.update.assert_called_once()
+
+    def test_version(self):
+        with (patch.object(main, '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.application.application, 'argv', ['python -m unittest', '--version'])):
+            _main = Main()
+            _main._Main__run_game = MagicMock()
+            _main.run()
+            _main._Main__pmachines.destroy()
+            _main._Main__run_game.assert_not_called()
+
+    @patch.object(main, '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()
+        _main.run()
+        _main._Main__pmachines.destroy()
+        _main._Main__pmachines.run.assert_called_once()
+
+    @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):
+        _main = Main()
+        _main._Main__pmachines.run = MagicMock(side_effect=Exception)
+        _main.run()
+        _main._Main__pmachines.destroy()
+        _main._Main__pmachines.run.trowed_exception()
+        print_exc_mock.assert_called_once()
index 9d16eb39355cf5aa3fe77e3dc0e27c6d26fd0947..c16d8c51febf5dba264ae5cb42dffdaec071c797 100644 (file)
@@ -2,26 +2,136 @@ from pathlib import Path
 import sys
 if '' in sys.path: sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent))
 import sys
 if '' in sys.path: sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent))
-from os import remove
-from os.path import exists
 from unittest import TestCase
 from unittest import TestCase
+from unittest.mock import patch
 from setuptools.dist import Distribution
 from setuptools.dist import Distribution
-from setup import AbsCmd
+from setuptools.command.develop import develop
+from setup import BaseCommand, SetupDevelopmentCommand, ModelsCommand, \
+    ImagesCommand, L10nCommand, BDistAppsCommand
 from ya2.build.build import exec_cmd
 from ya2.build.build import exec_cmd
-
+import os
+import direct.dist.commands, setup
 
 class SetupTests(TestCase):
 
 
 class SetupTests(TestCase):
 
-    def setUp(self):
-        self._prev_argv = sys.argv
+    # def setUp(self):
+    #     self._prev_argv = sys.argv
+
+    # def tearDown(self):
+    #     sys.argv = self._prev_argv
 
 
-    def tearDown(self):
-        sys.argv = self._prev_argv
+    @patch.object(develop, 'run')
+    @patch('setup.Popen', autospec=True)
+    def test_setup_development_command(self, popen, dev_run):
+        s = SetupDevelopmentCommand(Distribution())
+        s.run()
+        dev_run.assert_called_once()
+        for i, arg_list in enumerate(popen.call_args_list):
+            arg_list = arg_list.args[0]
+            assert 'python' in arg_list[0]
+            assert 'setup.py' in arg_list[1]
+            a = ['lang', 'models', 'images']
+            assert a[i] == arg_list[2]
+        assert popen.return_value.communicate.call_count == 3
 
     def test_cores(self):
 
     def test_cores(self):
-        cmd = AbsCmd(Distribution())
+        cmd = BaseCommand(Distribution())
         ncores = int(exec_cmd('grep -c ^processor /proc/cpuinfo'))
         self.assertEqual(cmd.cores, ncores)
         ncores = int(exec_cmd('grep -c ^processor /proc/cpuinfo'))
         self.assertEqual(cmd.cores, ncores)
-        sys.argv += ['--cores=2']
-        cmd = AbsCmd(Distribution())
-        self.assertEqual(cmd.cores, 2)
+        with patch.object(setup, 'argv', ['python setup.py', '--cores=2']):
+            cmd = BaseCommand(Distribution())
+            self.assertEqual(cmd.cores, 2)
+
+    @patch('setup.ModelsBuilder', autospec=True)
+    def test_models_command(self, mb_mock):
+        cmd = ModelsCommand(Distribution())
+        cmd.run()
+        assert mb_mock.call_count == 1
+        ncores = int(exec_cmd('grep -c ^processor /proc/cpuinfo'))
+        assert mb_mock.call_args_list[0].args == ('assets/models', ncores)
+        mb_mock.return_value.build.assert_called_once()
+        build_mock = mb_mock.return_value.build
+        build_mock.assert_called_once()
+        assert not build_mock.call_args_list[0].args
+
+    @patch('setup.ImagesBuilder', autospec=True)
+    @patch('setup.ScreenshotsBuilder', autospec=True)
+    def test_images_command(self, screenshot_mock, images_mock):
+        cmd = ImagesCommand(Distribution())
+        cmd.run()
+        screenshot_mock.assert_called_once()
+        assert len(screenshot_mock.call_args_list[0].args)
+        assert all(isinstance(a, str) for a in screenshot_mock.call_args_list[0].args[0])
+        build_mock = images_mock.return_value.build
+        build_mock.assert_called_once()
+        assert not build_mock.call_args_list[0].args
+        assert images_mock.call_count == 1
+        assert all('assets/images' in a for a in images_mock.call_args_list[0].args[0])
+        ncores = int(exec_cmd('grep -c ^processor /proc/cpuinfo'))
+        assert images_mock.call_args_list[0].args[1] == ncores
+        images_mock.return_value.build.assert_called_once()
+        build_mock = images_mock.return_value.build
+        build_mock.assert_called_once()
+        assert not build_mock.call_args_list[0].args
+
+    @patch('setup.LanguageBuilder', autospec=True)
+    def test_l10n_command(self, l_mock):
+        cmd = L10nCommand(Distribution())
+        cmd.run()
+        assert l_mock.call_count == 1
+        build_args = l_mock.call_args_list[0].args
+        assert isinstance(build_args[0], str)
+        assert 'locale' in build_args[1]
+        assert isinstance(build_args[2], str)
+        assert 'locale' in build_args[3]
+        build_mock = l_mock.return_value.build
+        build_mock.call_count == 2  # build_mock.assert_called_once()
+        assert not build_mock.call_args_list[0].args
+
+    @patch.object(direct.dist.commands.bdist_apps, 'run', autospec=True)
+    @patch.object(os, 'system', autospec=True)
+    @patch('setup.AppImageBuilder', autospec=True)
+    def test_bdistapps_command(self, a_mock, o_mock, b_mock):
+        cmd = BDistAppsCommand(Distribution())
+        cmd.run()
+        assert o_mock.call_count == 1
+        assert 'patch' in o_mock.call_args_list[0].args[0]
+        assert a_mock.call_count == 1
+        assert len(a_mock.call_args_list[0].args) == 1
+        build_mock = a_mock.return_value.build
+        build_mock.assert_called_once()
+        build_args = build_mock.call_args_list[0].args
+        assert isinstance(build_args[0], str)
+        assert isinstance(build_args[1], str)
+        assert 'https' in build_args[2]
+        with patch.object(setup, 'argv', ['python setup.py', '--no-linux=1']):
+            cmd = BDistAppsCommand(Distribution())
+            cmd.run()
+            assert a_mock.call_count == 1
+
+    def test_setup(self):
+        d = setup._build_setup_arguments()
+        assert isinstance(d['name'], str)
+        assert isinstance(d['version'], str)
+        assert 'win_amd64' in d['options']['build_apps']['platforms']
+        assert 'manylinux2010_x86_64' in d['options']['build_apps']['platforms']
+        assert len(d['options']['build_apps']['platforms']) == 2
+        assert 'win_amd64' in d['options']['bdist_apps']['installers']
+        assert 'manylinux2010_x86_64' in d['options']['bdist_apps']['installers']
+        assert len(d['options']['bdist_apps']['installers']) == 2
+        with patch.object(setup, 'argv', ['python setup.py', '--no-linux']):
+            d = setup._build_setup_arguments()
+            assert 'win_amd64' in d['options']['build_apps']['platforms']
+            assert 'manylinux2010_x86_64' not in d['options']['build_apps']['platforms']
+            assert len(d['options']['build_apps']['platforms']) == 1
+            assert 'win_amd64' in d['options']['bdist_apps']['installers']
+            assert 'manylinux2010_x86_64' not in d['options']['bdist_apps']['installers']
+            assert len(d['options']['bdist_apps']['installers']) == 1
+        with patch.object(setup, 'argv', ['python setup.py', '--no-windows']):
+            d = setup._build_setup_arguments()
+            assert 'win_amd64' not in d['options']['build_apps']['platforms']
+            assert 'manylinux2010_x86_64' in d['options']['build_apps']['platforms']
+            assert len(d['options']['build_apps']['platforms']) == 1
+            assert 'win_amd64' not in d['options']['bdist_apps']['installers']
+            assert 'manylinux2010_x86_64' in d['options']['bdist_apps']['installers']
+            assert len(d['options']['bdist_apps']['installers']) == 1
diff --git a/tests/test_system.py b/tests/test_system.py
new file mode 100644 (file)
index 0000000..18e99dc
--- /dev/null
@@ -0,0 +1,257 @@
+from pathlib import Path
+#from datetime import datetime
+from itertools import product
+from logging import info
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from shutil import rmtree#, copy
+from time import sleep
+from os import system, remove, environ
+from os.path import exists, join  # basename
+from glob import glob
+from subprocess import Popen, STDOUT
+from panda3d.core import Filename
+from ya2.build.build import exec_cmd, _branch, _version
+
+
+class SystemTests(TestCase):
+
+    def __init__(self, *args, **kwargs):
+        super(SystemTests, self).__init__(*args, **kwargs)
+        self.__itchio_updated = False
+
+    def __clean(self):
+        paths = [
+            'tests/functional',
+            str(Path.home()) + '/.local/share/pmachines/tests/functional/',
+            str(Path.home()) + '/.var/app/it.ya2.Pmachines/data/pmachines/tests/functional/',
+            str(Path.home()) + '/.wine/drive_c/users/flavio/AppData/Local/pmachines/tests/functional/']
+        for path in paths:
+            rmtree(path, ignore_errors=True)
+        dirs = [
+            Filename().get_user_appdata_directory(),
+            '/home/flavio/.var/app/it.ya2.Pmachines/data',
+            '/home/flavio/.wine/drive_c/users/flavio/AppData/Local']
+        files = [
+            'pmachines/options.ini',
+            'pmachines/observed_version.txt']
+        for dir_file in product(dirs, files):
+            _file = join(*dir_file)
+            if exists(_file):
+                remove(_file)
+                info('%s removed' % _file)
+            else:
+                info('%s does not exist' % _file)
+        opt_ini = str(Path.home()) + '/builders/pmachines_builder/pmachines/options.ini'
+        if exists(opt_ini):
+            remove(opt_ini)
+        system('pkill -f "pmachines.exe"')
+
+    # def __awake(self):
+    #     system('xdotool key shift')
+
+    def setUp(self):
+        self.__clean()
+
+    def tearDown(self):
+        pass  # self.__clean()
+
+    # def test_ref(self):
+    #     info('test_ref')
+    #     path = 'pmachines/tests/functional_ref_%s/*.png' % _branch()
+    #     files = glob(join(Filename().get_user_appdata_directory(), path))
+    #     self.assertGreater(len(files), 1)
+
+    # def __similar_images(self, ref_img, tst_img):
+    #     cmd = 'magick compare -metric NCC %s %s diff.png' % (ref_img, tst_img)
+    #     res = exec_cmd(cmd, False)
+    #     if exists('diff.png'): remove('diff.png')
+    #     print('compare %s %s: %s' % (ref_img, tst_img, res))
+    #     return float(res) > .8
+
+    def __test_template(self, app_cmd, path):
+        # if True:  #we're doing system tests  # environ.get('FUNCTIONAL') != '1':
+        #     self.skipTest('skipped functional tests')
+        self.__clean()
+
+        previous_logs = glob(path + '/**/*.log', recursive=True)
+        info('launching ' + app_cmd)
+        print('launching ' + app_cmd)
+        p_app = Popen(app_cmd, shell=True, stderr=STDOUT, universal_newlines=True)
+        sleep(8)
+        p_app_out, p_app_err = p_app.communicate()
+        info('output (%s): %s' % (app_cmd, p_app_out))
+        info('error (%s): %s' % (app_cmd, p_app_err))
+        print('output (%s): %s' % (app_cmd, p_app_out))
+        print('error (%s): %s' % (app_cmd, p_app_err))
+        self.assertEqual(p_app.returncode, 0, 'error while executing ' + app_cmd)
+        current_logs = glob(path + '/**/*.log', recursive=True)
+        new_logs = [l for l in current_logs if l not in previous_logs]
+        self.assertEqual(len(new_logs), 1, f'{path=} {previous_logs=} {current_logs=} {new_logs=}')
+        with open(new_logs[0]) as f: new_log = f.read()
+        self.assertIn('the application has been started correctly', new_log)
+
+        # self.__awake()
+        # system('amixer sset Master 0%')
+        #ret = system(cmd)
+        #proc = run(cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=check)
+        #return proc.stdout.strip() + proc.stderr.strip()
+        # for test_cmd in test_cmds:
+        #     sleep(8)
+        #     info('launching ' + app_cmd)
+        #     print('launching ' + app_cmd)
+        #     p_app = Popen(app_cmd, shell=True, stderr=STDOUT, universal_newlines=True)
+        #     sleep(30)
+        #     info('launching ' + test_cmd)
+        #     print('launching ' + test_cmd)
+        #     p_test = Popen(test_cmd, shell=True, stderr=STDOUT, universal_newlines=True)
+        #     p_app_out, p_app_err = p_app.communicate()
+        #     t_out, t_err = p_test.communicate()
+        #     info('output (%s): %s' % (app_cmd, p_app_out))
+        #     info('error (%s): %s' % (app_cmd, p_app_err))
+        #     info('output (%s): %s' % (test_cmd, t_out))
+        #     info('error (%s): %s' % (test_cmd, t_err))
+        #     print('output (%s): %s' % (app_cmd, p_app_out))
+        #     print('error (%s): %s' % (app_cmd, p_app_err))
+        #     print('output (%s): %s' % (test_cmd, t_out))
+        #     print('error (%s): %s' % (test_cmd, t_err))
+        #     self.assertEqual(p_app.returncode, 0, 'error while executing ' + app_cmd)
+        #     self.assertEqual(p_test.returncode, 0, 'error while executing ' + test_cmd)
+        # files = glob(str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/*.png' % _branch())
+        # self.assertGreater(len(files), 1)
+        # for fname in files:
+        #     self.assertTrue(exists(path + basename(fname)), '%s does not exist' % (path + basename(fname)))
+        #     similar = self.__similar_images(
+        #         str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/' % _branch() + basename(fname),
+        #         path + basename(fname)),
+        #     'error while comparing %s and %s' % (
+        #         str(Path.home()) + '/.local/share/pmachines/tests/functional_ref_%s/' % _branch() + basename(fname),
+        #         path + basename(fname))
+        #     if not similar:
+        #         timestamp = datetime.now().strftime('%y%m%d%H%M%S%f')
+        #         copy(path + basename(fname), '~/Desktop/' + basename(fname)[:-4] + timestamp + '.png')
+        #     self.assertTrue(similar)
+
+    def test_code(self):
+        info('test_code')
+        if environ.get('FUNCTIONALPOST') == '1':
+            self.skipTest('skipped functional-post test: test_code')
+        # this is tested manually
+        # self.__test_template(
+        #     '/home/flavio/venv/bin/python main.py --functional-test',
+        #     str(Path.home()) + '/.local/share/pmachines/logs/')
+
+    def test_appimage(self):
+        info('test_appimage')
+        if environ.get('FUNCTIONALPOST') == '1':
+            self.skipTest('skipped functional-post test: test_appimage')
+        bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+        bld_branch = '' if bld_branch == 'stable' else ('-' + bld_branch)
+        self.__test_template(
+            'timeout 7200s ./dist/Pmachines%s-x86_64.AppImage --system-test' % bld_branch,
+            #['timeout 7200s ~/venv/bin/python -m tests.functional_test.py 1',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 2',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 3'],
+            str(Path.home()) + '/.local/share/pmachines/logs/')
+
+    # def test_flatpak(self):
+    #     info('test_flatpak')
+    #     if environ.get('FUNCTIONALPOST') != '1':
+    #         self.skipTest('skipped functional-post tests')
+    #     bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+    #     cmd = 'flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch
+    #     info('executing: %s' % cmd)
+    #     #system(cmd)
+    #     fout = exec_cmd(cmd)
+    #     info('executed: %s' % cmd)
+    #     info(fout)
+    #     self.__test_template(
+    #         'timeout 720s flatpak run it.ya2.Pmachines//%s --functional-test & '
+    #         'timeout 720s ~/venv/bin/python -m tests.functional_test.py 1; sleep 5; '
+    #         'timeout 720s flatpak run it.ya2.Pmachines//%s --functional-test & ' % (bld_branch, bld_branch) +
+    #         'timeout 720s ~/venv/bin/python -m tests.functional_test.py 2',
+    #         str(Path.home()) + '/.var/app/it.ya2.Pmachines/data/pmachines/tests/functional/')
+
+    def __update_itchio(self):
+        info('update_itchio::itchio_updated::cond: %s' % str(self.__itchio_updated))
+        if self.__itchio_updated:
+            return
+        system('/home/flavio/.itch/itch')
+        sleep(120)
+        system('xdotool mousemove 860 620')
+        sleep(3)
+        system('xdotool click 1')
+        sleep(300)
+        system('killall itch')
+        self.__itchio_updated = True
+        info('update_itchio::itchio_updated::done: %s' % str(self.__itchio_updated))
+
+    def test_itchio(self):
+        info('test_itchio')
+        if environ.get('FUNCTIONALPOST') != '1':
+            self.skipTest('skipped functional-post tests')
+        if _branch() != 'master':
+            return
+        self.__update_itchio()
+        self.__test_template(
+            'timeout 7200s /home/flavio/.config/itch/apps/pmachines/pmachines --functional-test',
+            ['timeout 7200s ~/venv/bin/python -m tests.functional_test.py 1',
+             'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 2',
+             'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 3'],
+            str(Path.home()) + '/.local/share/pmachines/tests/functional/')
+
+    def test_windows(self):
+        info('test_windows')
+        if environ.get('FUNCTIONALPOST') == '1':
+            self.skipTest('skipped functional-post test: test_windows')
+        system('pkill -f "pmachines.exe"')
+        abspath = str(Path(__file__).parent.parent) + '/build/win_amd64/pmachines.exe'
+        self.__test_template(
+            'timeout 7200s wine %s --system-test' % abspath,
+            #['timeout 7200s ~/venv/bin/python -m tests.functional_test.py 1',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 2',
+            # 'timeout 7200s ~/venv/bin/python -m tests.functional_test.py 3'],
+            str(Path.home()) + '/.wine/drive_c/users/flavio/AppData/Local/pmachines/logs/')
+
+    def test_versions(self):
+        info('test_versions')
+        if environ.get('FUNCTIONAL') != '1':
+            self.skipTest('skipped functional tests')
+        #bld_branch = {'master': 'alpha', 'rc': 'rc', 'stable': 'stable'}[_branch()]
+        with open('/home/flavio/builders/pmachines/last_bld.txt') as f:
+            lines = f.readlines()
+        for line in lines:
+            if line.strip().split()[0] == _branch():
+                commit = line.strip().split()[1][:7]
+        __ver = _version()
+        if _branch() == 'stable':
+            with open('/home/flavio/builders/pmachines_builder/pmachines/assets/version.txt') as fver:
+                __ver = fver.read().strip() + '-'
+        exp = '%s-%s' % (__ver, commit)
+        appimage_branch = '' if _branch() == 'stable' else ('-' + _branch())
+        if appimage_branch == '-master': appimage_branch = '-alpha'
+        cmds = [
+            ('timeout 7200s ./build/manylinux2010_x86_64/pmachines --version', str(Filename.get_user_appdata_directory()) + '/pmachines/observed_version.txt'),
+            ('timeout 7200s ./dist/Pmachines%s-x86_64.AppImage --version' % appimage_branch, str(Filename.get_user_appdata_directory()) + '/pmachines/observed_version.txt'),
+            ('timeout 7200s wine ./build/win_amd64/pmachines.exe --version', '/home/flavio/.wine/drive_c/users/flavio/AppData/Local/pmachines/observed_version.txt')
+        ]
+        if environ.get('FUNCTIONALPOST') == '1':
+            if _branch() == 'master':
+                self.__update_itchio()
+                cmds += [('timeout 7200s /home/flavio/.config/itch/apps/pmachines/pmachines --version', str(Filename.get_user_appdata_directory()) + '/pmachines/observed_version.txt')]
+            # cmds += [('timeout 7200s flatpak run it.ya2.Pmachines//%s --version' % bld_branch, '/home/flavio/.var/app/it.ya2.Pmachines/data/pmachines/observed_version.txt')]
+        # info('executing flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch)
+        # #system('flatpak update -y --user it.ya2.Pmachines//%s' % bld_branch)
+        # fout = exec_cmd('flatpak update -y --user it.ya2.Pmachines//%s' % bld_branch)
+        # info('executed flatpak update --user -y it.ya2.Pmachines//%s' % bld_branch)
+        # info(fout)
+        for cmd in cmds:
+            if exists(cmd[1]):
+                remove(cmd[1])
+            info('launching %s' % cmd[0])
+            exec_cmd(cmd[0])
+            with open(cmd[1]) as f:
+                obs = f.read().strip()
+            self.assertEqual(obs, exp, 'during ' + cmd[0])
diff --git a/tests/ya2/build/test_blend2gltf.py b/tests/ya2/build/test_blend2gltf.py
new file mode 100644 (file)
index 0000000..281c3cc
--- /dev/null
@@ -0,0 +1,15 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from importlib import reload
+from ya2.build import blend2gltf
+
+
+class Blend2GltfTests(TestCase):
+
+    def test_import_module(self):
+        reload(blend2gltf)  # if we import only then pyflakes complains
+        # we can not do other tests since it runs only from blender's python
+        # it is indirectly tested by ModelsBuilder's tests
index 6713addb2fdc745f99605cdcb58bddac13c127fc..5b1ccb1c5bb3d8bae51394f1ef54d2304c7267a6 100644 (file)
@@ -1,15 +1,16 @@
 '''Unit tests for lib.build.'''
 from pathlib import Path
 import sys
 '''Unit tests for lib.build.'''
 from pathlib import Path
 import sys
-if '' in sys.path: sys.path.remove('')
+if '' in sys.path:
+    sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent.parent))
 from os import getcwd, makedirs, walk
 from os.path import basename
 from shutil import rmtree
 from unittest import TestCase
 sys.path.append(str(Path(__file__).parent.parent.parent.parent))
 from os import getcwd, makedirs, walk
 from os.path import basename
 from shutil import rmtree
 from unittest import TestCase
-from re import compile
-from ya2.build.build import InsideDir, files, exec_cmd, _branch, _version, \
-    to_be_built
+import re
+from ya2.build.build import InsideDir, find_file_names, exec_cmd, _branch, _version, \
+    to_be_built, FindFileNamesArgs
 
 
 class BuildTests(TestCase):
 
 
 class BuildTests(TestCase):
@@ -18,13 +19,16 @@ class BuildTests(TestCase):
         makedirs('test_get_files/a')
         makedirs('test_get_files/b')
         makedirs('test_get_files/c')
         makedirs('test_get_files/a')
         makedirs('test_get_files/b')
         makedirs('test_get_files/c')
-        with open('test_get_files/a/c.ext1', 'w') as ftest:
+        with open('test_get_files/a/c.ext1', 'w', encoding='utf8') as ftest:
             ftest.write('0123456789')
             ftest.write('0123456789')
-        with open('test_get_files/a/d.ext2', 'w'): pass
-        with open('test_get_files/b/e.ext2', 'w') as ftest:
+        with open('test_get_files/a/d.ext2', 'w', encoding='utf8'):
+            pass
+        with open('test_get_files/b/e.ext2', 'w', encoding='utf8') as ftest:
             ftest.write('0123456789')
             ftest.write('0123456789')
-        with open('test_get_files/b/f.ext3', 'w'): pass
-        with open('test_get_files/c/g.ext2', 'w'): pass
+        with open('test_get_files/b/f.ext3', 'w', encoding='utf8'):
+            pass
+        with open('test_get_files/c/g.ext2', 'w', encoding='utf8'):
+            pass
 
     def tearDown(self):
         rmtree('test_get_files')
 
     def tearDown(self):
         rmtree('test_get_files')
@@ -39,13 +43,14 @@ class BuildTests(TestCase):
         patterns = [
             "^0a[0-9]+$",
             "^0rc[0-9]+$",
         patterns = [
             "^0a[0-9]+$",
             "^0rc[0-9]+$",
-            "^0\.[0-9]+$"]
-        compiled = [compile(pattern) for pattern in patterns]
+            r"^0\.[0-9]+$"]
+        compiled = [re.compile(pattern) for pattern in patterns]
         matches = [pattern.match(_version()) for pattern in compiled]
         self.assertTrue(any(matches))
 
     def test_get_files(self):
         matches = [pattern.match(_version()) for pattern in compiled]
         self.assertTrue(any(matches))
 
     def test_get_files(self):
-        _files = files(['ext2'], 'c')
+        find_info = FindFileNamesArgs(['ext2'], 'c')
+        _files = find_file_names(find_info)
         self.assertSetEqual(set(_files),
                             set(['./test_get_files/a/d.ext2',
                                  './test_get_files/b/e.ext2']))
         self.assertSetEqual(set(_files),
                             set(['./test_get_files/a/d.ext2',
                                  './test_get_files/b/e.ext2']))
@@ -61,8 +66,8 @@ class BuildTests(TestCase):
 
     def test_to_be_built(self):
         tgt = 'test_get_files/tgt.txt'
 
     def test_to_be_built(self):
         tgt = 'test_get_files/tgt.txt'
-        with open('test_get_files/src.txt', 'w') as fsrc:
+        with open('test_get_files/src.txt', 'w', encoding='utf8') as fsrc:
             fsrc.write('src')
             fsrc.write('src')
-        with open(tgt, 'w') as ftgt:
+        with open(tgt, 'w', encoding='utf8') as ftgt:
             ftgt.write('tgt')
         self.assertTrue(to_be_built(tgt, ['test_get_files/src.txt']))
             ftgt.write('tgt')
         self.assertTrue(to_be_built(tgt, ['test_get_files/src.txt']))
diff --git a/tests/ya2/build/test_images.py b/tests/ya2/build/test_images.py
new file mode 100644 (file)
index 0000000..5376eaf
--- /dev/null
@@ -0,0 +1,26 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from os import remove
+from os.path import exists
+from glob import glob
+from unittest import TestCase
+from ya2.build.images import ImagesBuilder
+
+
+class BuildImagesTests(TestCase):
+
+    def setUp(self):
+        for i in glob('tests/assets/images/*.dds'): remove(i)
+
+    def tearDown(self):
+        for i in glob('tests/assets/images/*.dds'): remove(i)
+
+    def test_build_images(self):
+        self.assertFalse(exists('tests/assets/images/icon16.dds'))
+        self.assertFalse(exists('tests/assets/images/icon32.dds'))
+        i = ImagesBuilder(glob('tests/assets/images/*.png'))
+        i.build()
+        self.assertTrue(exists('tests/assets/images/icon16.dds'))
+        self.assertTrue(exists('tests/assets/images/icon32.dds'))
index 9ccf7cffb1fdbd5629b30d8e5d66861900ede911..689bedd834c3b28c9c4284325a2f401d5b31a6ed 100644 (file)
@@ -2,7 +2,7 @@ from pathlib import Path
 import sys
 if '' in sys.path: sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent))
 import sys
 if '' in sys.path: sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent))
-from os import remove, makedirs
+from os import makedirs
 from os.path import exists
 from shutil import rmtree, copy
 from unittest import TestCase
 from os.path import exists
 from shutil import rmtree, copy
 from unittest import TestCase
@@ -22,9 +22,8 @@ class LangTests(TestCase):
             rmtree('./tests/' + dirname, ignore_errors=True)
 
     def test_lang(self):
             rmtree('./tests/' + dirname, ignore_errors=True)
 
     def test_lang(self):
-        LanguageBuilder.pot('test_pmachines', './tests/po/')
-        self.assertTrue(exists('./tests/po/test_pmachines.pot'))
-        LanguageBuilder.merge('it_IT', './tests/po/', './tests/locale/', 'test_pmachines')
-        LanguageBuilder.mo('./tests/locale/it_IT/LC_MESSAGES/test_pmachines.mo',
-                           './tests/locale/', 'test_pmachines')
-        self.assertTrue(exists('./tests/locale/it_IT/LC_MESSAGES/test_pmachines.mo'))
+        #l = LanguageBuilder('test_pmachines', './po/', './po/', './locale/', 'tests')
+        l = LanguageBuilder('test', 'assets/locale/po/', 'assets/scenes/', 'assets/locale/', 'tests')
+        l.build()
+        self.assertTrue(exists('./tests/assets/locale/po/test.pot'))
+        self.assertTrue(exists('./tests/assets/locale/it_IT/LC_MESSAGES/test.mo'))
index 47c426b4fa946417bc0fbe316950998438c8dad6..f46bc6dace4c14067f55e21df5c30e21e88d698a 100644 (file)
@@ -1,53 +1,31 @@
-from pathlib import Path
-import sys
-if '' in sys.path: sys.path.remove('')
-sys.path.append(str(Path(__file__).parent.parent.parent))
-from os import remove, makedirs, environ
+from os import environ
 from os.path import exists
 from os.path import exists
-from shutil import rmtree, copy
+from shutil import rmtree
 from unittest import TestCase
 from itertools import product
 from unittest import TestCase
 from itertools import product
-from time import time
 from ya2.build.models import ModelsBuilder
 
 
 class ModelsBuilderTests(TestCase):
 
     def setUp(self):
 from ya2.build.models import ModelsBuilder
 
 
 class ModelsBuilderTests(TestCase):
 
     def setUp(self):
-        self.dirs = ['box', 'domino']
-        for fmt_dir in product(['bam', 'gltf'], self.dirs):
-            rmtree('tests/assets/models/%s/%s' % fmt_dir, ignore_errors=True)
+        self.__test_directories = ['cube1', 'cube2']
+        for f_d in product(['bam', 'gltf'], self.__test_directories):
+            rmtree('tests/assets/models/%s/%s' % f_d, ignore_errors=True)
 
     def test_models(self):
 
     def test_models(self):
-        if environ.get('FAST') == '1':
-            self.skipTest('skipped slow tests')
-        for fmt_dir in product(['bam', 'gltf'], self.dirs):
-            self.assertFalse(exists('tests/assets/%s/%s' % fmt_dir))
-        start = time()
-        ModelsBuilder().build('tests/assets/models', 1)
-        #self.assertTrue(time() - start > 1.5)
-        files = [
-            'tests/assets/models/bam/cube/cube.bam',
-            'tests/assets/models/bam/cube/diffuse.dds',
-            # 'assets/models/bam/box/box.bam',
-            # 'assets/models/bam/box/base.dds',
-            # 'assets/models/bam/box/ao_metal_roughness.dds',
-            # 'assets/models/bam/box/normal.dds',
-            # 'assets/models/bam/domino/domino.bam',
-            # 'assets/models/bam/domino/base.dds',
-            # 'assets/models/bam/domino/ao_roughness_metal.dds',
-            # 'assets/models/bam/domino/normal.dds',
-            # 'assets/models/gltf/box/box.gltf',
-            # 'assets/models/gltf/box/base.png',
-            # 'assets/models/gltf/box/ao_metal_roughness.png',
-            # 'assets/models/gltf/box/normal.png',
-            # 'assets/models/gltf/domino/domino.gltf',
-            # 'assets/models/gltf/domino/base.png',
-            # 'assets/models/gltf/domino/ao_roughness_metal.png',
-            # 'assets/models/gltf/domino/normal.png'
-        ]
-        [self.assertTrue(exists(fname)) for fname in files]
-        #start = time()
-        #ModelsBuilder().build('assets/models', 1)
-        #self.assertTrue(time() - start < 1.5)  # test caching
-        #[self.assertTrue(exists(fname)) for fname in files]
+        if environ.get('FAST') == '1': self.skipTest('skipped slow tests')
+        for f_d in product(['bam', 'gltf'], self.__test_directories):
+            self.assertFalse(exists('tests/assets/%s/%s' % f_d))
+        b = ModelsBuilder('tests/assets/models', 1)
+        built = b.build()
+        self.assertGreater(len(built), 0)
+        test_files = [
+            'tests/assets/models/bam/cube1/cube.bam',
+            'tests/assets/models/bam/cube1/diffuse.dds',
+            'tests/assets/models/bam/cube2/cube.bam',
+            'tests/assets/models/bam/cube2/diffuse.dds']
+        [self.assertTrue(exists(f)) for f in test_files]
+        b = ModelsBuilder('tests/assets/models', 1)
+        built = b.build()
+        self.assertEqual(len(built), 0)
diff --git a/tests/ya2/build/test_mtprocesser.py b/tests/ya2/build/test_mtprocesser.py
deleted file mode 100644 (file)
index 536cf7c..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-'''lib.build.mtprocesser's unit tests.'''
-from pathlib import Path
-import sys
-sys.path = [path for path in sys.path if path != '']
-
-# we're in yocto/tests/lib/build/
-sys.path.append(str(Path(__file__).parent.parent.parent.parent))
-from pathlib import Path
-from os.path import exists
-from unittest import TestCase
-from ya2.build.mtprocesser import ProcesserMgr
-
-
-class ProcesserMgrTests(TestCase):
-    '''ProcesserMgr's unit tests '''
-
-    def setUp(self):
-        '''unit tests' set up'''
-        for idx in [1, 2]:
-            Path('./tests/%s.txt' % idx).unlink(missing_ok=True)
-
-    def tearDown(self):
-        '''unit tests' tear down'''
-        self.setUp()
-
-    def test_threaded(self):
-        '''test of the threaded case'''
-        pmgr = ProcesserMgr(2)
-        pmgr.add('echo 1 > ./tests/1.txt')
-        pmgr.add('echo 2 > ./tests/2.txt')
-        pmgr.run()
-        self.assertTrue(exists('./tests/1.txt'))
-        self.assertTrue(exists('./tests/2.txt'))
-
-    def test_nothreaded(self):
-        '''test when we don't use threads'''
-        pmgr = ProcesserMgr(1)
-        pmgr.add('echo 1 > ./tests/1.txt')
-        pmgr.run()
-        self.assertTrue(exists('./tests/1.txt'))
diff --git a/tests/ya2/build/test_screenshots.py b/tests/ya2/build/test_screenshots.py
new file mode 100644 (file)
index 0000000..ceaea41
--- /dev/null
@@ -0,0 +1,21 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import patch
+from ya2.build.screenshots import ScreenshotsBuilder
+import ya2
+
+
+class ScreenshotsBuilderTests(TestCase):
+
+    @patch.object(ya2.build.screenshots, 'Pool', autospec=True)
+    def test_build(self, p_mock):
+        pool = p_mock.return_value.__enter__.return_value
+        scene_names = ['box', 'domino']
+        b = ScreenshotsBuilder(scene_names)
+        b.build()
+        assert p_mock.call_count == 1
+        assert pool.mock_calls[0][1][0].__name__ == ScreenshotsBuilder.do_screenshot.__name__
+        assert pool.mock_calls[0][1][1] == ['box', 'domino']
diff --git a/tests/ya2/patterns/__init__.py b/tests/ya2/patterns/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/tests/ya2/utils/gui/__init__.py b/tests/ya2/utils/gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/ya2/utils/gui/test_cursor.py b/tests/ya2/utils/gui/test_cursor.py
new file mode 100644 (file)
index 0000000..6ac9b8c
--- /dev/null
@@ -0,0 +1,33 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from direct.showbase.ShowBase import ShowBase
+from ya2.utils.gui.cursor import MouseCursor, MouseCursorArgs
+
+
+class CursorTests(TestCase):
+
+    def setUp(self):
+        self.__app = ShowBase()
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    def test_cursor(self):
+        i = MouseCursorArgs('tests/assets/images/icon16.png', False, (1, 1, 1), (1, 1, 1, 1), (0, 0))
+        c = MouseCursor(i)
+        c.set_image('tests/assets/images/icon32.png')
+        self.assertEqual('tests/assets/images/icon32.png', c._MouseCursor__image.get_texture().get_filename())
+        c.set_color((0, 1, 0, 1))
+        self.assertEqual((0, 1, 0, 1), c._MouseCursor__image.get_color())
+        c.show()
+        self.assertFalse(c._MouseCursor__image.is_hidden())
+        c.hide()
+        self.assertTrue(c._MouseCursor__image.is_hidden())
+        c.destroy()
+        self.assertNotIn('on_frame_cursor', [t.name for t in taskMgr.getTasks() + taskMgr.getDoLaters()])
+        self.assertTrue(c._MouseCursor__image.is_empty())
diff --git a/tests/ya2/utils/gui/test_gui.py b/tests/ya2/utils/gui/test_gui.py
new file mode 100644 (file)
index 0000000..1472a38
--- /dev/null
@@ -0,0 +1,42 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type offscreen')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import patch
+from panda3d.core import get_model_path, Texture
+from direct.showbase.ShowBase import ShowBase
+from ya2.utils.gui.gui import GuiTools
+import ya2
+
+
+class TestApp(ShowBase): pass
+
+
+class GraphicsToolsTests(TestCase):
+
+    def setUp(self):
+        self.__app = TestApp()
+        get_model_path().append_directory(str(Path(__file__).parent.parent.parent))
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    @patch.object(ya2.utils.gui.gui, 'load_prc_file_data')
+    def test_no_window(self, l_mock):
+        GuiTools.no_window()
+        l_mock.assert_called_once()
+        l_args = l_mock.call_args_list[0].args
+        self.assertEqual(l_args[0], '')
+        self.assertEqual(l_args[1], 'window-type none')
+        self.assertEqual(len(l_args), 2)
+
+    def test_font(self):
+        f = GuiTools.load_font('assets/fonts/Hanken-Book.ttf')
+        self.assertEqual(f.get_pixels_per_unit(), 60)
+        self.assertEqual(f.get_minfilter(), Texture.FTLinearMipmapLinear)
+        self.assertEqual(f.get_outline_color(), (0, 0, 0, 1))
+        self.assertAlmostEqual(f.get_outline_feather(), .2, delta=.01)
+        self.assertAlmostEqual(f.get_outline_width(), .8, delta=.01)
diff --git a/tests/ya2/utils/test_asserts.py b/tests/ya2/utils/test_asserts.py
new file mode 100644 (file)
index 0000000..236887d
--- /dev/null
@@ -0,0 +1,81 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from threading import Thread
+from panda3d.core import NodePath
+from direct.showbase.ShowBase import ShowBase
+from direct.showbase.DirectObject import DirectObject
+from ya2.utils.asserts import Assert
+
+
+class TestApp(ShowBase): pass
+
+
+class AssertsTests(TestCase):
+
+    def setUp(self):
+        self.__app = TestApp()
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    def test_render3d(self):
+        np = NodePath('new node')
+        np.reparent_to(render)
+        with self.assertRaises(Exception):
+            Assert.assert_render3d()
+        np.remove_node()
+        Assert.assert_render3d()
+
+    def test_render2d(self):
+        np = NodePath('new node')
+        np.reparent_to(render2d)
+        with self.assertRaises(Exception):
+            Assert.assert_render2d()
+        np.remove_node()
+        Assert.assert_render2d()
+
+    def test_aspect2d(self):
+        np = NodePath('new node')
+        np.reparent_to(aspect2d)
+        with self.assertRaises(Exception):
+            Assert.assert_aspect2d()
+        np.remove_node()
+        Assert.assert_aspect2d()
+
+    def test_events(self):
+        d = DirectObject()
+        d.accept('new event', self.__accept)
+        with self.assertRaises(Exception):
+            Assert.assert_events()
+        d.ignore('new event')
+        Assert.assert_events()
+
+    def __accept(): pass
+
+    def test_tasks(self):
+        t = taskMgr.add(self.__new_task)
+        with self.assertRaises(Exception):
+            Assert.assert_tasks()
+        taskMgr.remove(t)
+        Assert.assert_tasks()
+
+    def __new_task(self, task):
+        return task.again
+
+    def test_threads(self):
+        self.__flag = False
+        t = Thread(target=self.__while_flag)
+        t.start()
+        with self.assertRaises(Exception):
+            Assert.assert_threads()
+        self.__flag = True
+        t.join()
+        Assert.assert_threads()
+
+    def __while_flag(self):
+        while not self.__flag: pass
diff --git a/tests/ya2/utils/test_audio.py b/tests/ya2/utils/test_audio.py
new file mode 100644 (file)
index 0000000..2b9af91
--- /dev/null
@@ -0,0 +1,36 @@
+from panda3d.core import load_prc_file_data, AudioSound
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import patch
+from direct.showbase.ShowBase import ShowBase
+from ya2.utils.audio import AudioTools
+
+
+class AudioTests(TestCase):
+
+    def setUp(self):
+        self.__app = ShowBase()
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    def test_volume(self):
+        with (patch.object(base, 'musicManager', autospec=True) as m,
+              patch.object(base, 'sfxManagerList', autospec=True) as s):
+            AudioTools.set_volume(.8)
+            m.set_volume.assert_called_once()
+            m_args = m.set_volume.call_args_list[0].args
+            self.assertAlmostEqual(m_args[0], .64, delta=.01)
+            self.assertEqual(len(m_args), 1)
+            s[0].set_volume.assert_called_once()
+            s_args = s[0].set_volume.call_args_list[0].args
+            self.assertAlmostEqual(s_args[0], .8, delta=.01)
+            self.assertEqual(len(s_args), 1)
+
+    def test_loading(self):
+        s = AudioTools.load_sfx('assets/audio/sfx/rollover.ogg')
+        self.assertIsInstance(s, AudioSound)
diff --git a/tests/ya2/utils/test_decorator.py b/tests/ya2/utils/test_decorator.py
new file mode 100644 (file)
index 0000000..b73b424
--- /dev/null
@@ -0,0 +1,36 @@
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from ya2.utils.decorator import Decorator
+
+
+class C:
+
+    def m(self):
+        self.f = True
+        self._f = True
+        self.__f = True
+        return True
+
+
+class D(C, Decorator):
+
+    def m_d(self):
+        self.g = True
+        self._g = True
+        self.__g = True
+        return True
+
+
+class DecoratorTests(TestCase):
+
+    def test_decorator(self):
+        d = D(C())
+        self.assertTrue(d.m())
+        self.assertTrue(d.m_d())
+        self.assertTrue(d.f)
+        self.assertTrue(d.g)
+        self.assertTrue(d._f)
+        self.assertTrue(d._g)
diff --git a/tests/ya2/utils/test_functional.py b/tests/ya2/utils/test_functional.py
new file mode 100644 (file)
index 0000000..cd7cb55
--- /dev/null
@@ -0,0 +1,35 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from xmlrpc.client import ServerProxy
+from direct.showbase.ShowBase import ShowBase
+from ya2.utils.functional import FunctionalTest
+
+
+class FunctionalTests(TestCase):
+
+    def setUp(self):
+        self.__app = ShowBase()
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    def test_functional(self):
+        self.__position_manager = {}
+        self.__position_manager['a'] = [0, 1]
+        FunctionalTest(False, self.__position_manager, 'test')
+        self.__proxy = ServerProxy('http://localhost:7000')
+        self.__proxy.screenshot('a')
+        self.__proxy.enforce_result('win')
+        self.__proxy.verify()
+        self.__proxy.set_index(1)
+        self.__proxy.enforce_resolution('1920x1080')
+        p = self.__proxy.get_position('a')
+        self.assertEqual(p, [0, 1])
+        self.__proxy.destroy()
+        # we need the refactor cited in functional.py to actually test
+        # the callbacks
diff --git a/tests/ya2/utils/test_gameobject.py b/tests/ya2/utils/test_gameobject.py
deleted file mode 100644 (file)
index 58cae86..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-from pathlib import Path
-import sys
-if '' in sys.path: sys.path.remove('')
-sys.path.append(str(Path(__file__).parent.parent.parent))
-from unittest.mock import patch
-from unittest import TestCase
-from panda3d.core import loadPrcFileData
-#from ya2.engine.engine import Engine
-from ya2.patterns.gameobject import AiColleague, AudioColleague, EventColleague, \
-    FsmColleague, GameObject, GfxColleague, GuiColleague, LogicColleague, \
-    PhysColleague, Colleague
-
-
-class AiTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        #self.engine = Engine()
-        game_obj = GameObject()
-        ai = AiColleague(game_obj)
-        self.assertIsInstance(ai, AiColleague)
-
-
-class AudioTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        #self.engine = Engine()
-        game_obj = GameObject()
-        audio = AudioColleague(game_obj)
-        self.assertIsInstance(audio, AudioColleague)
-
-
-class ColleagueTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        #self.engine = Engine()
-        game_obj = GameObject()
-        colleague = Colleague(game_obj)
-        self.assertIsInstance(colleague, Colleague)
-
-
-class EventTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        #self.engine = Engine()
-        game_obj = GameObject()
-        event = EventColleague(game_obj)
-        self.assertIsInstance(event, EventColleague)
-
-
-class FsmTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        #self.engine = Engine()
-        game_obj = GameObject()
-        fsm = FsmColleague(game_obj)
-        self.assertIsInstance(fsm, FsmColleague)
-
-
-class GfxTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        #self.engine = Engine()
-        game_obj = GameObject()
-        gfx = GfxColleague(game_obj)
-        self.assertIsInstance(gfx, GfxColleague)
-
-
-class GuiTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        pass
-        #self.engine = Engine()
-        game_obj = GameObject()
-        gui = GuiColleague(game_obj)
-        self.assertIsInstance(gui, GuiColleague)
-
-
-class LogicTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        pass
-        #self.engine = Engine()
-        game_obj = GameObject()
-        logic = LogicColleague(game_obj)
-        self.assertIsInstance(logic, LogicColleague)
-
-
-class PhysicsTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    def test_init(self):
-        pass
-        #self.engine = Engine()
-        game_obj = GameObject()
-        phys = PhysColleague(game_obj)
-        self.assertIsInstance(phys, PhysColleague)
-
-
-class GameObjectInstance(GameObject):
-
-    def __init__(self):
-        GameObject.__init__(self)
-        self.fsm = FsmColleague(self)
-        self.event = EventColleague(self)
-        self.ai = AiColleague(self)
-        self.phys = PhysColleague(self)
-        self.audio = AudioColleague(self)
-        self.logic = LogicColleague(self)
-        self.gui = GuiColleague(self)
-        self.gfx = GfxColleague(self)
-
-    def destroy(self):
-        self.fsm.destroy()
-        self.event.destroy()
-        self.ai.destroy()
-        self.phys.destroy()
-        self.audio.destroy()
-        self.logic.destroy()
-        self.gui.destroy()
-        self.gfx.destroy()
-
-
-class GameObjectTests(TestCase):
-
-    def setUp(self):
-        loadPrcFileData('', 'window-type none')
-        loadPrcFileData('', 'audio-library-name null')
-
-    def tearDown(self):
-        pass
-        #self.engine.destroy()
-
-    @patch.object(GfxColleague, 'destroy')
-    @patch.object(GuiColleague, 'destroy')
-    @patch.object(LogicColleague, 'destroy')
-    @patch.object(AudioColleague, 'destroy')
-    @patch.object(PhysColleague, 'destroy')
-    @patch.object(AiColleague, 'destroy')
-    @patch.object(EventColleague, 'destroy')
-    @patch.object(FsmColleague, 'destroy')
-    def test_init(
-            self, mock_fsm_destroy, mock_event_destroy, mock_ai_destroy,
-            mock_phys_destroy, mock_audio_destroy, mock_logic_destroy,
-            mock_gui_destroy, mock_gfx_destroy):
-        #self.engine = Engine()
-        mock_event_destroy.__name__ = 'destroy'
-        game_obj = GameObjectInstance()
-        self.assertIsInstance(game_obj, GameObject)
-        game_obj.destroy()
-        assert mock_fsm_destroy.called
-        assert mock_event_destroy.called
-        assert mock_ai_destroy.called
-        assert mock_phys_destroy.called
-        assert mock_audio_destroy.called
-        assert mock_logic_destroy.called
-        assert mock_gui_destroy.called
-        assert mock_gfx_destroy.called
diff --git a/tests/ya2/utils/test_gfx.py b/tests/ya2/utils/test_gfx.py
new file mode 100644 (file)
index 0000000..4b580b9
--- /dev/null
@@ -0,0 +1,82 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type offscreen')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from panda3d.core import get_model_path, Texture
+from panda3d.bullet import BulletRigidBodyNode
+from direct.showbase.ShowBase import ShowBase
+from direct.gui.DirectGui import DirectButton
+from ya2.utils.gfx import GfxTools, Point, DirectGuiMixin, NodePathDecorator
+
+
+class TestApp(ShowBase): pass
+
+
+class GraphicsToolsTests(TestCase):
+
+    def setUp(self):
+        self.__app = TestApp()
+        get_model_path().append_directory(str(Path(__file__).parent.parent.parent))
+        self.__n = GfxTools.build_empty_node('temporary node')
+        self.__n.reparent_to(render)
+        self.__n.set_pos(1.2, 0, 0)
+        base.camera.set_pos(-8, -10, 5)
+        base.camera.look_at(0, 0, 0)
+
+    def tearDown(self):
+        self.__n.remove_node()
+        self.__app.destroy()
+
+    def test_srgb(self):
+        model = GfxTools.build_model('assets/models/bam/cube1/cube.bam')
+        srgb_formats = [Texture.F_srgb, Texture.F_srgb_alpha]
+        formats = [t.get_format() for t in model.find_all_textures()]
+        count_before = [f in srgb_formats for f in formats].count(True)
+        model.set_srgb_textures()
+        formats = [t.get_format() for t in model.find_all_textures()]
+        count_after = [f in srgb_formats for f in formats].count(True)
+        self.assertGreater(count_after, count_before)
+
+    def test_gfx_tools(self):
+        n = GfxTools.build_empty_node('temporary node')
+        self.assertEqual(n.__class__, NodePathDecorator)
+        n.remove_node()
+        n = GfxTools.build_model('panda')
+        self.assertEqual(n.__class__, NodePathDecorator)
+        n.remove_node()
+        p = BulletRigidBodyNode('')
+        n = GfxTools.build_node_from_physics(p)
+        self.assertEqual(n.__class__, NodePathDecorator)
+        n.remove_node()
+
+    def test_nodepath_decorator(self):
+        pos = self.__n.pos2d()
+        self.assertAlmostEqual(pos[0], .18, delta=.01)
+        self.assertAlmostEqual(pos[1], .07, delta=.01)
+        pos = self.__n.pos2d_pixel()
+        self.assertEqual(pos[0], 473)
+        self.assertEqual(pos[1], 279)
+        pos = self.__n.pos_as_widget()
+        self.assertEqual(pos[0], 880)
+        self.assertEqual(pos[1], 300)
+
+    def test_directgui_mixin(self):
+        b = DirectButton(pos=(.2, 1, .2))
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        pos = b.pos_pixel()
+        self.assertEqual(pos[0], 460)
+        self.assertEqual(pos[1], 240)
+        b.destroy()
+
+    def test_point(self):
+        pos = Point(self.__n.get_pos()).screen_coord()
+        self.assertAlmostEqual(pos[0], .24, delta=.01)
+        self.assertAlmostEqual(pos[1], .07, delta=.01)
+        from_, to = pos.from_to_points()
+        self.assertAlmostEqual(from_[0], -7.35, delta=.01)
+        self.assertAlmostEqual(from_[1], -9.32, delta=.01)
+        self.assertAlmostEqual(to[0], 65471.92, delta=.01)
+        self.assertAlmostEqual(to[1], 67972.78, delta=.01)
diff --git a/tests/ya2/utils/test_language.py b/tests/ya2/utils/test_language.py
new file mode 100644 (file)
index 0000000..d2a4575
--- /dev/null
@@ -0,0 +1,17 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from ya2.utils.language import LanguageManager
+
+
+class LogTests(TestCase):
+
+    def test_language(self):
+        l = LanguageManager('it', 'test', 'tests/assets/locale')
+        self.assertEqual(_('test string'), 'stringa di test')
+        l.set_language('en')
+        self.assertEqual(_('test string'), 'test string')
diff --git a/tests/ya2/utils/test_log.py b/tests/ya2/utils/test_log.py
new file mode 100644 (file)
index 0000000..6f1f41b
--- /dev/null
@@ -0,0 +1,71 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import patch
+from direct.showbase.ShowBase import ShowBase
+import logging
+from ya2.utils.dictfile import DctFile
+from ya2.utils.log import LogManager
+
+
+class LogTests(TestCase):
+
+    def setUp(self):
+        self.__app = ShowBase()
+        self.__optfile = DctFile('options.ini')
+        self.__verbose_val = self.__optfile['development']['verbose_log']
+        self.__headless_strings = [
+            'version: ',
+            'argv[0]: ',
+            'getcwd: ',
+            '__file__: ',
+            'env::',
+            'python version: ',
+            'panda version: ',
+            'bullet version: ',
+            'appdata: ']
+        self.__windowed_strings = [
+            'shader: ',
+            'driver version: ',
+            'fullscreen: ',
+            'resolution: ']
+
+    def tearDown(self):
+        self.__optfile['development']['verbose_log'] = self.__verbose_val
+        self.__optfile.store()
+        self.__app.destroy()
+
+    def test_logging(self):
+        LogManager.before_init_setup('pmachines')
+        self.assertEqual(logging.root.handlers[0].formatter._fmt, '%(asctime)s %(message)s')
+        self.assertEqual(logging.root.handlers[0].formatter.datefmt, '%H:%M:%S')
+        self.__optfile['development']['verbose_log'] = 0
+        self.__optfile.store()
+        self.assertEqual(logging.root.level, logging.INFO)
+        self.__optfile['development']['verbose_log'] = 1
+        self.__optfile.store()
+        LogManager.before_init_setup('pmachines')
+        self.assertEqual(logging.root.level, logging.DEBUG)
+        with patch.object(base, 'win', None):
+            l = LogManager.init_cls()
+            with self.assertLogs() as cm:
+                l.log_configuration()
+                for s in self.__headless_strings:
+                    self.assertTrue(any(s in m for m in cm.output))
+                for s in self.__windowed_strings:
+                    self.assertFalse(any(s in m for m in cm.output))
+
+    def test_windowed_logging(self):
+        #we must patch this at runtime
+        with patch.object(base, 'win', autospec=True):
+            l = LogManager.init_cls()
+            with self.assertLogs() as cm:
+                l.log_configuration()
+                for s in self.__headless_strings:
+                    self.assertTrue(any(s in m for m in cm.output))
+                for s in self.__windowed_strings:
+                    self.assertTrue(any(s in m for m in cm.output))
diff --git a/tests/ya2/utils/test_logics.py b/tests/ya2/utils/test_logics.py
new file mode 100644 (file)
index 0000000..d3515fa
--- /dev/null
@@ -0,0 +1,35 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type offscreen')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from unittest.mock import patch
+from direct.showbase.ShowBase import ShowBase
+from ya2.utils.logics import LogicsTools
+import ya2.utils.logics
+
+
+class TestApp(ShowBase): pass
+
+
+class LogicsTests(TestCase):
+
+    def test_is_in_build(self):
+        self.assertFalse(LogicsTools.in_build)
+        with patch.object(ya2.utils.logics, 'exists', autospec=True, return_value=False):
+            self.assertTrue(LogicsTools.in_build)
+
+    def test_fix_path(self):
+        p = str(Path.home())
+        expected = str(Path.home())
+        self.assertEqual(LogicsTools.platform_specific_path(p), expected)
+        with (patch.object(ya2.utils.logics, 'platform', 'win32'),
+              patch.object(ya2.utils.logics, 'exists', autospec=True, return_value=True)):
+            self.assertEqual(LogicsTools.platform_specific_path(p), expected)
+        p = '/c/path/to/test/file.ext'
+        expected = 'c:\\path\\to\\test\\file.ext'
+        with (patch.object(ya2.utils.logics, 'platform', 'win32'),
+              patch.object(ya2.utils.logics, 'exists', autospec=True, return_value=False)):
+            self.assertEqual(LogicsTools.platform_specific_path(p), expected)
diff --git a/tests/ya2/utils/test_observer.py b/tests/ya2/utils/test_observer.py
deleted file mode 100644 (file)
index 4a7c98e..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-from pathlib import Path
-import sys
-if '' in sys.path: sys.path.remove('')
-sys.path.append(str(Path(__file__).parent.parent.parent))
-from unittest import TestCase
-from unittest.mock import MagicMock
-from ya2.patterns.observer import Subject
-
-
-class Observed(Subject): pass
-
-
-class Observer:
-
-    def __init__(self, observed): self.__observed = observed
-
-    def callback(self): pass
-
-
-class ObserverTests(TestCase):
-
-    def test_all(self):
-        observed = Observed()
-        observer = Observer(observed)
-        observer.callback = MagicMock(side_effect=observer.callback)
-        observer.callback.__name__ = 'callback'
-        self.assertFalse(observed.observing(observer.callback))
-        observed.attach(observer.callback)
-        self.assertTrue(observed.observing(observer.callback))
-        observer.callback.assert_not_called()
-        observed.notify('callback')
-        observer.callback.assert_called()
-        observed.detach(observer.callback)
-        self.assertFalse(observed.observing(observer.callback))
index a65c16324538e6bcf2eb87881c7055ff8857fb45..b2f3afd1ba7eb2ac73d647308717e040758ea035 100644 (file)
@@ -1,3 +1,3 @@
-from pathlib import Path
-import sys
-sys.path.append(str(Path(__file__).parent.parent))
+#from pathlib import Path
+#import sys
+#sys.path.append(str(Path(__file__).parent.parent))
index b43f09f6238dadc192feb9651eb1a96850c891ed..7c6e8979b76dbd09da3151f241ec53cba633ac26 100644 (file)
@@ -1,9 +1,14 @@
-import bpy, sys, os
+import sys, os
+try:
+    import bpy
 
 
-filepath = os.path.abspath(sys.argv[-1])
-bpy.ops.export_scene.gltf(
-    filepath=filepath,
-    export_format='GLTF_SEPARATE',
-    export_extras=True,
-    export_tangents=True,
-    use_selection=True)
+
+    filepath = os.path.abspath(sys.argv[-1])
+    bpy.ops.export_scene.gltf(
+        filepath=filepath,
+        export_format='GLTF_SEPARATE',
+        export_extras=True,
+        export_tangents=True,
+        use_selection=True)
+except ModuleNotFoundError:
+    print('bpy can be used only from blender')
index 420a7eb7f63ae633dd22c29045f5ca825ef799e9..400c55eb7fd0be7c785423e49ef8614e9e7f69e6 100644 (file)
@@ -1,55 +1,55 @@
 '''Tools for making the builds.'''
 from os import walk, chdir, getcwd
 '''Tools for making the builds.'''
 from os import walk, chdir, getcwd
-from os.path import join, getsize, exists, dirname, getmtime, sep
-from subprocess import Popen, PIPE, run
-from logging import debug
+from os.path import join, exists, dirname, getmtime, sep
+from subprocess import PIPE, run
 from time import strftime
 from pathlib import Path
 from hashlib import md5
 
 
 from time import strftime
 from pathlib import Path
 from hashlib import md5
 
 
-#TODO refactor: make class BuilderTools
+# TODO refactor: make class BuilderTools
 
 
 
 
-def exec_cmd(cmd):
+def exec_cmd(cmd, check=True):
     '''Synchronously executes a command and returns its output.'''
     '''Synchronously executes a command and returns its output.'''
-    #ret = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()
-    #return ret[0].decode('utf-8').strip()
-    proc = run(cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True)
+    # ret = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()
+    # return ret[0].decode('utf-8').strip()
+    proc = run(cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=check)
     return proc.stdout.strip() + proc.stderr.strip()
 
 
 def _branch():
     '''Returns the current branch.'''
     git_branch = exec_cmd('git symbolic-ref HEAD').split('/')[-1].strip()
     return proc.stdout.strip() + proc.stderr.strip()
 
 
 def _branch():
     '''Returns the current branch.'''
     git_branch = exec_cmd('git symbolic-ref HEAD').split('/')[-1].strip()
-    print('git_branch result: %s' % git_branch)
+    print(f'git_branch result: {git_branch}')
     branches = ['master', 'rc', 'stable']
     if git_branch in branches:
     branches = ['master', 'rc', 'stable']
     if git_branch in branches:
-        print('git_branch: %s' % git_branch)
+        print(f'git_branch: {git_branch}')
         return git_branch
     root = str(Path(dirname(dirname(__file__))).parent) + '/'
     if 'itch' in __file__.split(sep):
         root = str(Path(dirname(__file__))) + '/'
     if __file__ == '/app/bin/pmachines':  # flatpak
         root = '/app/bin/'
         return git_branch
     root = str(Path(dirname(dirname(__file__))).parent) + '/'
     if 'itch' in __file__.split(sep):
         root = str(Path(dirname(__file__))) + '/'
     if __file__ == '/app/bin/pmachines':  # flatpak
         root = '/app/bin/'
-    for branch in branches:
+    for __branch in branches:
         try:
         try:
-            print('try: %s' % root + 'assets/bld_version.txt')
-            with open(root + 'assets/bld_version.txt') as fver:
-                ver = fver.read()
-                print('ver: %s' % ver)
-            #if branch in ver:
+            print(f'try: {root}' + 'assets/build_version.txt')
+            with open(root + 'assets/build_version.txt', encoding='utf8') as fver:
+                __ver = fver.read()
+                print(f'ver: {__ver}')
+            # if __branch in __ver:
             b2c = {'master': 'a', 'rc': 'r', 'stable': '.'}
             b2c = {'master': 'a', 'rc': 'r', 'stable': '.'}
-            if ver[1] == b2c[branch]:
-                return branch
+            if __ver[1] == b2c[__branch]:
+                return __branch
         except FileNotFoundError:
         except FileNotFoundError:
-            print('file not found: %s' % root + 'assets/bld_version.txt')
+            print(f'file not found: {root}' + 'assets/build_version.txt')
+    return ''
 
 
 def _commit():
     '''Returns the current branch.'''
     git_commit = exec_cmd('git rev-parse HEAD')[:7]
 
 
 def _commit():
     '''Returns the current branch.'''
     git_commit = exec_cmd('git rev-parse HEAD')[:7]
-    print('git_commit result: %s' % git_commit)
+    print(f'git_commit result: {git_commit}')
     if not git_commit.startswith("Can't r"):
         return git_commit
     root = str(Path(dirname(dirname(__file__))).parent) + '/'
     if not git_commit.startswith("Can't r"):
         return git_commit
     root = str(Path(dirname(dirname(__file__))).parent) + '/'
@@ -58,13 +58,14 @@ def _commit():
     if __file__ == '/app/bin/pmachines':  # flatpak
         root = '/app/bin/'
     try:
     if __file__ == '/app/bin/pmachines':  # flatpak
         root = '/app/bin/'
     try:
-        print('try: %s' % root + 'assets/bld_version.txt')
-        with open(root + 'assets/bld_version.txt') as fver:
-            ver = fver.read()
-            print('ver: %s' % ver)
-            return ver.split('-')[1]
+        print(f'try: {root}' + 'assets/build_version.txt')
+        with open(root + 'assets/build_version.txt', encoding='utf8') as fver:
+            __ver = fver.read()
+            print(f'ver: {__ver}')
+            return __ver.split('-')[1]
     except FileNotFoundError:
     except FileNotFoundError:
-        print('file not found: %s' % root + 'assets/bld_version.txt')
+        print(f'file not found: {root}' + 'assets/build_version.txt')
+    return ''
 
 
 def _version():
 
 
 def _version():
@@ -76,28 +77,43 @@ def _version():
     if _branch() == 'stable':
         pref, _ver = '', ''
         if exists(root + 'assets/version.txt'):
     if _branch() == 'stable':
         pref, _ver = '', ''
         if exists(root + 'assets/version.txt'):
-            with open(root + 'assets/version.txt') as fver:
+            with open(root + 'assets/version.txt', encoding='utf8') as fver:
                 pref = fver.read().strip() + '-'  # + _branch() + '-'
                 _ver = fver.read().strip()
         ret_ver = _ver or ('0.' + day)
     else:
                 pref = fver.read().strip() + '-'  # + _branch() + '-'
                 _ver = fver.read().strip()
         ret_ver = _ver or ('0.' + day)
     else:
-        #try:  we want an error!
+        # try:  we want an error!
         pref = {'master': 'a', 'rc': 'rc', '': 'runtime'}[_branch()]
         pref = {'master': 'a', 'rc': 'rc', '': 'runtime'}[_branch()]
-        #except KeyError:
+        # except KeyError:
         #    pref = 'notfound'
         #    pref = 'notfound'
-        ret_ver = '0%s%s' % (pref, day)
+        ret_ver = f'0{pref}{day}'
         pref = ret_ver
     bld_ver = pref + '-' + _commit()
     try:
         pref = ret_ver
     bld_ver = pref + '-' + _commit()
     try:
-        with open(root + 'assets/bld_version.txt', 'w') as fver:
+        with open(root + 'assets/build_version.txt', 'w', encoding='utf8') as fver:
             fver.write(bld_ver)
     except OSError:
         print("we can't write inside flatpaks, but we don't need it")
     return ret_ver
 
 
             fver.write(bld_ver)
     except OSError:
         print("we can't write inside flatpaks, but we don't need it")
     return ret_ver
 
 
-def files(_extensions, excl_dirs=None, excl_ends_with=None, root_path='.'):
-    '''Retrieves filenames in root_path with _extensions, with filters.'''
+class FindFileNamesArgs:
+
+    def __init__(self, extensions, excluding_directories=None,
+                 excluding_files_ending_with=None, root_path='.'):
+        self.extensions = extensions
+        self.excluding_directories = excluding_directories
+        self.excluding_files_ending_with = excluding_files_ending_with
+        self.root_path = root_path
+
+
+def find_file_names(find_info):
+    #TODO avoid FindFileNamesArgs, write multiple functions with different
+    # behaviors: this is a function which does more than one thing
+    _extensions = find_info.extensions
+    excl_dirs = find_info.excluding_directories
+    excl_ends_with = find_info.excluding_files_ending_with
+    root_path = find_info.root_path
     return [join(root, fname)
             for root, _, fnames in walk(root_path)
             for fname in __files_ext(fnames, _extensions)
     return [join(root, fname)
             for root, _, fnames in walk(root_path)
             for fname in __files_ext(fnames, _extensions)
@@ -112,30 +128,30 @@ def __files_ext(fnames, _extensions):
 
 def __to_be_built_single(src, tgt):
     if getmtime(tgt) > getmtime(src):
 
 def __to_be_built_single(src, tgt):
     if getmtime(tgt) > getmtime(src):
-        print('%s is newer than %s: do not build' % (tgt, src))
+        print(f'{tgt} is newer than {src}: do not build')
         return False
         return False
-    with open(src, 'rb') as f:
-        src_content = f.read()
-    with open(tgt, 'rb') as f:
-        tgt_content = f.read()
+    with open(src, 'rb') as fsrc:
+        src_content = fsrc.read()
+    with open(tgt, 'rb') as ftgt:
+        tgt_content = ftgt.read()
     hash_src = md5(src_content).hexdigest()
     hash_tgt = md5(tgt_content).hexdigest()
     cache = {}
     lines = []
     if exists('hash_cache.txt'):
     hash_src = md5(src_content).hexdigest()
     hash_tgt = md5(tgt_content).hexdigest()
     cache = {}
     lines = []
     if exists('hash_cache.txt'):
-        with open('hash_cache.txt') as f:
-            lines = f.readlines()
+        with open('hash_cache.txt', encoding='utf8') as fhash:
+            lines = fhash.readlines()
     for line in lines:
         line_spl = line.split()
     for line in lines:
         line_spl = line.split()
-        hash = line_spl[-1]
+        _hash = line_spl[-1]
         fname = ' '.join(line_spl[:-1])
         fname = ' '.join(line_spl[:-1])
-        cache[fname] = hash
+        cache[fname] = _hash
     if src in cache and tgt in cache:
         if hash_src == cache[src] and \
            hash_tgt == cache[tgt]:
     if src in cache and tgt in cache:
         if hash_src == cache[src] and \
            hash_tgt == cache[tgt]:
-            print('%s and %s are in the cache: do not build' % (tgt, src))
+            print(f'{tgt} and {src} are in the cache: do not build')
             return False
             return False
-    print('%s and %s are not up-to-date: building...' % (src, tgt))
+    print(f'{src} and {tgt} are not up-to-date: building...')
     return True
 
 
     return True
 
 
@@ -161,11 +177,11 @@ class InsideDir:
         chdir(self.old_dir)
 
 
         chdir(self.old_dir)
 
 
-bld_dpath = 'build/'
-branch = _branch()
-ver = _version()
-win_fpath = '{dst_dir}{appname}-%s-windows.exe' % branch
-#osx_fpath = '{dst_dir}{appname}-%s-osx.zip' % branch
-#flatpak_fpath = '{dst_dir}{appname}-%s-flatpak' % branch
-appimage_fpath = '{dst_dir}{appname}-%s-appimage' % branch
-#docs_fpath = '{dst_dir}{appname}-%s-docs.tar.gz' % branch
+bld_dpath = 'build/'
+branch = _branch()
+ver = _version()
+# win_fpath = '{dst_dir}{appname}-' + f'{branch}-windows.exe'
+# osx_fpath = '{dst_dir}{appname}-%s-osx.zip' % branch
+# flatpak_fpath = '{dst_dir}{appname}-%s-flatpak' % branch
+# appimage_fpath = '{dst_dir}{appname}-' + f'{branch}-appimage'
+# docs_fpath = '{dst_dir}{appname}-%s-docs.tar.gz' % branch
index 839960a1be3922e8c74085e543850c8c5355cf46..6f293a7d9314fb2192554d069519c4b17b57e2ee 100644 (file)
@@ -1,18 +1,15 @@
-from os.path import dirname
-from sys import executable
-from ya2.build.mtprocesser import ProcesserMgr
+from multiprocessing import Pool
+from os import system
 
 
 
 
-#TODO refactor: make class ImagesBuilder
+class ImagesBuilder:
 
 
+    def __init__(self, filenames, cores=None):
+        self.__filenames = filenames
+        self.__cores = cores
 
 
-def bld_images(files, cores):
-    mp_mgr = ProcesserMgr(cores)
-    list(map(__bld_img, [(str(src), mp_mgr) for src in files]))
-    mp_mgr.run()
-
-
-def __bld_img(fname_mp_mgr):
-    fname, mp_mgr = fname_mp_mgr
-    curr_path = dirname(__file__)
-    mp_mgr.add('convert "%s" "%s"' % (fname, fname[:-3] + 'dds'))
+    def build(self):
+        commands = [
+            'convert "%s" "%s"' % (f, f[:-3] + 'dds') for f in self.__filenames]
+        with Pool(processes=self.__cores) as pool:
+            pool.map(system, commands)
index 3d58730baaf7ff4f0d97ac31efe2d4f60bd15ad5..760c4eb51af939c7ec82743b7a2a1899c1ca6a9f 100644 (file)
 '''Tools for l10n.'''
 '''Tools for l10n.'''
-from os import system, makedirs, remove
-from os.path import exists
+from os import system, makedirs, remove, listdir, getcwd, chdir
+from os.path import exists, isfile, join
 from shutil import move, copy
 from shutil import move, copy
-from ya2.build.build import files
+from json import loads
+from polib import pofile, POEntry
+from ya2.build.build import find_file_names, FindFileNamesArgs
 
 
 class LanguageBuilder:
     '''Tools for building files for l10n.'''
 
 
 
 class LanguageBuilder:
     '''Tools for building files for l10n.'''
 
-    @staticmethod
-    def mo(tgt, lng_dir_code, appname):
-        '''Builds the mo file in the lng_dir_code directory.'''
-        lng_code = tgt[len(lng_dir_code):].split('/')[0]
-        lng_dir = lng_dir_code + lng_code + '/LC_MESSAGES/'
-        cmd = 'msgfmt -o {lng_dir}{appname}.mo assets/locale/po/{lng_code}.po'
-        system(cmd.format(lng_dir=lng_dir, appname=appname, lng_code=lng_code))
+    def __init__(self, app_name, po_path, json_path, mo_path, root):
+        self.__app_name = app_name
+        self.__po_path = po_path
+        self.__json_path = json_path
+        self.__mo_path = mo_path
+        self.__root = root
+
+    def build(self):
+        orig_dir = getcwd()
+        chdir(self.__root)
+        self.__build_pot()
+        po_files = [name for name in listdir(self.__po_path)
+                   if isfile(join(self.__po_path, name)) and
+                       name.endswith('.po')]
+        language_codes = [f.split('.')[0] for f in po_files]
+        for l in language_codes: self.__process_language(l)
+        chdir(orig_dir)
 
 
-    @staticmethod
-    def pot(appname, pot_path):
+    def __build_pot(self):
         '''Builds the pot file in the lng_dir_code directory.'''
         '''Builds the pot file in the lng_dir_code directory.'''
-        src_files = ' '.join(files(['py']))
+        find_info = FindFileNamesArgs(['py'], ['tests'])
+        src_files = ' '.join(find_file_names(find_info))
         cmd_tmpl = 'xgettext -ci18n -d {appname} -L python ' + \
         cmd_tmpl = 'xgettext -ci18n -d {appname} -L python ' + \
-            '-o {pot_path}{appname}.pot '
-        system(cmd_tmpl.format(appname=appname, pot_path=pot_path) + src_files)
+            '--no-location -o {pot_path}{appname}.pot '
+        system(cmd_tmpl.format(appname=self.__app_name, pot_path=self.__po_path) + src_files)
+        self.__add_from_json_to_pot()
+
+    def __add_from_json_to_pot(self):
+        json_files = [name for name in listdir(self.__json_path)
+                      if isfile(join(self.__json_path, name)) and
+                      name.endswith('.json') and
+                      name != 'index.json']
+        json_strings = []
+        for json_file in json_files:
+            with open(f'{self.__json_path}{json_file}') as f:
+                json = loads(f.read())
+            json_strings += [json['name']]
+            for instruction_line in json['instructions'].split('\n'):
+                if instruction_line:
+                    json_strings += [instruction_line]
+        def process_json_escape(string):
+            return bytes(string, 'utf-8').decode('unicode-escape')
+        json_strings = [process_json_escape(s) for s in json_strings]
+        json_strings = list(set(json_strings))
+        pot = pofile(f'{self.__po_path}{self.__app_name}.pot')
+        pot_ids = [entry.msgid for entry in pot]
+        for json_string in json_strings:
+            if json_string not in pot_ids:
+                entry = POEntry(msgid=json_string)
+                pot.append(entry)
+        pot.save(f'{self.__po_path}{self.__app_name}.pot')
+
+    def __process_language(self, language_code):
+        self.__merge(language_code)
+        self.__build_mo(language_code)
+
+    def __build_mo(self, language_code):
+        '''Builds the mo file in the lng_dir_code directory.'''
+        mo_template = '%s%s/LC_MESSAGES/%s.mo'
+        mo_name = mo_template % (self.__mo_path, language_code, self.__app_name)
+        lng_code = mo_name[len(self.__mo_path):].split('/')[0]
+        lng_dir = self.__mo_path + lng_code + '/LC_MESSAGES/'
+        cmd = 'msgfmt -o {lng_dir}{appname}.mo {po_path}{lng_code}.po'
+        system(cmd.format(lng_dir=lng_dir, appname=self.__app_name, po_path=self.__po_path, lng_code=lng_code))
 
 
-    @staticmethod
-    def merge(lng_code, tgt_path, lng_dir, appname):
+    def __merge(self, lng_code):
         '''Merges the new strings with the previous ones.'''
         '''Merges the new strings with the previous ones.'''
-        lng_base_dir = LanguageBuilder.__prepare(lng_dir, lng_code, appname)
-        LanguageBuilder.__merge(lng_base_dir, lng_code, appname, tgt_path)
-        LanguageBuilder.__postprocess(lng_code)
+        lng_base_dir = self.__prepare(lng_code)
+        self.__do_merge(lng_base_dir, lng_code)
+        self.__postprocess(lng_code)
 
 
-    @staticmethod
-    def __prepare(lng_base_dir, lng, appname):
+    def __prepare(self, lng_code):
         '''Prepares a directory for working with languages.'''
         '''Prepares a directory for working with languages.'''
-        makedirs(lng_base_dir + lng + '/LC_MESSAGES', exist_ok=True)
-        lng_dir = lng_base_dir + lng + '/LC_MESSAGES/'
-        if not exists('assets/locale/po/' + lng + '.po'):
+        makedirs(self.__mo_path + lng_code + '/LC_MESSAGES', exist_ok=True)
+        lng_dir = self.__mo_path + lng_code + '/LC_MESSAGES/'
+        if not exists('assets/locale/po/' + lng_code + '.po'):
             lines_to_fix = ['CHARSET/UTF-8', 'ENCODING/8bit']
             lines_to_fix = ['CHARSET/UTF-8', 'ENCODING/8bit']
-            [LanguageBuilder.__fix_line(line, lng_dir, appname) for line in lines_to_fix]
-            copy(lng_dir + appname + '.pot', lng_dir + appname + '.po')
+            [self.__fix_line(line, lng_dir)
+             for line in lines_to_fix]
+            copy(lng_dir + self.__app_name + '.pot', lng_dir + self.__app_name + '.po')
         return lng_dir
 
         return lng_dir
 
-    @staticmethod
-    def __fix_line(line, lng_dir, appname):
+    def __fix_line(self, line, lng_dir):
         '''Fixes po files (misaligned entries).'''
         cmd_tmpl = "sed 's/{line}/' {lng_dir}{appname}.pot > " + \
             "{lng_dir}{appname}tmp.po"
         '''Fixes po files (misaligned entries).'''
         cmd_tmpl = "sed 's/{line}/' {lng_dir}{appname}.pot > " + \
             "{lng_dir}{appname}tmp.po"
-        system(cmd_tmpl.format(line=line, lng_dir=lng_dir, appname=appname))
-        move(lng_dir + appname + 'tmp.po', lng_dir + appname + '.pot')
+        system(cmd_tmpl.format(line=line, lng_dir=lng_dir, appname=self.__app_name))
+        move(lng_dir + self.__app_name + 'tmp.po', lng_dir + self.__app_name + '.pot')
 
 
-    @staticmethod
-    def __merge(lng_dir, lng_code, appname, tgt_path):
+    def __do_merge(self, lng_dir, lng_code):
         '''Manages the msgmerge's invokation.'''
         print('merge', lng_dir)
         '''Manages the msgmerge's invokation.'''
         print('merge', lng_dir)
-        cmd = 'msgmerge -o {lng_dir}{appname}merge.po ' + \
+        cmd = 'msgmerge --no-location -o {lng_dir}{appname}merge.po ' + \
             '{tgt_path}{lng_code}.po {tgt_path}{appname}.pot'
             '{tgt_path}{lng_code}.po {tgt_path}{appname}.pot'
-        cmd = cmd.format(lng_dir=lng_dir, lng_code=lng_code, appname=appname,
-                         tgt_path=tgt_path)
+        cmd = cmd.format(lng_dir=lng_dir, lng_code=lng_code, appname=self.__app_name,
+                         tgt_path=self.__po_path)
         system(cmd)
         system(cmd)
-        copy(lng_dir + appname + 'merge.po', 'assets/locale/po/%s.po' % lng_code)
+        copy(lng_dir + self.__app_name + 'merge.po',
+             'assets/locale/po/%s.po' % lng_code)
         poname_tmpl = '{lng_dir}{appname}merge.po'
         poname_tmpl = '{lng_dir}{appname}merge.po'
-        remove(poname_tmpl.format(lng_dir=lng_dir, appname=appname))
+        remove(poname_tmpl.format(lng_dir=lng_dir, appname=self.__app_name))
 
 
-    @staticmethod
-    def __postprocess(lng_code):
+    def __postprocess(self, lng_code):
         '''Fixes po files at the end of the building process.'''
         lines = open('assets/locale/po/%s.po' % lng_code, 'r').readlines()
         with open('assets/locale/po/%s.po' % lng_code, 'w') as outf:
         '''Fixes po files at the end of the building process.'''
         lines = open('assets/locale/po/%s.po' % lng_code, 'r').readlines()
         with open('assets/locale/po/%s.po' % lng_code, 'w') as outf:
index bf9f473fbf334573445c707f74d5071cbae3082e..07980b311ede55216268b128035ca5aba85e12d6 100644 (file)
-'''Provides tools for building models.'''
 from logging import info
 from os import system, walk, makedirs
 from logging import info
 from os import system, walk, makedirs
-from os.path import exists, basename, dirname
+from os.path import exists, dirname, join, basename, splitext
+from sys import executable
+from functools import reduce
 from multiprocessing import Pool
 from multiprocessing import Pool
-from glob import glob
 from hashlib import md5
 from hashlib import md5
-from shutil import copyfile, move, rmtree
-from ya2.build.mtprocesser import ProcesserMgr
+from shutil import rmtree
 from ya2.build.build import to_be_built
 
 
 class ModelsBuilder():
 
 from ya2.build.build import to_be_built
 
 
 class ModelsBuilder():
 
+    def __init__(self, blend_path, cores):
+        self.__blend_path = blend_path
+        self.__cores = cores
+        self.__manifest = ModelsManifest()
+
+    def build(self):
+        self.__compute_files_to_build()
+        return self.__run_build()
+
+    def __compute_files_to_build(self):
+        self.__blend_files = []
+        for root, _, file_names in walk(self.__blend_path):
+            b = [f for f in file_names if f.endswith('.blend')]
+            if '/prototypes/' in root:
+                b = []
+            b = [join(root, f)
+                 for f in b
+                 if to_be_built(join(root, f), [join(root, f)])]
+            self.__blend_files += b
+
+    def __run_build(self):
+        with Pool(self.__cores) as p:
+            just_built = p.map(self.blend2bam, self.__blend_files)
+            p.close()
+            p.join()
+        just_built = [assets for b in just_built for assets in b]
+        self.__manifest.update(just_built)
+        return just_built
+
+    def blend2bam(self, file_path):
+        built = []
+        if to_be_built(self.gltf_name(file_path), [file_path]):
+            self.__clean_gltf_dir(file_path)
+            built += self.__blend2gltf(file_path)
+            built += self.__gltf2bam(file_path)
+        return built
+
+    def __clean_gltf_dir(self, file_path):
+        gltf_dir = dirname(file_path)
+        gltf_dir = gltf_dir.replace('assets/models/blend/', 'assets/models/gltf/')
+        rmtree(gltf_dir, ignore_errors=True)
+
+    @staticmethod
+    def gltf_name(file_path):
+        gltf_name = file_path.replace('assets/models/blend/', 'assets/models/gltf/')
+        return gltf_name.replace('.blend', '.gltf')
+
+    def __blend2gltf(self, file_path):
+        b2g = Blend2Gltf(file_path)
+        return b2g.export()
+
+    def __gltf2bam(self, file_path):
+        g2b = Gltf2Bam(file_path)
+        return g2b.export()
+
+
+class ModelsManifest:
+
     def __init__(self):
     def __init__(self):
-        self._cache_files = []  # for avoiding rebuilding the same file
-
-    def build(self, blend_path, cores):
-        '''Builds the models i.e. creates glTF and bam files from blend
-        ones.'''
-        args = []
-        for root, _, fnames in walk(blend_path):
-            for fname in [fname for fname in fnames if fname.endswith('.blend')]:
-                if '/prototypes/' not in root:
-                    args += [(root, fname)]
-        with Pool() as p:
-            p.starmap(self._export_blend, args)
-        # caching is broken now: it is based on the previous
-        # non-multiprocessing approach
-        # i should evaluate if fix it or just change the blend_path for building
-        # only specific models
-        cache, lines = [], []
-        if exists('hash_cache.txt'):
-            with open('hash_cache.txt') as fhash:
-                lines = fhash.readlines()
-        for line in lines:  # line's e.g. assets/path/to/gltf_or_png.ext 68ced1
-            line_spl = line.split()
-            hashval = line_spl[-1]
-            fname = ' '.join(line_spl[:-1])
-            if fname not in self._cache_files:
-                cache += [(fname, hashval)]
-        for cfile in self._cache_files:
-            cache += [(cfile, md5(open(cfile, 'rb').read()).hexdigest())]
-        cache = cache[-2048:]
-        with open('hash_cache.txt', 'w') as fhash:
-            fhash.write('\n'.join([' '.join(line) for line in cache]))
-
-    def _export_blend(self, root, fname):
-        '''Exports blend files to glTF and bam formats.'''
-        if self._export_gltf(root, fname):
-            self._export_bam(root, fname)
-
-    def _export_gltf(self, root, fname):
-        '''Exports glTF files from blend ones.'''
-        _fname = '%s/%s' % (root, fname)
-        pgltf = 'assets/models/gltf/'
-        new_dir = root.replace('assets/models/blend/', pgltf)
-        rmtree(new_dir, ignore_errors=True)
-        makedirs(new_dir)
-        #files_before = [basename(gname) for gname in glob('./*')]
-        cmd = 'blender %s --background --python ya2/build/blend2gltf.py '
-        cmd += '-- %s.gltf'
-        cmd = cmd % (_fname, new_dir + '/' + fname[:-6])
-        gltf_name = _fname.replace('assets/models/blend/', pgltf)
-        gltf_name = gltf_name.replace('.blend', '.gltf')
-        if not to_be_built(gltf_name, [_fname]):
-            return False
-        system(cmd)
-        self._cache_files += [_fname, gltf_name]
-        #files_after = [basename(gname) for gname in glob('./*')]
-        #new_files = [nnm for nnm in files_after if nnm not in files_before]
-        #new_dir = root.replace('assets/models/blend/', pgltf)
-        #rmtree(new_dir, ignore_errors=True)
-        #makedirs(new_dir)
-        #for mname in new_files:
-        #    new_name = '%s/%s' % (new_dir, mname)
-        #    move(mname, new_name)
-        #    info('move %s %s' % (mname, new_name))
-        # # blender rewrites metal files: let's restore them
-        # metal_files = [fnm for fnm in glob(new_dir + '/*') if 'metal' in fnm]
-        # for metal_file in metal_files:
-        #     src = metal_file.replace(pgltf, 'assets/models/')
-        #     if not exists(src):
-        #         src = metal_file.replace(pgltf, 'assets/models/prototypes/')
-        #         src_split = src.split('/')
-        #         src_tracks_idx = src_split.index('tracks')
-        #         before = src_split[:src_tracks_idx]
-        #         after = src_split[src_tracks_idx + 2:]
-        #         src = '/'.join(before + after)
-        #     copyfile(src, metal_file)
-        return True
-
-    def _export_bam(self, root, fname):
-        '''Exports bam files from glTF ones.'''
-        _fname = '%s/%s' % (root, fname)
-        gltf_name = (_fname[:-5] + 'gltf').replace('/blend/', '/gltf/', 1)
-        bam_name = (_fname[:-5] + 'bam').replace('/blend/', '/bam/', 1)
-        cmd_args = gltf_name, bam_name
-        # use dds files in place of png/jpg in gltf2bam
-        copyfile(gltf_name, gltf_name + '.tmp')
-        with open(gltf_name) as fgltf:
-            lines = fgltf.readlines()
-        deps = []
-        for line in lines:
-            if ('.png' in line or '.jpg' in line) and '"uri"' in line:
-                rln = line[line.index('"uri"') + 9:].rstrip(',\n"')
-                tname = '%s/%s' % (root, rln)
-                deps += [tname.replace('/models/blend/', '/models/gltf/', 1)]
-        for dep in deps:
-            tgt = dep.replace('/gltf/', '/bam/', 1)
-            tgt = tgt.replace('.png', '.dds').replace('.jpg', '.dds')
-            makedirs(dirname(tgt), exist_ok=True)
-            info('convert %s %s' % (dep, tgt))
-            system('convert %s %s' % (dep, tgt))
-        rpl = lambda lin: lin.replace('.png', '.dds').replace('.jpg', '.dds').replace('/png', '/dds').replace('/jpg', '/dds')
-        with open(gltf_name, 'w') as fgltf:
-            fgltf.write(''.join([rpl(line) for line in lines]))
-        makedirs(dirname(bam_name), exist_ok=True)
-        if to_be_built(bam_name, deps):
-            system('gltf2bam %s %s' % cmd_args)
-        self._cache_files += [bam_name] + deps
+        self.__lines = []
+        self.__manifest = []
+        self.__file_name = 'models_manifest.txt'
+
+    def update(self, just_built):
+        self.__just_built = just_built
+        self.__load()
+        self.__add_just_built()
+        self.__discard_old_entries()
+        self.__rewrite()
+
+    def __load(self):
+        if exists(self.__file_name):
+            with open(self.__file_name) as f: self.__lines = f.readlines()
+        for l in self.__lines:  # line's e.g. assets/path/to/gltf_or_png.ext 68ced1
+            self.__process_line(l)
+
+    def __process_line(self, l):
+        line_splitted = l.split()
+        hash_value = line_splitted[-1]
+        file_name = ' '.join(line_splitted[:-1])
+        if file_name not in self.__just_built:
+            self.__manifest += [(file_name, hash_value)]
+
+    def __add_just_built(self):
+        for f in self.__just_built:
+            self.__manifest += [(f, md5(open(f, 'rb').read()).hexdigest())]
+
+    def __discard_old_entries(self):
+        self.__manifest = self.__manifest[-2048:]
+
+    def __rewrite(self):
+        with open(self.__file_name, 'w') as f:
+            f.write('\n'.join([' '.join(l) for l in self.__manifest]))
+
+
+class Blend2Gltf:
+
+    def __init__(self, file_path):
+        self.__file_path = file_path
+
+    def export(self):
+        gltf_dir, file_name = dirname(self.__file_path), basename(self.__file_path)
+        gltf_dir = gltf_dir.replace('assets/models/blend/', 'assets/models/gltf/')
+        makedirs(gltf_dir, exist_ok=True)
+        file_path = splitext(gltf_dir + '/' + file_name)[0]
+        command = f'blender {self.__file_path} --background --python ya2/build/blend2gltf.py -- {file_path}.gltf'
+        system(command)
+        return [self.__file_path, ModelsBuilder.gltf_name(self.__file_path)]
+
+
+class Gltf2Bam:
+
+    def __init__(self, file_path):
+        self.__gltf_name = (file_path[:-5] + 'gltf').replace('/blend/', '/gltf/', 1)
+        self.__bam_name = (file_path[:-5] + 'bam').replace('/blend/', '/bam/', 1)
+
+    def export(self):
+        self.__build_dds_from_jpg_and_png()
+        self.__rewrite_gltf()
+        return self.__export_bam()
+
+    def __build_dds_from_jpg_and_png(self):
+        with open(self.__gltf_name) as f: self.__lines = f.readlines()
+        self.__jpg_png_files = []
+        for l in self.__lines:
+            if ('.png' in l or '.jpg' in l) and '"uri"' in l:
+                r = l[l.index('"uri"') + 7:].rstrip(',\n"')
+                t = f'{dirname(self.__gltf_name)}/{r}'
+                self.__jpg_png_files += [t.replace('/models/blend/', '/models/gltf/', 1)]
+        self.__create_dds_files()
+
+    def __create_dds_files(self):
+        for d in self.__jpg_png_files:
+            t = d.replace('/gltf/', '/bam/', 1)
+            t = t.replace('.png', '.dds').replace('.jpg', '.dds')
+            makedirs(dirname(t), exist_ok=True)
+            info(f'convert {d} {t}')
+            system(f'convert {d} {t}')
+
+    def __rewrite_gltf(self):
+        def r(lin):
+            e = [('.png', '.dds'), ('.jpg', '.dds'),
+                 ('/png', '/dds'), ('/jpg', '/dds')]
+            return reduce(lambda s, p: s.replace(*p), e, lin)
+        with open(self.__gltf_name, 'w') as f:
+            f.write(''.join([r(line) for line in self.__lines]))
+
+    def __export_bam(self):
+        makedirs(dirname(self.__bam_name), exist_ok=True)
+        if to_be_built(self.__bam_name, self.__jpg_png_files):
+            venv_path = dirname(executable)
+            gltf2bam_path = join(venv_path, 'gltf2bam')
+            system(f'{gltf2bam_path} {self.__gltf_name} {self.__bam_name}')
+        return [self.__bam_name] + self.__jpg_png_files
diff --git a/ya2/build/mtprocesser.py b/ya2/build/mtprocesser.py
deleted file mode 100644 (file)
index 5bdb6a4..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-'''A multi-threaded command dispatcher.'''
-from datetime import datetime
-from multiprocessing import cpu_count
-from threading import Thread, RLock
-from os import system
-from logging import info, debug
-
-
-class ProcesserThread(Thread):
-    '''A thread which asynchronously processes commands from a command list.'''
-
-    def __init__(self, cmd_lst, lock):
-        '''The constructor.
-        cmd_lst: commands that can be exectuted from serveral processers
-        lock: shared lock between processers for accessing the command list'''
-        Thread.__init__(self)
-        self.cmd_lst = cmd_lst
-        self.lock = lock
-
-    def run(self):
-        '''The thread's logics.'''
-        while True:
-            with self.lock:
-                if not self.cmd_lst:
-                    return
-                cmd = self.cmd_lst.pop(0)
-            info('%s %s' % (datetime.now().strftime("%H:%M:%S"), cmd))
-            system(cmd)
-
-
-class SyncProcesser:
-    '''Synchronously processes a command list.'''
-
-    def __init__(self, cmd_lst):
-        '''The constructor.
-        cmd_lst: the list that must be executed'''
-        self.cmd_lst = cmd_lst
-
-    def run(self):
-        '''The processer's logics.'''
-        for cmd in self.cmd_lst:
-            before_str = datetime.now().strftime("(executing) %H:%M:%S")
-            info('%s %s' % (before_str, cmd))
-            system(cmd)
-            after_str = datetime.now().strftime("(executed) %H:%M:%S")
-            debug('%s %s' % (after_str, cmd))
-
-
-class ProcesserMgr:
-    '''Synchronously processes commands that are submitted (eventually) using
-    multiple threads.'''
-
-    def __init__(self, cores):
-        '''The constructor.
-        cores: how many cpu cores are used to process the commands'''
-        try:
-            self.cores = cpu_count()
-        except NotImplementedError:
-            self.cores = 1
-        self.cores = cores if cores else int(self.cores / 4 + 1)
-        debug('processer-mgr: using %s cores' % self.cores)
-        self.cmd_lst = []
-
-    def add(self, cmd):
-        '''Adds cmd to the list that will be processed.'''
-        self.cmd_lst += [cmd]
-
-    def run(self):
-        '''Performs the commands that have been added.'''
-        if self.cores != 1:
-            threads, lock = [], RLock()
-            threads = [ProcesserThread(self.cmd_lst, lock)
-                       for _ in range(self.cores)]
-            [thread.start() for thread in threads]
-            [thread.join() for thread in threads]
-        else:
-            SyncProcesser(self.cmd_lst).run()
index 964cc595e0e726bcbdcd3402d54e4b88e0d87a51..f1f407dff3028121c29413e6df1ff586c3813d36 100644 (file)
@@ -1,15 +1,15 @@
 from os import system
 from os import system
-from glob import glob
-from importlib import import_module
-from inspect import isclass
 from multiprocessing import Pool
 from multiprocessing import Pool
-from logics.scene import Scene
 
 
 
 
-def do_screenshot(cls):
-    system('python main.py --screenshot ' + cls.__name__)
+class ScreenshotsBuilder:
 
 
+    def __init__(self, scene_names):
+        self.__scene_names = scene_names
 
 
-def bld_screenshots(scene_classes):
-    with Pool() as p:
-        p.map(do_screenshot, scene_classes)
+    def do_screenshot(self, scene_name):
+        system('python main.py --screenshot ' + scene_name)
+
+    def build(self):
+        with Pool() as p:
+            p.map(self.do_screenshot, self.__scene_names)
diff --git a/ya2/p3d/__init__.py b/ya2/p3d/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/ya2/p3d/gfx.py b/ya2/p3d/gfx.py
deleted file mode 100755 (executable)
index 56f6a87..0000000
+++ /dev/null
@@ -1,369 +0,0 @@
-import datetime
-from logging import debug, info
-from os import getcwd
-from os.path import exists, dirname
-from panda3d.core import get_model_path, AntialiasAttrib, PandaNode, \
-    LightRampAttrib, Camera, OrthographicLens, NodePath, OmniBoundingVolume, \
-    AmbientLight as P3DAmbientLight, Spotlight as P3DSpotlight, Point2, \
-    Point3, Texture
-from direct.filter.CommonFilters import CommonFilters
-from direct.actor.Actor import Actor
-# from ya2.lib.p3d.p3d import LibP3d
-
-
-def set_srgb(model):
-    for texture in model.find_all_textures():
-        if texture.get_format() in [Texture.F_rgba, Texture.F_rgbm]:
-            texture.set_format(Texture.F_srgb_alpha)
-        elif texture.get_format() in [Texture.F_rgb]:
-            texture.set_format(Texture.F_srgb)
-
-
-# class RenderToTexture:
-
-#     def __init__(self, size=(256, 256)):
-#         self.__set_buffer(size)
-#         self.__set_display_region()
-#         self.__set_camera()
-#         self.__set_root()
-#         self.display_region.set_camera(self.camera)
-
-#     def __set_buffer(self, size):
-#         self.buffer = base.win.make_texture_buffer('result buffer', size[0],
-#                                                    size[1])
-#         self.buffer.set_sort(-100)
-
-#     def __set_display_region(self):
-#         self.display_region = self.buffer.make_display_region()
-#         self.display_region.set_sort(20)
-
-#     def __set_camera(self):
-#         self.camera = NodePath(Camera('camera 2d'))
-#         lens = OrthographicLens()
-#         lens.set_film_size(1, 1)
-#         lens.set_near_far(-1000, 1000)
-#         self.camera.node().set_lens(lens)
-
-#     def __set_root(self):
-#         self.root = NodePath('root')
-#         self.root.set_depth_test(False)
-#         self.root.set_depth_write(False)
-#         self.camera.reparent_to(self.root)
-
-#     @property
-#     def texture(self): return self.buffer.get_texture()
-
-#     def destroy(self):
-#         base.graphicsEngine.remove_window(self.buffer)
-#         if base.win:  # if you close the window during a race
-#             base.win.remove_display_region(self.display_region)
-#         list(map(lambda node: node.remove_node(), [self.camera, self.root]))
-
-
-class P3dGfxMgr:
-
-    def __init__(self, model_path, antialiasing, shaders, srgb):
-        self.root = P3dNode(render)
-        self.__srgb = srgb
-        self.callbacks = {}
-        self.filters = None
-        get_model_path().append_directory(model_path)
-        if LibP3d.runtime():
-            root_dir = LibP3d.p3dpath(dirname(__file__))
-            paths = [root_dir + '/' + model_path, root_dir]
-            list(map(get_model_path().append_directory, paths))
-        render.set_shader_auto()
-        # render.set_two_sided(True)  # it breaks shadows
-        if antialiasing: render.set_antialias(AntialiasAttrib.MAuto)
-        if shaders and base.win:
-            self.filters = CommonFilters(base.win, base.cam)
-
-    def load_model(self, filename, callback=None, anim=None):
-        ext = '.bam' if exists(filename + '.bam') else ''
-        if anim:
-            anim_dct = {'anim': filename + '-Anim' + ext}
-            node = P3dNode(self.set_srgb(Actor(filename + ext, anim_dct)))
-        elif callback:
-            callb = lambda model: callback(P3dNode(self.set_srgb(model)))
-            node = loader.loadModel(filename + ext, callback=callb)
-        else:
-            node = P3dNode(self.set_srgb(
-                loader.loadModel(LibP3d.p3dpath(filename + ext))))
-        return node
-
-    def set_srgb(self, model):
-        if self.__srgb:
-            for texture in model.find_all_textures():
-                if texture.get_format() in [Texture.F_rgba, Texture.F_rgbm]:
-                    texture.set_format(Texture.F_srgb_alpha)
-                elif texture.get_format() in [Texture.F_rgb]:
-                    texture.set_format(Texture.F_srgb)
-        return model
-
-    @staticmethod
-    def toggle_aa():
-        aa_not_none = render.get_antialias() != AntialiasAttrib.MNone
-        if render.has_antialias() and aa_not_none:
-            render.clear_antialias()
-        else: render.set_antialias(AntialiasAttrib.MAuto, 1)
-
-    def set_toon(self):
-        tmp_node = NodePath(PandaNode('temp node'))
-        tmp_node.set_attrib(LightRampAttrib.make_single_threshold(.5, .4))
-        tmp_node.set_shader_auto()
-        base.cam.node().set_initial_state(tmp_node.get_state())
-        self.filters.set_cartoon_ink(separation=1)
-
-    def set_bloom(self):
-        if not base.win: return
-        self.filters.setBloom(
-            blend=(.3, .4, .3, 0), mintrigger=.6, maxtrigger=1.0, desat=.6,
-            intensity=1.0, size='medium')
-        # default: (.3, .4, .3, 0), .6, 1, .6, 1, 'medium'
-
-    @staticmethod
-    def pos2d(node):
-        p3d = base.cam.get_relative_point(node.node, Point3(0, 0, 0))
-        p2d = Point2()
-        return p2d if base.camLens.project(p3d, p2d) else None
-
-    @staticmethod
-    def screen_coord(pos):
-        new_node = NodePath('temp')
-        new_node.set_pos(pos)
-        coord3d = new_node.get_pos(base.cam)
-        new_node.remove_node()
-        coord2d = Point2()
-        base.camLens.project(coord3d, coord2d)
-        coord_r2d = Point3(coord2d[0], 0, coord2d[1])
-        coord_a2d = base.aspect2d.get_relative_point(render2d, coord_r2d)
-        return coord_a2d[0], coord_a2d[2]
-
-    @staticmethod
-    def world_from_to(pos):
-        p_from, p_to = Point3(), Point3()    # in camera coordinates
-        base.camLens.extrude(pos, p_from, p_to)
-        p_from = render.get_relative_point(base.cam, p_from)  # global coords
-        p_to = render.get_relative_point(base.cam, p_to)  # global coords
-        return p_from, p_to
-
-    @property
-    def shader_support(self):
-        return base.win.get_gsg().get_supports_basic_shaders()
-
-    def screenshot(self, path=None):
-        time = datetime.datetime.now().strftime('%y%m%d%H%M%S')
-        #res = base.win.save_screenshot(Filename(path or ("yocto%s.png" % time)))
-        #debug('screenshot %s (%s)' % (path or ("yocto%s.png" % time), res))
-        res = base.screenshot(path or ("pmachines%s.png" % time), False)
-        info('screenshot %s (%s; %s)' % (path or ("pmachines%s.png" % time), res, getcwd()))
-
-    @staticmethod
-    def enable_shader(): render.set_shader_auto()
-
-    @staticmethod
-    def disable_shader(): render.set_shader_off()
-
-    @staticmethod
-    def print_stats(two_d=True, three_d=True, analyze=True, ls=True):
-        '''Print graphics stats. They use standard output (from p3d).'''
-        info = []
-        if two_d and analyze:
-            info +=[('render2d.analyze', base.render2d.analyze)]
-        if three_d and analyze:
-            info +=[('render.analyze', base.render.analyze)]
-        if two_d and ls:
-            info +=[('render2d.ls', base.render2d.ls)]
-        if three_d and ls:
-            info +=[('render.ls', base.render.ls)]
-        for elm in info:
-            print('\n\n#####\n%s()' % elm[0])
-            elm[1]()
-
-
-# class P3dNode:
-
-#     def __init__(self, nodepath):
-#         self.nodepath = nodepath
-#         self.node.set_python_tag('libnode', self)
-
-#     def set_collide_mask(self, mask): return self.node.set_collide_mask(mask)
-#     def set_x(self, val): return self.node.set_x(val)
-#     def set_y(self, val): return self.node.set_y(val)
-#     def set_z(self, val): return self.node.set_z(val)
-#     def set_hpr(self, val): return self.node.set_hpr(val)
-#     def set_h(self, val): return self.node.set_h(val)
-#     def set_p(self, val): return self.node.set_p(val)
-#     def set_r(self, val): return self.node.set_r(val)
-#     def set_scale(self, val): return self.node.set_scale(val)
-#     def set_transparency(self, val): return self.node.set_transparency(val)
-#     def set_alpha_scale(self, val): return self.node.set_alpha_scale(val)
-#     def set_texture(self, texturestage, texture):
-#         return self.node.set_texture(texturestage, texture)
-#     def has_tag(self, name): return self.node.has_tag(name)
-#     def get_tag(self, name): return self.node.get_tag(name)
-#     def get_python_tag(self, name): return self.node.get_python_tag(name)
-#     def remove_node(self): return self.node.remove_node()
-#     def flatten_strong(self): return self.node.flatten_strong()
-#     def clear_model_nodes(self): return self.node.clear_model_nodes()
-#     def show(self): return self.node.show()
-#     def set_depth_offset(self, val): return self.node.set_depth_offset(val)
-#     def loop(self, val): return self.node.loop(val)
-#     def cleanup(self): return self.node.cleanup()
-#     def write_bam_file(self, fname): return self.node.write_bam_file(fname)
-
-#     def attach_node(self, name):
-#         return P3dNode(self.node.attach_new_node(name))
-
-#     def add_shape(self, shape):
-#         return self.node.node().add_shape(shape._mesh_shape)
-#         #TODO: don't access a protected member
-
-#     @property
-#     def name(self): return self.node.get_name()
-
-#     @property
-#     def node(self): return self.nodepath
-
-#     @property
-#     def p3dnode(self): return self.node.node()
-
-#     def set_pos(self, pos): return self.node.set_pos(pos._vec)
-#         #TODO: don't access a protected member
-
-#     def get_pos(self, other=None):
-#         return self.node.get_pos(* [] if other is None else [other.node])
-
-#     @property
-#     def x(self): return self.node.get_x()
-
-#     @property
-#     def y(self): return self.node.get_y()
-
-#     @property
-#     def z(self): return self.node.get_z()
-
-#     @property
-#     def hpr(self): return self.node.get_hpr()
-
-#     @property
-#     def h(self): return self.node.get_h()
-
-#     @property
-#     def p(self): return self.node.get_p()
-
-#     @property
-#     def r(self): return self.node.get_r()
-
-#     @property
-#     def scale(self): return self.node.get_scale()
-
-#     @property
-#     def is_empty(self): return self.node.is_empty()
-
-#     def get_relative_vector(self, node, vec):
-#         return self.node.get_relative_vector(node.node, vec)
-
-#     def set_material(self, mat): return self.node.set_material(mat, 1)
-
-#     def set_python_tag(self, name, val):
-#         return self.node.set_python_tag(name, val)
-
-#     def get_distance(self, other_node):
-#         return self.node.get_distance(other_node.node)
-
-#     def reparent_to(self, parent): return self.node.reparent_to(parent.node)
-
-#     def wrt_reparent_to(self, parent):
-#         return self.node.wrt_reparent_to(parent.node)
-
-#     @staticmethod
-#     def __get_pandanode(nodepath):
-#         if nodepath.has_python_tag('libnode'):
-#             return nodepath.get_python_tag('libnode')
-#         return P3dNode(nodepath)
-
-#     def find_all_matches(self, name):
-#         nodes = self.node.find_all_matches(name)
-#         return [self.__get_pandanode(node) for node in nodes]
-
-#     def find(self, name):
-#         model = self.node.find(name)
-#         if model: return self.__get_pandanode(model)
-
-#     def optimize(self):
-#         self.node.prepare_scene(base.win.get_gsg())  # crash with texture.set_format
-#         self.node.premunge_scene(base.win.get_gsg())
-
-#     def hide(self, mask=None):
-#         return self.node.hide(*[] if mask is None else [mask])
-
-#     @property
-#     def tight_bounds(self): return self.node.get_tight_bounds()
-
-#     @property
-#     def parent(self): return self.node.get_parent()
-
-#     @property
-#     def children(self): return self.node.get_children()
-
-#     def destroy(self): return self.node.remove_node()
-
-
-# class P3dAnimNode:
-
-#     def __init__(self, filepath, anim_dct):
-#         self.node = Actor(filepath, anim_dct)
-
-#     def loop(self, val): return self.node.loop(val)
-
-#     def reparent_to(self, node): self.node.reparent_to(node)
-
-#     @property
-#     def name(self): return self.node.get_name()
-
-#     def optimize(self):
-#         self.node.prepare_scene(base.win.get_gsg())
-#         self.node.premunge_scene(base.win.get_gsg())
-
-#     def set_omni(self):
-#         self.node.node().set_bounds(OmniBoundingVolume())
-#         self.node.node().set_final(True)
-
-#     def destroy(self): self.node.cleanup()
-
-
-# class P3dAmbientLight:
-
-#     def __init__(self, color):
-#         ambient_lgt = P3DAmbientLight('ambient light')
-#         ambient_lgt.set_color(color)
-#         self.ambient_np = render.attach_new_node(ambient_lgt)
-#         render.set_light(self.ambient_np)
-
-#     def destroy(self):
-#         render.clear_light(self.ambient_np)
-#         self.ambient_np.remove_node()
-
-
-# class P3dSpotlight:
-
-#     def __init__(self, mask=None):
-#         self.spot_lgt = render.attach_new_node(P3DSpotlight('spot'))
-#         snode = self.spot_lgt.node()
-#         snode.set_scene(render)
-#         snode.set_shadow_caster(True, 1024, 1024)
-#         snode.get_lens().set_fov(40)
-#         snode.get_lens().set_near_far(20, 200)
-#         if mask: snode.set_camera_mask(mask)
-#         render.set_light(self.spot_lgt)
-
-#     def set_pos(self, pos): return self.spot_lgt.set_pos(*pos)
-
-#     def look_at(self, pos): return self.spot_lgt.look_at(*pos)
-
-#     def set_color(self, color): return self.spot_lgt.set_color(*color)
-
-#     def destroy(self):
-#         render.clear_light(self.spot_lgt)
-#         self.spot_lgt.remove_node()
diff --git a/ya2/p3d/gui.py b/ya2/p3d/gui.py
deleted file mode 100755 (executable)
index 46d2a21..0000000
+++ /dev/null
@@ -1,405 +0,0 @@
-from inspect import getmro
-from panda3d.core import TextNode, Texture
-from direct.gui.DirectGuiGlobals import FLAT, ENTER, EXIT, DISABLED, NORMAL, \
-    B1PRESS
-from direct.showbase.DirectObject import DirectObject
-from direct.gui.DirectButton import DirectButton
-from direct.gui.DirectCheckButton import DirectCheckButton
-from direct.gui.DirectOptionMenu import DirectOptionMenu
-from direct.gui.OnscreenImage import OnscreenImage
-from direct.gui.DirectSlider import DirectSlider
-from direct.gui.DirectEntry import DirectEntry, ENTRY_FOCUS_STATE
-from direct.gui.DirectLabel import DirectLabel
-from direct.gui.DirectFrame import DirectFrame
-from direct.gui.OnscreenText import OnscreenText
-from direct.gui.DirectScrolledFrame import DirectScrolledFrame
-from ya2.patterns.observer import Subject
-# from ya2.lib.ivals import Seq, Wait, PosIval, Func
-
-
-class CommonBase:
-
-    def set_widget(self):
-        from ya2.lib.gui import Frame, Slider, Btn, Label, OptionMenu, \
-            CheckBtn, Entry, Img, Text, ScrolledFrame
-        from ya2.p3d.widget import FrameMixin, SliderMixin, BtnMixin, \
-            OptionMenuMixin, CheckBtnMixin, EntryMixin, ImgMixin, \
-            ScrolledFrameMixin
-        self.__class__ = self.__class__  # for pylint
-        libwdg2wdg = {
-            FrameMixin: [Frame],
-            ScrolledFrameMixin: [ScrolledFrame],
-            SliderMixin: [Slider],
-            BtnMixin: [Btn, Label],
-            OptionMenuMixin: [OptionMenu],
-            CheckBtnMixin: [CheckBtn],
-            EntryMixin: [Entry],
-            ImgMixin: [Img, Text]}
-        for libwdg, wdgcls in libwdg2wdg.items():
-            if any(cls in getmro(self.__class__) for cls in wdgcls):
-                par_cls = libwdg
-        clsname = self.__class__.__name__ + 'Widget'
-        self.__class__ = type(clsname, (self.__class__, par_cls), {})
-        self.init(self)
-        if not hasattr(self, 'bind'): return
-        bind_args = [(ENTER, self.on_wdg_enter), (EXIT, self.on_wdg_exit)]
-        list(map(lambda args: self.bind(*args), bind_args))
-
-    def set_enter_transition(self):
-        start_pos = self.get_pos()
-        pos = self.pos - (3.6, 0)
-        self.set_pos((pos.x, 1, pos.y))
-        Seq(
-            Wait(abs(pos.y - 1) / 4),
-            PosIval(self.get_np(), .5, start_pos)
-        ).start()
-
-    def set_exit_transition(self, destroy):
-        start_pos = self.get_pos()
-        end_pos = (self.pos.x + 3.6, 1, self.pos.y)
-        seq = Seq(
-            Wait(abs(self.pos.y - 1) / 4),
-            PosIval(self.get_np(), .5, end_pos),
-            Func(self.destroy if destroy else self.hide))
-        if not destroy: seq += Func(self.set_pos, start_pos)
-        seq.start()
-
-    def translate(self):
-        if hasattr(self, 'bind_transl'): self.wdg['text'] = self.bind_transl
-
-
-class P3dImg(CommonBase):
-
-    def __init__(self, filepath, pos=(0, 0), scale=1.0, background=False,
-                 foreground=False, parent=None):
-        self.img = OnscreenImage(
-            filepath, pos=(pos[0], 1, pos[1]), scale=scale, parent=parent)
-        if background: self.img.set_bin('background', 10)
-        alpha_formats = [12]  # panda3d.core.texture.Frgba
-        if self.img.get_texture().get_format() in alpha_formats:
-            self.img.set_transparency(True)
-        if foreground: self.img.set_bin('gui-popup', 50)
-
-    def reparent_to(self, node): return self.img.reparent_to(node)
-    def show(self): return self.img.show()
-    def hide(self): return self.img.hide()
-    def set_shader(self, shader): return self.img.set_shader(shader)
-    def set_shader_input(self, name, val):
-        return self.img.set_shader_input(name, val)
-    def set_texture(self, texturestage, texture):
-        return self.img.set_texture(texturestage, texture)
-
-    def set_exit_transition(self, destroy):
-        start_pos = self.get_pos()
-        end_pos = (self.pos.x + 3.6, 1, self.pos.y)
-        seq = Seq(
-            Wait(abs(self.pos.y - 1) / 4),
-            PosIval(self.get_np(), .5, end_pos),
-            Func(self.destroy if destroy else self.hide))
-        if not destroy: seq += Func(self.set_pos, (start_pos[0], start_pos[2]))
-        seq.start()
-
-    def set_pos(self, pos): return self.img.set_pos(pos[0], 1, pos[1])
-
-    def get_pos(self, pos=None): return self.img.get_pos(*[pos] if pos else [])
-
-    @property
-    def parent(self): return self.img.get_parent()
-
-    @property
-    def hidden(self): return self.img.is_hidden()
-
-    def set_transparent(self): return self.img.set_transparency(True)
-
-    def destroy(self): self.img = self.img.destroy()
-
-
-# class P3dBase(CommonBase):
-
-#     def __init__(self, tra_src=None, tra_tra=None):
-#         # self.text_src_tra = None  # it breaks the gui
-#         if tra_src and tra_tra: self.bind_tra(tra_src, tra_tra)
-
-#     def set_pos(self, pos): return self.wdg.set_pos(pos)
-#     def show(self): return self.wdg.show()
-#     def hide(self): return self.wdg.hide()
-
-#     def bind_tra(self, text_src, text_transl):
-#         # text_transl is not used, anyway we need it since we have this kind of
-#         # use: self.bind_transl('example str', _('example str'))
-#         # this allows to change translations on the fly keeping the source
-#         # text for remapping it later
-#         # TODO: try reverse mapping? i.e. retrieve the src string from the
-#         # translated one
-#         self.text_src_tra = text_src
-#         self.text_tra_tra = text_transl
-#         tra = lambda self: _(self.text_tra_tra)
-#         self.__class__.bind_transl = property(tra)
-#         self['text'] = self.bind_transl
-
-#     def get_pos(self, pos=None):
-#         return self.wdg.get_pos(*[pos] if pos else [])
-
-#     def __setitem__(self, key, value): self.wdg[key] = value
-
-#     def __getitem__(self, key): return self.wdg[key]
-
-#     def get_np(self): return self.wdg
-
-#     @property
-#     def hidden(self): return self.wdg.is_hidden()
-
-#     def destroy(self): self.wdg.destroy()
-
-
-# class P3dAbs(P3dBase):
-
-#     def get_value(self): return self.wdg.getValue()
-#     def initialiseoptions(self): return self.wdg.initialiseoptions()
-#     def set_z(self, val): return self.wdg.set_z(val)
-#     def set_shader(self, shader): return self.wdg.set_shader(shader)
-#     def set_shader_input(self, name, val):
-#         return self.wdg.set_shader_input(name, val)
-#     def set_transparency(self, val): return self.wdg.set_transparency(val)
-#     def bind(self, evt, mth): return self.wdg.bind(evt, mth)
-
-#     def attachNewNode(self, gui_itm, sort_order):
-#         # it won't work if we name it attach_node. hopefully this will be
-#         # possible when we'll use decorators in place of mixins
-#         return self.wdg.attachNewNode(gui_itm, sort_order)
-
-#     @property
-#     def is_enabled(self): return self.wdg['state'] != DISABLED
-
-
-# class P3dBtn(P3dAbs):
-
-#     def __init__(
-#             self, text='', parent=None, pos=(0, 0), scale=(1, 1),
-#             cmd=None, frame_size=(-1, 1, -1, 1), click_snd=None,
-#             text_fg=(1, 1, 1, 1), frame_col=(1, 1, 1, 1), text_font=None,
-#             over_snd=None, extra_args=None, frame_texture=None, img=None,
-#             tra_src=None, tra_tra=None, text_scale=1.0):
-#         str2par = {'bottomcenter': base.a2dBottomCenter}
-#         parent = str2par.get(parent, parent)
-#         extra_args = extra_args or []
-#         self.wdg = DirectButton(
-#             text=text, parent=parent, pos=(pos[0], 1, pos[1]),
-#             scale=(scale[0], 1, scale[1]), command=cmd,
-#             frameSize=frame_size, clickSound=click_snd, text_fg=text_fg,
-#             frameColor=frame_col, text_font=text_font, rolloverSound=over_snd,
-#             extraArgs=extra_args, frameTexture=frame_texture, image=img,
-#             text_scale=text_scale)
-#         P3dAbs.__init__(self, tra_src, tra_tra)
-#         self['relief'] = FLAT
-#         args = [(ENTER, self._on_enter), (EXIT, self._on_exit)]
-#         list(map(lambda args: self.bind(*args), args))
-
-#     def _on_enter(self, pos): pass  # pos comes from mouse
-
-#     def _on_exit(self, pos): pass  # pos comes from mouse
-
-#     # we add these with the mixins
-#     # def enable(self): self['state'] = NORMAL
-
-#     # def disable(self): self['state'] = DISABLED
-
-
-# class P3dSlider(P3dAbs):
-
-#     def __init__(
-#             self, parent=None, pos=(0, 0), scale=1, val=0,
-#             frame_col=(1, 1, 1, 1), thumb_frame_col=(1, 1, 1, 1),
-#             cmd=None, range_=(0, 1), tra_src=None, tra_tra=None):
-#         self.wdg = DirectSlider(
-#             parent=parent, pos=(pos[0], 1, pos[1]), scale=scale, value=val,
-#             frameColor=frame_col, thumb_frameColor=thumb_frame_col,
-#             command=cmd, range=range_)
-#         P3dAbs.__init__(self, tra_src, tra_tra)
-
-
-# class P3dCheckBtn(P3dAbs):
-
-#     def __init__(
-#             self, pos=(0, 0), text='', indicator_val=False,
-#             indicator_frame_col=(1, 1, 1, 1), frame_col=(1, 1, 1, 1),
-#             scale=(1, 1, 1), click_snd=None, over_snd=None,
-#             text_fg=(1, 1, 1, 1), text_font=None, cmd=None, tra_src=None,
-#             tra_tra=None):
-#         self.wdg = DirectCheckButton(
-#             pos=(pos[0], 1, pos[1]), text=text, indicatorValue=indicator_val,
-#             indicator_frameColor=indicator_frame_col,
-#             frameColor=frame_col, scale=scale, clickSound=click_snd,
-#             rolloverSound=over_snd, text_fg=text_fg, text_font=text_font,
-#             command=cmd)
-#         P3dAbs.__init__(self, tra_src, tra_tra)
-
-
-# class P3dOptionMenu(P3dAbs):
-
-#     def __init__(
-#             self, text='', items=None, pos=(0, 0), scale=(1, 1, 1),
-#             initialitem='', cmd=None, frame_size=(-1, 1, -1, 1),
-#             click_snd=None, over_snd=None, text_may_change=False,
-#             text_fg=(1, 1, 1, 1), item_frame_col=(1, 1, 1, 1),
-#             frame_col=(1, 1, 1, 1), highlight_col=(1, 1, 1, 1),
-#             text_scale=.05, popup_marker_col=(1, 1, 1, 1),
-#             item_relief=None, item_text_font=None, text_font=None,
-#             tra_src=None, tra_tra=None):
-#         items = items or []
-#         self.wdg = DirectOptionMenu(
-#             text=text, items=items, pos=(pos[0], 1, pos[1]), scale=scale,
-#             initialitem=initialitem, command=cmd, frameSize=frame_size,
-#             clickSound=click_snd, rolloverSound=over_snd,
-#             textMayChange=text_may_change, text_fg=text_fg,
-#             item_frameColor=item_frame_col, frameColor=frame_col,
-#             highlightColor=highlight_col, text_scale=text_scale,
-#             popupMarker_frameColor=popup_marker_col,
-#             item_relief=item_relief, item_text_font=item_text_font,
-#             text_font=text_font)
-#         P3dAbs.__init__(self, tra_src, tra_tra)
-
-#     def set(self, idx, f_cmd=1): return self.wdg.set(idx, f_cmd)
-
-#     @property
-#     def curr_val(self): return self.wdg.get()
-
-#     @property
-#     def curr_idx(self): return self.wdg.selectedIndex
-
-
-# class P3dEntry(P3dAbs, DirectObject, Subject):
-
-#     def __init__(
-#             self, scale=.05, pos=(0, 0), entry_font=None, width=12,
-#             frame_col=(1, 1, 1, 1), initial_text='', obscured=False,
-#             cmd=None, focus_in_cmd=None, focus_in_args=None,
-#             focus_out_cmd=None, focus_out_args=None, parent=None,
-#             tra_src=None, tra_tra=None, text_fg=(1, 1, 1, 1), on_tab=None,
-#             on_click=None):
-#         self.__focused = False
-#         self.__focus_in_cmd = focus_in_cmd
-#         self.__focus_out_cmd = focus_out_cmd
-#         DirectObject.__init__(self)
-#         Subject.__init__(self)
-#         focus_in_args = focus_in_args or []
-#         focus_out_args = focus_out_args or []
-#         self.wdg = DirectEntry(
-#             scale=scale, pos=(pos[0], 1, pos[1]), entryFont=entry_font,
-#             width=width, frameColor=frame_col, initialText=initial_text,
-#             obscured=obscured, command=cmd, focusInCommand=self._focus_in_cmd,
-#             focusInExtraArgs=focus_in_args,
-#             focusOutCommand=self._focus_out_cmd,
-#             focusOutExtraArgs=focus_out_args, parent=parent,
-#             text_fg=text_fg)
-#         P3dAbs.__init__(self, tra_src, tra_tra)
-#         if on_tab:
-#             self.on_tab_cb = on_tab
-#             self.accept('tab-up', self.on_tab)
-#         if on_click: self.wdg.bind(B1PRESS, on_click)
-
-#     def set(self, txt): return self.wdg.set(txt)
-
-#     def _focus_in_cmd(self, *args):
-#         self.__focused = True
-#         if self.__focus_in_cmd: self.__focus_in_cmd(*args)
-#         self.notify('on_entry_enter')
-
-#     def _focus_out_cmd(self, *args):
-#         self.__focused = False
-#         if self.__focus_out_cmd: self.__focus_out_cmd(*args)
-#         self.notify('on_entry_exit')
-
-#     def on_tab(self):
-#         if self.wdg['focus'] == ENTRY_FOCUS_STATE: self.on_tab_cb()
-
-#     @property
-#     def focused(self): return self.__focused
-
-#     @property
-#     def text(self): return self.wdg.get()
-
-#     def enter_text(self, txt):
-#         return self.wdg.enterText(txt)
-
-#     def enable(self): self['state'] = NORMAL
-
-#     def disable(self): self['state'] = DISABLED
-
-#     def destroy(self):
-#         self.ignore('tab-up')
-#         self.on_tab_cb = None
-#         Subject.destroy(self)
-#         P3dAbs.destroy(self)
-
-
-# class P3dLabel(P3dAbs):
-
-#     def __init__(
-#             self, text='', pos=(0, 0), parent=None, text_wordwrap=12,
-#             text_align=None, text_fg=(1, 1, 1, 1), text_font=None, scale=.05,
-#             frame_col=(1, 1, 1, 1), tra_src=None, tra_tra=None, hpr=(0, 0, 0)):
-#         self.wdg = DirectLabel(
-#             text=text, pos=(pos[0], 1, pos[1]), parent=parent,
-#             text_wordwrap=text_wordwrap, text_align=text_align,
-#             text_fg=text_fg, text_font=text_font, scale=scale,
-#             frameColor=frame_col, hpr=hpr)
-#         P3dAbs.__init__(self, tra_src, tra_tra)
-
-#     def set_bin(self, bin_name, priority): return self.wdg.set_bin(bin_name, priority)
-
-#     def set_x(self, x): return self.wdg.set_x(x)
-
-#     def set_alpha_scale(self, alpha): return self.wdg.set_alpha_scale(alpha)
-
-
-# class P3dTxt(P3dBase):
-
-#     def __init__(
-#             self, txt='', pos=(0, 0), scale=.05, wordwrap=12, parent=None,
-#             fg=(1, 1, 1, 1), font=None, align=None, tra_src=None,
-#             tra_tra=None):
-#         str2par = {'bottomleft': base.a2dBottomLeft,
-#                    'bottomright': base.a2dBottomRight,
-#                    'leftcenter': base.a2dLeftCenter}
-#         str2al = {'left': TextNode.A_left, 'right': TextNode.A_right,
-#                   'center': TextNode.A_center}
-#         if parent and parent in str2par: parent = str2par[parent]
-#         if align: align = str2al[align]
-#         self.wdg = OnscreenText(
-#             text=txt, pos=pos, scale=scale, wordwrap=wordwrap,
-#             parent=parent, fg=fg, font=font, align=align)
-#         P3dBase.__init__(self, tra_src, tra_tra)
-
-#     def set_r(self, r): return self.wdg.set_r(r)
-
-
-# class P3dFrame(P3dAbs):
-
-#     def __init__(self, frame_size=(-1, 1, -1, 1), frame_col=(1, 1, 1, 1),
-#                  pos=(0, 0), parent=None, texture_coord=False):
-#         P3dAbs.__init__(self)
-#         self.wdg = DirectFrame(frameSize=frame_size, frameColor=frame_col,
-#                                pos=(pos[0], 1, pos[1]), parent=parent)
-#         if texture_coord: self.wdg['frameTexture'] = Texture()
-
-
-# class P3dScrolledFrame(P3dAbs):
-
-#     def __init__(
-#             self, frame_sz=(-1, 1, -1, 1), canvas_sz=(0, 1, 0, 1),
-#             scrollbar_width=.05, frame_col=(1, 1, 1, 1),
-#             pos=(0, 0), parent='topleft'):
-#         P3dAbs.__init__(self)
-#         par2p3d = {'topleft': base.a2dTopLeft}
-#         if parent and parent in par2p3d: parent = par2p3d[parent]
-#         self.wdg = DirectScrolledFrame(
-#             frameSize=frame_sz,
-#             canvasSize=canvas_sz,
-#             scrollBarWidth=scrollbar_width,
-#             frameColor=frame_col,
-#             pos=(pos[0], 1, pos[1]),
-#             parent=parent)
-
-#     @property
-#     def canvas(self): return self.wdg.getCanvas()
diff --git a/ya2/p3d/p3d.py b/ya2/p3d/p3d.py
deleted file mode 100755 (executable)
index 2ea7b6f..0000000
+++ /dev/null
@@ -1,357 +0,0 @@
-import sys
-from logging import info
-from os.path import exists, dirname
-from os import getcwd, _exit
-from glob import glob
-from pathlib import Path
-from panda3d.core import loadPrcFileData, Texture, TextPropertiesManager, \
-    TextProperties, PandaSystem, Filename, WindowProperties, GraphicsWindow
-from panda3d.bullet import get_bullet_version
-from direct.showbase.ShowBase import ShowBase
-from direct.showbase.DirectObject import DirectObject
-from direct.task.Task import Task
-#from gltf import patch_loader
-
-
-# class LibShowBase(ShowBase): pass
-
-
-class LibP3d(DirectObject):
-
-    task_cont = Task.cont
-
-    def __init__(self):
-        DirectObject.__init__(self)
-        self.__end_cb = self.__notify = None
-        self.__logged_keys = {}
-
-    @staticmethod
-    def runtime(): return not exists('main.py')
-
-    @staticmethod
-    def configure():
-        loadPrcFileData('', 'notify-level-ya2 info')
-        # loadPrcFileData('', 'gl-version 3 2')
-
-    @staticmethod
-    def fixpath(path):
-        home = '/home/flavio'
-        if sys.platform == 'win32' and not exists(exists(home + '/.wine/')):
-            if path.startswith('/'): path = path[1] + ':\\' + path[3:]
-            path = path.replace('/', '\\')
-        return path
-
-    @staticmethod
-    def p3dpath(path): return Filename.fromOsSpecific(path)
-
-    @property
-    def last_frame_dt(self): return globalClock.get_dt()
-
-    @property
-    def build_version(self):
-        appimg_mnt = glob('/tmp/.mount_Yocto*')
-        if appimg_mnt:
-            #with open(appimg_mnt[0] + '/usr/bin/appimage_version.txt') as fver:
-            with open(self.curr_path + '/assets/bld_version.txt') as fver:
-                return fver.read().strip()
-        try:
-            with open(self.curr_path + '/assets/bld_version.txt') as fver:
-                return fver.read().strip()
-        except FileNotFoundError:
-            info(self.curr_path + '/assets/bld_version.txt')
-            return 'notfound'
-
-    @property
-    def is_appimage(self):
-        par_path = str(Path(__file__).parent.absolute())
-        is_appimage = par_path.startswith('/tmp/.mount_Yocto')
-        return is_appimage and par_path.endswith('/usr/bin')
-
-    @property
-    def curr_path(self):
-        if sys.platform == 'darwin':
-            return dirname(__file__) + '/../Resources/'
-        # return dirname(__file__)
-        par_path = str(Path(__file__).parent.absolute())
-        if self.is_appimage:
-            return str(Path(par_path).absolute())
-        is_snap = par_path.startswith('/snap/')
-        is_snap = is_snap and par_path.endswith('/x1')
-        if is_snap:
-            return str(Path(par_path).absolute())
-        #return getcwd()
-        curr_path = dirname(__file__)
-        info('current path: %s' % curr_path)
-        return curr_path
-
-    @staticmethod
-    def send(msg): return messenger.send(msg)
-
-    @staticmethod
-    def do_later(time, meth, args=None):
-        args = args or []
-        return taskMgr.doMethodLater(
-            time, lambda meth, args: meth(*args), meth.__name__, [meth, args])
-
-    @staticmethod
-    def add_task(mth, priority=0):
-        return taskMgr.add(mth, mth.__name__, priority)
-
-    @staticmethod
-    def remove_task(tsk): taskMgr.remove(tsk)
-
-    def init(self, green=(.2, .8, .2, 1), red=(.8, .2, .2, 1), end_cb=None):
-        LibShowBase()
-        base.disableMouse()
-        #patch_loader(base.loader)
-        self.__end_cb = end_cb
-        self.__init_win()
-        self.__init_fonts(green, red)
-        self.__set_roots()
-        self.accept('aspectRatioChanged', self.on_aspect_ratio_changed)
-
-    @staticmethod
-    def __set_roots():
-        base.a2dTopQuarter = base.aspect2d.attachNewNode('a2dTopQuarter')
-        base.a2dTopQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dTop)
-        base.a2dTopThirdQuarter = \
-            base.aspect2d.attachNewNode('a2dTopThirdQuarter')
-        base.a2dTopThirdQuarter.set_pos(base.a2dRight / 2, 0, base.a2dTop)
-        base.a2dCenterQuarter = base.aspect2d.attachNewNode('a2dCenterQuarter')
-        base.a2dCenterQuarter.set_pos(base.a2dLeft / 2, 0, 0)
-        base.a2dCenterThirdQuarter = \
-            base.aspect2d.attachNewNode('a2dCenterThirdQuarter')
-        base.a2dCenterThirdQuarter.set_pos(base.a2dRight / 2, 0, 0)
-        base.a2dBottomQuarter = base.aspect2d.attachNewNode('a2dBottomQuarter')
-        base.a2dBottomQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dBottom)
-        base.a2dBottomThirdQuarter = \
-            base.aspect2d.attachNewNode('a2dBottomThirdQuarter')
-        base.a2dBottomThirdQuarter.set_pos(
-            base.a2dRight / 2, 0, base.a2dBottom)
-
-    @staticmethod
-    def on_aspect_ratio_changed():
-        base.a2dTopQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dTop)
-        base.a2dTopThirdQuarter.set_pos(base.a2dRight / 2, 0, base.a2dTop)
-        base.a2dBottomQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dBottom)
-        base.a2dBottomThirdQuarter.set_pos(
-            base.a2dRight / 2, 0, base.a2dBottom)
-
-    @property
-    def has_window(self): return bool(base.win)
-
-    @property
-    def resolution(self):
-        if not isinstance(base.win, GraphicsWindow):
-            return 800, 600
-        win_prop = base.win.get_properties()
-        return win_prop.get_x_size(), win_prop.get_y_size()
-
-    @property
-    def resolutions(self):
-        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)
-        ret = [res(idx) for idx in range(d_i.get_total_display_modes())]
-        return ret if ret else [self.resolution]
-
-    @staticmethod
-    def toggle_fullscreen():
-        props = WindowProperties()
-        props.set_fullscreen(not base.win.is_fullscreen())
-        base.win.request_properties(props)
-
-    @staticmethod
-    def set_resolution(res, fullscreen=None):
-        props = WindowProperties()
-        props.set_size(res)
-        if fullscreen: props.set_fullscreen(True)
-        if isinstance(base.win, GraphicsWindow):
-            base.win.request_properties(props)
-
-    def __init_win(self):
-        if base.win and isinstance(base.win, GraphicsWindow):
-            base.win.set_close_request_event('window-closed')
-        # not headless
-        self.accept('window-closed', self.__on_end)
-
-    @staticmethod
-    def __init_fonts(green=(.2, .8, .2, 1), red=(.8, .2, .2, 1)):
-        tp_mgr = TextPropertiesManager.get_global_ptr()
-        for namecol, col in zip(['green', 'red'], [green, red]):
-            props = TextProperties()
-            props.set_text_color(col)
-            tp_mgr.set_properties(namecol, props)
-        for namesize, col in zip(['small', 'smaller'], [.46, .72]):
-            props = TextProperties()
-            props.set_text_scale(.46)
-            tp_mgr.set_properties(namesize, props)
-        tp_italic = TextProperties()
-        tp_italic.set_slant(.2)
-        tp_mgr.set_properties('italic', tp_italic)
-
-    def __on_end(self):
-        base.closeWindow(base.win)
-        if self.__end_cb: self.__end_cb()
-        _exit(0)
-
-    @staticmethod
-    def load_font(filepath, outline=True):
-        font = base.loader.loadFont(filepath)
-        font.set_pixels_per_unit(60)
-        font.set_minfilter(Texture.FTLinearMipmapLinear)
-        if outline: font.set_outline((0, 0, 0, 1), .8, .2)
-        return font
-
-    @staticmethod
-    def log(msg): print(msg)
-
-    @property
-    def version(self): return PandaSystem.get_version_string()
-
-    @property
-    def lib_commit(self): return PandaSystem.get_git_commit()
-
-    @property
-    def phys_version(self): return get_bullet_version()
-
-    @property
-    def user_appdata_dir(self): return Filename.get_user_appdata_directory()
-
-    @property
-    def driver_vendor(self): return base.win.get_gsg().get_driver_vendor()
-
-    @property
-    def driver_renderer(self): return base.win.get_gsg().get_driver_renderer()
-
-    @property
-    def driver_shader_version_major(self):
-        return base.win.get_gsg().get_driver_shader_version_major()
-
-    @property
-    def driver_shader_version_minor(self):
-        return base.win.get_gsg().get_driver_shader_version_minor()
-
-    @property
-    def driver_version(self): return base.win.get_gsg().get_driver_version()
-
-    @property
-    def driver_version_major(self):
-        return base.win.get_gsg().get_driver_version_major()
-
-    @property
-    def driver_version_minor(self):
-        return base.win.get_gsg().get_driver_version_minor()
-
-    @property
-    def fullscreen(self):
-        if isinstance(base.win, GraphicsWindow):
-            return base.win.get_properties().get_fullscreen()
-
-    @property
-    def volume(self): return base.sfxManagerList[0].get_volume()
-
-    @volume.setter
-    def volume(self, vol): base.sfxManagerList[0].set_volume(vol)
-
-    @property
-    def mousepos(self):
-        mwn = base.mouseWatcherNode
-        if not mwn: return 0, 0
-        if not mwn.hasMouse(): return 0, 0
-        return mwn.get_mouse_x(), mwn.get_mouse_y()
-
-    @property
-    def aspect_ratio(self): return base.getAspectRatio()
-
-    @staticmethod
-    def wdg_pos(wdg):
-        pos = wdg.get_pos(pixel2d)
-        return int(round(pos[0])), int(round(-pos[2]))
-
-    @staticmethod
-    def set_icon(filename):
-        props = WindowProperties()
-        props.set_icon_filename(filename)
-        if isinstance(base.win, GraphicsWindow):
-            base.win.requestProperties(props)
-
-    @staticmethod
-    def __set_std_cursor(show):
-        props = WindowProperties()
-        props.set_cursor_hidden(not show)
-        if isinstance(base.win, GraphicsWindow):
-            base.win.requestProperties(props)
-
-    @staticmethod
-    def show_std_cursor(): LibP3d.__set_std_cursor(True)
-
-    @staticmethod
-    def hide_std_cursor(): LibP3d.__set_std_cursor(False)
-
-    @staticmethod
-    def find_geoms(model, name):  # no need to be cached
-        geoms = model.node.find_all_matches('**/+GeomNode')
-        is_nm = lambda geom: geom.get_name().startswith(name)
-        named_geoms = [geom for geom in geoms if is_nm(geom)]
-        return [ng for ng in named_geoms if name in ng.get_name()]
-
-    @staticmethod
-    def load_sfx(filepath, loop=False):
-        sfx = loader.loadSfx(filepath)
-        sfx.set_loop(loop)
-        return sfx
-
-    def remap_code(self, key):
-        kmap = base.win.get_keyboard_map()
-        for i in range(kmap.get_num_buttons()):
-            if key.lower() == kmap.get_mapped_button_label(i).lower():
-                self.__log_key(
-                    'code mapping %s to key %s' %
-                    (key, kmap.get_mapped_button(i)), key,
-                    kmap.get_mapped_button(i))
-                return kmap.get_mapped_button(i)
-        for i in range(kmap.get_num_buttons()):
-            if key.lower() == kmap.get_mapped_button(i).get_name().lower():
-                self.__log_key(
-                    'code mapping %s to key %s' %
-                    (key, kmap.get_mapped_button(i)), key,
-                    kmap.get_mapped_button(i))
-                return kmap.get_mapped_button(i)
-        self.__log_key('not found a code mapping for %s' %
-                       key, key, 'not_found')
-        return key
-
-    def remap_str(self, key):
-        if not base.win:  # when launched with --version
-            return key
-        #if isinstance(base.win, GraphicsBuffer):
-        #    return key
-        kmap = base.win.get_keyboard_map()
-        for i in range(kmap.get_num_buttons()):
-            if str(key).lower() == kmap.get_mapped_button_label(i).lower():
-                self.__log_key(
-                    'string mapping %s to key %s' %
-                    (key, kmap.get_mapped_button(i).get_name()), key,
-                    kmap.get_mapped_button(i).get_name())
-                return kmap.get_mapped_button(i).get_name()
-        for i in range(kmap.get_num_buttons()):
-            if key.lower() == kmap.get_mapped_button(i).get_name().lower():
-                self.__log_key(
-                    'string mapping %s to key %s' %
-                    (key, kmap.get_mapped_button(i).get_name()), key,
-                    kmap.get_mapped_button(i).get_name())
-                return kmap.get_mapped_button(i).get_name()
-        self.__log_key('not found a string mapping for %s' %
-                       key, key, kmap.get_mapped_button(i).get_name())
-        return key
-
-    def __log_key(self, msg, key1, key2):
-        if key1 in self.__logged_keys and self.__logged_keys[key1] == key2:
-            return
-        self.__logged_keys[key1] = key2
-        print(msg)
-
-    def destroy(self): pass
diff --git a/ya2/patterns/__init__.py b/ya2/patterns/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/ya2/patterns/gameobject.py b/ya2/patterns/gameobject.py
deleted file mode 100644 (file)
index 6f9b292..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-from direct.fsm.FSM import FSM
-from direct.showbase.DirectObject import DirectObject
-from ya2.patterns.observer import Subject
-
-
-class Colleague(Subject):
-
-    eng = None
-
-    def __init__(self, mediator):
-        Subject.__init__(self)
-        self.mediator = mediator  # refactor: remove it
-
-    def destroy(self):
-        self.mediator = None
-        Subject.destroy(self)
-
-
-class FsmColleague(FSM, Colleague):
-
-    def __init__(self, mediator):
-        FSM.__init__(self, self.__class__.__name__)
-        Colleague.__init__(self, mediator)
-
-    def destroy(self):
-        if self.state: self.cleanup()
-        Colleague.destroy(self)
-
-
-class EventColleague(Colleague, DirectObject):
-
-    def destroy(self):
-        self.ignoreAll()
-        Colleague.destroy(self)
-
-
-class AudioColleague(Colleague): pass
-
-
-class AiColleague(Colleague): pass
-
-
-class GfxColleague(Colleague): pass
-
-
-class GuiColleague(Colleague): pass
-
-
-class LogicColleague(Colleague):
-
-    def on_start(self): pass
-
-
-class PhysColleague(Colleague): pass
-
-
-class GODirector:
-
-    def __init__(self, tgt_obj, init_lst, end_cb):
-        self.__obj = tgt_obj
-        tgt_obj.attach(self.on_comp_blt)
-        self.end_cb = end_cb
-        self.completed = [False for _ in init_lst]
-        self.pending = {}
-        self.__init_lst = init_lst
-        for idx, _ in enumerate(init_lst): self.__process_lst(tgt_obj, idx)
-
-    def __process_lst(self, obj, idx):
-        if not self.__init_lst[idx]:
-            self.end_lst(idx)
-            return
-        comp_info = self.__init_lst[idx].pop(0)
-        attr_name, cls, arguments = comp_info
-        self.pending[cls.__name__] = idx
-        setattr(obj, attr_name, cls(*arguments))
-
-    def on_comp_blt(self, obj):
-        self.__process_lst(obj.mediator, self.pending[obj.__class__.__name__])
-
-    def end_lst(self, idx):
-        self.completed[idx] = True
-        if all(self.completed):
-            if self.end_cb: self.end_cb()
-            self.destroy()
-
-    def destroy(self):
-        self.__obj.detach(self.on_comp_blt)
-        self.__obj = self.end_cb = self.__init_lst = None
-
-
-class GameObject(Subject): pass
diff --git a/ya2/patterns/observer.py b/ya2/patterns/observer.py
deleted file mode 100644 (file)
index 7c49116..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-class ObsInfo:
-
-    def __init__(self, mth, sort, args):
-        self.mth = mth
-        self.sort = sort
-        self.args = args
-
-    def __repr__(self): return str(self.mth)
-
-
-class Subject:
-
-    def __init__(self):
-        self.observers = {}
-
-    def attach(self, obs_meth, sort=10, rename='', args=None):
-        args = args or []
-        onm = rename or obs_meth.__name__
-        if onm not in self.observers: self.observers[onm] = []
-        self.observers[onm] += [ObsInfo(obs_meth, sort, args)]
-        sorted_obs = sorted(self.observers[onm], key=lambda obs: obs.sort)
-        self.observers[onm] = sorted_obs
-
-    def detach(self, obs_meth, lambda_call=None):
-        if isinstance(obs_meth, str):
-            onm = obs_meth
-            observers = [obs for obs in self.observers[onm]
-                         if obs.mth == lambda_call]
-        else:
-            onm = obs_meth.__name__
-            observers = [obs for obs in self.observers[onm]
-                         if obs.mth == obs_meth]
-        if not observers: raise Exception
-        list(map(self.observers[onm].remove, observers))
-
-    def notify(self, meth, *args, **kwargs):
-        if meth not in self.observers: return  # no obs for this notification
-        for obs in self.observers[meth][:]:
-            if obs in self.observers[meth]:  # if an obs removes another one
-                try:
-                    act_args = obs.args + list(args)
-                    obs.mth(*act_args, **kwargs)
-                except SystemError:
-                    print('Quit')
-                    import sys; sys.exit()
-
-    def observing(self, obs_meth):
-        if callable(obs_meth): obs_meth = obs_meth.__name__
-        return obs_meth in self.observers and self.observers[obs_meth]
-
-    def destroy(self): self.observers = None
-
-
-class Observer: pass
diff --git a/ya2/tools/pdfsingle.py b/ya2/tools/pdfsingle.py
deleted file mode 100755 (executable)
index 6d76361..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-# python ya2/tools/pdfsingle.py path/to/file.py
-from os import chdir, getcwd, system
-from os.path import dirname, basename, exists
-from sys import argv
-
-
-class InsideDir:
-
-    def __init__(self, dir_):
-        self.dir = dir_
-        self.old_dir = getcwd()
-
-    def __enter__(self):
-        chdir(self.dir)
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        chdir(self.old_dir)
-
-
-filename = argv[1]
-name = basename(filename)
-path = dirname(filename)
-noext = name.rsplit('.', 1)[0]
-test_tmpl = "tail -n +1 {found} " + \
-    "| sed 's/==> /# ==> /' > tmp.txt ; enscript --font=Courier10 " + \
-    "--continuous-page-numbers --no-header --pretty-print=python " + \
-    "-o - tmp.txt | psnup -2 -P letter -p a4 -m12 | ps2pdf - {name}.pdf ; rm tmp.txt"
-    #"-o - tmp.txt | ps2pdf - {name}.pdf ; rm tmp.txt"
-found = filename
-with InsideDir('tests/' + path):
-    if exists('test_' + name):
-        found += ' ya2/tests/%s/test_%s' % (path, name)
-test_cmd = test_tmpl.format(name=noext, found=found)
-system(test_cmd)
-#system('pdfnup --nup 2x1 -o {noext}.pdf {noext}.pdf'.format(noext=noext))
diff --git a/ya2/utils/asserts.py b/ya2/utils/asserts.py
new file mode 100644 (file)
index 0000000..8eb0439
--- /dev/null
@@ -0,0 +1,112 @@
+import threading
+from re import search
+
+
+class Assert:
+
+    @staticmethod
+    def assert_render3d():
+        preserve = ['camera', 'DIRECT']
+        unexpected_nodes = [c for c in render.children if c.name not in preserve]
+        for node in unexpected_nodes: Assert.__process_render3d_exception(node)
+
+    @staticmethod
+    def __process_render3d_exception(node):
+        render.ls()
+        message = f'unexpected render3d node: {node.name}'
+        raise Exception(message)
+
+    @staticmethod
+    def assert_aspect2d():
+        preserve = [
+            'a2dBackground', 'a2dTopCenter', 'a2dTopCenterNS',
+            'a2dBottomCenter', 'a2dBottomCenterNS', 'a2dLeftCenter',
+            'a2dLeftCenterNS', 'a2dRightCenter', 'a2dRightCenterNS',
+            'a2dTopLeft', 'a2dTopLeftNS', 'a2dTopRight',
+            'a2dTopRightNS', 'a2dBottomLeft', 'a2dBottomLeftNS',
+            'a2dBottomRight', 'a2dBottomRightNS', 'test_txt']
+        unexpected_nodes = [c for c in aspect2d.children
+                            if c.name not in preserve and not c.has_python_tag('preserve')]
+        for node in unexpected_nodes: Assert.__process_aspect2d_exception(node)
+
+    @staticmethod
+    def __process_aspect2d_exception(node):
+        aspect2d.ls()
+        message = f'unexpected aspect2d node: {node.name}'
+        raise Exception(message)
+
+    @staticmethod
+    def assert_render2d():
+        preserve = ['aspect2d', 'pixel2d', 'camera2d']
+        unexpected_nodes = [c for c in render2d.children if c.name not in preserve and not c.has_python_tag('preserve')]
+        for node in unexpected_nodes: Assert.__process_render2d_exception(node)
+
+    @staticmethod
+    def __process_render2d_exception(self, node):
+        render2d.ls()
+        message = f'unexpected render2d node: {node.name}'
+        raise Exception(message)
+
+    @staticmethod
+    def assert_events():
+        preserve = ['window-event', 'window-closed', 'async_loader_0', 'async_loader_1',
+                    'render-texture-targets-changed', 'aspectRatioChanged', 'new_scene']
+        preserve_re = ['^async_loader_.*', '^click-mouse1-pg.*']
+        unexpected_events = [e for e in messenger.getEvents()
+                             if e not in preserve and not any(search(r, e) for r in preserve_re)]
+        for e in unexpected_events: Assert.__process_event_exception(e)
+
+    @staticmethod
+    def __process_event_exception(event):
+        if (acc := messenger.who_accepts(event)):
+            Assert.__process_event_acceptors(acc, event)
+
+    @staticmethod
+    def __process_event_acceptors(acceptors, event):
+        for key, a in acceptors.items(): Assert.__process_event_acceptor(key, a, event)
+
+    @staticmethod
+    def __process_event_acceptor(key, acceptor, event):
+        #if acceptor[2]:
+        #    print(f'the event {event} accepted by <{key}, {acceptor[0]}> is persistent')
+        #else:
+            Assert.__actually_process_event_exception(acceptor, event)
+
+    @staticmethod
+    def __actually_process_event_exception(acceptor, event):
+        message = f'unexpected event: {event}, {str(acceptor)}'
+        raise Exception(message)
+
+    @staticmethod
+    def assert_tasks():
+        preserve = [
+            'ivalLoop', 'garbageCollectStates', 'collisionLoop',
+            'igLoop', 'audioLoop', 'resetPrevTransform', 'dataLoop',
+            'eventManager', 'simplepbr update', 'on frame music',
+            'assert_fps', 'DIRECTContextTask']
+        unexpected_tasks = [t for t in taskMgr.getTasks() + taskMgr.getDoLaters()
+                            if t.name not in preserve and not hasattr(t, 'preserve')]
+        for t in unexpected_tasks: Assert.__process_task_exception(t)
+
+    @staticmethod
+    def __process_task_exception(task):
+        message = f'unexpected task: {task.name}'
+        raise Exception(message)
+
+    @staticmethod
+    def assert_buffers():
+        pass
+        #if RenderToTexture.buffers:
+        #    raise Error()
+
+    @staticmethod
+    def assert_threads():
+        thread_names = [thread.name for thread in threading.enumerate()]
+        preserve = ['MainThread', 'rpc_server']
+        unexpected_tasks = [t for t in thread_names if t not in preserve]
+        for t in unexpected_tasks: Assert.__process_unexpected_thread(t)
+
+    @staticmethod
+    def __process_unexpected_thread(thread_name):
+        message = f'unexpected thread: {thread_name}'
+        raise Exception(message)
diff --git a/ya2/utils/audio.py b/ya2/utils/audio.py
new file mode 100644 (file)
index 0000000..98b5195
--- /dev/null
@@ -0,0 +1,9 @@
+class AudioTools:
+
+    @staticmethod
+    def set_volume(volume):
+        base.musicManager.set_volume(.8 * volume)
+        base.sfxManagerList[0].set_volume(volume)
+
+    @staticmethod
+    def load_sfx(path): return loader.load_sfx(path)
diff --git a/ya2/utils/build_metal_texture.py b/ya2/utils/build_metal_texture.py
new file mode 100644 (file)
index 0000000..f20ba4a
--- /dev/null
@@ -0,0 +1,16 @@
+from sys import argv
+from os.path import abspath
+from os import system
+
+ao = abspath(argv[1])
+roughness = abspath(argv[2])
+metal = abspath(argv[3])
+fout = abspath(argv[4])
+
+system('cp %s %s' % (ao, fout))
+cmd = 'convert %s %s -alpha off -compose copy_green -composite %s' % (fout, roughness, fout)
+print(cmd)
+system(cmd)
+cmd = 'convert %s %s -alpha off -compose copy_blue -composite %s' % (fout, metal, fout)
+print(cmd)
+system(cmd)
diff --git a/ya2/utils/cursor.py b/ya2/utils/cursor.py
deleted file mode 100644 (file)
index 36ff05b..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-from panda3d.core import GraphicsWindow, WindowProperties
-from ya2.p3d.gui import P3dImg as Img
-from ya2.patterns.gameobject import GameObject
-
-
-class MouseCursorFacade:
-
-    def show(self):
-        if not self.eng.cfg.dev_cfg.functional_test:
-            return self.cursor_img.show()
-    def hide(self): return self.cursor_img.hide()
-
-
-class MouseCursor(GameObject, MouseCursorFacade):
-
-    def __init__(self, filepath, scale, color, hotspot):
-        GameObject.__init__(self)
-        MouseCursorFacade.__init__(self)
-        if not filepath: return
-        self.__set_std_cursor(False)
-        self.cursor_img = Img(filepath, scale=scale, foreground=True)
-        self.cursor_img.img.set_color(color)
-        #if self.eng.cfg.dev_cfg.functional_test:
-        #    self.cursor_img.hide()
-        self.hotspot_dx = scale[0] * (1 - 2 * hotspot[0])
-        self.hotspot_dy = scale[2] * (1 - 2 * hotspot[1])
-        #self.eng.attach_obs(self.on_frame)
-        #self.eng.attach_obs(self.on_frame_unpausable)
-        self._tsk = taskMgr.add(self.__on_frame, 'on frame cursor')
-
-    @staticmethod
-    def __set_std_cursor(show):
-        props = WindowProperties()
-        props.set_cursor_hidden(not show)
-        if isinstance(base.win, GraphicsWindow):
-            base.win.requestProperties(props)
-
-    #def show_standard(self): self.eng.lib.show_std_cursor()
-
-    #def hide_standard(self): self.eng.lib.hide_std_cursor()
-
-    #def cursor_top(self):
-    #    self.cursor_img.reparent_to(self.cursor_img.parent)
-
-    def __on_frame(self, task):
-        mwn = base.mouseWatcherNode
-        if not mwn or not mwn.hasMouse():
-            return task.again
-        mouse = mwn.get_mouse_x(), mwn.get_mouse_y()
-        h_x = mouse[0] * base.getAspectRatio() + self.hotspot_dx
-        self.cursor_img.set_pos((h_x, mouse[1] - self.hotspot_dy))
-        return task.again
-
-    #def on_frame(self):
-    #    if not self.eng.pause.paused: self.__on_frame()
-
-    #def on_frame_unpausable(self):
-    #    if self.eng.pause.paused: self.__on_frame()
-
-    def set_image(self, img):
-        self.cursor_img.img.set_texture(loader.load_texture(img), 1)
-
-    def destroy(self):
-        taskMgr.remove(self._tsk)
-        self.cursor_img.destroy()
diff --git a/ya2/utils/decorator.py b/ya2/utils/decorator.py
new file mode 100644 (file)
index 0000000..7341443
--- /dev/null
@@ -0,0 +1,13 @@
+class Decorator:
+
+    def __init__(self, decorated):
+        self.__dict__['_decorated'] = decorated
+
+    def __getattr__(self, attr):
+        return getattr(self._decorated, attr)
+
+    def __setattr__(self, attr, value):
+        return setattr(self._decorated, attr, value)
+
+    def __getitem__(self, key):
+        return self._decorated[key]
index b30aefe53263a627b68956740e3fc0e4544f5328..622ecd2c2f294dcdf2c3be20e97fe72fd42bb8d4 100644 (file)
@@ -4,42 +4,45 @@ from os import makedirs
 from os.path import dirname
 from collections.abc import Mapping
 from configparser import ConfigParser
 from os.path import dirname
 from collections.abc import Mapping
 from configparser import ConfigParser
-from json import load, dumps
-from ya2.patterns.gameobject import GameObject
-from ya2.p3d.p3d import LibP3d
+from ya2.utils.logics import LogicsTools
 
 
 
 
-class DctFile(GameObject):
+class DctFile:
 
     def __init__(self, fpath, default_dct=None, persistent=True):
 
     def __init__(self, fpath, default_dct=None, persistent=True):
-        GameObject.__init__(self)
         default_dct = default_dct or {}
         default_dct = default_dct or {}
-        if sys.platform == 'darwin' and LibP3d.runtime():
+        if sys.platform == 'darwin' and LogicsTools.in_build:
             fpath = dirname(__file__) + '/' + fpath
         self.fpath = fpath
         self.persistent = persistent
         try:
             fpath = dirname(__file__) + '/' + fpath
         self.fpath = fpath
         self.persistent = persistent
         try:
-            #with open(fpath) as json: fdct = load(json)
+            # with open(fpath) as json: fdct = load(json)
             config = ConfigParser()
             config.read(fpath)
             config = ConfigParser()
             config.read(fpath)
-            fdct = {section: dict(config.items(section)) for section in config.sections()}
+            fdct = {section: dict(config.items(section))
+                    for section in config.sections()}
             fdct = self.__typed_dct(fdct)
             self.dct = self.__add_default(default_dct, fdct)
             fdct = self.__typed_dct(fdct)
             self.dct = self.__add_default(default_dct, fdct)
-        except IOError: self.dct = default_dct
+        except IOError:
+            self.dct = default_dct
 
     @staticmethod
     def __typed_dct(dct):
         def convert_single_val(val):
 
     @staticmethod
     def __typed_dct(dct):
         def convert_single_val(val):
-            try: return int(val)
+            try:
+                return int(val)
             except ValueError:
             except ValueError:
-                try: return float(val)
+                try:
+                    return float(val)
                 except ValueError:
                     if not val or val[0] != '[':
                         return val
                     else:
                         raise ValueError
                 except ValueError:
                     if not val or val[0] != '[':
                         return val
                     else:
                         raise ValueError
+
         def converted(val):
         def converted(val):
-            try: return convert_single_val(val)
+            try:
+                return convert_single_val(val)
             except ValueError:
                 return [elm.strip() for elm in val[1:-1].split(',')]
         new_dct = {}
             except ValueError:
                 return [elm.strip() for elm in val[1:-1].split(',')]
         new_dct = {}
@@ -55,7 +58,8 @@ class DctFile(GameObject):
         for key, val in upd.items():
             if isinstance(val, Mapping):
                 dct[key] = DctFile.__add_default(dct.get(key, {}), val)
         for key, val in upd.items():
             if isinstance(val, Mapping):
                 dct[key] = DctFile.__add_default(dct.get(key, {}), val)
-            else: dct[key] = upd[key]
+            else:
+                dct[key] = upd[key]
         return dct
 
     @staticmethod
         return dct
 
     @staticmethod
@@ -63,15 +67,16 @@ class DctFile(GameObject):
         for key, val in new_dct.items():
             if isinstance(val, Mapping):
                 dct[key] = DctFile.deepupdate(dct.get(key, {}), val)
         for key, val in new_dct.items():
             if isinstance(val, Mapping):
                 dct[key] = DctFile.deepupdate(dct.get(key, {}), val)
-            else: dct[key] = val
+            else:
+                dct[key] = val
         return dct
 
     def store(self):
         info('storing %s' % self.fpath)
         if not self.persistent: return
         return dct
 
     def store(self):
         info('storing %s' % self.fpath)
         if not self.persistent: return
-        #json_str = dumps(self.dct, sort_keys=True, indent=4,
-        #                 separators=(',', ': '))
-        #with open(self.fpath, 'w') as json: json.write(json_str)
+        # json_str = dumps(self.dct, sort_keys=True, indent=4,
+        #                  separators=(',', ': '))
+        # with open(self.fpath, 'w') as json: json.write(json_str)
         fdct = {}
         for section, sec_dct in self.dct.items():
             if section not in fdct:
         fdct = {}
         for section, sec_dct in self.dct.items():
             if section not in fdct:
index 1a886cb13155af3bf08270258cdc4fe887ca7b5a..b218deff1fcb922fadc22876994f3f176b86f1a1 100644 (file)
-import datetime
-from os import getcwd, system
-from logging import debug, info
-from pathlib import Path
+from os import getcwd, makedirs
+from logging import info
 from shutil import rmtree
 from shutil import rmtree
-from os import makedirs
 from os.path import join, exists
 from os.path import join, exists
-from glob import glob
-from sys import exit
+from pathlib import Path
 from xmlrpc.server import SimpleXMLRPCServer
 from threading import Thread
 from xmlrpc.server import SimpleXMLRPCServer
 from threading import Thread
+from collections import namedtuple
 from panda3d.core import Filename
 from direct.gui.OnscreenText import OnscreenText
 from panda3d.core import Filename
 from direct.gui.OnscreenText import OnscreenText
-from ya2.patterns.gameobject import GameObject
 from ya2.build.build import _branch
 
 
 class RPCServer(SimpleXMLRPCServer):
 
     def __init__(self, callbacks):
 from ya2.build.build import _branch
 
 
 class RPCServer(SimpleXMLRPCServer):
 
     def __init__(self, callbacks):
-        super().__init__(('localhost', 6000), allow_none=True)
-        self._callbacks = callbacks
+        super().__init__(('localhost', 7000), allow_none=True)
+        self.__callbacks = callbacks
         self.register_introspection_functions()
         self.register_introspection_functions()
-        self.register_function(self.screenshot, 'screenshot')
-        self.register_function(self.enforce_res, 'enforce_res')
-        self.register_function(self.verify, 'verify')
-        self.register_function(self.set_idx, 'set_idx')
-        self.register_function(self.enforce_resolution, 'enforce_resolution')
-        self.register_function(self.get_pos, 'get_pos')
+        self.register_function(self.__screenshot, 'screenshot')
+        self.register_function(self.__enforce_result, 'enforce_result')
+        self.register_function(self.__verify, 'verify')
+        self.register_function(self.__set_index, 'set_index')
+        self.register_function(self.__enforce_resolution, 'enforce_resolution')
+        self.register_function(self.get_position, 'get_position')
         self.register_function(self.destroy, 'destroy')
 
         self.register_function(self.destroy, 'destroy')
 
-    def screenshot(self, arg):
-        taskMgr.doMethodLater(.01, self._callbacks[0], 'cb0', [arg])
+    def __screenshot(self, argument):
+        # refactor: these methods should not depend on taskMgr
+        # since i can't write unit tests for them
+        info(f'RPCServer::__screnshot {argument}')
+        taskMgr.do_method_later(.01, self.__callbacks.screenshot, 'rpc_screenshot', [argument])
 
 
-    def enforce_res(self, arg):
-        taskMgr.doMethodLater(.01, self._callbacks[1], 'cb1', [arg])
+    def __enforce_result(self, argument):
+        info(f'RPCServer::__enforce_result {argument}')
+        taskMgr.do_method_later(.01, self.__callbacks.enforce_result, 'rpc_enforce_result', [argument])
 
 
-    def verify(self):
-        taskMgr.doMethodLater(.01, self._callbacks[2], 'cb2')
+    def __verify(self):
+        info('RPCServer::__verify')
+        taskMgr.do_method_later(.01, self.__callbacks.verify, 'rpc_verify')
 
 
-    def set_idx(self, arg):
-        taskMgr.doMethodLater(.01, self._callbacks[3], 'cb3', [arg])
+    def __set_index(self, argument):
+        info(f'RPCServer::__set_index {argument}')
+        taskMgr.do_method_later(.01, self.__callbacks.set_index, 'rpc_set_index', [argument])
 
 
-    def enforce_resolution(self, arg):
-        taskMgr.doMethodLater(.01, self._callbacks[4], 'cb4', [arg])
+    def __enforce_resolution(self, argument):
+        info(f'RPCServer::__enforce_resolution {argument}')
+        taskMgr.do_method_later(.01, self.__callbacks.enforce_resolution, 'rpc_enforce_resolution', [argument])
 
 
-    def get_pos(self, arg):
-        return self._callbacks[5](arg)
+    def get_position(self, arg):
+        info(f'RPCServer::get_position {arg}')
+        return self.__callbacks.get_position(arg)
 
     def destroy(self):
 
     def destroy(self):
+        info('RPCServer::destroy')
         self._BaseServer__shutdown_request = True
 
 
 class RPCServerThread(Thread):
 
     def __init__(self, callbacks):
         self._BaseServer__shutdown_request = True
 
 
 class RPCServerThread(Thread):
 
     def __init__(self, callbacks):
-        Thread.__init__(self)
-        self._callbacks = callbacks
+        Thread.__init__(self, name='rpc_server')
+        self.__callbacks = callbacks
 
     def run(self):
 
     def run(self):
-        self.server = RPCServer(self._callbacks)
-        self.server.serve_forever()
-
-
-class FunctionalTest(GameObject):
-
-    def __init__(self, ref, pos_mgr):
-        super().__init__()
-        self._pos_mgr = pos_mgr
-        RPCServerThread([self._do_screenshot, self._do_enforce_res, self.__verify, self._set_idx, self._do_enforce_resolution, self.__get_pos]).start()
-        self.txt = OnscreenText('', fg=(1, 0, 0, 1), scale=.16)
-        #self._path = ''
-        #if self.eng.is_appimage:
-        self._path = str(Filename().get_user_appdata_directory())
-        self._path += '/pmachines/'
-        self._path += 'tests/functional%s/' % ('_ref' if ref else '')
-        home = '/home/flavio'  # we must force this for wine
-        # if self._path.startswith('/c/users/') and exists(str(Path.home()) + '/.local/share/flatpak-wine601/default/'):
-        #     self._path = str(Path.home()) + '/.local/share/flatpak-wine601/default/drive_' + self._path[1:]
-        if self._path.startswith('/c/users/') and exists(home + '/.wine/'):
-            self._path = home + '/.wine/drive_' + self._path[1:]
-        if ref:
-            self._path = join(
+        self.__server = RPCServer(self.__callbacks)
+        self.__server.serve_forever()
+
+
+class FunctionalTest:
+
+    def __init__(self, creating_references, position_manager, app_name):
+        self.__creating_references = creating_references
+        self.__position_manager = position_manager
+        self.__app_name = app_name
+        Callbacks = namedtuple('Callbacks', 'screenshot enforce_result verify set_index enforce_resolution get_position')
+        callbacks = Callbacks(self.__screenshot,
+                              self.__enforce_result,
+                              self.__verify,
+                              self.__set_index,
+                              self.__enforce_resolution,
+                              self.__get_position)
+        RPCServerThread(callbacks).start()
+        self.__debug_text = OnscreenText('', fg=(1, 0, 0, 1), scale=.16)
+        self.__debug_text.name = 'test_txt'
+        self.__set_path()
+        self.__file_names = []
+
+    def __set_path(self):
+        self.__set_path_base_case()
+        self.__set_path_wine()
+        self.__set_path_references()
+
+    def __set_path_base_case(self):
+        self.__path = str(Filename().get_user_appdata_directory())
+        self.__path += f'/{self.__app_name}/'
+        self.__path += f'tests/functional{"_ref" if self.__creating_references else ""}/'
+
+    def __set_path_wine(self):
+        home = str(Path.home())
+        p = self.__path[1] + ':' + self.__path[2:].replace('/', '\\')
+        info(f"__set_path_wine: {self.__path} {home} {p}")
+        #if self.__path.startswith('/c/users/') and exists(home + '/.wine/'):
+        #    self.__path = home + '/.wine/drive_' + self.__path[1:]
+        if self.__path.startswith('/c/users/'):
+            self.__path = p
+
+    def __set_path_references(self):
+        if self.__creating_references:
+            self.__path = join(
                 Filename().get_user_appdata_directory(),
                 Filename().get_user_appdata_directory(),
-                'pmachines/tests/functional_ref_%s/' % _branch())
-        self._fnames = []
-        #taskMgr.add(self.on_frame_unpausable, 'on-frame-unpausable')
-        #self._do_screenshots(idx)
-
-    def _set_idx(self, idx):
-        if int(idx) == 1:
-            rmtree(self._path, ignore_errors=True)
-        info('creating dir: %s' % self._path)
-        makedirs(self._path, exist_ok=True)
+                f'{self.__app_name}/tests/functional_ref_%s/' % _branch())
 
 
-    def __get_pos(self, tgt):
-        return self._pos_mgr.get(tgt)
+    def __set_index(self, idx):
+        if int(idx) == 1: rmtree(self.__path, ignore_errors=True)
+        info(f'creating dir: {self.__path}')
+        makedirs(self.__path, exist_ok=True)
 
 
-    def _do_screenshot(self, name):
-        self._fnames += [self._path + name]
-        #time = datetime.datetime.now().strftime('%y%m%d%H%M%S')
-        #res = base.win.save_screenshot(Filename(path or ("yocto%s.png" % time)))
-        #debug('screenshot %s (%s)' % (path or ("yocto%s.png" % time), res))
-        res = base.screenshot(self._path + name, False)
-        info('screenshot %s (%s; %s)' % (self._path + name, res, getcwd()))
+    def __get_position(self, target):
+        info(f'FunctionalTest::__get_position {target}')
+        if target not in self.__position_manager or not target:
+            info(f'{self.__position_manager=}')
+        return self.__position_manager[target]
 
 
-    def _do_enforce_res(self, res):
-        info('enforce_res %s' % res)
-        messenger.send('enforce_res', [res])
+    def __screenshot(self, name):
+        self.__file_names += [self.__path + name]
+        r = base.screenshot(self.__path + name, False)
+        info(f'screenshot {self.__path + name} ({r}; {getcwd()})')
 
 
-    def _do_enforce_resolution(self, res):
-        info('enforce resolution %s (callback)' % res)
-        messenger.send('enforce_resolution', [res])
+    def __enforce_result(self, result):
+        info(f'enforce_result {result}')
+        messenger.send('enforce_result', [result])
 
 
-    #def _screenshot(self, time, name):
-        #self._fnames += [self._path + name + '.png']
-        #self._tasks += [(
-        #    self._curr_time + time,
-        #    lambda: self._do_screenshot(self._path + name + '.png'),
-        #    'screenshot: %s' % name)]
-        #def txt(show_hide):
-        #    self.txt['text'] = name
-        #    (self.txt.show if show_hide else self.txt.hide)()
-        #self._tasks += [(
-        #    self._curr_time + time + .1,
-        #    lambda: txt(True),
-        #    'screenshot: %s (show)' % name)]
-        #self._tasks += [(
-        #    self._curr_time + time + FunctionalTest.evt_time - .1,
-        #    lambda: txt(False),
-        #    'screenshot: %s (hide)' % name)]
-        #self._curr_time += time
-
-    #def __keypress(self, key):
-        #'''Emulates a keypress'''
-        #dev = base.win.getInputDevice(0)
-        #dev.buttonDown(key)
-        #dev.buttonUp(key)
-
-    #def __char_entered(self, char):
-        #'''Emulates a character being entered.'''
-        #dev = base.win.getInputDevice(0)
-        #dev.keystroke(ord(char))
-
-    # def _event(self, time, evt, messenger_evt=False, append_up=True, mouse_args=None):
-    #     def _append_up(evt_name):
-    #         return evt + ('' if evt.endswith('-up') or not append_up else '-up')
-    #     def cback_char(_evt):
-    #         self.__char_entered(_evt)
-    #     def cback_keyp(_evt):
-    #         self.__keypress(_evt)
-    #         self.__keypress('raw-' + _evt)
-    #     cback = lambda: (cback_char(evt) if len(evt) == 1 else cback_keyp(evt))
-    #     if evt in ['mousemove', 'mouseclick', 'mousedrag']:
-    #         if evt == 'mousemove':
-    #             cback = lambda: self.__mouse_move(*mouse_args)
-    #         elif evt == 'mouseclick':
-    #             cback = lambda: self.__mouse_click(*mouse_args)
-    #         elif evt == 'mousedrag':
-    #             cback = lambda: self.__mouse_drag(*mouse_args)
-    #     if messenger_evt:
-    #         cback = lambda: messenger.send(_append_up(evt))
-    #     self._tasks += [(
-    #         self._curr_time + time,
-    #         cback,
-    #         'event: %s' % evt)]
-    #     def txt(show_hide):
-    #         self.txt['text'] = evt
-    #         (self.txt.show if show_hide else self.txt.hide)()
-    #     self._tasks += [(
-    #         self._curr_time + time + .2,
-    #         lambda: txt(True),
-    #         'event: %s (show)' % evt)]
-    #     self._tasks += [(
-    #         self._curr_time + time + .8,
-    #         lambda: txt(False),
-    #         'event: %s (hide)' % evt)]
-    #     self._curr_time += time
-
-    # def _enforce_res(self, time, res):
-    #     cback = lambda: messenger.send('enforce_res', [res])
-    #     self._tasks += [(
-    #         self._curr_time + time,
-    #         cback,
-    #         'enforce res: %s' % res)]
-    #     self._curr_time += time
+    def __enforce_resolution(self, resolution):
+        info(f'enforce resolution {resolution} (callback)')
+        messenger.send('enforce_resolution', [resolution])
 
     def __verify(self, task):
 
     def __verify(self, task):
-        files = glob(self._path + '*')
-        for fname in self._fnames:
-            info('verifying %s' % fname)
-            assert exists(fname)
-
-    #def on_frame_unpausable(self, task):
-        #self._process_conn()
-        #for tsk in self._tasks:
-        #    #if self._prev_time <= tsk[0] < self.eng.event.unpaused_time:
-        #    if self._prev_time <= tsk[0] < globalClock.getFrameTime():
-        #        debug('%s %s' % (tsk[0], tsk[2]))
-        #        tsk[1]()
-        #self._prev_time = globalClock.getFrameTime()  # self.eng.event.unpaused_time
-        #return task.cont
-
-    # def _do_screenshots_1(self):
-    #     info('_do_screenshots_1')
-    #     self._screenshot(FunctionalTest.start_time, 'main_menu')
-    #     self._do_screenshots_credits()
-    #     self._do_screenshots_options()
-    #     self._do_screenshots_exit()
-
-    # def _do_screenshots_credits(self):
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 450), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'credits_menu')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 680), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'main_menu_back_from_credits')
-    #     # # go to credits
-    #     # self._event(FunctionalTest.evt_time, 'joypad0-dpad_down', True)
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     # self._event(FunctionalTest.evt_time, 'joypad0-dpad_down', True)
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'main_menu_highlight')
-    #     # self._event(FunctionalTest.evt_time, 'rcontrol')
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'credits_menu')
-    #     # # go to supporters
-    #     # self._event(FunctionalTest.evt_time, 'joypad0-face_a', True)
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'supporters_menu')
-    #     # # back to main
-    #     # self._event(FunctionalTest.evt_time, 'rcontrol')
-    #     # self._event(FunctionalTest.evt_time, 'joypad0-face_b', True)
-    #     # self._event(FunctionalTest.evt_time, 'arrow_up')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_up')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_up')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_up')
-
-    # def _do_screenshots_options(self):
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 300), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu')
-    #     # languages
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 60), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'open_languages')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(980, 120), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu_italian')
-    #     # volume
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(740, 163), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu_drag_1')
-    #     # antialiasing
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 440), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'antialiasing_no')
-    #     # shadows
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 540), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'shadows_no')
-    #     # test aa and shadows
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 680), 'left'])  # back
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 140), 'left'])  # play
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(230, 160), 'left'])  # domino
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(900, 490), 'left'])  # close instructions
-    #     self._screenshot(FunctionalTest.screenshot_time, 'aa_no_shadows_no')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(25, 740), 'left'])  # home
-
-    # def _do_screenshots_restore_options(self):
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 300), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu_restored')
-    #     # languages
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 60), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'open_languages_restored')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(980, 20), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu_english')
-    #     # volume
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(719, 163), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu_drag_2')
-    #     # fullscreen
-    #     # the first one is because of the windowed mode in test
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 250), 'left'])
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'fullscreen')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 250), 'left'])
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'fullscreen')
-    #     # self._event(8 + FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 250), 'left'])
-    #     # self._screenshot(8 + FunctionalTest.screenshot_time, 'back_from_fullscreen')
-    #     # resolution
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 340), 'left'])
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'resolutions')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1020, 160), 'left'])
-    #     # self._screenshot(FunctionalTest.screenshot_time, '1440x900')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(740, 400), 'left'])
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'resolutions_2')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1110, 80), 'left'])
-    #     # self._screenshot(FunctionalTest.screenshot_time, '1360x768')
-    #     # antialiasing
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 440), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'antialiasing_yes')
-    #     # shadows
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 540), 'left'])
-    #     self._screenshot(FunctionalTest.screenshot_time, 'shadows_yes')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 680), 'left'])  # back
-
-    # #     # go to options
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu')
-    # #     # language
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'language_open')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'language_highlight')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'language_it')
-    # #     # volume
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_right')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_right')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'volume')
-    # #     # car's number
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'cars_open')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'cars_changed')
-    # #     # back
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up')
-
-    # def _do_screenshots_play(self):
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 140), 'left'])  # play
-    #     self._screenshot(FunctionalTest.screenshot_time, 'play_menu')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 680), 'left'])  # back
-    #     self._screenshot(FunctionalTest.screenshot_time, 'back_from_play')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 140), 'left'])  # play
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(230, 160), 'left'])  # domino scene
-    #     self._screenshot(FunctionalTest.screenshot_time, 'scene_domino_instructions')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(850, 490), 'left'])  # close instructions
-    #     self._screenshot(FunctionalTest.screenshot_time, 'scene_domino')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(25, 740), 'left'])  # home
-    #     self._screenshot(FunctionalTest.screenshot_time, 'home_back_from_scene')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 140), 'left'])  # play
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(230, 160), 'left'])  # domino
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(850, 490), 'left'])  # close instructions
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(70, 740), 'left'])  # info
-    #     self._screenshot(FunctionalTest.screenshot_time, 'info')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(850, 490), 'left'])  # close instructions
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(35, 60), (430, 280), 'left'])  # drag a piece
-    #     self._screenshot(FunctionalTest.screenshot_time, 'domino_dragged')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1220, 740), 'left'])  # rewind
-    #     self._screenshot(FunctionalTest.screenshot_time, 'rewind')
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(35, 60), (550, 380), 'left'])  # drag a piece
-    #     # self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(35, 60), (715, 380), 'left'])  # drag a piece
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(630, 450), 'left'])  # home
-    #     self._screenshot(FunctionalTest.screenshot_time, 'home_back_from_fail')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 140), 'left'])  # play
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(230, 160), 'left'])  # domino
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(850, 490), 'left'])  # close instructions
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(35, 60), (550, 380), 'left'])  # drag a piece
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(35, 60), (715, 380), 'left'])  # drag a piece
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino_2')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 450), 'left'])  # replay
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(35, 60), (570, 380), 'left'])  # drag a piece
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(570, 355), (605, 355), 'right'])  # rotate the piece
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(35, 60), (715, 380), 'left'])  # drag a piece
-    #     self._enforce_res(FunctionalTest.evt_time, 'win')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'win_domino')
-    #     self._enforce_res(FunctionalTest.evt_time, '')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(735, 450), 'left'])  # next
-    #     self._screenshot(FunctionalTest.screenshot_time, 'scene_box')
-    #     # scene 2
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(880, 490), 'left'])  # close instructions
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (710, 620), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (710, 540), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_box')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 450), 'left'])  # replay
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (710, 620), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (710, 540), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (705, 460), 'left'])  # drag a box
-    #     self._enforce_res(FunctionalTest.evt_time, 'win')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'win_box')
-    #     self._enforce_res(FunctionalTest.evt_time, '')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(735, 450), 'left'])  # next
-    #     self._screenshot(FunctionalTest.screenshot_time, 'scene_box_domino')
-    #     # scene 3
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(930, 485), 'left'])  # close instructions
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (910, 440), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (910, 360), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_box_domino')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 450), 'left'])  # replay
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (910, 440), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (835, 250), 'left'])  # drag a box
-    #     self._enforce_res(FunctionalTest.evt_time, 'win')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'win_box_domino')
-    #     self._enforce_res(FunctionalTest.evt_time, '')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(735, 450), 'left'])  # next
-    #     self._screenshot(FunctionalTest.screenshot_time, 'scene_basketball')
-    #     # scene 4
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(870, 490), 'left'])  # close instructions
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(55, 50), (650, 310), 'left'])  # drag a ball
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_basketball')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 450), 'left'])  # replay
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(55, 50), (380, 50), 'left'])  # drag a ball
-    #     self._enforce_res(FunctionalTest.evt_time, 'win')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'win_basketball')
-    #     self._enforce_res(FunctionalTest.evt_time, '')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(735, 450), 'left'])  # next
-    #     self._screenshot(FunctionalTest.screenshot_time, 'scene_domino_box_basketball')
-    #     # scene 5
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(865, 490), 'left'])  # close instructions
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (580, 440), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(30, 60), (590, 370), 'left'])  # drag a piece
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_domino_box_basketball')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 450), 'left'])  # replay
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(65, 60), (580, 440), 'left'])  # drag a box
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(30, 60), (660, 440), 'left'])  # drag a piece
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(660, 425), (625, 425), 'right'])  # rotate a piece
-    #     self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(660, 435), (650, 445), 'left'])  # drag a piece
-    #     self._enforce_res(FunctionalTest.evt_time, 'win')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1340, 740), 'left'])  # play
-    #     self._screenshot(16 + FunctionalTest.screenshot_time, 'win_domino_box_basketball')
-    #     self._enforce_res(FunctionalTest.evt_time, '')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(735, 450), 'left'])  # next
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'scene_teeter_tooter')
-    #     # # scene 6
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(820, 455), 'left'])  # close instructions
-    #     # self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(60, 60), (490, 300), 'left'])  # drag a box
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1260, 695), 'left'])  # play
-    #     # self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_teeter_tooter')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(640, 420), 'left'])  # replay
-    #     # self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(60, 60), (490, 150), 'left'])  # drag a box
-    #     # self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(515, 115), (515, 122), 'right'])  # rotate a box
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1260, 695), 'left'])  # play
-    #     # self._screenshot(16 + FunctionalTest.screenshot_time, 'win_teeter_tooter')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(690, 420), 'left'])  # next
-    #     # self._screenshot(FunctionalTest.screenshot_time, 'scene_teeter_domino_box_basketball')
-    #     # # scene 7
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(880, 455), 'left'])  # close instructions
-    #     # self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(60, 60), (155, 180), 'left'])  # drag a box
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1260, 695), 'left'])  # play
-    #     # self._screenshot(16 + FunctionalTest.screenshot_time, 'fail_teeter_domino_box_basketball')
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(640, 420), 'left'])  # replay
-    #     # self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(60, 60), (170, 80), 'left'])  # drag a box
-    #     # self._event(FunctionalTest.evt_time, 'mousedrag', False, False, [(195, 50), (195, 80), 'right'])  # rotate a box
-    #     # self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(1260, 695), 'left'])  # play
-    #     # self._screenshot(16 + FunctionalTest.screenshot_time, 'win_teeter_domino_box_basketball')
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(630, 450), 'left'])  # home
-    #     self._screenshot(FunctionalTest.screenshot_time, 'home_from_play')
-
-    # def _do_screenshots_exit(self):
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     # self._event(FunctionalTest.evt_time, 'rcontrol')
-    #     # self._event(FunctionalTest.evt_time, 'arrow_down')
-    #     self._verify()
-    #     # self._event(FunctionalTest.evt_time, 'rcontrol')
-    #     # self._exit()
-    #     self._event(FunctionalTest.evt_time, 'mouseclick', False, False, [(680, 600), 'left'])
-
-
-    # def _do_screenshots_2(self):
-    #     info('_do_screenshots_2')
-    #     self._screenshot(FunctionalTest.start_time, 'main_menu_2')
-    #     self._do_screenshots_restore_options()
-    #     self._do_screenshots_play()
-    #     self._do_screenshots_exit()
-    # #     self._do_screenshots_game()
-    # #     self._do_screenshots_end()
-
-    # # def _do_screenshots_restore_options(self):
-    # #     # go to options
-    # #     self._event(FunctionalTest.evt_time, 'joypad0-dpad_down', True)
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'options_menu_restored')
-    # #     # # language
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'language_en_restored')
-    # #     # # volume
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_left')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_left')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'volume_restored')
-    # #     # car's number
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'cars_restored')
-    # #     # graphics settings
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'graphics_settings')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'antialiasing')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'shadows')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'fog')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'normal_mapping')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'occlusion')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     # input
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'input')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_rec')
-    # #     self._event(FunctionalTest.evt_time, '8', True, False)
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_changed')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up', True, False)
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_restored')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'w', True, False)
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_already')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_already_closed')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p2')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p3')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p4')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up')
-
-    # # def _do_screenshots_game(self):
-    # #     # single player
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'single_player_menu')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'track_page')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'car_page_start')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_left')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'car_page_sel')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'driver_page_start')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_left')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_left')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_up')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'driver_page_entry')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._event(FunctionalTest.evt_time, 'backspace')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'driver_page_entry_empty')
-    # #     self._event(FunctionalTest.evt_time, 'f')
-    # #     self._event(FunctionalTest.evt_time, 'l')
-    # #     self._event(FunctionalTest.evt_time, 'a')
-    # #     self._event(FunctionalTest.evt_time, 'v')
-    # #     self._event(FunctionalTest.evt_time, 'i')
-    # #     self._event(FunctionalTest.evt_time, 'o')
-    # #     self._event(FunctionalTest.evt_time, 'enter')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'driver_page_entry_full')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_right')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'driver_page_sel')
-    # #     # some ai tests
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._event(40, 'escape-up')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'ingame_menu')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'race_back')
-    # #     self._event(FunctionalTest.evt_time, 'escape-up')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'ingame_sel')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'main_page_back_race')
-
-    # # def _do_screenshots_end(self):
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'exit_page')
-    # #     self._event(FunctionalTest.evt_time, 'arrow_down')
-    # #     self._screenshot(FunctionalTest.screenshot_time, 'exit_page_sel')
-    # #     self._verify()
-    # #     self._event(FunctionalTest.evt_time, 'rcontrol')
-    # #     self._exit()
-
-    # def _do_screenshots(self, idx):
-    #     [self._do_screenshots_1, self._do_screenshots_2][int(idx) - 1]()
+        for f in self.__file_names:
+            info(f'verifying {f}')
+            assert exists(f)
diff --git a/ya2/utils/gfx.py b/ya2/utils/gfx.py
new file mode 100755 (executable)
index 0000000..a25365a
--- /dev/null
@@ -0,0 +1,125 @@
+from panda3d.core import NodePath, Point2, Point3, Texture, LVector3f, LVector2f, TextNode
+from direct.gui.OnscreenText import OnscreenText
+from direct.gui.DirectGuiGlobals import ENTER, EXIT
+from ya2.utils.decorator import Decorator
+
+
+class GfxTools:
+
+    @staticmethod
+    def build_model(file_path):
+        m = loader.load_model(file_path)
+        m = NodePathDecorator(m)  # we can't use mixins with c++ classes
+        return m
+
+    @staticmethod
+    def build_empty_node(name):
+        return NodePathDecorator(NodePath(name))
+
+    @staticmethod
+    def build_node_from_physics(physics_node):
+        return NodePathDecorator(render.attach_new_node(physics_node))
+
+
+class NodePathDecorator(NodePath, Decorator):
+
+    def set_srgb_textures(self):
+        for t in self.find_all_textures():
+            f = t.get_format()
+            match t.get_format():
+                case Texture.F_rgba | Texture.F_rgbm:
+                    f = Texture.F_srgb_alpha
+                case Texture.F_rgb:
+                    f = Texture.F_srgb
+            t.set_format(f)
+
+    def pos2d(self):
+        p3d = base.cam.get_relative_point(self, Point3(0, 0, 0))
+        p2d = Point2()
+        return p2d if base.camLens.project(p3d, p2d) else None
+
+    def pos2d_pixel(self):
+        if p2d := self.pos2d():
+            p = _get_pos_relative((p2d[0], 0, p2d[1]), pixel2d)
+            return int(round(p[0])), int(round(-p[2]))
+
+    def pos_as_widget(self):
+        pos = self.get_pos(pixel2d)
+        return int(round(pos[0])), int(round(-pos[2]))
+
+
+class DirectGuiMixin():
+
+    registered_tooltips = []
+
+    def pos_pixel(self):
+        pos = self.get_pos(pixel2d)
+        return int(round(pos[0])), int(round(-pos[2]))
+
+    def set_tooltip(self, text, font, scale, fg, side='left'):
+        self.__tooltip_txt = text
+        self.__tooltip_font = font
+        self.__tooltip_scale = scale
+        self.__tooltip_fg = fg
+        self.bind(ENTER, self.__on_enter)
+        self.bind(EXIT, self.__on_exit)
+        match side:
+            case 'left':
+                self.__side_delta = (-.08, -.02)
+                self.__align = TextNode.A_right
+            case 'right':
+                self.__side_delta = (.08, -.02)
+                self.__align = TextNode.A_left
+
+    def __on_enter(self, mouse_pos):
+        self.__tooltip_text = OnscreenText(
+            self.__tooltip_txt,
+            pos=(self.get_pos()[0] + self.__side_delta[0], self.get_pos()[2] + self.__side_delta[1]),
+            parent=self.parent,
+            font=self.__tooltip_font,
+            scale=self.__tooltip_scale,
+            fg=self.__tooltip_fg,
+            align=self.__align)
+        DirectGuiMixin.registered_tooltips += [self.__tooltip_text]
+
+    def __on_exit(self, mouse_pos):
+        self.__tooltip_text.destroy()
+
+    @staticmethod
+    def clear_tooltips():
+        # when we'll have proper gui classes with has-a containment
+        # remove this hack: the destroy will destroy the tooltip as well
+        for t in DirectGuiMixin.registered_tooltips:
+            t.destroy()
+        DirectGuiMixin.registered_tooltips = []
+
+
+def pos_pixel(widget):
+    pos = widget.get_pos(pixel2d)
+    return int(round(pos[0])), int(round(-pos[2]))
+
+
+class Point(Decorator):
+
+    def screen_coord(self):
+        pos3d = _get_pos_relative(LVector3f(*self), base.cam)
+        pos2d = Point2()
+        base.camLens.project(pos3d, pos2d)
+        pos_wrt_render2d = Point3(pos2d[0], 0, pos2d[1])
+        pos_wrt_aspect2d = base.aspect2d.get_relative_point(render2d, pos_wrt_render2d)
+        return Point((pos_wrt_aspect2d[0], pos_wrt_aspect2d[2]))
+
+    def from_to_points(self):
+        p_from, p_to = Point3(), Point3()    # in camera coordinates
+        base.camLens.extrude(LVector2f(*self), p_from, p_to)
+        p_from = render.get_relative_point(base.cam, p_from)  # global coords
+        p_to = render.get_relative_point(base.cam, p_to)  # global coords
+        return p_from, p_to
+
+
+def _get_pos_relative(pos, other):
+    n = NodePath('temporary_node')
+    n.set_pos(pos)
+    pos = n.get_pos(other)
+    n.remove_node()
+    return pos
diff --git a/ya2/utils/gui/__init__.py b/ya2/utils/gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/ya2/utils/gui/base_page.py b/ya2/utils/gui/base_page.py
new file mode 100644 (file)
index 0000000..359ed76
--- /dev/null
@@ -0,0 +1,243 @@
+from dataclasses import dataclass
+from panda3d.core import TextNode
+from direct.gui.OnscreenText import OnscreenText
+from direct.gui.DirectGui import DirectButton, DirectOptionMenu, DirectSlider, DirectCheckButton
+from direct.gui.DirectGuiGlobals import FLAT
+from ya2.utils.audio import AudioTools
+from ya2.utils.gfx import DirectGuiMixin, GfxTools
+from ya2.utils.gui.gui import GuiTools
+
+
+class DirectOptionMenuTestable(DirectOptionMenu):
+
+    def __init__(self, parent=None, **kw):
+        DirectOptionMenu.__init__(self, parent, **kw)
+        self.initialiseoptions(DirectOptionMenuTestable)
+
+    def showPopupMenu(self, event=None):
+        #super().showPopupMenu(event)  # it does not work with mixins
+        DirectOptionMenu.showPopupMenu(self, event)
+        self._show_cb([self.component(c) for c in self.components()])
+
+
+@dataclass
+class BaseArgs:
+    id: ... = ''
+    text: ... = None
+    pos: ... = None
+    scale: ... = None
+    wordwrap: ... = None
+    texture: ... = None
+    text_scale: ... = None
+    font: ... = None
+    color: ... = None
+
+    def to_p3d_args(self):
+        d = {}
+        d['text'] = self.text
+        d['pos'] = self.pos or (0, 1, 0)
+        d['scale'] = self.scale or .12
+        d['font'] = self.font or GuiTools.load_font('assets/fonts/Hanken-Book.ttf')
+        d['fg'] = self.color or (.9, .9, .9, 1)
+        return d
+
+
+@dataclass
+class ButtonArgs(BaseArgs):
+    command: ... = None
+    command_args: ... = None
+    size: ... = None
+    text_color: ... = None
+    relief: ... = None
+    rollover_sound: ... = None
+    click_sound: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        d['frameSize'] = self.size or (-4.8, 4.8, -.6, 1.2)
+        d['command'] = self.command
+        d['extraArgs'] = self.command_args or []
+        d['text_fg'] = self.text_color or d.pop('fg')
+        if 'fg' in d: del d['fg']
+        d['text_font'] = self.font or d.pop('font')
+        d['frameColor'] = self.color or (.4, .4, .4, .14)
+        d['relief'] = self.relief or FLAT
+        d['rolloverSound'] = self.rollover_sound or AudioTools.load_sfx('assets/audio/sfx/rollover.ogg')
+        d['clickSound'] = self.click_sound or AudioTools.load_sfx('assets/audio/sfx/click.ogg')
+        d['pos'] = (self.pos[0], 1, self.pos[1]) if len(self.pos) == 2 else self.pos
+        d['text_wordwrap'] = self.wordwrap
+        d['frameTexture'] = self.texture
+        d['text_scale'] = self.text_scale or 1
+        return d
+
+    def __build_gui_args(self):
+        base = {
+            'scale': .12,
+            'text_font': GuiTools.load_font('assets/fonts/Hanken-Book.ttf'),
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': AudioTools.load_sfx('assets/audio/sfx/rollover.ogg'),
+            'clickSound': AudioTools.load_sfx('assets/audio/sfx/click.ogg')}
+        button = {'size': (-4.8, 4.8, -.6, 1.2)} | base
+        return button
+
+
+@dataclass
+class SliderArgs(ButtonArgs):
+    range: ... = None
+    scale: ... = None
+    value: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        d['range'] = self.range or (0, 1)
+        d['thumb_frameColor'] = (.4, .4, .4, .4)
+        d['thumb_scale'] = 1.6
+        d['thumb_relief'] = d['relief']
+        d['scale'] = self.scale or .4
+        d['value'] = self.value or 0
+        del d['rolloverSound']
+        del d['clickSound']
+        del d['frameSize']
+        return d
+
+
+@dataclass
+class OptionMenuArgs(ButtonArgs):
+    items: ... = None
+    initial_item: ... = None
+    item_color: ... = None
+    popup_color: ... = None
+    item_relief: ... = None
+    item_text_font: ... = None
+    item_text_fg: ... = None
+    highlight_color: ... = None
+    text_align: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        d['items'] = self.items
+        d['initialitem'] = self.initial_item
+        d['item_frameColor'] = d['frameColor']
+        d['popupMarker_frameColor'] = d['frameColor']
+        d['popupMarker_relief'] = d['relief']
+        d['item_relief'] = d['relief']
+        d['item_text_font'] = d['text_font']
+        d['item_text_fg'] = d['text_fg']
+        d['textMayChange'] = 1
+        h = d['frameColor']
+        h = (h[0] + .2, h[1] + .2, h[2] + .2, h[3] + .2)
+        d['highlightColor'] = self.highlight_color or h
+        d['text_align'] = TextNode.A_center
+        d['frameSize'] = (f:=d['frameSize'])[0], f[1] - .56, f[2], f[3]
+        return d
+
+
+@dataclass
+class CheckButtonArgs(ButtonArgs):
+    value: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        h = d['frameColor']
+        h = (h[0] + .2, h[1] + .2, h[2] + .2, h[3] + .2)
+        d['indicator_frameColor'] = h
+        d['indicator_relief'] = d['relief']
+        d['indicatorValue'] = self.value or 0
+        return d
+
+
+@dataclass
+class TextArgs(BaseArgs):
+    align: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        if self.align:
+            a = {'left': TextNode.A_left,
+                 'right': TextNode.A_right,
+                 'center': TextNode.A_center}[self.align]
+            d['align'] = a
+        return d
+
+
+class BasePage:
+
+    def __init__(self, page_info):
+        self.__widgets = []
+        self._page_info = page_info
+        for k in list(self._page_info.test_positions.keys()):
+            del self._page_info.test_positions[k]
+        self._build_widgets()
+
+    def _add_text(self, text_info):
+        t = OnscreenText(
+            **text_info.to_p3d_args())
+        self.__widgets += [t]
+        return t
+
+    def _add_button(self, button_args, vertically_centered=False):
+        b = DirectButton(**button_args.to_p3d_args())
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._page_info.test_positions[button_args.id] = b.pos_pixel()
+        self.__widgets += [b]
+        if vertically_centered:
+            for j in range(4):
+                tnode = b.component('text%s' % j).textNode
+                height = - tnode.getLineHeight() / 2
+                height += (tnode.get_height() - tnode.get_line_height()) / 2
+                b.component('text%s' % j).set_pos(0, 0, height)
+        return b
+
+    def _add_check_button(self, button_args):
+        b = DirectCheckButton(**button_args.to_p3d_args())
+        b.__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
+        self.__widgets += [b]
+        self._page_info.test_positions[button_args.id] = b.pos_pixel()
+        return b
+
+    def _add_option_menu(self, option_args, cb):
+        o = DirectOptionMenuTestable(**option_args.to_p3d_args())
+        o.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
+        o._show_cb = cb
+        o.popupMenu['frameColor'] = option_args.to_p3d_args()['frameColor']
+        o.popupMenu['relief'] = option_args.to_p3d_args()['relief']
+        self.__widgets += [o]
+        self._page_info.test_positions[option_args.id] = o.pos_pixel()
+        return o
+
+    def _add_slider(self, slider_args):
+        s = DirectSlider(**slider_args.to_p3d_args())
+        s.__class__ = type('DirectSliderMixed', (DirectSlider, DirectGuiMixin), {})
+        self._page_info.test_positions['volume'] = s.pos_pixel()
+        np_left = GfxTools.build_empty_node('left_slider')
+        np_left.set_pos(s.get_net_transform().get_pos())
+        np_left.set_x(np_left.get_x() - s.get_scale()[0])
+        lpos = np_left.pos_as_widget()  # try with pos2d_pixel and remove pos_as_widget if ok
+        self.__widgets += [s]
+        self._page_info.test_positions['volume_0'] = lpos
+        return s
+
+    def _set_page(self, page_name):
+        self._page_info.set_page(page_name)
+        self.destroy()
+
+    def on_back(self):
+        self._set_page('main')
+
+    def _rearrange_buttons_width(self):
+        max_width = 0
+        for w in self.__widgets:
+            t = w.component('text0')
+            ul = t.textNode.get_upper_left_3d()
+            lr = t.textNode.get_lower_right_3d()
+            max_width = max(lr[0] - ul[0], max_width)
+        for w in self.__widgets:
+            m_w = max_width / 2 + .8
+            w['frameSize'] = -m_w, m_w, w['frameSize'][2], w['frameSize'][3]
+
+    def destroy(self):
+        for w in self.__widgets: w.destroy()
+        self._page_info = None
+        self.__widgets = []
diff --git a/ya2/utils/gui/cursor.py b/ya2/utils/gui/cursor.py
new file mode 100644 (file)
index 0000000..7f0b031
--- /dev/null
@@ -0,0 +1,65 @@
+from dataclasses import dataclass
+from panda3d.core import WindowProperties, Texture
+from direct.gui.OnscreenImage import OnscreenImage
+from ya2.utils.logics import LogicsTools
+
+
+@dataclass
+class MouseCursorArgs:
+    image_path: ...
+    functional_test: ...
+    scale: ... = 1
+    color: ... = (1, 1, 1, 1)
+    hotspot_position: ... = (0, 0)
+
+
+class MouseCursor:
+
+    def __init__(self, info):
+        self.__functional_test = info.functional_test
+        if not info.image_path: return
+        self.__hide_system_cursor()
+        self.__set_image(info.image_path, info.scale, info.color)
+        self.__set_hotspot(info.hotspot_position, info.scale)
+        self._tsk = taskMgr.add(self.__on_frame, 'on_frame_cursor')
+
+    def __hide_system_cursor(self):
+        p = WindowProperties()
+        p.set_cursor_hidden(True)
+        if LogicsTools.windowed: base.win.request_properties(p)
+
+    def __set_image(self, image_path, scale, color):
+        self.__image = OnscreenImage(image_path, scale=scale)
+        self.__image.set_color(color)
+        self.__image.set_bin('gui-popup', 50)
+        alpha_formats = [Texture.FRgba]
+        if self.__image.get_texture().get_format() in alpha_formats:
+            self.__image.set_transparency(True)
+
+    def __set_hotspot(self, hotspot_position, scale):
+        self.__hotspot_dx = scale[0] * (1 - 2 * hotspot_position[0])
+        self.__hotspot_dy = scale[2] * (1 - 2 * hotspot_position[1])
+
+    def __on_frame(self, task):
+        m = base.mouseWatcherNode
+        if not m or not m.hasMouse(): return task.again
+        mouse = m.get_mouse_x(), m.get_mouse_y()
+        h_x = mouse[0] * base.get_aspect_ratio() + self.__hotspot_dx
+        self.__image.set_pos((h_x, 1, mouse[1] - self.__hotspot_dy))
+        return task.again
+
+    def set_image(self, image):
+        self.__image.set_texture(loader.load_texture(image), 1)
+
+    def set_color(self, color):
+        self.__image.set_color(color)
+
+    def show(self):
+        if not self.__functional_test: return self.__image.show()
+
+    def hide(self):
+        return self.__image.hide()
+
+    def destroy(self):
+        taskMgr.remove(self._tsk)
+        self.__image.destroy()
diff --git a/ya2/utils/gui/gui.py b/ya2/utils/gui/gui.py
new file mode 100644 (file)
index 0000000..ff58709
--- /dev/null
@@ -0,0 +1,29 @@
+from panda3d.core import load_prc_file_data, Texture, TextProperties, TextPropertiesManager
+from ya2.utils.gfx import Point
+
+class GuiTools:
+
+    @staticmethod
+    def init():
+        GuiTools.__create_text_properties_scale()
+
+    def __create_text_properties_scale():
+        (t := TextProperties()).set_text_scale(.64)
+        TextPropertiesManager.get_global_ptr().set_properties('scale', t)
+
+    @staticmethod
+    def no_window():
+        load_prc_file_data('', 'window-type none')
+
+    @staticmethod
+    def get_mouse():
+        return Point(base.mouseWatcherNode.get_mouse())
+
+    @staticmethod
+    def load_font(path):
+        font = base.loader.load_font(path)
+        font.clear()
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        font.set_outline((0, 0, 0, 1), .8, .2)
+        return font
diff --git a/ya2/utils/gui/menu.py b/ya2/utils/gui/menu.py
new file mode 100644 (file)
index 0000000..af2e5b2
--- /dev/null
@@ -0,0 +1,27 @@
+from dataclasses import dataclass
+from ya2.utils.gui.cursor import MouseCursor
+
+
+@dataclass
+class PageArgs:
+    set_page: ...
+    test_positions: ...
+    running_functional_tests: ...
+
+
+class BaseMenu:
+
+    def __init__(self, cursor_info, running_functional_tests, test_positions):
+        self._page_info = PageArgs(self.set_page, test_positions, running_functional_tests)
+        self._page = None
+        self._cursor = MouseCursor(cursor_info)
+        self.set_page('main')
+
+    def set_page(self, page_name):
+        if self._page: self._page.destroy()
+        p = self._set_page(page_name)
+        self._page = p
+
+    def destroy(self):
+        if self._page: self._page = self._page.destroy()
+        self._cursor.destroy()
diff --git a/ya2/utils/lang.py b/ya2/utils/lang.py
deleted file mode 100644 (file)
index bbfa49b..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-from logging import info
-from os.path import join, exists, dirname
-from gettext import translation
-from pathlib import Path
-from ya2.patterns.gameobject import GameObject
-import sys
-
-
-def is_runtime(): return not exists('main.py')
-
-
-def is_appimage():
-    par_path = str(Path(__file__).parent.absolute())
-    is_appimage = par_path.startswith('/tmp/.mount_Pmachines')
-    return is_appimage and par_path.endswith('/usr/bin')
-
-
-def curr_path():
-    if not is_runtime(): return ''
-    if sys.platform == 'darwin':
-        return dirname(__file__) + '/../Resources/'
-    # return dirname(__file__)
-    par_path = str(Path(__file__).parent.absolute())
-    if is_appimage():
-        return str(Path(par_path).absolute())
-    is_snap = par_path.startswith('/snap/')
-    is_snap = is_snap and par_path.endswith('/x1')
-    if is_snap:
-        return str(Path(par_path).absolute())
-    #return getcwd()
-    curr_path = dirname(__file__)
-    info('current path: %s' % curr_path)
-    return curr_path + '/'
-
-
-class LangMgr(GameObject):
-
-    def __init__(self, lang, domain, dpath):
-        GameObject.__init__(self)
-        self.lang = lang
-        self.domain = domain
-        self.dpath = join(curr_path(), dpath)
-        info('language: %s, %s' % (self.domain, self.dpath))
-        self.set_lang(lang)
-
-    @property
-    def lang_codes(self):
-        return [lang[1] for lang in self.eng.cfg.lang_cfg.languages]
-
-    def set_lang(self, lang):
-        self.lang = lang
-        args = lang, self.domain, self.dpath
-        info('setting language %s, %s, %s' % args)
-        tra = translation(self.domain, self.dpath, [lang], fallback=True)
-        tra.install()
diff --git a/ya2/utils/language.py b/ya2/utils/language.py
new file mode 100644 (file)
index 0000000..3e5794d
--- /dev/null
@@ -0,0 +1,20 @@
+from logging import info
+from os.path import join
+from gettext import translation
+from ya2.utils.logics import LogicsTools
+
+
+class LanguageManager:
+
+    def __init__(self, language_code, app_name, languages_path):
+        self.__language_code = language_code
+        self.__app_name = app_name
+        self.__languages_path = join(LogicsTools.current_path(app_name), languages_path)
+        info(f'language: {self.__app_name}, {self.__languages_path}')
+        self.set_language(language_code)
+
+    def set_language(self, language_code):
+        self.__language_code = language_code
+        info(f'setting language {language_code}, {self.__app_name}, {self.__languages_path}')
+        t = translation(self.__app_name, self.__languages_path, [language_code], fallback=True)
+        t.install()
index 68fa6287f5860620eeccd637088d9dc0cc208d6a..2761867bcc08a57c8102cce61e7e3303ef0b39d8 100755 (executable)
 from logging import basicConfig, info, INFO, DEBUG, getLogger
 from configparser import ConfigParser
 from sys import platform, argv
 from logging import basicConfig, info, INFO, DEBUG, getLogger
 from configparser import ConfigParser
 from sys import platform, argv
-from platform import system
-from pathlib import Path
 from glob import glob
 from glob import glob
-from json import load, dumps
-#from datetime import datetime
-from pprint import pprint
 from os import getcwd, environ
 from os import getcwd, environ
-from os.path import exists, dirname
-from traceback import print_stack
+from os.path import exists
 from sys import version_info
 # from platform import system, release, architecture, platform, processor, \
 #     version, machine
 # from multiprocessing import cpu_count
 from sys import version_info
 # from platform import system, release, architecture, platform, processor, \
 #     version, machine
 # from multiprocessing import cpu_count
-from panda3d.core import Filename, GraphicsWindow, PandaSystem
+from panda3d.core import Filename, PandaSystem
 from panda3d.bullet import get_bullet_version
 from panda3d.bullet import get_bullet_version
-from ya2.patterns.gameobject import Colleague
-from ya2.p3d.p3d import LibP3d
+from ya2.utils.logics import LogicsTools, class_property
 import sys
 
 
 import sys
 
 
-lev = INFO
-opt_path = ''
-if platform in ['win32', 'linux'] and not exists('main.py'):
-    # it is the deployed version for windows
-    opt_path = str(Filename.get_user_appdata_directory()) + '/pmachines'
-opath = LibP3d.fixpath(opt_path + '/options.ini') if opt_path else \
-        'options.ini'
-if exists(opath):
-    with open(opath) as json_file:
-        #optfile = load(json_file)
-        optfile = ConfigParser()
-        optfile.read(opath)
-        # optfile['development']['verbose'] and int(optfile['development']['verbose']) or \
-        if optfile['development']['verbose_log'] and int(optfile['development']['verbose_log']):
-            lev = DEBUG
-
-basicConfig(level=lev, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')
-getLogger().setLevel(lev)  # it doesn't work otherwise
-
-
-class LogMgrBase(Colleague):  # headless log manager
+class LogManager:
 
     @staticmethod
 
     @staticmethod
-    def init_cls():
-        return LogMgr if base.win else LogMgrBase
+    def before_init_setup(app_name):
+        LogManager.__app_name = app_name
+        o = LogManager.option_file_path
+        LogManager.__set_level(o)
+
+    @class_property
+    def option_file_path(cls):
+        r = 'options.ini'
+        path = ''
+        if LogicsTools.in_build:
+            path = str(Filename.get_user_appdata_directory()) + '/pmachines'
+        if path:
+            r = LogicsTools.platform_specific_path(path + '/options.ini')
+        return r
 
 
-    def __init__(self, mediator):
-        Colleague.__init__(self, mediator)
-        self.log_cfg()
+    @staticmethod
+    def __set_level(options_file_path):
+        l = LogManager.__set_level_from_settings(options_file_path)
+        basicConfig(level=l, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')
+        getLogger().setLevel(l)
 
 
-    def log(self, msg, verbose=False):
+    @staticmethod
+    def __set_level_from_settings(options_file_path):
+        l = INFO
+        if exists(options_file_path):
+            with open(options_file_path):
+                o = ConfigParser()
+                o.read(options_file_path)
+                v = o['development']['verbose_log']
+                if v and int(v):
+                    l = DEBUG
+        return l
+
+    @class_property
+    def init_cls(cls):
+        return WindowedLogManager if base.win else LogManager
+
+    def __init__(self):
+        self.log_configuration()
+
+    def log(self, message, verbose=False):
         if verbose and not self.eng.cfg.dev_cfg.verbose_log: return
         if verbose and not self.eng.cfg.dev_cfg.verbose_log: return
-        info(msg)
+        info(message)
 
     @property
 
     @property
-    def is_appimage(self):
-        par_path = str(Path(__file__).parent.absolute())
-        is_appimage = par_path.startswith('/tmp/.mount_Pmachi')
-        return is_appimage and par_path.endswith('/usr/bin')
-
-    # @property
-    # def curr_path(self):
-    #     # this is different from the music's one since it does not work
-    #     # with the version in windows
-    #     if sys.platform == 'darwin':
-    #         return dirname(__file__) + '/../Resources/'
-    #     # return dirname(__file__)
-    #     par_path = str(Path(__file__).parent.absolute())
-    #     if self.is_appimage:
-    #         return str(Path(par_path).absolute())
-    #     is_snap = par_path.startswith('/snap/')
-    #     is_snap = is_snap and par_path.endswith('/x1')
-    #     if is_snap:
-    #         return str(Path(par_path).absolute())
-    #     #return getcwd()
-    #     #curr_path = dirname(__file__)
-    #     curr_path = str(Path(__file__).parent.parent.parent.absolute())
-    #     info('current path: %s' % curr_path)
-    #     return curr_path
+    def build_version(self):
+        appimage_version = self.__build_version_appimage()
+        if appimage_version: return appimage_version
+        return self.__build_version_from_file
 
 
-    @property
-    def curr_path(self):
-        if system() == 'Windows':
-            return ''
-        if exists('main.py'):
-            return ''
-        else:
-            par_path = str(Path(__file__).parent.absolute())
-        if self.is_appimage:
-            par_path = str(Path(par_path).absolute())
-        par_path += '/'
-        return par_path
+    def __build_version_appimage(self):
+        appimage_mounted = glob(f'{LogicsTools.appimage_path(self.__app_name)}*')
+        if appimage_mounted:
+            with open(LogicsTools.current_path(self.__app_name) + 'assets/build_version.txt') as f:
+                return f.read().strip()
 
     @property
 
     @property
-    def build_version(self):
-        appimg_mnt = glob('/tmp/.mount_Pmachi*')
-        if appimg_mnt:
-            #with open(appimg_mnt[0] + '/usr/bin/appimage_version.txt') as fver:
-            with open(self.curr_path + 'assets/bld_version.txt') as fver:
-                return fver.read().strip()
-        try:
-            with open(self.curr_path + 'assets/bld_version.txt') as fver:
-                return fver.read().strip()
+    def __build_version_from_file(self):
+        try: return self.__load_version_from_file()
         except FileNotFoundError:
         except FileNotFoundError:
-            info('not found ' + self.curr_path + 'assets/bld_version.txt')
+            info('not found ' + LogicsTools.current_path(self.__app_name) + 'assets/build_version.txt')
             return 'notfound'
 
             return 'notfound'
 
-    def log_cfg(self):
+    def __load_version_from_file(self):
+        with open(LogicsTools.current_path(self.__app_name) + 'assets/build_version.txt') as f:
+            return f.read().strip()
+
+    def log_configuration(self):
         if '--version' in argv:
         if '--version' in argv:
-            path = str(Filename.get_user_appdata_directory())
-            home = '/home/flavio'  # we must force this for wine
-            if path.startswith('/c/users/') and exists(home + '/.wine/'):
-                path = home + '/.wine/drive_' + path[1:]
-            info('writing %s' % path + '/pmachines/obs_version.txt')
-            with open(path + '/pmachines/obs_version.txt', 'w') as f:
-                #f.write(self.eng.logic.version)
-                f.write(self.build_version)
-            if not platform.startswith('win'):
-                from os import ttyname  # here because it doesn't work on windows
-                import sys
-                try:
-                    with open(ttyname(0), 'w') as fout:
-                        sys.stdout = fout
-                        print('version: ' + self.build_version)  # self.eng.logic.version)
-                except OSError:  # it doesn't work with crontab
-                    print('version: ' + self.build_version)
-        messages = ['version: ' + self.build_version]  # self.eng.logic.version]
-        messages += ['argv[0]: %s' % argv[0]]
-        messages += ['getcwd: %s' % getcwd()]
-        messages += ['__file__: %s' % __file__]
-        for elm in environ.items():
-            messages += ['env::%s: %s' % elm]
+            self.__write_observed_version()
+            self.__print_version()
+        self.__messages = [f'version: {self.build_version}',
+                    f'argv[0]: {argv[0]}',
+                    f'getcwd: {getcwd()}',
+                    f'__file__: {__file__}']
+        for e in environ.items():
+            self.__messages += [f'env::{e[0]}: {e[1]}']
         # os_info = (system(), release(), version())
         # messages += ['operative system: %s %s %s' % os_info]
         # messages += ['architecture: ' + str(architecture())]
         # os_info = (system(), release(), version())
         # messages += ['operative system: %s %s %s' % os_info]
         # messages += ['architecture: ' + str(architecture())]
@@ -144,58 +105,72 @@ class LogMgrBase(Colleague):  # headless log manager
         # except NotImplementedError:  # on Windows
         #     messages += ['cores: not implemented']
         lib_ver = PandaSystem.get_version_string()
         # except NotImplementedError:  # on Windows
         #     messages += ['cores: not implemented']
         lib_ver = PandaSystem.get_version_string()
-        try:
-            import psutil
-            mem = psutil.virtual_memory().total / 1000000000.0
-            messages += ['memory: %s GB' % round(mem, 2)]
-        except ImportError: info("can't import psutil")  # windows
+        self.__log_system_memory()
         lib_commit = PandaSystem.get_git_commit()
         lib_commit = PandaSystem.get_git_commit()
-        py_ver = [str(elm) for elm in version_info[:3]]
-        messages += ['python version: %s' % '.'.join(py_ver)]
-        messages += ['panda version: %s %s' % (lib_ver, lib_commit)]
-        messages += ['bullet version: ' + str(get_bullet_version())]
-        messages += ['appdata: ' + str(Filename.get_user_appdata_directory())]
-        if base.win and isinstance(base.win, GraphicsWindow):  # not headless
+        py_ver = [str(e) for e in version_info[:3]]
+        self.__messages += [f'python version: {".".join(py_ver)}',
+                     f'panda version: {lib_ver} {lib_commit}',
+                     f'bullet version: {str(get_bullet_version())}',
+                     f'appdata: {str(Filename.get_user_appdata_directory())}']
+        if LogicsTools.windowed:
+            # not headless
             print(base.win.get_keyboard_map())
             print(base.win.get_keyboard_map())
-        list(map(self.log, messages))
-
-    @staticmethod
-    def log_tasks():
-        info('tasks: %s' % taskMgr.getAllTasks())
-        info('do-laters: %s' % taskMgr.getDoLaters())
-
-    @staticmethod
-    def plog(obj):
-        print('\n\n')
-        print_stack()
-        pprint(obj)
-        print('\n\n')
-
-
-class LogMgr(LogMgrBase):
-
-    def log_cfg(self):
-        LogMgrBase.log_cfg(self)
-        messages = [base.win.get_gsg().get_driver_vendor()]
-        messages += [base.win.get_gsg().get_driver_renderer()]
-        shad_maj = base.win.get_gsg().get_driver_shader_version_major()
-        shad_min = base.win.get_gsg().get_driver_shader_version_minor()
-        messages += ['shader: {maj}.{min}'.format(maj=shad_maj, min=shad_min)]
-        messages += [base.win.get_gsg().get_driver_version()]
+        list(map(self.log, self.__messages))
+
+    def __write_observed_version(self):
+        info(f'writing {LogicsTools.appdata_path}/{self.__app_name}/observed_version.txt')
+        with open(LogicsTools.appdata_path + '/pmachines/observed_version.txt', 'w') as f:
+            f.write(self.build_version)
+
+    def __print_version(self):
+        if not platform.startswith('win'):
+            from os import ttyname  # here because it doesn't work on windows
+            try:
+                with open(ttyname(0), 'w') as f:
+                    sys.stdout = f
+                    print(f'version: {self.build_version}')
+            except OSError:  # it doesn't work with crontab
+                print('version: ' + self.build_version)
+
+    def __log_system_memory(self):
+        try:
+            import psutil
+            mem = psutil.virtual_memory().total / 1000000000
+            self.__messages += [f'memory: {round(mem, 2)} GB']
+        except ImportError:
+            info("can't import psutil")  # windows
+
+
+class WindowedLogManager(LogManager):
+
+    def log_configuration(self):
+        super().log_configuration()
+        self.__messages = [base.win.get_gsg().get_driver_vendor()]
+        self.__messages += [base.win.get_gsg().get_driver_renderer()]
+        shad_maj = base.win.get_gsg().\
+            get_driver_shader_version_major()
+        shad_min = base.win.get_gsg().\
+            get_driver_shader_version_minor()
+        self.__messages += ['shader: {maj}.{min}'.format(maj=shad_maj, min=shad_min)]
+        self.__messages += [base.win.get_gsg().get_driver_version()]
         drv_maj = base.win.get_gsg().get_driver_version_major()
         drv_min = base.win.get_gsg().get_driver_version_minor()
         drv = 'driver version: {maj}.{min}'
         drv_maj = base.win.get_gsg().get_driver_version_major()
         drv_min = base.win.get_gsg().get_driver_version_minor()
         drv = 'driver version: {maj}.{min}'
-        messages += [drv.format(maj=drv_maj, min=drv_min)]
+        self.__messages += [drv.format(maj=drv_maj, min=drv_min)]
+        self.__log_fullscreen()
+        self.__log_resolution()
+        list(map(self.log, self.__messages))
+
+    def __log_fullscreen(self):
         fullscreen = None
         fullscreen = None
-        if isinstance(base.win, GraphicsWindow):
+        if LogicsTools.windowed:
             fullscreen = base.win.get_properties().get_fullscreen()
             fullscreen = base.win.get_properties().get_fullscreen()
-        messages += ['fullscreen: ' + str(fullscreen)]
+        self.__messages += ['fullscreen: ' + str(fullscreen)]
+
+    def __log_resolution(self):
         def resolution():
         def resolution():
-            if not isinstance(base.win, GraphicsWindow):
+            if not LogicsTools.windowed:
                 return 800, 600
                 return 800, 600
-            win_prop = base.win.get_properties()
-            return win_prop.get_x_size(), win_prop.get_y_size()
-        res_x, res_y = resolution()
-        res_tmpl = 'resolution: {res_x}x{res_y}'
-        messages += [res_tmpl.format(res_x=res_x, res_y=res_y)]
-        list(map(self.log, messages))
+            w = base.win.get_properties()
+            return w.get_x_size(), w.get_y_size()
+        self.__messages += [f'resolution: {(r := resolution())[0]}x{r[1]}']
diff --git a/ya2/utils/logics.py b/ya2/utils/logics.py
new file mode 100755 (executable)
index 0000000..64d536c
--- /dev/null
@@ -0,0 +1,65 @@
+from sys import platform
+from os.path import exists
+from pathlib import Path
+from platform import system
+from panda3d.core import GraphicsWindow, Filename
+
+
+class class_property(property):
+    def __get__(self, cls, owner):
+        return classmethod(self.fget).__get__(None, owner)()
+
+
+class LogicsTools:
+
+    @class_property
+    def in_build(cls): return not exists('main.py')
+
+    @class_property
+    def windowed(cls): return base.win and isinstance(base.win, GraphicsWindow)
+
+    @staticmethod
+    def is_appimage(app_name):
+        parent_path = str(Path(__file__).parent.absolute())
+        mounted = parent_path.startswith(LogicsTools.appimage_path(app_name))
+        return mounted and parent_path.endswith('/usr/bin')
+
+    @staticmethod
+    def appimage_path(app_name):
+        folder_mounted_appimage = app_name[:6].capitalize()
+        return f'/tmp/.mount_{folder_mounted_appimage}'
+
+    @class_property
+    def appdata_path(cls):
+        appdata_path = str(Filename.get_user_appdata_directory())
+        #home = str(Path.home())  # '/home/flavio'  # we must force this for wine
+        home = '/home/flavio'  # we must force this for wine
+        if appdata_path.startswith('/c/users/') and exists(home + '/.wine/'):
+            appdata_path = home + '/.wine/drive_' + appdata_path[1:]
+        return appdata_path
+
+    @staticmethod
+    def current_path(app_name):
+        if not LogicsTools.in_build: return ''
+        if system() == 'Windows' or exists('main.py'): return ''
+        else: par_path = str(Path(__file__).parent.absolute())
+        if LogicsTools.is_appimage(app_name):
+            par_path = str(Path(par_path).absolute())
+        return par_path + '/'
+
+    @staticmethod
+    def platform_specific_path(path):
+        if LogicsTools.__is_windows_not_wine():
+            path = LogicsTools.__linux2windows_path(path)
+        return path
+
+    @staticmethod
+    def __is_windows_not_wine():
+        home = str(Path.home())
+        return platform.startswith('win') and not exists(home + '/.wine/')
+
+    @staticmethod
+    def __linux2windows_path(path):
+        if path.startswith('/'):
+            path = path[1] + ':\\' + path[3:]
+        return path.replace('/', '\\')