diff --git a/.all-contributorsrc b/.all-contributorsrc index e840bc6..a68ae0a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -2,7 +2,7 @@ "files": [ "README.md" ], - "imageSize": 100, + "imageSize": 96, "commit": false, "contributors": [ { @@ -52,7 +52,7 @@ ] } ], - "contributorsPerLine": 7, + "contributorsPerLine": 6, "projectName": "scratch2python", "projectOwner": "Secret-chest", "repoType": "github", diff --git a/.gitignore b/.gitignore index 58ffaad..5fdc31e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Project exclude paths /venv/ /.idea/ -/download/** \ No newline at end of file +/download/** +/assets/** \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 02dd564..f1f87fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,87 @@ -## Coding Style +## British English for docs, comments and names +Also correct the old ones written in American English. I recently switched (excuse me), and there's no going back. +This includes ending words in -ise and -yse, not -ize and -yze. +I have set my LanguageTool (I use PyCharm) to catch American English spellings, if you have it too, it may help. +The only exception is “colour”, American English is the +standard in programming so please use “color” in names. You should still use “colour” when not referring to a name. +If you don't know the differences, [this article on Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_American_and_British_English) may help you. + +## Code style guidelines +For the most part, we follow PEP 8. + +### Naming style Use `mixedCase` for most names. For class names, use `CamelCase`. -Always use DOUBLE QUOTES (like ")! -## Methods +### Quotes in code +Always use DOUBLE QUOTES (like `"this"`)! In some fonts single quotes look invisible. + +### Indentation +Only use 4 spaces. I did not make commit checks yet (I will), but you should be sorry if you use anything other than 4 +spaces in Python. + +#### Hanging indents +For most hanging indents the rule is to align them with the opening, like these: +```python +someList = [1, 2, 3, + 4, 5, 6,] + +if (1 == 1 + and 1 + 1 == 2 + and not "this".startswith("that")): + # At least I have no problem with the confusion described in PEP 8. + pass +``` + +### Comments +**Always** put a space after the pound sign (#)! Separate inline comments with two spaces from the rest of the line. + +### Break before operators! +> To solve this readability problem, mathematicians and their publishers follow the opposite convention. Donald Knuth explains the traditional rule in his Computers and Typesetting series: “Although formulas within a paragraph always break after binary operations and relations, displayed formulas always break before binary operations”. +> Following the tradition from mathematics usually results in more readable code +> **- PEP 8** +```python +# Do it like described in PEP 8. + +sum_ = (10000 + + 20000 + + 30000) +``` + +### Avoid backslashes. Use parentheses to be able to break lines when possible. + +### Blank lines +> Surround top-level function and class definitions with two blank lines. +> Method definitions inside a class are surrounded by a single blank line. +> **- PEP 8** + +Also group related code together and add some blank lines to separate it. Look at the source code already defined +to see examples of this. Do not add blank line _after_ comments! Don't worry, I will not reject your PR if you don't separate the code like I do. + +### Imports on separate lines +```python +# This is right: +import os +import sys + +# This is wrong: +import sys, os + +# This is OK though: +from subprocess import Popen, PIPE +``` + +### Wildcard imports +Wildcard imports look like this: +```python +from json import * +``` +Do not use them as they create confusion -Please look for methods instead of writing it again. If it may be done many times, create a method. +### Whitespace should make sense +Whitespace in Python literally follows the rules of whitespace in English for punctuation. +For operators, the slice colon should have no space on either side, but all others should have one space +on either side. The equal in arguments (`function(x=y)`) should have no space though. -## GNU/Linux +Avoid trailing whitespace as it's invisible and confusing. -Scratch2Python is developed on GNU/Linux. While Windows support is important, Linux is too. Don't do anything that dosen't work on GNU/Linux. diff --git a/MainMenu.glade b/MainMenu.glade~ similarity index 85% rename from MainMenu.glade rename to MainMenu.glade~ index 31543e3..ae851aa 100644 --- a/MainMenu.glade +++ b/MainMenu.glade~ @@ -334,6 +334,12 @@ 4 10 + + 3000 + 250 + 25 + 10 + 240 15 @@ -416,7 +422,7 @@ True True - bottom + left True True @@ -532,10 +538,35 @@ - + True False - Download cache + + + True + False + folder-download + 3 + + + False + True + 0 + + + + + True + False + Download cache + 1 + + + False + True + 1 + + False @@ -613,7 +644,7 @@ 0 maxFPS True - 30 + 30.000000000223519 1 @@ -929,10 +960,35 @@ - + True False - Project defaults + + + True + False + computer + 3 + + + False + True + 0 + + + + + True + False + Project defaults + 1 + + + False + True + 1 + + 1 @@ -966,10 +1022,35 @@ - + True False - Language + + + True + False + preferences-desktop-locale + 3 + + + False + True + 0 + + + + + True + False + Languages + 1 + + + False + True + 1 + + 2 @@ -1199,16 +1280,148 @@ on startup. - + True False - Debug + + + True + False + utilities-system-monitor + 3 + + + False + True + 0 + + + + + True + False + Debug + 1 + + + False + True + 1 + + 3 False + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Key delay + 0 + + + + + + False + True + 0 + + + + + True + False + Milliseconds before keys will start repeating, 0 means keys won't repeat. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + 4 + keyDelay + True + 4 + + + 1 + 0 + + + + + 4 + + + + + True + False + + + True + False + preferences-desktop-accessibility + 3 + + + False + True + 0 + + + + + True + False + Accesibility + 1 + + + False + True + 1 + + + + + 4 + False + + diff --git a/MainMenu.ui~ b/MainMenu.ui~ new file mode 100644 index 0000000..4934b8c --- /dev/null +++ b/MainMenu.ui~ @@ -0,0 +1,396 @@ + + + + + + True + False + dialog-information + + + 576 + 320 + False + 8 + Scratch2Python + False + scratch + + + True + False + vertical + + + True + False + vertical + + + True + False + Scratch2Python project loader + 0 + + + + + + + False + True + 0 + + + + + True + False + version {VERSION} + 0 + + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + center + center + 8 + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + application-x-executable + 6 + + + False + True + 0 + + + + + True + False + Load local project + + + + + + False + True + 1 + + + + + True + False + from your computer + + + + + + False + True + 2 + + + + + + + False + True + 0 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + application-x-partial-download + 6 + + + False + True + 0 + + + + + True + False + Cache project + + + + + + False + True + 1 + + + + + True + False + from scratch.mit.edu + + + + + + False + True + 2 + + + + + + + False + True + 1 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + user-bookmarks + 6 + + + False + True + 0 + + + + + True + False + Featured projects + + + + + + False + True + 1 + + + + + True + False + by us, TurboWarp, etc. + + + + + + False + True + 2 + + + + + + + False + True + 2 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + preferences-system + 6 + + + False + True + 0 + + + + + True + False + Settings + + + + + + False + True + 1 + + + + + + + + + + False + True + 3 + + + + + True + True + 1 + + + + + True + False + + + Need a GUI for browsing projects on scratch.mit.edu? + True + True + True + none + https://example.org + + + False + True + 0 + + + + + About + True + True + True + aboutIcon + True + + + + False + True + end + 1 + + + + + False + True + 2 + + + + + + diff --git a/README.md b/README.md index 9554bd8..d934216 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,62 @@ +# ![](icon.svg) Scratch2Python + + GitHub GitHub issues GitHub pull requests GitHub milestones GitHub forks GitHub all releases + [![All Contributors](https://img.shields.io/badge/all_contributors-4-ff9800.svg?style=flat-square)](#contributors-) -![Logo](s2p.svg) - GitHub GitHub issues GitHub pull requests GitHub milestones GitHub forks +Scratch2Python is a Scratch project interpreter that runs Scratch projects in Python, **using pygame to render your sprites**. +Go to #23 if you want to change the name. -# Scratch2Python -Scratch2Python is a Scratch project interpreter that runs Scratch projects in Python, using pygame to render your sprites. +Unlike others, Scratch2Python can actually _display_ your projects, not only process the variables, control flow, math and logic in the project, +and simply print speech bubble blocks. -A GUI for accessing the Scratch website is planned. It will most probably use... Qt. I would love using GTK, but **WINDOWS...** [Or maybe not](https://www.gtk.org/docs/installations/windows). +It's not finished though. It only supports a few blocks, but it supports them well. The goal is to support almost everything +(save for online services like text-to-speech) when we'll declare it as done. (Of course we'll still update things like the GUI.) -Scratch2Python may only be the temporary name, as this may get confused with [Scratch2Py](https://github.com/The-Cloud-Dev/scratch2py). See #23 for more information. -## 📝 Requirements -Install from requirements.txt (Scratch2Python also needs Python 3.8 or newer). +Scratch2Python is not *yet* a transpiler. -On Windows, Scratch2Python needs to be installed in a non-protected folder. -By default, the "Documents" folder is protected. Installing it anywhere else will work. +## 📝 Requirements and installation +[📥 Visit the website to easily download Scratch2Python](https://secret-chest.github.io/s2p/download/) -## 📘 Docs -[Read the wiki here](https://github.com/Secret-chest/scratch2python/wiki). Also read `CONTRIBUTING.md`. +Then install from requirements.txt using: -## 🔨 How to use -Assuming that you installed all necessary requirements, place your sb3 files somewhere accessible, or in the Scratch2Python folder. You can use an absolute or relative path. -Then, go to `config.py` and change the projectFileName variable to your project file. -There you can also choose to use a command-line argument, or an interactive prompt. The variable option is the default as it's more useful for testing. +`pip install -r requirements.txt` -Now, just run `python3 main.py` and the project will start! +Scratch2Python also needs Python 3.8 or newer. [Click here to download Python for Windows and Mac](https://www.python.org/downloads/). +On recent GNU/Linux distros (Ubuntu 20.04+, Debian 11+, Linux Mint 20+, Fedora 32+, updated rolling-release distros), +Python 3 is preinstalled or downloadable from the repositories. Check your distro's documentation for more info. + +
+Getting errors on Windows? + +On Windows, Scratch2Python needs to be installed in a non-protected folder. +By default, the “Documents” folder is protected. Installing it anywhere else will work. -### ✅ Config -The `config.py` file contains some more configuration options. +To unprotect the “Documents” folder, go to its Properties and uncheck the “Read-only” checkbox. -Each of them is nicely explained, so why not just check it out? +
-## 🌐 Localization -To translate Scratch2Python, add a new file in the `lang` directory. Copy the English file for reference, and replace the string. +## ▶️ Setting the project +Now that you have downloaded Scratch2Python, let's run a project. -Then add it on the supported languages list both here and in `config.py`. -Though I would not recommend translating it right now. It is still very WIP and you would have to update your language -file very frequently. +### Using test mode +For now Scratch2Python is in test mode by default. That means it will always run the project +defined in the `projectFileName` variable in `config.py`. The variable can be a path, which will load the project there, +or a Scratch ID / URL, which will download and cache the specified online project. No support for downloading unshared +projects is provided! (The Scratch Team will implement access control, and it won't be possible anyway soon too.) -Currently supported languages: +### GUI (experimental) +Change the `testMode` variable to `False` so Scratch2Python will open a GUI when started. For now this is a basic filechooser, +but we'll implement a proper full GUI soon. -| Language code | Language name (English) | Language name (translated) | Flag | -|---------------|-------------------------|----------------------------|------| -| en | English | English | 🇬🇧 | -| ro | Romanian | limba română | 🇷🇴 | +## 📖 Wiki +Our [GitHub Wiki](https://github.com/Secret-chest/scratch2python/wiki) may have some outdated information and it is short, but still useful. -## ✨ Contributors +## 🧑‍💻 Contributors -These people have contributed to this project and helped shape it ([emoji key](https://allcontributors.org/docs/en/emoji-key)): +Thanks to the people listed here for contributing to Scratch2Python! ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/__pycache__/block.cpython-310.pyc b/__pycache__/block.cpython-310.pyc index a6a0104..eab4568 100644 Binary files a/__pycache__/block.cpython-310.pyc and b/__pycache__/block.cpython-310.pyc differ diff --git a/__pycache__/config.cpython-310.pyc b/__pycache__/config.cpython-310.pyc index 3e6aaa3..d03bbf1 100644 Binary files a/__pycache__/config.cpython-310.pyc and b/__pycache__/config.cpython-310.pyc differ diff --git a/__pycache__/downloader.cpython-310.pyc b/__pycache__/downloader.cpython-310.pyc new file mode 100644 index 0000000..115ecd8 Binary files /dev/null and b/__pycache__/downloader.cpython-310.pyc differ diff --git a/__pycache__/eventContainer.cpython-310.pyc b/__pycache__/eventContainer.cpython-310.pyc new file mode 100644 index 0000000..55f4405 Binary files /dev/null and b/__pycache__/eventContainer.cpython-310.pyc differ diff --git a/__pycache__/sb3Unpacker.cpython-310.pyc b/__pycache__/sb3Unpacker.cpython-310.pyc index 79bd126..c956b51 100644 Binary files a/__pycache__/sb3Unpacker.cpython-310.pyc and b/__pycache__/sb3Unpacker.cpython-310.pyc differ diff --git a/__pycache__/scratch.cpython-310.pyc b/__pycache__/scratch.cpython-310.pyc index 2a2213c..8e46dc7 100644 Binary files a/__pycache__/scratch.cpython-310.pyc and b/__pycache__/scratch.cpython-310.pyc differ diff --git a/__pycache__/target.cpython-310.pyc b/__pycache__/target.cpython-310.pyc index 97d419b..e59f57d 100644 Binary files a/__pycache__/target.cpython-310.pyc and b/__pycache__/target.cpython-310.pyc differ diff --git a/__pycache__/targetSprite.cpython-310.pyc b/__pycache__/targetSprite.cpython-310.pyc index a0415b7..40b61d0 100644 Binary files a/__pycache__/targetSprite.cpython-310.pyc and b/__pycache__/targetSprite.cpython-310.pyc differ diff --git a/assets/4af792de2989798e6848a92117092aca.svg b/assets/4af792de2989798e6848a92117092aca.svg deleted file mode 100644 index 8372e4c..0000000 --- a/assets/4af792de2989798e6848a92117092aca.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/83c36d806dc92327b9e7049a565c6bff.wav b/assets/83c36d806dc92327b9e7049a565c6bff.wav deleted file mode 100644 index 45742d5..0000000 Binary files a/assets/83c36d806dc92327b9e7049a565c6bff.wav and /dev/null differ diff --git a/assets/bcf454acf82e4504149f7ffe07081dbc.svg b/assets/bcf454acf82e4504149f7ffe07081dbc.svg deleted file mode 100644 index 03df23e..0000000 --- a/assets/bcf454acf82e4504149f7ffe07081dbc.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - Codestin Search App - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/cd21514d0531fdffb22204e0ec5ed84a.svg b/assets/cd21514d0531fdffb22204e0ec5ed84a.svg deleted file mode 100644 index 15f7311..0000000 --- a/assets/cd21514d0531fdffb22204e0ec5ed84a.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/assets/metadata.json b/assets/metadata.json deleted file mode 100644 index 73d5518..0000000 --- a/assets/metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"id":729175960,"title":"EventHandlers - A Scratch2Python Test Project","description":"","instructions":"","visibility":"visible","public":true,"comments_allowed":true,"is_published":true,"author":{"id":57831979,"username":"mumu245","scratchteam":false,"history":{"joined":"1900-01-01T00:00:00.000Z"},"profile":{"id":null,"images":{"90x90":"https://cdn2.scratch.mit.edu/get_image/user/57831979_90x90.png?v=","60x60":"https://cdn2.scratch.mit.edu/get_image/user/57831979_60x60.png?v=","55x55":"https://cdn2.scratch.mit.edu/get_image/user/57831979_55x55.png?v=","50x50":"https://cdn2.scratch.mit.edu/get_image/user/57831979_50x50.png?v=","32x32":"https://cdn2.scratch.mit.edu/get_image/user/57831979_32x32.png?v="}}},"image":"https://cdn2.scratch.mit.edu/get_image/project/729175960_480x360.png","images":{"282x218":"https://cdn2.scratch.mit.edu/get_image/project/729175960_282x218.png?v=1663778011","216x163":"https://cdn2.scratch.mit.edu/get_image/project/729175960_216x163.png?v=1663778011","200x200":"https://cdn2.scratch.mit.edu/get_image/project/729175960_200x200.png?v=1663778011","144x108":"https://cdn2.scratch.mit.edu/get_image/project/729175960_144x108.png?v=1663778011","135x102":"https://cdn2.scratch.mit.edu/get_image/project/729175960_135x102.png?v=1663778011","100x80":"https://cdn2.scratch.mit.edu/get_image/project/729175960_100x80.png?v=1663778011"},"history":{"created":"2022-09-07T15:49:39.000Z","modified":"2022-09-21T16:33:31.000Z","shared":"2022-09-21T16:33:31.000Z"},"stats":{"views":1,"loves":0,"favorites":0,"remixes":0},"remix":{"parent":null,"root":null},"project_token":"1663778466_0ba90b15796edf4cf6d4b45e68197a5a074256d2"} \ No newline at end of file diff --git a/assets/project.json b/assets/project.json index e210dba..a89e23f 100644 --- a/assets/project.json +++ b/assets/project.json @@ -1 +1 @@ -{"targets":[{"isStage":true,"name":"Stage","variables":{},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"name":"backdrop1","dataFormat":"svg","assetId":"cd21514d0531fdffb22204e0ec5ed84a","md5ext":"cd21514d0531fdffb22204e0ec5ed84a.svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":"ro"},{"isStage":false,"name":"Cat","variables":{},"lists":{},"broadcasts":{},"blocks":{"4NEH+1=zgYzgtfTbI_g_":{"opcode":"event_whenkeypressed","next":"m=bR4H@v7G-#HYSLw?zr","parent":null,"inputs":{},"fields":{"KEY_OPTION":["space",null]},"shadow":false,"topLevel":true,"x":264,"y":311},"m=bR4H@v7G-#HYSLw?zr":{"opcode":"motion_changexby","next":null,"parent":"4NEH+1=zgYzgtfTbI_g_","inputs":{"DX":[1,[4,"10"]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"cat-a","bitmapResolution":1,"dataFormat":"svg","assetId":"bcf454acf82e4504149f7ffe07081dbc","md5ext":"bcf454acf82e4504149f7ffe07081dbc.svg","rotationCenterX":48,"rotationCenterY":50}],"sounds":[{"name":"Meow","assetId":"83c36d806dc92327b9e7049a565c6bff","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":2,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"},{"isStage":false,"name":"Cat2","variables":{},"lists":{},"broadcasts":{},"blocks":{"2**JJA=gqz.x-q-C:[)y":{"opcode":"event_whenkeypressed","next":"iDb,X8,-{Eg-.!~@q+1i","parent":null,"inputs":{},"fields":{"KEY_OPTION":["space"]},"shadow":false,"topLevel":true,"x":120,"y":24},"iDb,X8,-{Eg-.!~@q+1i":{"opcode":"motion_changexby","next":null,"parent":"2**JJA=gqz.x-q-C:[)y","inputs":{"DX":[1,[4,"-10"]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"cat-a","bitmapResolution":1,"dataFormat":"svg","assetId":"4af792de2989798e6848a92117092aca","md5ext":"4af792de2989798e6848a92117092aca.svg","rotationCenterX":47.67898252524472,"rotationCenterY":49.49923017660271}],"sounds":[{"name":"Meow","assetId":"83c36d806dc92327b9e7049a565c6bff","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"1.1.6","agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.101 Safari/537.36"}} \ No newline at end of file +{"targets":[{"isStage":true,"name":"Stage","variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":["my variable",0]},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"name":"Hay Field","bitmapResolution":2,"dataFormat":"png","assetId":"da102a69d135973e0fc139131dec785a","rotationCenterX":480,"rotationCenterY":360}],"sounds":[{"name":"pop","assetId":"83a9787d4cb6f3b7632b4ddfebf74367","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":null},{"isStage":false,"name":"Sprite1","variables":{},"lists":{},"broadcasts":{},"blocks":{"D:TgXIVyJrJ_k|v^Lisx":{"opcode":"event_whenflagclicked","next":"ptHi3IywC$(o5mihO!ET","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":500,"y":579},"ptHi3IywC$(o5mihO!ET":{"opcode":"control_forever","next":null,"parent":"D:TgXIVyJrJ_k|v^Lisx","inputs":{"SUBSTACK":[2,"vrpIp+Y+4DNbk)DEO@kn"]},"fields":{},"shadow":false,"topLevel":false},"vrpIp+Y+4DNbk)DEO@kn":{"opcode":"motion_pointtowards","next":null,"parent":"ptHi3IywC$(o5mihO!ET","inputs":{"TOWARDS":[1,"9fYz0RDgmONr6LZ3ggiH"]},"fields":{},"shadow":false,"topLevel":false},"9fYz0RDgmONr6LZ3ggiH":{"opcode":"motion_pointtowards_menu","next":null,"parent":"vrpIp+Y+4DNbk)DEO@kn","inputs":{},"fields":{"TOWARDS":["Crystal",null]},"shadow":true,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"Arrow1-a","bitmapResolution":1,"dataFormat":"svg","assetId":"be8fcd10da0b082f8d4775088ef7bd52","rotationCenterX":28,"rotationCenterY":23}],"sounds":[{"name":"Meow","assetId":"83c36d806dc92327b9e7049a565c6bff","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":102.09042871030996,"draggable":false,"rotationStyle":"all around"},{"isStage":false,"name":"Crystal","variables":{},"lists":{},"broadcasts":{},"blocks":{"LM+.Uo#HB1kE6,D02hS}":{"opcode":"event_whenflagclicked","next":"k[fbM51((#Fz45U+/c}U","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":499,"y":460},"k[fbM51((#Fz45U+/c}U":{"opcode":"control_forever","next":null,"parent":"LM+.Uo#HB1kE6,D02hS}","inputs":{"SUBSTACK":[2,"u/fFS`XChmDRFm;%GOp:"]},"fields":{},"shadow":false,"topLevel":false},"u/fFS`XChmDRFm;%GOp:":{"opcode":"motion_turnright","next":"Fpi~c0Aar+ud+MdN#CkK","parent":"k[fbM51((#Fz45U+/c}U","inputs":{"DEGREES":[1,[4,"15"]]},"fields":{},"shadow":false,"topLevel":false},"Fpi~c0Aar+ud+MdN#CkK":{"opcode":"motion_movesteps","next":null,"parent":"u/fFS`XChmDRFm;%GOp:","inputs":{"STEPS":[1,[4,"20"]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"crystal-a","bitmapResolution":1,"dataFormat":"svg","assetId":"ecd1e7805b37db4caf207b7eef2b7a42","md5ext":"ecd1e7805b37db4caf207b7eef2b7a42.svg","rotationCenterX":15,"rotationCenterY":15},{"name":"crystal-b","bitmapResolution":1,"dataFormat":"svg","assetId":"0a7b872042cecaf30cc154c0144f002b","md5ext":"0a7b872042cecaf30cc154c0144f002b.svg","rotationCenterX":12,"rotationCenterY":24}],"sounds":[{"name":"Magic Spell","assetId":"1cb60ecdb1075c8769cb346d5c2a22c7","dataFormat":"wav","format":"adpcm","rate":22050,"sampleCount":43689,"md5ext":"1cb60ecdb1075c8769cb346d5c2a22c7.wav"},{"name":"collect","assetId":"32514c51e03db680e9c63857b840ae78","dataFormat":"wav","format":"adpcm","rate":22050,"sampleCount":14225,"md5ext":"32514c51e03db680e9c63857b840ae78.wav"}],"volume":100,"layerOrder":2,"visible":true,"x":50.26527836193807,"y":-10.767159525430722,"size":100,"direction":105,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"1.5.76","agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"}} \ No newline at end of file diff --git a/block.py b/block.py index d61d7fa..d571ea8 100644 --- a/block.py +++ b/block.py @@ -11,6 +11,8 @@ import config import math import pygame +import eventContainer +from scratch import KEY_MAPPING i18n.set("locale", config.language) i18n.set("filename_format", "{locale}.{format}") @@ -35,14 +37,16 @@ def __init__(self): self.timeDelay = 0 # wait time from the wait block self.target = None # parent self.substack = set() # blocks inside + self.substack2 = set() # only for if then else self.script = set() # blocks below self.screenRefresh = False # do a screen refresh self.inEventLoop = False self.value = None # reported value self.repeatCounter = None # for repeat block + self.canRun = None # for if then / if then else # Evaluates block value (for reporters) - def evaluateBlockValue(self): + def evaluateBlockValue(self, eventContainer=eventContainer.EventContainer): if self.opcode == "operator_add": # () + () self.value = float(self.getInputValue("num1")) + float(self.getInputValue("num2")) return self.value @@ -58,6 +62,7 @@ def evaluateBlockValue(self): except ZeroDivisionError: raise ZeroDivisionError(_("zero-division-error")) return self.value + elif self.opcode == "operator_random": # pick random from () to () decimals1 = len(str(math.modf(float(self.getInputValue("from"))))) - 2 decimals2 = len(str(math.modf(float(self.getInputValue("to"))))) - 2 @@ -67,34 +72,66 @@ def evaluateBlockValue(self): decimals = decimals2 self.value = random.randint(int(self.getInputValue("from")) * 10 ** decimals, int(self.getInputValue("to")) * 10 ** decimals) / 10 ** decimals return self.value + + elif self.opcode == "operator_equals": # () = () + self.value = self.getInputValue("operand1") == self.getInputValue("operand2") + return self.value + elif self.opcode == "operator_lt": # () < () + self.value = self.getInputValue("operand1") < self.getInputValue("operand2") + return self.value + elif self.opcode == "operator_gt": # () > () + self.value = self.getInputValue("operand1") > self.getInputValue("operand2") + return self.value + elif self.opcode == "motion_xposition": # x position self.value = self.target.x return self.value elif self.opcode == "motion_xposition": # y position self.value = self.target.y return self.value + elif self.opcode == "motion_direction": # y position + self.value = self.target.direction + return self.value + elif self.opcode == "sensing_mousex": # mouse x newX, newY = pygame.mouse.get_pos() newX = newX - config.screenWidth // 2 self.value = newX - return newX + return self.value elif self.opcode == "sensing_mousey": # mouse y newX, newY = pygame.mouse.get_pos() - newY = newY - config.screenWidth // 2 + newY = newY - config.screenHeight // 2 self.value = newY - return newY + return self.value + + elif self.opcode == "sensing_keypressed": # key pressed? + self.value = KEY_MAPPING[self.getMenuValue("key_option")] in eventContainer.keys + return self.value + elif self.opcode == "sensing_mousedown": # mouse down? + self.value = pygame.mouse.get_pressed()[0] + return self.value + + elif self.opcode == "operator_not": # not <> + self.value = not self.target.blocks[self.getBlockInputValue("operand")].evaluateBlockValue(eventContainer) + return self.value + elif self.opcode == "operator_and": # <> and <> + self.value = self.target.blocks[self.getBlockInputValue("operand1")].evaluateBlockValue(eventContainer) and self.target.blocks[self.getBlockInputValue("operand2")].evaluateBlockValue(eventContainer) + return self.value + elif self.opcode == "operator_or": # <> or <> + self.value = self.target.blocks[self.getBlockInputValue("operand1")].evaluateBlockValue(eventContainer) or self.target.blocks[self.getBlockInputValue("operand2")].evaluateBlockValue(eventContainer) + return self.value # Returns block input value def getBlockInputValue(self, inputId): return self.inputs[inputId.upper()][1] # Returns block input value - def getInputValue(self, inputId, lookIn=(1, 1)): - if self.inputs[inputId.upper()][lookIn[0]][0] in {4, 0, 5, 6}: + def getInputValue(self, inputId, lookIn=(1, 1), eventContainer=eventContainer.EventContainer()): + if self.inputs[inputId.upper()][lookIn[0]][0] in {4, 0, 5, 6, 8}: return self.inputs[inputId.upper()][lookIn[0]][1] or 0 elif self.inputs[inputId.upper()][0] == 3: blockLink = self.inputs[inputId.upper()][1] - return self.target.blocks[blockLink].evaluateBlockValue() + return self.target.blocks[blockLink].evaluateBlockValue(eventContainer) else: pass @@ -104,7 +141,7 @@ def getCustomInputValue(self, number, lookIn=(1, 1)): # Returns dropdown menu value (menus are separate blocks) def getMenuValue(self, menuId): - return self.inputs[menuId.upper()][1].fields[menuId.upper()][0] + return self.target.blocks[self.inputs[menuId.upper()][1]].fields[menuId.upper()][0] # Returns field value (menus are separate blocks) def getFieldValue(self, fieldId, lookIn=0): diff --git a/config.py b/config.py index 12dd604..6dbab2c 100644 --- a/config.py +++ b/config.py @@ -24,7 +24,7 @@ # Project file name # If in test mode, set the Scratch project file to load. -projectFileName: str = "https://scratch.mit.edu/projects/729175960/" +projectFileName: str = "projects/PointTowardsSprite.sb3" # Download cache size # Number of recent downloaded projects stored. 0 means infinity. @@ -47,9 +47,11 @@ # pygame X.Y.Z (SDL X.Y.Z, Python 3.Y.Z) # Hello from the pygame community. https://www.pygame.org/contribute.html" # message. - pygameWelcomeMessage: bool = True +# Disable print function +disablePrint: bool = False + # Enable Scratch Addons debugger logs # This allows projects using Scratch Addons to print messages to the console. Vanilla Scratch doesn't support it. showSALogs: bool = True @@ -68,7 +70,7 @@ # Screen width/height # Stage size. You can change that, but most projects won't work with it. # A scaling mode will be added later. -# Vanilla is 480x360. +# Vanilla is 480x360. Try 640x360 for 16/9 widescreen. screenWidth: int = 480 screenHeight: int = 360 diff --git a/costume.py b/costume.py index 608f8b8..9d0ccbd 100644 --- a/costume.py +++ b/costume.py @@ -13,6 +13,7 @@ def __init__(self): self.dataFormat = "svg" self.rotationCenterX = 0 self.rotationCenterY = 0 + self.offset = None self.bitmapResolution = 1 self.file = None self.name = "" # display name diff --git a/eventContainer.py b/eventContainer.py new file mode 100644 index 0000000..3e1d957 --- /dev/null +++ b/eventContainer.py @@ -0,0 +1,5 @@ +class EventContainer: + def __init__(self): + self.keys = set() + self.keyEvents = set() + self.otherEvents = set() diff --git a/fonts/Grand9K-Pixel.ttf b/fonts/Grand9K-Pixel.ttf new file mode 100644 index 0000000..cf6fdf4 Binary files /dev/null and b/fonts/Grand9K-Pixel.ttf differ diff --git a/fonts/Griffy-Regular.ttf b/fonts/Griffy-Regular.ttf new file mode 100644 index 0000000..22eea37 Binary files /dev/null and b/fonts/Griffy-Regular.ttf differ diff --git a/fonts/Knewave.ttf b/fonts/Knewave.ttf new file mode 100644 index 0000000..7d1545b Binary files /dev/null and b/fonts/Knewave.ttf differ diff --git a/fonts/LICENSE_CC_BY-SA_3.0.txt b/fonts/LICENSE_CC_BY-SA_3.0.txt new file mode 100644 index 0000000..48ae233 --- /dev/null +++ b/fonts/LICENSE_CC_BY-SA_3.0.txt @@ -0,0 +1,5 @@ +Applies to: Grand9K-Pixel.ttf + +This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this +license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, PO Box 1866, +Mountain View, CA 94042, USA. diff --git a/fonts/LICENSE_SIL_OFL.txt b/fonts/LICENSE_SIL_OFL.txt new file mode 100644 index 0000000..2fd9b6e --- /dev/null +++ b/fonts/LICENSE_SIL_OFL.txt @@ -0,0 +1,87 @@ +Applies to: Griffy-Regular.ttf, handlee-regular.ttf, Knewave.ttf, NotoSans-Medium.ttf, SourceSansPro-Regular.ttf, SourceSerifPro-Regular.ttf + +SIL OPEN FONT LICENSE +Version 1.1 - 26 February 2007 + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting — in part or in whole — any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/NotoSans-Medium.ttf b/fonts/NotoSans-Medium.ttf new file mode 100644 index 0000000..25050f7 Binary files /dev/null and b/fonts/NotoSans-Medium.ttf differ diff --git a/fonts/SourceSansPro-Regular.ttf b/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..98e8579 Binary files /dev/null and b/fonts/SourceSansPro-Regular.ttf differ diff --git a/fonts/SourceSerifPro-Regular.otf b/fonts/SourceSerifPro-Regular.otf new file mode 100644 index 0000000..bcad74a Binary files /dev/null and b/fonts/SourceSerifPro-Regular.otf differ diff --git a/fonts/handlee-regular.ttf b/fonts/handlee-regular.ttf new file mode 100644 index 0000000..02a94e1 Binary files /dev/null and b/fonts/handlee-regular.ttf differ diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..c4286b0 --- /dev/null +++ b/gui.py @@ -0,0 +1,51 @@ +import gi +import os +import sys + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +def pause(button): + print("PAUSE", file=p.stdin) + print("PAUSE") + + +def stop(button): + print("STOP", file=p.stdin) + print("STOP") + + +handlers = { + "onDestroy": Gtk.main_quit, + "pause": pause, + "stop": stop, +} + +builder = Gtk.Builder() +builder.add_from_file("main.glade") +builder.connect_signals(handlers) + +window = builder.get_object("test") +window.show_all() + +stdin = sys.stdin.fileno() # usually 0 +stdout = sys.stdout.fileno() # usually 1 + +parentStdin, childStdout = os.pipe() +childStdin, parentStdout = os.pipe() +pid = os.fork() +if pid: + # parent process + os.close(childStdout) + os.close(childStdin) + os.dup2(parentStdin, stdin) + os.dup2(parentStdout, stdout) + Gtk.main() +else: + # child process + os.close(parentStdin) + os.close(parentStdout) + os.dup2(childStdin, stdin) + os.dup2(childStdout, stdout) + os.execl("python", "main.py", "main.py") diff --git a/lang/en.yml b/lang/en.yml index 4cc700d..a165c8c 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -48,4 +48,5 @@ en: zero-division-error: "Project was trying to divide by 0" costumes-count: "%{sprite} has %{costumes} costumes" stage-not-found: "There is no stage sprite! Invalid project." - no-any-key: "Sorry, there is no support for the \"any\" key option yet. We're working on other things and it will be added when the code for the main key block is done." \ No newline at end of file + new-sprite-rotation: "Sprite %{name} rotated to %{rot} degrees" + new-sprite-size: "Sprite %{name} resized to %{size}%" \ No newline at end of file diff --git a/lang/pl.yml b/lang/pl.yml new file mode 100644 index 0000000..3a244ec --- /dev/null +++ b/lang/pl.yml @@ -0,0 +1,52 @@ +pl: + start: "Scratch2Python %{version}, %{os}" + unrecognized-os: "Niestety Scratch2Python nie rozpoznaje twojego systemu operacyjnego. Twój platform string to: %{platform}. Jeżeli wystąpi jakiś błąd, proszę go zgłosić tutaj: %{url}" + filename-prompt: "Nazwa pliku projektu:" + sb3-desc: "Projekty Scratch 3" + all-files-desc: "Wszystkie pliki (*.*)" + choose-project-title: "Wybierz projekt do załadowania" + invalid-setting-error: "Nieznane ustawienie %{setting}" + paused-message: "Zapausowane (naciśnij przycisk %{keybind}, aby odpauzować)" + window-title: "%{projectName} - %{s2pVersionString}" + extracting-project: "Wypakowywanie projektu" + screen-width-prompt: "Szerokość:" + screen-height-prompt: "Wysokość:" + ok: "OK" + cancel: "Anuluj" + project-started: "Zaczęto projekt" + player-closed: "Zamknięto odtwarzacz" + nothing-to-see-here: "Nie ma co tu zobaczyć" + help-title: "Pomoc" + extract-title: "Wypakuj projekt" + project-info-title: "Informacje projektu" + fps-title: "FPS (klatki na sekundę)" + screen-title: "Rozdzielczość ekranu" + fps-prompt: "Wprowadź ilość klatek na sekundę (FPS)" + fps-message: "Ustawiono FPS na:" + extract-prompt: "Wypakować wszystko z projektu?" + screen-message: "Ustawiono rozdzielczość ekranu na:" + redraw-message: "Ekran przerysowany" + config-warning-screen-too-small: "Ta rozdzielczość jest bardzo mała. Zalecana rozdzielczość minimalna to %{res}. Jeżeli chcesz zignorować ten błąd, włącz zmienną INSANE w config.py." + config-warning-screen-too-large: "Ta rozdzielczość jest bardzo duża. Zalecana rozdzielczość maksymalna to %{res}. Jeżeli chcesz zignorować ten błąd, włącz zmienną INSANE w config.py." + project-file-not-found: "Nie znaleziono pliku projektu." + loading-project: "Ładowanie projektu" + debug-prefix: "DEBUG:" + block-waiting: "Czekanie %{time} ms" + keypress-handling: "Naciśnięto %{keyName}" + key-any: "każdy" + key-left: "strzałka w lewo" + key-right: "strzałka w prawo" + key-up: "strzałka w górę" + key-down: "strzałka w dół" + key-space: "spacja" + project-log: "PROJEKT:" + project-warn: "OSTRZEŻENIE PROJEKTU:" + project-error: "BŁĄD PROJEKTU:" + unknown-opcode: "Nieznany kod operacji:" + new-sprite-position: "Nowa pozycja (%{x}, %{y}) dla duszka %{name}" + stage: "Tło" + zero-division-error: "Projekt próbował dzielić przez 0" + costumes-count: "Duszek %{sprite} ma %{costumes} kostiumów" + stage-not-found: "Nie ma tła! Nieprawidłowy projekt." + new-sprite-rotation: "Duszek %{name} obrócony %{rot} stopni" + new-sprite-size: "Nowy rozmiar duszka %{name}: %{size}%" diff --git a/lang/ro.yml b/lang/ro.yml index dd3995c..3bc73de 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -24,7 +24,7 @@ ro: fps-prompt: "Introdu numărul de cadre pe secundă" fps-message: "Rata de cadre pe secundă a fost setată la:" extract-prompt: "Extrage toate resursele proiectului?" - screen-message: "Rezoulția ecranului a fost setată la:" + screen-message: "Rezoluția ecranului a fost setată la:" redraw-message: "Ecran redesenat" config-warning-screen-too-small: "Această rezoluție este foarte mică. Rezoluția minimă recomandată este cel puțin %{res}. Dacă vrei să nu apară acest mesaj, setează variabila INSANE în config.py pe True." config-warning-screen-too-large: "Această rezoluție este foarte mare. Rezoluția maximă recomandată este cel mult %{res}. Dacă vrei să nu apară acest mesaj, setează variabila INSANE în config.py pe True." @@ -48,4 +48,5 @@ ro: zero-division-error: "Proiectul a încercat să împartă la 0" costumes-count: "%{sprite} are %{costumes} costume" stage-not-found: "Nu există scena! Proiect invalid." - no-any-key: "Scuze, nu există suport pentru opțiunea de taste „oricare” încă. Lucrăm la altceva și va fi adăugat când blocul principal e gata." \ No newline at end of file + new-sprite-rotation: "Personajul %{name} a fost rotit la %{rot} grade" + new-sprite-size: "Personajul %{name} a fost redimensionat la %{size}%" \ No newline at end of file diff --git a/main.glade b/main.glade new file mode 100644 index 0000000..c2e12c8 --- /dev/null +++ b/main.glade @@ -0,0 +1,1648 @@ + + + + + + True + False + dialog-information + + + 576 + 320 + False + 8 + Scratch2Python + False + scratch + + + True + False + vertical + + + True + False + vertical + + + True + False + Scratch2Python project loader + 0 + + + + + + + False + True + 0 + + + + + True + False + version {VERSION} + 0 + + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + center + center + 8 + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + application-x-executable + 6 + + + False + True + 0 + + + + + True + False + Load local project + + + + + + False + True + 1 + + + + + True + False + from your computer + + + + + + False + True + 2 + + + + + + + False + True + 0 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + application-x-partial-download + 6 + + + False + True + 0 + + + + + True + False + Cache project + + + + + + False + True + 1 + + + + + True + False + from scratch.mit.edu + + + + + + False + True + 2 + + + + + + + False + True + 1 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + preferences-system + 6 + + + False + True + 0 + + + + + True + False + Settings + + + + + + False + True + 1 + + + + + + + + + + False + True + 3 + + + + + True + True + 1 + + + + + True + False + + + Need a GUI for browsing projects on scratch.mit.edu? + True + True + True + none + https://example.org + + + False + True + 0 + + + + + About + True + True + True + aboutIcon + True + + + + False + True + end + 1 + + + + + False + True + 2 + + + + + + + True + False + dialog-information + + + 64 + 2 + 10 + + + 4096 + 300 + 4 + 10 + + + 3000 + 250 + 25 + 10 + + + 240 + 15 + 10 + + + + False + bottom + + + True + False + vertical + + + True + False + Cache and load project + + + + + + False + True + 0 + + + + + True + True + projectIDValue + Project ID or URL + + + False + True + 1 + + + + + Load + True + True + True + + + + False + True + 2 + + + + + + + 16 + 2048 + 360 + 8 + 10 + + + 16 + 2048 + 480 + 8 + 10 + + + False + Scratch2Python Settings + 540 + 360 + + + True + True + left + True + True + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Download cache size + 0 + + + + + + False + True + 0 + + + + + True + False + Number of stored recently downloaded projects before they start getting deleted. 0 means infinity. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + cacheSize + True + 4 + + + 1 + 0 + + + + + Clear download cache + True + True + True + + + + 1 + 1 + + + + + True + False + Clear cache + 0 + + + + + + 0 + 1 + + + + + + + + + + + True + False + + + True + False + folder-download + 3 + + + False + True + 0 + + + + + True + False + Download cache + 1 + + + False + True + 1 + + + + + False + + + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Maximum framerate + 0 + + + + + + False + True + 0 + + + + + True + False + The maximum framerate for projects. Sometimes changing it can improve things, but most projects will feel too fast or too slow. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + 0 + maxFPS + True + 30.000000000223519 + + + 1 + 0 + + + + + True + False + vertical + + + True + False + Turbo mode + 0 + + + + + + False + True + 0 + + + + + True + False + Makes screen refreshes as fast as possible. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 1 + + + + + True + True + + + 1 + 1 + + + + + True + False + vertical + + + True + False + Screen resolution + 0 + + + + + + False + True + 0 + + + + + True + False + Change the stage size. Most projects won't adapt properly. Try 640x360 for a widescreen version of the default stage. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 2 + + + + + + True + False + + + True + True + screenWidth + + + 1 + 0 + + + + + True + False + W + + + 0 + 0 + + + + + True + False + H + + + 0 + 1 + + + + + True + True + 480 + screenHeight + 360 + + + 1 + 1 + + + + + 1 + 2 + + + + + True + False + vertical + + + True + False + Allow off-screen sprites + 0 + + + + + + False + True + 0 + + + + + True + False + Allow sprites to bypass sprite fencing and completely leave the stage by normal means. Projects may break. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 3 + + + + + True + True + + + 1 + 3 + + + + + True + False + Reset all project defaults + 0 + + + + + + 0 + 5 + + + + + Reset + True + True + True + + + + + 1 + 5 + + + + + True + False + vertical + + + True + False + Clone limit + 0 + + + + + + False + True + 0 + + + + + True + False + Maximum number of clones allowed at one time. 0 means unlimited. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 4 + + + + + True + True + cloneLimit + + + 1 + 4 + + + + + + + + + 1 + + + + + True + False + + + True + False + computer + 3 + + + False + True + 0 + + + + + True + False + Project defaults + 1 + + + False + True + 1 + + + + + 1 + False + + + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + True + False + + + + + + + 2 + + + + + True + False + + + True + False + preferences-desktop-locale + 3 + + + False + True + 0 + + + + + True + False + Languages + 1 + + + False + True + 1 + + + + + 2 + False + + + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Extract project on run + 0 + + + + + + False + True + 0 + + + + + True + False + If enabled, Scratch2Python will automatically extract the project that is being run into the "assets" directory inside the Scratch2Python install location. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + True + + + 1 + 0 + + + + + True + False + Allow debug messages in terminal + 0 + + + + + + 0 + 2 + + + + + True + True + True + + + 1 + 2 + + + + + True + False + Allow Scratch Addons logs in terminal + 0 + + + + + + 0 + 3 + + + + + True + True + True + + + 1 + 3 + + + + + True + False + Allow terminal output + 0 + + + + + + 0 + 1 + + + + + True + True + True + + + 1 + 1 + + + + + True + True + True + + + 1 + 4 + + + + + True + False + vertical + + + True + False + Allow pygame welcome message + 0 + + + + + + False + True + 0 + + + + + True + False + Allow pygame to display +"pygame X.Y.Z (SDL X.Y.Z, Python 3.Y.Z) +Hello from the pygame community. https://www.pygame.org/contribute.html" +on startup. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 4 + + + + + + + + + 3 + + + + + True + False + + + True + False + utilities-system-monitor + 3 + + + False + True + 0 + + + + + True + False + Debug + 1 + + + False + True + 1 + + + + + 3 + False + + + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Key delay + 0 + + + + + + False + True + 0 + + + + + True + False + Milliseconds before keys will start repeating, 0 means keys won't repeat. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + 4 + keyDelay + True + 4 + + + 1 + 0 + + + + + 4 + + + + + True + False + + + True + False + preferences-desktop-accessibility + 3 + + + False + True + 0 + + + + + True + False + Accesibility + 1 + + + False + True + 1 + + + + + 4 + False + + + + + + + False + + + True + False + 8 + 8 + 8 + 8 + vertical + + + True + False + vertical + + + True + False + Test controls + 0 + + + + + + + False + True + 0 + + + + + False + True + 0 + + + + + True + False + center + center + 8 + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + media-playback-pause + 6 + + + False + True + 0 + + + + + True + False + Pause/resume project + + + + + + False + True + 1 + + + + + + + + + + False + True + 0 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + media-playback-stop + 6 + + + False + True + 0 + + + + + True + False + Quit player + + + + + + False + True + 1 + + + + + + + + + + False + True + 1 + + + + + True + True + 1 + + + + + True + False + + + Need a GUI for browsing projects on scratch.mit.edu? + True + True + True + none + https://example.org + + + False + True + 0 + + + + + About + True + True + True + aboutIcon1 + True + + + False + True + end + 1 + + + + + False + True + 2 + + + + + + diff --git a/main.glade~ b/main.glade~ new file mode 100644 index 0000000..c2e12c8 --- /dev/null +++ b/main.glade~ @@ -0,0 +1,1648 @@ + + + + + + True + False + dialog-information + + + 576 + 320 + False + 8 + Scratch2Python + False + scratch + + + True + False + vertical + + + True + False + vertical + + + True + False + Scratch2Python project loader + 0 + + + + + + + False + True + 0 + + + + + True + False + version {VERSION} + 0 + + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + center + center + 8 + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + application-x-executable + 6 + + + False + True + 0 + + + + + True + False + Load local project + + + + + + False + True + 1 + + + + + True + False + from your computer + + + + + + False + True + 2 + + + + + + + False + True + 0 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + application-x-partial-download + 6 + + + False + True + 0 + + + + + True + False + Cache project + + + + + + False + True + 1 + + + + + True + False + from scratch.mit.edu + + + + + + False + True + 2 + + + + + + + False + True + 1 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + preferences-system + 6 + + + False + True + 0 + + + + + True + False + Settings + + + + + + False + True + 1 + + + + + + + + + + False + True + 3 + + + + + True + True + 1 + + + + + True + False + + + Need a GUI for browsing projects on scratch.mit.edu? + True + True + True + none + https://example.org + + + False + True + 0 + + + + + About + True + True + True + aboutIcon + True + + + + False + True + end + 1 + + + + + False + True + 2 + + + + + + + True + False + dialog-information + + + 64 + 2 + 10 + + + 4096 + 300 + 4 + 10 + + + 3000 + 250 + 25 + 10 + + + 240 + 15 + 10 + + + + False + bottom + + + True + False + vertical + + + True + False + Cache and load project + + + + + + False + True + 0 + + + + + True + True + projectIDValue + Project ID or URL + + + False + True + 1 + + + + + Load + True + True + True + + + + False + True + 2 + + + + + + + 16 + 2048 + 360 + 8 + 10 + + + 16 + 2048 + 480 + 8 + 10 + + + False + Scratch2Python Settings + 540 + 360 + + + True + True + left + True + True + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Download cache size + 0 + + + + + + False + True + 0 + + + + + True + False + Number of stored recently downloaded projects before they start getting deleted. 0 means infinity. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + cacheSize + True + 4 + + + 1 + 0 + + + + + Clear download cache + True + True + True + + + + 1 + 1 + + + + + True + False + Clear cache + 0 + + + + + + 0 + 1 + + + + + + + + + + + True + False + + + True + False + folder-download + 3 + + + False + True + 0 + + + + + True + False + Download cache + 1 + + + False + True + 1 + + + + + False + + + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Maximum framerate + 0 + + + + + + False + True + 0 + + + + + True + False + The maximum framerate for projects. Sometimes changing it can improve things, but most projects will feel too fast or too slow. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + 0 + maxFPS + True + 30.000000000223519 + + + 1 + 0 + + + + + True + False + vertical + + + True + False + Turbo mode + 0 + + + + + + False + True + 0 + + + + + True + False + Makes screen refreshes as fast as possible. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 1 + + + + + True + True + + + 1 + 1 + + + + + True + False + vertical + + + True + False + Screen resolution + 0 + + + + + + False + True + 0 + + + + + True + False + Change the stage size. Most projects won't adapt properly. Try 640x360 for a widescreen version of the default stage. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 2 + + + + + + True + False + + + True + True + screenWidth + + + 1 + 0 + + + + + True + False + W + + + 0 + 0 + + + + + True + False + H + + + 0 + 1 + + + + + True + True + 480 + screenHeight + 360 + + + 1 + 1 + + + + + 1 + 2 + + + + + True + False + vertical + + + True + False + Allow off-screen sprites + 0 + + + + + + False + True + 0 + + + + + True + False + Allow sprites to bypass sprite fencing and completely leave the stage by normal means. Projects may break. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 3 + + + + + True + True + + + 1 + 3 + + + + + True + False + Reset all project defaults + 0 + + + + + + 0 + 5 + + + + + Reset + True + True + True + + + + + 1 + 5 + + + + + True + False + vertical + + + True + False + Clone limit + 0 + + + + + + False + True + 0 + + + + + True + False + Maximum number of clones allowed at one time. 0 means unlimited. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 4 + + + + + True + True + cloneLimit + + + 1 + 4 + + + + + + + + + 1 + + + + + True + False + + + True + False + computer + 3 + + + False + True + 0 + + + + + True + False + Project defaults + 1 + + + False + True + 1 + + + + + 1 + False + + + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + True + False + + + + + + + 2 + + + + + True + False + + + True + False + preferences-desktop-locale + 3 + + + False + True + 0 + + + + + True + False + Languages + 1 + + + False + True + 1 + + + + + 2 + False + + + + + True + True + 8 + 8 + 8 + 8 + in + + + True + False + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Extract project on run + 0 + + + + + + False + True + 0 + + + + + True + False + If enabled, Scratch2Python will automatically extract the project that is being run into the "assets" directory inside the Scratch2Python install location. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + True + + + 1 + 0 + + + + + True + False + Allow debug messages in terminal + 0 + + + + + + 0 + 2 + + + + + True + True + True + + + 1 + 2 + + + + + True + False + Allow Scratch Addons logs in terminal + 0 + + + + + + 0 + 3 + + + + + True + True + True + + + 1 + 3 + + + + + True + False + Allow terminal output + 0 + + + + + + 0 + 1 + + + + + True + True + True + + + 1 + 1 + + + + + True + True + True + + + 1 + 4 + + + + + True + False + vertical + + + True + False + Allow pygame welcome message + 0 + + + + + + False + True + 0 + + + + + True + False + Allow pygame to display +"pygame X.Y.Z (SDL X.Y.Z, Python 3.Y.Z) +Hello from the pygame community. https://www.pygame.org/contribute.html" +on startup. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 4 + + + + + + + + + 3 + + + + + True + False + + + True + False + utilities-system-monitor + 3 + + + False + True + 0 + + + + + True + False + Debug + 1 + + + False + True + 1 + + + + + 3 + False + + + + + + True + False + 8 + 4 + + + True + False + vertical + + + True + False + Key delay + 0 + + + + + + False + True + 0 + + + + + True + False + Milliseconds before keys will start repeating, 0 means keys won't repeat. + True + 0 + + + + + + + False + True + 1 + + + + + 0 + 0 + + + + + True + True + 4 + keyDelay + True + 4 + + + 1 + 0 + + + + + 4 + + + + + True + False + + + True + False + preferences-desktop-accessibility + 3 + + + False + True + 0 + + + + + True + False + Accesibility + 1 + + + False + True + 1 + + + + + 4 + False + + + + + + + False + + + True + False + 8 + 8 + 8 + 8 + vertical + + + True + False + vertical + + + True + False + Test controls + 0 + + + + + + + False + True + 0 + + + + + False + True + 0 + + + + + True + False + center + center + 8 + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + media-playback-pause + 6 + + + False + True + 0 + + + + + True + False + Pause/resume project + + + + + + False + True + 1 + + + + + + + + + + False + True + 0 + + + + + 160 + 128 + True + True + True + center + center + + + + True + False + center + center + vertical + + + True + False + 48 + media-playback-stop + 6 + + + False + True + 0 + + + + + True + False + Quit player + + + + + + False + True + 1 + + + + + + + + + + False + True + 1 + + + + + True + True + 1 + + + + + True + False + + + Need a GUI for browsing projects on scratch.mit.edu? + True + True + True + none + https://example.org + + + False + True + 0 + + + + + About + True + True + True + aboutIcon1 + True + + + False + True + end + 1 + + + + + False + True + 2 + + + + + + diff --git a/main.py b/main.py index c12db62..5ebd641 100644 --- a/main.py +++ b/main.py @@ -19,7 +19,7 @@ along with this program. If not, see . """ -__version__ = "v0.1.0" +__version__ = "v0.8.0" __author__ = "Secret-chest" import tkinter.simpledialog @@ -73,6 +73,9 @@ from tkinter.simpledialog import * from tkinter import filedialog from targetSprite import TargetSprite +import eventContainer +import select +from scratch import display, setProject, mainWindow sys.stdout = sys.__stdout__ @@ -82,6 +85,13 @@ if not config.enableDebugMessages: sys.stderr = open(os.devnull, "w") +# Disable the print function +def decorator(func): + def disabledPrint(*args, **kwargs): + if not config.disablePrint: + func(*args, **kwargs) + return disabledPrint +print = decorator(print) # Define a dialog class for screen resolution class SizeDialog(tkinter.simpledialog.Dialog): @@ -117,34 +127,17 @@ def buttonbox(self): self.bind("", lambda event: self.cancelPressed()) -# Start tkinter for showing some popups, and hide main window -mainWindow = tk.Tk() -mainWindow.withdraw() +# Clean the cache if limit is exceeded +downloads = sorted(Path("./download/").iterdir(), key=os.path.getmtime) +downloadsToDelete = downloads[config.cachedDownloads:] +for f in downloadsToDelete: + os.remove(f) -# Get project file name based on options and arguments -if len(sys.argv) > 1: - setProject = sys.argv[1] -else: - if config.testMode: - if not config.projectFileName.endswith(".sb3"): - if "http" not in config.projectFileName\ - or "https" not in config.projectFileName: - setProject = downloader.downloadByID(config.projectFileName, "./download") - else: - setProject = downloader.downloadByURL(config.projectFileName, "./download") - else: - setProject = config.projectFileName - else: - fileTypes = [(_("sb3-desc"), ".sb3"), (_("all-files-desc"), ".*")] - setProject = filedialog.askopenfilename(parent=mainWindow, - initialdir=os.getcwd(), - title=_("choose-project-title"), - filetypes=fileTypes) # Get project data and create sprites targets, project = sb3Unpacker.sb3Unpack(setProject) allSprites = pygame.sprite.Group() -for t in targets: +for t in sorted(targets, key=lambda t: t.layerOrder): sprite = TargetSprite(t) t.sprite = sprite allSprites.add(sprite) @@ -155,8 +148,8 @@ def buttonbox(self): pygame.mixer.pre_init(22050, -16, 1, 12193) pygame.init() -font = pygame.font.SysFont(pygame.font.get_default_font(), 16) -fontXl = pygame.font.SysFont(pygame.font.get_default_font(), 36) +font = pygame.font.Font("./fonts/SourceSansPro-Regular.ttf", 16) +fontXl = pygame.font.Font("./fonts/SourceSansPro-Regular.ttf", 36) # Create paused message paused = fontXl.render(_("paused-message", keybind="F6"), True, (0, 0, 0)) @@ -166,15 +159,6 @@ def buttonbox(self): HEIGHT = config.projectScreenHeight WIDTH = config.projectScreenWidth -# Get project name and set icon -projectName = Path(setProject).stem -icon = pygame.image.load("icon.svg") - -# Create project player and window -display = pygame.display.set_mode([WIDTH, HEIGHT]) -pygame.display.set_caption(_("window-title", projectName=projectName, s2pVersionString="Scratch2Python " + __version__)) -pygame.display.set_icon(icon) - # Extract if requested if config.extractOnProjectRun: print(_("extracting-project")) @@ -224,16 +208,27 @@ def buttonbox(self): if nextBlock: # Add the next block to the queue toExecute.append(nextBlock) - elif block.opcode.startswith("event_"): # add "when I start as a clone" code later + elif block.opcode.startswith("event_"): # add “when I start as a clone” code later eventHandlers.append(block) -s = None +s = None # so we don't mix it up # Prepare keyboard pygame.key.set_repeat(config.keyDelay, 1000 // config.projectMaxFPS) -keyEvents = set() +keyEventContainer = eventContainer.EventContainer() # Mainloop +lastTime = time.time_ns() while projectRunning: + rlist, _, _ = select.select([sys.stdin], [], [], 0.1) + if rlist: + line = sys.stdin.readline().strip() + if line: + if line.startswith("STOP"): + pygame.quit() + elif line.startswith("PAUSE"): + isPaused = not isPaused + + keyEventContainer.keyEvents = set() # Process Pygame events for event in pygame.event.get(): # Window quit (ALT-F4 / X button / etc.) @@ -242,33 +237,36 @@ def buttonbox(self): projectRunning = False # Debug and utility functions - keyEvents = set() + # TODO why are the events correct here but not in execute??? + keyEventContainer.keyEvents = set() if event.type == pygame.KEYDOWN: - keyEvents.add(event.key) + keyEventContainer.keyEvents.add(event.key) + # print(event.key, "+" + str((time.time_ns() - lastTime) // 1000000)) + lastTime = time.time_ns() keysRaw = pygame.key.get_pressed() - keys = set(k for k in scratch.KEY_MAPPING.values() if keysRaw[k]) + keyEventContainer.keys = set(k for k in scratch.KEY_MAPPING.values() if keysRaw[k]) - if pygame.K_F1 in keys: # Help + if pygame.K_F1 in keyEventContainer.keys: # Help showinfo(helpTitle, nothingToSeeHere) - if pygame.K_F4 in keys: # Project info + if pygame.K_F4 in keyEventContainer.keys: # Project info showinfo(projectInfoTitle, nothingToSeeHere) - if pygame.K_F3 in keys: # Extract + if pygame.K_F3 in keyEventContainer.keys: # Extract confirm = askokcancel(extractTitle, extractPrompt) if confirm: print(extractMessage) shutil.rmtree("assets") os.mkdir("assets") project.extractall("assets") - if pygame.K_F6 in keys: # Pause + if pygame.K_F6 in keyEventContainer.keys: # Pause isPaused = not isPaused - if pygame.K_F7 in keys: # Set new FPS + if pygame.K_F7 in keyEventContainer.keys: # Set new FPS # Open dialog newFPS = askinteger(title=fpsTitle, prompt=fpsPrompt) if newFPS is not None: print(fpsMessage, newFPS) config.projectMaxFPS = newFPS pygame.key.set_repeat(1000, 1000 // config.projectMaxFPS) - if pygame.K_F8 in keys: # Set new screen resolution + if pygame.K_F8 in keyEventContainer.keys: # Set new screen resolution try: # Open special dialog dialog = SizeDialog(mainWindow, title=screenTitle) @@ -286,7 +284,7 @@ def buttonbox(self): print(screenMessage, str(HEIGHT) + "x" + str(WIDTH)) except ValueError: pass - if pygame.K_F5 in keys: # Redraw + if pygame.K_F5 in keyEventContainer.keys: # Redraw # Redraw everything and recalculate sprite operations display = pygame.display.set_mode([config.projectScreenWidth, config.projectScreenHeight]) HEIGHT = config.projectScreenHeight @@ -297,19 +295,22 @@ def buttonbox(self): print(redrawMessage) display.fill((255, 255, 255)) - if toExecute: - for block in toExecute: - pass - # print("Running block", block.blockID, "of type", block.opcode) if not isPaused: + # print("starting new frame", keyEventContainer.keyEvents) for e in eventHandlers: - if e.opcode == "event_whenkeypressed" and keys and not e.blockRan: + # TODO why does it run so many times??? + if e.opcode == "event_whenkeypressed" and keyEventContainer.keyEvents and not e.blockRan: + # print("running", e.blockID) + e.blockRan = True - nextBlock = scratch.execute(e, e.target.sprite, keys, keyEvents) + nextBlock = scratch.execute(e, e.target.sprite, keyEventContainer) if nextBlock and isinstance(nextBlock, list): toExecute.extend(nextBlock) + # print("next:", (b.blockID for b in nextBlock)) elif nextBlock: toExecute.append(nextBlock) + # print("next:", nextBlock.blockID) + # print(keyEventContainer.keyEvents, "in main.py", "(" + str(len(eventHandlers)) + ")") if e.opcode == "event_whenkeypressed": # print(s.target.blocks, e.script) @@ -328,13 +329,12 @@ def buttonbox(self): block.blockRan = True nextBlocks.append(block.target.blocks[block.next]) block.executionTime, block.timeDelay = 0, 0 - if not block.blockRan and not block.opcode.startswith("event"): - nextBlock = scratch.execute(block, block.target.sprite, keys, keyEvents) + if not block.blockRan and not block.opcode.startswith("event"): # TODO add broadcast blocks + nextBlock = scratch.execute(block, block.target.sprite, keyEventContainer) if not block.next \ and block.top \ and block.top.opcode.startswith("event") \ and block.top.opcode != "event_whenflagclicked": - print(block.top.blockRan, block.top.blockID) waitFinished = False waitFinishedFor = set() for b in block.top.script: @@ -354,7 +354,8 @@ def buttonbox(self): allSprites.draw(display) allSprites.update() else: - display.blit(paused, (WIDTH // 2 - pausedWidth // 2, WIDTH // 2 - pausedHeight // 2)) + display.blit(paused, (WIDTH // 2 - pausedWidth // 2, HEIGHT // 2 - pausedHeight // 2)) + pygame.display.flip() mainWindow.update() doScreenRefresh = False diff --git a/projects/AbsoluteRotation.sb3 b/projects/AbsoluteRotation.sb3 new file mode 100644 index 0000000..733a624 Binary files /dev/null and b/projects/AbsoluteRotation.sb3 differ diff --git a/projects/Balls.sb3 b/projects/Balls.sb3 new file mode 100644 index 0000000..676e6be Binary files /dev/null and b/projects/Balls.sb3 differ diff --git a/projects/ChangeColour.sb3 b/projects/ChangeColour.sb3 new file mode 100644 index 0000000..59b841b Binary files /dev/null and b/projects/ChangeColour.sb3 differ diff --git a/projects/ChangeColourWithElse.sb3 b/projects/ChangeColourWithElse.sb3 new file mode 100644 index 0000000..e502930 Binary files /dev/null and b/projects/ChangeColourWithElse.sb3 differ diff --git a/projects/GtkPauseTest.sb3 b/projects/GtkPauseTest.sb3 new file mode 100644 index 0000000..be76316 Binary files /dev/null and b/projects/GtkPauseTest.sb3 differ diff --git a/projects/Layering.sb3 b/projects/Layering.sb3 new file mode 100644 index 0000000..15c84e9 Binary files /dev/null and b/projects/Layering.sb3 differ diff --git a/projects/LeftRight.sb3 b/projects/LeftRight.sb3 new file mode 100644 index 0000000..facb82e Binary files /dev/null and b/projects/LeftRight.sb3 differ diff --git a/projects/MouseDown.sb3 b/projects/MouseDown.sb3 new file mode 100644 index 0000000..6ca55f4 Binary files /dev/null and b/projects/MouseDown.sb3 differ diff --git a/projects/MoveSteps.sb3 b/projects/MoveSteps.sb3 index b520bd6..1f0ee37 100644 Binary files a/projects/MoveSteps.sb3 and b/projects/MoveSteps.sb3 differ diff --git a/projects/NotKeyPressed.sb3 b/projects/NotKeyPressed.sb3 new file mode 100644 index 0000000..e07a8d9 Binary files /dev/null and b/projects/NotKeyPressed.sb3 differ diff --git a/projects/PointTowardsMouse.sb3 b/projects/PointTowardsMouse.sb3 new file mode 100644 index 0000000..299db65 Binary files /dev/null and b/projects/PointTowardsMouse.sb3 differ diff --git a/projects/PointTowardsSprite.sb3 b/projects/PointTowardsSprite.sb3 new file mode 100644 index 0000000..72d7efb Binary files /dev/null and b/projects/PointTowardsSprite.sb3 differ diff --git a/projects/Rotation.sb3 b/projects/Rotation.sb3 new file mode 100644 index 0000000..e1467d3 Binary files /dev/null and b/projects/Rotation.sb3 differ diff --git a/projects/Rotation2-medium.sb3 b/projects/Rotation2-medium.sb3 new file mode 100644 index 0000000..d4b0fcd Binary files /dev/null and b/projects/Rotation2-medium.sb3 differ diff --git a/projects/Rotation2-small.sb3 b/projects/Rotation2-small.sb3 new file mode 100644 index 0000000..486abab Binary files /dev/null and b/projects/Rotation2-small.sb3 differ diff --git a/projects/Rotation2.sb3 b/projects/Rotation2.sb3 new file mode 100644 index 0000000..9cb388d Binary files /dev/null and b/projects/Rotation2.sb3 differ diff --git a/projects/RotoZoom.sb3 b/projects/RotoZoom.sb3 new file mode 100644 index 0000000..4c570ec Binary files /dev/null and b/projects/RotoZoom.sb3 differ diff --git a/projects/Time.sb3 b/projects/Time.sb3 new file mode 100644 index 0000000..d57e686 Binary files /dev/null and b/projects/Time.sb3 differ diff --git a/projects/VariableStorage.sb3 b/projects/VariableStorage.sb3 new file mode 100644 index 0000000..aad905c Binary files /dev/null and b/projects/VariableStorage.sb3 differ diff --git a/projects/WaitUntil.sb3 b/projects/WaitUntil.sb3 new file mode 100644 index 0000000..e152547 Binary files /dev/null and b/projects/WaitUntil.sb3 differ diff --git a/sb3Unpacker.py b/sb3Unpacker.py index df8f3a6..6657226 100644 --- a/sb3Unpacker.py +++ b/sb3Unpacker.py @@ -9,7 +9,7 @@ import zipfile as zf import json import config -import target, costume, sound, block, variable, monitor # , broadcast +import target, costume, sound, block, monitor # , broadcast from pathlib import Path import io import pygame @@ -51,8 +51,10 @@ def sb3Unpack(sb3): t.y = targetObj["y"] t.direction = targetObj["direction"] t.size = targetObj["size"] + t.rotationStyle = targetObj["rotationStyle"] t.currentCostume = targetObj["currentCostume"] t.isStage = targetObj["isStage"] + t.layerOrder = targetObj["layerOrder"] t.name = targetObj["name"] # Get costumes @@ -62,6 +64,7 @@ def sb3Unpack(sb3): c.md5ext = costumeObj["md5ext"] c.rotationCenterX, c.rotationCenterY = costumeObj["rotationCenterX"], costumeObj["rotationCenterY"] c.dataFormat = costumeObj["dataFormat"] + c.offset = pygame.math.Vector2(c.rotationCenterX, c.rotationCenterY) c.file = project.read(costumeObj["assetId"] + "." + costumeObj["dataFormat"]) c.name = costumeObj["name"] if costumeObj["dataFormat"] != "svg": @@ -82,6 +85,10 @@ def sb3Unpack(sb3): s.name = soundObj["name"] t.sounds.append(s) + # Get variables + for variable in targetObj["variables"]: + t.variables[variable] = targetObj["variables"][variable] + # Set blocks to their correct values for blockId, blockObj in targetObj["blocks"].items(): b = block.Block() diff --git a/scratch.py b/scratch.py index 21ca7f7..73b0b7a 100644 --- a/scratch.py +++ b/scratch.py @@ -14,6 +14,15 @@ import bs4 import time from datetime import datetime +import eventContainer +from pathlib import Path +import tkinter as tk +import downloader +from tkinter import filedialog +import tkinter.simpledialog + +__version__ = "v0.8.0" +__author__ = "Secret-chest" i18n.set("locale", config.language) i18n.set("filename_format", "{locale}.{format}") @@ -31,9 +40,42 @@ class SpriteNotFoundError(Exception): sys.stdout = open(os.devnull, "w") +# Start tkinter for showing some popups, and hide main window +mainWindow = tk.Tk() +mainWindow.withdraw() + + +# Get project file name based on options and arguments +if len(sys.argv) > 1: + setProject = sys.argv[1] +else: + if config.testMode: + if not config.projectFileName.endswith(".sb3"): + if "http" not in config.projectFileName\ + or "https" not in config.projectFileName: + setProject = downloader.downloadByID(config.projectFileName, "./download") + else: + setProject = downloader.downloadByURL(config.projectFileName, "./download") + else: + setProject = config.projectFileName + else: + fileTypes = [(_("sb3-desc"), ".sb3"), (_("all-files-desc"), ".*")] + setProject = filedialog.askopenfilename(parent=mainWindow, + initialdir=os.getcwd(), + title=_("choose-project-title"), + filetypes=fileTypes) + + HEIGHT = config.projectScreenHeight WIDTH = config.projectScreenWidth +# Create project player and window +projectName = Path(setProject).stem +icon = pygame.image.load("icon.svg") +display = pygame.display.set_mode([WIDTH, HEIGHT]) +pygame.display.set_caption(_("window-title", projectName=projectName, s2pVersionString="Scratch2Python " + __version__)) +pygame.display.set_icon(icon) + # Key maps to convert the key option in blocks to Pygame constants KEY_MAPPING = { "up arrow": pygame.K_UP, @@ -151,18 +193,21 @@ def getStage(): # Run the given block object -def execute(block, s, keys=set(), keyEvents=set()): +def execute(block, s, events=eventContainer.EventContainer()): # Get block values opcode = block.opcode - id = block.blockID blockRan = block.blockRan inputs = block.inputs fields = block.fields shadow = block.shadow nextBlock = None + # Get keys + keys = events.keys + keyEvents = events.keyEvents + if opcode == "motion_gotoxy": # go to x: () y: () - s.setXy(int(block.getInputValue("x")), int(block.getInputValue("y"))) + s.setXy(float(block.getInputValue("x", eventContainer=events)), float(block.getInputValue("y", eventContainer=events))) elif opcode == "motion_goto": nextBlock = block.getBlockInputValue("to") @@ -190,31 +235,93 @@ def execute(block, s, keys=set(), keyEvents=set()): return elif opcode == "motion_setx": # set x to () - s.setXy(int(block.getInputValue("x")), s.y) + s.setXy(float(block.getInputValue("x", eventContainer=events)), s.y) elif opcode == "motion_changexby": # change x by ( ) - s.setXyDelta(int(block.getInputValue("dx")), 0) + s.setXyDelta(float(block.getInputValue("dx", eventContainer=events)), 0) elif opcode == "motion_sety": # set y to () - s.setXy(s.x, int(block.getInputValue("y"))) + s.setXy(s.x, float(block.getInputValue("y", eventContainer=events))) elif opcode == "motion_changeyby": # change y by () - s.setXyDelta(0, int(block.getInputValue("dy"))) + s.setXyDelta(0, float(block.getInputValue("dy", eventContainer=events))) + + elif opcode == "motion_turnleft": # turn ccw () degrees + s.setRotDelta(0 - float(block.getInputValue("degrees", eventContainer=events))) + + elif opcode == "motion_turnright": # turn cw () degrees + s.setRotDelta(float(block.getInputValue("degrees", eventContainer=events))) + + elif opcode == "motion_pointindirection": # point in direction () + s.setRot(float(block.getInputValue("direction", eventContainer=events))) + + elif opcode == "looks_changesizeby": # turn cw () degrees + s.setSizeDelta(float(block.getInputValue("change", eventContainer=events))) + + elif opcode == "looks_setsizeto": # turn cw () degrees + s.setSize(float(block.getInputValue("size", eventContainer=events))) + + elif opcode == "motion_pointindirection": # point in direction () + s.setRot(float(block.getInputValue("direction", eventContainer=events))) + + elif opcode == "motion_pointtowards": + nextBlock = block.getBlockInputValue("towards") + return s.target.blocks[nextBlock] + + elif opcode == "motion_pointtowards_menu": + if block.getFieldValue("towards") == "_mouse_": # go to [mouse pointer v] + newX, newY = pygame.mouse.get_pos() + newX = newX - WIDTH // 2 + newY = HEIGHT // 2 - newY + s.pointTowards(newX, newY) + if s.target.blocks[block.parent].next: + return s.target.blocks[s.target.blocks[block.parent].next] + return + + elif block.getFieldValue("towards") == "_random_": # go to [random position v] + minX = 0 - WIDTH // 2 + maxX = WIDTH // 2 + minY = 0 - HEIGHT // 2 + maxY = HEIGHT // 2 + newX, newY = (random.randint(minX, maxX), random.randint(minY, maxY)) + s.setXy(newX, newY) + if s.target.blocks[block.parent].next: + return s.target.blocks[s.target.blocks[block.parent].next] + return + + # TODO: sprite lookup table + + elif opcode == "motion_movesteps": # move () steps + offset = pygame.math.Vector2(float(block.getInputValue("steps", eventContainer=events)), 0) + offset.rotate_ip(90 + s.direction) + s.setXyDelta(-offset.x, -offset.y) elif opcode == "control_wait": # wait () seconds block.screenRefresh = True if not block.waiting: # Get time delay and convert it to milliseconds - block.timeDelay = int(round(float(float(block.getInputValue("duration"))) * 1000)) + block.timeDelay = int(round(float(float(block.getInputValue("duration", eventContainer=events))) * 1000)) block.waiting = True block.executionTime = 0 print(_("debug-prefix"), _("block-waiting", time=block.timeDelay), file=sys.stderr) return block + elif opcode == "control_wait_until": # wait until <> + block.screenRefresh = True + truth = block.target.blocks[inputs["CONDITION"][1]].evaluateBlockValue(events) + if truth: + block.blockRan = True + nextBlock = s.target.blocks[block.next] + return nextBlock + else: + return block + elif opcode == "event_whenflagclicked": # when green flag clicked pass elif opcode == "event_whenkeypressed": + # print(time.time_ns(), "in whenkeypressed") + # print("Handling key event") # if not block.waiting: # # Get time delay and convert it to milliseconds @@ -226,25 +333,8 @@ def execute(block, s, keys=set(), keyEvents=set()): # print(key) if key == "any": # when key [any v] pressed - # TODO any key - - pass - - elif KEY_MAPPING[key] in keys and block.next: # when key [. . . v] pressed - if KEY_MAPPING[key] in keys: - if key == "left arrow": - keyName = _("key-left") - elif key == "right arrow": - keyName = _("key-right") - elif key == "up arrow": - keyName = _("key-up") - elif key == "down arrow": - keyName = _("key-down") - elif key == "space": - keyName = _("key-space") - else: - keyName = key - print(_("debug-prefix"), _("keypress-handling", keyName=keyName), file=sys.stderr) + if keyEvents and keys and block.next: + print(_("debug-prefix"), _("keypress-handling", keyName=_("key-any")), file=sys.stderr) # print(time.time_ns() // 1000000, keyName) for b in block.script: s.target.blocks[b].blockRan = False @@ -265,12 +355,48 @@ def execute(block, s, keys=set(), keyEvents=set()): if nb: block.script.add(nb.blockID) block.script.remove(block.blockID) - print("script:", block.script) nb.blockRan = False nextBlock = s.target.blocks[block.next] return nextBlock + elif KEY_MAPPING[key] in keyEvents and block.next: # when key [. . . v] pressed + # print(keyEvents, "received in execute()") + if key == "left arrow": + keyName = _("key-left") + elif key == "right arrow": + keyName = _("key-right") + elif key == "up arrow": + keyName = _("key-up") + elif key == "down arrow": + keyName = _("key-down") + elif key == "space": + keyName = _("key-space") + else: + keyName = key + print(_("debug-prefix"), _("keypress-handling", keyName=keyName), file=sys.stderr) + # print(time.time_ns() // 1000000, keyName) + for b in block.script: + s.target.blocks[b].blockRan = False + nb = block # s.target.blocks[block.next] + # nb.blockRan = False + block.script.add(nb.blockID) + nb = s.target.blocks[nb.next] + while nb.next and nb.next != block.blockID: + # Reset block + nb.blockRan = False + nb.timeDelay = 0 + nb.executionTime = 0 + + block.script.add(nb.blockID) + nb = s.target.blocks[nb.next] + if not nb.next: + nb.next = block.blockID + if nb: + block.script.add(nb.blockID) + block.script.remove(block.blockID) + nb.blockRan = False + nextBlock = s.target.blocks[block.next] + return nextBlock else: - # print(f"Unknown event: { key } in { keyEvents }, all keys: { keys }") pass block.blockRan = False @@ -301,7 +427,7 @@ def execute(block, s, keys=set(), keyEvents=set()): elif opcode == "control_repeat": # repeat (10) {...} if block.repeatCounter is None: - block.repeatCounter = int(block.getInputValue("times")) + block.repeatCounter = int(block.getInputValue("times", eventContainer=events)) # Don't mark the loop as ran until done, and do a screen refresh if block.repeatCounter > 0: block.blockRan = False @@ -335,6 +461,73 @@ def execute(block, s, keys=set(), keyEvents=set()): nb.next = block.blockID return nextBlock + elif opcode == "control_if": # if <> then {...} + if block.target.blocks[inputs["CONDITION"][1]].evaluateBlockValue(events): + # If there are blocks, get them + if inputs["SUBSTACK"][1]: + # No blocks will be flagged as ran inside a forever loop + for b in block.substack: + s.target.blocks[b].blockRan = False + nextBlock = s.target.blocks[inputs["SUBSTACK"][1]] + nb = s.target.blocks[inputs["SUBSTACK"][1]] + block.substack.add(nb.blockID) + while nb.next and nb.next not in block.substack: + nb.blockRan = False + nb.waiting = False + nb.timeDelay = 0 + nb.executionTime = 0 + nb = s.target.blocks[nb.next] + block.substack.add(nb.blockID) + nb.next = block.next + block.blockRan = True + return nextBlock + block.blockRan = True + else: + block.blockRan = True + return s.target.blocks[block.next] + + elif opcode == "control_if_else": # if <> then {...} + if block.target.blocks[inputs["CONDITION"][1]].evaluateBlockValue(events): + # If there are blocks, get them + if inputs["SUBSTACK"][1]: + # No blocks will be flagged as ran inside a forever loop + for b in block.substack: + s.target.blocks[b].blockRan = False + nextBlock = s.target.blocks[inputs["SUBSTACK"][1]] + nb = s.target.blocks[inputs["SUBSTACK"][1]] + block.substack.add(nb.blockID) + while nb.next and nb.next not in block.substack: + nb.blockRan = False + nb.waiting = False + nb.timeDelay = 0 + nb.executionTime = 0 + nb = s.target.blocks[nb.next] + block.substack.add(nb.blockID) + nb.next = block.next + block.blockRan = True + return nextBlock + block.blockRan = True + else: + # If there are blocks, get them + if inputs["SUBSTACK2"][1]: + # No blocks will be flagged as ran inside a forever loop + for b in block.substack2: + s.target.blocks[b].blockRan = False + nextBlock = s.target.blocks[inputs["SUBSTACK2"][1]] + nb = s.target.blocks[inputs["SUBSTACK2"][1]] + block.substack2.add(nb.blockID) + while nb.next and nb.next not in block.substack2: + nb.blockRan = False + nb.waiting = False + nb.timeDelay = 0 + nb.executionTime = 0 + nb = s.target.blocks[nb.next] + block.substack2.add(nb.blockID) + nb.next = block.next + block.blockRan = True + return nextBlock + block.blockRan = True + elif opcode == "looks_switchcostumeto": # switch costume to [... v] nextBlock = block.getBlockInputValue("costume") return s.target.blocks[nextBlock] @@ -398,9 +591,9 @@ def execute(block, s, keys=set(), keyEvents=set()): if block.proccode == "​​log​​ %s": # Scratch Addons log () print("[", datetime.now().strftime("%H:%M:%S:%f"), "]", _("project-log"), block.getCustomInputValue(0), file=sys.stderr) elif block.proccode == "​​warn​​ %s": # Scratch Addons warn () - print(_("project-warn"), block.getCustomInputValue(0), file=sys.stderr) + print("[", datetime.now().strftime("%H:%M:%S:%f"), "]", _("project-warn"), block.getCustomInputValue(0), file=sys.stderr) elif block.proccode == "​​error​​ %s": # Scratch Addons error () - print(_("project-error"), block.getCustomInputValue(0), file=sys.stderr) + print("[", datetime.now().strftime("%H:%M:%S:%f"), "]", _("project-error"), block.getCustomInputValue(0), file=sys.stderr) else: print(_("unknown-opcode"), opcode) diff --git a/sprite.png b/sprite.png new file mode 100644 index 0000000..498e7bc Binary files /dev/null and b/sprite.png differ diff --git a/target.py b/target.py index e1cefe6..8faf2c3 100644 --- a/target.py +++ b/target.py @@ -27,3 +27,14 @@ def __init__(self): self.rotationStyle = "all around" # all around, left-right or do not rotate self.sprite = None self.name = "" + + # Variable functions + def getVariableValue(self, varId): + return self.variables[varId][1] + + def getVariableName(self, varId): + return self.variables[varId][0] + + def setVariableValue(self, varId, newValue): + self.variables[varId][1] = newValue + return newValue diff --git a/targetSprite.py b/targetSprite.py index e32ddb8..d17b39a 100644 --- a/targetSprite.py +++ b/targetSprite.py @@ -4,7 +4,7 @@ Targets as pygame sprites """ import time - +import math import pygame import cairosvg import io @@ -12,6 +12,8 @@ import config import sys import i18n +import copy +import random i18n.set("locale", config.language) i18n.set("filename_format", "{locale}.{format}") @@ -20,44 +22,52 @@ sprites = set() +# Disable the print function +def decorator(func): + def disabledPrint(*args, **kwargs): + if not config.disablePrint: + func(*args, **kwargs) + return disabledPrint +print = decorator(print) class TargetSprite(pygame.sprite.Sprite): def __init__(self, target): sprites.add(self) pygame.sprite.Sprite.__init__(self) - self.padX = 0 - self.padY = 0 self.target = target self.target.currentCostume = target.currentCostume # Load costume if target.costumes[self.target.currentCostume].dataFormat != "svg": + self.isBitmap = True sprite = pygame.image.load(io.BytesIO(target.costumes[target.currentCostume].file)) initialWidth = sprite.get_width() initialHeight = sprite.get_height() - sprite = pygame.transform.smoothscale(sprite, (sprite.get_width() // target.costumes[target.currentCostume].bitmapResolution, sprite.get_height() // target.costumes[target.currentCostume].bitmapResolution)) - # self.padX = initialWidth - sprite.get_width() - # self.padY = initialHeight - sprite.get_height() + sprite = pygame.transform.smoothscale(sprite, (initialWidth / 2, initialHeight / 2)) else: + self.isBitmap = False sprite = scratch.loadSvg(target.costumes[target.currentCostume].file) - sprite = pygame.transform.rotate(sprite, 90 - target.direction) - self.x = target.x + self.padX // 2 - self.y = target.y - self.padY // 2 + self.x = target.x + self.y = target.y + self.direction = target.direction self.size = target.size - self.image = sprite + self.sprite = sprite + self.image = self.sprite.copy() self.rect = self.image.get_rect() + self.spriteRect = self.sprite.get_rect() self.isStage = target.isStage + self.rotationStyle = target.rotationStyle + self.imageSize = sprite.get_size() + self.flipped = False + self.layerOrder = target.layerOrder if self.target.name == "Stage": self.name = _("stage") else: self.name = self.target.name - self.setXy(self.x, self.y) - # # Convert Scratch coordinates into Pygame coordinates - # self.rect.x = (self.x + scratch.WIDTH // 2 - self.target.costumes[self.target.currentCostume].rotationCenterX) - # self.rect.y = (scratch.HEIGHT // 2 - self.y - self.target.costumes[self.target.currentCostume].rotationCenterY) - # pygame.transform.scale(self.image, (int(round(self.rect.width * self.size / 100)), int(round(self.rect.height * self.size / 100)))) - # - # print(_("costumes-count", sprite=self.name, costumes=len(self.target.costumes))) + self.setXy(self.x, self.y) + self.setRot(self.direction) + self.setSize(self.size) + print(self.rect.size) # Set self position def setXy(self, x, y): @@ -84,19 +94,88 @@ def setXy(self, x, y): y = scratch.HEIGHT / 2 + self.rect.height / 2 - 16 elif y < scratch.HEIGHT / -2 - self.rect.height / 2 + 16: y = scratch.HEIGHT / -2 - self.rect.height / 2 + 16 - # Set X and Y - self.x = x + self.padX // 2 - self.y = y - self.padY // 2 - print(_("debug-prefix"), _("new-sprite-position", x=x, y=y, name=self.name), file=sys.stderr) - self.rect.x = self.x + scratch.WIDTH // 2 - round(self.target.costumes[self.target.currentCostume].rotationCenterX) - self.rect.y = scratch.HEIGHT // 2 - self.y - round(self.target.costumes[self.target.currentCostume].rotationCenterY) - - # Move + + self.x = x + self.y = y + # print(_("debug-prefix"), _("new-sprite-position", x=x, y=y, name=self.name), file=sys.stderr) + #rect = self.sprite.get_rect(topleft=(self.x - self.target.costumes[self.target.currentCostume].rotationCenterX, self.y - self.target.costumes[self.target.currentCostume].rotationCenterY)) + if not self.isStage: + self.image = pygame.transform.smoothscale(self.sprite, (self.size / 100 * self.imageSize[0], self.size / 100 * self.imageSize[1])) + else: + self.image = self.sprite + + offset = self.target.costumes[self.target.currentCostume].offset.elementwise() * self.size / 100 - pygame.math.Vector2(self.sprite.get_width() / 2, self.sprite.get_height() / 2).elementwise() * self.size / 100 + + if self.rotationStyle == "all around": + self.image = pygame.transform.rotate(self.image, 90 - self.direction) + offset.rotate_ip(90 + self.direction) + elif self.rotationStyle == "left-right": + angle = self.direction % 360 + print(angle, self.flipped) + if angle > 180: + self.flipped = True + else: + self.flipped = False + if self.flipped: + self.image = pygame.transform.flip(self.image, True, False) + offset = self.target.costumes[self.target.currentCostume].offset.elementwise() * self.size / 100 - pygame.math.Vector2(self.sprite.get_width() / 2, self.sprite.get_height() / 2).elementwise() * self.size / 100 + else: + offset = pygame.math.Vector2(-self.target.costumes[self.target.currentCostume].offset.x, self.target.costumes[self.target.currentCostume].offset.y).elementwise() * self.size / 100 - pygame.math.Vector2(-self.sprite.get_width() / 2, self.sprite.get_height() / 2).elementwise() * self.size / 100 + else: + self.image = self.image + + relativePosition = pygame.math.Vector2(self.spriteRect.centerx, self.spriteRect.centery) + position = pygame.math.Vector2(self.x - self.sprite.get_width() / 2 + scratch.WIDTH / 2, self.y - self.sprite.get_height() / 2 + scratch.HEIGHT / 2) + + if not self.isStage: + self.rect = self.image.get_rect(center=position+relativePosition+offset) + else: + self.rect = self.image.get_rect(center=(scratch.WIDTH / 2, scratch.HEIGHT / 2)) + + # Relatively set self position def setXyDelta(self, dx, dy): x = self.x + dx y = self.y + dy self.setXy(x, y) + # Set self rotation + def setRot(self, rot): + self.direction = rot + print(_("debug-prefix"), _("new-sprite-rotation", rot=rot, name=self.name), file=sys.stderr) + + self.setXy(self.x, self.y) + + # Relatively set self rotation (turn) + def setRotDelta(self, drot): + rot = self.direction + drot + self.setRot(rot) + + def pointTowards(self, x, y): + if self.y == y: + if self.y < y: + direction = -90 + else: + direction = 90 + else: + if self.y < y: + direction = math.degrees(math.atan(((self.x - x) / (self.y - y)))) + else: + direction = math.degrees(math.atan(((self.x - x) / (self.y - y)))) + 180 + + self.setRot(direction) + + # Set self rotation + def setSize(self, size): + self.size = size + print(_("debug-prefix"), _("new-sprite-size", size=size, name=self.name), file=sys.stderr) + + self.setXy(self.x, self.y) + + # Relatively set self rotation (turn) + def setSizeDelta(self, dsize): + size = self.size + dsize + self.setSize(size) + # Change costume def setCostume(self, costumeId): self.target.currentCostume = costumeId % len(self.target.costumes) @@ -107,10 +186,9 @@ def setCostume(self, costumeId): initialWidth = sprite.get_width() initialHeight = sprite.get_height() sprite = pygame.transform.smoothscale(sprite, (sprite.get_width() // self.target.costumes[self.target.currentCostume].bitmapResolution, sprite.get_height() // self.target.costumes[self.target.currentCostume].bitmapResolution)) - self.padX = initialWidth - sprite.get_width() - self.padY = initialHeight - sprite.get_height() else: sprite = scratch.loadSvg(self.target.costumes[self.target.currentCostume].file) self.image = sprite + self.imageSize = sprite.get_size() self.rect = self.image.get_rect() self.setXy(self.x, self.y) diff --git a/test-gtk.py b/test-gtk.py new file mode 100644 index 0000000..d9a1ba5 --- /dev/null +++ b/test-gtk.py @@ -0,0 +1,108 @@ +import pygame +import os +from gi.repository import GObject +from gi.repository import Gtk +from gi.repository import GdkX11 + +class GameWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self) + vbox = Gtk.VBox(False, 2) + vbox.show() + self.add(vbox) + + #create the menu + file_menu = Gtk.Menu() + + accel_group = Gtk.AccelGroup() + self.add_accel_group(accel_group) + + dialog_item = Gtk.MenuItem() + dialog_item.set_label("Dialog") + dialog_item.show() + dialog_item.connect("activate",self.show_dialog) + file_menu.append(dialog_item) + dialog_item.show() + + quit_item = Gtk.MenuItem() + quit_item.set_label("Quit") + quit_item.show() + quit_item.connect("activate",self.quit) + file_menu.append(quit_item) + quit_item.show() + + menu_bar = Gtk.MenuBar() + vbox.pack_start(menu_bar, False, False, 0) + menu_bar.show() + + file_item = Gtk.MenuItem() + file_item.set_label("_File") + file_item.set_use_underline(True) + file_item.show() + + file_item.set_submenu(file_menu) + menu_bar.append(file_item) + + #create the drawing area + da = Gtk.DrawingArea() + da.set_size_request(300,300) + da.show() + vbox.pack_end(da, False, False, 0) + da.connect("realize",self._realized) + + #set up the pygame objects + self.image = pygame.image.load("sprite.png") + self.background = pygame.image.load("background.png") + self.x = 150 + self.y = 150 + + #collect key press events + self.connect("key-press-event", self.key_pressed) + + def key_pressed(self, widget, event, data=None): + if event.keyval == 65361: + self.x -= 5 + elif event.keyval == 65362: + self.y -= 5 + elif event.keyval == 65363: + self.x += 5 + elif event.keyval == 65364: + self.y += 5 + + def show_dialog(self, widget, data=None): + #prompts.info("A Pygtk Dialog", "See it works easy") + title = "PyGame embedded in Gtk Example" + dialog = Gtk.Dialog(title, None, Gtk.DialogFlags.MODAL,(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) + content_area = dialog.get_content_area() + label = Gtk.Label("See, it still works") + label.show() + content_area.add(label) + response = dialog.run() + dialog.destroy() + + def quit(self, widget, data=None): + self.destroy() + + def draw(self): + self.screen.blit(self.background,[0,0]) + + rect = self.image.get_rect() + rect.x = self.x + rect.y = self.y + self.screen.blit(self.image, rect) + pygame.display.flip() + + return True + + def _realized(self, widget, data=None): + os.putenv('SDL_WINDOWID', str(widget.get_window().get_xid())) + pygame.init() + pygame.display.set_mode((300, 300), 0, 0) + self.screen = pygame.display.get_surface() + GObject.timeout_add(200, self.draw) + +if __name__ == "__main__": + window = GameWindow() + window.connect("destroy",Gtk.main_quit) + window.show() + Gtk.main() diff --git a/testPygameKeyEvents.py b/testPygameKeyEvents.py new file mode 100644 index 0000000..044811b --- /dev/null +++ b/testPygameKeyEvents.py @@ -0,0 +1,18 @@ +import time +import pygame + +pygame.init() +pygame.key.set_repeat(2000, 1000 // 30) + +pygame.display.set_mode((240, 180)) + +lastTime = time.time_ns() + +if __name__ == "__main__": + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + exit() + if event.type == pygame.KEYDOWN: + print(event.key, "+" + str((time.time_ns() - lastTime) // 1000000)) + lastTime = time.time_ns() diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..d6c00f0 --- /dev/null +++ b/tests.py @@ -0,0 +1,63 @@ +import pygame as pg +from pygame.math import Vector2 + + +class Entity(pg.sprite.Sprite): + + def __init__(self, pos): + super().__init__() + self.image = pg.Surface((122, 70), pg.SRCALPHA) + pg.draw.polygon(self.image, pg.Color('dodgerblue1'), + ((1, 0), (120, 35), (1, 70))) + # A reference to the original image to preserve the quality. + self.orig_image = self.image + self.rect = self.image.get_rect(center=pos) + self.pos = Vector2(pos) # The original center position/pivot point. + self.offset = Vector2(50, 0) # We shift the sprite 50 px to the right. + self.angle = 0 + + def update(self): + self.angle += 2 + self.rotate() + + def rotate(self): + """Rotate the image of the sprite around a pivot point.""" + # Rotate the image. + self.image = pg.transform.rotozoom(self.orig_image, -self.angle, 1) + # Rotate the offset vector. + offset_rotated = self.offset.rotate(self.angle) + # Create a new rect with the center of the sprite + the offset. + self.rect = self.image.get_rect(center=self.pos+offset_rotated) + + +def main(): + screen = pg.display.set_mode((640, 480)) + clock = pg.time.Clock() + entity = Entity((320, 240)) + all_sprites = pg.sprite.Group(entity) + + while True: + for event in pg.event.get(): + if event.type == pg.QUIT: + return + + keys = pg.key.get_pressed() + if keys[pg.K_d]: + entity.pos.x += 5 + elif keys[pg.K_a]: + entity.pos.x -= 5 + + all_sprites.update() + screen.fill((30, 30, 30)) + all_sprites.draw(screen) + pg.draw.circle(screen, (255, 128, 0), [int(i) for i in entity.pos], 3) + pg.draw.rect(screen, (255, 128, 0), entity.rect, 2) + pg.draw.line(screen, (100, 200, 255), (0, 240), (640, 240), 1) + pg.display.flip() + clock.tick(30) + + +if __name__ == '__main__': + pg.init() + main() + pg.quit() diff --git a/variable.py b/variable.py deleted file mode 100644 index 3ada38f..0000000 --- a/variable.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Variable class - -======= CLASS INFO ======= -The various files with classes are used by s2p_unpacker and the correct data is -set. Those are then used to build the project in main.py. -""" - - -class Variable: - def __init__(self): - self.id = None - self.name = "" - self.value = 0