diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f43a6897..46d0d8cb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,7 +21,7 @@ jobs:
     - name: Install dependencies
       run: |
         sudo apt-get update
-        sudo apt-get install -y nlohmann-json3-dev libutfcpp-dev
+        sudo apt-get install -y nlohmann-json3-dev libutfcpp-dev libgd-dev
       shell: bash
     - name: Configure CMake
       run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
diff --git a/.github/workflows/utests.yml b/.github/workflows/utests.yml
index 9eefb482..9828530c 100644
--- a/.github/workflows/utests.yml
+++ b/.github/workflows/utests.yml
@@ -21,7 +21,7 @@ jobs:
     - name: Install dependencies
       run: |
         sudo apt-get update
-        sudo apt-get install -y nlohmann-json3-dev libutfcpp-dev
+        sudo apt-get install -y nlohmann-json3-dev libutfcpp-dev libgd-dev
       shell: bash
     - name: Configure CMake
       run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DLIBSCRATCHCPP_BUILD_UNIT_TESTS=ON
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bb155ab7..3bcdaf5b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -47,6 +47,11 @@ target_sources(scratchcpp
     include/scratchcpp/stage.h
     include/scratchcpp/sprite.h
     include/scratchcpp/itimer.h
+    include/scratchcpp/keyevent.h
+    include/scratchcpp/iimageformat.h
+    include/scratchcpp/iimageformatfactory.h
+    include/scratchcpp/rect.h
+    include/scratchcpp/igraphicseffect.h
 )
 
 add_library(zip SHARED
@@ -58,6 +63,8 @@ include_directories(thirdparty/zip/src)
 
 include_directories(thirdparty/spimpl)
 
+include(build/FindGD.cmake)
+
 target_link_libraries(scratchcpp PRIVATE nlohmann_json::nlohmann_json)
 target_link_libraries(scratchcpp PRIVATE utf8cpp)
 target_link_libraries(scratchcpp PRIVATE zip)
diff --git a/Doxyfile b/Doxyfile
index 36ced89e..1edbd141 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -975,6 +975,7 @@ EXCLUDE_SYMLINKS       = NO
 
 EXCLUDE_PATTERNS       = */internal/* \
                          */src/blocks/* \
+                         */src/imageformats/* \
                          *_p.cpp \
                          *_p.h
 
diff --git a/build/FindGD.cmake b/build/FindGD.cmake
new file mode 100644
index 00000000..2860d6cc
--- /dev/null
+++ b/build/FindGD.cmake
@@ -0,0 +1,121 @@
+# - Find GD
+# Find the native GD includes and library
+# This module defines
+#  GD_INCLUDE_DIR, where to find gd.h, etc.
+#  GD_LIBRARIES, the libraries needed to use GD.
+#  GD_FOUND, If false, do not try to use GD.
+# also defined, but not for general use are
+#  GD_LIBRARY, where to find the GD library.
+#  GD_SUPPORTS_PNG, GD_SUPPORTS_JPEG, GD_SUPPORTS_GIF, test
+#  support for image formats in GD.
+
+FIND_PATH(GD_INCLUDE_DIR gd.h
+/usr/local/include
+/usr/include
+)
+
+if(WIN32 AND NOT CYGWIN)
+  SET(GD_NAMES ${GD_NAMES} bgd)
+else(WIN32)
+  SET(GD_NAMES ${GD_NAMES} gd)
+endif(WIN32 AND NOT CYGWIN)
+
+FIND_LIBRARY(GD_LIBRARY
+  NAMES ${GD_NAMES}
+  PATHS /usr/lib64 /usr/lib /usr/local/lib
+  )
+
+IF (GD_LIBRARY AND GD_INCLUDE_DIR)
+    SET(GD_LIBRARIES ${GD_LIBRARY})
+    SET(GD_FOUND "YES")
+ELSE (GD_LIBRARY AND GD_INCLUDE_DIR)
+  SET(GD_FOUND "NO")
+ENDIF (GD_LIBRARY AND GD_INCLUDE_DIR)
+message("Found GD: ${GD_FOUND}")
+IF (GD_FOUND)
+	IF (WIN32 AND NOT CYGWIN)
+		SET(GD_SUPPORTS_PNG ON)
+		SET(GD_SUPPORTS_JPEG ON)
+		SET(GD_SUPPORTS_GIF ON)
+		get_filename_component(GD_LIBRARY_DIR ${GD_LIBRARY} PATH)
+	ELSE (WIN32 AND NOT CYGWIN)
+		INCLUDE(CheckLibraryExists)
+		GET_FILENAME_COMPONENT(GD_LIB_PATH ${GD_LIBRARY} PATH)
+		GET_FILENAME_COMPONENT(GD_LIB ${GD_LIBRARY} NAME)
+
+		CHECK_LIBRARY_EXISTS("${GD_LIBRARY}" "gdImagePng" "${GD_LIB_PATH}" GD_SUPPORTS_PNG)
+		IF (GD_SUPPORTS_PNG)
+			find_package(PNG)
+			IF (PNG_FOUND)
+				SET(GD_LIBRARIES ${GD_LIBRARIES} ${PNG_LIBRARIES})
+			SET(GD_INCLUDE_DIR ${GD_INCLUDE_DIR} ${PNG_INCLUDE_DIR})
+			ELSE (PNG_FOUND)
+				SET(GD_SUPPORTS_PNG "NO")
+			ENDIF (PNG_FOUND)
+		ENDIF (GD_SUPPORTS_PNG)
+
+		CHECK_LIBRARY_EXISTS("${GD_LIBRARY}" "gdImageJpeg" "${GD_LIB_PATH}" GD_SUPPORTS_JPEG)
+		IF (GD_SUPPORTS_JPEG)
+			find_package(JPEG)
+			IF (JPEG_FOUND)
+				SET(GD_LIBRARIES ${GD_LIBRARIES} ${JPEG_LIBRARIES})
+				SET(GD_INCLUDE_DIR ${GD_INCLUDE_DIR} ${JPEG_INCLUDE_DIR})
+			ELSE (JPEG_FOUND)
+				SET(GD_SUPPORTS_JPEG "NO")
+			ENDIF (JPEG_FOUND)
+		ENDIF (GD_SUPPORTS_JPEG)
+
+		CHECK_LIBRARY_EXISTS("${GD_LIBRARY}" "gdImageGif" "${GD_LIB_PATH}" GD_SUPPORTS_GIF)
+
+		# Trim the list of include directories
+		SET(GDINCTRIM)
+		FOREACH(GD_DIR ${GD_INCLUDE_DIR})
+			SET(GD_TMP_FOUND OFF)
+			FOREACH(GD_TRIMMED ${GDINCTRIM})
+				IF ("${GD_DIR}" STREQUAL "${GD_TRIMMED}")
+				SET(GD_TMP_FOUND ON)
+				ENDIF ("${GD_DIR}" STREQUAL "${GD_TRIMMED}")
+			ENDFOREACH(GD_TRIMMED ${GDINCTRIM})
+			IF (NOT GD_TMP_FOUND)
+				SET(GDINCTRIM "${GDINCTRIM}" "${GD_DIR}")
+			ENDIF (NOT GD_TMP_FOUND)
+		ENDFOREACH(GD_DIR ${GD_INCLUDE_DIR})
+		SET(GD_INCLUDE_DIR ${GDINCTRIM})
+
+		SET(GD_LIBRARY_DIR)
+
+		# Generate trimmed list of library directories and list of libraries
+		FOREACH(GD_LIB ${GD_LIBRARIES})
+			GET_FILENAME_COMPONENT(GD_NEXTLIBDIR ${GD_LIB} PATH)
+			SET(GD_TMP_FOUND OFF)
+			FOREACH(GD_LIBDIR ${GD_LIBRARY_DIR})
+				IF ("${GD_NEXTLIBDIR}" STREQUAL "${GD_LIBDIR}")
+					SET(GD_TMP_FOUND ON)
+				ENDIF ("${GD_NEXTLIBDIR}" STREQUAL "${GD_LIBDIR}")
+			ENDFOREACH(GD_LIBDIR ${GD_LIBRARIES})
+			IF (NOT GD_TMP_FOUND)
+				SET(GD_LIBRARY_DIR "${GD_LIBRARY_DIR}" "${GD_NEXTLIBDIR}")
+			ENDIF (NOT GD_TMP_FOUND)
+		ENDFOREACH(GD_LIB ${GD_LIBRARIES})
+	ENDIF (WIN32 AND NOT CYGWIN)
+ENDIF (GD_FOUND)
+
+IF (GD_FOUND)
+   IF (NOT GD_FIND_QUIETLY)
+      MESSAGE(STATUS "Found GD: ${GD_LIBRARY}")
+   ENDIF (NOT GD_FIND_QUIETLY)
+ELSE (GD_FOUND)
+   IF (GD_FIND_REQUIRED)
+      MESSAGE(FATAL_ERROR "Could not find GD library")
+   ENDIF (GD_FIND_REQUIRED)
+ENDIF (GD_FOUND)
+
+MARK_AS_ADVANCED(
+  GD_LIBRARY
+  GD_LIBRARIES
+  GD_INCLUDE_DIR
+  GD_LIBRARY_DIR
+  GD_SUPPORTS_PNG
+  GD_SUPPORTS_JPEG
+  GD_SUPPORTS_GIF
+)
diff --git a/docs/Graphics effects.md b/docs/Graphics effects.md
new file mode 100644
index 00000000..421f9b66
--- /dev/null
+++ b/docs/Graphics effects.md	
@@ -0,0 +1,38 @@
+\page graphicsEffects Graphics effects
+
+libscratchcpp provides API for implementing custom graphics effects.
+No effects are currently included with libscratchcpp, but that is planned to change.
+
+In case you've implemented some of the effects yourself, feel free to make pull requests!
+These are the issues (tasks) for the default graphics effects:
+
+- Color: https://github.com/scratchcpp/libscratchcpp/issues/283
+- Fisheye: https://github.com/scratchcpp/libscratchcpp/issues/284
+- Whirl: https://github.com/scratchcpp/libscratchcpp/issues/285
+- Pixelate: https://github.com/scratchcpp/libscratchcpp/issues/286
+- Mosaic: https://github.com/scratchcpp/libscratchcpp/issues/287
+- Brightness: https://github.com/scratchcpp/libscratchcpp/issues/288
+- Ghost: https://github.com/scratchcpp/libscratchcpp/issues/289
+
+# Implementing a graphics effect
+To implement a graphics effect that libscratchcpp doesn't support,
+subclass \link libscratchcpp::IGraphicsEffect IGraphicsEffect \endlink
+and override all of the methods.
+
+It's recommended to use the libscratchcpp namespace like this in your class:
+```cpp
+using namespace libscratchcpp;
+```
+
+The `name()` method should return the name of the effect which is
+used by the set and change effect blocks, for example `ghost` or `fisheye`.
+
+The `apply()` method is used to apply the effect on a bitmap. The bitmap
+can be accessed using the `bitmap` parameter and is writable.
+
+# Registering the graphics effect
+To register the graphics effect, use \link libscratchcpp::ScratchConfiguration::registerGraphicsEffect() ScratchConfiguration::registerGraphicsEffect() \endlink.
+
+```cpp
+libscratchcpp::ScratchConfiguration::registerGraphicsEffect(std::make_shared<MyGraphicsEffect>());
+```
diff --git a/docs/Image formats.md b/docs/Image formats.md
new file mode 100644
index 00000000..f1173bd1
--- /dev/null
+++ b/docs/Image formats.md	
@@ -0,0 +1,33 @@
+\page imageFormats Image formats
+
+To work with costumes, libscratchcpp has to read image files from Scratch projects.
+Only JPEG and PNG are currently supported, but SVG is going to be supported in the future.
+
+# Implementing a custom image format
+To implement a custom image format that libscratchcpp doesn't support,
+subclass \link libscratchcpp::IImageFormat IImageFormat \endlink
+and override all of the methods.
+
+Then subclass \link libscratchcpp::IImageFormatFactory IImageFormatFactory \endlink
+and override \link libscratchcpp::IImageFormatFactory::createInstance() createInstance() \endlink.
+
+It's recommended to use the libscratchcpp namespace like this in your classes:
+```cpp
+using namespace libscratchcpp;
+```
+
+Here's an example of the `createInstance()` method:
+```cpp
+std::shared_ptr<IImageFormat> TiffImageFormatFactory::createInstance() {
+    return std::make_shared<TiffImageFormat>();
+}
+```
+
+# Registering the image format
+To register the image format, you need to register the factory class.
+It will be used by classes such as \link libscratchcpp::Costume Costume \endlink
+internally to instantiate your image format.
+
+```cpp
+libscratchcpp::ScratchConfiguration::registerImageFormat("tiff", std::make_shared<TiffImageFormatFactory>());
+```
diff --git a/include/scratchcpp/asset.h b/include/scratchcpp/asset.h
index 0b2f04c9..975c3e16 100644
--- a/include/scratchcpp/asset.h
+++ b/include/scratchcpp/asset.h
@@ -28,6 +28,13 @@ class LIBSCRATCHCPP_EXPORT Asset : public Entity
 
         const std::string &dataFormat() const;
 
+        const void *data() const;
+        unsigned int dataSize() const;
+        void setData(unsigned int size, void *data);
+
+    protected:
+        virtual void processData(unsigned int size, void *data) { }
+
     private:
         spimpl::unique_impl_ptr<AssetPrivate> impl;
 };
diff --git a/include/scratchcpp/block.h b/include/scratchcpp/block.h
index 11660ab0..48c319cd 100644
--- a/include/scratchcpp/block.h
+++ b/include/scratchcpp/block.h
@@ -19,6 +19,8 @@ class BlockPrivate;
 class LIBSCRATCHCPP_EXPORT Block : public Entity
 {
     public:
+        friend class Engine;
+
         Block(const std::string &id, const std::string &opcode);
         Block(const Block &) = delete;
 
@@ -41,14 +43,12 @@ class LIBSCRATCHCPP_EXPORT Block : public Entity
         std::shared_ptr<Input> inputAt(int index) const;
         int findInput(const std::string &inputName) const;
         Input *findInputById(int id) const;
-        void updateInputMap();
 
         std::vector<std::shared_ptr<Field>> fields() const;
         int addField(std::shared_ptr<Field> field);
         std::shared_ptr<Field> fieldAt(int index) const;
         int findField(const std::string &fieldName) const;
         Field *findFieldById(int id) const;
-        void updateFieldMap();
 
         bool shadow() const;
         void setShadow(bool newShadow);
@@ -75,6 +75,9 @@ class LIBSCRATCHCPP_EXPORT Block : public Entity
         InputValue *topLevelReporterInfo();
 
     private:
+        void updateInputMap();
+        void updateFieldMap();
+
         spimpl::unique_impl_ptr<BlockPrivate> impl;
 };
 
diff --git a/include/scratchcpp/costume.h b/include/scratchcpp/costume.h
index 1436a139..fff291a7 100644
--- a/include/scratchcpp/costume.h
+++ b/include/scratchcpp/costume.h
@@ -2,6 +2,7 @@
 
 #pragma once
 
+#include "iimageformat.h"
 #include "spimpl.h"
 
 #include "asset.h"
@@ -10,6 +11,7 @@ namespace libscratchcpp
 {
 
 class Broadcast;
+class IGraphicsEffect;
 class CostumePrivate;
 
 /*! \brief The Costume class represents a Scratch costume. */
@@ -28,8 +30,27 @@ class LIBSCRATCHCPP_EXPORT Costume : public Asset
         int rotationCenterY() const;
         void setRotationCenterY(int newRotationCenterY);
 
+        unsigned int width() const;
+        unsigned int height() const;
+
+        double scale() const;
+        void setScale(double scale);
+
+        bool mirrorHorizontally() const;
+        void setMirrorHorizontally(bool mirror);
+
+        Rgb **bitmap() const;
+
+        double graphicsEffectValue(IGraphicsEffect *effect) const;
+        void setGraphicsEffectValue(IGraphicsEffect *effect, double value);
+
+        void clearGraphicsEffects();
+
         Broadcast *broadcast();
 
+    protected:
+        void processData(unsigned int size, void *data) override;
+
     private:
         spimpl::unique_impl_ptr<CostumePrivate> impl;
 };
diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index b497f1db..f83cddd1 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -81,6 +81,9 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Calls the "when I start as a clone" blocks of the given sprite. */
         virtual void initClone(Sprite *clone) = 0;
 
+        /*! Automatically called from clones that are being deleted. */
+        virtual void deinitClone(Sprite *clone) = 0;
+
         /*!
          * Runs the event loop and calls "when green flag clicked" blocks.
          * \note This function returns when all scripts finish.\n
@@ -98,6 +101,15 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Sets the framerate of the project. */
         virtual void setFps(double fps) = 0;
 
+        /*! Returns true if the given key is pressed. */
+        virtual bool keyPressed(const std::string &name) const = 0;
+
+        /*! Sets the state of the given key. */
+        virtual void setKeyState(const std::string &name, bool pressed) = 0;
+
+        /*! Sets whether any key is pressed (use this for any key, even for unsupported keys). */
+        virtual void setAnyKeyPressed(bool pressed) = 0;
+
         /*! Returns the X coordinate of the mouse pointer. */
         virtual double mouseX() const = 0;
 
@@ -110,6 +122,12 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Sets the Y coordinate of the mouse pointer. */
         virtual void setMouseY(double y) = 0;
 
+        /*! Returns true if mouse is pressed. */
+        virtual bool mousePressed() const = 0;
+
+        /*! Sets the pressed state of mouse. */
+        virtual void setMousePressed(bool pressed) = 0;
+
         /*! Returns the stage width. */
         virtual unsigned int stageWidth() const = 0;
 
@@ -122,6 +140,18 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Sets the stage height. */
         virtual void setStageHeight(unsigned int height) = 0;
 
+        /*! Returns the maximum number of clones (or -1 if the limit is disabled). */
+        virtual int cloneLimit() const = 0;
+
+        /*! Sets the maximum number of clones (use -1 or any negative number to disable the limit). */
+        virtual void setCloneLimit(int limit) = 0;
+
+        /*! Returns true if sprite fencing is enabled. */
+        virtual bool spriteFencingEnabled() const = 0;
+
+        /*! Toggles sprite fencing. */
+        virtual void setSpriteFencingEnabled(bool enable) = 0;
+
         /*! Returns true if there are any running script of the broadcast with the given index. */
         virtual bool broadcastRunning(unsigned int index, VirtualMachine *sourceScript) = 0;
 
@@ -210,6 +240,9 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /* Registers the given "when I start as clone" script. */
         virtual void addCloneInitScript(std::shared_ptr<Block> hatBlock) = 0;
 
+        /* Registers the given "when key pressed" script. */
+        virtual void addKeyPressScript(std::shared_ptr<Block> hatBlock, std::string keyName) = 0;
+
         /*! Returns the list of targets. */
         virtual const std::vector<std::shared_ptr<Target>> &targets() const = 0;
 
@@ -219,18 +252,16 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Returns the target at index. */
         virtual Target *targetAt(int index) const = 0;
 
-        /*! Returns the index of the target with the given name. */
+        /*!
+         * Returns the index of the target with the given name.
+         * \note Using "Stage" will return the index of the sprite with this name, or nullptr if it doesn't exist.
+         *       Use the "_stage_" reserved name to get the stage, or use the stage() method.
+         */
         virtual int findTarget(const std::string &targetName) const = 0;
 
         /*! Returns the Stage. */
         virtual Stage *stage() const = 0;
 
-        /*! Returns the target which owns the given variable. If it is the stage, the variable is global. */
-        virtual Target *variableOwner(Variable *variable) const = 0;
-
-        /*! Returns the target which owns the given list. If it is the stage, the list is global. */
-        virtual Target *listOwner(List *list) const = 0;
-
         /*! Returns the list of extension names. */
         virtual const std::vector<std::string> &extensions() const = 0;
 
diff --git a/include/scratchcpp/igraphicseffect.h b/include/scratchcpp/igraphicseffect.h
new file mode 100644
index 00000000..c28c2ab1
--- /dev/null
+++ b/include/scratchcpp/igraphicseffect.h
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <string>
+
+#include "iimageformat.h"
+
+namespace libscratchcpp
+{
+
+/*! \brief The IGraphicsEffects class is an interface for implementing custom graphics effects. */
+class LIBSCRATCHCPP_EXPORT IGraphicsEffect
+{
+    public:
+        virtual ~IGraphicsEffect() { }
+
+        /*! Returns the name of the graphics effect. */
+        virtual std::string name() const = 0;
+
+        /*! Applies the effect on the given bitmap. */
+        virtual void apply(Rgb **bitmap, unsigned int width, unsigned int height, double value) const = 0;
+};
+
+} // namespace libscratchcpp
diff --git a/include/scratchcpp/iimageformat.h b/include/scratchcpp/iimageformat.h
new file mode 100644
index 00000000..1458d955
--- /dev/null
+++ b/include/scratchcpp/iimageformat.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "global.h"
+
+namespace libscratchcpp
+{
+
+/*! A typedef for unsigned int. Holds the RGBA values. */
+using Rgb = unsigned int;
+
+/*! Returns the red component of the quadruplet rgb. */
+inline constexpr int red(Rgb rgb)
+{
+    return ((rgb >> 16) & 0xff);
+}
+
+/*! Returns the green component of the quadruplet rgb. */
+inline constexpr int green(Rgb rgb)
+{
+    return ((rgb >> 8) & 0xff);
+}
+
+/*! Returns the blue component of the quadruplet rgb. */
+inline constexpr int blue(Rgb rgb)
+{
+    return (rgb & 0xff);
+}
+
+/*! Returns the alpha component of the quadruplet rgb. */
+inline constexpr int alpha(Rgb rgb)
+{
+    return rgb >> 24;
+}
+
+/*! Creates an RGB triplet from the given color components. */
+inline constexpr Rgb rgb(int r, int g, int b)
+{
+    return (0xffu << 24) | ((r & 0xffu) << 16) | ((g & 0xffu) << 8) | (b & 0xffu);
+}
+
+/*! Creates an RGBA quadruplet from the given color components. */
+inline constexpr Rgb rgba(int r, int g, int b, int a)
+{
+    return ((a & 0xffu) << 24) | ((r & 0xffu) << 16) | ((g & 0xffu) << 8) | (b & 0xffu);
+}
+
+/*! \brief The IImageFormat class is an interface for image format implementation. */
+class LIBSCRATCHCPP_EXPORT IImageFormat
+{
+    public:
+        virtual ~IImageFormat() { }
+
+        /*! Sets the image data (buffer). */
+        virtual void setData(unsigned int size, void *data) = 0;
+
+        /*! Returns the width of the image. */
+        virtual unsigned int width() const = 0;
+
+        /*! Returns the height of the image. */
+        virtual unsigned int height() const = 0;
+
+        /*! Returns the color at the given pixel. */
+        virtual Rgb colorAt(unsigned int x, unsigned int y, double scale = 1) const = 0;
+};
+
+} // namespace libscratchcpp
diff --git a/include/scratchcpp/iimageformatfactory.h b/include/scratchcpp/iimageformatfactory.h
new file mode 100644
index 00000000..5e8d3f13
--- /dev/null
+++ b/include/scratchcpp/iimageformatfactory.h
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <memory>
+
+#include "global.h"
+
+namespace libscratchcpp
+{
+
+class IImageFormat;
+
+/*! \brief The IImageFormatFactory class is an interface which is used to instantiate image formats. */
+class LIBSCRATCHCPP_EXPORT IImageFormatFactory
+{
+    public:
+        virtual ~IImageFormatFactory() { }
+
+        /*! Creates an instance of the image format. */
+        virtual std::shared_ptr<IImageFormat> createInstance() const = 0;
+};
+
+} // namespace libscratchcpp
diff --git a/include/scratchcpp/keyevent.h b/include/scratchcpp/keyevent.h
new file mode 100644
index 00000000..2dd80715
--- /dev/null
+++ b/include/scratchcpp/keyevent.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "global.h"
+#include "spimpl.h"
+
+namespace libscratchcpp
+{
+
+class KeyEventPrivate;
+
+/*! \brief The KeyEvent class represents a Scratch key event. */
+class LIBSCRATCHCPP_EXPORT KeyEvent
+{
+    public:
+        enum class Type
+        {
+            Any,
+            Space,
+            Left,
+            Up,
+            Right,
+            Down,
+            Enter
+        };
+
+        KeyEvent(Type type = Type::Any);
+        KeyEvent(const std::string &name);
+
+        Type type() const;
+        const std::string &name() const;
+
+    private:
+        spimpl::impl_ptr<KeyEventPrivate> impl;
+};
+
+} // namespace libscratchcpp
diff --git a/include/scratchcpp/list.h b/include/scratchcpp/list.h
index bd4be76c..0ac380ab 100644
--- a/include/scratchcpp/list.h
+++ b/include/scratchcpp/list.h
@@ -11,6 +11,7 @@
 namespace libscratchcpp
 {
 
+class Target;
 class ListPrivate;
 
 /*! \brief The List class represents a Scratch list. */
@@ -25,6 +26,9 @@ class LIBSCRATCHCPP_EXPORT List
         const std::string &name();
         void setName(const std::string &name);
 
+        Target *target() const;
+        void setTarget(Target *target);
+
         long indexOf(const Value &value) const;
         bool contains(const Value &value) const;
 
diff --git a/include/scratchcpp/rect.h b/include/scratchcpp/rect.h
new file mode 100644
index 00000000..6a73576e
--- /dev/null
+++ b/include/scratchcpp/rect.h
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "global.h"
+#include "spimpl.h"
+
+namespace libscratchcpp
+{
+
+class RectPrivate;
+
+/*! The Rect class represents a rectangle. */
+class LIBSCRATCHCPP_EXPORT Rect
+{
+    public:
+        Rect(double left, double top, double right, double bottom);
+        Rect();
+
+        double left() const;
+        void setLeft(double left);
+
+        double top() const;
+        void setTop(double top);
+
+        double right() const;
+        void setRight(double right);
+
+        double bottom() const;
+        void setBottom(double bottom);
+
+        double width() const;
+        double height() const;
+
+    private:
+        spimpl::impl_ptr<RectPrivate> impl;
+};
+
+} // namespace libscratchcpp
diff --git a/include/scratchcpp/scratchconfiguration.h b/include/scratchcpp/scratchconfiguration.h
index 9f0d9039..12ff0d5f 100644
--- a/include/scratchcpp/scratchconfiguration.h
+++ b/include/scratchcpp/scratchconfiguration.h
@@ -11,6 +11,9 @@ namespace libscratchcpp
 {
 
 class IExtension;
+class IImageFormat;
+class IImageFormatFactory;
+class IGraphicsEffect;
 class ScratchConfigurationPrivate;
 
 /*! \brief The ScratchConfiguration class provides methods for adding custom extensions. */
@@ -35,6 +38,14 @@ class LIBSCRATCHCPP_EXPORT ScratchConfiguration
             return nullptr;
         };
 
+        static void registerImageFormat(const std::string &name, std::shared_ptr<IImageFormatFactory> formatFactory);
+        static void removeImageFormat(const std::string &name);
+        static std::shared_ptr<IImageFormat> createImageFormat(const std::string &name);
+
+        static void registerGraphicsEffect(std::shared_ptr<IGraphicsEffect> effect);
+        static void removeGraphicsEffect(const std::string &name);
+        static IGraphicsEffect *getGraphicsEffect(const std::string &name);
+
     private:
         static const std::vector<std::shared_ptr<IExtension>> getExtensions();
 
diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index dd50f56e..9bd82ab2 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -8,6 +8,8 @@ namespace libscratchcpp
 {
 
 class ISpriteHandler;
+class Rect;
+class IGraphicsEffect;
 class SpritePrivate;
 
 /*! \brief The Sprite class represents a Scratch sprite. */
@@ -22,6 +24,7 @@ class LIBSCRATCHCPP_EXPORT Sprite : public Target
         };
 
         Sprite();
+        Sprite(const Sprite &) = delete;
         ~Sprite();
 
         void setInterface(ISpriteHandler *newInterface);
@@ -47,6 +50,8 @@ class LIBSCRATCHCPP_EXPORT Sprite : public Target
         double size() const;
         void setSize(double newSize);
 
+        void setCurrentCostume(int newCostume);
+
         double direction() const;
         void setDirection(double newDirection);
 
@@ -59,8 +64,16 @@ class LIBSCRATCHCPP_EXPORT Sprite : public Target
         void setRotationStyle(const std::string &newRotationStyle);
         void setRotationStyle(const char *newRotationStyle);
 
+        Rect boundingRect() const;
+
+        double graphicsEffectValue(IGraphicsEffect *effect) const;
+        void setGraphicsEffectValue(IGraphicsEffect *effect, double value);
+
+        void clearGraphicsEffects();
+
     private:
         Target *dataSource() const override;
+        void setXY(double x, double y);
 
         spimpl::unique_impl_ptr<SpritePrivate> impl;
 };
diff --git a/include/scratchcpp/stage.h b/include/scratchcpp/stage.h
index c4b9be39..0c11e681 100644
--- a/include/scratchcpp/stage.h
+++ b/include/scratchcpp/stage.h
@@ -24,6 +24,7 @@ class LIBSCRATCHCPP_EXPORT Stage : public Target
         };
 
         Stage();
+        Stage(const Stage &) = delete;
 
         void setInterface(IStageHandler *newInterface);
 
diff --git a/include/scratchcpp/variable.h b/include/scratchcpp/variable.h
index 54e58ab2..0f5a06be 100644
--- a/include/scratchcpp/variable.h
+++ b/include/scratchcpp/variable.h
@@ -9,6 +9,7 @@
 namespace libscratchcpp
 {
 
+class Target;
 class VariablePrivate;
 
 /*! \brief The Variable class represents a Scratch variable. */
@@ -29,6 +30,9 @@ class LIBSCRATCHCPP_EXPORT Variable : public Entity
         bool isCloudVariable() const;
         void setIsCloudVariable(bool isCloudVariable);
 
+        Target *target() const;
+        void setTarget(Target *target);
+
         std::shared_ptr<Variable> clone();
 
     private:
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 69ae13d6..d544e200 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -6,9 +6,13 @@ target_sources(scratchcpp
     scratchconfiguration.cpp
     scratchconfiguration_p.cpp
     scratchconfiguration_p.h
+    rect.cpp
+    rect_p.cpp
+    rect_p.h
 )
 
 add_subdirectory(blocks)
 add_subdirectory(engine)
 add_subdirectory(internal)
 add_subdirectory(scratch)
+add_subdirectory(imageformats)
diff --git a/src/blocks/eventblocks.cpp b/src/blocks/eventblocks.cpp
index 1961ef13..7cffe7ce 100644
--- a/src/blocks/eventblocks.cpp
+++ b/src/blocks/eventblocks.cpp
@@ -26,6 +26,7 @@ void EventBlocks::registerBlocks(IEngine *engine)
     engine->addCompileFunction(this, "event_broadcastandwait", &compileBroadcastAndWait);
     engine->addCompileFunction(this, "event_whenbroadcastreceived", &compileWhenBroadcastReceived);
     engine->addCompileFunction(this, "event_whenbackdropswitchesto", &compileWhenBackdropSwitchesTo);
+    engine->addCompileFunction(this, "event_whenkeypressed", &compileWhenKeyPressed);
 
     // Inputs
     engine->addInput(this, "BROADCAST_INPUT", BROADCAST_INPUT);
@@ -33,6 +34,7 @@ void EventBlocks::registerBlocks(IEngine *engine)
     // Fields
     engine->addField(this, "BROADCAST_OPTION", BROADCAST_OPTION);
     engine->addField(this, "BACKDROP", BACKDROP);
+    engine->addField(this, "KEY_OPTION", KEY_OPTION);
 }
 
 void EventBlocks::compileBroadcast(Compiler *compiler)
@@ -84,6 +86,12 @@ void EventBlocks::compileWhenBackdropSwitchesTo(Compiler *compiler)
     }
 }
 
+void EventBlocks::compileWhenKeyPressed(Compiler *compiler)
+{
+    // NOTE: Field values don't have to be registered because keys are referenced by their names
+    compiler->engine()->addKeyPressScript(compiler->block(), compiler->field(KEY_OPTION)->value().toString());
+}
+
 unsigned int EventBlocks::broadcast(VirtualMachine *vm)
 {
     vm->engine()->broadcast(vm->engine()->findBroadcast(vm->getInput(0, 1)->toString()), vm);
diff --git a/src/blocks/eventblocks.h b/src/blocks/eventblocks.h
index ca310d51..5e9e4502 100644
--- a/src/blocks/eventblocks.h
+++ b/src/blocks/eventblocks.h
@@ -22,7 +22,8 @@ class EventBlocks : public IBlockSection
         enum Fields
         {
             BROADCAST_OPTION,
-            BACKDROP
+            BACKDROP,
+            KEY_OPTION
         };
 
         std::string name() const override;
@@ -33,6 +34,7 @@ class EventBlocks : public IBlockSection
         static void compileBroadcastAndWait(Compiler *compiler);
         static void compileWhenBroadcastReceived(Compiler *compiler);
         static void compileWhenBackdropSwitchesTo(Compiler *compiler);
+        static void compileWhenKeyPressed(Compiler *compiler);
 
         static unsigned int broadcast(VirtualMachine *vm);
         static unsigned int broadcastByIndex(VirtualMachine *vm);
diff --git a/src/blocks/looksblocks.cpp b/src/blocks/looksblocks.cpp
index 754c3272..f55d73b0 100644
--- a/src/blocks/looksblocks.cpp
+++ b/src/blocks/looksblocks.cpp
@@ -7,6 +7,7 @@
 #include <scratchcpp/input.h>
 #include <scratchcpp/field.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/scratchconfiguration.h>
 
 #include "looksblocks.h"
 #include "../engine/internal/randomgenerator.h"
@@ -25,6 +26,9 @@ void LooksBlocks::registerBlocks(IEngine *engine)
     // Blocks
     engine->addCompileFunction(this, "looks_show", &compileShow);
     engine->addCompileFunction(this, "looks_hide", &compileHide);
+    engine->addCompileFunction(this, "looks_changeeffectby", &compileChangeEffectBy);
+    engine->addCompileFunction(this, "looks_seteffectto", &compileSetEffectTo);
+    engine->addCompileFunction(this, "looks_cleargraphiceffects", &compileClearGraphicEffects);
     engine->addCompileFunction(this, "looks_changesizeby", &compileChangeSizeBy);
     engine->addCompileFunction(this, "looks_setsizeto", &compileSetSizeTo);
     engine->addCompileFunction(this, "looks_size", &compileSize);
@@ -41,13 +45,22 @@ void LooksBlocks::registerBlocks(IEngine *engine)
     engine->addInput(this, "SIZE", SIZE);
     engine->addInput(this, "COSTUME", COSTUME);
     engine->addInput(this, "BACKDROP", BACKDROP);
+    engine->addInput(this, "VALUE", VALUE);
 
     // Fields
     engine->addField(this, "NUMBER_NAME", NUMBER_NAME);
+    engine->addField(this, "EFFECT", EFFECT);
 
     // Field values
     engine->addFieldValue(this, "number", Number);
     engine->addFieldValue(this, "name", Name);
+    engine->addFieldValue(this, "COLOR", ColorEffect);
+    engine->addFieldValue(this, "FISHEYE", FisheyeEffect);
+    engine->addFieldValue(this, "WHIRL", WhirlEffect);
+    engine->addFieldValue(this, "PIXELATE", PixelateEffect);
+    engine->addFieldValue(this, "MOSAIC", MosaicEffect);
+    engine->addFieldValue(this, "BRIGHTNESS", BrightnessEffect);
+    engine->addFieldValue(this, "GHOST", GhostEffect);
 }
 
 void LooksBlocks::compileShow(Compiler *compiler)
@@ -60,6 +73,177 @@ void LooksBlocks::compileHide(Compiler *compiler)
     compiler->addFunctionCall(&hide);
 }
 
+void LooksBlocks::compileChangeEffectBy(Compiler *compiler)
+{
+    int option = compiler->field(EFFECT)->specialValueId();
+
+    switch (option) {
+        case ColorEffect:
+            if (!m_colorEffect)
+                m_colorEffect = ScratchConfiguration::getGraphicsEffect("color");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&changeColorEffectBy);
+            break;
+
+        case FisheyeEffect:
+            if (!m_fisheyeEffect)
+                m_fisheyeEffect = ScratchConfiguration::getGraphicsEffect("fisheye");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&changeFisheyeEffectBy);
+            break;
+
+        case WhirlEffect:
+            if (!m_whirlEffect)
+                m_whirlEffect = ScratchConfiguration::getGraphicsEffect("whirl");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&changeWhirlEffectBy);
+            break;
+
+        case PixelateEffect:
+            if (!m_pixelateEffect)
+                m_pixelateEffect = ScratchConfiguration::getGraphicsEffect("pixelate");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&changePixelateEffectBy);
+            break;
+
+        case MosaicEffect:
+            if (!m_mosaicEffect)
+                m_mosaicEffect = ScratchConfiguration::getGraphicsEffect("mosaic");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&changeMosaicEffectBy);
+            break;
+
+        case BrightnessEffect:
+            if (!m_brightnessEffect)
+                m_brightnessEffect = ScratchConfiguration::getGraphicsEffect("brightness");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&changeBrightnessEffectBy);
+            break;
+
+        case GhostEffect:
+            if (!m_ghostEffect)
+                m_ghostEffect = ScratchConfiguration::getGraphicsEffect("ghost");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&changeGhostEffectBy);
+            break;
+
+        default:
+            IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect(compiler->field(EFFECT)->value().toString());
+
+            if (effect) {
+                auto it = std::find(m_customGraphicsEffects.begin(), m_customGraphicsEffects.end(), effect);
+                size_t index;
+
+                if (it == m_customGraphicsEffects.end()) {
+                    index = m_customGraphicsEffects.size();
+                    m_customGraphicsEffects.push_back(effect);
+                } else
+                    index = it - m_customGraphicsEffects.begin();
+
+                compiler->addConstValue(index);
+                compiler->addInput(CHANGE);
+                compiler->addFunctionCall(&changeEffectBy);
+            }
+
+            break;
+    }
+}
+
+void LooksBlocks::compileSetEffectTo(Compiler *compiler)
+{
+    int option = compiler->field(EFFECT)->specialValueId();
+
+    switch (option) {
+        case ColorEffect:
+            if (!m_colorEffect)
+                m_colorEffect = ScratchConfiguration::getGraphicsEffect("color");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&setColorEffectTo);
+            break;
+
+        case FisheyeEffect:
+            if (!m_fisheyeEffect)
+                m_fisheyeEffect = ScratchConfiguration::getGraphicsEffect("fisheye");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&setFisheyeEffectTo);
+            break;
+
+        case WhirlEffect:
+            if (!m_whirlEffect)
+                m_whirlEffect = ScratchConfiguration::getGraphicsEffect("whirl");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&setWhirlEffectTo);
+            break;
+
+        case PixelateEffect:
+            if (!m_pixelateEffect)
+                m_pixelateEffect = ScratchConfiguration::getGraphicsEffect("pixelate");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&setPixelateEffectTo);
+            break;
+
+        case MosaicEffect:
+            if (!m_mosaicEffect)
+                m_mosaicEffect = ScratchConfiguration::getGraphicsEffect("mosaic");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&setMosaicEffectTo);
+            break;
+
+        case BrightnessEffect:
+            if (!m_brightnessEffect)
+                m_brightnessEffect = ScratchConfiguration::getGraphicsEffect("brightness");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&setBrightnessEffectTo);
+            break;
+
+        case GhostEffect:
+            if (!m_ghostEffect)
+                m_ghostEffect = ScratchConfiguration::getGraphicsEffect("ghost");
+
+            compiler->addInput(CHANGE);
+            compiler->addFunctionCall(&setGhostEffectTo);
+            break;
+
+        default:
+            IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect(compiler->field(EFFECT)->value().toString());
+
+            if (effect) {
+                auto it = std::find(m_customGraphicsEffects.begin(), m_customGraphicsEffects.end(), effect);
+                size_t index;
+
+                if (it == m_customGraphicsEffects.end()) {
+                    index = m_customGraphicsEffects.size();
+                    m_customGraphicsEffects.push_back(effect);
+                } else
+                    index = it - m_customGraphicsEffects.begin();
+
+                compiler->addConstValue(index);
+                compiler->addInput(CHANGE);
+                compiler->addFunctionCall(&setEffectTo);
+            }
+
+            break;
+    }
+}
+
+void LooksBlocks::compileClearGraphicEffects(Compiler *compiler)
+{
+    compiler->addFunctionCall(&clearGraphicEffects);
+}
+
 void LooksBlocks::compileChangeSizeBy(Compiler *compiler)
 {
     compiler->addInput(CHANGE);
@@ -254,6 +438,178 @@ unsigned int LooksBlocks::hide(VirtualMachine *vm)
     return 0;
 }
 
+unsigned int LooksBlocks::changeEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite) {
+        IGraphicsEffect *effect = m_customGraphicsEffects[vm->getInput(0, 2)->toLong()];
+        sprite->setGraphicsEffectValue(effect, sprite->graphicsEffectValue(effect) + vm->getInput(1, 2)->toDouble());
+    }
+
+    return 2;
+}
+
+unsigned int LooksBlocks::changeColorEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_colorEffect, sprite->graphicsEffectValue(m_colorEffect) + vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::changeFisheyeEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_fisheyeEffect, sprite->graphicsEffectValue(m_fisheyeEffect) + vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::changeWhirlEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_whirlEffect, sprite->graphicsEffectValue(m_whirlEffect) + vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::changePixelateEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_pixelateEffect, sprite->graphicsEffectValue(m_pixelateEffect) + vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::changeMosaicEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_mosaicEffect, sprite->graphicsEffectValue(m_mosaicEffect) + vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::changeBrightnessEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_brightnessEffect, sprite->graphicsEffectValue(m_brightnessEffect) + vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::changeGhostEffectBy(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_ghostEffect, sprite->graphicsEffectValue(m_ghostEffect) + vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::setEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_customGraphicsEffects[vm->getInput(0, 2)->toLong()], vm->getInput(1, 2)->toDouble());
+
+    return 2;
+}
+
+unsigned int LooksBlocks::setColorEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_colorEffect, vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::setFisheyeEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_fisheyeEffect, vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::setWhirlEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_whirlEffect, vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::setPixelateEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_pixelateEffect, vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::setMosaicEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_mosaicEffect, vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::setBrightnessEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_brightnessEffect, vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::setGhostEffectTo(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->setGraphicsEffectValue(m_ghostEffect, vm->getInput(0, 1)->toDouble());
+
+    return 1;
+}
+
+unsigned int LooksBlocks::clearGraphicEffects(VirtualMachine *vm)
+{
+    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+
+    if (sprite)
+        sprite->clearGraphicsEffects();
+
+    return 0;
+}
+
 unsigned int LooksBlocks::changeSizeBy(VirtualMachine *vm)
 {
     Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
diff --git a/src/blocks/looksblocks.h b/src/blocks/looksblocks.h
index cefe7e9b..060d1eba 100644
--- a/src/blocks/looksblocks.h
+++ b/src/blocks/looksblocks.h
@@ -3,6 +3,7 @@
 #pragma once
 
 #include <scratchcpp/iblocksection.h>
+#include <vector>
 
 namespace libscratchcpp
 {
@@ -10,6 +11,7 @@ namespace libscratchcpp
 class Target;
 class Stage;
 class Value;
+class IGraphicsEffect;
 class IRandomGenerator;
 
 /*! \brief The LooksBlocks class contains the implementation of looks blocks. */
@@ -21,18 +23,27 @@ class LooksBlocks : public IBlockSection
             CHANGE,
             SIZE,
             COSTUME,
-            BACKDROP
+            BACKDROP,
+            VALUE
         };
 
         enum Fields
         {
-            NUMBER_NAME
+            NUMBER_NAME,
+            EFFECT
         };
 
         enum FieldValues
         {
             Number,
-            Name
+            Name,
+            ColorEffect,
+            FisheyeEffect,
+            WhirlEffect,
+            PixelateEffect,
+            MosaicEffect,
+            BrightnessEffect,
+            GhostEffect
         };
 
         std::string name() const override;
@@ -41,6 +52,9 @@ class LooksBlocks : public IBlockSection
 
         static void compileShow(Compiler *compiler);
         static void compileHide(Compiler *compiler);
+        static void compileChangeEffectBy(Compiler *compiler);
+        static void compileSetEffectTo(Compiler *compiler);
+        static void compileClearGraphicEffects(Compiler *compiler);
         static void compileChangeSizeBy(Compiler *compiler);
         static void compileSetSizeTo(Compiler *compiler);
         static void compileSize(Compiler *compiler);
@@ -54,6 +68,26 @@ class LooksBlocks : public IBlockSection
 
         static unsigned int show(VirtualMachine *vm);
         static unsigned int hide(VirtualMachine *vm);
+
+        static unsigned int changeEffectBy(VirtualMachine *vm);
+        static unsigned int changeColorEffectBy(VirtualMachine *vm);
+        static unsigned int changeFisheyeEffectBy(VirtualMachine *vm);
+        static unsigned int changeWhirlEffectBy(VirtualMachine *vm);
+        static unsigned int changePixelateEffectBy(VirtualMachine *vm);
+        static unsigned int changeMosaicEffectBy(VirtualMachine *vm);
+        static unsigned int changeBrightnessEffectBy(VirtualMachine *vm);
+        static unsigned int changeGhostEffectBy(VirtualMachine *vm);
+
+        static unsigned int setEffectTo(VirtualMachine *vm);
+        static unsigned int setColorEffectTo(VirtualMachine *vm);
+        static unsigned int setFisheyeEffectTo(VirtualMachine *vm);
+        static unsigned int setWhirlEffectTo(VirtualMachine *vm);
+        static unsigned int setPixelateEffectTo(VirtualMachine *vm);
+        static unsigned int setMosaicEffectTo(VirtualMachine *vm);
+        static unsigned int setBrightnessEffectTo(VirtualMachine *vm);
+        static unsigned int setGhostEffectTo(VirtualMachine *vm);
+
+        static unsigned int clearGraphicEffects(VirtualMachine *vm);
         static unsigned int changeSizeBy(VirtualMachine *vm);
         static unsigned int setSizeTo(VirtualMachine *vm);
         static unsigned int size(VirtualMachine *vm);
@@ -88,6 +122,15 @@ class LooksBlocks : public IBlockSection
         static unsigned int backdropNumber(VirtualMachine *vm);
         static unsigned int backdropName(VirtualMachine *vm);
 
+        static inline std::vector<IGraphicsEffect *> m_customGraphicsEffects;
+        static inline IGraphicsEffect *m_colorEffect = nullptr;
+        static inline IGraphicsEffect *m_fisheyeEffect = nullptr;
+        static inline IGraphicsEffect *m_whirlEffect = nullptr;
+        static inline IGraphicsEffect *m_pixelateEffect = nullptr;
+        static inline IGraphicsEffect *m_mosaicEffect = nullptr;
+        static inline IGraphicsEffect *m_brightnessEffect = nullptr;
+        static inline IGraphicsEffect *m_ghostEffect = nullptr;
+
         static IRandomGenerator *rng;
 };
 
diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp
index 4a873cfc..f60213b7 100644
--- a/src/blocks/sensingblocks.cpp
+++ b/src/blocks/sensingblocks.cpp
@@ -7,6 +7,8 @@
 #include <scratchcpp/input.h>
 #include <scratchcpp/field.h>
 #include <scratchcpp/sprite.h>
+#include <scratchcpp/stage.h>
+#include <scratchcpp/costume.h>
 #include "sensingblocks.h"
 
 #include "../engine/internal/clock.h"
@@ -24,16 +26,26 @@ void SensingBlocks::registerBlocks(IEngine *engine)
 {
     // Blocks
     engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo);
+    engine->addCompileFunction(this, "sensing_keypressed", &compileKeyPressed);
+    engine->addCompileFunction(this, "sensing_mousedown", &compileMouseDown);
+    engine->addCompileFunction(this, "sensing_mousex", &compileMouseX);
+    engine->addCompileFunction(this, "sensing_mousey", &compileMouseY);
+    engine->addCompileFunction(this, "sensing_setdragmode", &compileSetDragMode);
     engine->addCompileFunction(this, "sensing_timer", &compileTimer);
     engine->addCompileFunction(this, "sensing_resettimer", &compileResetTimer);
+    engine->addCompileFunction(this, "sensing_of", &compileOf);
     engine->addCompileFunction(this, "sensing_current", &compileCurrent);
     engine->addCompileFunction(this, "sensing_dayssince2000", &compileDaysSince2000);
 
     // Inputs
     engine->addInput(this, "DISTANCETOMENU", DISTANCETOMENU);
+    engine->addInput(this, "KEY_OPTION", KEY_OPTION);
+    engine->addInput(this, "OBJECT", OBJECT);
 
     // Fields
     engine->addField(this, "CURRENTMENU", CURRENTMENU);
+    engine->addField(this, "DRAG_MODE", DRAG_MODE);
+    engine->addField(this, "PROPERTY", PROPERTY);
 
     // Field values
     engine->addFieldValue(this, "YEAR", YEAR);
@@ -43,6 +55,18 @@ void SensingBlocks::registerBlocks(IEngine *engine)
     engine->addFieldValue(this, "HOUR", HOUR);
     engine->addFieldValue(this, "MINUTE", MINUTE);
     engine->addFieldValue(this, "SECOND", SECOND);
+    engine->addFieldValue(this, "draggable", Draggable);
+    engine->addFieldValue(this, "not draggable", NotDraggable);
+    engine->addFieldValue(this, "x position", XPosition);
+    engine->addFieldValue(this, "y position", YPosition);
+    engine->addFieldValue(this, "direction", Direction);
+    engine->addFieldValue(this, "costume #", CostumeNumber);
+    engine->addFieldValue(this, "costume name", CostumeName);
+    engine->addFieldValue(this, "size", Size);
+    engine->addFieldValue(this, "volume", Volume);
+    engine->addFieldValue(this, "background #", BackdropNumber); // Scratch 1.4 support
+    engine->addFieldValue(this, "backdrop #", BackdropNumber);
+    engine->addFieldValue(this, "backdrop name", BackdropName);
 }
 
 void SensingBlocks::compileDistanceTo(Compiler *compiler)
@@ -66,6 +90,45 @@ void SensingBlocks::compileDistanceTo(Compiler *compiler)
     }
 }
 
+void SensingBlocks::compileKeyPressed(Compiler *compiler)
+{
+    compiler->addInput(KEY_OPTION);
+    compiler->addFunctionCall(&keyPressed);
+}
+
+void SensingBlocks::compileMouseDown(Compiler *compiler)
+{
+    compiler->addFunctionCall(&mouseDown);
+}
+
+void SensingBlocks::compileMouseX(Compiler *compiler)
+{
+    compiler->addFunctionCall(&mouseX);
+}
+
+void SensingBlocks::compileMouseY(Compiler *compiler)
+{
+    compiler->addFunctionCall(&mouseY);
+}
+
+void SensingBlocks::compileSetDragMode(Compiler *compiler)
+{
+    int option = compiler->field(DRAG_MODE)->specialValueId();
+
+    switch (option) {
+        case Draggable:
+            compiler->addFunctionCall(&setDraggableMode);
+            break;
+
+        case NotDraggable:
+            compiler->addFunctionCall(&setNotDraggableMode);
+            break;
+
+        default:
+            break;
+    }
+}
+
 void SensingBlocks::compileTimer(Compiler *compiler)
 {
     compiler->addFunctionCall(&timer);
@@ -76,6 +139,111 @@ void SensingBlocks::compileResetTimer(Compiler *compiler)
     compiler->addFunctionCall(&resetTimer);
 }
 
+void SensingBlocks::compileOf(Compiler *compiler)
+{
+    int option = compiler->field(PROPERTY)->specialValueId();
+    Input *input = compiler->input(OBJECT);
+    BlockFunc f = nullptr;
+
+    if (input->type() != Input::Type::ObscuredShadow) {
+        assert(input->pointsToDropdownMenu());
+        std::string value = input->selectedMenuItem();
+
+        int index = compiler->engine()->findTarget(value);
+
+        switch (option) {
+            case XPosition:
+                f = &xPositionOfSpriteByIndex;
+                break;
+
+            case YPosition:
+                f = &yPositionOfSpriteByIndex;
+                break;
+
+            case Direction:
+                f = &directionOfSpriteByIndex;
+                break;
+
+            case CostumeNumber:
+                f = &costumeNumberOfSpriteByIndex;
+                break;
+
+            case CostumeName:
+                f = &costumeNameOfSpriteByIndex;
+                break;
+
+            case Size:
+                f = &sizeOfSpriteByIndex;
+                break;
+
+            case Volume:
+                f = &volumeOfTargetByIndex;
+                break;
+
+            case BackdropNumber:
+                f = &backdropNumberOfStageByIndex;
+                break;
+
+            case BackdropName:
+                f = &backdropNameOfStageByIndex;
+                break;
+
+            default:
+                break;
+        }
+
+        if (f)
+            compiler->addConstValue(index);
+    } else {
+        switch (option) {
+            case XPosition:
+                f = &xPositionOfSprite;
+                break;
+
+            case YPosition:
+                f = &yPositionOfSprite;
+                break;
+
+            case Direction:
+                f = &directionOfSprite;
+                break;
+
+            case CostumeNumber:
+                f = &costumeNumberOfSprite;
+                break;
+
+            case CostumeName:
+                f = &costumeNameOfSprite;
+                break;
+
+            case Size:
+                f = &sizeOfSprite;
+                break;
+
+            case Volume:
+                f = &volumeOfTarget;
+                break;
+
+            case BackdropNumber:
+                f = &backdropNumberOfStage;
+                break;
+
+            case BackdropName:
+                f = &backdropNameOfStage;
+                break;
+
+            default:
+                break;
+        }
+
+        if (f)
+            compiler->addInput(input);
+    }
+
+    if (f)
+        compiler->addFunctionCall(f);
+}
+
 void SensingBlocks::compileCurrent(Compiler *compiler)
 {
     int id = compiler->field(CURRENTMENU)->specialValueId();
@@ -119,6 +287,46 @@ void SensingBlocks::compileDaysSince2000(Compiler *compiler)
     compiler->addFunctionCall(&daysSince2000);
 }
 
+unsigned int SensingBlocks::keyPressed(VirtualMachine *vm)
+{
+    vm->replaceReturnValue(vm->engine()->keyPressed(vm->getInput(0, 1)->toString()), 1);
+    return 0;
+}
+
+unsigned int SensingBlocks::mouseDown(VirtualMachine *vm)
+{
+    vm->addReturnValue(vm->engine()->mousePressed());
+    return 0;
+}
+
+unsigned int SensingBlocks::mouseX(VirtualMachine *vm)
+{
+    vm->addReturnValue(vm->engine()->mouseX());
+    return 0;
+}
+
+unsigned int SensingBlocks::mouseY(VirtualMachine *vm)
+{
+    vm->addReturnValue(vm->engine()->mouseY());
+    return 0;
+}
+
+unsigned int SensingBlocks::setDraggableMode(VirtualMachine *vm)
+{
+    if (Sprite *sprite = dynamic_cast<Sprite *>(vm->target()))
+        sprite->setDraggable(true);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::setNotDraggableMode(VirtualMachine *vm)
+{
+    if (Sprite *sprite = dynamic_cast<Sprite *>(vm->target()))
+        sprite->setDraggable(false);
+
+    return 0;
+}
+
 unsigned int SensingBlocks::distanceTo(VirtualMachine *vm)
 {
     Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
@@ -183,6 +391,238 @@ unsigned int SensingBlocks::resetTimer(VirtualMachine *vm)
     return 0;
 }
 
+unsigned int SensingBlocks::xPositionOfSprite(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->x(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::xPositionOfSpriteByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->x(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::yPositionOfSprite(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->y(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::yPositionOfSpriteByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->y(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::directionOfSprite(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->direction(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::directionOfSpriteByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->direction(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::costumeNumberOfSprite(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->currentCostume(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::costumeNumberOfSpriteByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->currentCostume(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::costumeNameOfSprite(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->costumeAt(sprite->currentCostume() - 1)->name(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::costumeNameOfSpriteByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->costumeAt(sprite->currentCostume() - 1)->name(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::sizeOfSprite(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->size(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::sizeOfSpriteByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Sprite *sprite = dynamic_cast<Sprite *>(target);
+
+    if (sprite)
+        vm->replaceReturnValue(sprite->size(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::volumeOfTarget(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+
+    if (target)
+        vm->replaceReturnValue(target->volume(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::volumeOfTargetByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+
+    if (target)
+        vm->replaceReturnValue(target->volume(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::backdropNumberOfStage(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Stage *stage = dynamic_cast<Stage *>(target);
+
+    if (stage)
+        vm->replaceReturnValue(stage->currentCostume(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::backdropNumberOfStageByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Stage *stage = dynamic_cast<Stage *>(target);
+
+    if (stage)
+        vm->replaceReturnValue(stage->currentCostume(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::backdropNameOfStage(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->engine()->findTarget(vm->getInput(0, 1)->toString()));
+    Stage *stage = dynamic_cast<Stage *>(target);
+
+    if (stage)
+        vm->replaceReturnValue(stage->costumeAt(stage->currentCostume() - 1)->name(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
+unsigned int SensingBlocks::backdropNameOfStageByIndex(VirtualMachine *vm)
+{
+    Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
+    Stage *stage = dynamic_cast<Stage *>(target);
+
+    if (stage)
+        vm->replaceReturnValue(stage->costumeAt(stage->currentCostume() - 1)->name(), 1);
+    else
+        vm->replaceReturnValue(0, 1);
+
+    return 0;
+}
+
 unsigned int SensingBlocks::currentYear(VirtualMachine *vm)
 {
     time_t now = time(0);
diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h
index abbaff2f..b2f17f83 100644
--- a/src/blocks/sensingblocks.h
+++ b/src/blocks/sensingblocks.h
@@ -14,12 +14,16 @@ class SensingBlocks : public IBlockSection
     public:
         enum Inputs
         {
-            DISTANCETOMENU
+            DISTANCETOMENU,
+            KEY_OPTION,
+            OBJECT
         };
 
         enum Fields
         {
-            CURRENTMENU
+            CURRENTMENU,
+            DRAG_MODE,
+            PROPERTY
         };
 
         enum FieldValues
@@ -30,7 +34,18 @@ class SensingBlocks : public IBlockSection
             DAYOFWEEK,
             HOUR,
             MINUTE,
-            SECOND
+            SECOND,
+            Draggable,
+            NotDraggable,
+            XPosition,
+            YPosition,
+            Direction,
+            CostumeNumber,
+            CostumeName,
+            Size,
+            Volume,
+            BackdropNumber,
+            BackdropName
         };
 
         std::string name() const override;
@@ -38,17 +53,51 @@ class SensingBlocks : public IBlockSection
         void registerBlocks(IEngine *engine) override;
 
         static void compileDistanceTo(Compiler *compiler);
+        static void compileKeyPressed(Compiler *compiler);
+        static void compileMouseDown(Compiler *compiler);
+        static void compileMouseX(Compiler *compiler);
+        static void compileMouseY(Compiler *compiler);
+        static void compileSetDragMode(Compiler *compiler);
         static void compileTimer(Compiler *compiler);
         static void compileResetTimer(Compiler *compiler);
+        static void compileOf(Compiler *compiler);
         static void compileCurrent(Compiler *compiler);
         static void compileDaysSince2000(Compiler *compiler);
 
+        static unsigned int keyPressed(VirtualMachine *vm);
+        static unsigned int mouseDown(VirtualMachine *vm);
+        static unsigned int mouseX(VirtualMachine *vm);
+        static unsigned int mouseY(VirtualMachine *vm);
+
+        static unsigned int setDraggableMode(VirtualMachine *vm);
+        static unsigned int setNotDraggableMode(VirtualMachine *vm);
+
         static unsigned int distanceTo(VirtualMachine *vm);
         static unsigned int distanceToByIndex(VirtualMachine *vm);
         static unsigned int distanceToMousePointer(VirtualMachine *vm);
 
         static unsigned int timer(VirtualMachine *vm);
         static unsigned int resetTimer(VirtualMachine *vm);
+
+        static unsigned int xPositionOfSprite(VirtualMachine *vm);
+        static unsigned int xPositionOfSpriteByIndex(VirtualMachine *vm);
+        static unsigned int yPositionOfSprite(VirtualMachine *vm);
+        static unsigned int yPositionOfSpriteByIndex(VirtualMachine *vm);
+        static unsigned int directionOfSprite(VirtualMachine *vm);
+        static unsigned int directionOfSpriteByIndex(VirtualMachine *vm);
+        static unsigned int costumeNumberOfSprite(VirtualMachine *vm);
+        static unsigned int costumeNumberOfSpriteByIndex(VirtualMachine *vm);
+        static unsigned int costumeNameOfSprite(VirtualMachine *vm);
+        static unsigned int costumeNameOfSpriteByIndex(VirtualMachine *vm);
+        static unsigned int sizeOfSprite(VirtualMachine *vm);
+        static unsigned int sizeOfSpriteByIndex(VirtualMachine *vm);
+        static unsigned int volumeOfTarget(VirtualMachine *vm);
+        static unsigned int volumeOfTargetByIndex(VirtualMachine *vm);
+        static unsigned int backdropNumberOfStage(VirtualMachine *vm);
+        static unsigned int backdropNumberOfStageByIndex(VirtualMachine *vm);
+        static unsigned int backdropNameOfStage(VirtualMachine *vm);
+        static unsigned int backdropNameOfStageByIndex(VirtualMachine *vm);
+
         static unsigned int currentYear(VirtualMachine *vm);
         static unsigned int currentMonth(VirtualMachine *vm);
         static unsigned int currentDate(VirtualMachine *vm);
diff --git a/src/engine/internal/clock.cpp b/src/engine/internal/clock.cpp
index 9473dc48..6b6087e2 100644
--- a/src/engine/internal/clock.cpp
+++ b/src/engine/internal/clock.cpp
@@ -1,5 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
+#include <thread>
+
 #include "clock.h"
 
 using namespace libscratchcpp;
@@ -24,3 +26,8 @@ std::chrono::system_clock::time_point Clock::currentSystemTime() const
 {
     return std::chrono::system_clock::now();
 }
+
+void Clock::sleep(const std::chrono::milliseconds &time) const
+{
+    std::this_thread::sleep_for(time);
+}
diff --git a/src/engine/internal/clock.h b/src/engine/internal/clock.h
index 7ebf756c..5eb906bb 100644
--- a/src/engine/internal/clock.h
+++ b/src/engine/internal/clock.h
@@ -19,6 +19,8 @@ class Clock : public IClock
         std::chrono::steady_clock::time_point currentSteadyTime() const override;
         std::chrono::system_clock::time_point currentSystemTime() const override;
 
+        void sleep(const std::chrono::milliseconds &time) const override;
+
     private:
         static std::shared_ptr<Clock> m_instance;
 };
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index 4fb00a68..b9d48496 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -14,28 +14,36 @@
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/keyevent.h>
 #include <cassert>
 #include <iostream>
-#include <thread>
 
 #include "engine.h"
 #include "blocksectioncontainer.h"
 #include "timer.h"
+#include "clock.h"
 #include "../../blocks/standardblocks.h"
 
 using namespace libscratchcpp;
 
 Engine::Engine() :
     m_defaultTimer(std::make_unique<Timer>()),
-    m_timer(m_defaultTimer.get())
+    m_timer(m_defaultTimer.get()),
+    m_clock(Clock::instance().get())
 {
 }
 
+Engine::~Engine()
+{
+    m_clones.clear();
+}
+
 void Engine::clear()
 {
     m_sections.clear();
     m_targets.clear();
     m_broadcasts.clear();
+    m_clones.clear();
 }
 
 // Resolves ID references and sets pointers of entities.
@@ -171,6 +179,12 @@ void Engine::frame()
 
 void Engine::start()
 {
+    // NOTE: Running scripts should be deleted, but this method will probably be removed anyway
+    /*if (m_running)
+        finalize();*/
+
+    deleteClones();
+
     m_timer->reset();
     m_running = true;
 
@@ -183,9 +197,8 @@ void Engine::start()
 
 void Engine::stop()
 {
-    m_runningScripts.clear();
-    m_scriptsToRemove.clear();
-    m_running = false;
+    finalize();
+    deleteClones();
 }
 
 void Engine::startScript(std::shared_ptr<Block> topLevelBlock, std::shared_ptr<Target> target)
@@ -300,7 +313,7 @@ void Engine::stopTarget(Target *target, VirtualMachine *exceptScript)
 
 void Engine::initClone(Sprite *clone)
 {
-    if (!clone)
+    if (!clone || ((m_cloneLimit >= 0) && (m_clones.size() >= m_cloneLimit)))
         return;
 
     Sprite *source = clone->cloneParent();
@@ -327,6 +340,14 @@ void Engine::initClone(Sprite *clone)
             m_runningScripts.push_back(vm);
         }
     }
+
+    assert(std::find(m_clones.begin(), m_clones.end(), clone) == m_clones.end());
+    m_clones.push_back(clone);
+}
+
+void Engine::deinitClone(Sprite *clone)
+{
+    m_clones.erase(std::remove(m_clones.begin(), m_clones.end(), clone), m_clones.end());
 }
 
 void Engine::run()
@@ -335,7 +356,7 @@ void Engine::run()
     start();
 
     while (true) {
-        auto lastFrameTime = std::chrono::steady_clock::now();
+        auto lastFrameTime = m_clock->currentSteadyTime();
         m_skipFrame = false;
 
         // Execute the frame
@@ -344,13 +365,13 @@ void Engine::run()
             break;
 
         // Sleep until the time for the next frame
-        auto currentTime = std::chrono::steady_clock::now();
+        auto currentTime = m_clock->currentSteadyTime();
         auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastFrameTime);
         auto sleepTime = m_frameDuration - elapsedTime;
         bool timeOut = sleepTime <= std::chrono::milliseconds::zero();
 
         if (!timeOut && !m_skipFrame)
-            std::this_thread::sleep_for(sleepTime);
+            m_clock->sleep(sleepTime);
 
         if ((m_skipFrame && timeOut) || !m_skipFrame) {
             // TODO: Repaint here
@@ -359,7 +380,7 @@ void Engine::run()
         lastFrameTime = currentTime;
     }
 
-    stop();
+    finalize();
 }
 
 bool Engine::isRunning() const
@@ -378,6 +399,61 @@ void Engine::setFps(double fps)
     updateFrameDuration();
 }
 
+bool Engine::keyPressed(const std::string &name) const
+{
+    if (name == "any") {
+        if (m_anyKeyPressed)
+            return true;
+
+        for (const auto &[key, value] : m_keyMap) {
+            if (value)
+                return true;
+        }
+
+        return false;
+    }
+
+    KeyEvent event(name);
+    auto it = m_keyMap.find(event.name());
+
+    if (it == m_keyMap.cend())
+        return false;
+    else
+        return it->second;
+}
+
+void Engine::setKeyState(const std::string &name, bool pressed)
+{
+    KeyEvent event(name);
+    m_keyMap[event.name()] = pressed;
+
+    // Start "when key pressed" scripts
+    if (pressed) {
+        auto it = m_whenKeyPressedScripts.find(event.name());
+
+        if (it != m_whenKeyPressedScripts.cend())
+            startWhenKeyPressedScripts(it->second);
+
+        it = m_whenKeyPressedScripts.find("any");
+
+        if (it != m_whenKeyPressedScripts.cend())
+            startWhenKeyPressedScripts(it->second);
+    }
+}
+
+void Engine::setAnyKeyPressed(bool pressed)
+{
+    m_anyKeyPressed = pressed;
+
+    // Start "when key pressed" scripts
+    if (pressed) {
+        auto it = m_whenKeyPressedScripts.find("any");
+
+        if (it != m_whenKeyPressedScripts.cend())
+            startWhenKeyPressedScripts(it->second);
+    }
+}
+
 double Engine::mouseX() const
 {
     return m_mouseX;
@@ -398,6 +474,16 @@ void Engine::setMouseY(double y)
     m_mouseY = y;
 }
 
+bool Engine::mousePressed() const
+{
+    return m_mousePressed;
+}
+
+void Engine::setMousePressed(bool pressed)
+{
+    m_mousePressed = pressed;
+}
+
 unsigned int Engine::stageWidth() const
 {
     return m_stageWidth;
@@ -418,6 +504,26 @@ void Engine::setStageHeight(unsigned int height)
     m_stageHeight = height;
 }
 
+int Engine::cloneLimit() const
+{
+    return m_cloneLimit;
+}
+
+void Engine::setCloneLimit(int limit)
+{
+    m_cloneLimit = limit < 0 ? -1 : limit;
+}
+
+bool Engine::spriteFencingEnabled() const
+{
+    return m_spriteFencingEnabled;
+}
+
+void Engine::setSpriteFencingEnabled(bool enable)
+{
+    m_spriteFencingEnabled = enable;
+}
+
 bool Engine::broadcastRunning(unsigned int index, VirtualMachine *sourceScript)
 {
     if (index < 0 || index >= m_broadcasts.size())
@@ -611,7 +717,23 @@ void Engine::addCloneInitScript(std::shared_ptr<Block> hatBlock)
     auto it = m_cloneInitScriptsMap.find(target);
 
     if (it == m_cloneInitScriptsMap.cend())
-        m_cloneInitScriptsMap[target] = { m_scripts[hatBlock].get() };
+        m_cloneInitScriptsMap[target] = { script };
+    else {
+        auto &scripts = it->second;
+
+        if (std::find(scripts.begin(), scripts.end(), script) == scripts.cend())
+            scripts.push_back(script);
+    }
+}
+
+void Engine::addKeyPressScript(std::shared_ptr<Block> hatBlock, std::string keyName)
+{
+    std::transform(keyName.begin(), keyName.end(), keyName.begin(), ::tolower);
+    Script *script = m_scripts[hatBlock].get();
+    auto it = m_whenKeyPressedScripts.find(keyName);
+
+    if (it == m_whenKeyPressedScripts.cend())
+        m_whenKeyPressedScripts[keyName] = { script };
     else {
         auto &scripts = it->second;
 
@@ -628,8 +750,6 @@ const std::vector<std::shared_ptr<Target>> &Engine::targets() const
 void Engine::setTargets(const std::vector<std::shared_ptr<Target>> &newTargets)
 {
     m_targets = newTargets;
-    m_variableOwners.clear();
-    m_listOwners.clear();
 
     for (auto target : m_targets) {
         // Set engine in the target
@@ -641,18 +761,6 @@ void Engine::setTargets(const std::vector<std::shared_ptr<Target>> &newTargets)
             block->setEngine(this);
             block->setTarget(target.get());
         }
-
-        // Add variables to owner map
-        const auto &variables = target->variables();
-
-        for (auto variable : variables)
-            m_variableOwners[variable.get()] = target.get();
-
-        // Add lists to owner map
-        const auto &lists = target->lists();
-
-        for (auto list : lists)
-            m_listOwners[list.get()] = target.get();
     }
 }
 
@@ -668,7 +776,7 @@ int Engine::findTarget(const std::string &targetName) const
 {
     int i = 0;
     for (auto target : m_targets) {
-        if (target->name() == targetName)
+        if ((target->isStage() && targetName == "_stage_") || (!target->isStage() && target->name() == targetName))
             return i;
         i++;
     }
@@ -685,26 +793,6 @@ Stage *Engine::stage() const
         return dynamic_cast<Stage *>((*it).get());
 }
 
-Target *Engine::variableOwner(Variable *variable) const
-{
-    auto it = m_variableOwners.find(variable);
-
-    if (it == m_variableOwners.cend())
-        return nullptr;
-
-    return it->second;
-}
-
-Target *Engine::listOwner(List *list) const
-{
-    auto it = m_listOwners.find(list);
-
-    if (it == m_listOwners.cend())
-        return nullptr;
-
-    return it->second;
-}
-
 const std::vector<std::string> &Engine::extensions() const
 {
     return m_extensions;
@@ -852,7 +940,48 @@ BlockSectionContainer *Engine::blockSectionContainer(IBlockSection *section) con
     return nullptr;
 }
 
+void Engine::finalize()
+{
+    m_runningScripts.clear();
+    m_scriptsToRemove.clear();
+    m_running = false;
+}
+
+void Engine::deleteClones()
+{
+    m_clones.clear();
+
+    for (auto target : m_targets) {
+        Sprite *sprite = dynamic_cast<Sprite *>(target.get());
+
+        if (sprite) {
+            std::vector<std::shared_ptr<Sprite>> clones = sprite->children();
+
+            for (auto clone : clones) {
+                assert(clone);
+                clone->~Sprite();
+            }
+        }
+    }
+}
+
 void Engine::updateFrameDuration()
 {
     m_frameDuration = std::chrono::milliseconds(static_cast<long>(1000 / m_fps));
 }
+
+void Engine::startWhenKeyPressedScripts(const std::vector<Script *> &scripts)
+{
+    for (auto script : scripts) {
+        std::shared_ptr<Block> block = nullptr;
+
+        for (const auto &[b, s] : m_scripts) {
+            if (s.get() == script)
+                block = b;
+        }
+
+        assert(block);
+        assert(std::find_if(m_targets.begin(), m_targets.end(), [script](std::shared_ptr<Target> target) { return script->target() == target.get(); }) != m_targets.end());
+        startScript(block, *std::find_if(m_targets.begin(), m_targets.end(), [script](std::shared_ptr<Target> target) { return script->target() == target.get(); }));
+    }
+}
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index 60dfd6aa..53a56ab4 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -15,12 +15,14 @@ namespace libscratchcpp
 {
 
 class Entity;
+class IClock;
 
 class Engine : public IEngine
 {
     public:
         Engine();
         Engine(const Engine &) = delete;
+        ~Engine();
 
         void clear() override;
         void resolveIds();
@@ -35,6 +37,7 @@ class Engine : public IEngine
         void stopScript(VirtualMachine *vm) override;
         void stopTarget(Target *target, VirtualMachine *exceptScript) override;
         void initClone(libscratchcpp::Sprite *clone) override;
+        void deinitClone(libscratchcpp::Sprite *clone) override;
         void run() override;
 
         bool isRunning() const override;
@@ -42,18 +45,31 @@ class Engine : public IEngine
         double fps() const override;
         void setFps(double fps) override;
 
+        bool keyPressed(const std::string &name) const override;
+        void setKeyState(const std::string &name, bool pressed) override;
+        void setAnyKeyPressed(bool pressed) override;
+
         double mouseX() const override;
         void setMouseX(double x) override;
 
         double mouseY() const override;
         void setMouseY(double y) override;
 
+        bool mousePressed() const override;
+        void setMousePressed(bool pressed) override;
+
         unsigned int stageWidth() const override;
         void setStageWidth(unsigned int width) override;
 
         unsigned int stageHeight() const override;
         void setStageHeight(unsigned int height) override;
 
+        int cloneLimit() const override;
+        void setCloneLimit(int limit) override;
+
+        bool spriteFencingEnabled() const override;
+        void setSpriteFencingEnabled(bool enable) override;
+
         bool broadcastRunning(unsigned int index, VirtualMachine *sourceScript) override;
         bool broadcastByPtrRunning(Broadcast *broadcast, VirtualMachine *sourceScript) override;
 
@@ -83,8 +99,8 @@ class Engine : public IEngine
         int findBroadcastById(const std::string &broadcastId) const override;
 
         void addBroadcastScript(std::shared_ptr<Block> whenReceivedBlock, Broadcast *broadcast) override;
-
         void addCloneInitScript(std::shared_ptr<Block> hatBlock) override;
+        void addKeyPressScript(std::shared_ptr<Block> hatBlock, std::string keyName) override;
 
         const std::vector<std::shared_ptr<Target>> &targets() const override;
         void setTargets(const std::vector<std::shared_ptr<Target>> &newTargets) override;
@@ -93,9 +109,6 @@ class Engine : public IEngine
 
         Stage *stage() const override;
 
-        Target *variableOwner(Variable *variable) const override;
-        Target *listOwner(List *list) const override;
-
         const std::vector<std::string> &extensions() const override;
         void setExtensions(const std::vector<std::string> &newExtensions) override;
 
@@ -104,7 +117,11 @@ class Engine : public IEngine
         BlockSectionContainer *blockSectionContainer(const std::string &opcode) const;
         BlockSectionContainer *blockSectionContainer(IBlockSection *section) const;
 
+        IClock *m_clock = nullptr;
+
     private:
+        void finalize();
+        void deleteClones();
         std::shared_ptr<Block> getBlock(const std::string &id);
         std::shared_ptr<Variable> getVariable(const std::string &id);
         std::shared_ptr<List> getList(const std::string &id);
@@ -113,6 +130,7 @@ class Engine : public IEngine
         std::shared_ptr<IBlockSection> blockSection(const std::string &opcode) const;
 
         void updateFrameDuration();
+        void startWhenKeyPressedScripts(const std::vector<Script *> &scripts);
 
         std::unordered_map<std::shared_ptr<IBlockSection>, std::unique_ptr<BlockSectionContainer>> m_sections;
         std::vector<std::shared_ptr<Target>> m_targets;
@@ -120,22 +138,27 @@ class Engine : public IEngine
         std::unordered_map<Broadcast *, std::vector<Script *>> m_broadcastMap;
         std::unordered_map<Broadcast *, std::vector<std::pair<VirtualMachine *, VirtualMachine *>>> m_runningBroadcastMap; // source script, "when received" script
         std::unordered_map<Target *, std::vector<Script *>> m_cloneInitScriptsMap;                                         // target (no clones), "when I start as a clone" scripts
+        std::unordered_map<std::string, std::vector<Script *>> m_whenKeyPressedScripts;                                    // key name, "when key pressed" scripts
         std::vector<std::string> m_extensions;
         std::vector<std::shared_ptr<VirtualMachine>> m_runningScripts;
         std::vector<VirtualMachine *> m_scriptsToRemove;
         std::unordered_map<std::shared_ptr<Block>, std::shared_ptr<Script>> m_scripts;
         std::vector<BlockFunc> m_functions;
-        std::unordered_map<Variable *, Target *> m_variableOwners;
-        std::unordered_map<List *, Target *> m_listOwners;
 
         std::unique_ptr<ITimer> m_defaultTimer;
         ITimer *m_timer = nullptr;
-        double m_fps = 30;                         // default FPS
-        std::chrono::milliseconds m_frameDuration; // will be computed in run()
+        double m_fps = 30;                              // default FPS
+        std::chrono::milliseconds m_frameDuration;      // will be computed in run()
+        std::unordered_map<std::string, bool> m_keyMap; // holds key states
+        bool m_anyKeyPressed = false;
         double m_mouseX = 0;
         double m_mouseY = 0;
+        bool m_mousePressed = false;
         unsigned int m_stageWidth = 480;
         unsigned int m_stageHeight = 360;
+        int m_cloneLimit = 300;
+        std::vector<Sprite *> m_clones;
+        bool m_spriteFencingEnabled = true;
 
         bool m_running = false;
         bool m_breakFrame = false;
diff --git a/src/engine/internal/iclock.h b/src/engine/internal/iclock.h
index 90c55308..796d0de4 100644
--- a/src/engine/internal/iclock.h
+++ b/src/engine/internal/iclock.h
@@ -14,6 +14,8 @@ class IClock
 
         virtual std::chrono::steady_clock::time_point currentSteadyTime() const = 0;
         virtual std::chrono::system_clock::time_point currentSystemTime() const = 0;
+
+        virtual void sleep(const std::chrono::milliseconds &time) const = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/src/engine/script.cpp b/src/engine/script.cpp
index 14e2c9e1..ce4c5c42 100644
--- a/src/engine/script.cpp
+++ b/src/engine/script.cpp
@@ -77,7 +77,7 @@ std::shared_ptr<VirtualMachine> Script::start(Target *target)
         std::vector<List *> lists;
 
         for (const auto &var : impl->variables) {
-            Target *owner = impl->engine->variableOwner(var);
+            Target *owner = var->target();
 
             if (owner && (owner == root)) {
                 auto cloneVar = sprite->variableAt(sprite->findVariableById(var->id()));
@@ -90,7 +90,7 @@ std::shared_ptr<VirtualMachine> Script::start(Target *target)
         }
 
         for (const auto &list : impl->lists) {
-            Target *owner = impl->engine->listOwner(list);
+            Target *owner = list->target();
 
             if (owner && (owner == root)) {
                 auto cloneList = sprite->listAt(sprite->findListById(list->id()));
diff --git a/src/engine/virtualmachine.cpp b/src/engine/virtualmachine.cpp
index 74248799..d10a6b1f 100644
--- a/src/engine/virtualmachine.cpp
+++ b/src/engine/virtualmachine.cpp
@@ -164,10 +164,15 @@ void VirtualMachine::replaceReturnValue(const Value &v, unsigned int offset)
 /*! Continues running the script from last position (the first instruction is skipped). */
 void VirtualMachine::run()
 {
+    impl->running = true;
+
     unsigned int *ret = impl->run(impl->pos);
     assert(ret);
+
     if (impl->savePos)
         impl->pos = ret;
+
+    impl->running = false;
 }
 
 /*! Jumps back to the initial position. */
@@ -175,7 +180,10 @@ void VirtualMachine::reset()
 {
     assert(impl->bytecode);
     impl->pos = impl->bytecode;
-    impl->regCount = 0;
+    impl->atEnd = false;
+
+    if (!impl->running) // Registers will be freed when the script stops running
+        impl->regCount = 0;
 }
 
 /*! Moves back to the last vm::OP_CHECKPOINT instruction in the bytecode. */
diff --git a/src/engine/virtualmachine_p.cpp b/src/engine/virtualmachine_p.cpp
index 2c747c30..4ca4d1c4 100644
--- a/src/engine/virtualmachine_p.cpp
+++ b/src/engine/virtualmachine_p.cpp
@@ -10,11 +10,16 @@
 #include "virtualmachine_p.h"
 #include "internal/randomgenerator.h"
 
-#define MAX_REG_COUNT 1024
-
 #define DISPATCH() goto *dispatch_table[*++pos]
 #define FREE_REGS(count) regCount -= count
-#define ADD_RET_VALUE(value) *regs[regCount++] = value
+#define ADD_RET_VALUE(value)                                                                                                                                                                           \
+    if (regCount + 1 >= regsVector.size()) {                                                                                                                                                           \
+        regsVector.reserve(regsVector.size() + 1024);                                                                                                                                                  \
+        for (size_t i = 0; i < 1024; i++)                                                                                                                                                              \
+            regsVector.push_back(new Value());                                                                                                                                                         \
+        regs = regsVector.data();                                                                                                                                                                      \
+    }                                                                                                                                                                                                  \
+    *regs[regCount++] = value
 #define REPLACE_RET_VALUE(value, offset) *regs[regCount - offset] = value
 #define GET_NEXT_ARG() constValues[*++pos]
 #define READ_REG(index, count) regs[regCount - count + index]
@@ -103,9 +108,10 @@ VirtualMachinePrivate::VirtualMachinePrivate(VirtualMachine *vm, Target *target,
     engine(engine),
     script(script)
 {
-    regs = new Value *[MAX_REG_COUNT];
-    for (int i = 0; i < MAX_REG_COUNT; i++)
-        regs[i] = new Value();
+    regsVector.reserve(1024);
+    for (int i = 0; i < 1024; i++)
+        regsVector.push_back(new Value());
+    regs = regsVector.data();
     loops.reserve(256);
     callTree.reserve(1024);
 
@@ -115,9 +121,8 @@ VirtualMachinePrivate::VirtualMachinePrivate(VirtualMachine *vm, Target *target,
 
 VirtualMachinePrivate::~VirtualMachinePrivate()
 {
-    for (int i = 0; i < MAX_REG_COUNT; i++)
-        delete regs[i];
-    delete regs;
+    for (int i = 0; i < regsVector.size(); i++)
+        delete regsVector[i];
 }
 
 unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
diff --git a/src/engine/virtualmachine_p.h b/src/engine/virtualmachine_p.h
index 18dfaffa..43716ac3 100644
--- a/src/engine/virtualmachine_p.h
+++ b/src/engine/virtualmachine_p.h
@@ -43,6 +43,7 @@ struct VirtualMachinePrivate
         Script *script = nullptr;
         unsigned int *pos = nullptr;
         unsigned int *checkpoint = nullptr;
+        bool running = false;
         bool atEnd = false;
         std::vector<Loop> loops;
         std::vector<unsigned int *> callTree;
@@ -65,6 +66,7 @@ struct VirtualMachinePrivate
         std::vector<List *> listsVector;
 
         Value **regs = nullptr;
+        std::vector<Value *> regsVector;
         size_t regCount = 0;
 
         static IRandomGenerator *rng;
diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt
new file mode 100644
index 00000000..878bb0c4
--- /dev/null
+++ b/src/imageformats/CMakeLists.txt
@@ -0,0 +1,19 @@
+option(LIBSCRATCHCPP_JPEG_SUPPORT "JPEG image support" ON)
+option(LIBSCRATCHCPP_PNG_SUPPORT "PNG image support" ON)
+
+add_subdirectory(stub)
+
+if (LIBSCRATCHCPP_JPEG_SUPPORT)
+    target_compile_definitions(scratchcpp PRIVATE JPEG_SUPPORT)
+    add_subdirectory(jpeg)
+endif()
+
+if (LIBSCRATCHCPP_PNG_SUPPORT)
+    target_compile_definitions(scratchcpp PRIVATE PNG_SUPPORT)
+    add_subdirectory(png)
+endif()
+
+target_sources(scratchcpp
+  PRIVATE
+    defaultimageformats.cpp
+)
diff --git a/src/imageformats/defaultimageformats.cpp b/src/imageformats/defaultimageformats.cpp
new file mode 100644
index 00000000..94fdbfe6
--- /dev/null
+++ b/src/imageformats/defaultimageformats.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <scratchcpp/scratchconfiguration.h>
+#include <string>
+
+#ifdef JPEG_SUPPORT
+#include "jpeg/jpegimageformatfactory.h"
+#endif
+
+#ifdef PNG_SUPPORT
+#include "png/pngimageformatfactory.h"
+#endif
+
+namespace libscratchcpp
+{
+
+class DefaultImageFormats
+{
+    public:
+        DefaultImageFormats()
+        {
+#ifdef JPEG_SUPPORT
+            ScratchConfiguration::registerImageFormat("jpg", std::make_shared<JpegImageFormatFactory>());
+#endif
+
+#ifdef PNG_SUPPORT
+            ScratchConfiguration::registerImageFormat("png", std::make_shared<PngImageFormatFactory>());
+#endif
+        }
+};
+
+static DefaultImageFormats instance;
+
+} // namespace libscratchcpp
diff --git a/src/imageformats/jpeg/CMakeLists.txt b/src/imageformats/jpeg/CMakeLists.txt
new file mode 100644
index 00000000..276e2f0f
--- /dev/null
+++ b/src/imageformats/jpeg/CMakeLists.txt
@@ -0,0 +1,9 @@
+target_sources(scratchcpp
+  PRIVATE
+    jpegimageformat.cpp
+    jpegimageformat.h
+    jpegimageformatfactory.cpp
+    jpegimageformatfactory.h
+)
+
+target_link_libraries(scratchcpp PRIVATE gd)
diff --git a/src/imageformats/jpeg/jpegimageformat.cpp b/src/imageformats/jpeg/jpegimageformat.cpp
new file mode 100644
index 00000000..45bb8802
--- /dev/null
+++ b/src/imageformats/jpeg/jpegimageformat.cpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "jpegimageformat.h"
+
+using namespace libscratchcpp;
+
+JpegImageFormat::JpegImageFormat()
+{
+}
+
+JpegImageFormat::~JpegImageFormat()
+{
+    if (m_img)
+        gdImageDestroy(m_img);
+}
+
+void JpegImageFormat::setData(unsigned int size, void *data)
+{
+    if (m_img)
+        gdImageDestroy(m_img);
+
+    m_img = gdImageCreateFromJpegPtr(size, data);
+}
+
+unsigned int JpegImageFormat::width() const
+{
+    return m_img ? gdImageSX(m_img) : 0;
+}
+
+unsigned int JpegImageFormat::height() const
+{
+    return m_img ? gdImageSY(m_img) : 0;
+}
+
+Rgb JpegImageFormat::colorAt(unsigned int x, unsigned int y, double scale) const
+{
+    if (!m_img)
+        return 0;
+
+    int color = gdImageGetPixel(m_img, x / scale, y / scale);
+
+    return rgb(gdImageRed(m_img, color), gdImageGreen(m_img, color), gdImageBlue(m_img, color));
+}
diff --git a/src/imageformats/jpeg/jpegimageformat.h b/src/imageformats/jpeg/jpegimageformat.h
new file mode 100644
index 00000000..e51a9378
--- /dev/null
+++ b/src/imageformats/jpeg/jpegimageformat.h
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <scratchcpp/iimageformat.h>
+#include <gd.h>
+
+namespace libscratchcpp
+{
+
+class JpegImageFormat : public IImageFormat
+{
+    public:
+        JpegImageFormat();
+        ~JpegImageFormat();
+
+        void setData(unsigned int size, void *data) override;
+
+        unsigned int width() const override;
+        unsigned int height() const override;
+
+        Rgb colorAt(unsigned int x, unsigned int y, double scale) const override;
+
+    private:
+        gdImagePtr m_img = nullptr;
+};
+
+} // namespace libscratchcpp
diff --git a/src/imageformats/jpeg/jpegimageformatfactory.cpp b/src/imageformats/jpeg/jpegimageformatfactory.cpp
new file mode 100644
index 00000000..fd0946d2
--- /dev/null
+++ b/src/imageformats/jpeg/jpegimageformatfactory.cpp
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "jpegimageformatfactory.h"
+#include "jpegimageformat.h"
+
+using namespace libscratchcpp;
+
+JpegImageFormatFactory::JpegImageFormatFactory()
+{
+}
+
+std::shared_ptr<IImageFormat> JpegImageFormatFactory::createInstance() const
+{
+    return std::make_shared<JpegImageFormat>();
+}
diff --git a/src/imageformats/jpeg/jpegimageformatfactory.h b/src/imageformats/jpeg/jpegimageformatfactory.h
new file mode 100644
index 00000000..bcb9c168
--- /dev/null
+++ b/src/imageformats/jpeg/jpegimageformatfactory.h
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <scratchcpp/iimageformatfactory.h>
+
+namespace libscratchcpp
+{
+
+class JpegImageFormatFactory : public IImageFormatFactory
+{
+    public:
+        JpegImageFormatFactory();
+
+        std::shared_ptr<IImageFormat> createInstance() const override;
+};
+
+} // namespace libscratchcpp
diff --git a/src/imageformats/png/CMakeLists.txt b/src/imageformats/png/CMakeLists.txt
new file mode 100644
index 00000000..da9b48ec
--- /dev/null
+++ b/src/imageformats/png/CMakeLists.txt
@@ -0,0 +1,9 @@
+target_sources(scratchcpp
+  PRIVATE
+    pngimageformat.cpp
+    pngimageformat.h
+    pngimageformatfactory.cpp
+    pngimageformatfactory.h
+)
+
+target_link_libraries(scratchcpp PRIVATE gd)
diff --git a/src/imageformats/png/pngimageformat.cpp b/src/imageformats/png/pngimageformat.cpp
new file mode 100644
index 00000000..63a86966
--- /dev/null
+++ b/src/imageformats/png/pngimageformat.cpp
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "pngimageformat.h"
+
+using namespace libscratchcpp;
+
+PngImageFormat::PngImageFormat()
+{
+}
+
+PngImageFormat::~PngImageFormat()
+{
+    if (m_img)
+        gdImageDestroy(m_img);
+}
+
+void PngImageFormat::setData(unsigned int size, void *data)
+{
+    if (m_img)
+        gdImageDestroy(m_img);
+
+    m_img = gdImageCreateFromPngPtr(size, data);
+}
+
+unsigned int PngImageFormat::width() const
+{
+    return m_img ? gdImageSX(m_img) : 0;
+}
+
+unsigned int PngImageFormat::height() const
+{
+    return m_img ? gdImageSY(m_img) : 0;
+}
+
+Rgb PngImageFormat::colorAt(unsigned int x, unsigned int y, double scale) const
+{
+    if (!m_img)
+        return 0;
+
+    int color = gdImageGetPixel(m_img, x / scale, y / scale);
+    int alpha = 127 - gdImageAlpha(m_img, color); // gdImageAlpha() returns values from 0 to 127
+
+    if (alpha == 127) // 127 should be the max (255)
+        alpha = 255;
+    else
+        alpha *= 2;
+
+    return rgba(gdImageRed(m_img, color), gdImageGreen(m_img, color), gdImageBlue(m_img, color), alpha);
+}
diff --git a/src/imageformats/png/pngimageformat.h b/src/imageformats/png/pngimageformat.h
new file mode 100644
index 00000000..7abad264
--- /dev/null
+++ b/src/imageformats/png/pngimageformat.h
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <scratchcpp/iimageformat.h>
+#include <gd.h>
+
+namespace libscratchcpp
+{
+
+class PngImageFormat : public IImageFormat
+{
+    public:
+        PngImageFormat();
+        ~PngImageFormat();
+
+        void setData(unsigned int size, void *data) override;
+
+        unsigned int width() const override;
+        unsigned int height() const override;
+
+        Rgb colorAt(unsigned int x, unsigned int y, double scale) const override;
+
+    private:
+        gdImagePtr m_img = nullptr;
+};
+
+} // namespace libscratchcpp
diff --git a/src/imageformats/png/pngimageformatfactory.cpp b/src/imageformats/png/pngimageformatfactory.cpp
new file mode 100644
index 00000000..279ad355
--- /dev/null
+++ b/src/imageformats/png/pngimageformatfactory.cpp
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "pngimageformatfactory.h"
+#include "pngimageformat.h"
+
+using namespace libscratchcpp;
+
+PngImageFormatFactory::PngImageFormatFactory()
+{
+}
+
+std::shared_ptr<IImageFormat> PngImageFormatFactory::createInstance() const
+{
+    return std::make_shared<PngImageFormat>();
+}
diff --git a/src/imageformats/png/pngimageformatfactory.h b/src/imageformats/png/pngimageformatfactory.h
new file mode 100644
index 00000000..d8aef203
--- /dev/null
+++ b/src/imageformats/png/pngimageformatfactory.h
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <scratchcpp/iimageformatfactory.h>
+
+namespace libscratchcpp
+{
+
+class PngImageFormatFactory : public IImageFormatFactory
+{
+    public:
+        PngImageFormatFactory();
+
+        std::shared_ptr<IImageFormat> createInstance() const override;
+};
+
+} // namespace libscratchcpp
diff --git a/src/imageformats/stub/CMakeLists.txt b/src/imageformats/stub/CMakeLists.txt
new file mode 100644
index 00000000..f207f428
--- /dev/null
+++ b/src/imageformats/stub/CMakeLists.txt
@@ -0,0 +1,5 @@
+target_sources(scratchcpp
+  PRIVATE
+    imageformatstub.cpp
+    imageformatstub.h
+)
diff --git a/src/imageformats/stub/imageformatstub.cpp b/src/imageformats/stub/imageformatstub.cpp
new file mode 100644
index 00000000..a88bd295
--- /dev/null
+++ b/src/imageformats/stub/imageformatstub.cpp
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "imageformatstub.h"
+
+namespace libscratchcpp
+{
+
+ImageFormatStub::ImageFormatStub()
+{
+}
+
+void ImageFormatStub::setData(unsigned int size, void *)
+{
+}
+
+unsigned int ImageFormatStub::width() const
+{
+    return 0;
+}
+
+unsigned int ImageFormatStub::height() const
+{
+    return 0;
+}
+
+Rgb ImageFormatStub::colorAt(unsigned int, unsigned int, double) const
+{
+    return 0;
+}
+
+} // namespace libscratchcpp
diff --git a/src/imageformats/stub/imageformatstub.h b/src/imageformats/stub/imageformatstub.h
new file mode 100644
index 00000000..b8dac6f2
--- /dev/null
+++ b/src/imageformats/stub/imageformatstub.h
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <scratchcpp/iimageformat.h>
+
+namespace libscratchcpp
+{
+
+class ImageFormatStub : public IImageFormat
+{
+    public:
+        ImageFormatStub();
+
+        void setData(unsigned int size, void *) override;
+
+        unsigned int width() const override;
+        unsigned int height() const override;
+
+        Rgb colorAt(unsigned int, unsigned int, double) const override;
+};
+
+} // namespace libscratchcpp
diff --git a/src/internal/scratch3reader.cpp b/src/internal/scratch3reader.cpp
index 052bf238..1a36531c 100644
--- a/src/internal/scratch3reader.cpp
+++ b/src/internal/scratch3reader.cpp
@@ -14,7 +14,6 @@
 
 #include "scratch3reader.h"
 #include "reader_common.h"
-#include "zipreader.h"
 
 using namespace libscratchcpp;
 using json = nlohmann::json;
@@ -194,10 +193,6 @@ bool Scratch3Reader::load()
 
             // TODO: Add comments
 
-            // currentCostume
-            READER_STEP(step, "target -> currentCostume");
-            target->setCurrentCostume(jsonTarget["currentCostume"]);
-
             // costumes
             READER_STEP(step, "target -> costumes");
             auto costumes = jsonTarget["costumes"];
@@ -211,9 +206,18 @@ bool Scratch3Reader::load()
                 costume->setRotationCenterX(jsonCostume["rotationCenterX"]);
                 READER_STEP(step, "target -> costume -> rotationCenterY");
                 costume->setRotationCenterY(jsonCostume["rotationCenterY"]);
+
+                void *data;
+                unsigned int size = m_zipReader->readFile(costume->fileName(), &data);
+                costume->setData(size, data);
+
                 target->addCostume(costume);
             }
 
+            // currentCostume
+            READER_STEP(step, "target -> currentCostume");
+            target->setCurrentCostume(jsonToValue(jsonTarget["currentCostume"]).toInt() + 1);
+
             // sounds
             READER_STEP(step, "target -> sounds");
             auto sounds = jsonTarget["sounds"];
@@ -360,11 +364,11 @@ std::vector<std::string> Scratch3Reader::extensions()
 void Scratch3Reader::read()
 {
     // Read project.json
-    ZipReader reader(fileName());
-    if (reader.open()) {
+    m_zipReader = std::make_unique<ZipReader>(fileName());
+    if (m_zipReader->open()) {
         // Parse the JSON
         try {
-            m_json = json::parse(reader.readFileToString("project.json"));
+            m_json = json::parse(m_zipReader->readFileToString("project.json"));
         } catch (std::exception &e) {
             printErr("invalid JSON file", e.what());
         }
diff --git a/src/internal/scratch3reader.h b/src/internal/scratch3reader.h
index 528895b4..1b010daf 100644
--- a/src/internal/scratch3reader.h
+++ b/src/internal/scratch3reader.h
@@ -5,6 +5,8 @@
 #include "iprojectreader.h"
 #include <nlohmann/json.hpp>
 
+#include "zipreader.h"
+
 namespace libscratchcpp
 {
 
@@ -20,6 +22,7 @@ class Scratch3Reader : public IProjectReader
 
     private:
         void read();
+        std::unique_ptr<ZipReader> m_zipReader;
         nlohmann::json m_json = "";
         std::vector<std::shared_ptr<Target>> m_targets;
         std::vector<std::shared_ptr<Broadcast>> m_broadcasts;
diff --git a/src/rect.cpp b/src/rect.cpp
new file mode 100644
index 00000000..7e93f08d
--- /dev/null
+++ b/src/rect.cpp
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <scratchcpp/rect.h>
+#include <cmath>
+#include "rect_p.h"
+
+using namespace libscratchcpp;
+
+/*! Constructs Rect. */
+Rect::Rect(double left, double top, double right, double bottom) :
+    impl(spimpl::make_impl<RectPrivate>(left, top, right, bottom))
+{
+}
+
+/*! \copydoc Rect() */
+Rect::Rect() :
+    impl(spimpl::make_impl<RectPrivate>())
+{
+}
+
+/*! Returns the x-coordinate of the left edge. */
+double Rect::left() const
+{
+    return impl->left;
+}
+
+/*! Sets the x-coordinate of the left edge. */
+void Rect::setLeft(double left)
+{
+    impl->left = left;
+}
+
+/*! Returns the y-coordinate of the top edge. */
+double Rect::top() const
+{
+    return impl->top;
+}
+
+/*! Sets the y-coordinate of the top edge. */
+void Rect::setTop(double top)
+{
+    impl->top = top;
+}
+
+/*! Returns the x-coordinate of the right edge. */
+double Rect::right() const
+{
+    return impl->right;
+}
+
+/*! Sets the x-coordinate of the right edge. */
+void Rect::setRight(double right)
+{
+    impl->right = right;
+}
+
+/*! Returns the y-coordinate of the bottom edge. */
+double Rect::bottom() const
+{
+    return impl->bottom;
+}
+
+/*! Sets the y-coordinate of the bottom edge. */
+void Rect::setBottom(double bottom)
+{
+    impl->bottom = bottom;
+}
+
+/*! Returns the width of the rectangle. */
+double Rect::width() const
+{
+    return std::abs(impl->right - impl->left);
+}
+
+/*! Returns the height of the rectangle. */
+double Rect::height() const
+{
+    return std::abs(impl->top - impl->bottom);
+}
diff --git a/src/rect_p.cpp b/src/rect_p.cpp
new file mode 100644
index 00000000..b69464f7
--- /dev/null
+++ b/src/rect_p.cpp
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "rect_p.h"
+
+namespace libscratchcpp
+{
+
+RectPrivate::RectPrivate(double left, double top, double right, double bottom) :
+    left(left),
+    top(top),
+    right(right),
+    bottom(bottom)
+{
+}
+
+RectPrivate::RectPrivate()
+{
+}
+
+} // namespace libscratchcpp
diff --git a/src/rect_p.h b/src/rect_p.h
new file mode 100644
index 00000000..cf451544
--- /dev/null
+++ b/src/rect_p.h
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+namespace libscratchcpp
+{
+
+struct RectPrivate
+{
+        RectPrivate(double left, double top, double right, double bottom);
+        RectPrivate();
+
+        double left = 0;
+        double top = 0;
+        double right = 0;
+        double bottom = 0;
+};
+
+} // namespace libscratchcpp
diff --git a/src/scratch/CMakeLists.txt b/src/scratch/CMakeLists.txt
index aa2a9d0e..6cde28d3 100644
--- a/src/scratch/CMakeLists.txt
+++ b/src/scratch/CMakeLists.txt
@@ -45,4 +45,7 @@ target_sources(scratchcpp
     entity.cpp
     entity_p.cpp
     entity_p.h
+    keyevent.cpp
+    keyevent_p.cpp
+    keyevent_p.h
 )
diff --git a/src/scratch/asset.cpp b/src/scratch/asset.cpp
index 7b000a94..bad195a4 100644
--- a/src/scratch/asset.cpp
+++ b/src/scratch/asset.cpp
@@ -38,3 +38,23 @@ const std::string &Asset::dataFormat() const
 {
     return impl->dataFormat;
 }
+
+/*! Returns the asset data. */
+const void *Asset::data() const
+{
+    return impl->data;
+}
+
+/*! Returns the size of the asset data. */
+unsigned int Asset::dataSize() const
+{
+    return impl->dataSize;
+}
+
+/*! Sets the asset data. */
+void Asset::setData(unsigned int size, void *data)
+{
+    impl->dataSize = size;
+    impl->data = data;
+    processData(size, data);
+}
diff --git a/src/scratch/asset_p.h b/src/scratch/asset_p.h
index f1c2a16f..81cd3219 100644
--- a/src/scratch/asset_p.h
+++ b/src/scratch/asset_p.h
@@ -17,6 +17,8 @@ struct AssetPrivate
         std::string name;
         std::string dataFormat;
         std::string fileName;
+        const void *data = nullptr;
+        unsigned int dataSize = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/src/scratch/block.cpp b/src/scratch/block.cpp
index 619950ac..5ad69cd6 100644
--- a/src/scratch/block.cpp
+++ b/src/scratch/block.cpp
@@ -168,6 +168,8 @@ int Block::addInput(std::shared_ptr<Input> input)
         return it - impl->inputs.begin();
 
     impl->inputs.push_back(input);
+    impl->inputMap[input->inputId()] = input.get();
+
     return impl->inputs.size() - 1;
 }
 
@@ -197,10 +199,15 @@ Input *Block::findInputById(int id) const
 {
     if (impl->inputMap.count(id) == 1)
         return impl->inputMap.at(id);
+    else {
+        auto it = std::find_if(impl->inputs.begin(), impl->inputs.end(), [id](std::shared_ptr<Input> input) { return input->inputId() == id; });
+
+        if (it != impl->inputs.end())
+            return it->get();
+    }
     return nullptr;
 }
 
-/*! Updates the map that assigns input IDs to input indexes. Used internally by Engine. */
 void Block::updateInputMap()
 {
     impl->inputMap.clear();
@@ -223,6 +230,8 @@ int Block::addField(std::shared_ptr<Field> field)
         return it - impl->fields.begin();
 
     impl->fields.push_back(field);
+    impl->fieldMap[field->fieldId()] = field.get();
+
     return impl->fields.size() - 1;
 }
 
@@ -252,10 +261,15 @@ Field *Block::findFieldById(int id) const
 {
     if (impl->fieldMap.count(id) == 1)
         return impl->fieldMap.at(id);
+    else {
+        auto it = std::find_if(impl->fields.begin(), impl->fields.end(), [id](std::shared_ptr<Field> field) { return field->fieldId() == id; });
+
+        if (it != impl->fields.end())
+            return it->get();
+    }
     return nullptr;
 }
 
-/*! Updates the map that assigns input IDs to input indexes. Used internally by Engine. */
 void Block::updateFieldMap()
 {
     impl->fieldMap.clear();
diff --git a/src/scratch/costume.cpp b/src/scratch/costume.cpp
index dbb6e1eb..db651073 100644
--- a/src/scratch/costume.cpp
+++ b/src/scratch/costume.cpp
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include <scratchcpp/costume.h>
+#include <scratchcpp/scratchconfiguration.h>
 
 #include "costume_p.h"
 
@@ -11,6 +12,9 @@ Costume::Costume(const std::string &name, const std::string &id, const std::stri
     Asset(name, id, format),
     impl(spimpl::make_unique_impl<CostumePrivate>())
 {
+    // NOTE: image is always initialized, so there's no need for null checks
+    impl->image = ScratchConfiguration::createImageFormat(format);
+    impl->updateImage();
 }
 
 /*! Returns the reciprocal of the costume scaling factor for bitmap costumes. */
@@ -22,7 +26,11 @@ double Costume::bitmapResolution() const
 /*! Sets the reciprocal of the costume scaling factor for bitmap costumes. */
 void Costume::setBitmapResolution(double newBitmapResolution)
 {
+    if (impl->bitmapResolution == newBitmapResolution)
+        return;
+
     impl->bitmapResolution = newBitmapResolution;
+    impl->updateImage();
 }
 
 /*! Returns the x-coordinate of the rotation center. */
@@ -49,6 +57,92 @@ void Costume::setRotationCenterY(int newRotationCenterY)
     impl->rotationCenterY = newRotationCenterY;
 }
 
+/*! Returns the costume width. */
+unsigned int Costume::width() const
+{
+    return impl->image->width() * impl->scale / impl->bitmapResolution;
+}
+
+/*! Returns the costume height. */
+unsigned int Costume::height() const
+{
+    return impl->image->height() * impl->scale / impl->bitmapResolution;
+}
+
+/*! Returns the image scale. */
+double Costume::scale() const
+{
+    return impl->scale;
+}
+
+/*! Sets the image scale (this is automatically set by the sprite). */
+void Costume::setScale(double scale)
+{
+    if (impl->scale == scale)
+        return;
+
+    impl->scale = scale;
+    impl->updateImage();
+}
+
+/*! Returns true if the costume image is mirrored horizontally. */
+bool Costume::mirrorHorizontally() const
+{
+    return impl->mirrorHorizontally;
+}
+
+/*! Sets whether the costume image is mirrored horizontally (this is automatically set by the sprite). */
+void Costume::setMirrorHorizontally(bool mirror)
+{
+    if (impl->mirrorHorizontally == mirror)
+        return;
+
+    impl->mirrorHorizontally = mirror;
+    impl->updateImage();
+}
+
+/*! Returns the bitmap of the costume (an array of pixel rows). */
+Rgb **Costume::bitmap() const
+{
+    return impl->bitmap;
+}
+
+/*! Returns the value of the given graphics effect. */
+double Costume::graphicsEffectValue(IGraphicsEffect *effect) const
+{
+    auto it = impl->graphicsEffects.find(effect);
+
+    if (it == impl->graphicsEffects.cend())
+        return 0;
+    else
+        return it->second;
+}
+
+/*! Sets the value of the given graphics effect (this is automatically set by the sprite). */
+void Costume::setGraphicsEffectValue(IGraphicsEffect *effect, double value)
+{
+    auto it = impl->graphicsEffects.find(effect);
+    bool update = ((it == impl->graphicsEffects.cend()) || (it->second != value));
+
+    if (value == 0)
+        impl->graphicsEffects.erase(effect);
+    else
+        impl->graphicsEffects[effect] = value;
+
+    if (update)
+        impl->updateImage();
+}
+
+/*! Clears all graphics effects (this is automatically called by the sprite). */
+void Costume::clearGraphicsEffects()
+{
+    bool update = !impl->graphicsEffects.empty();
+    impl->graphicsEffects.clear();
+
+    if (update)
+        impl->updateImage();
+}
+
 /*!
  * Returns the Broadcast linked with this costume.
  * \note This is used by the "switch backdrop to and wait" block.
@@ -57,3 +151,10 @@ Broadcast *Costume::broadcast()
 {
     return &impl->broadcast;
 }
+
+void Costume::processData(unsigned int size, void *data)
+{
+    impl->image->setData(size, data);
+    impl->freeImage();
+    impl->updateImage();
+}
diff --git a/src/scratch/costume_p.cpp b/src/scratch/costume_p.cpp
index 0b70f216..e07db8aa 100644
--- a/src/scratch/costume_p.cpp
+++ b/src/scratch/costume_p.cpp
@@ -1,5 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
+#include <scratchcpp/igraphicseffect.h>
+
 #include "costume_p.h"
 
 using namespace libscratchcpp;
@@ -8,3 +10,53 @@ CostumePrivate::CostumePrivate() :
     broadcast("", "")
 {
 }
+
+CostumePrivate::~CostumePrivate()
+{
+    freeImage();
+}
+
+void CostumePrivate::updateImage()
+{
+    unsigned int scaledWidth = image->width() * scale / bitmapResolution;
+    unsigned int scaledHeight = image->height() * scale / bitmapResolution;
+
+    if (scaledWidth == 0 || scaledHeight == 0) {
+        freeImage();
+        return;
+    }
+
+    if (!bitmap || (scale != oldScale) || (bitmapResolution != oldBitmapResolution)) {
+        freeImage();
+        bitmap = static_cast<Rgb **>(malloc(sizeof(Rgb *) * scaledHeight));
+
+        for (unsigned int i = 0; i < scaledHeight; i++)
+            bitmap[i] = static_cast<Rgb *>(malloc(sizeof(Rgb) * scaledWidth));
+
+        oldScale = scale;
+        oldBitmapResolution = bitmapResolution;
+        oldWidth = scaledWidth;
+        oldHeight = scaledHeight;
+    }
+
+    double actualScale = scale / bitmapResolution;
+
+    for (unsigned int i = 0; i < scaledHeight; i++) {
+        for (unsigned int j = 0; j < scaledWidth; j++)
+            bitmap[i][j] = image->colorAt(mirrorHorizontally ? (scaledWidth - 1 - j) : j, i, actualScale);
+    }
+
+    for (const auto &[effect, value] : graphicsEffects)
+        effect->apply(bitmap, scaledWidth, scaledHeight, value);
+}
+
+void CostumePrivate::freeImage()
+{
+    if (bitmap) {
+        for (unsigned int i = 0; i < oldHeight; i++)
+            free(bitmap[i]);
+
+        free(bitmap);
+        bitmap = nullptr;
+    }
+}
diff --git a/src/scratch/costume_p.h b/src/scratch/costume_p.h
index 05c50a5d..155e75ca 100644
--- a/src/scratch/costume_p.h
+++ b/src/scratch/costume_p.h
@@ -3,18 +3,35 @@
 #pragma once
 
 #include <scratchcpp/broadcast.h>
+#include <scratchcpp/iimageformat.h>
+#include <unordered_map>
 
 namespace libscratchcpp
 {
 
+class IGraphicsEffect;
+
 struct CostumePrivate
 {
         CostumePrivate();
         CostumePrivate(const CostumePrivate &) = delete;
+        ~CostumePrivate();
+
+        void updateImage();
+        void freeImage();
 
+        double oldBitmapResolution = 1;
         double bitmapResolution = 1;
         int rotationCenterX = 0;
         int rotationCenterY = 0;
+        std::shared_ptr<IImageFormat> image;
+        Rgb **bitmap = nullptr;
+        double oldWidth = 0;
+        double oldHeight = 0;
+        double oldScale = 1;
+        double scale = 1;
+        bool mirrorHorizontally = false;
+        std::unordered_map<IGraphicsEffect *, double> graphicsEffects;
         Broadcast broadcast;
 };
 
diff --git a/src/scratch/keyevent.cpp b/src/scratch/keyevent.cpp
new file mode 100644
index 00000000..a63d124e
--- /dev/null
+++ b/src/scratch/keyevent.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <scratchcpp/keyevent.h>
+
+#include "keyevent_p.h"
+
+namespace libscratchcpp
+{
+
+/*! Constructs KeyEvent. */
+KeyEvent::KeyEvent(Type type) :
+    impl(spimpl::make_impl<KeyEventPrivate>(type))
+{
+}
+
+/*! Constructs KeyEvent from a key name. */
+KeyEvent::KeyEvent(const std::string &name) :
+    impl(spimpl::make_impl<KeyEventPrivate>(name))
+{
+}
+
+/*! Returns the type of the key. */
+KeyEvent::Type KeyEvent::type() const
+{
+    return impl->type;
+}
+
+/*! Returns the name of the key. */
+const std::string &KeyEvent::name() const
+{
+    return impl->name;
+}
+
+} // namespace libscratchcpp
diff --git a/src/scratch/keyevent_p.cpp b/src/scratch/keyevent_p.cpp
new file mode 100644
index 00000000..ed22c467
--- /dev/null
+++ b/src/scratch/keyevent_p.cpp
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <unordered_map>
+#include <algorithm>
+
+#include "keyevent_p.h"
+
+namespace libscratchcpp
+{
+
+static std::unordered_map<KeyEvent::Type, std::string> KEY_NAME = {
+    { KeyEvent::Type::Space, "space" },       { KeyEvent::Type::Left, "left arrow" }, { KeyEvent::Type::Up, "up arrow" },
+    { KeyEvent::Type::Right, "right arrow" }, { KeyEvent::Type::Down, "down arrow" }, { KeyEvent::Type::Enter, "enter" }
+};
+
+KeyEventPrivate::KeyEventPrivate(KeyEvent::Type type) :
+    type(type)
+{
+    auto it = KEY_NAME.find(type);
+
+    if (it == KEY_NAME.cend())
+        this->type = KeyEvent::Type::Any;
+    else
+        name = it->second;
+}
+
+KeyEventPrivate::KeyEventPrivate(const std::string &name) :
+    name(name)
+{
+    // See https://github.com/scratchfoundation/scratch-vm/blob/2a00145b8a968b04f278808858f79fa6d6e79946/src/io/keyboard.js#L93
+
+    // Convert from ASCII if a number was dropped in
+    try {
+        double numberDouble = std::stod(name);
+        long number = static_cast<long>(numberDouble);
+
+        if (numberDouble == number) {
+
+            if (number >= 48 && number <= 90) {
+                this->name = static_cast<char>(number);
+                convertNameToLowercase();
+
+                return;
+            }
+
+            switch (number) {
+                case 32:
+                    type = KeyEvent::Type::Space;
+                    break;
+
+                case 37:
+                    type = KeyEvent::Type::Left;
+                    break;
+
+                case 38:
+                    type = KeyEvent::Type::Up;
+                    break;
+
+                case 39:
+                    type = KeyEvent::Type::Right;
+                    break;
+
+                case 40:
+                    type = KeyEvent::Type::Down;
+                    break;
+
+                default:
+                    break;
+            }
+
+            if (type != KeyEvent::Type::Any) {
+                this->name = KEY_NAME[type];
+                return;
+            }
+        }
+    } catch (...) {
+    }
+
+    // Check for a special key
+    for (const auto &[key, value] : KEY_NAME) {
+        if (value == name) {
+            type = key;
+            return;
+        }
+    }
+
+    // Use only the first character
+    if (this->name.size() > 1)
+        this->name = this->name[0];
+
+    // Check for the space character
+    if (this->name == " ") {
+        type = KeyEvent::Type::Space;
+        this->name = KEY_NAME[type];
+    }
+
+    convertNameToLowercase();
+}
+
+void KeyEventPrivate::convertNameToLowercase()
+{
+    std::transform(this->name.begin(), this->name.end(), this->name.begin(), ::tolower);
+}
+
+} // namespace libscratchcpp
diff --git a/src/scratch/keyevent_p.h b/src/scratch/keyevent_p.h
new file mode 100644
index 00000000..4da4984a
--- /dev/null
+++ b/src/scratch/keyevent_p.h
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <scratchcpp/keyevent.h>
+#include <string>
+
+namespace libscratchcpp
+{
+
+struct KeyEventPrivate
+{
+        KeyEventPrivate(KeyEvent::Type type);
+        KeyEventPrivate(const std::string &name);
+
+        void convertNameToLowercase();
+
+        KeyEvent::Type type = KeyEvent::Type::Any;
+        std::string name;
+};
+
+} // namespace libscratchcpp
diff --git a/src/scratch/list.cpp b/src/scratch/list.cpp
index 3ac5d472..22e4af74 100644
--- a/src/scratch/list.cpp
+++ b/src/scratch/list.cpp
@@ -26,6 +26,18 @@ void List::setName(const std::string &name)
     impl->name = name;
 }
 
+/*! Returns the sprite or stage this list belongs to. */
+Target *List::target() const
+{
+    return impl->target;
+}
+
+/*! Sets the sprite or stage this list belongs to. */
+void List::setTarget(Target *target)
+{
+    impl->target = target;
+}
+
 /*! Returns the index of the given item. */
 long List::indexOf(const Value &value) const
 {
diff --git a/src/scratch/list_p.h b/src/scratch/list_p.h
index 6dbcfecb..95a4196c 100644
--- a/src/scratch/list_p.h
+++ b/src/scratch/list_p.h
@@ -7,11 +7,14 @@
 namespace libscratchcpp
 {
 
+class Target;
+
 struct ListPrivate
 {
         ListPrivate(const std::string &name);
 
         std::string name;
+        Target *target = nullptr;
 };
 
 } // namespace libscratchcpp
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index 85c30317..5a220138 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -5,6 +5,8 @@
 #include <scratchcpp/iengine.h>
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
+#include <scratchcpp/costume.h>
+#include <scratchcpp/rect.h>
 #include <cassert>
 
 #include "sprite_p.h"
@@ -14,7 +16,7 @@ using namespace libscratchcpp;
 /*! Constructs Sprite. */
 Sprite::Sprite() :
     Target(),
-    impl(spimpl::make_unique_impl<SpritePrivate>())
+    impl(spimpl::make_unique_impl<SpritePrivate>(this))
 {
 }
 
@@ -22,6 +24,16 @@ Sprite::Sprite() :
 Sprite::~Sprite()
 {
     if (isClone()) {
+        IEngine *eng = engine();
+
+        if (eng) {
+            eng->deinitClone(this);
+
+            auto children = allChildren();
+            for (auto child : children)
+                eng->deinitClone(child.get());
+        }
+
         assert(impl->cloneParent);
         impl->cloneParent->impl->removeClone(this);
     }
@@ -150,7 +162,8 @@ double Sprite::x() const
 /*! Sets the X position of the sprite. */
 void Sprite::setX(double newX)
 {
-    impl->x = newX;
+    setXY(newX, impl->y);
+
     if (impl->iface)
         impl->iface->onXChanged(impl->x);
 }
@@ -164,7 +177,8 @@ double Sprite::y() const
 /*! Sets the Y position of the sprite. */
 void Sprite::setY(double newY)
 {
-    impl->y = newY;
+    setXY(impl->x, newY);
+
     if (impl->iface)
         impl->iface->onYChanged(impl->y);
 }
@@ -181,6 +195,29 @@ void Sprite::setSize(double newSize)
     impl->size = newSize;
     if (impl->iface)
         impl->iface->onSizeChanged(impl->size);
+
+    // TODO: Make currentCostume() return the costume (not index)
+    auto costume = costumeAt(currentCostume() - 1);
+
+    if (costume)
+        costume->setScale(newSize / 100);
+}
+
+/*! Overrides Target#setCurrentCostume(). */
+void Sprite::setCurrentCostume(int newCostume)
+{
+    // TODO: Make currentCostume() return the costume (not index)
+    auto costume = costumeAt(newCostume - 1);
+
+    if (costume) {
+        costume->setScale(impl->size / 100);
+        costume->setMirrorHorizontally(impl->rotationStyle == RotationStyle::LeftRight);
+
+        for (const auto &[effect, value] : impl->graphicsEffects)
+            costume->setGraphicsEffectValue(effect, value);
+    }
+
+    Target::setCurrentCostume(newCostume);
 }
 
 /*! Returns the direction. */
@@ -239,6 +276,12 @@ std::string Sprite::rotationStyleStr() const
 void Sprite::setRotationStyle(RotationStyle newRotationStyle)
 {
     impl->rotationStyle = newRotationStyle;
+    // TODO: Make currentCostume() return the costume (not index)
+    auto costume = costumeAt(currentCostume() - 1);
+
+    if (costume)
+        costume->setMirrorHorizontally(newRotationStyle == RotationStyle::LeftRight);
+
     if (impl->iface)
         impl->iface->onRotationStyleChanged(impl->rotationStyle);
 }
@@ -260,7 +303,62 @@ void Sprite::setRotationStyle(const char *newRotationStyle)
     setRotationStyle(std::string(newRotationStyle));
 }
 
+/*! Returns the bounding rectangle of the sprite. */
+Rect Sprite::boundingRect() const
+{
+    Rect ret;
+    impl->getBoundingRect(&ret);
+
+    return ret;
+}
+
+/*! Returns the value of the given graphics effect. */
+double Sprite::graphicsEffectValue(IGraphicsEffect *effect) const
+{
+    auto it = impl->graphicsEffects.find(effect);
+
+    if (it == impl->graphicsEffects.cend())
+        return 0;
+    else
+        return it->second;
+}
+
+/*! Sets the value of the given graphics effect. */
+void Sprite::setGraphicsEffectValue(IGraphicsEffect *effect, double value)
+{
+    impl->graphicsEffects[effect] = value;
+
+    // TODO: Make currentCostume() return the costume (not index)
+    auto costume = costumeAt(currentCostume() - 1);
+
+    if (costume)
+        costume->setGraphicsEffectValue(effect, value);
+}
+
+/*! Sets the value of all graphics effects to 0 (clears them). */
+void Sprite::clearGraphicsEffects()
+{
+    impl->graphicsEffects.clear();
+
+    // TODO: Make currentCostume() return the costume (not index)
+    auto costume = costumeAt(currentCostume() - 1);
+
+    if (costume)
+        costume->clearGraphicsEffects();
+}
+
 Target *Sprite::dataSource() const
 {
     return impl->cloneRoot;
 }
+
+void Sprite::setXY(double x, double y)
+{
+    IEngine *eng = engine();
+
+    if (eng && !eng->spriteFencingEnabled()) {
+        impl->x = x;
+        impl->y = y;
+    } else
+        impl->getFencedPosition(x, y, &impl->x, &impl->y);
+}
diff --git a/src/scratch/sprite_p.cpp b/src/scratch/sprite_p.cpp
index 22267c42..87c7c9d9 100644
--- a/src/scratch/sprite_p.cpp
+++ b/src/scratch/sprite_p.cpp
@@ -1,12 +1,20 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include <scratchcpp/ispritehandler.h>
+#include <scratchcpp/rect.h>
+#include <scratchcpp/costume.h>
+#include <scratchcpp/iengine.h>
+#include <cmath>
 
 #include "sprite_p.h"
 
 using namespace libscratchcpp;
 
-SpritePrivate::SpritePrivate()
+static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20
+static const double FENCE_WIDTH = 15;
+
+SpritePrivate::SpritePrivate(Sprite *sprite) :
+    sprite(sprite)
 {
 }
 
@@ -28,3 +36,99 @@ void SpritePrivate::setCostumeData(const char *data)
     if (iface)
         iface->onCostumeChanged(data);
 }
+
+void SpritePrivate::getBoundingRect(Rect *out) const
+{
+    assert(out);
+    assert(sprite);
+    // TODO: Make currentCostume() return the costume
+    auto costume = sprite->costumeAt(sprite->currentCostume() - 1);
+
+    if (!costume) {
+        out->setLeft(x);
+        out->setTop(y);
+        out->setRight(x);
+        out->setBottom(y);
+        return;
+    }
+
+    double cosTheta = std::cos((90 - direction) * pi / 180);
+    double sinTheta = std::sin((90 - direction) * pi / 180);
+    double maxX = 0, maxY = 0, minX = 0, minY = 0;
+    bool firstPixel = true;
+    unsigned int width = costume->width();
+    unsigned int height = costume->height();
+    double rotationCenterX = width / 2.0 + costume->rotationCenterX();
+    double rotationCenterY = height / 2.0 + costume->rotationCenterY();
+    Rgb **bitmap = costume->bitmap();
+
+    for (unsigned int y = 0; y < height; y++) {
+        for (unsigned int x = 0; x < width; x++) {
+            if (bitmap[y][x] != rgba(0, 0, 0, 0)) {
+                double rotatedX = ((x - rotationCenterX) * cosTheta - (y - rotationCenterY) * sinTheta);
+                double rotatedY = ((x - rotationCenterX) * sinTheta + (y - rotationCenterY) * cosTheta);
+
+                if (firstPixel) {
+                    firstPixel = false;
+                    minX = maxX = rotatedX;
+                    minY = maxY = rotatedY;
+                } else {
+                    if (rotatedX < minX)
+                        minX = rotatedX;
+                    else if (rotatedX > maxX)
+                        maxX = rotatedX;
+
+                    if (rotatedY < minY)
+                        minY = rotatedY;
+                    else if (rotatedY > maxY)
+                        maxY = rotatedY;
+                }
+            }
+        }
+    }
+
+    out->setLeft(x + minX);
+    out->setTop(y + maxY);
+    out->setRight(x + maxX);
+    out->setBottom(y + minY);
+}
+
+void SpritePrivate::getFencedPosition(double x, double y, double *outX, double *outY) const
+{
+    assert(outX);
+    assert(outY);
+
+    if (!sprite->engine()) {
+        *outX = x;
+        *outY = y;
+        return;
+    }
+
+    // https://github.com/scratchfoundation/scratch-render/blob/0b51e5a66ae1c8102fe881107145d7ef3d71a1ab/src/RenderWebGL.js#L1526
+    double dx = x - this->x;
+    double dy = y - this->y;
+    Rect rect;
+    getBoundingRect(&rect);
+    double inset = std::floor(std::min(rect.width(), rect.height()) / 2);
+
+    double xRight = sprite->engine()->stageWidth() / 2;
+    double sx = xRight - std::min(FENCE_WIDTH, inset);
+
+    if (rect.right() + dx < -sx) {
+        x = std::ceil(this->x - (sx + rect.right()));
+    } else if (rect.left() + dx > sx) {
+        x = std::floor(this->x + (sx - rect.left()));
+    }
+
+    double yTop = sprite->engine()->stageHeight() / 2;
+    double sy = yTop - std::min(FENCE_WIDTH, inset);
+
+    if (rect.top() + dy < -sy) {
+        y = std::ceil(this->y - (sy + rect.top()));
+    } else if (rect.bottom() + dy > sy) {
+        y = std::floor(this->y + (sy - rect.bottom()));
+    }
+
+    *outX = x;
+    *outY = y;
+}
diff --git a/src/scratch/sprite_p.h b/src/scratch/sprite_p.h
index 84ca01ea..26a439ae 100644
--- a/src/scratch/sprite_p.h
+++ b/src/scratch/sprite_p.h
@@ -3,19 +3,25 @@
 #pragma once
 
 #include <scratchcpp/sprite.h>
+#include <unordered_map>
 
 namespace libscratchcpp
 {
 
+class Rect;
+
 struct SpritePrivate
 {
-        SpritePrivate();
+        SpritePrivate(Sprite *sprite);
         SpritePrivate(const SpritePrivate &) = delete;
 
         void removeClone(Sprite *clone);
 
         void setCostumeData(const char *data);
+        void getBoundingRect(Rect *out) const;
+        void getFencedPosition(double inX, double inY, double *outX, double *outY) const;
 
+        Sprite *sprite = nullptr;
         ISpriteHandler *iface = nullptr;
         Sprite *cloneRoot = nullptr;
         Sprite *cloneParent = nullptr;
@@ -27,6 +33,7 @@ struct SpritePrivate
         double direction = 90;
         bool draggable = false;
         Sprite::RotationStyle rotationStyle = Sprite::RotationStyle::AllAround;
+        std::unordered_map<IGraphicsEffect *, double> graphicsEffects;
 };
 
 } // namespace libscratchcpp
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index ff2ddc14..dd5696db 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -42,6 +42,8 @@ int Target::addVariable(std::shared_ptr<Variable> variable)
         return it - impl->variables.begin();
 
     impl->variables.push_back(variable);
+    variable->setTarget(this);
+
     return impl->variables.size() - 1;
 }
 
@@ -93,6 +95,8 @@ int Target::addList(std::shared_ptr<List> list)
         return it - impl->lists.begin();
 
     impl->lists.push_back(list);
+    list->setTarget(this);
+
     return impl->lists.size() - 1;
 }
 
@@ -203,7 +207,8 @@ int Target::currentCostume() const
 /*! Sets the ID of the current costume. */
 void Target::setCurrentCostume(int newCostume)
 {
-    impl->currentCostume = newCostume;
+    if (newCostume > 0 && newCostume <= costumes().size())
+        impl->currentCostume = newCostume;
 }
 
 /*! Returns the list of costumes. */
diff --git a/src/scratch/target_p.h b/src/scratch/target_p.h
index 4ea13e05..da4126f2 100644
--- a/src/scratch/target_p.h
+++ b/src/scratch/target_p.h
@@ -26,7 +26,7 @@ struct TargetPrivate
         std::vector<std::shared_ptr<Variable>> variables;
         std::vector<std::shared_ptr<List>> lists;
         std::vector<std::shared_ptr<Block>> blocks;
-        int currentCostume = 1;
+        int currentCostume = 0;
         std::vector<std::shared_ptr<Costume>> costumes;
         std::vector<std::shared_ptr<Sound>> sounds;
         int layerOrder = 0;
diff --git a/src/scratch/variable.cpp b/src/scratch/variable.cpp
index 6edf7450..13f47996 100644
--- a/src/scratch/variable.cpp
+++ b/src/scratch/variable.cpp
@@ -49,6 +49,18 @@ void Variable::setIsCloudVariable(bool isCloudVariable)
     impl->isCloudVariable = isCloudVariable;
 }
 
+/*! Returns the sprite or stage this variable belongs to. */
+Target *Variable::target() const
+{
+    return impl->target;
+}
+
+/*! Sets the sprite or stage this variable belongs to. */
+void Variable::setTarget(Target *target)
+{
+    impl->target = target;
+}
+
 /*! Creates a copy of the variable. */
 std::shared_ptr<Variable> Variable::clone()
 {
diff --git a/src/scratch/variable_p.h b/src/scratch/variable_p.h
index 5d2d324d..ac5edd8f 100644
--- a/src/scratch/variable_p.h
+++ b/src/scratch/variable_p.h
@@ -7,6 +7,8 @@
 namespace libscratchcpp
 {
 
+class Target;
+
 struct VariablePrivate
 {
         VariablePrivate(const std::string &name, const Value &value = Value(), bool isCloudVariable = false);
@@ -15,6 +17,7 @@ struct VariablePrivate
         std::string name;
         Value value;
         bool isCloudVariable;
+        Target *target = nullptr;
 };
 
 } // namespace libscratchcpp
diff --git a/src/scratchconfiguration.cpp b/src/scratchconfiguration.cpp
index c8ab05ce..5e483585 100644
--- a/src/scratchconfiguration.cpp
+++ b/src/scratchconfiguration.cpp
@@ -1,9 +1,13 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include <scratchcpp/scratchconfiguration.h>
+#include <scratchcpp/iimageformatfactory.h>
+#include <scratchcpp/igraphicseffect.h>
+#include <iostream>
 
 #include "scratchconfiguration_p.h"
 #include "blocks/standardblocks.h"
+#include "imageformats/stub/imageformatstub.h"
 
 using namespace libscratchcpp;
 
@@ -21,6 +25,58 @@ IExtension *ScratchConfiguration::getExtension(const std::string &name)
     return impl->getExtension(name);
 }
 
+/*! Registers the given image format. */
+void ScratchConfiguration::registerImageFormat(const std::string &name, std::shared_ptr<IImageFormatFactory> formatFactory)
+{
+    impl->imageFormats[name] = formatFactory;
+}
+
+/*! Removes the given image format. */
+void ScratchConfiguration::removeImageFormat(const std::string &name)
+{
+    impl->imageFormats.erase(name);
+}
+
+/*! Creates an instance of the given image format. If the format doesn't exist, a "stub" format will be created. */
+std::shared_ptr<IImageFormat> ScratchConfiguration::createImageFormat(const std::string &name)
+{
+    auto it = impl->imageFormats.find(name);
+
+    if (it == impl->imageFormats.cend()) {
+        std::cerr << "Unsupported image format: " << name << std::endl;
+
+        // Use stub if the format doesn't exist
+        return std::make_shared<ImageFormatStub>();
+    } else
+        return it->second->createInstance();
+}
+
+/*! Registers the given graphics effect. */
+void ScratchConfiguration::registerGraphicsEffect(std::shared_ptr<IGraphicsEffect> effect)
+{
+    if (!effect)
+        return;
+
+    impl->graphicsEffects[effect->name()] = effect;
+}
+
+/*! Removes the given graphics effect. */
+void ScratchConfiguration::removeGraphicsEffect(const std::string &name)
+{
+    impl->graphicsEffects.erase(name);
+}
+
+/*! Returns the graphics effect with the given name, or nullptr if it isn't registered. */
+IGraphicsEffect *ScratchConfiguration::getGraphicsEffect(const std::string &name)
+{
+    auto it = impl->graphicsEffects.find(name);
+
+    if (it == impl->graphicsEffects.cend())
+        return nullptr;
+    else
+        return it->second.get();
+}
+
 const std::vector<std::shared_ptr<IExtension>> ScratchConfiguration::getExtensions()
 {
     return impl->extensions;
diff --git a/src/scratchconfiguration_p.h b/src/scratchconfiguration_p.h
index d43fc12f..534d30c3 100644
--- a/src/scratchconfiguration_p.h
+++ b/src/scratchconfiguration_p.h
@@ -7,11 +7,14 @@
 #include <memory>
 #include <string>
 #include <vector>
+#include <unordered_map>
 
 namespace libscratchcpp
 {
 
 class IExtension;
+class IImageFormatFactory;
+class IGraphicsEffect;
 
 struct ScratchConfigurationPrivate
 {
@@ -19,6 +22,8 @@ struct ScratchConfigurationPrivate
         IExtension *getExtension(std::string name);
 
         std::vector<std::shared_ptr<IExtension>> extensions = { std::make_shared<StandardBlocks>() };
+        std::unordered_map<std::string, std::shared_ptr<IImageFormatFactory>> imageFormats;
+        std::unordered_map<std::string, std::shared_ptr<IGraphicsEffect>> graphicsEffects;
 };
 
 } // namespace libscratchcpp
diff --git a/test/2_frames.sb3 b/test/2_frames.sb3
new file mode 100644
index 00000000..510a2d08
Binary files /dev/null and b/test/2_frames.sb3 differ
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 47a4a127..bd8f3190 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -31,3 +31,5 @@ add_subdirectory(engine)
 add_subdirectory(clock)
 add_subdirectory(timer)
 add_subdirectory(randomgenerator)
+add_subdirectory(imageformats)
+add_subdirectory(rect)
diff --git a/test/assets/CMakeLists.txt b/test/assets/CMakeLists.txt
index 0b0e6214..88ce0e83 100644
--- a/test/assets/CMakeLists.txt
+++ b/test/assets/CMakeLists.txt
@@ -2,6 +2,8 @@
 add_executable(
   asset_test
   asset_test.cpp
+  testasset.cpp
+  testasset.h
 )
 
 target_link_libraries(
@@ -21,7 +23,9 @@ add_executable(
 target_link_libraries(
   costume_test
   GTest::gtest_main
+  GTest::gmock_main
   scratchcpp
+  scratchcpp_mocks
 )
 
 gtest_discover_tests(costume_test)
diff --git a/test/assets/asset_test.cpp b/test/assets/asset_test.cpp
index 7875b5b3..d08d56d6 100644
--- a/test/assets/asset_test.cpp
+++ b/test/assets/asset_test.cpp
@@ -1,6 +1,7 @@
 #include <scratchcpp/asset.h>
 
 #include "../common.h"
+#include "testasset.h"
 
 using namespace libscratchcpp;
 
@@ -21,3 +22,16 @@ TEST(AssetTest, Id)
     ASSERT_EQ(asset.id(), "b");
     ASSERT_EQ(asset.fileName(), "b.svg");
 }
+
+TEST(AssetTest, Data)
+{
+    TestAsset asset;
+    ASSERT_EQ(asset.data(), nullptr);
+
+    static char data[5] = "abcd";
+    asset.setData(5, data);
+    ASSERT_EQ(asset.data(), data);
+    ASSERT_EQ(asset.size, 5);
+    ASSERT_EQ(asset.processedData, data);
+    ASSERT_EQ(asset.callCount, 1);
+}
diff --git a/test/assets/costume_test.cpp b/test/assets/costume_test.cpp
index 6ef79ed3..303b1ff1 100644
--- a/test/assets/costume_test.cpp
+++ b/test/assets/costume_test.cpp
@@ -1,11 +1,35 @@
 #include <scratchcpp/costume.h>
+#include <scratchcpp/scratchconfiguration.h>
 #include <scratchcpp/broadcast.h>
+#include <imageformatfactorymock.h>
+#include <imageformatmock.h>
+#include <graphicseffectmock.h>
 
 #include "../common.h"
 
 using namespace libscratchcpp;
 
-TEST(CostumeTest, Constructors)
+using ::testing::Return;
+using ::testing::_;
+using ::testing::SaveArg;
+
+class CostumeTest : public testing::Test
+{
+    public:
+        void SetUp() override
+        {
+            m_imageFormatFactory = std::make_shared<ImageFormatFactoryMock>();
+            m_imageFormat = std::make_shared<ImageFormatMock>();
+            ScratchConfiguration::registerImageFormat("test", m_imageFormatFactory);
+        }
+
+        void TearDown() override { ScratchConfiguration::removeImageFormat("test"); }
+
+        std::shared_ptr<ImageFormatFactoryMock> m_imageFormatFactory;
+        std::shared_ptr<ImageFormatMock> m_imageFormat;
+};
+
+TEST_F(CostumeTest, Constructors)
 {
     Costume costume("costume1", "a", "svg");
     ASSERT_EQ(costume.id(), "a");
@@ -17,7 +41,7 @@ TEST(CostumeTest, Constructors)
     ASSERT_EQ(costume.rotationCenterY(), 0);
 }
 
-TEST(CostumeTest, BitmapResolution)
+TEST_F(CostumeTest, BitmapResolution)
 {
     Costume costume("costume1", "a", "svg");
 
@@ -25,7 +49,7 @@ TEST(CostumeTest, BitmapResolution)
     ASSERT_EQ(costume.bitmapResolution(), 5.52);
 }
 
-TEST(CostumeTest, RotationCenterX)
+TEST_F(CostumeTest, RotationCenterX)
 {
     Costume costume("costume1", "a", "svg");
 
@@ -33,7 +57,7 @@ TEST(CostumeTest, RotationCenterX)
     ASSERT_EQ(costume.rotationCenterX(), 240);
 }
 
-TEST(CostumeTest, RotationCenterY)
+TEST_F(CostumeTest, RotationCenterY)
 {
     Costume costume("costume1", "a", "svg");
 
@@ -41,7 +65,358 @@ TEST(CostumeTest, RotationCenterY)
     ASSERT_EQ(costume.rotationCenterY(), 180);
 }
 
-TEST(CostumeTest, Broadcast)
+TEST_F(CostumeTest, WidthAndHeight)
+{
+    EXPECT_CALL(*m_imageFormatFactory, createInstance()).WillOnce(Return(m_imageFormat));
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(0));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(0));
+    Costume costume("costume1", "a", "test");
+
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(480));
+    ASSERT_EQ(costume.width(), 480);
+
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(360));
+    ASSERT_EQ(costume.height(), 360);
+
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(0));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(0));
+    costume.setScale(3.5);
+
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(480));
+    ASSERT_EQ(costume.width(), 1680);
+
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(360));
+    ASSERT_EQ(costume.height(), 1260);
+}
+
+TEST_F(CostumeTest, Scale)
+{
+    Costume costume("costume1", "a", "");
+    ASSERT_EQ(costume.scale(), 1);
+
+    costume.setScale(2.894);
+    ASSERT_EQ(costume.scale(), 2.894);
+}
+
+TEST_F(CostumeTest, MirrorHorizontally)
+{
+    Costume costume("costume1", "a", "");
+    ASSERT_FALSE(costume.mirrorHorizontally());
+
+    costume.setMirrorHorizontally(true);
+    ASSERT_TRUE(costume.mirrorHorizontally());
+}
+
+TEST_F(CostumeTest, Bitmap)
+{
+    EXPECT_CALL(*m_imageFormatFactory, createInstance()).WillOnce(Return(m_imageFormat));
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(0));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(0));
+    Costume costume("costume1", "a", "test");
+
+    // Do not mirror horizontally
+    static char data[5] = "abcd";
+    EXPECT_CALL(*m_imageFormat, setData(5, data));
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(3));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 0, 1)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 0, 1)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 0, 1)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 0, 1)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 1, 1)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 1, 1)).WillOnce(Return(10));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 1, 1)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 1, 1)).WillOnce(Return(8));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 2, 1)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 2, 1)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 2, 1)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 2, 1)).WillOnce(Return(8));
+    costume.setData(5, data);
+
+    auto bitmap = costume.bitmap();
+    ASSERT_TRUE(bitmap);
+    ASSERT_EQ(bitmap[0][0], 5);
+    ASSERT_EQ(bitmap[0][1], 3);
+    ASSERT_EQ(bitmap[0][2], 8);
+    ASSERT_EQ(bitmap[0][3], 1);
+
+    ASSERT_EQ(bitmap[1][0], 9);
+    ASSERT_EQ(bitmap[1][1], 10);
+    ASSERT_EQ(bitmap[1][2], 5);
+    ASSERT_EQ(bitmap[1][3], 8);
+
+    ASSERT_EQ(bitmap[2][0], 1);
+    ASSERT_EQ(bitmap[2][1], 9);
+    ASSERT_EQ(bitmap[2][2], 4);
+    ASSERT_EQ(bitmap[2][3], 8);
+
+    // Mirror horizontally
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(3));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 0, 1)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 0, 1)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 0, 1)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 0, 1)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 1, 1)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 1, 1)).WillOnce(Return(10));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 1, 1)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 1, 1)).WillOnce(Return(8));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 2, 1)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 2, 1)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 2, 1)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 2, 1)).WillOnce(Return(8));
+    costume.setMirrorHorizontally(true);
+
+    bitmap = costume.bitmap();
+    ASSERT_TRUE(bitmap);
+    ASSERT_EQ(bitmap[0][0], 1);
+    ASSERT_EQ(bitmap[0][1], 8);
+    ASSERT_EQ(bitmap[0][2], 3);
+    ASSERT_EQ(bitmap[0][3], 5);
+
+    ASSERT_EQ(bitmap[1][0], 8);
+    ASSERT_EQ(bitmap[1][1], 5);
+    ASSERT_EQ(bitmap[1][2], 10);
+    ASSERT_EQ(bitmap[1][3], 9);
+
+    ASSERT_EQ(bitmap[2][0], 8);
+    ASSERT_EQ(bitmap[2][1], 4);
+    ASSERT_EQ(bitmap[2][2], 9);
+    ASSERT_EQ(bitmap[2][3], 1);
+}
+
+TEST_F(CostumeTest, ScaledBitmap)
+{
+    EXPECT_CALL(*m_imageFormatFactory, createInstance()).WillOnce(Return(m_imageFormat));
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(0));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(0));
+    Costume costume("costume1", "a", "test");
+
+    // Do not mirror horizontally
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(3));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 0, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 0, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 0, 1.5)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 0, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 0, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 0, 1.5)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 1, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 1, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 1, 1.5)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 1, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 1, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 1, 1.5)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 2, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 2, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 2, 1.5)).WillOnce(Return(10));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 2, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 2, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 2, 1.5)).WillOnce(Return(8));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 3, 1.5)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 3, 1.5)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 3, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 3, 1.5)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 3, 1.5)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 3, 1.5)).WillOnce(Return(8));
+    costume.setScale(1.5);
+
+    auto bitmap = costume.bitmap();
+    ASSERT_TRUE(bitmap);
+    ASSERT_EQ(bitmap[0][0], 5);
+    ASSERT_EQ(bitmap[0][1], 5);
+    ASSERT_EQ(bitmap[0][2], 3);
+    ASSERT_EQ(bitmap[0][3], 8);
+    ASSERT_EQ(bitmap[0][4], 8);
+    ASSERT_EQ(bitmap[0][5], 1);
+
+    ASSERT_EQ(bitmap[1][0], 5);
+    ASSERT_EQ(bitmap[1][1], 5);
+    ASSERT_EQ(bitmap[1][2], 3);
+    ASSERT_EQ(bitmap[1][3], 8);
+    ASSERT_EQ(bitmap[1][4], 8);
+    ASSERT_EQ(bitmap[1][5], 1);
+
+    ASSERT_EQ(bitmap[2][0], 9);
+    ASSERT_EQ(bitmap[2][1], 9);
+    ASSERT_EQ(bitmap[2][2], 10);
+    ASSERT_EQ(bitmap[2][3], 5);
+    ASSERT_EQ(bitmap[2][4], 5);
+    ASSERT_EQ(bitmap[2][5], 8);
+
+    ASSERT_EQ(bitmap[3][0], 1);
+    ASSERT_EQ(bitmap[3][1], 1);
+    ASSERT_EQ(bitmap[3][2], 9);
+    ASSERT_EQ(bitmap[3][3], 4);
+    ASSERT_EQ(bitmap[3][4], 4);
+    ASSERT_EQ(bitmap[3][5], 8);
+
+    // Mirror horizontally
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(3));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 0, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 0, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 0, 1.5)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 0, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 0, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 0, 1.5)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 1, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 1, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 1, 1.5)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 1, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 1, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 1, 1.5)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 2, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 2, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 2, 1.5)).WillOnce(Return(10));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 2, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 2, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 2, 1.5)).WillOnce(Return(8));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 3, 1.5)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 3, 1.5)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 3, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 3, 1.5)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 3, 1.5)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 3, 1.5)).WillOnce(Return(8));
+    costume.setMirrorHorizontally(true);
+
+    bitmap = costume.bitmap();
+    ASSERT_TRUE(bitmap);
+    ASSERT_EQ(bitmap[0][0], 1);
+    ASSERT_EQ(bitmap[0][1], 8);
+    ASSERT_EQ(bitmap[0][2], 8);
+    ASSERT_EQ(bitmap[0][3], 3);
+    ASSERT_EQ(bitmap[0][4], 5);
+    ASSERT_EQ(bitmap[0][5], 5);
+
+    ASSERT_EQ(bitmap[1][0], 1);
+    ASSERT_EQ(bitmap[1][1], 8);
+    ASSERT_EQ(bitmap[1][2], 8);
+    ASSERT_EQ(bitmap[1][3], 3);
+    ASSERT_EQ(bitmap[1][4], 5);
+    ASSERT_EQ(bitmap[1][5], 5);
+
+    ASSERT_EQ(bitmap[2][0], 8);
+    ASSERT_EQ(bitmap[2][1], 5);
+    ASSERT_EQ(bitmap[2][2], 5);
+    ASSERT_EQ(bitmap[2][3], 10);
+    ASSERT_EQ(bitmap[2][4], 9);
+    ASSERT_EQ(bitmap[2][5], 9);
+
+    ASSERT_EQ(bitmap[3][0], 8);
+    ASSERT_EQ(bitmap[3][1], 4);
+    ASSERT_EQ(bitmap[3][2], 4);
+    ASSERT_EQ(bitmap[3][3], 9);
+    ASSERT_EQ(bitmap[3][4], 1);
+    ASSERT_EQ(bitmap[3][5], 1);
+}
+
+TEST_F(CostumeTest, BitmapWithGraphicsEffects)
+{
+    EXPECT_CALL(*m_imageFormatFactory, createInstance()).WillOnce(Return(m_imageFormat));
+    EXPECT_CALL(*m_imageFormat, width()).Times(3).WillRepeatedly(Return(0));
+    EXPECT_CALL(*m_imageFormat, height()).Times(3).WillRepeatedly(Return(0));
+    Costume costume("costume1", "a", "test");
+
+    GraphicsEffectMock effect1, effect2;
+    costume.setGraphicsEffectValue(&effect1, 54.12);
+    ASSERT_EQ(costume.graphicsEffectValue(&effect1), 54.12);
+    ASSERT_EQ(costume.graphicsEffectValue(&effect2), 0);
+
+    costume.setGraphicsEffectValue(&effect2, -89.03);
+    ASSERT_EQ(costume.graphicsEffectValue(&effect1), 54.12);
+    ASSERT_EQ(costume.graphicsEffectValue(&effect2), -89.03);
+
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(3));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 0, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 0, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 0, 1.5)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 0, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 0, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 0, 1.5)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 1, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 1, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 1, 1.5)).WillOnce(Return(3));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 1, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 1, 1.5)).WillOnce(Return(8));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 1, 1.5)).WillOnce(Return(1));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 2, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 2, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 2, 1.5)).WillOnce(Return(10));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 2, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 2, 1.5)).WillOnce(Return(5));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 2, 1.5)).WillOnce(Return(8));
+
+    EXPECT_CALL(*m_imageFormat, colorAt(0, 3, 1.5)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(1, 3, 1.5)).WillOnce(Return(1));
+    EXPECT_CALL(*m_imageFormat, colorAt(2, 3, 1.5)).WillOnce(Return(9));
+    EXPECT_CALL(*m_imageFormat, colorAt(3, 3, 1.5)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(4, 3, 1.5)).WillOnce(Return(4));
+    EXPECT_CALL(*m_imageFormat, colorAt(5, 3, 1.5)).WillOnce(Return(8));
+
+    Rgb **bitmapPtr1, **bitmapPtr2;
+    EXPECT_CALL(effect1, apply(_, 6, 4, 54.12)).WillOnce(SaveArg<0>(&bitmapPtr1));
+    EXPECT_CALL(effect2, apply(_, 6, 4, -89.03)).WillOnce(SaveArg<0>(&bitmapPtr2));
+    costume.setScale(1.5);
+
+    auto bitmap = costume.bitmap();
+    ASSERT_TRUE(bitmap);
+    ASSERT_EQ(bitmap, bitmapPtr1);
+    ASSERT_EQ(bitmap, bitmapPtr2);
+    ASSERT_EQ(bitmap[0][0], 5);
+    ASSERT_EQ(bitmap[0][1], 5);
+    ASSERT_EQ(bitmap[0][2], 3);
+    ASSERT_EQ(bitmap[0][3], 8);
+    ASSERT_EQ(bitmap[0][4], 8);
+    ASSERT_EQ(bitmap[0][5], 1);
+
+    ASSERT_EQ(bitmap[1][0], 5);
+    ASSERT_EQ(bitmap[1][1], 5);
+    ASSERT_EQ(bitmap[1][2], 3);
+    ASSERT_EQ(bitmap[1][3], 8);
+    ASSERT_EQ(bitmap[1][4], 8);
+    ASSERT_EQ(bitmap[1][5], 1);
+
+    ASSERT_EQ(bitmap[2][0], 9);
+    ASSERT_EQ(bitmap[2][1], 9);
+    ASSERT_EQ(bitmap[2][2], 10);
+    ASSERT_EQ(bitmap[2][3], 5);
+    ASSERT_EQ(bitmap[2][4], 5);
+    ASSERT_EQ(bitmap[2][5], 8);
+
+    ASSERT_EQ(bitmap[3][0], 1);
+    ASSERT_EQ(bitmap[3][1], 1);
+    ASSERT_EQ(bitmap[3][2], 9);
+    ASSERT_EQ(bitmap[3][3], 4);
+    ASSERT_EQ(bitmap[3][4], 4);
+    ASSERT_EQ(bitmap[3][5], 8);
+
+    EXPECT_CALL(*m_imageFormat, width()).WillOnce(Return(0));
+    EXPECT_CALL(*m_imageFormat, height()).WillOnce(Return(0));
+    costume.clearGraphicsEffects();
+    ASSERT_EQ(costume.graphicsEffectValue(&effect1), 0);
+    ASSERT_EQ(costume.graphicsEffectValue(&effect2), 0);
+}
+
+TEST_F(CostumeTest, Broadcast)
 {
     Costume costume("costume1", "a", "svg");
 
diff --git a/test/assets/testasset.cpp b/test/assets/testasset.cpp
new file mode 100644
index 00000000..e17c26cd
--- /dev/null
+++ b/test/assets/testasset.cpp
@@ -0,0 +1,15 @@
+#include "testasset.h"
+
+using namespace libscratchcpp;
+
+TestAsset::TestAsset() :
+    Asset("", "", "")
+{
+}
+
+void TestAsset::processData(unsigned int size, void *data)
+{
+    this->size = size;
+    processedData = data;
+    callCount++;
+}
diff --git a/test/assets/testasset.h b/test/assets/testasset.h
new file mode 100644
index 00000000..f147189f
--- /dev/null
+++ b/test/assets/testasset.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <scratchcpp/asset.h>
+
+namespace libscratchcpp
+{
+
+class TestAsset : public Asset
+{
+    public:
+        TestAsset();
+
+        void *processedData = nullptr;
+        unsigned int size = 0;
+        unsigned int callCount = 0;
+
+    protected:
+        void processData(unsigned int size, void *data) override;
+};
+
+} // namespace libscratchcpp
diff --git a/test/blocks/control_blocks_test.cpp b/test/blocks/control_blocks_test.cpp
index 8ba068e0..2074255c 100644
--- a/test/blocks/control_blocks_test.cpp
+++ b/test/blocks/control_blocks_test.cpp
@@ -48,13 +48,21 @@ class ControlBlocksTest : public testing::Test
             return block;
         }
 
+        std::shared_ptr<Block> createNullBlock(const std::string &id)
+        {
+            std::shared_ptr<Block> block = std::make_shared<Block>(id, "");
+            BlockComp func = [](Compiler *compiler) { compiler->addInstruction(vm::OP_NULL); };
+            block->setCompileFunction(func);
+
+            return block;
+        }
+
         void addSubstackInput(std::shared_ptr<Block> block, const std::string &name, ControlBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
         {
             auto input = std::make_shared<Input>(name, Input::Type::NoShadow);
             input->setValueBlock(valueBlock);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addObscuredInput(std::shared_ptr<Block> block, const std::string &name, ControlBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
@@ -63,7 +71,6 @@ class ControlBlocksTest : public testing::Test
             input->setValueBlock(valueBlock);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addValueInput(std::shared_ptr<Block> block, const std::string &name, ControlBlocks::Inputs id, const Value &value) const
@@ -72,7 +79,6 @@ class ControlBlocksTest : public testing::Test
             input->setPrimaryValue(value);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         std::shared_ptr<Input> addNullInput(std::shared_ptr<Block> block, const std::string &name, ControlBlocks::Inputs id) const
@@ -80,7 +86,6 @@ class ControlBlocksTest : public testing::Test
             auto input = std::make_shared<Input>(name, Input::Type::Shadow);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
 
             return input;
         }
@@ -91,7 +96,6 @@ class ControlBlocksTest : public testing::Test
             field->setFieldId(id);
             field->setSpecialValueId(valueId);
             block->addField(field);
-            block->updateFieldMap();
         }
 
         void addVariableField(std::shared_ptr<Block> block, std::shared_ptr<Variable> variable)
@@ -99,7 +103,6 @@ class ControlBlocksTest : public testing::Test
             auto variableField = std::make_shared<Field>("VARIABLE", Value(), variable);
             variableField->setFieldId(ControlBlocks::VARIABLE);
             block->addField(variableField);
-            block->updateFieldMap();
         }
 
         std::shared_ptr<Block> createSubstack(const std::string &id)
@@ -859,10 +862,8 @@ TEST_F(ControlBlocksTest, CreateCloneOf)
     // create clone of [myself]
     auto block2 = createCloneBlock("b", "_myself_");
 
-    // create clone of (join "" "")
-    auto joinBlock = std::make_shared<Block>("d", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
-    auto block3 = createCloneBlock("c", "", joinBlock);
+    // create clone of (null block)
+    auto block3 = createCloneBlock("c", "", createNullBlock("d"));
 
     EXPECT_CALL(m_engineMock, findTarget("Sprite1")).WillOnce(Return(4));
     EXPECT_CALL(m_engineMock, functionIndex(&ControlBlocks::createCloneByIndex)).WillOnce(Return(0));
@@ -878,9 +879,7 @@ TEST_F(ControlBlocksTest, CreateCloneOf)
     ControlBlocks::compileCreateClone(&compiler);
     compiler.end();
 
-    ASSERT_EQ(
-        compiler.bytecode(),
-        std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_NULL, vm::OP_NULL, vm::OP_STR_CONCAT, vm::OP_EXEC, 2, vm::OP_HALT }));
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT }));
     ASSERT_EQ(compiler.constValues().size(), 1);
     ASSERT_EQ(compiler.constValues()[0].toDouble(), 4);
     ASSERT_TRUE(compiler.variables().empty());
diff --git a/test/blocks/custom_blocks_test.cpp b/test/blocks/custom_blocks_test.cpp
index 0597e0f0..d133cdb5 100644
--- a/test/blocks/custom_blocks_test.cpp
+++ b/test/blocks/custom_blocks_test.cpp
@@ -27,7 +27,6 @@ class CustomBlocksTest : public testing::Test
             input->setValueBlock(prototypeBlock);
             input->setInputId(CustomBlocks::CUSTOM_BLOCK);
             definitionBlock->addInput(input);
-            definitionBlock->updateInputMap();
         }
 
         void addArgumentInput(std::shared_ptr<Block> block, const std::string &argId, const Value &value) const
@@ -94,7 +93,6 @@ TEST_F(CustomBlocksTest, CustomBlocks)
     auto valueField = std::make_shared<Field>("VALUE", "boolean");
     valueField->setFieldId(CustomBlocks::VALUE);
     argBlock->addField(valueField);
-    argBlock->updateFieldMap();
     input->setValueBlock(argBlock);
     input->setInputId(-100);
     testBlock->addInput(input);
@@ -105,12 +103,10 @@ TEST_F(CustomBlocksTest, CustomBlocks)
     valueField = std::make_shared<Field>("VALUE", "invalid");
     valueField->setFieldId(CustomBlocks::VALUE);
     argBlock->addField(valueField);
-    argBlock->updateFieldMap();
     input->setValueBlock(argBlock);
     input->setInputId(-101);
     testBlock->addInput(input);
 
-    testBlock->updateInputMap();
     testBlock->setCompileFunction([](Compiler *compiler) {
         compiler->addInput(-100);
         compiler->addInstruction(vm::OP_PRINT);
diff --git a/test/blocks/event_blocks_test.cpp b/test/blocks/event_blocks_test.cpp
index 91e8c584..684d9131 100644
--- a/test/blocks/event_blocks_test.cpp
+++ b/test/blocks/event_blocks_test.cpp
@@ -40,7 +40,6 @@ class EventBlocksTest : public testing::Test
             input->primaryValue()->setType(InputValue::Type::Broadcast);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addObscuredInput(std::shared_ptr<Block> block, const std::string &name, EventBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
@@ -49,7 +48,6 @@ class EventBlocksTest : public testing::Test
             input->setValueBlock(valueBlock);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addValueField(std::shared_ptr<Block> block, const std::string &name, EventBlocks::Fields id, const std::string &value) const
@@ -57,7 +55,6 @@ class EventBlocksTest : public testing::Test
             auto field = std::make_shared<Field>(name, value);
             field->setFieldId(id);
             block->addField(field);
-            block->updateFieldMap();
         }
 
         void addBroadcastField(std::shared_ptr<Block> block, const std::string &name, EventBlocks::Fields id, std::shared_ptr<Broadcast> broadcast) const
@@ -65,7 +62,6 @@ class EventBlocksTest : public testing::Test
             auto field = std::make_shared<Field>(name, Value(), broadcast);
             field->setFieldId(id);
             block->addField(field);
-            block->updateFieldMap();
         }
 
         std::unique_ptr<IBlockSection> m_section;
@@ -92,6 +88,7 @@ TEST_F(EventBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_broadcastandwait", &EventBlocks::compileBroadcastAndWait));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenbroadcastreceived", &EventBlocks::compileWhenBroadcastReceived));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenbackdropswitchesto", &EventBlocks::compileWhenBackdropSwitchesTo));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenkeypressed", &EventBlocks::compileWhenKeyPressed));
 
     // Inputs
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "BROADCAST_INPUT", EventBlocks::BROADCAST_INPUT)).Times(1);
@@ -99,6 +96,7 @@ TEST_F(EventBlocksTest, RegisterBlocks)
     // Fields
     EXPECT_CALL(m_engineMock, addField(m_section.get(), "BROADCAST_OPTION", EventBlocks::BROADCAST_OPTION));
     EXPECT_CALL(m_engineMock, addField(m_section.get(), "BACKDROP", EventBlocks::BACKDROP));
+    EXPECT_CALL(m_engineMock, addField(m_section.get(), "KEY_OPTION", EventBlocks::KEY_OPTION));
 
     m_section->registerBlocks(&m_engineMock);
 }
@@ -271,3 +269,33 @@ TEST_F(EventBlocksTest, WhenBackdropSwitchesTo)
     ASSERT_TRUE(compiler.variables().empty());
     ASSERT_TRUE(compiler.lists().empty());
 }
+
+TEST_F(EventBlocksTest, WhenKeyPressed)
+{
+    Compiler compiler(&m_engineMock);
+
+    // when key "a" pressed
+    auto block1 = createEventBlock("a", "event_whenkeypressed");
+    addValueField(block1, "KEY_OPTION", EventBlocks::KEY_OPTION, "a");
+
+    // when key "left arrow" pressed
+    auto block2 = createEventBlock("b", "event_whenkeypressed");
+    addValueField(block2, "KEY_OPTION", EventBlocks::KEY_OPTION, "left arrow");
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, addKeyPressScript(block1, "a"));
+    compiler.setBlock(block1);
+    EventBlocks::compileWhenKeyPressed(&compiler);
+
+    EXPECT_CALL(m_engineMock, addKeyPressScript(block2, "left arrow"));
+    compiler.setBlock(block2);
+    EventBlocks::compileWhenKeyPressed(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+    ASSERT_TRUE(compiler.variables().empty());
+    ASSERT_TRUE(compiler.lists().empty());
+}
diff --git a/test/blocks/list_blocks_test.cpp b/test/blocks/list_blocks_test.cpp
index a6e9af07..5ec69805 100644
--- a/test/blocks/list_blocks_test.cpp
+++ b/test/blocks/list_blocks_test.cpp
@@ -28,7 +28,6 @@ class ListBlocksTest : public testing::Test
             auto listField = std::make_shared<Field>("LIST", Value(), list);
             listField->setFieldId(ListBlocks::LIST);
             block->addField(listField);
-            block->updateFieldMap();
 
             return block;
         }
@@ -39,7 +38,6 @@ class ListBlocksTest : public testing::Test
             input->setPrimaryValue(item);
             input->setInputId(ListBlocks::ITEM);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addIndexInput(std::shared_ptr<Block> block, const Value &index) const
@@ -48,7 +46,6 @@ class ListBlocksTest : public testing::Test
             input->setPrimaryValue(index);
             input->setInputId(ListBlocks::INDEX);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         // For add item, item index and list contains item
diff --git a/test/blocks/looks_blocks_test.cpp b/test/blocks/looks_blocks_test.cpp
index 665bcf46..0f06ff86 100644
--- a/test/blocks/looks_blocks_test.cpp
+++ b/test/blocks/looks_blocks_test.cpp
@@ -5,8 +5,10 @@
 #include <scratchcpp/sprite.h>
 #include <scratchcpp/stage.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/scratchconfiguration.h>
 #include <enginemock.h>
 #include <randomgeneratormock.h>
+#include <graphicseffectmock.h>
 
 #include "../common.h"
 #include "blocks/looksblocks.h"
@@ -30,13 +32,21 @@ class LooksBlocksTest : public testing::Test
         // For any looks block
         std::shared_ptr<Block> createLooksBlock(const std::string &id, const std::string &opcode) const { return std::make_shared<Block>(id, opcode); }
 
+        std::shared_ptr<Block> createNullBlock(const std::string &id)
+        {
+            std::shared_ptr<Block> block = std::make_shared<Block>(id, "");
+            BlockComp func = [](Compiler *compiler) { compiler->addInstruction(vm::OP_NULL); };
+            block->setCompileFunction(func);
+
+            return block;
+        }
+
         void addValueInput(std::shared_ptr<Block> block, const std::string &name, LooksBlocks::Inputs id, const Value &value) const
         {
             auto input = std::make_shared<Input>(name, Input::Type::Shadow);
             input->setPrimaryValue(value);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addObscuredInput(std::shared_ptr<Block> block, const std::string &name, LooksBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
@@ -45,7 +55,6 @@ class LooksBlocksTest : public testing::Test
             input->setValueBlock(valueBlock);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         std::shared_ptr<Input> addNullInput(std::shared_ptr<Block> block, const std::string &name, LooksBlocks::Inputs id) const
@@ -53,7 +62,6 @@ class LooksBlocksTest : public testing::Test
             auto input = std::make_shared<Input>(name, Input::Type::Shadow);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
 
             return input;
         }
@@ -76,7 +84,6 @@ class LooksBlocksTest : public testing::Test
             field->setFieldId(id);
             field->setSpecialValueId(valueId);
             block->addField(field);
-            block->updateFieldMap();
         }
 
         std::unique_ptr<IBlockSection> m_section;
@@ -99,6 +106,9 @@ TEST_F(LooksBlocksTest, RegisterBlocks)
     // Blocks
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_show", &LooksBlocks::compileShow));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_hide", &LooksBlocks::compileHide));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_changeeffectby", &LooksBlocks::compileChangeEffectBy));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_seteffectto", &LooksBlocks::compileSetEffectTo));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_cleargraphiceffects", &LooksBlocks::compileClearGraphicEffects));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_changesizeby", &LooksBlocks::compileChangeSizeBy));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_setsizeto", &LooksBlocks::compileSetSizeTo));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_size", &LooksBlocks::compileSize));
@@ -115,13 +125,22 @@ TEST_F(LooksBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SIZE", LooksBlocks::SIZE));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "COSTUME", LooksBlocks::COSTUME));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "BACKDROP", LooksBlocks::BACKDROP));
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "VALUE", LooksBlocks::VALUE));
 
     // Fields
     EXPECT_CALL(m_engineMock, addField(m_section.get(), "NUMBER_NAME", LooksBlocks::NUMBER_NAME));
+    EXPECT_CALL(m_engineMock, addField(m_section.get(), "EFFECT", LooksBlocks::EFFECT));
 
     // Field values
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "number", LooksBlocks::Number));
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "name", LooksBlocks::Name));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "COLOR", LooksBlocks::ColorEffect));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "FISHEYE", LooksBlocks::FisheyeEffect));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "WHIRL", LooksBlocks::WhirlEffect));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "PIXELATE", LooksBlocks::PixelateEffect));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "MOSAIC", LooksBlocks::MosaicEffect));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "BRIGHTNESS", LooksBlocks::BrightnessEffect));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "GHOST", LooksBlocks::GhostEffect));
 
     m_section->registerBlocks(&m_engineMock);
 }
@@ -206,6 +225,604 @@ TEST_F(LooksBlocksTest, HideImpl)
     ASSERT_FALSE(sprite.visible());
 }
 
+TEST_F(LooksBlocksTest, ChangeEffectBy)
+{
+    Compiler compiler(&m_engineMock);
+
+    // change [custom1] effect by 12.5
+    auto block1 = std::make_shared<Block>("a", "looks_changeeffectby");
+    addDropdownField(block1, "EFFECT", LooksBlocks::EFFECT, "custom1", static_cast<LooksBlocks::FieldValues>(-1));
+    addValueInput(block1, "CHANGE", LooksBlocks::CHANGE, 12.5);
+
+    // change [custom2] effect by -78.15
+    auto block2 = std::make_shared<Block>("b", "looks_changeeffectby");
+    addDropdownField(block2, "EFFECT", LooksBlocks::EFFECT, "custom2", static_cast<LooksBlocks::FieldValues>(-1));
+    addValueInput(block2, "CHANGE", LooksBlocks::CHANGE, -78.15);
+
+    // change [color] effect by 154.152
+    auto block3 = std::make_shared<Block>("c", "looks_changeeffectby");
+    addDropdownField(block3, "EFFECT", LooksBlocks::EFFECT, "color", LooksBlocks::ColorEffect);
+    addValueInput(block3, "CHANGE", LooksBlocks::CHANGE, 154.152);
+
+    // change [fisheye] effect by -124.054
+    auto block4 = std::make_shared<Block>("d", "looks_changeeffectby");
+    addDropdownField(block4, "EFFECT", LooksBlocks::EFFECT, "fisheye", LooksBlocks::FisheyeEffect);
+    addValueInput(block4, "CHANGE", LooksBlocks::CHANGE, -124.054);
+
+    // change [whirl] effect by 45.858
+    auto block5 = std::make_shared<Block>("e", "looks_changeeffectby");
+    addDropdownField(block5, "EFFECT", LooksBlocks::EFFECT, "whirl", LooksBlocks::WhirlEffect);
+    addValueInput(block5, "CHANGE", LooksBlocks::CHANGE, 45.858);
+
+    // change [pixelate] effect by -0.15
+    auto block6 = std::make_shared<Block>("f", "looks_changeeffectby");
+    addDropdownField(block6, "EFFECT", LooksBlocks::EFFECT, "pixelate", LooksBlocks::PixelateEffect);
+    addValueInput(block6, "CHANGE", LooksBlocks::CHANGE, -0.15);
+
+    // change [mosaic] effect by 0.84
+    auto block7 = std::make_shared<Block>("g", "looks_changeeffectby");
+    addDropdownField(block7, "EFFECT", LooksBlocks::EFFECT, "mosaic", LooksBlocks::MosaicEffect);
+    addValueInput(block7, "CHANGE", LooksBlocks::CHANGE, 0.84);
+
+    // change [brightness] effect by 40.87
+    auto block8 = std::make_shared<Block>("h", "looks_changeeffectby");
+    addDropdownField(block8, "EFFECT", LooksBlocks::EFFECT, "brightness", LooksBlocks::BrightnessEffect);
+    addValueInput(block8, "CHANGE", LooksBlocks::CHANGE, 40.87);
+
+    // change [ghost] effect by 50.04
+    auto block9 = std::make_shared<Block>("i", "looks_changeeffectby");
+    addDropdownField(block9, "EFFECT", LooksBlocks::EFFECT, "ghost", LooksBlocks::GhostEffect);
+    addValueInput(block9, "CHANGE", LooksBlocks::CHANGE, 50.04);
+
+    auto effect1 = std::make_shared<GraphicsEffectMock>();
+    auto effect2 = std::make_shared<GraphicsEffectMock>();
+    EXPECT_CALL(*effect1, name()).WillOnce(Return("custom1"));
+    ScratchConfiguration::registerGraphicsEffect(effect1);
+    EXPECT_CALL(*effect2, name()).WillOnce(Return("custom2"));
+    ScratchConfiguration::registerGraphicsEffect(effect2);
+
+    compiler.init();
+    LooksBlocks::m_customGraphicsEffects.clear();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changeEffectBy)).Times(3).WillRepeatedly(Return(0));
+    compiler.setBlock(block1);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+
+    compiler.setBlock(block1);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+
+    compiler.setBlock(block2);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(1), effect2.get());
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changeColorEffectBy)).WillOnce(Return(1));
+    compiler.setBlock(block3);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changeFisheyeEffectBy)).WillOnce(Return(2));
+    compiler.setBlock(block4);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changeWhirlEffectBy)).WillOnce(Return(3));
+    compiler.setBlock(block5);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changePixelateEffectBy)).WillOnce(Return(4));
+    compiler.setBlock(block6);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changeMosaicEffectBy)).WillOnce(Return(5));
+    compiler.setBlock(block7);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changeBrightnessEffectBy)).WillOnce(Return(6));
+    compiler.setBlock(block8);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::changeGhostEffectBy)).WillOnce(Return(7));
+    compiler.setBlock(block9);
+    LooksBlocks::compileChangeEffectBy(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(1), effect2.get());
+
+    ASSERT_EQ(
+        compiler.bytecode(),
+        std::vector<unsigned int>(
+            { vm::OP_START,
+              vm::OP_CONST,
+              0,
+              vm::OP_CONST,
+              1,
+              vm::OP_EXEC,
+              0,
+              vm::OP_CONST,
+              2,
+              vm::OP_CONST,
+              1,
+              vm::OP_EXEC,
+              0,
+              vm::OP_CONST,
+              3,
+              vm::OP_CONST,
+              4,
+              vm::OP_EXEC,
+              0,
+              vm::OP_CONST,
+              5,
+              vm::OP_EXEC,
+              1,
+              vm::OP_CONST,
+              6,
+              vm::OP_EXEC,
+              2,
+              vm::OP_CONST,
+              7,
+              vm::OP_EXEC,
+              3,
+              vm::OP_CONST,
+              8,
+              vm::OP_EXEC,
+              4,
+              vm::OP_CONST,
+              9,
+              vm::OP_EXEC,
+              5,
+              vm::OP_CONST,
+              10,
+              vm::OP_EXEC,
+              6,
+              vm::OP_CONST,
+              11,
+              vm::OP_EXEC,
+              7,
+              vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 0, 12.5, 0, 1, -78.15, 154.152, -124.054, 45.858, -0.15, 0.84, 40.87, 50.04 }));
+
+    ScratchConfiguration::removeGraphicsEffect("custom1");
+    ScratchConfiguration::removeGraphicsEffect("custom2");
+}
+
+static void initEffects()
+{
+    LooksBlocks::m_colorEffect = ScratchConfiguration::getGraphicsEffect("color");
+    LooksBlocks::m_fisheyeEffect = ScratchConfiguration::getGraphicsEffect("fisheye");
+    LooksBlocks::m_whirlEffect = ScratchConfiguration::getGraphicsEffect("whirl");
+    LooksBlocks::m_pixelateEffect = ScratchConfiguration::getGraphicsEffect("pixelate");
+    LooksBlocks::m_mosaicEffect = ScratchConfiguration::getGraphicsEffect("mosaic");
+    LooksBlocks::m_brightnessEffect = ScratchConfiguration::getGraphicsEffect("brightness");
+    LooksBlocks::m_ghostEffect = ScratchConfiguration::getGraphicsEffect("ghost");
+}
+
+TEST_F(LooksBlocksTest, ChangeEffectByImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 3, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 2, vm::OP_HALT };
+    static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 3, vm::OP_HALT };
+    static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 4, vm::OP_HALT };
+    static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 8, vm::OP_EXEC, 5, vm::OP_HALT };
+    static unsigned int bytecode8[] = { vm::OP_START, vm::OP_CONST, 9, vm::OP_EXEC, 6, vm::OP_HALT };
+    static unsigned int bytecode9[] = { vm::OP_START, vm::OP_CONST, 10, vm::OP_EXEC, 7, vm::OP_HALT };
+    static BlockFunc functions[] = {
+        &LooksBlocks::changeEffectBy,         &LooksBlocks::changeColorEffectBy,  &LooksBlocks::changeFisheyeEffectBy,    &LooksBlocks::changeWhirlEffectBy,
+        &LooksBlocks::changePixelateEffectBy, &LooksBlocks::changeMosaicEffectBy, &LooksBlocks::changeBrightnessEffectBy, &LooksBlocks::changeGhostEffectBy
+    };
+    static Value constValues[] = { 0, 55.15, 1, -40.54, 1.2, 2.3, -3.4, -4.5, 0.5, -8.54, 0.01 };
+
+    GraphicsEffectMock effect1, effect2;
+
+    Sprite sprite;
+    sprite.setGraphicsEffectValue(&effect1, 12.5);
+    sprite.setGraphicsEffectValue(&effect2, -100.48);
+
+    // custom1
+    VirtualMachine vm(&sprite, nullptr, nullptr);
+    LooksBlocks::m_customGraphicsEffects.clear();
+    LooksBlocks::m_customGraphicsEffects.push_back(&effect1);
+    vm.setBytecode(bytecode1);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 67.65);
+
+    // custom2
+    LooksBlocks::m_customGraphicsEffects.push_back(&effect2);
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), -141.02);
+
+    // Initialize graphics effects
+    initEffects();
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color"), 12.4);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye"), -8.45);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl"), 50.15);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate"), -45.21);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic"), 0.2);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness"), -0.01);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost"), 45.78);
+
+    // TODO: Uncomment commented lines (#283, #284, #285, #286, #287, #288, #289)
+    // color
+    /*vm.reset();
+    vm.setBytecode(bytecode3);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color")), 13.6);
+
+    // fisheye
+    vm.reset();
+    vm.setBytecode(bytecode4);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye")), -6.15);
+
+    // whirl
+    vm.reset();
+    vm.setBytecode(bytecode5);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl")), 46.75);
+
+    // pixelate
+    vm.reset();
+    vm.setBytecode(bytecode6);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate")), -49.71);
+
+    // mosaic
+    vm.reset();
+    vm.setBytecode(bytecode7);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic")), 0.7);
+
+    // brightness
+    vm.reset();
+    vm.setBytecode(bytecode8);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness")), -8.55);
+
+    // ghost
+    vm.reset();
+    vm.setBytecode(bytecode9);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost")), 45.79);*/
+}
+
+TEST_F(LooksBlocksTest, SetEffectTo)
+{
+    Compiler compiler(&m_engineMock);
+
+    // set [custom1] effect to 12.5
+    auto block1 = std::make_shared<Block>("a", "looks_seteffectto");
+    addDropdownField(block1, "EFFECT", LooksBlocks::EFFECT, "custom1", static_cast<LooksBlocks::FieldValues>(-1));
+    addValueInput(block1, "CHANGE", LooksBlocks::CHANGE, 12.5);
+
+    // set [custom2] effect to -78.15
+    auto block2 = std::make_shared<Block>("b", "looks_seteffectto");
+    addDropdownField(block2, "EFFECT", LooksBlocks::EFFECT, "custom2", static_cast<LooksBlocks::FieldValues>(-1));
+    addValueInput(block2, "CHANGE", LooksBlocks::CHANGE, -78.15);
+
+    // set [color] effect to 154.152
+    auto block3 = std::make_shared<Block>("c", "looks_seteffectto");
+    addDropdownField(block3, "EFFECT", LooksBlocks::EFFECT, "color", LooksBlocks::ColorEffect);
+    addValueInput(block3, "CHANGE", LooksBlocks::CHANGE, 154.152);
+
+    // set [fisheye] effect to -124.054
+    auto block4 = std::make_shared<Block>("d", "looks_seteffectto");
+    addDropdownField(block4, "EFFECT", LooksBlocks::EFFECT, "fisheye", LooksBlocks::FisheyeEffect);
+    addValueInput(block4, "CHANGE", LooksBlocks::CHANGE, -124.054);
+
+    // set [whirl] effect to 45.858
+    auto block5 = std::make_shared<Block>("e", "looks_seteffectto");
+    addDropdownField(block5, "EFFECT", LooksBlocks::EFFECT, "whirl", LooksBlocks::WhirlEffect);
+    addValueInput(block5, "CHANGE", LooksBlocks::CHANGE, 45.858);
+
+    // set [pixelate] effect to -0.15
+    auto block6 = std::make_shared<Block>("f", "looks_seteffectto");
+    addDropdownField(block6, "EFFECT", LooksBlocks::EFFECT, "pixelate", LooksBlocks::PixelateEffect);
+    addValueInput(block6, "CHANGE", LooksBlocks::CHANGE, -0.15);
+
+    // set [mosaic] effect to 0.84
+    auto block7 = std::make_shared<Block>("g", "looks_seteffectto");
+    addDropdownField(block7, "EFFECT", LooksBlocks::EFFECT, "mosaic", LooksBlocks::MosaicEffect);
+    addValueInput(block7, "CHANGE", LooksBlocks::CHANGE, 0.84);
+
+    // set [brightness] effect to 40.87
+    auto block8 = std::make_shared<Block>("h", "looks_seteffectto");
+    addDropdownField(block8, "EFFECT", LooksBlocks::EFFECT, "brightness", LooksBlocks::BrightnessEffect);
+    addValueInput(block8, "CHANGE", LooksBlocks::CHANGE, 40.87);
+
+    // set [ghost] effect to 50.04
+    auto block9 = std::make_shared<Block>("i", "looks_seteffectto");
+    addDropdownField(block9, "EFFECT", LooksBlocks::EFFECT, "ghost", LooksBlocks::GhostEffect);
+    addValueInput(block9, "CHANGE", LooksBlocks::CHANGE, 50.04);
+
+    auto effect1 = std::make_shared<GraphicsEffectMock>();
+    auto effect2 = std::make_shared<GraphicsEffectMock>();
+    EXPECT_CALL(*effect1, name()).WillOnce(Return("custom1"));
+    ScratchConfiguration::registerGraphicsEffect(effect1);
+    EXPECT_CALL(*effect2, name()).WillOnce(Return("custom2"));
+    ScratchConfiguration::registerGraphicsEffect(effect2);
+
+    compiler.init();
+    LooksBlocks::m_customGraphicsEffects.clear();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setEffectTo)).Times(3).WillRepeatedly(Return(0));
+    compiler.setBlock(block1);
+    LooksBlocks::compileSetEffectTo(&compiler);
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+
+    compiler.setBlock(block1);
+    LooksBlocks::compileSetEffectTo(&compiler);
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+
+    compiler.setBlock(block2);
+    LooksBlocks::compileSetEffectTo(&compiler);
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(1), effect2.get());
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setColorEffectTo)).WillOnce(Return(1));
+    compiler.setBlock(block3);
+    LooksBlocks::compileSetEffectTo(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setFisheyeEffectTo)).WillOnce(Return(2));
+    compiler.setBlock(block4);
+    LooksBlocks::compileSetEffectTo(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setWhirlEffectTo)).WillOnce(Return(3));
+    compiler.setBlock(block5);
+    LooksBlocks::compileSetEffectTo(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setPixelateEffectTo)).WillOnce(Return(4));
+    compiler.setBlock(block6);
+    LooksBlocks::compileSetEffectTo(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setMosaicEffectTo)).WillOnce(Return(5));
+    compiler.setBlock(block7);
+    LooksBlocks::compileSetEffectTo(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setBrightnessEffectTo)).WillOnce(Return(6));
+    compiler.setBlock(block8);
+    LooksBlocks::compileSetEffectTo(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::setGhostEffectTo)).WillOnce(Return(7));
+    compiler.setBlock(block9);
+    LooksBlocks::compileSetEffectTo(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(0), effect1.get());
+    ASSERT_EQ(LooksBlocks::m_customGraphicsEffects.at(1), effect2.get());
+
+    ASSERT_EQ(
+        compiler.bytecode(),
+        std::vector<unsigned int>(
+            { vm::OP_START,
+              vm::OP_CONST,
+              0,
+              vm::OP_CONST,
+              1,
+              vm::OP_EXEC,
+              0,
+              vm::OP_CONST,
+              2,
+              vm::OP_CONST,
+              1,
+              vm::OP_EXEC,
+              0,
+              vm::OP_CONST,
+              3,
+              vm::OP_CONST,
+              4,
+              vm::OP_EXEC,
+              0,
+              vm::OP_CONST,
+              5,
+              vm::OP_EXEC,
+              1,
+              vm::OP_CONST,
+              6,
+              vm::OP_EXEC,
+              2,
+              vm::OP_CONST,
+              7,
+              vm::OP_EXEC,
+              3,
+              vm::OP_CONST,
+              8,
+              vm::OP_EXEC,
+              4,
+              vm::OP_CONST,
+              9,
+              vm::OP_EXEC,
+              5,
+              vm::OP_CONST,
+              10,
+              vm::OP_EXEC,
+              6,
+              vm::OP_CONST,
+              11,
+              vm::OP_EXEC,
+              7,
+              vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 0, 12.5, 0, 1, -78.15, 154.152, -124.054, 45.858, -0.15, 0.84, 40.87, 50.04 }));
+
+    ScratchConfiguration::removeGraphicsEffect("custom1");
+    ScratchConfiguration::removeGraphicsEffect("custom2");
+}
+
+TEST_F(LooksBlocksTest, SetEffectToImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 3, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 2, vm::OP_HALT };
+    static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 3, vm::OP_HALT };
+    static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 4, vm::OP_HALT };
+    static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 8, vm::OP_EXEC, 5, vm::OP_HALT };
+    static unsigned int bytecode8[] = { vm::OP_START, vm::OP_CONST, 9, vm::OP_EXEC, 6, vm::OP_HALT };
+    static unsigned int bytecode9[] = { vm::OP_START, vm::OP_CONST, 10, vm::OP_EXEC, 7, vm::OP_HALT };
+    static BlockFunc functions[] = {
+        &LooksBlocks::setEffectTo,         &LooksBlocks::setColorEffectTo,  &LooksBlocks::setFisheyeEffectTo,    &LooksBlocks::setWhirlEffectTo,
+        &LooksBlocks::setPixelateEffectTo, &LooksBlocks::setMosaicEffectTo, &LooksBlocks::setBrightnessEffectTo, &LooksBlocks::setGhostEffectTo
+    };
+    static Value constValues[] = { 0, 55.15, 1, -40.54, 1.2, 2.3, -3.4, -4.5, 0.5, -8.54, 0.01 };
+
+    GraphicsEffectMock effect1, effect2;
+
+    Sprite sprite;
+    sprite.setGraphicsEffectValue(&effect1, 12.5);
+    sprite.setGraphicsEffectValue(&effect2, -100.48);
+
+    // custom1
+    VirtualMachine vm(&sprite, nullptr, nullptr);
+    LooksBlocks::m_customGraphicsEffects.clear();
+    LooksBlocks::m_customGraphicsEffects.push_back(&effect1);
+    vm.setBytecode(bytecode1);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 55.15);
+
+    // custom2
+    LooksBlocks::m_customGraphicsEffects.push_back(&effect2);
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), -40.54);
+
+    // Initialize graphics effects
+    initEffects();
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color"), 12.4);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye"), -8.45);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl"), 50.15);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate"), -45.21);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic"), 0.2);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness"), -0.01);
+    sprite.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost"), 45.78);
+
+    // TODO: Uncomment commented lines (#283, #284, #285, #286, #287, #288, #289)
+    // color
+    /*vm.reset();
+    vm.setBytecode(bytecode3);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color")), 1.2);
+
+    // fisheye
+    vm.reset();
+    vm.setBytecode(bytecode4);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye")), 2.3);
+
+    // whirl
+    vm.reset();
+    vm.setBytecode(bytecode5);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl")), -3.4);
+
+    // pixelate
+    vm.reset();
+    vm.setBytecode(bytecode6);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate")), -4.5);
+
+    // mosaic
+    vm.reset();
+    vm.setBytecode(bytecode7);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic")), 0.5);
+
+    // brightness
+    vm.reset();
+    vm.setBytecode(bytecode8);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness")), -8.54);
+
+    // ghost
+    vm.reset();
+    vm.setBytecode(bytecode9);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost")), 0.01);*/
+}
+
+TEST_F(LooksBlocksTest, ClearGraphicEffects)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = std::make_shared<Block>("a", "looks_cleargraphiceffects");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::clearGraphicEffects)).WillOnce(Return(0));
+
+    compiler.init();
+    compiler.setBlock(block);
+    LooksBlocks::compileClearGraphicEffects(&compiler);
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+}
+
+TEST_F(LooksBlocksTest, ClearGraphicEffectsImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &LooksBlocks::clearGraphicEffects };
+
+    Sprite sprite;
+    GraphicsEffectMock effect1, effect2;
+    sprite.setGraphicsEffectValue(&effect1, 48.21);
+    sprite.setGraphicsEffectValue(&effect2, -107.08);
+
+    VirtualMachine vm(&sprite, nullptr, nullptr);
+    vm.setBytecode(bytecode);
+    vm.setFunctions(functions);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), 0);
+}
+
 TEST_F(LooksBlocksTest, ChangeSizeBy)
 {
     Compiler compiler(&m_engineMock);
@@ -363,11 +980,9 @@ TEST_F(LooksBlocksTest, SwitchCostumeTo)
     auto block10 = std::make_shared<Block>("f", "looks_switchcostumeto");
     addDropdownInput(block10, "COSTUME", LooksBlocks::COSTUME, "previous costume");
 
-    // switch costume to (join "" "")
-    auto joinBlock = std::make_shared<Block>("h", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
+    // switch costume to (null block)
     auto block11 = std::make_shared<Block>("g", "looks_switchcostumeto");
-    addDropdownInput(block11, "COSTUME", LooksBlocks::COSTUME, "", joinBlock);
+    addDropdownInput(block11, "COSTUME", LooksBlocks::COSTUME, "", createNullBlock("h"));
 
     compiler.init();
 
@@ -473,8 +1088,6 @@ TEST_F(LooksBlocksTest, SwitchCostumeTo)
               vm::OP_EXEC,
               0,
               vm::OP_NULL,
-              vm::OP_NULL,
-              vm::OP_STR_CONCAT,
               vm::OP_EXEC,
               3,
               vm::OP_HALT }));
@@ -785,11 +1398,9 @@ TEST_F(LooksBlocksTest, SwitchBackdropTo)
     auto block12 = std::make_shared<Block>("h", "looks_switchbackdropto");
     addDropdownInput(block12, "BACKDROP", LooksBlocks::BACKDROP, "random backdrop");
 
-    // switch backdrop to (join "" "")
-    auto joinBlock = std::make_shared<Block>("j", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
+    // switch backdrop to (null block)
     auto block13 = std::make_shared<Block>("i", "looks_switchbackdropto");
-    addDropdownInput(block13, "BACKDROP", LooksBlocks::BACKDROP, "", joinBlock);
+    addDropdownInput(block13, "BACKDROP", LooksBlocks::BACKDROP, "", createNullBlock("j"));
 
     compiler.init();
 
@@ -925,8 +1536,6 @@ TEST_F(LooksBlocksTest, SwitchBackdropTo)
               vm::OP_EXEC,
               0,
               vm::OP_NULL,
-              vm::OP_NULL,
-              vm::OP_STR_CONCAT,
               vm::OP_EXEC,
               4,
               vm::OP_HALT }));
@@ -1196,11 +1805,9 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
     auto block12 = std::make_shared<Block>("h", "looks_switchbackdroptoandwait");
     addDropdownInput(block12, "BACKDROP", LooksBlocks::BACKDROP, "random backdrop");
 
-    // switch backdrop to (join "" "") and wait
-    auto joinBlock = std::make_shared<Block>("j", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
+    // switch backdrop to (null block) and wait
     auto block13 = std::make_shared<Block>("i", "looks_switchbackdroptoandwait");
-    addDropdownInput(block13, "BACKDROP", LooksBlocks::BACKDROP, "", joinBlock);
+    addDropdownInput(block13, "BACKDROP", LooksBlocks::BACKDROP, "", createNullBlock("j"));
 
     compiler.init();
 
@@ -1376,8 +1983,6 @@ TEST_F(LooksBlocksTest, SwitchBackdropToAndWait)
               vm::OP_EXEC,
               5,
               vm::OP_NULL,
-              vm::OP_NULL,
-              vm::OP_STR_CONCAT,
               vm::OP_EXEC,
               4,
               vm::OP_EXEC,
diff --git a/test/blocks/motion_blocks_test.cpp b/test/blocks/motion_blocks_test.cpp
index fc6aa58f..a1fa70ca 100644
--- a/test/blocks/motion_blocks_test.cpp
+++ b/test/blocks/motion_blocks_test.cpp
@@ -30,13 +30,21 @@ class MotionBlocksTest : public testing::Test
         // For any motion block
         std::shared_ptr<Block> createMotionBlock(const std::string &id, const std::string &opcode) const { return std::make_shared<Block>(id, opcode); }
 
+        std::shared_ptr<Block> createNullBlock(const std::string &id)
+        {
+            std::shared_ptr<Block> block = std::make_shared<Block>(id, "");
+            BlockComp func = [](Compiler *compiler) { compiler->addInstruction(vm::OP_NULL); };
+            block->setCompileFunction(func);
+
+            return block;
+        }
+
         void addValueInput(std::shared_ptr<Block> block, const std::string &name, MotionBlocks::Inputs id, const Value &value) const
         {
             auto input = std::make_shared<Input>(name, Input::Type::Shadow);
             input->setPrimaryValue(value);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addObscuredInput(std::shared_ptr<Block> block, const std::string &name, MotionBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
@@ -45,7 +53,6 @@ class MotionBlocksTest : public testing::Test
             input->setValueBlock(valueBlock);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         std::shared_ptr<Input> addNullInput(std::shared_ptr<Block> block, const std::string &name, MotionBlocks::Inputs id) const
@@ -53,7 +60,6 @@ class MotionBlocksTest : public testing::Test
             auto input = std::make_shared<Input>(name, Input::Type::Shadow);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
 
             return input;
         }
@@ -76,7 +82,6 @@ class MotionBlocksTest : public testing::Test
             field->setFieldId(id);
             field->setSpecialValueId(valueId);
             block->addField(field);
-            block->updateFieldMap();
         }
 
         std::unique_ptr<IBlockSection> m_section;
@@ -308,11 +313,9 @@ TEST_F(MotionBlocksTest, PointTowards)
     auto block3 = std::make_shared<Block>("c", "motion_pointtowards");
     addDropdownInput(block3, "TOWARDS", MotionBlocks::TOWARDS, "Sprite2");
 
-    // point towards (join "" "")
-    auto joinBlock = std::make_shared<Block>("e", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
+    // point towards (null block)
     auto block4 = std::make_shared<Block>("d", "motion_pointtowards");
-    addDropdownInput(block4, "TOWARDS", MotionBlocks::TOWARDS, "", joinBlock);
+    addDropdownInput(block4, "TOWARDS", MotionBlocks::TOWARDS, "", createNullBlock("e"));
 
     compiler.init();
 
@@ -335,9 +338,7 @@ TEST_F(MotionBlocksTest, PointTowards)
 
     compiler.end();
 
-    ASSERT_EQ(
-        compiler.bytecode(),
-        std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_NULL, vm::OP_STR_CONCAT, vm::OP_EXEC, 3, vm::OP_HALT }));
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 3, vm::OP_HALT }));
     ASSERT_EQ(compiler.constValues().size(), 1);
     ASSERT_EQ(compiler.constValues()[0].toDouble(), 5);
 }
@@ -526,11 +527,9 @@ TEST_F(MotionBlocksTest, GoTo)
     auto block3 = std::make_shared<Block>("c", "motion_goto");
     addDropdownInput(block3, "TO", MotionBlocks::TO, "Sprite2");
 
-    // go to (join "" "")
-    auto joinBlock = std::make_shared<Block>("e", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
+    // go to (null block)
     auto block4 = std::make_shared<Block>("d", "motion_goto");
-    addDropdownInput(block4, "TO", MotionBlocks::TO, "", joinBlock);
+    addDropdownInput(block4, "TO", MotionBlocks::TO, "", createNullBlock("e"));
 
     compiler.init();
 
@@ -553,9 +552,7 @@ TEST_F(MotionBlocksTest, GoTo)
 
     compiler.end();
 
-    ASSERT_EQ(
-        compiler.bytecode(),
-        std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_NULL, vm::OP_STR_CONCAT, vm::OP_EXEC, 3, vm::OP_HALT }));
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 3, vm::OP_HALT }));
     ASSERT_EQ(compiler.constValues().size(), 1);
     ASSERT_EQ(compiler.constValues()[0].toDouble(), 5);
 }
@@ -762,12 +759,10 @@ TEST_F(MotionBlocksTest, GlideTo)
     addValueInput(block3, "SECS", MotionBlocks::SECS, 3.25);
     addDropdownInput(block3, "TO", MotionBlocks::TO, "Sprite2");
 
-    // glide (6.5) secs to (join "" "")
-    auto joinBlock = std::make_shared<Block>("e", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
+    // glide (6.5) secs to (null block)
     auto block4 = std::make_shared<Block>("d", "motion_glideto");
     addValueInput(block4, "SECS", MotionBlocks::SECS, 6.5);
-    addDropdownInput(block4, "TO", MotionBlocks::TO, "", joinBlock);
+    addDropdownInput(block4, "TO", MotionBlocks::TO, "", createNullBlock("e"));
 
     compiler.init();
 
@@ -821,8 +816,6 @@ TEST_F(MotionBlocksTest, GlideTo)
               vm::OP_CONST,
               4,
               vm::OP_NULL,
-              vm::OP_NULL,
-              vm::OP_STR_CONCAT,
               vm::OP_EXEC,
               3,
               vm::OP_EXEC,
diff --git a/test/blocks/operator_blocks_test.cpp b/test/blocks/operator_blocks_test.cpp
index 24f6504b..cb5a465e 100644
--- a/test/blocks/operator_blocks_test.cpp
+++ b/test/blocks/operator_blocks_test.cpp
@@ -30,7 +30,6 @@ class OperatorBlocksTest : public testing::Test
             input->setPrimaryValue(value);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addObscuredInput(std::shared_ptr<Block> block, const std::string &name, OperatorBlocks::Inputs id, std::shared_ptr<Block> valueBlock) const
@@ -39,7 +38,6 @@ class OperatorBlocksTest : public testing::Test
             input->setValueBlock(valueBlock);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         void addNullInput(std::shared_ptr<Block> block, const std::string &name, OperatorBlocks::Inputs id) const
@@ -47,7 +45,6 @@ class OperatorBlocksTest : public testing::Test
             auto input = std::make_shared<Input>(name, Input::Type::Shadow);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         // For the mathop block
@@ -57,7 +54,6 @@ class OperatorBlocksTest : public testing::Test
             field->setFieldId(OperatorBlocks::OPERATOR);
             field->setSpecialValueId(operatorId);
             block->addField(field);
-            block->updateFieldMap();
         }
 
         std::unique_ptr<IBlockSection> m_section;
diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index 69d8fddc..4a9fe5bb 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -3,6 +3,8 @@
 #include <scratchcpp/input.h>
 #include <scratchcpp/field.h>
 #include <scratchcpp/sprite.h>
+#include <scratchcpp/stage.h>
+#include <scratchcpp/costume.h>
 #include <enginemock.h>
 #include <timermock.h>
 #include <clockmock.h>
@@ -32,7 +34,15 @@ class SensingBlocksTest : public testing::Test
             optionField->setFieldId(SensingBlocks::CURRENTMENU);
             optionField->setSpecialValueId(option);
             block->addField(optionField);
-            block->updateFieldMap();
+
+            return block;
+        }
+
+        std::shared_ptr<Block> createNullBlock(const std::string &id)
+        {
+            std::shared_ptr<Block> block = std::make_shared<Block>(id, "");
+            BlockComp func = [](Compiler *compiler) { compiler->addInstruction(vm::OP_NULL); };
+            block->setCompileFunction(func);
 
             return block;
         }
@@ -43,7 +53,6 @@ class SensingBlocksTest : public testing::Test
             input->setValueBlock(valueBlock);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
         }
 
         std::shared_ptr<Input> addNullInput(std::shared_ptr<Block> block, const std::string &name, SensingBlocks::Inputs id) const
@@ -51,7 +60,6 @@ class SensingBlocksTest : public testing::Test
             auto input = std::make_shared<Input>(name, Input::Type::Shadow);
             input->setInputId(id);
             block->addInput(input);
-            block->updateInputMap();
 
             return input;
         }
@@ -74,7 +82,6 @@ class SensingBlocksTest : public testing::Test
             field->setFieldId(id);
             field->setSpecialValueId(valueId);
             block->addField(field);
-            block->updateFieldMap();
         }
 
         std::unique_ptr<IBlockSection> m_section;
@@ -98,16 +105,26 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
 {
     // Blocks
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_distanceto", &SensingBlocks::compileDistanceTo));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_keypressed", &SensingBlocks::compileKeyPressed));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousedown", &SensingBlocks::compileMouseDown));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousex", &SensingBlocks::compileMouseX));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousey", &SensingBlocks::compileMouseY));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_setdragmode", &SensingBlocks::compileSetDragMode));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_timer", &SensingBlocks::compileTimer));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_resettimer", &SensingBlocks::compileResetTimer));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_of", &SensingBlocks::compileOf));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_current", &SensingBlocks::compileCurrent));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_dayssince2000", &SensingBlocks::compileDaysSince2000));
 
     // Inputs
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "DISTANCETOMENU", SensingBlocks::DISTANCETOMENU));
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "KEY_OPTION", SensingBlocks::KEY_OPTION));
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "OBJECT", SensingBlocks::OBJECT));
 
     // Fields
     EXPECT_CALL(m_engineMock, addField(m_section.get(), "CURRENTMENU", SensingBlocks::CURRENTMENU));
+    EXPECT_CALL(m_engineMock, addField(m_section.get(), "DRAG_MODE", SensingBlocks::DRAG_MODE));
+    EXPECT_CALL(m_engineMock, addField(m_section.get(), "PROPERTY", SensingBlocks::PROPERTY));
 
     // Field values
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "YEAR", SensingBlocks::YEAR));
@@ -117,6 +134,18 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "HOUR", SensingBlocks::HOUR));
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "MINUTE", SensingBlocks::MINUTE));
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "SECOND", SensingBlocks::SECOND));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "draggable", SensingBlocks::Draggable));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "not draggable", SensingBlocks::NotDraggable));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "x position", SensingBlocks::XPosition));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "y position", SensingBlocks::YPosition));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "direction", SensingBlocks::Direction));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "costume #", SensingBlocks::CostumeNumber));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "costume name", SensingBlocks::CostumeName));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "size", SensingBlocks::Size));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "volume", SensingBlocks::Volume));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "background #", SensingBlocks::BackdropNumber));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "backdrop #", SensingBlocks::BackdropNumber));
+    EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "backdrop name", SensingBlocks::BackdropName));
 
     m_section->registerBlocks(&m_engineMock);
 }
@@ -133,11 +162,9 @@ TEST_F(SensingBlocksTest, DistanceTo)
     auto block2 = std::make_shared<Block>("b", "sensing_distanceto");
     addDropdownInput(block2, "DISTANCETOMENU", SensingBlocks::DISTANCETOMENU, "_mouse_");
 
-    // distance to (join "" "")
-    auto joinBlock = std::make_shared<Block>("d", "operator_join");
-    joinBlock->setCompileFunction(&OperatorBlocks::compileJoin);
+    // distance to (null block)
     auto block3 = std::make_shared<Block>("c", "sensing_distanceto");
-    addDropdownInput(block3, "DISTANCETOMENU", SensingBlocks::DISTANCETOMENU, "", joinBlock);
+    addDropdownInput(block3, "DISTANCETOMENU", SensingBlocks::DISTANCETOMENU, "", createNullBlock("d"));
 
     compiler.init();
 
@@ -156,9 +183,7 @@ TEST_F(SensingBlocksTest, DistanceTo)
 
     compiler.end();
 
-    ASSERT_EQ(
-        compiler.bytecode(),
-        std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_NULL, vm::OP_NULL, vm::OP_STR_CONCAT, vm::OP_EXEC, 2, vm::OP_HALT }));
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT }));
     ASSERT_EQ(compiler.constValues().size(), 1);
     ASSERT_EQ(compiler.constValues()[0].toDouble(), 5);
 }
@@ -256,6 +281,260 @@ TEST_F(SensingBlocksTest, DistanceToImpl)
     ASSERT_EQ(std::round(vm.getInput(0, 1)->toDouble() * 10000) / 10000, 261.0096);
 }
 
+TEST_F(SensingBlocksTest, KeyPressed)
+{
+    Compiler compiler(&m_engineMock);
+
+    // key (space) pressed?
+    auto block1 = std::make_shared<Block>("a", "sensing_keypressed");
+    addDropdownInput(block1, "KEY_OPTION", SensingBlocks::KEY_OPTION, "space");
+
+    // key (any) pressed?
+    auto block2 = std::make_shared<Block>("b", "sensing_keypressed");
+    addDropdownInput(block2, "KEY_OPTION", SensingBlocks::KEY_OPTION, "any");
+
+    // key (null block) pressed?
+    auto block3 = std::make_shared<Block>("c", "sensing_keypressed");
+    addDropdownInput(block3, "KEY_OPTION", SensingBlocks::KEY_OPTION, "", createNullBlock("d"));
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::keyPressed)).WillOnce(Return(0));
+    compiler.setBlock(block1);
+    SensingBlocks::compileKeyPressed(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::keyPressed)).WillOnce(Return(0));
+    compiler.setBlock(block2);
+    SensingBlocks::compileKeyPressed(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::keyPressed)).WillOnce(Return(0));
+    compiler.setBlock(block3);
+    SensingBlocks::compileKeyPressed(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_NULL, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "space", "any" }));
+}
+
+TEST_F(SensingBlocksTest, KeyPressedImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::keyPressed };
+    static Value constValues[] = { "space", "any" };
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, keyPressed("space")).WillOnce(Return(true));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toBool(), true);
+
+    EXPECT_CALL(m_engineMock, keyPressed("space")).WillOnce(Return(false));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toBool(), false);
+
+    EXPECT_CALL(m_engineMock, keyPressed("any")).WillOnce(Return(true));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toBool(), true);
+
+    EXPECT_CALL(m_engineMock, keyPressed("any")).WillOnce(Return(false));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toBool(), false);
+}
+
+TEST_F(SensingBlocksTest, MouseDown)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = std::make_shared<Block>("a", "sensing_mousedown");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::mouseDown)).WillOnce(Return(0));
+    compiler.init();
+
+    compiler.setBlock(block);
+    SensingBlocks::compileMouseDown(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+}
+
+TEST_F(SensingBlocksTest, MouseDownImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::mouseDown };
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+
+    EXPECT_CALL(m_engineMock, mousePressed()).WillOnce(Return(true));
+    vm.setBytecode(bytecode);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toBool(), true);
+
+    EXPECT_CALL(m_engineMock, mousePressed()).WillOnce(Return(false));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toBool(), false);
+}
+
+TEST_F(SensingBlocksTest, MouseX)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = std::make_shared<Block>("a", "sensing_mousex");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::mouseX)).WillOnce(Return(0));
+    compiler.init();
+
+    compiler.setBlock(block);
+    SensingBlocks::compileMouseX(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+}
+
+TEST_F(SensingBlocksTest, MouseXImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::mouseX };
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+
+    EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(48.165));
+    vm.setBytecode(bytecode);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 48.165);
+
+    EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(-239.09));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -239.09);
+}
+
+TEST_F(SensingBlocksTest, MouseY)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = std::make_shared<Block>("a", "sensing_mousey");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::mouseY)).WillOnce(Return(0));
+    compiler.init();
+
+    compiler.setBlock(block);
+    SensingBlocks::compileMouseY(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+}
+
+TEST_F(SensingBlocksTest, MouseYImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::mouseY };
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+
+    EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(159.084));
+    vm.setBytecode(bytecode);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 159.084);
+
+    EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-95.564));
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -95.564);
+}
+
+TEST_F(SensingBlocksTest, SetDragMode)
+{
+    Compiler compiler(&m_engineMock);
+
+    // set drag mode [draggable]
+    auto block1 = std::make_shared<Block>("a", "sensing_setdragmode");
+    addDropdownField(block1, "DRAG_MODE", SensingBlocks::DRAG_MODE, "draggable", SensingBlocks::Draggable);
+
+    // set drag mode [not draggable]
+    auto block2 = std::make_shared<Block>("b", "sensing_setdragmode");
+    addDropdownField(block2, "DRAG_MODE", SensingBlocks::DRAG_MODE, "not draggable", SensingBlocks::NotDraggable);
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::setDraggableMode)).WillOnce(Return(0));
+    compiler.setBlock(block1);
+    SensingBlocks::compileSetDragMode(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::setNotDraggableMode)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    SensingBlocks::compileSetDragMode(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_HALT }));
+    ASSERT_TRUE(compiler.constValues().empty());
+}
+
+TEST_F(SensingBlocksTest, SetDragModeImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::setDraggableMode, &SensingBlocks::setNotDraggableMode };
+
+    Sprite sprite;
+    sprite.setDraggable(false);
+
+    VirtualMachine vm(&sprite, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(sprite.draggable());
+
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_FALSE(sprite.draggable());
+}
+
 TEST_F(SensingBlocksTest, Timer)
 {
     Compiler compiler(&m_engineMock);
@@ -323,6 +602,584 @@ TEST_F(SensingBlocksTest, ResetTimerImpl)
     ASSERT_EQ(vm.registerCount(), 0);
 }
 
+TEST_F(SensingBlocksTest, Of)
+{
+    Compiler compiler(&m_engineMock);
+
+    // [x position] of (Sprite2)
+    auto block1 = std::make_shared<Block>("a", "sensing_of");
+    addDropdownField(block1, "PROPERTY", SensingBlocks::PROPERTY, "x position", SensingBlocks::XPosition);
+    addDropdownInput(block1, "OBJECT", SensingBlocks::OBJECT, "Sprite2");
+
+    // [x position] of (null block)
+    auto block2 = std::make_shared<Block>("b", "sensing_of");
+    addDropdownField(block2, "PROPERTY", SensingBlocks::PROPERTY, "x position", SensingBlocks::XPosition);
+    addDropdownInput(block2, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("c"));
+
+    // [y position] of (Sprite2)
+    auto block3 = std::make_shared<Block>("d", "sensing_of");
+    addDropdownField(block3, "PROPERTY", SensingBlocks::PROPERTY, "y position", SensingBlocks::YPosition);
+    addDropdownInput(block3, "OBJECT", SensingBlocks::OBJECT, "Sprite2");
+
+    // [y position] of (null block)
+    auto block4 = std::make_shared<Block>("e", "sensing_of");
+    addDropdownField(block4, "PROPERTY", SensingBlocks::PROPERTY, "y position", SensingBlocks::YPosition);
+    addDropdownInput(block4, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("f"));
+
+    // [direction] of (Sprite2)
+    auto block5 = std::make_shared<Block>("g", "sensing_of");
+    addDropdownField(block5, "PROPERTY", SensingBlocks::PROPERTY, "direction", SensingBlocks::Direction);
+    addDropdownInput(block5, "OBJECT", SensingBlocks::OBJECT, "Sprite2");
+
+    // [direction] of (null block)
+    auto block6 = std::make_shared<Block>("h", "sensing_of");
+    addDropdownField(block6, "PROPERTY", SensingBlocks::PROPERTY, "direction", SensingBlocks::Direction);
+    addDropdownInput(block6, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("i"));
+
+    // [costume #] of (Sprite2)
+    auto block7 = std::make_shared<Block>("j", "sensing_of");
+    addDropdownField(block7, "PROPERTY", SensingBlocks::PROPERTY, "costume #", SensingBlocks::CostumeNumber);
+    addDropdownInput(block7, "OBJECT", SensingBlocks::OBJECT, "Sprite2");
+
+    // [costume #] of (null block)
+    auto block8 = std::make_shared<Block>("k", "sensing_of");
+    addDropdownField(block8, "PROPERTY", SensingBlocks::PROPERTY, "costume #", SensingBlocks::CostumeNumber);
+    addDropdownInput(block8, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("l"));
+
+    // [costume name] of (Sprite2)
+    auto block9 = std::make_shared<Block>("m", "sensing_of");
+    addDropdownField(block9, "PROPERTY", SensingBlocks::PROPERTY, "costume name", SensingBlocks::CostumeName);
+    addDropdownInput(block9, "OBJECT", SensingBlocks::OBJECT, "Sprite2");
+
+    // [costume name] of (null block)
+    auto block10 = std::make_shared<Block>("n", "sensing_of");
+    addDropdownField(block10, "PROPERTY", SensingBlocks::PROPERTY, "costume name", SensingBlocks::CostumeName);
+    addDropdownInput(block10, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("o"));
+
+    // [size] of (Sprite2)
+    auto block11 = std::make_shared<Block>("p", "sensing_of");
+    addDropdownField(block11, "PROPERTY", SensingBlocks::PROPERTY, "size", SensingBlocks::Size);
+    addDropdownInput(block11, "OBJECT", SensingBlocks::OBJECT, "Sprite2");
+
+    // [size] of (null block)
+    auto block12 = std::make_shared<Block>("q", "sensing_of");
+    addDropdownField(block12, "PROPERTY", SensingBlocks::PROPERTY, "size", SensingBlocks::Size);
+    addDropdownInput(block12, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("r"));
+
+    // [volume] of (Sprite2)
+    auto block13 = std::make_shared<Block>("s", "sensing_of");
+    addDropdownField(block13, "PROPERTY", SensingBlocks::PROPERTY, "volume", SensingBlocks::Volume);
+    addDropdownInput(block13, "OBJECT", SensingBlocks::OBJECT, "Sprite2");
+
+    // [volume] of (null block)
+    auto block14 = std::make_shared<Block>("t", "sensing_of");
+    addDropdownField(block14, "PROPERTY", SensingBlocks::PROPERTY, "volume", SensingBlocks::Volume);
+    addDropdownInput(block14, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("u"));
+
+    // [background #] of (Stage)
+    auto block15 = std::make_shared<Block>("v", "sensing_of");
+    addDropdownField(block15, "PROPERTY", SensingBlocks::PROPERTY, "background #", SensingBlocks::BackdropNumber);
+    addDropdownInput(block15, "OBJECT", SensingBlocks::OBJECT, "_stage_");
+
+    // [background #] of (null block)
+    auto block16 = std::make_shared<Block>("w", "sensing_of");
+    addDropdownField(block16, "PROPERTY", SensingBlocks::PROPERTY, "background #", SensingBlocks::BackdropNumber);
+    addDropdownInput(block16, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("x"));
+
+    // [backdrop #] of (Stage)
+    auto block17 = std::make_shared<Block>("y", "sensing_of");
+    addDropdownField(block17, "PROPERTY", SensingBlocks::PROPERTY, "backdrop #", SensingBlocks::BackdropNumber);
+    addDropdownInput(block17, "OBJECT", SensingBlocks::OBJECT, "_stage_");
+
+    // [backdrop #] of (null block)
+    auto block18 = std::make_shared<Block>("z", "sensing_of");
+    addDropdownField(block18, "PROPERTY", SensingBlocks::PROPERTY, "backdrop #", SensingBlocks::BackdropNumber);
+    addDropdownInput(block18, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("aa"));
+
+    // [backdrop name] of (Stage)
+    auto block19 = std::make_shared<Block>("ab", "sensing_of");
+    addDropdownField(block19, "PROPERTY", SensingBlocks::PROPERTY, "backdrop name", SensingBlocks::BackdropName);
+    addDropdownInput(block19, "OBJECT", SensingBlocks::OBJECT, "_stage_");
+
+    // [backdrop name] of (null block)
+    auto block20 = std::make_shared<Block>("ac", "sensing_of");
+    addDropdownField(block20, "PROPERTY", SensingBlocks::PROPERTY, "backdrop name", SensingBlocks::BackdropName);
+    addDropdownInput(block20, "OBJECT", SensingBlocks::OBJECT, "", createNullBlock("ad"));
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).Times(7).WillRepeatedly(Return(6));
+    EXPECT_CALL(m_engineMock, findTarget("_stage_")).Times(3).WillRepeatedly(Return(0));
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::xPositionOfSpriteByIndex)).WillOnce(Return(0));
+    compiler.setBlock(block1);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::xPositionOfSprite)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::yPositionOfSpriteByIndex)).WillOnce(Return(2));
+    compiler.setBlock(block3);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::yPositionOfSprite)).WillOnce(Return(3));
+    compiler.setBlock(block4);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::directionOfSpriteByIndex)).WillOnce(Return(4));
+    compiler.setBlock(block5);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::directionOfSprite)).WillOnce(Return(5));
+    compiler.setBlock(block6);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::costumeNumberOfSpriteByIndex)).WillOnce(Return(6));
+    compiler.setBlock(block7);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::costumeNumberOfSprite)).WillOnce(Return(7));
+    compiler.setBlock(block8);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::costumeNameOfSpriteByIndex)).WillOnce(Return(8));
+    compiler.setBlock(block9);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::costumeNameOfSprite)).WillOnce(Return(9));
+    compiler.setBlock(block10);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::sizeOfSpriteByIndex)).WillOnce(Return(10));
+    compiler.setBlock(block11);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::sizeOfSprite)).WillOnce(Return(11));
+    compiler.setBlock(block12);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::volumeOfTargetByIndex)).WillOnce(Return(12));
+    compiler.setBlock(block13);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::volumeOfTarget)).WillOnce(Return(13));
+    compiler.setBlock(block14);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::backdropNumberOfStageByIndex)).WillOnce(Return(14));
+    compiler.setBlock(block15);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::backdropNumberOfStage)).WillOnce(Return(15));
+    compiler.setBlock(block16);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::backdropNumberOfStageByIndex)).WillOnce(Return(14));
+    compiler.setBlock(block17);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::backdropNumberOfStage)).WillOnce(Return(15));
+    compiler.setBlock(block18);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::backdropNameOfStageByIndex)).WillOnce(Return(16));
+    compiler.setBlock(block19);
+    SensingBlocks::compileOf(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::backdropNameOfStage)).WillOnce(Return(17));
+    compiler.setBlock(block20);
+    SensingBlocks::compileOf(&compiler);
+
+    compiler.end();
+
+    ASSERT_EQ(
+        compiler.bytecode(),
+        std::vector<unsigned int>(
+            { vm::OP_START,
+              vm::OP_CONST,
+              0,
+              vm::OP_EXEC,
+              0,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              1,
+              vm::OP_CONST,
+              1,
+              vm::OP_EXEC,
+              2,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              3,
+              vm::OP_CONST,
+              2,
+              vm::OP_EXEC,
+              4,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              5,
+              vm::OP_CONST,
+              3,
+              vm::OP_EXEC,
+              6,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              7,
+              vm::OP_CONST,
+              4,
+              vm::OP_EXEC,
+              8,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              9,
+              vm::OP_CONST,
+              5,
+              vm::OP_EXEC,
+              10,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              11,
+              vm::OP_CONST,
+              6,
+              vm::OP_EXEC,
+              12,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              13,
+              vm::OP_CONST,
+              7,
+              vm::OP_EXEC,
+              14,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              15,
+              vm::OP_CONST,
+              8,
+              vm::OP_EXEC,
+              14,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              15,
+              vm::OP_CONST,
+              9,
+              vm::OP_EXEC,
+              16,
+              vm::OP_NULL,
+              vm::OP_EXEC,
+              17,
+              vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ 6, 6, 6, 6, 6, 6, 6, 0, 0, 0 }));
+}
+
+TEST_F(SensingBlocksTest, XPositionOfSprite)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::xPositionOfSprite, &SensingBlocks::xPositionOfSpriteByIndex };
+    static Value constValues[] = { "Sprite2", 6 };
+
+    Sprite sprite;
+    sprite.setX(-168.088);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -168.088);
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -168.088);
+}
+
+TEST_F(SensingBlocksTest, YPositionOfSprite)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::yPositionOfSprite, &SensingBlocks::yPositionOfSpriteByIndex };
+    static Value constValues[] = { "Sprite2", 6 };
+
+    Sprite sprite;
+    sprite.setY(-16.548);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -16.548);
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -16.548);
+}
+
+TEST_F(SensingBlocksTest, DirectionOfSprite)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::directionOfSprite, &SensingBlocks::directionOfSpriteByIndex };
+    static Value constValues[] = { "Sprite2", 6 };
+
+    Sprite sprite;
+    sprite.setDirection(-109.087);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -109.087);
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), -109.087);
+}
+
+TEST_F(SensingBlocksTest, CostumeNumberOfSprite)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::costumeNumberOfSprite, &SensingBlocks::costumeNumberOfSpriteByIndex };
+    static Value constValues[] = { "Sprite2", 6 };
+
+    Sprite sprite;
+    sprite.addCostume(std::make_shared<Costume>("costume1", "a", "png"));
+    sprite.addCostume(std::make_shared<Costume>("costume2", "b", "svg"));
+    sprite.setCurrentCostume(2);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 2);
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 2);
+}
+
+TEST_F(SensingBlocksTest, CostumeNameOfSprite)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::costumeNameOfSprite, &SensingBlocks::costumeNameOfSpriteByIndex };
+    static Value constValues[] = { "Sprite2", 6 };
+
+    Sprite sprite;
+    sprite.addCostume(std::make_shared<Costume>("costume1", "a", "png"));
+    sprite.addCostume(std::make_shared<Costume>("costume2", "b", "svg"));
+    sprite.setCurrentCostume(2);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toString(), "costume2");
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toString(), "costume2");
+}
+
+TEST_F(SensingBlocksTest, SizeOfSprite)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::sizeOfSprite, &SensingBlocks::sizeOfSpriteByIndex };
+    static Value constValues[] = { "Sprite2", 6 };
+
+    Sprite sprite;
+    sprite.setSize(48.642);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 48.642);
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 48.642);
+}
+
+TEST_F(SensingBlocksTest, VolumeOfTarget)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::volumeOfTarget, &SensingBlocks::volumeOfTargetByIndex };
+    static Value constValues[] = { "Sprite2", "_stage_", 6, 0 };
+
+    Sprite sprite;
+    sprite.setVolume(85);
+
+    Stage stage;
+    stage.setVolume(48);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("Sprite2")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), sprite.volume());
+
+    EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(&stage));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), stage.volume());
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&sprite));
+    vm.reset();
+    vm.setBytecode(bytecode3);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), sprite.volume());
+
+    EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(&stage));
+    vm.reset();
+    vm.setBytecode(bytecode4);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), stage.volume());
+}
+
+TEST_F(SensingBlocksTest, BackdropNumberOfStage)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::backdropNumberOfStage, &SensingBlocks::backdropNumberOfStageByIndex };
+    static Value constValues[] = { "_stage_", 6 };
+
+    Stage stage;
+    stage.addCostume(std::make_shared<Costume>("backdrop1", "a", "png"));
+    stage.addCostume(std::make_shared<Costume>("backdrop2", "b", "svg"));
+    stage.setCurrentCostume(2);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&stage));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 2);
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&stage));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toDouble(), 2);
+}
+
+TEST_F(SensingBlocksTest, BackdropNameOfStage)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::backdropNameOfStage, &SensingBlocks::backdropNameOfStageByIndex };
+    static Value constValues[] = { "_stage_", 6 };
+
+    Stage stage;
+    stage.addCostume(std::make_shared<Costume>("backdrop1", "a", "png"));
+    stage.addCostume(std::make_shared<Costume>("backdrop2", "b", "svg"));
+    stage.setCurrentCostume(2);
+
+    VirtualMachine vm(nullptr, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(6));
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&stage));
+    vm.setBytecode(bytecode1);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toString(), "backdrop2");
+
+    EXPECT_CALL(m_engineMock, targetAt(6)).WillOnce(Return(&stage));
+    vm.reset();
+    vm.setBytecode(bytecode2);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toString(), "backdrop2");
+}
+
 TEST_F(SensingBlocksTest, Current)
 {
     Compiler compiler(&m_engineMock);
diff --git a/test/blocks/variable_blocks_test.cpp b/test/blocks/variable_blocks_test.cpp
index 24f0d1ec..33eecad3 100644
--- a/test/blocks/variable_blocks_test.cpp
+++ b/test/blocks/variable_blocks_test.cpp
@@ -28,13 +28,11 @@ class VariableBlocksTest : public testing::Test
             auto variableField = std::make_shared<Field>("VARIABLE", Value(), variable);
             variableField->setFieldId(VariableBlocks::VARIABLE);
             block->addField(variableField);
-            block->updateFieldMap();
 
             auto valueInput = std::make_shared<Input>("VALUE", Input::Type::Shadow);
             valueInput->setPrimaryValue(value);
             valueInput->setInputId(VariableBlocks::VALUE);
             block->addInput(valueInput);
-            block->updateInputMap();
 
             return block;
         }
diff --git a/test/broadcasts.sb3 b/test/broadcasts.sb3
new file mode 100644
index 00000000..0347dd43
Binary files /dev/null and b/test/broadcasts.sb3 differ
diff --git a/test/clone_limit.sb3 b/test/clone_limit.sb3
new file mode 100644
index 00000000..75338c6e
Binary files /dev/null and b/test/clone_limit.sb3 differ
diff --git a/test/compiler/compiler_test.cpp b/test/compiler/compiler_test.cpp
index 97b38abb..baf71059 100644
--- a/test/compiler/compiler_test.cpp
+++ b/test/compiler/compiler_test.cpp
@@ -179,8 +179,7 @@ TEST_F(CompilerTest, ResolveIds)
     ASSERT_EQ(engine->broadcasts().size(), 1);
 
     // Stage
-    ASSERT_NE(engine->findTarget("Stage"), -1);
-    Stage *stage = dynamic_cast<Stage *>(engine->targetAt(engine->findTarget("Stage")));
+    Stage *stage = engine->stage();
     ASSERT_TRUE(stage);
     ASSERT_EQ(stage->blocks().size(), 2);
     auto block = stage->greenFlagBlocks().at(0);
@@ -339,7 +338,6 @@ TEST_F(CompilerTest, ResolveInput)
     input->setPrimaryValue("test");
     input->setInputId(TestBlockSection::INPUT1);
     block->addInput(input);
-    block->updateInputMap();
     block->setCompileFunction(&TestBlockSection::compileTestBlock1);
 
     compiler.compile(block);
@@ -362,7 +360,6 @@ TEST_F(CompilerTest, ResolveDropdownMenuInput)
     input->setValueBlock(menu);
     menu->addField(optionField);
     block->addInput(input);
-    block->updateInputMap();
     block->setCompileFunction(&TestBlockSection::compileTestBlock1);
 
     compiler.compile(block);
@@ -403,7 +400,6 @@ TEST_F(CompilerTest, ResolveField)
     field->setFieldId(TestBlockSection::FIELD1);
     field->setSpecialValueId(TestBlockSection::TestValue);
     block->addField(field);
-    block->updateFieldMap();
     block->setCompileFunction(&TestBlockSection::compileTestBlock2);
 
     compiler.compile(block);
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 44d14a7c..d8f94e10 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -6,15 +6,25 @@
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <timermock.h>
+#include <clockmock.h>
 
 #include "../common.h"
 #include "testsection.h"
 #include "engine/internal/engine.h"
+#include "engine/internal/clock.h"
 
 using namespace libscratchcpp;
 
+using ::testing::Return;
+
 // NOTE: resolveIds() and compile() are tested in load_project_test
 
+TEST(EngineTest, Clock)
+{
+    Engine engine;
+    ASSERT_EQ(engine.m_clock, Clock::instance().get());
+}
+
 TEST(EngineTest, Clear)
 {
     Engine engine;
@@ -61,6 +71,299 @@ TEST(EngineTest, Fps)
     ASSERT_EQ(engine.fps(), 60.25);
 }
 
+TEST(EngineTest, FpsProject)
+{
+    Project p("2_frames.sb3");
+    ASSERT_TRUE(p.load());
+
+    ClockMock clock;
+    Engine *engine = dynamic_cast<Engine *>(p.engine().get());
+    engine->m_clock = &clock;
+
+    std::chrono::steady_clock::time_point time1(std::chrono::milliseconds(50));
+    std::chrono::steady_clock::time_point time2(std::chrono::milliseconds(75));
+    std::chrono::steady_clock::time_point time3(std::chrono::milliseconds(83));
+    std::chrono::steady_clock::time_point time4(std::chrono::milliseconds(116));
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time1)).WillOnce(Return(time2)).WillOnce(Return(time3)).WillOnce(Return(time4)).WillOnce(Return(time4));
+    EXPECT_CALL(clock, sleep(std::chrono::milliseconds(8)));
+    p.run();
+
+    engine->setFps(10);
+    std::chrono::steady_clock::time_point time5(std::chrono::milliseconds(100));
+    std::chrono::steady_clock::time_point time6(std::chrono::milliseconds(115));
+    std::chrono::steady_clock::time_point time7(std::chrono::milliseconds(200));
+    std::chrono::steady_clock::time_point time8(std::chrono::milliseconds(300));
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time5)).WillOnce(Return(time6)).WillOnce(Return(time7)).WillOnce(Return(time8)).WillOnce(Return(time8));
+    EXPECT_CALL(clock, sleep(std::chrono::milliseconds(85)));
+    p.run();
+}
+
+TEST(EngineTest, KeyState)
+{
+    Engine engine;
+    ASSERT_FALSE(engine.keyPressed("a"));
+    ASSERT_FALSE(engine.keyPressed("b"));
+    ASSERT_FALSE(engine.keyPressed("up arrow"));
+    ASSERT_FALSE(engine.keyPressed("any"));
+
+    engine.setKeyState("A", true);
+    ASSERT_TRUE(engine.keyPressed("a"));
+    ASSERT_FALSE(engine.keyPressed("b"));
+    ASSERT_FALSE(engine.keyPressed("up arrow"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("up arrow", true);
+    ASSERT_TRUE(engine.keyPressed("a"));
+    ASSERT_FALSE(engine.keyPressed("b"));
+    ASSERT_TRUE(engine.keyPressed("up arrow"));
+    ASSERT_FALSE(engine.keyPressed("U"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("up arrow", false);
+    engine.setKeyState("UP arrow", true);
+    ASSERT_TRUE(engine.keyPressed("a"));
+    ASSERT_FALSE(engine.keyPressed("b"));
+    ASSERT_FALSE(engine.keyPressed("up arrow"));
+    ASSERT_TRUE(engine.keyPressed("u"));
+    ASSERT_TRUE(engine.keyPressed("U"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("up arrow", true);
+    engine.setKeyState("b", true);
+    ASSERT_TRUE(engine.keyPressed("a"));
+    ASSERT_TRUE(engine.keyPressed("b"));
+    ASSERT_TRUE(engine.keyPressed("up arrow"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("a", false);
+    ASSERT_FALSE(engine.keyPressed("a"));
+    ASSERT_TRUE(engine.keyPressed("b"));
+    ASSERT_TRUE(engine.keyPressed("up arrow"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("up arrow", false);
+    ASSERT_FALSE(engine.keyPressed("a"));
+    ASSERT_TRUE(engine.keyPressed("b"));
+    ASSERT_FALSE(engine.keyPressed("up arrow"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("b", false);
+    ASSERT_FALSE(engine.keyPressed("a"));
+    ASSERT_FALSE(engine.keyPressed("b"));
+    ASSERT_FALSE(engine.keyPressed("up arrow"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("u", false);
+    ASSERT_FALSE(engine.keyPressed("any"));
+
+    engine.setKeyState("32", true);
+    ASSERT_TRUE(engine.keyPressed("32"));
+    ASSERT_TRUE(engine.keyPressed("space"));
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setAnyKeyPressed(true);
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setAnyKeyPressed(false);
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setKeyState("space", false);
+    engine.setKeyState("a", false);
+    engine.setKeyState("b", false);
+    ASSERT_FALSE(engine.keyPressed("any"));
+
+    engine.setAnyKeyPressed(true);
+    ASSERT_TRUE(engine.keyPressed("any"));
+
+    engine.setAnyKeyPressed(false);
+    ASSERT_FALSE(engine.keyPressed("any"));
+}
+
+TEST(EngineTest, WhenKeyPressed)
+{
+    Project p("when_key_pressed.sb3");
+    ASSERT_TRUE(p.load());
+
+    p.run();
+
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    // no pressed key
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 0);
+
+    // space
+    engine->setKeyState("space", true);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 1);
+    engine->setKeyState("space", false);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 0);
+
+    // right arrow
+    engine->setKeyState("right arrow", true);
+    p.run();
+
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 1);
+    engine->setKeyState("right arrow", false);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 2);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 0);
+
+    // any key
+    engine->setAnyKeyPressed(true);
+    p.run();
+
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 3);
+    engine->setAnyKeyPressed(false);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 3);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 0);
+
+    // a
+    engine->setKeyState("a", true);
+    p.run();
+
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 1);
+    engine->setKeyState("a", false);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 4);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 0);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 0);
+
+    // x
+    engine->setKeyState("x", true);
+    p.run();
+
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 1);
+    engine->setKeyState("x", false);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 5);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 0);
+
+    // 4
+    engine->setKeyState("4", true);
+    p.run();
+
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 1);
+    engine->setKeyState("4", false);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 6);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 1);
+
+    // multiple
+    engine->setKeyState("space", true);
+    engine->setKeyState("x", true);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 2);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 2);
+    engine->setKeyState("space", false);
+    engine->setKeyState("x", false);
+    p.run();
+
+    ASSERT_VAR(stage, "space_pressed");
+    ASSERT_EQ(GET_VAR(stage, "space_pressed")->value().toInt(), 2);
+    ASSERT_VAR(stage, "right_arrow_pressed");
+    ASSERT_EQ(GET_VAR(stage, "right_arrow_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "any_key_pressed");
+    ASSERT_EQ(GET_VAR(stage, "any_key_pressed")->value().toInt(), 8);
+    ASSERT_VAR(stage, "a_pressed");
+    ASSERT_EQ(GET_VAR(stage, "a_pressed")->value().toInt(), 1);
+    ASSERT_VAR(stage, "x_pressed");
+    ASSERT_EQ(GET_VAR(stage, "x_pressed")->value().toInt(), 2);
+    ASSERT_VAR(stage, "4_pressed");
+    ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 1);
+}
+
 TEST(EngineTest, MouseX)
 {
     Engine engine;
@@ -79,6 +382,18 @@ TEST(EngineTest, MouseY)
     ASSERT_EQ(engine.mouseY(), 179.9258);
 }
 
+TEST(EngineTest, MousePressed)
+{
+    Engine engine;
+    ASSERT_FALSE(engine.mousePressed());
+
+    engine.setMousePressed(true);
+    ASSERT_TRUE(engine.mousePressed());
+
+    engine.setMousePressed(false);
+    ASSERT_FALSE(engine.mousePressed());
+}
+
 TEST(EngineTest, StageWidth)
 {
     Engine engine;
@@ -97,6 +412,18 @@ TEST(EngineTest, StageHeight)
     ASSERT_EQ(engine.stageHeight(), 515);
 }
 
+TEST(EngineTest, SpriteFencingEnabled)
+{
+    Engine engine;
+    ASSERT_TRUE(engine.spriteFencingEnabled());
+
+    engine.setSpriteFencingEnabled(false);
+    ASSERT_FALSE(engine.spriteFencingEnabled());
+
+    engine.setSpriteFencingEnabled(true);
+    ASSERT_TRUE(engine.spriteFencingEnabled());
+}
+
 TEST(EngineTest, BreakFrame)
 {
     Engine engine;
@@ -347,7 +674,7 @@ TEST(EngineTest, Targets)
     t2->setName("Sprite2");
     t2->addBlock(block1);
     t2->addBlock(block2);
-    auto t3 = std::make_shared<Target>();
+    auto t3 = std::make_shared<Stage>();
     t3->setName("Stage");
     engine.setTargets({ t1, t2, t3 });
 
@@ -361,7 +688,18 @@ TEST(EngineTest, Targets)
     ASSERT_EQ(engine.findTarget("invalid"), -1);
     ASSERT_EQ(engine.findTarget("Sprite1"), 0);
     ASSERT_EQ(engine.findTarget("Sprite2"), 1);
+    ASSERT_EQ(engine.findTarget("Stage"), -1);
+    ASSERT_EQ(engine.findTarget("_stage_"), 2);
+
+    auto t4 = std::make_shared<Target>();
+    t4->setName("Stage");
+    engine.setTargets({ t1, t2, t4 });
     ASSERT_EQ(engine.findTarget("Stage"), 2);
+    ASSERT_EQ(engine.findTarget("_stage_"), -1);
+
+    engine.setTargets({ t1, t2, t3, t4 });
+    ASSERT_EQ(engine.findTarget("Stage"), 3);
+    ASSERT_EQ(engine.findTarget("_stage_"), 2);
 
     ASSERT_EQ(t1->engine(), &engine);
     ASSERT_EQ(t2->engine(), &engine);
@@ -398,52 +736,6 @@ TEST(EngineTest, Stage)
     ASSERT_EQ(engine.stage(), nullptr);
 }
 
-TEST(EngineTest, VariableOwner)
-{
-    Engine engine;
-    auto t1 = std::make_shared<Target>();
-    auto t2 = std::make_shared<Target>();
-    auto t3 = std::make_shared<Target>();
-
-    auto var1 = std::make_shared<Variable>("", "", Value());
-    auto var2 = std::make_shared<Variable>("", "", Value());
-    auto var3 = std::make_shared<Variable>("", "", Value());
-    auto var4 = std::make_shared<Variable>("", "", Value());
-    t1->addVariable(var1);
-    t1->addVariable(var4);
-    t3->addVariable(var3);
-
-    engine.setTargets({ t1, t2, t3 });
-
-    ASSERT_EQ(engine.variableOwner(var1.get()), t1.get());
-    ASSERT_EQ(engine.variableOwner(var2.get()), nullptr);
-    ASSERT_EQ(engine.variableOwner(var3.get()), t3.get());
-    ASSERT_EQ(engine.variableOwner(var4.get()), t1.get());
-}
-
-TEST(EngineTest, ListOwner)
-{
-    Engine engine;
-    auto t1 = std::make_shared<Target>();
-    auto t2 = std::make_shared<Target>();
-    auto t3 = std::make_shared<Target>();
-
-    auto list1 = std::make_shared<List>("", "");
-    auto list2 = std::make_shared<List>("", "");
-    auto list3 = std::make_shared<List>("", "");
-    auto list4 = std::make_shared<List>("", "");
-    t1->addList(list1);
-    t1->addList(list4);
-    t3->addList(list3);
-
-    engine.setTargets({ t1, t2, t3 });
-
-    ASSERT_EQ(engine.listOwner(list1.get()), t1.get());
-    ASSERT_EQ(engine.listOwner(list2.get()), nullptr);
-    ASSERT_EQ(engine.listOwner(list3.get()), t3.get());
-    ASSERT_EQ(engine.listOwner(list4.get()), t1.get());
-}
-
 TEST(EngineTest, Clones)
 {
     Project p("clones.sb3");
@@ -452,7 +744,7 @@ TEST(EngineTest, Clones)
 
     auto engine = p.engine();
 
-    Target *stage = engine->targetAt(engine->findTarget("Stage"));
+    Stage *stage = engine->stage();
     ASSERT_TRUE(stage);
 
     ASSERT_VAR(stage, "clone1");
@@ -489,6 +781,58 @@ TEST(EngineTest, Clones)
     }
 }
 
+TEST(EngineTest, CloneLimit)
+{
+    Project p("clone_limit.sb3");
+    ASSERT_TRUE(p.load());
+    auto engine = p.engine();
+    ASSERT_EQ(engine->cloneLimit(), 300);
+
+    // TODO: Set "infinite" FPS and remove this (#254)
+    engine->setFps(100000);
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    p.run();
+    ASSERT_VAR(stage, "count");
+    ASSERT_EQ(GET_VAR(stage, "count")->value().toInt(), 300);
+    ASSERT_VAR(stage, "delete_passed");
+    ASSERT_TRUE(GET_VAR(stage, "delete_passed")->value().toBool());
+
+    engine->setCloneLimit(475);
+    ASSERT_EQ(engine->cloneLimit(), 475);
+    p.run();
+    ASSERT_VAR(stage, "count");
+    ASSERT_EQ(GET_VAR(stage, "count")->value().toInt(), 475);
+    ASSERT_VAR(stage, "delete_passed");
+    ASSERT_TRUE(GET_VAR(stage, "delete_passed")->value().toBool());
+
+    engine->setCloneLimit(0);
+    ASSERT_EQ(engine->cloneLimit(), 0);
+    p.run();
+    ASSERT_VAR(stage, "count");
+    ASSERT_EQ(GET_VAR(stage, "count")->value().toInt(), 0);
+    ASSERT_VAR(stage, "delete_passed");
+    ASSERT_TRUE(GET_VAR(stage, "delete_passed")->value().toBool());
+
+    engine->setCloneLimit(-1);
+    ASSERT_EQ(engine->cloneLimit(), -1);
+    p.run();
+    ASSERT_VAR(stage, "count");
+    ASSERT_GT(GET_VAR(stage, "count")->value().toInt(), 500);
+    ASSERT_VAR(stage, "delete_passed");
+    ASSERT_TRUE(GET_VAR(stage, "delete_passed")->value().toBool());
+
+    engine->setCloneLimit(-5);
+    ASSERT_EQ(engine->cloneLimit(), -1);
+    p.run();
+    ASSERT_VAR(stage, "count");
+    ASSERT_GT(GET_VAR(stage, "count")->value().toInt(), 500);
+    ASSERT_VAR(stage, "delete_passed");
+    ASSERT_TRUE(GET_VAR(stage, "delete_passed")->value().toBool());
+}
+
 // TODO: Uncomment this after fixing #256 and #257
 /*TEST(EngineTest, BackdropBroadcasts)
 {
@@ -499,7 +843,32 @@ TEST(EngineTest, Clones)
 
     auto engine = p.engine();
 
-    Target *stage = engine->targetAt(engine->findTarget("Stage"));
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    ASSERT_VAR(stage, "test1");
+    ASSERT_EQ(GET_VAR(stage, "test1")->value().toInt(), 4);
+    ASSERT_VAR(stage, "test2");
+    ASSERT_EQ(GET_VAR(stage, "test2")->value().toInt(), 14);
+    ASSERT_VAR(stage, "test3");
+    ASSERT_EQ(GET_VAR(stage, "test3")->value().toInt(), 10);
+    ASSERT_VAR(stage, "test4");
+    ASSERT_EQ(GET_VAR(stage, "test4")->value().toInt(), 10);
+    ASSERT_VAR(stage, "test5");
+    ASSERT_EQ(GET_VAR(stage, "test5")->value().toString(), "2 2 0 0");
+}*/
+
+// TODO: Uncomment this after fixing #256 and #257
+/*TEST(EngineTest, BroadcastsProject)
+{
+    // TODO: Set "infinite" FPS (#254)
+    Project p("broadcasts.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
+
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
     ASSERT_TRUE(stage);
 
     ASSERT_VAR(stage, "test1");
@@ -514,6 +883,47 @@ TEST(EngineTest, Clones)
     ASSERT_EQ(GET_VAR(stage, "test5")->value().toString(), "2 2 0 0");
 }*/
 
+// TODO: Uncomment this after fixing #305
+/*TEST(EngineTest, StopAll)
+{
+    Project p("stop_all.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
+
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    ASSERT_VAR(stage, "i");
+    ASSERT_EQ(GET_VAR(stage, "i")->value().toInt(), 11);
+}*/
+
+// TODO: Uncomment this after fixing #305
+/*TEST(EngineTest, StopOtherScriptsInSprite)
+{
+    Project p("stop_other_scripts_in_sprite.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
+
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    ASSERT_VAR(stage, "i");
+    ASSERT_EQ(GET_VAR(stage, "i")->value().toInt(), 10);
+
+    ASSERT_VAR(stage, "j");
+    ASSERT_EQ(GET_VAR(stage, "j")->value().toInt(), 109);
+
+    ASSERT_VAR(stage, "k");
+    ASSERT_EQ(GET_VAR(stage, "k")->value().toInt(), 10);
+
+    ASSERT_VAR(stage, "l");
+    ASSERT_EQ(GET_VAR(stage, "l")->value().toInt(), 110);
+}*/
+
 TEST(EngineTest, NoCrashAfterStop)
 {
     // Regtest for #186
@@ -521,3 +931,11 @@ TEST(EngineTest, NoCrashAfterStop)
     ASSERT_TRUE(p.load());
     p.run();
 }
+
+TEST(EngineTest, NoCrashOnBroadcastSelfCall)
+{
+    // Regtest for #256
+    Project p("regtest_projects/256_broadcast_self_call_crash.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
+}
diff --git a/test/image1.jpg b/test/image1.jpg
new file mode 100644
index 00000000..cb51bd48
Binary files /dev/null and b/test/image1.jpg differ
diff --git a/test/image1.png b/test/image1.png
new file mode 100644
index 00000000..9f5477d0
Binary files /dev/null and b/test/image1.png differ
diff --git a/test/image2.jpg b/test/image2.jpg
new file mode 100644
index 00000000..d4050d20
Binary files /dev/null and b/test/image2.jpg differ
diff --git a/test/image2.png b/test/image2.png
new file mode 100644
index 00000000..6be36ef8
Binary files /dev/null and b/test/image2.png differ
diff --git a/test/imageformats/CMakeLists.txt b/test/imageformats/CMakeLists.txt
new file mode 100644
index 00000000..b92341ea
--- /dev/null
+++ b/test/imageformats/CMakeLists.txt
@@ -0,0 +1,41 @@
+# stub
+add_executable(
+  imageformatstub_test
+  imageformatstub_test.cpp
+)
+
+target_link_libraries(
+  imageformatstub_test
+  GTest::gtest_main
+  scratchcpp
+)
+
+gtest_discover_tests(imageformatstub_test)
+
+# jpeg
+add_executable(
+  jpeg_test
+  jpeg_test.cpp
+)
+
+target_link_libraries(
+  jpeg_test
+  GTest::gtest_main
+  scratchcpp
+)
+
+gtest_discover_tests(jpeg_test)
+
+# png
+add_executable(
+  png_test
+  png_test.cpp
+)
+
+target_link_libraries(
+  png_test
+  GTest::gtest_main
+  scratchcpp
+)
+
+gtest_discover_tests(png_test)
diff --git a/test/imageformats/imageformatstub_test.cpp b/test/imageformats/imageformatstub_test.cpp
new file mode 100644
index 00000000..58ef0f43
--- /dev/null
+++ b/test/imageformats/imageformatstub_test.cpp
@@ -0,0 +1,32 @@
+#include <imageformats/stub/imageformatstub.h>
+
+#include "../common.h"
+
+using namespace libscratchcpp;
+
+TEST(ImageFormatStubTest, Width)
+{
+    ImageFormatStub img;
+    ASSERT_EQ(img.width(), 0);
+}
+
+TEST(ImageFormatStubTest, Height)
+{
+    ImageFormatStub img;
+    ASSERT_EQ(img.height(), 0);
+}
+
+TEST(ImageFormatStubTest, ColorAt)
+{
+    ImageFormatStub img;
+    ASSERT_EQ(img.colorAt(0, 0, 1), 0);
+    ASSERT_EQ(img.colorAt(0, 1, 1), 0);
+    ASSERT_EQ(img.colorAt(0, 2, 1), 0);
+    ASSERT_EQ(img.colorAt(1, 0, 1), 0);
+    ASSERT_EQ(img.colorAt(1, 1, 1), 0);
+    ASSERT_EQ(img.colorAt(1, 2, 1), 0);
+    ASSERT_EQ(img.colorAt(2, 0, 1), 0);
+    ASSERT_EQ(img.colorAt(2, 1, 1), 0);
+    ASSERT_EQ(img.colorAt(2, 2, 1), 0);
+    ASSERT_EQ(img.colorAt(2, 2, 2.5), 0);
+}
diff --git a/test/imageformats/jpeg_test.cpp b/test/imageformats/jpeg_test.cpp
new file mode 100644
index 00000000..67ca00d1
--- /dev/null
+++ b/test/imageformats/jpeg_test.cpp
@@ -0,0 +1,105 @@
+#include <scratchcpp/scratchconfiguration.h>
+#include <scratchcpp/iimageformat.h>
+
+#include "../common.h"
+
+using namespace libscratchcpp;
+
+class JpegTest : public testing::Test
+{
+    public:
+        void SetUp() override
+        {
+            img1 = ScratchConfiguration::createImageFormat("jpg");
+            img2 = ScratchConfiguration::createImageFormat("jpg");
+
+            struct stat stat_buf;
+
+            FILE *f1 = fopen("image1.jpg", "r");
+            ASSERT_TRUE(f1);
+            ASSERT_EQ(fstat(fileno(f1), &stat_buf), 0);
+            file1 = malloc(stat_buf.st_size);
+            ASSERT_TRUE(file1);
+            ASSERT_EQ(fread(file1, 1, stat_buf.st_size, f1), stat_buf.st_size);
+            img1->setData(stat_buf.st_size, file1);
+
+            FILE *f2 = fopen("image2.jpg", "r");
+            ASSERT_TRUE(f2);
+            ASSERT_EQ(fstat(fileno(f2), &stat_buf), 0);
+            file2 = malloc(stat_buf.st_size);
+            ASSERT_TRUE(file2);
+            ASSERT_EQ(fread(file2, 1, stat_buf.st_size, f2), stat_buf.st_size);
+            img2->setData(stat_buf.st_size, file2);
+        }
+
+        void TearDown() override
+        {
+            if (file1)
+                free(file1);
+
+            if (file2)
+                free(file2);
+        }
+
+        std::shared_ptr<IImageFormat> img1;
+        std::shared_ptr<IImageFormat> img2;
+        void *file1 = nullptr;
+        void *file2 = nullptr;
+};
+
+TEST_F(JpegTest, Width)
+{
+    ASSERT_EQ(img1->width(), 6);
+    ASSERT_EQ(img2->width(), 4);
+}
+
+TEST_F(JpegTest, Height)
+{
+    ASSERT_EQ(img1->height(), 3);
+    ASSERT_EQ(img2->height(), 6);
+}
+
+TEST_F(JpegTest, ColorAt)
+{
+    // image1 - without scaling
+    ASSERT_EQ(img1->colorAt(0, 0, 1), rgb(140, 204, 255));
+    ASSERT_EQ(img1->colorAt(3, 0, 1), rgb(238, 116, 101));
+    ASSERT_EQ(img1->colorAt(5, 0, 1), rgb(101, 197, 37));
+
+    ASSERT_EQ(img1->colorAt(0, 1, 1), rgb(151, 183, 230));
+    ASSERT_EQ(img1->colorAt(3, 1, 1), rgb(232, 143, 109));
+    ASSERT_EQ(img1->colorAt(5, 1, 1), rgb(198, 255, 165));
+
+    ASSERT_EQ(img1->colorAt(0, 2, 1), rgb(21, 0, 4));
+    ASSERT_EQ(img1->colorAt(3, 2, 1), rgb(137, 114, 44));
+    ASSERT_EQ(img1->colorAt(5, 2, 1), rgb(16, 136, 47));
+
+    // image1 - with scaling
+    ASSERT_EQ(img1->colorAt(0, 0, 2.5), rgb(140, 204, 255));
+    ASSERT_EQ(img1->colorAt(8, 0, 2.5), rgb(238, 116, 101));
+    ASSERT_EQ(img1->colorAt(13, 0, 2.5), rgb(101, 197, 37));
+
+    ASSERT_EQ(img1->colorAt(0, 3, 2.5), rgb(151, 183, 230));
+    ASSERT_EQ(img1->colorAt(8, 3, 2.5), rgb(232, 143, 109));
+    ASSERT_EQ(img1->colorAt(13, 3, 2.5), rgb(198, 255, 165));
+
+    ASSERT_EQ(img1->colorAt(0, 5, 2.5), rgb(21, 0, 4));
+    ASSERT_EQ(img1->colorAt(8, 5, 2.5), rgb(137, 114, 44));
+    ASSERT_EQ(img1->colorAt(13, 5, 2.5), rgb(16, 136, 47));
+
+    // image2 - without scaling
+    ASSERT_EQ(img2->colorAt(1, 0, 1), rgb(0, 0, 57));
+    ASSERT_EQ(img2->colorAt(3, 1, 1), rgb(226, 130, 194));
+    ASSERT_EQ(img2->colorAt(2, 2, 1), rgb(0, 13, 0));
+    ASSERT_EQ(img2->colorAt(0, 3, 1), rgb(15, 0, 46));
+    ASSERT_EQ(img2->colorAt(2, 4, 1), rgb(0, 0, 0));
+    ASSERT_EQ(img2->colorAt(1, 5, 1), rgb(0, 0, 0));
+
+    // image2 - with scaling
+    ASSERT_EQ(img2->colorAt(3, 0, 2.5), rgb(0, 0, 57));
+    ASSERT_EQ(img2->colorAt(8, 3, 2.5), rgb(226, 130, 194));
+    ASSERT_EQ(img2->colorAt(5, 5, 2.5), rgb(0, 13, 0));
+    ASSERT_EQ(img2->colorAt(0, 8, 2.5), rgb(15, 0, 46));
+    ASSERT_EQ(img2->colorAt(5, 10, 2.5), rgb(0, 0, 0));
+    ASSERT_EQ(img2->colorAt(3, 13, 2.5), rgb(0, 0, 0));
+}
diff --git a/test/imageformats/png_test.cpp b/test/imageformats/png_test.cpp
new file mode 100644
index 00000000..e73fb38b
--- /dev/null
+++ b/test/imageformats/png_test.cpp
@@ -0,0 +1,107 @@
+#include <scratchcpp/scratchconfiguration.h>
+#include <scratchcpp/iimageformat.h>
+
+#include "../common.h"
+
+using namespace libscratchcpp;
+
+class PngTest : public testing::Test
+{
+    public:
+        void SetUp() override
+        {
+            img1 = ScratchConfiguration::createImageFormat("png");
+            img2 = ScratchConfiguration::createImageFormat("png");
+
+            struct stat stat_buf;
+
+            FILE *f1 = fopen("image1.png", "r");
+            ASSERT_TRUE(f1);
+            ASSERT_EQ(fstat(fileno(f1), &stat_buf), 0);
+            file1 = malloc(stat_buf.st_size);
+            ASSERT_TRUE(file1);
+            ASSERT_EQ(fread(file1, 1, stat_buf.st_size, f1), stat_buf.st_size);
+            img1->setData(stat_buf.st_size, file1);
+
+            FILE *f2 = fopen("image2.png", "r");
+            ASSERT_TRUE(f2);
+            ASSERT_EQ(fstat(fileno(f2), &stat_buf), 0);
+            file2 = malloc(stat_buf.st_size);
+            ASSERT_TRUE(file2);
+            ASSERT_EQ(fread(file2, 1, stat_buf.st_size, f2), stat_buf.st_size);
+            img2->setData(stat_buf.st_size, file2);
+        }
+
+        void TearDown() override
+        {
+            if (file1)
+                free(file1);
+
+            if (file2)
+                free(file2);
+        }
+
+        std::shared_ptr<IImageFormat> img1;
+        std::shared_ptr<IImageFormat> img2;
+        void *file1 = nullptr;
+        void *file2 = nullptr;
+};
+
+TEST_F(PngTest, Width)
+{
+    ASSERT_EQ(img1->width(), 6);
+    ASSERT_EQ(img2->width(), 4);
+}
+
+TEST_F(PngTest, Height)
+{
+    ASSERT_EQ(img1->height(), 3);
+    ASSERT_EQ(img2->height(), 6);
+}
+
+TEST_F(PngTest, ColorAt)
+{
+    // image1 - without scaling
+    ASSERT_EQ(img1->colorAt(0, 0, 1), rgb(192, 192, 192));
+    ASSERT_EQ(img1->colorAt(3, 0, 1), rgb(255, 128, 0));
+    ASSERT_EQ(img1->colorAt(5, 0, 1), rgb(0, 255, 0));
+
+    ASSERT_EQ(img1->colorAt(0, 1, 1), rgb(0, 255, 255));
+    ASSERT_EQ(img1->colorAt(3, 1, 1), rgb(255, 128, 128));
+    ASSERT_EQ(img1->colorAt(5, 1, 1), rgb(255, 255, 255));
+
+    ASSERT_EQ(img1->colorAt(0, 2, 1), rgb(0, 0, 0));
+    ASSERT_EQ(img1->colorAt(3, 2, 1), rgb(128, 128, 0));
+    ASSERT_EQ(img1->colorAt(5, 2, 1), rgb(0, 128, 128));
+
+    // image1 - with scaling
+    ASSERT_EQ(img1->colorAt(0, 0, 2.5), rgb(192, 192, 192));
+    ASSERT_EQ(img1->colorAt(8, 0, 2.5), rgb(255, 128, 0));
+    ASSERT_EQ(img1->colorAt(13, 0, 2.5), rgb(0, 255, 0));
+
+    ASSERT_EQ(img1->colorAt(0, 3, 2.5), rgb(0, 255, 255));
+    ASSERT_EQ(img1->colorAt(8, 3, 2.5), rgb(255, 128, 128));
+    ASSERT_EQ(img1->colorAt(13, 3, 2.5), rgb(255, 255, 255));
+
+    ASSERT_EQ(img1->colorAt(0, 5, 2.5), rgb(0, 0, 0));
+    ASSERT_EQ(img1->colorAt(8, 5, 2.5), rgb(128, 128, 0));
+    ASSERT_EQ(img1->colorAt(13, 5, 2.5), rgb(0, 128, 128));
+
+    // image2 - without scaling
+    ASSERT_EQ(img2->colorAt(1, 0, 1), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(3, 1, 1), rgb(255, 128, 128));
+    ASSERT_EQ(img2->colorAt(3, 2, 1), rgba(149, 255, 149, 148));
+    ASSERT_EQ(img2->colorAt(2, 2, 1), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(0, 3, 1), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(2, 4, 1), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(1, 5, 1), rgba(0, 0, 0, 0));
+
+    // image2 - with scaling
+    ASSERT_EQ(img2->colorAt(3, 0, 2.5), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(8, 3, 2.5), rgb(255, 128, 128));
+    ASSERT_EQ(img2->colorAt(8, 5, 2.5), rgba(149, 255, 149, 148));
+    ASSERT_EQ(img2->colorAt(5, 5, 2.5), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(0, 8, 2.5), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(5, 10, 2.5), rgba(0, 0, 0, 0));
+    ASSERT_EQ(img2->colorAt(3, 13, 2.5), rgba(0, 0, 0, 0));
+}
diff --git a/test/load_project/load_project_test.cpp b/test/load_project/load_project_test.cpp
index a698883d..6dcdafdd 100644
--- a/test/load_project/load_project_test.cpp
+++ b/test/load_project/load_project_test.cpp
@@ -40,7 +40,7 @@ TEST(LoadProjectTest, EmptyProject)
         ASSERT_EQ(stage->blocks().size(), 0);
         ASSERT_EQ(stage->costumes().size(), 1);
         // TODO: Add comments
-        ASSERT_EQ(stage->currentCostume(), 0);
+        ASSERT_EQ(stage->currentCostume(), 1);
         ASSERT_EQ(stage->sounds().size(), 0);
         ASSERT_EQ(stage->layerOrder(), 0);
         ASSERT_EQ(stage->volume(), 100);
@@ -54,6 +54,14 @@ TEST(LoadProjectTest, EmptyProject)
         ASSERT_FALSE(backdrop->id().empty());
         ASSERT_EQ(backdrop->fileName(), backdrop->id() + ".svg");
         ASSERT_EQ(backdrop->dataFormat(), "svg");
+        ASSERT_TRUE(backdrop->data());
+        ASSERT_EQ(
+            memcmp(
+                backdrop->data(),
+                "<svg version=\"1.1\" width=\"2\" height=\"2\" viewBox=\"-1 -1 2 2\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n  <!-- Exported by Scratch - "
+                "http://scratch.mit.edu/ -->\n</svg>",
+                backdrop->dataSize()),
+            0);
 
         i++;
     }
@@ -73,8 +81,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(engine->broadcasts().size(), 1);
 
         // Stage
-        ASSERT_NE(engine->findTarget("Stage"), -1);
-        Stage *stage = dynamic_cast<Stage *>(engine->targetAt(engine->findTarget("Stage")));
+        Stage *stage = engine->stage();
         ASSERT_EQ(stage->variables().size(), 2);
         ASSERT_EQ(stage->lists().size(), 1);
 
@@ -97,6 +104,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(sprite1->lists().size(), 1);
         ASSERT_EQ(sprite1->blocks().size(), 18);
         ASSERT_EQ(sprite1->costumes().size(), 2);
+        ASSERT_EQ(sprite1->currentCostume(), 2);
         ASSERT_EQ(sprite1->sounds().size(), 1);
         ASSERT_TRUE(sprite1->visible());
         ASSERT_EQ(sprite1->x(), 0);
diff --git a/test/mocks/clockmock.h b/test/mocks/clockmock.h
index c8f8b1e3..1d52c63f 100644
--- a/test/mocks/clockmock.h
+++ b/test/mocks/clockmock.h
@@ -10,4 +10,6 @@ class ClockMock : public IClock
     public:
         MOCK_METHOD(std::chrono::steady_clock::time_point, currentSteadyTime, (), (const, override));
         MOCK_METHOD(std::chrono::system_clock::time_point, currentSystemTime, (), (const, override));
+
+        MOCK_METHOD(void, sleep, (const std::chrono::milliseconds &time), (const, override));
 };
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index 04abeae4..e3655c82 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -23,6 +23,7 @@ class EngineMock : public IEngine
         MOCK_METHOD(void, stopScript, (VirtualMachine *), (override));
         MOCK_METHOD(void, stopTarget, (Target *, VirtualMachine *), (override));
         MOCK_METHOD(void, initClone, (Sprite *), (override));
+        MOCK_METHOD(void, deinitClone, (Sprite *), (override));
         MOCK_METHOD(void, run, (), (override));
 
         MOCK_METHOD(bool, isRunning, (), (const, override));
@@ -30,18 +31,31 @@ class EngineMock : public IEngine
         MOCK_METHOD(double, fps, (), (const, override));
         MOCK_METHOD(void, setFps, (double fps), (override));
 
+        MOCK_METHOD(bool, keyPressed, (const std::string &), (const, override));
+        MOCK_METHOD(void, setKeyState, (const std::string &, bool), (override));
+        MOCK_METHOD(void, setAnyKeyPressed, (bool), (override));
+
         MOCK_METHOD(double, mouseX, (), (const, override));
         MOCK_METHOD(void, setMouseX, (double x), (override));
 
         MOCK_METHOD(double, mouseY, (), (const, override));
         MOCK_METHOD(void, setMouseY, (double y), (override));
 
+        MOCK_METHOD(bool, mousePressed, (), (const, override));
+        MOCK_METHOD(void, setMousePressed, (bool), (override));
+
         MOCK_METHOD(unsigned int, stageWidth, (), (const, override));
         MOCK_METHOD(void, setStageWidth, (unsigned int), (override));
 
         MOCK_METHOD(unsigned int, stageHeight, (), (const, override));
         MOCK_METHOD(void, setStageHeight, (unsigned int), (override));
 
+        MOCK_METHOD(int, cloneLimit, (), (const, override));
+        MOCK_METHOD(void, setCloneLimit, (int), (override));
+
+        MOCK_METHOD(bool, spriteFencingEnabled, (), (const, override));
+        MOCK_METHOD(void, setSpriteFencingEnabled, (bool), (override));
+
         MOCK_METHOD(bool, broadcastRunning, (unsigned int, VirtualMachine *), (override));
         MOCK_METHOD(bool, broadcastByPtrRunning, (Broadcast *, VirtualMachine *), (override));
 
@@ -69,8 +83,8 @@ class EngineMock : public IEngine
         MOCK_METHOD(int, findBroadcastById, (const std::string &), (const, override));
 
         MOCK_METHOD(void, addBroadcastScript, (std::shared_ptr<Block>, Broadcast *), (override));
-
         MOCK_METHOD(void, addCloneInitScript, (std::shared_ptr<Block>), (override));
+        MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr<Block>, std::string), (override));
 
         MOCK_METHOD(const std::vector<std::shared_ptr<Target>> &, targets, (), (const, override));
         MOCK_METHOD(void, setTargets, (const std::vector<std::shared_ptr<Target>> &), (override));
@@ -79,9 +93,6 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(Stage *, stage, (), (const, override));
 
-        MOCK_METHOD(Target *, variableOwner, (Variable *), (const, override));
-        MOCK_METHOD(Target *, listOwner, (List *), (const, override));
-
         MOCK_METHOD(std::vector<std::string> &, extensions, (), (const, override));
         MOCK_METHOD(void, setExtensions, (const std::vector<std::string> &), (override));
 
diff --git a/test/mocks/graphicseffectmock.h b/test/mocks/graphicseffectmock.h
new file mode 100644
index 00000000..fc2b171b
--- /dev/null
+++ b/test/mocks/graphicseffectmock.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <scratchcpp/igraphicseffect.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+class GraphicsEffectMock : public IGraphicsEffect
+{
+    public:
+        MOCK_METHOD(std::string, name, (), (const, override));
+        MOCK_METHOD(void, apply, (Rgb **, unsigned int, unsigned int, double), (const, override));
+};
diff --git a/test/mocks/imageformatfactorymock.h b/test/mocks/imageformatfactorymock.h
new file mode 100644
index 00000000..3352d437
--- /dev/null
+++ b/test/mocks/imageformatfactorymock.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <scratchcpp/iimageformatfactory.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+class ImageFormatFactoryMock : public IImageFormatFactory
+{
+    public:
+        MOCK_METHOD(std::shared_ptr<IImageFormat>, createInstance, (), (const, override));
+};
diff --git a/test/mocks/imageformatmock.h b/test/mocks/imageformatmock.h
new file mode 100644
index 00000000..69c30fee
--- /dev/null
+++ b/test/mocks/imageformatmock.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <scratchcpp/iimageformat.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+class ImageFormatMock : public IImageFormat
+{
+    public:
+        MOCK_METHOD(void, setData, (unsigned int, void *), (override));
+
+        MOCK_METHOD(unsigned int, width, (), (const, override));
+        MOCK_METHOD(unsigned int, height, (), (const, override));
+
+        MOCK_METHOD(Rgb, colorAt, (unsigned int, unsigned int, double), (const, override));
+};
diff --git a/test/rect/CMakeLists.txt b/test/rect/CMakeLists.txt
new file mode 100644
index 00000000..74d62043
--- /dev/null
+++ b/test/rect/CMakeLists.txt
@@ -0,0 +1,12 @@
+add_executable(
+  rect_test
+  rect_test.cpp
+)
+
+target_link_libraries(
+  rect_test
+  GTest::gtest_main
+  scratchcpp
+)
+
+gtest_discover_tests(rect_test)
diff --git a/test/rect/rect_test.cpp b/test/rect/rect_test.cpp
new file mode 100644
index 00000000..cdfc7515
--- /dev/null
+++ b/test/rect/rect_test.cpp
@@ -0,0 +1,79 @@
+#include <scratchcpp/rect.h>
+#include <cmath>
+
+#include "../common.h"
+
+using namespace libscratchcpp;
+
+TEST(RectTest, Constructors)
+{
+    {
+        Rect rect;
+        ASSERT_EQ(rect.left(), 0);
+        ASSERT_EQ(rect.top(), 0);
+        ASSERT_EQ(rect.right(), 0);
+        ASSERT_EQ(rect.bottom(), 0);
+        ASSERT_EQ(rect.width(), 0);
+        ASSERT_EQ(rect.height(), 0);
+    }
+
+    {
+        Rect rect(8.4, 150.78, 145.89, -179.99);
+        ASSERT_EQ(rect.left(), 8.4);
+        ASSERT_EQ(rect.top(), 150.78);
+        ASSERT_EQ(rect.right(), 145.89);
+        ASSERT_EQ(rect.bottom(), -179.99);
+        ASSERT_EQ(std::round(rect.width() * 100) / 100, 137.49);
+        ASSERT_EQ(rect.height(), 330.77);
+    }
+}
+
+TEST(RectTest, Left)
+{
+    Rect rect;
+
+    rect.setLeft(-78.05);
+    ASSERT_EQ(rect.left(), -78.05);
+}
+
+TEST(RectTest, Top)
+{
+    Rect rect;
+
+    rect.setTop(-22.89);
+    ASSERT_EQ(rect.top(), -22.89);
+}
+
+TEST(RectTest, Right)
+{
+    Rect rect;
+
+    rect.setRight(100.512);
+    ASSERT_EQ(rect.right(), 100.512);
+}
+
+TEST(RectTest, Bottom)
+{
+    Rect rect;
+
+    rect.setBottom(-58.162);
+    ASSERT_EQ(rect.bottom(), -58.162);
+}
+
+TEST(RectTest, Width)
+{
+    Rect rect;
+
+    rect.setLeft(-78.05);
+    rect.setRight(100.512);
+    ASSERT_EQ(rect.width(), 178.562);
+}
+
+TEST(RectTest, Height)
+{
+    Rect rect;
+
+    rect.setTop(-22.89);
+    rect.setBottom(-58.162);
+    ASSERT_EQ(rect.height(), 35.272);
+}
diff --git a/test/regtest_projects/256_broadcast_self_call_crash.sb3 b/test/regtest_projects/256_broadcast_self_call_crash.sb3
new file mode 100644
index 00000000..7ce68d4d
Binary files /dev/null and b/test/regtest_projects/256_broadcast_self_call_crash.sb3 differ
diff --git a/test/scratch_classes/CMakeLists.txt b/test/scratch_classes/CMakeLists.txt
index dfdd4c21..c98db0e3 100644
--- a/test/scratch_classes/CMakeLists.txt
+++ b/test/scratch_classes/CMakeLists.txt
@@ -187,3 +187,17 @@ target_link_libraries(
 )
 
 gtest_discover_tests(variable_test)
+
+# keyevent_test
+add_executable(
+  keyevent_test
+  keyevent_test.cpp
+)
+
+target_link_libraries(
+  keyevent_test
+  GTest::gtest_main
+  scratchcpp
+)
+
+gtest_discover_tests(keyevent_test)
diff --git a/test/scratch_classes/block_test.cpp b/test/scratch_classes/block_test.cpp
index 14430946..cec36baf 100644
--- a/test/scratch_classes/block_test.cpp
+++ b/test/scratch_classes/block_test.cpp
@@ -114,11 +114,15 @@ TEST_F(BlockTest, Inputs)
     ASSERT_EQ(block.findInput("VALUE2"), 1);
     ASSERT_EQ(block.findInput("VALUE3"), 2);
 
-    block.updateInputMap();
     ASSERT_EQ(block.findInputById(5), nullptr);
     ASSERT_EQ(block.findInputById(11), i1.get());
     ASSERT_EQ(block.findInputById(12), i2.get());
     ASSERT_EQ(block.findInputById(15), i3.get());
+
+    auto i4 = std::make_shared<Input>("VALUE4", Input::Type::Shadow);
+    block.addInput(i4);
+    i4->setInputId(20);
+    ASSERT_EQ(block.findInputById(20), i4.get());
 }
 
 TEST_F(BlockTest, Fields)
@@ -150,11 +154,15 @@ TEST_F(BlockTest, Fields)
     ASSERT_EQ(block.findField("VARIABLE2"), 1);
     ASSERT_EQ(block.findField("VARIABLE3"), 2);
 
-    block.updateFieldMap();
     ASSERT_EQ(block.findFieldById(5), nullptr);
     ASSERT_EQ(block.findFieldById(11), f1.get());
     ASSERT_EQ(block.findFieldById(12), f2.get());
     ASSERT_EQ(block.findFieldById(15), f3.get());
+
+    auto f4 = std::make_shared<Field>("VARIABLE4", Value());
+    block.addField(f4);
+    f4->setFieldId(20);
+    ASSERT_EQ(block.findFieldById(20), f4.get());
 }
 
 TEST_F(BlockTest, Shadow)
diff --git a/test/scratch_classes/keyevent_test.cpp b/test/scratch_classes/keyevent_test.cpp
new file mode 100644
index 00000000..280f6482
--- /dev/null
+++ b/test/scratch_classes/keyevent_test.cpp
@@ -0,0 +1,215 @@
+#include <scratchcpp/keyevent.h>
+
+#include "../common.h"
+
+using namespace libscratchcpp;
+
+TEST(KeyEventTest, Constructors)
+{
+    // Default constructor
+    {
+        KeyEvent event;
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_TRUE(event.name().empty());
+    }
+
+    // Type
+    {
+        KeyEvent event(KeyEvent::Type::Any);
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_TRUE(event.name().empty());
+    }
+
+    {
+        KeyEvent event(KeyEvent::Type::Space);
+        ASSERT_EQ(event.type(), KeyEvent::Type::Space);
+        ASSERT_EQ(event.name(), "space");
+    }
+
+    {
+        KeyEvent event(KeyEvent::Type::Left);
+        ASSERT_EQ(event.type(), KeyEvent::Type::Left);
+        ASSERT_EQ(event.name(), "left arrow");
+    }
+
+    {
+        KeyEvent event(KeyEvent::Type::Up);
+        ASSERT_EQ(event.type(), KeyEvent::Type::Up);
+        ASSERT_EQ(event.name(), "up arrow");
+    }
+
+    {
+        KeyEvent event(KeyEvent::Type::Right);
+        ASSERT_EQ(event.type(), KeyEvent::Type::Right);
+        ASSERT_EQ(event.name(), "right arrow");
+    }
+
+    {
+        KeyEvent event(KeyEvent::Type::Down);
+        ASSERT_EQ(event.type(), KeyEvent::Type::Down);
+        ASSERT_EQ(event.name(), "down arrow");
+    }
+
+    {
+        KeyEvent event(KeyEvent::Type::Enter);
+        ASSERT_EQ(event.type(), KeyEvent::Type::Enter);
+        ASSERT_EQ(event.name(), "enter");
+    }
+
+    {
+        KeyEvent event(static_cast<KeyEvent::Type>(-1));
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_TRUE(event.name().empty());
+    }
+
+    // Name
+    {
+        KeyEvent event("");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_TRUE(event.name().empty());
+    }
+
+    {
+        KeyEvent event("a");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "a");
+    }
+
+    {
+        KeyEvent event("D");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "d");
+    }
+
+    {
+        KeyEvent event("0");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "0");
+    }
+
+    {
+        KeyEvent event("space");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Space);
+        ASSERT_EQ(event.name(), "space");
+    }
+
+    {
+        KeyEvent event("left arrow");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Left);
+        ASSERT_EQ(event.name(), "left arrow");
+    }
+
+    {
+        KeyEvent event("up arrow");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Up);
+        ASSERT_EQ(event.name(), "up arrow");
+    }
+
+    {
+        KeyEvent event("right arrow");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Right);
+        ASSERT_EQ(event.name(), "right arrow");
+    }
+
+    {
+        KeyEvent event("down arrow");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Down);
+        ASSERT_EQ(event.name(), "down arrow");
+    }
+
+    {
+        KeyEvent event("enter");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Enter);
+        ASSERT_EQ(event.name(), "enter");
+    }
+
+    {
+        KeyEvent event("Enter");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "e");
+    }
+
+    {
+        KeyEvent event("test");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "t");
+    }
+
+    {
+        KeyEvent event(" ");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Space);
+        ASSERT_EQ(event.name(), "space");
+    }
+
+    {
+        KeyEvent event(" a");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Space);
+        ASSERT_EQ(event.name(), "space");
+    }
+
+    {
+        KeyEvent event("33");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "3");
+    }
+
+    {
+        KeyEvent event("47");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "4");
+    }
+
+    {
+        KeyEvent event("48");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "0");
+    }
+
+    {
+        KeyEvent event("64");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "@");
+    }
+
+    {
+        KeyEvent event("77");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "m");
+    }
+
+    {
+        KeyEvent event("90");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Any);
+        ASSERT_EQ(event.name(), "z");
+    }
+
+    {
+        KeyEvent event("32");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Space);
+        ASSERT_EQ(event.name(), "space");
+    }
+
+    {
+        KeyEvent event("37");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Left);
+        ASSERT_EQ(event.name(), "left arrow");
+    }
+
+    {
+        KeyEvent event("38");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Up);
+        ASSERT_EQ(event.name(), "up arrow");
+    }
+
+    {
+        KeyEvent event("39");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Right);
+        ASSERT_EQ(event.name(), "right arrow");
+    }
+
+    {
+        KeyEvent event("40");
+        ASSERT_EQ(event.type(), KeyEvent::Type::Down);
+        ASSERT_EQ(event.name(), "down arrow");
+    }
+}
diff --git a/test/scratch_classes/list_test.cpp b/test/scratch_classes/list_test.cpp
index 9e1c9f1f..93a22670 100644
--- a/test/scratch_classes/list_test.cpp
+++ b/test/scratch_classes/list_test.cpp
@@ -1,4 +1,5 @@
 #include <scratchcpp/list.h>
+#include <scratchcpp/target.h>
 
 #include "../common.h"
 
@@ -18,6 +19,16 @@ TEST(ListTest, Name)
     ASSERT_EQ(list.name(), "test list");
 }
 
+TEST(ListTest, Target)
+{
+    List list("", "");
+    ASSERT_EQ(list.target(), nullptr);
+
+    Target target;
+    list.setTarget(&target);
+    ASSERT_EQ(list.target(), &target);
+}
+
 TEST(ListTest, IndexOf)
 {
     List list("", "test list");
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index e5b60f70..ffadbf32 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -1,8 +1,15 @@
 #include <scratchcpp/sprite.h>
+#include <scratchcpp/stage.h>
 #include <scratchcpp/variable.h>
 #include <scratchcpp/list.h>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/scratchconfiguration.h>
+#include <scratchcpp/rect.h>
+#include <scratchcpp/project.h>
 #include <enginemock.h>
+#include <imageformatfactorymock.h>
+#include <imageformatmock.h>
+#include <graphicseffectmock.h>
 
 #include "../common.h"
 
@@ -10,6 +17,7 @@ using namespace libscratchcpp;
 
 using ::testing::_;
 using ::testing::SaveArg;
+using ::testing::Return;
 
 TEST(SpriteTest, IsStage)
 {
@@ -27,34 +35,36 @@ TEST(SpriteTest, Visible)
 
 TEST(SpriteTest, Clone)
 {
-    Sprite sprite;
-    sprite.setName("Sprite1");
+    std::unique_ptr<Sprite> sprite = std::make_unique<Sprite>();
+    sprite->setName("Sprite1");
     auto var1 = std::make_shared<Variable>("a", "var1", "hello");
     auto var2 = std::make_shared<Variable>("b", "var2", "world");
-    sprite.addVariable(var1);
-    sprite.addVariable(var2);
+    sprite->addVariable(var1);
+    sprite->addVariable(var2);
     auto c1 = std::make_shared<Costume>("costume1", "", "svg");
-    sprite.addCostume(c1);
+    sprite->addCostume(c1);
 
     auto list1 = std::make_shared<List>("c", "list1");
     list1->push_back("item1");
     list1->push_back("item2");
     auto list2 = std::make_shared<List>("d", "list2");
     list2->push_back("test");
-    sprite.addList(list1);
-    sprite.addList(list2);
-
-    sprite.setCurrentCostume(2);
-    sprite.setLayerOrder(5);
-    sprite.setVolume(50);
-
-    sprite.setVisible(false);
-    sprite.setX(100.25);
-    sprite.setY(-45.43);
-    sprite.setSize(54.121);
-    sprite.setDirection(179.4);
-    sprite.setDraggable(true);
-    sprite.setRotationStyle(Sprite::RotationStyle::DoNotRotate);
+    sprite->addList(list1);
+    sprite->addList(list2);
+
+    sprite->addCostume(std::make_shared<Costume>("", "", ""));
+    sprite->addCostume(std::make_shared<Costume>("", "", ""));
+    sprite->setCurrentCostume(2);
+    sprite->setLayerOrder(5);
+    sprite->setVolume(50);
+
+    sprite->setVisible(false);
+    sprite->setX(100.25);
+    sprite->setY(-45.43);
+    sprite->setSize(54.121);
+    sprite->setDirection(179.4);
+    sprite->setDraggable(true);
+    sprite->setRotationStyle(Sprite::RotationStyle::DoNotRotate);
 
     auto checkCloneData = [](Sprite *clone) {
         ASSERT_TRUE(clone);
@@ -69,6 +79,7 @@ TEST(SpriteTest, Clone)
         ASSERT_EQ(clone->variableAt(1)->id(), "b");
         ASSERT_EQ(clone->variableAt(1)->name(), "var2");
         ASSERT_EQ(clone->variableAt(1)->value().toString(), "world");
+        ASSERT_EQ(clone->variableAt(1)->target(), clone);
 
         ASSERT_EQ(clone->lists().size(), 2);
         ASSERT_NE(clone->lists(), root->lists());
@@ -78,6 +89,7 @@ TEST(SpriteTest, Clone)
         ASSERT_EQ(clone->listAt(1)->id(), "d");
         ASSERT_EQ(clone->listAt(1)->name(), "list2");
         ASSERT_EQ(*clone->listAt(1), std::deque<Value>({ "test" }));
+        ASSERT_EQ(clone->listAt(1)->target(), clone);
 
         ASSERT_EQ(clone->currentCostume(), 2);
         ASSERT_EQ(clone->layerOrder(), 5);
@@ -93,42 +105,42 @@ TEST(SpriteTest, Clone)
         ASSERT_EQ(clone->rotationStyle(), Sprite::RotationStyle::DoNotRotate);
     };
 
-    ASSERT_FALSE(sprite.isClone());
-    ASSERT_EQ(sprite.cloneRoot(), nullptr);
-    ASSERT_EQ(sprite.cloneParent(), nullptr);
+    ASSERT_FALSE(sprite->isClone());
+    ASSERT_EQ(sprite->cloneRoot(), nullptr);
+    ASSERT_EQ(sprite->cloneParent(), nullptr);
 
-    ASSERT_EQ(sprite.clone(), nullptr);
-    ASSERT_FALSE(sprite.isClone());
-    ASSERT_EQ(sprite.cloneRoot(), nullptr);
-    ASSERT_EQ(sprite.cloneParent(), nullptr);
+    ASSERT_EQ(sprite->clone(), nullptr);
+    ASSERT_FALSE(sprite->isClone());
+    ASSERT_EQ(sprite->cloneRoot(), nullptr);
+    ASSERT_EQ(sprite->cloneParent(), nullptr);
 
     EngineMock engine;
-    sprite.setEngine(&engine);
+    sprite->setEngine(&engine);
 
     Sprite *clone1;
     EXPECT_CALL(engine, initClone(_)).WillOnce(SaveArg<0>(&clone1));
-    ASSERT_EQ(sprite.clone().get(), clone1);
-    ASSERT_FALSE(sprite.isClone());
-    ASSERT_EQ(sprite.cloneRoot(), nullptr);
-    ASSERT_EQ(sprite.cloneParent(), nullptr);
+    ASSERT_EQ(sprite->clone().get(), clone1);
+    ASSERT_FALSE(sprite->isClone());
+    ASSERT_EQ(sprite->cloneRoot(), nullptr);
+    ASSERT_EQ(sprite->cloneParent(), nullptr);
 
     ASSERT_TRUE(clone1->isClone());
-    ASSERT_EQ(clone1->cloneRoot(), &sprite);
-    ASSERT_EQ(clone1->cloneParent(), &sprite);
+    ASSERT_EQ(clone1->cloneRoot(), sprite.get());
+    ASSERT_EQ(clone1->cloneParent(), sprite.get());
 
     checkCloneData(clone1);
 
     // Modify root sprite data to make sure parent is used
-    sprite.setLayerOrder(3);
+    sprite->setLayerOrder(3);
 
     Sprite *clone2;
     EXPECT_CALL(engine, initClone(_)).WillOnce(SaveArg<0>(&clone2));
     ASSERT_EQ(clone1->clone().get(), clone2);
     ASSERT_TRUE(clone1->isClone());
-    ASSERT_EQ(clone1->cloneRoot(), &sprite);
-    ASSERT_EQ(clone1->cloneParent(), &sprite);
+    ASSERT_EQ(clone1->cloneRoot(), sprite.get());
+    ASSERT_EQ(clone1->cloneParent(), sprite.get());
     ASSERT_TRUE(clone2->isClone());
-    ASSERT_EQ(clone2->cloneRoot(), &sprite);
+    ASSERT_EQ(clone2->cloneRoot(), sprite.get());
     ASSERT_EQ(clone2->cloneParent(), clone1);
 
     checkCloneData(clone2);
@@ -139,10 +151,10 @@ TEST(SpriteTest, Clone)
 
     Sprite *clone4;
     EXPECT_CALL(engine, initClone(_)).WillOnce(SaveArg<0>(&clone4));
-    ASSERT_EQ(sprite.clone().get(), clone4);
+    ASSERT_EQ(sprite->clone().get(), clone4);
 
     // children
-    const auto &children1 = sprite.children();
+    const auto &children1 = sprite->children();
     ASSERT_EQ(children1.size(), 2);
     ASSERT_EQ(children1[0].get(), clone1);
     ASSERT_EQ(children1[1].get(), clone4);
@@ -159,7 +171,7 @@ TEST(SpriteTest, Clone)
     ASSERT_TRUE(clone4->children().empty());
 
     // allChildren
-    auto allChildren = sprite.allChildren();
+    auto allChildren = sprite->allChildren();
     ASSERT_EQ(allChildren.size(), 4);
     ASSERT_EQ(allChildren[0].get(), clone1);
     ASSERT_EQ(allChildren[1].get(), clone2);
@@ -177,42 +189,214 @@ TEST(SpriteTest, Clone)
 
     ASSERT_TRUE(clone4->allChildren().empty());
 
-    ASSERT_EQ(clone2->costumes(), sprite.costumes());
+    ASSERT_EQ(clone2->costumes(), sprite->costumes());
     auto c2 = std::make_shared<Costume>("costume2", "", "png");
     clone2->addCostume(c2);
-    ASSERT_EQ(clone2->costumes(), sprite.costumes());
+    ASSERT_EQ(clone2->costumes(), sprite->costumes());
+
+    EXPECT_CALL(engine, deinitClone(clone2));
+    clone2->~Sprite();
+
+    EXPECT_CALL(engine, deinitClone(clone3));
+    clone3->~Sprite();
+
+    EXPECT_CALL(engine, deinitClone(clone1)).Times(2);
+    clone1->~Sprite();
+
+    EXPECT_CALL(engine, deinitClone(clone4)).Times(2);
+    clone4->~Sprite();
+
+    sprite.reset();
 }
 
-TEST(SpriteTest, X)
+TEST(SpriteTest, XY)
 {
     Sprite sprite;
     ASSERT_EQ(sprite.x(), 0);
+    ASSERT_EQ(sprite.y(), 0);
 
     sprite.setX(-53.25);
     ASSERT_EQ(sprite.x(), -53.25);
 
-    sprite.setX(239.999);
-    ASSERT_EQ(sprite.x(), 239.999);
+    sprite.setX(259.999);
+    ASSERT_EQ(sprite.x(), 259.999);
+
+    sprite.setY(-53.25);
+    ASSERT_EQ(sprite.y(), -53.25);
+
+    sprite.setY(189.999);
+    ASSERT_EQ(sprite.y(), 189.999);
+
+    auto imageFormatFactory = std::make_shared<ImageFormatFactoryMock>();
+    auto imageFormat = std::make_shared<ImageFormatMock>();
+
+    ScratchConfiguration::registerImageFormat("test", imageFormatFactory);
+    EXPECT_CALL(*imageFormatFactory, createInstance()).WillOnce(Return(imageFormat));
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(0));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(0));
+    auto costume = std::make_shared<Costume>("costume1", "a", "test");
+
+    sprite.addCostume(costume);
+    sprite.setCurrentCostume(1);
+
+    static char data[5] = "abcd";
+    EXPECT_CALL(*imageFormat, setData(5, data));
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+
+    EXPECT_CALL(*imageFormat, colorAt(0, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(1, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(2, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+    EXPECT_CALL(*imageFormat, colorAt(3, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+
+    EXPECT_CALL(*imageFormat, colorAt(0, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(1, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+    EXPECT_CALL(*imageFormat, colorAt(2, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(3, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+
+    EXPECT_CALL(*imageFormat, colorAt(0, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+    EXPECT_CALL(*imageFormat, colorAt(1, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(2, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(3, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    costume->setData(5, data);
+
+    EngineMock engine;
+    sprite.setEngine(&engine);
+    sprite.setDirection(34.45);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setX(230);
+    ASSERT_EQ(sprite.x(), 230);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setX(-230);
+    ASSERT_EQ(sprite.x(), -230);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setX(250);
+    ASSERT_EQ(sprite.x(), 241);
+
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    sprite.setX(250);
+    ASSERT_EQ(sprite.x(), 250);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setX(-250);
+    ASSERT_EQ(sprite.x(), -241);
+
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    sprite.setX(-250);
+    ASSERT_EQ(sprite.x(), -250);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setY(170);
+    ASSERT_EQ(sprite.y(), 170);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setY(-170);
+    ASSERT_EQ(sprite.y(), -170);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setY(190);
+    ASSERT_EQ(sprite.y(), 181);
+
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    sprite.setY(190);
+    ASSERT_EQ(sprite.y(), 190);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    sprite.setY(-190);
+    ASSERT_EQ(sprite.y(), -180);
+
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    sprite.setY(-190);
+    ASSERT_EQ(sprite.y(), -190);
+
+    ScratchConfiguration::removeImageFormat("test");
 }
 
-TEST(SpriteTest, Y)
+TEST(SpriteTest, Fencing)
 {
-    Sprite sprite;
-    ASSERT_EQ(sprite.y(), 0);
+    Project p("sprite_fencing.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
 
-    sprite.setY(-53.25);
-    ASSERT_EQ(sprite.y(), -53.25);
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    ASSERT_VAR(stage, "maxX");
+    ASSERT_EQ(GET_VAR(stage, "maxX")->value().toDouble(), 240);
 
-    sprite.setY(179.999);
-    ASSERT_EQ(sprite.y(), 179.999);
+    ASSERT_VAR(stage, "maxY");
+    ASSERT_EQ(GET_VAR(stage, "maxY")->value().toDouble(), 205);
+
+    ASSERT_VAR(stage, "minX");
+    ASSERT_EQ(GET_VAR(stage, "minX")->value().toDouble(), -362);
+
+    ASSERT_VAR(stage, "minY");
+    ASSERT_EQ(GET_VAR(stage, "minY")->value().toDouble(), -213);
 }
 
 TEST(SpriteTest, Size)
 {
+    auto c1 = std::make_shared<Costume>("", "", "");
+    auto c2 = std::make_shared<Costume>("", "", "");
+
     Sprite sprite;
     ASSERT_EQ(sprite.size(), 100);
+    sprite.addCostume(c1);
+    sprite.addCostume(c2);
+    sprite.setCurrentCostume(1);
+
+    ASSERT_EQ(c1->scale(), 1);
+
     sprite.setSize(63.724);
     ASSERT_EQ(sprite.size(), 63.724);
+    ASSERT_EQ(std::round(c1->scale() * 100000) / 100000, 0.63724);
+
+    sprite.setCurrentCostume(2);
+    ASSERT_EQ(std::round(c2->scale() * 100000) / 100000, 0.63724);
+
+    sprite.setSize(186.84);
+    ASSERT_EQ(sprite.size(), 186.84);
+    ASSERT_EQ(std::round(c2->scale() * 10000) / 10000, 1.8684);
+
+    sprite.setCurrentCostume(1);
+    ASSERT_EQ(std::round(c1->scale() * 10000) / 10000, 1.8684);
 }
 
 TEST(SpriteTest, Direction)
@@ -261,51 +445,213 @@ TEST(SpriteTest, Draggable)
 
 TEST(SpriteTest, RotationStyle)
 {
+    auto c1 = std::make_shared<Costume>("", "", "");
+    auto c2 = std::make_shared<Costume>("", "", "");
+
     Sprite sprite;
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::AllAround); // default
     ASSERT_EQ(sprite.rotationStyleStr(), "all around");
+    sprite.addCostume(c1);
+    sprite.addCostume(c2);
+    sprite.setCurrentCostume(1);
+
+    ASSERT_EQ(c1->mirrorHorizontally(), false);
 
     sprite.setRotationStyle(Sprite::RotationStyle::DoNotRotate);
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::DoNotRotate);
     ASSERT_EQ(sprite.rotationStyleStr(), "don't rotate");
+    ASSERT_EQ(c1->mirrorHorizontally(), false);
 
     sprite.setRotationStyle(Sprite::RotationStyle::LeftRight);
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::LeftRight);
     ASSERT_EQ(sprite.rotationStyleStr(), "left-right");
+    ASSERT_EQ(c1->mirrorHorizontally(), true);
 
     sprite.setRotationStyle(Sprite::RotationStyle::AllAround);
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::AllAround);
     ASSERT_EQ(sprite.rotationStyleStr(), "all around");
+    ASSERT_EQ(c1->mirrorHorizontally(), false);
 
     sprite.setRotationStyle("invalid");
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::AllAround); // shouldn't change
     ASSERT_EQ(sprite.rotationStyleStr(), "all around");
+    ASSERT_EQ(c1->mirrorHorizontally(), false);
+
+    sprite.setCurrentCostume(2);
+    sprite.setRotationStyle(Sprite::RotationStyle::LeftRight);
+    ASSERT_EQ(c2->mirrorHorizontally(), true);
+
+    sprite.setCurrentCostume(1);
+    sprite.setRotationStyle(Sprite::RotationStyle::AllAround);
+    sprite.setCurrentCostume(2);
+    ASSERT_EQ(c2->mirrorHorizontally(), false);
 
     sprite.setRotationStyle("don't rotate");
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::DoNotRotate);
     ASSERT_EQ(sprite.rotationStyleStr(), "don't rotate");
+    ASSERT_EQ(c2->mirrorHorizontally(), false);
 
     sprite.setRotationStyle("left-right");
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::LeftRight);
     ASSERT_EQ(sprite.rotationStyleStr(), "left-right");
+    ASSERT_EQ(c2->mirrorHorizontally(), true);
 
     sprite.setRotationStyle("all around");
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::AllAround);
     ASSERT_EQ(sprite.rotationStyleStr(), "all around");
+    ASSERT_EQ(c2->mirrorHorizontally(), false);
 
     sprite.setRotationStyle(std::string("invalid"));
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::AllAround); // shouldn't change
     ASSERT_EQ(sprite.rotationStyleStr(), "all around");
+    ASSERT_EQ(c2->mirrorHorizontally(), false);
 
     sprite.setRotationStyle(std::string("don't rotate"));
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::DoNotRotate);
     ASSERT_EQ(sprite.rotationStyleStr(), "don't rotate");
+    ASSERT_EQ(c2->mirrorHorizontally(), false);
 
     sprite.setRotationStyle(std::string("left-right"));
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::LeftRight);
     ASSERT_EQ(sprite.rotationStyleStr(), "left-right");
+    ASSERT_EQ(c2->mirrorHorizontally(), true);
 
     sprite.setRotationStyle(std::string("all around"));
     ASSERT_EQ(sprite.rotationStyle(), Sprite::RotationStyle::AllAround);
     ASSERT_EQ(sprite.rotationStyleStr(), "all around");
+    ASSERT_EQ(c2->mirrorHorizontally(), false);
+}
+
+TEST(SpriteTest, BoundingRect)
+{
+    auto imageFormatFactory = std::make_shared<ImageFormatFactoryMock>();
+    auto imageFormat = std::make_shared<ImageFormatMock>();
+
+    ScratchConfiguration::registerImageFormat("test", imageFormatFactory);
+    EXPECT_CALL(*imageFormatFactory, createInstance()).WillOnce(Return(imageFormat));
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(0));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(0));
+    auto costume = std::make_shared<Costume>("costume1", "a", "test");
+
+    Sprite sprite;
+    sprite.addCostume(costume);
+    sprite.setCurrentCostume(1);
+
+    static char data[5] = "abcd";
+    EXPECT_CALL(*imageFormat, setData(5, data));
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+
+    EXPECT_CALL(*imageFormat, colorAt(0, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(1, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(2, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+    EXPECT_CALL(*imageFormat, colorAt(3, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+
+    EXPECT_CALL(*imageFormat, colorAt(0, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(1, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+    EXPECT_CALL(*imageFormat, colorAt(2, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(3, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+
+    EXPECT_CALL(*imageFormat, colorAt(0, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
+    EXPECT_CALL(*imageFormat, colorAt(1, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(2, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    EXPECT_CALL(*imageFormat, colorAt(3, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
+    costume->setData(5, data);
+
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    Rect rect = sprite.boundingRect();
+    ASSERT_EQ(rect.left(), -2);
+    ASSERT_EQ(rect.top(), 0.5);
+    ASSERT_EQ(rect.right(), 1);
+    ASSERT_EQ(rect.bottom(), -1.5);
+
+    sprite.setDirection(45);
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    rect = sprite.boundingRect();
+    ASSERT_EQ(std::round(rect.left() * 10000) / 10000, -1.7678);
+    ASSERT_EQ(std::round(rect.top() * 10000) / 10000, 0.3536);
+    ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 1.0607);
+    ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -1.0607);
+
+    sprite.setDirection(-160);
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    rect = sprite.boundingRect();
+    ASSERT_EQ(std::round(rect.left() * 10000) / 10000, -1.4095);
+    ASSERT_EQ(std::round(rect.top() * 10000) / 10000, 1.7084);
+    ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 1.1539);
+    ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -0.7687);
+
+    sprite.setX(86.48);
+    sprite.setY(-147.16);
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    rect = sprite.boundingRect();
+    ASSERT_EQ(std::round(rect.left() * 10000) / 10000, 85.0705);
+    ASSERT_EQ(std::round(rect.top() * 10000) / 10000, -145.4516);
+    ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 87.6339);
+    ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -147.9287);
+
+    costume->setRotationCenterX(-4);
+    costume->setRotationCenterY(8);
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    rect = sprite.boundingRect();
+    ASSERT_EQ(std::round(rect.left() * 10000) / 10000, 76.1848);
+    ASSERT_EQ(std::round(rect.top() * 10000) / 10000, -146.4742);
+    ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 78.7483);
+    ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -148.9513);
+
+    sprite.setDirection(90);
+    sprite.setX(0);
+    sprite.setY(0);
+    EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
+    EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
+    rect = sprite.boundingRect();
+    ASSERT_EQ(rect.left(), 2);
+    ASSERT_EQ(rect.top(), -7.5);
+    ASSERT_EQ(rect.right(), 5);
+    ASSERT_EQ(rect.bottom(), -9.5);
+
+    ScratchConfiguration::removeImageFormat("test");
+}
+
+TEST(SpriteTest, GraphicsEffects)
+{
+    auto c1 = std::make_shared<Costume>("", "", "");
+    auto c2 = std::make_shared<Costume>("", "", "");
+
+    Sprite sprite;
+    sprite.addCostume(c1);
+    sprite.addCostume(c2);
+    sprite.setCurrentCostume(1);
+
+    GraphicsEffectMock effect1, effect2;
+    sprite.setGraphicsEffectValue(&effect1, 48.21);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 48.21);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), 0);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect1), 48.21);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect2), 0);
+
+    sprite.setCurrentCostume(2);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect1), 48.21);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect2), 0);
+
+    sprite.setGraphicsEffectValue(&effect2, -107.08);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 48.21);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), -107.08);
+    ASSERT_EQ(c2->graphicsEffectValue(&effect1), 48.21);
+    ASSERT_EQ(c2->graphicsEffectValue(&effect2), -107.08);
+
+    sprite.setCurrentCostume(1);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect1), 48.21);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect2), -107.08);
+
+    sprite.clearGraphicsEffects();
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 0);
+    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), 0);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect1), 0);
+    ASSERT_EQ(c1->graphicsEffectValue(&effect2), 0);
 }
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index 79790356..628e7770 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -38,6 +38,10 @@ TEST(TargetTest, Variables)
     ASSERT_EQ(target.addVariable(v3), 2);
     ASSERT_EQ(target.addVariable(v2), 1); // add existing variable
 
+    ASSERT_EQ(v1->target(), &target);
+    ASSERT_EQ(v2->target(), &target);
+    ASSERT_EQ(v3->target(), &target);
+
     ASSERT_EQ(target.variables(), std::vector<std::shared_ptr<Variable>>({ v1, v2, v3 }));
     ASSERT_EQ(target.variableAt(0), v1);
     ASSERT_EQ(target.variableAt(1), v2);
@@ -68,6 +72,10 @@ TEST(TargetTest, Lists)
     ASSERT_EQ(target.addList(l3), 2);
     ASSERT_EQ(target.addList(l2), 1); // add existing list
 
+    ASSERT_EQ(l1->target(), &target);
+    ASSERT_EQ(l2->target(), &target);
+    ASSERT_EQ(l3->target(), &target);
+
     ASSERT_EQ(target.lists(), std::vector<std::shared_ptr<List>>({ l1, l2, l3 }));
     ASSERT_EQ(target.listAt(0), l1);
     ASSERT_EQ(target.listAt(1), l2);
@@ -178,9 +186,31 @@ TEST(TargetTest, Blocks)
 TEST(TargetTest, CurrentCostume)
 {
     Target target;
+    ASSERT_EQ(target.currentCostume(), 0);
+
+    target.setCurrentCostume(1);
+    ASSERT_EQ(target.currentCostume(), 0);
+
+    target.setCurrentCostume(2);
+    ASSERT_EQ(target.currentCostume(), 0);
+
+    target.addCostume(std::make_shared<Costume>("", "", ""));
+    ASSERT_EQ(target.currentCostume(), 0);
+
+    target.setCurrentCostume(1);
     ASSERT_EQ(target.currentCostume(), 1);
-    target.setCurrentCostume(5);
-    ASSERT_EQ(target.currentCostume(), 5);
+
+    target.setCurrentCostume(2);
+    ASSERT_EQ(target.currentCostume(), 1);
+
+    target.addCostume(std::make_shared<Costume>("", "", ""));
+    ASSERT_EQ(target.currentCostume(), 1);
+
+    target.setCurrentCostume(2);
+    ASSERT_EQ(target.currentCostume(), 2);
+
+    target.setCurrentCostume(3);
+    ASSERT_EQ(target.currentCostume(), 2);
 }
 
 TEST(TargetTest, Costumes)
diff --git a/test/scratch_classes/variable_test.cpp b/test/scratch_classes/variable_test.cpp
index 4613b004..e669d8eb 100644
--- a/test/scratch_classes/variable_test.cpp
+++ b/test/scratch_classes/variable_test.cpp
@@ -1,4 +1,5 @@
 #include <scratchcpp/variable.h>
+#include <scratchcpp/target.h>
 
 #include "../common.h"
 
@@ -57,6 +58,16 @@ TEST(VariableTest, IsCloudVariable)
     ASSERT_TRUE(var.isCloudVariable());
 }
 
+TEST(VariableTest, Target)
+{
+    Variable var("", "");
+    ASSERT_EQ(var.target(), nullptr);
+
+    Target target;
+    var.setTarget(&target);
+    ASSERT_EQ(var.target(), &target);
+}
+
 TEST(VariableTest, Clone)
 {
     std::shared_ptr<Variable> clone;
diff --git a/test/scratchconfiguration/CMakeLists.txt b/test/scratchconfiguration/CMakeLists.txt
index add8ff2d..b5effd0e 100644
--- a/test/scratchconfiguration/CMakeLists.txt
+++ b/test/scratchconfiguration/CMakeLists.txt
@@ -14,7 +14,9 @@ add_executable(
 target_link_libraries(
   scratchconfiguration_test
   GTest::gtest_main
+  GTest::gmock_main
   scratchcpp
+  scratchcpp_mocks
 )
 
 gtest_discover_tests(scratchconfiguration_test)
diff --git a/test/scratchconfiguration/scratchconfiguration_test.cpp b/test/scratchconfiguration/scratchconfiguration_test.cpp
index 78a5732b..67068582 100644
--- a/test/scratchconfiguration/scratchconfiguration_test.cpp
+++ b/test/scratchconfiguration/scratchconfiguration_test.cpp
@@ -1,12 +1,17 @@
 #include <scratchcpp/scratchconfiguration.h>
+#include <imageformatfactorymock.h>
+#include <graphicseffectmock.h>
 
 #include "../common.h"
+#include "imageformats/stub/imageformatstub.h"
 #include "extension1.h"
 #include "extension2.h"
 #include "extension3.h"
 
 using namespace libscratchcpp;
 
+using ::testing::Return;
+
 class ScratchConfigurationTest : public testing::Test
 {
     public:
@@ -15,11 +20,15 @@ class ScratchConfigurationTest : public testing::Test
             m_extension1 = std::make_shared<Extension1>();
             m_extension2 = std::make_shared<Extension2>();
             m_extension3 = std::make_shared<Extension3>();
+            m_imageFormatMock1 = std::make_shared<ImageFormatFactoryMock>();
+            m_imageFormatMock2 = std::make_shared<ImageFormatFactoryMock>();
         }
 
         std::shared_ptr<IExtension> m_extension1;
         std::shared_ptr<IExtension> m_extension2;
         std::shared_ptr<IExtension> m_extension3;
+        std::shared_ptr<ImageFormatFactoryMock> m_imageFormatMock1;
+        std::shared_ptr<ImageFormatFactoryMock> m_imageFormatMock2;
 };
 
 TEST_F(ScratchConfigurationTest, Extensions)
@@ -35,3 +44,64 @@ TEST_F(ScratchConfigurationTest, Extensions)
     ASSERT_EQ(ScratchConfiguration::getExtension<Extension2>(), m_extension2.get());
     ASSERT_EQ(ScratchConfiguration::getExtension<Extension3>(), nullptr);
 }
+
+TEST_F(ScratchConfigurationTest, ImageFormats)
+{
+    ScratchConfiguration::registerImageFormat("format1", m_imageFormatMock1);
+    ScratchConfiguration::registerImageFormat("format2", m_imageFormatMock2);
+    auto format1 = std::make_shared<ImageFormatStub>();
+    auto format2 = std::make_shared<ImageFormatStub>();
+
+    EXPECT_CALL(*m_imageFormatMock1, createInstance()).WillOnce(Return(format1));
+    ASSERT_EQ(ScratchConfiguration::createImageFormat("format1"), format1);
+
+    EXPECT_CALL(*m_imageFormatMock2, createInstance()).WillOnce(Return(format2));
+    ASSERT_EQ(ScratchConfiguration::createImageFormat("format2"), format2);
+
+    auto format = ScratchConfiguration::createImageFormat("format3");
+    IImageFormat *unsupportedFormat = format.get();
+    ASSERT_TRUE(unsupportedFormat);
+    ASSERT_TRUE(dynamic_cast<ImageFormatStub *>(unsupportedFormat));
+
+    ScratchConfiguration::removeImageFormat("format1");
+    format = ScratchConfiguration::createImageFormat("format1");
+    unsupportedFormat = format.get();
+    ASSERT_TRUE(unsupportedFormat);
+    ASSERT_TRUE(dynamic_cast<ImageFormatStub *>(unsupportedFormat));
+
+    EXPECT_CALL(*m_imageFormatMock2, createInstance()).WillOnce(Return(format2));
+    ASSERT_EQ(ScratchConfiguration::createImageFormat("format2"), format2);
+
+    ScratchConfiguration::removeImageFormat("format2");
+    format = ScratchConfiguration::createImageFormat("format2");
+    unsupportedFormat = format.get();
+    ASSERT_TRUE(unsupportedFormat);
+    ASSERT_TRUE(dynamic_cast<ImageFormatStub *>(unsupportedFormat));
+
+    testing::Mock::AllowLeak(m_imageFormatMock1.get());
+    testing::Mock::AllowLeak(m_imageFormatMock2.get());
+}
+
+TEST_F(ScratchConfigurationTest, GraphicsEffects)
+{
+    auto effect1 = std::make_shared<GraphicsEffectMock>();
+    auto effect2 = std::make_shared<GraphicsEffectMock>();
+
+    EXPECT_CALL(*effect1, name()).WillOnce(Return("effect1"));
+    EXPECT_CALL(*effect2, name()).WillOnce(Return("effect2"));
+    ScratchConfiguration::registerGraphicsEffect(effect1);
+    ScratchConfiguration::registerGraphicsEffect(effect2);
+    auto format1 = std::make_shared<ImageFormatStub>();
+    auto format2 = std::make_shared<ImageFormatStub>();
+
+    ASSERT_EQ(ScratchConfiguration::getGraphicsEffect("effect1"), effect1.get());
+    ASSERT_EQ(ScratchConfiguration::getGraphicsEffect("effect2"), effect2.get());
+    ASSERT_EQ(ScratchConfiguration::getGraphicsEffect("effect3"), nullptr);
+
+    ScratchConfiguration::removeGraphicsEffect("effect2");
+    ASSERT_EQ(ScratchConfiguration::getGraphicsEffect("effect1"), effect1.get());
+    ASSERT_EQ(ScratchConfiguration::getGraphicsEffect("effect2"), nullptr);
+
+    ScratchConfiguration::removeGraphicsEffect("effect1");
+    ASSERT_EQ(ScratchConfiguration::getGraphicsEffect("effect1"), nullptr);
+}
diff --git a/test/script/script_test.cpp b/test/script/script_test.cpp
index caca86a3..9130028d 100644
--- a/test/script/script_test.cpp
+++ b/test/script/script_test.cpp
@@ -49,12 +49,12 @@ TEST_F(ScriptTest, Start)
     static std::vector<BlockFunc> functions = { &testFunction };
     static std::vector<Value> constValues = { "test" };
 
-    std::unique_ptr<Variable> var1 = std::make_unique<Variable>("a", "", Value());
-    std::unique_ptr<Variable> var2 = std::make_unique<Variable>("b", "", Value());
+    std::shared_ptr<Variable> var1 = std::make_unique<Variable>("a", "", Value());
+    std::shared_ptr<Variable> var2 = std::make_unique<Variable>("b", "", Value());
     static std::vector<Variable *> variables = { var1.get(), var2.get() };
 
-    std::unique_ptr<List> list1 = std::make_unique<List>("c", "");
-    std::unique_ptr<List> list2 = std::make_unique<List>("d", "");
+    std::shared_ptr<List> list1 = std::make_unique<List>("c", "");
+    std::shared_ptr<List> list2 = std::make_unique<List>("d", "");
     static std::vector<List *> lists = { list1.get(), list2.get() };
 
     Script script1(nullptr, nullptr);
@@ -95,6 +95,8 @@ TEST_F(ScriptTest, Start)
     ASSERT_EQ(vm->lists()[0], lists[0]);
 
     Target target;
+    target.addVariable(var1);
+    target.addList(list1);
     vm = script3.start(&target);
     ASSERT_TRUE(vm);
     ASSERT_EQ(vm->target(), &target);
@@ -107,8 +109,8 @@ TEST_F(ScriptTest, Start)
 
     Sprite root;
     root.setEngine(&m_engine);
-    root.addVariable(std::make_shared<Variable>("b", "", Value()));
-    root.addList(std::make_shared<List>("d", ""));
+    root.addVariable(var2);
+    root.addList(list2);
 
     EXPECT_CALL(m_engine, initClone).Times(1);
     auto clone = root.clone();
@@ -121,10 +123,6 @@ TEST_F(ScriptTest, Start)
     script4.setVariables(variables);
     script4.setLists(lists);
 
-    EXPECT_CALL(m_engine, variableOwner(var1.get())).WillOnce(Return(&target));
-    EXPECT_CALL(m_engine, variableOwner(var2.get())).WillOnce(Return(&root));
-    EXPECT_CALL(m_engine, listOwner(list1.get())).WillOnce(Return(&target));
-    EXPECT_CALL(m_engine, listOwner(list2.get())).WillOnce(Return(&root));
     vm = script4.start(clone.get());
 
     ASSERT_TRUE(vm);
diff --git a/test/sprite_fencing.sb3 b/test/sprite_fencing.sb3
new file mode 100644
index 00000000..b0bdda73
Binary files /dev/null and b/test/sprite_fencing.sb3 differ
diff --git a/test/stop_all.sb3 b/test/stop_all.sb3
new file mode 100644
index 00000000..8ccc6eec
Binary files /dev/null and b/test/stop_all.sb3 differ
diff --git a/test/stop_other_scripts_in_sprite.sb3 b/test/stop_other_scripts_in_sprite.sb3
new file mode 100644
index 00000000..96ab3369
Binary files /dev/null and b/test/stop_other_scripts_in_sprite.sb3 differ
diff --git a/test/target_interfaces/ispritehandler_test.cpp b/test/target_interfaces/ispritehandler_test.cpp
index 75b69fc7..92ff98bb 100644
--- a/test/target_interfaces/ispritehandler_test.cpp
+++ b/test/target_interfaces/ispritehandler_test.cpp
@@ -34,6 +34,10 @@ TEST_F(ISpriteHandlerTest, Clone)
     m_sprite.clone();
     ASSERT_TRUE(clone);
     ASSERT_EQ(cloneArg, clone);
+
+    // Engine is used during deletion, so let's just set it to nullptr (this is tested elsewhere)
+    m_sprite.setEngine(nullptr);
+    clone->setEngine(nullptr);
 }
 
 TEST_F(ISpriteHandlerTest, Visible)
@@ -47,14 +51,28 @@ TEST_F(ISpriteHandlerTest, Visible)
 
 TEST_F(ISpriteHandlerTest, X)
 {
-    EXPECT_CALL(m_handler, onXChanged(300.25)).Times(1);
-    m_sprite.setX(300.25);
+    EXPECT_CALL(m_handler, onXChanged(189.46)).Times(1);
+    EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(m_engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(m_engine, stageHeight()).WillOnce(Return(360));
+    m_sprite.setX(189.46);
+
+    EXPECT_CALL(m_handler, onXChanged(284.61)).Times(1);
+    EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(false));
+    m_sprite.setX(284.61);
 }
 
 TEST_F(ISpriteHandlerTest, Y)
 {
     EXPECT_CALL(m_handler, onYChanged(-153.7)).Times(1);
+    EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(true));
+    EXPECT_CALL(m_engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(m_engine, stageHeight()).WillOnce(Return(360));
     m_sprite.setY(-153.7);
+
+    EXPECT_CALL(m_handler, onYChanged(207.08)).Times(1);
+    EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(false));
+    m_sprite.setY(207.08);
 }
 
 TEST_F(ISpriteHandlerTest, Size)
diff --git a/test/virtual_machine/virtual_machine_test.cpp b/test/virtual_machine/virtual_machine_test.cpp
index 2f4010a4..17e9961b 100644
--- a/test/virtual_machine/virtual_machine_test.cpp
+++ b/test/virtual_machine/virtual_machine_test.cpp
@@ -133,6 +133,35 @@ TEST(VirtualMachineTest, MinimalScript)
     ASSERT_EQ(vm.registerCount(), 0);
 }
 
+TEST(VirtualMachineTest, RegCountLimit)
+{
+    static unsigned int bytecode[20482] = { OP_START };
+
+    for (int i = 0; i < 10240; i++) {
+        bytecode[2 * i + 1] = OP_CONST;
+        bytecode[2 * i + 2] = 0;
+    }
+
+    bytecode[20481] = OP_HALT;
+
+    static Value constValues[] = { 1 };
+
+    VirtualMachine vm;
+    vm.setBytecode(bytecode);
+    vm.setConstValues(constValues);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 10240);
+
+    for (int i = 1; i < 10240; i++)
+        bytecode[i] = OP_ADD;
+
+    bytecode[10240] = OP_HALT;
+    vm.setBytecode(bytecode);
+    vm.run();
+    ASSERT_EQ(vm.registerCount(), 1);
+    ASSERT_EQ(vm.getInput(0, 1)->toInt(), 10240);
+}
+
 TEST(VirtualMachineTest, OP_CONST)
 {
     static unsigned int bytecode[] = { OP_START, OP_CONST, 0, OP_HALT };
@@ -1327,6 +1356,21 @@ unsigned int testFunction5(VirtualMachine *vm)
     return 0;
 }
 
+unsigned int testFunction6(VirtualMachine *vm)
+{
+    vm->addReturnValue(0);
+    vm->stop(true, false, false);
+    return 1;
+}
+
+unsigned int testFunction7(VirtualMachine *vm)
+{
+    vm->addReturnValue(0);
+    vm->stop(true, false, false);
+    vm->reset();
+    return 1;
+}
+
 TEST(VirtualMachineTest, OP_EXEC)
 {
     static unsigned int bytecode[] = {
@@ -1518,18 +1562,41 @@ TEST(VirtualMachineTest, OP_WARP)
 
 TEST(VirtualMachineTest, Reset)
 {
-    static unsigned int bytecode[] = { OP_START, OP_NULL, OP_EXEC, 0, OP_HALT };
-    static BlockFunc functions[] = { &testFunction3 };
+    static unsigned int bytecode1[] = { OP_START, OP_NULL, OP_EXEC, 0, OP_HALT };
+    static unsigned int bytecode2[] = { OP_START, OP_NULL, OP_EXEC, 1, OP_HALT };
+    static BlockFunc functions[] = { &testFunction6, &testFunction7 };
 
     VirtualMachine vm;
-    vm.setBytecode(bytecode);
+    vm.setBytecode(bytecode1);
     vm.setFunctions(functions);
+
+    vm.run();
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 1);
+    vm.reset();
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_FALSE(vm.atEnd());
+    vm.run();
+    ASSERT_FALSE(vm.atEnd());
     vm.run();
+    ASSERT_TRUE(vm.atEnd());
+
+    vm.reset();
     ASSERT_FALSE(vm.atEnd());
+
+    vm.setBytecode(bytecode2);
+
+    vm.run();
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 1);
     vm.reset();
+    ASSERT_EQ(vm.registerCount(), 0);
     ASSERT_FALSE(vm.atEnd());
     vm.run();
     ASSERT_FALSE(vm.atEnd());
     vm.run();
     ASSERT_TRUE(vm.atEnd());
+
+    vm.reset();
+    ASSERT_FALSE(vm.atEnd());
 }
diff --git a/test/when_key_pressed.sb3 b/test/when_key_pressed.sb3
new file mode 100644
index 00000000..2516fcbb
Binary files /dev/null and b/test/when_key_pressed.sb3 differ
