diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8b1cdc32..4dcfec42 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,6 +9,12 @@ set(ZIP_SRC thirdparty/zip/src)
 
 option(LIBSCRATCHCPP_BUILD_UNIT_TESTS "Build unit tests" ON)
 option(LIBSCRATCHCPP_NETWORK_SUPPORT "Support for downloading projects" ON)
+option(LIBSCRATCHCPP_COMPUTED_GOTO "Support for computed goto" ON)
+
+if (NOT (CMAKE_CXX_COMPILER_ID STREQUAL "GNU"))
+	# Computed goto not supported on anything except GCC
+	set(LIBSCRATCHCPP_COMPUTED_GOTO OFF CACHE BOOL "" FORCE)
+endif()
 
 find_package(nlohmann_json 3.9.1 REQUIRED)
 find_package(utf8cpp REQUIRED)
@@ -18,6 +24,10 @@ add_subdirectory(src)
 include_directories(src) # TODO: Remove this line
 include_directories(include)
 
+if (LIBSCRATCHCPP_COMPUTED_GOTO)
+	target_compile_definitions(scratchcpp PRIVATE ENABLE_COMPUTED_GOTO)
+endif()
+
 target_sources(scratchcpp
   PUBLIC
     include/scratchcpp/global.h
diff --git a/Doxyfile b/Doxyfile
index 7fa13a47..9b266624 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -128,6 +128,7 @@ REPEAT_BRIEF           = YES
 # specifies, contains, represents, a, an and the.
 
 ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name struct" \
                          "The $name interface" \
                          "The $name widget" \
                          "The $name file" \
diff --git a/README.md b/README.md
index f478d015..f945d5a9 100644
--- a/README.md
+++ b/README.md
@@ -123,7 +123,7 @@ libscratchcpp::Project p("https://scratch.mit.edu/projects/XXXXXX");
 ## Roadmap
 
 - [x] Motion blocks
-- [ ] Looks blocks
+- [x] Looks blocks
 - [ ] Sound blocks
 - [ ] Event blocks
 - [x] Control blocks
@@ -132,12 +132,12 @@ libscratchcpp::Project p("https://scratch.mit.edu/projects/XXXXXX");
 - [x] Custom blocks
 - [x] Top level reporter blocks
 - [x] Broadcasts
-- [x] Variables (monitors are not implemented yet)
-- [x] Lists (monitors are not implemented yet)
+- [x] Variables
+- [x] Lists
 - [x] Clones
 - [x] Timer
 - [x] API for monitors
-- [ ] Project metadata
+- [x] Project metadata
 - [x] Turbo mode
 - [ ] Multithreading (experimental)
 - [x] Custom FPS in the default event loop (`libscratchcpp::Engine::run()`)
diff --git a/include/scratchcpp/iengine.h b/include/scratchcpp/iengine.h
index 1b347fff..7361ee40 100644
--- a/include/scratchcpp/iengine.h
+++ b/include/scratchcpp/iengine.h
@@ -139,6 +139,12 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Sets whether any key is pressed (use this for any key, even for unsupported keys). */
         virtual void setAnyKeyPressed(bool pressed) = 0;
 
+        /*! Starts "when up arrow key pressed" hats. */
+        virtual void mouseWheelUp() = 0;
+
+        /*! Starts "when down arrow key pressed" hats. */
+        virtual void mouseWheelDown() = 0;
+
         /*! Returns the X coordinate of the mouse pointer. */
         virtual double mouseX() const = 0;
 
@@ -283,6 +289,9 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /* Registers the given "when key pressed" script. */
         virtual void addKeyPressScript(std::shared_ptr<Block> hatBlock, int fieldId) = 0;
 
+        /* Registers the given "when this sprite/stage clicked" script. */
+        virtual void addTargetClickScript(std::shared_ptr<Block> hatBlock) = 0;
+
         /*! Returns the list of targets. */
         virtual const std::vector<std::shared_ptr<Target>> &targets() const = 0;
 
@@ -329,6 +338,18 @@ class LIBSCRATCHCPP_EXPORT IEngine
         /*! Sets the function which is called when a monitor is removed. */
         virtual void setRemoveMonitorHandler(const std::function<void(Monitor *, IMonitorHandler *)> &handler) = 0;
 
+        /*! Returns the function which is called when a question is asked, for example using the 'ask and wait' block. */
+        virtual const std::function<void(const std::string &)> &questionAsked() const = 0;
+
+        /*! Sets the function which is called when a question is asked, for example using the 'ask and wait' block. */
+        virtual void setQuestionAsked(const std::function<void(const std::string &)> &f) = 0;
+
+        /*! Returns the function which should be called when a question is answered. */
+        virtual const std::function<void(const std::string &)> &questionAnswered() const = 0;
+
+        /*! Sets the function which should be called when a question is answered. */
+        virtual void setQuestionAnswered(const std::function<void(const std::string &)> &f) = 0;
+
         /*! Returns the list of extension names. */
         virtual const std::vector<std::string> &extensions() const = 0;
 
@@ -337,6 +358,12 @@ class LIBSCRATCHCPP_EXPORT IEngine
 
         /*! Returns the map of scripts (each top level block has a Script object). */
         virtual const std::unordered_map<std::shared_ptr<Block>, std::shared_ptr<Script>> &scripts() const = 0;
+
+        /*! Returns the user agent of the last person to edit the project. */
+        virtual const std::string &userAgent() const = 0;
+
+        /*! Sets the user agent of the last person to edit the project. */
+        virtual void setUserAgent(const std::string &agent) = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/include/scratchcpp/ispritehandler.h b/include/scratchcpp/ispritehandler.h
index c413901e..663bffad 100644
--- a/include/scratchcpp/ispritehandler.h
+++ b/include/scratchcpp/ispritehandler.h
@@ -35,6 +35,9 @@ class LIBSCRATCHCPP_EXPORT ISpriteHandler
         /*! Called when the Y-coordinate changes. */
         virtual void onYChanged(double y) = 0;
 
+        /*! Called when the sprite position changes. */
+        virtual void onMoved(double oldX, double oldY, double newX, double newY) = 0;
+
         /*! Called when the size changes. */
         virtual void onSizeChanged(double size) = 0;
 
@@ -56,6 +59,12 @@ class LIBSCRATCHCPP_EXPORT ISpriteHandler
         /*! Called when all graphics effects are cleared. */
         virtual void onGraphicsEffectsCleared() = 0;
 
+        /*! Called when the bubble type changes. */
+        virtual void onBubbleTypeChanged(Target::BubbleType type) = 0;
+
+        /*! Called when the bubble text changes. */
+        virtual void onBubbleTextChanged(const std::string &text) = 0;
+
         /*!
          * Used to get the bounding rectangle of the sprite.
          * \note The rectangle must be relative to the stage, so make sure to use the sprite's coordinates.
diff --git a/include/scratchcpp/istagehandler.h b/include/scratchcpp/istagehandler.h
index 09a61ccd..354144d9 100644
--- a/include/scratchcpp/istagehandler.h
+++ b/include/scratchcpp/istagehandler.h
@@ -37,6 +37,12 @@ class LIBSCRATCHCPP_EXPORT IStageHandler
 
         /*! Called when all graphics effects are cleared. */
         virtual void onGraphicsEffectsCleared() = 0;
+
+        /*! Called when the bubble type changes. */
+        virtual void onBubbleTypeChanged(Target::BubbleType type) = 0;
+
+        /*! Called when the bubble text changes. */
+        virtual void onBubbleTextChanged(const std::string &text) = 0;
 };
 
 } // namespace libscratchcpp
diff --git a/include/scratchcpp/list.h b/include/scratchcpp/list.h
index 0ac380ab..6d63e8b2 100644
--- a/include/scratchcpp/list.h
+++ b/include/scratchcpp/list.h
@@ -12,6 +12,7 @@ namespace libscratchcpp
 {
 
 class Target;
+class Monitor;
 class ListPrivate;
 
 /*! \brief The List class represents a Scratch list. */
@@ -29,6 +30,9 @@ class LIBSCRATCHCPP_EXPORT List
         Target *target() const;
         void setTarget(Target *target);
 
+        Monitor *monitor() const;
+        void setMonitor(Monitor *monitor);
+
         long indexOf(const Value &value) const;
         bool contains(const Value &value) const;
 
diff --git a/include/scratchcpp/monitor.h b/include/scratchcpp/monitor.h
index efe89dc8..6fd84a41 100644
--- a/include/scratchcpp/monitor.h
+++ b/include/scratchcpp/monitor.h
@@ -38,6 +38,7 @@ class LIBSCRATCHCPP_EXPORT Monitor : public Entity
         void setInterface(IMonitorHandler *iface);
 
         const std::string &name() const;
+        void setName(const std::string &name);
 
         Mode mode() const;
         void setMode(Mode mode);
@@ -48,6 +49,7 @@ class LIBSCRATCHCPP_EXPORT Monitor : public Entity
         void setScript(std::shared_ptr<Script> script);
 
         std::shared_ptr<IBlockSection> blockSection() const;
+        void setBlockSection(std::shared_ptr<IBlockSection> blockSection);
 
         Sprite *sprite() const;
         void setSprite(Sprite *sprite);
diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h
index 17be2725..23191429 100644
--- a/include/scratchcpp/sprite.h
+++ b/include/scratchcpp/sprite.h
@@ -29,6 +29,7 @@ class LIBSCRATCHCPP_EXPORT Sprite
         Sprite(const Sprite &) = delete;
 
         void setInterface(ISpriteHandler *newInterface);
+        ISpriteHandler *getInterface() const; // NOTE: This can't be called interface because of... Microsoft... (it wouldn't compile on Windows)
 
         std::shared_ptr<Sprite> clone();
         void deleteClone();
@@ -47,6 +48,8 @@ class LIBSCRATCHCPP_EXPORT Sprite
         double y() const;
         void setY(double newY);
 
+        void setPosition(double x, double y);
+
         double size() const;
         void setSize(double newSize);
 
@@ -73,6 +76,9 @@ class LIBSCRATCHCPP_EXPORT Sprite
 
         void clearGraphicsEffects() override;
 
+        virtual void setBubbleType(Target::BubbleType type) override;
+        virtual void setBubbleText(const std::string &text) override;
+
     private:
         Target *dataSource() const override;
         void setXY(double x, double y);
diff --git a/include/scratchcpp/stage.h b/include/scratchcpp/stage.h
index 16b311f7..902f9b73 100644
--- a/include/scratchcpp/stage.h
+++ b/include/scratchcpp/stage.h
@@ -27,6 +27,7 @@ class LIBSCRATCHCPP_EXPORT Stage : public Target
         Stage(const Stage &) = delete;
 
         void setInterface(IStageHandler *newInterface);
+        IStageHandler *getInterface() const; // NOTE: This can't be called interface because of... Microsoft... (it wouldn't compile on Windows)
 
         bool isStage() const override;
 
@@ -51,6 +52,9 @@ class LIBSCRATCHCPP_EXPORT Stage : public Target
 
         void clearGraphicsEffects() override;
 
+        virtual void setBubbleType(Target::BubbleType type) override;
+        virtual void setBubbleText(const std::string &text) override;
+
     private:
         spimpl::unique_impl_ptr<StagePrivate> impl;
 };
diff --git a/include/scratchcpp/target.h b/include/scratchcpp/target.h
index 6bd8a977..1b4a7874 100644
--- a/include/scratchcpp/target.h
+++ b/include/scratchcpp/target.h
@@ -24,6 +24,12 @@ class TargetPrivate;
 class LIBSCRATCHCPP_EXPORT Target
 {
     public:
+        enum class BubbleType
+        {
+            Say,
+            Think
+        };
+
         Target();
         Target(const Target &) = delete;
         virtual ~Target() { }
@@ -83,6 +89,12 @@ class LIBSCRATCHCPP_EXPORT Target
 
         virtual void clearGraphicsEffects();
 
+        BubbleType bubbleType() const;
+        virtual void setBubbleType(BubbleType type);
+
+        const std::string &bubbleText() const;
+        virtual void setBubbleText(const std::string &text);
+
         IEngine *engine() const;
         void setEngine(IEngine *engine);
 
diff --git a/include/scratchcpp/value.h b/include/scratchcpp/value.h
index 5186748f..32180df0 100644
--- a/include/scratchcpp/value.h
+++ b/include/scratchcpp/value.h
@@ -3,10 +3,10 @@
 #pragma once
 
 #include <string>
-#include <variant>
 #include <algorithm>
 #include <limits>
 #include <ctgmath>
+#include <cassert>
 #include <utf8.h>
 
 #include "global.h"
@@ -548,63 +548,75 @@ class LIBSCRATCHCPP_EXPORT Value
             }
         }
 
+        double getNumber(bool *ok = nullptr) const
+        {
+            // Equivalent to JavaScript Number(), *ok == false means NaN
+            if (ok)
+                *ok = true;
+
+            switch (m_type) {
+                case Type::Integer:
+                    return m_intValue;
+
+                case Type::Double:
+                    return m_doubleValue;
+
+                case Type::Bool:
+                    return m_boolValue;
+
+                case Type::String:
+                    return stringToDouble(m_stringValue, ok);
+
+                case Type::Infinity:
+                    return std::numeric_limits<double>::infinity();
+
+                case Type::NegativeInfinity:
+                    return -std::numeric_limits<double>::infinity();
+
+                case Type::NaN:
+                    if (ok)
+                        *ok = false;
+
+                    return 0;
+
+                default:
+                    assert(false); // this should never happen
+                    if (ok)
+                        *ok = false;
+
+                    return 0;
+            }
+        }
+
         void initString(const char *str) { initString(std::string(str)); }
 
         Type m_type;
 
         friend bool operator==(const Value &v1, const Value &v2)
         {
-            if (v1.m_type == v2.m_type) {
-                auto type = v1.m_type;
-                switch (type) {
-                    case Type::Integer:
-                        return v1.toLong() == v2.toLong();
-                    case Type::Double:
-                        return v1.toDouble() == v2.toDouble();
-                    case Type::Bool:
-                        return v1.toBool() == v2.toBool();
-                    case Type::String:
-                        return stringsEqual(v1.toUtf16(), v2.toUtf16());
-                    default:
-                        if ((static_cast<int>(v1.m_type) < 0) && (static_cast<int>(v2.m_type) < 0)) {
-                            return v1.m_type == v2.m_type;
-                        }
-                }
-            } else {
-                if (v1.isString() || v2.isString()) {
-                    if (static_cast<int>(v1.m_type) < 0 || static_cast<int>(v2.m_type) < 0)
-                        return stringsEqual(v1.toUtf16(), v2.toUtf16());
-
-                    long l1, l2;
-                    double d1, d2;
-                    int type1 = checkString(v1.toString(), &l1, &d1);
-                    int type2 = checkString(v2.toString(), &l2, &d2);
-
-                    if (type1 == 1)
-                        d1 = l1;
-
-                    if (type2 == 1)
-                        d2 = l2;
-
-                    if (type1 > 0 && type2 > 0)
-                        return d1 == d2;
-                    else if (type2 > 0)
-                        return v1.toString() == doubleToString(d2);
-                    else if (type1 > 0)
-                        return doubleToString(d1) == v2.toString();
-                    else
-                        return stringsEqual(v1.toUtf16(), v2.toUtf16());
-                } else if (v1.isNumber() || v2.isNumber()) {
-                    if (static_cast<int>(v1.m_type) < 0 || static_cast<int>(v2.m_type) < 0)
-                        return false;
-                    else
-                        return v1.toDouble() == v2.toDouble();
-                } else if (v1.isBool() || v2.isBool())
-                    return ((v1.m_type != Type::NaN && v2.m_type != Type::NaN) && (v1.toBool() == v2.toBool()));
-                else
-                    return false;
+            // https://github.com/scratchfoundation/scratch-vm/blob/112989da0e7306eeb405a5c52616e41c2164af24/src/util/cast.js#L121-L150
+            bool ok;
+            double n1 = v1.getNumber(&ok);
+            double n2;
+
+            if (ok)
+                n2 = v2.getNumber(&ok);
+
+            if (!ok) {
+                // At least one argument can't be converted to a number
+                // Scratch compares strings as case insensitive
+                return stringsEqual(v1.toUtf16(), v2.toUtf16());
             }
-            return false;
+
+            // Handle the special case of Infinity
+            if ((static_cast<int>(v1.m_type) < 0) && (static_cast<int>(v2.m_type) < 0)) {
+                assert(v1.m_type != Type::NaN);
+                assert(v2.m_type != Type::NaN);
+                return v1.m_type == v2.m_type;
+            }
+
+            // Compare as numbers
+            return n1 == n2;
         }
 
         friend bool operator!=(const Value &v1, const Value &v2) { return !(v1 == v2); }
diff --git a/include/scratchcpp/variable.h b/include/scratchcpp/variable.h
index 0f5a06be..7e2bfdc8 100644
--- a/include/scratchcpp/variable.h
+++ b/include/scratchcpp/variable.h
@@ -10,6 +10,7 @@ namespace libscratchcpp
 {
 
 class Target;
+class Monitor;
 class VariablePrivate;
 
 /*! \brief The Variable class represents a Scratch variable. */
@@ -33,6 +34,9 @@ class LIBSCRATCHCPP_EXPORT Variable : public Entity
         Target *target() const;
         void setTarget(Target *target);
 
+        Monitor *monitor() const;
+        void setMonitor(Monitor *monitor);
+
         std::shared_ptr<Variable> clone();
 
     private:
diff --git a/include/scratchcpp/virtualmachine.h b/include/scratchcpp/virtualmachine.h
index c00a0996..58541495 100644
--- a/include/scratchcpp/virtualmachine.h
+++ b/include/scratchcpp/virtualmachine.h
@@ -71,16 +71,16 @@ enum Opcode
     OP_LIST_LENGTH,    /*!< Stores the length of the list with the index in the argument, in the last register. */
     OP_LIST_CONTAINS,  /*!< Stores true in the last register if the list with the index in the argument contains the value from the last register. */
     OP_STR_CONCAT,     /*!< Concatenates the strings stored in the last 2 registers and stores the result in the last register, deleting the input registers. */
-    OP_STR_AT,         /*! Stores the character at index in the last register of the string in the second last register, in the last register. */
-    OP_STR_LENGTH,     /*! Stores the length of the string in the last register, in the last register. */
-    OP_STR_CONTAINS,   /*! Stores true in the last register if the string stored in the second last register contains the substring in the last register. */
+    OP_STR_AT,         /*!< Stores the character at index in the last register of the string in the second last register, in the last register. */
+    OP_STR_LENGTH,     /*!< Stores the length of the string in the last register, in the last register. */
+    OP_STR_CONTAINS,   /*!< Stores true in the last register if the string stored in the second last register contains the substring in the last register. */
     OP_EXEC,           /*!< Calls the function with the index in the argument. */
     OP_INIT_PROCEDURE, /*!< Initializes the list of procedure (custom block) arguments. */
     OP_CALL_PROCEDURE, /*! Calls the procedure (custom block) with the index in the argument. */
     OP_ADD_ARG,        /*!< Adds a procedure (custom block) argument with the value from the last register. */
     OP_READ_ARG,       /*!< Reads the procedure (custom block) argument with the index in the argument and stores the value in the last register. */
     OP_BREAK_FRAME,    /*!< Breaks current frame at the end of the loop. */
-    OP_WARP            /*! Runs the script without screen refresh. */
+    OP_WARP            /*!< Runs the script without screen refresh. */
 };
 
 }
@@ -133,6 +133,8 @@ class LIBSCRATCHCPP_EXPORT VirtualMachine
         void moveToLastCheckpoint();
 
         void stop(bool savePos = true, bool breakFrame = false, bool goBack = false);
+        void promise();
+        void resolvePromise();
 
         bool atEnd() const;
 
diff --git a/src/blocks/eventblocks.cpp b/src/blocks/eventblocks.cpp
index 4edd07b7..81f925ef 100644
--- a/src/blocks/eventblocks.cpp
+++ b/src/blocks/eventblocks.cpp
@@ -22,6 +22,8 @@ void EventBlocks::registerBlocks(IEngine *engine)
 {
     // Blocks
     engine->addCompileFunction(this, "event_whenflagclicked", &compileWhenFlagClicked);
+    engine->addCompileFunction(this, "event_whenthisspriteclicked", &compileWhenThisSpriteClicked);
+    engine->addCompileFunction(this, "event_whenstageclicked", &compileWhenStageClicked);
     engine->addCompileFunction(this, "event_broadcast", &compileBroadcast);
     engine->addCompileFunction(this, "event_broadcastandwait", &compileBroadcastAndWait);
     engine->addCompileFunction(this, "event_whenbroadcastreceived", &compileWhenBroadcastReceived);
@@ -42,6 +44,16 @@ void EventBlocks::compileWhenFlagClicked(Compiler *compiler)
     compiler->engine()->addGreenFlagScript(compiler->block());
 }
 
+void EventBlocks::compileWhenThisSpriteClicked(Compiler *compiler)
+{
+    compiler->engine()->addTargetClickScript(compiler->block());
+}
+
+void EventBlocks::compileWhenStageClicked(Compiler *compiler)
+{
+    compiler->engine()->addTargetClickScript(compiler->block());
+}
+
 void EventBlocks::compileBroadcast(Compiler *compiler)
 {
     auto input = compiler->input(BROADCAST_INPUT);
diff --git a/src/blocks/eventblocks.h b/src/blocks/eventblocks.h
index 21f7e812..ec63bdf8 100644
--- a/src/blocks/eventblocks.h
+++ b/src/blocks/eventblocks.h
@@ -31,6 +31,8 @@ class EventBlocks : public IBlockSection
         void registerBlocks(IEngine *engine) override;
 
         static void compileWhenFlagClicked(Compiler *compiler);
+        static void compileWhenThisSpriteClicked(Compiler *compiler);
+        static void compileWhenStageClicked(Compiler *compiler);
         static void compileBroadcast(Compiler *compiler);
         static void compileBroadcastAndWait(Compiler *compiler);
         static void compileWhenBroadcastReceived(Compiler *compiler);
diff --git a/src/blocks/listblocks.cpp b/src/blocks/listblocks.cpp
index 5a52c56f..03705deb 100644
--- a/src/blocks/listblocks.cpp
+++ b/src/blocks/listblocks.cpp
@@ -5,6 +5,8 @@
 #include <scratchcpp/field.h>
 #include <scratchcpp/block.h>
 #include <scratchcpp/list.h>
+#include <scratchcpp/stage.h>
+#include <scratchcpp/monitor.h>
 
 #include "listblocks.h"
 
@@ -33,6 +35,8 @@ void ListBlocks::registerBlocks(IEngine *engine)
     engine->addCompileFunction(this, "data_itemnumoflist", &compileItemNumberInList);
     engine->addCompileFunction(this, "data_lengthoflist", &compileLengthOfList);
     engine->addCompileFunction(this, "data_listcontainsitem", &compileListContainsItem);
+    engine->addCompileFunction(this, "data_showlist", &compileShowList);
+    engine->addCompileFunction(this, "data_hidelist", &compileHideList);
 
     // Monitor names
     engine->addMonitorNameFunction(this, "data_listcontents", &listContentsMonitorName);
@@ -107,6 +111,84 @@ void ListBlocks::compileListContainsItem(Compiler *compiler)
     compiler->addInstruction(vm::OP_LIST_CONTAINS, { compiler->listIndex(compiler->field(LIST)->valuePtr()) });
 }
 
+void ListBlocks::compileShowList(Compiler *compiler)
+{
+    Field *field = compiler->field(LIST);
+    assert(field);
+    List *var = static_cast<List *>(field->valuePtr().get());
+    assert(var);
+
+    compiler->addConstValue(var->id());
+
+    if (var->target() == static_cast<Target *>(compiler->engine()->stage()))
+        compiler->addFunctionCall(&showGlobalList);
+    else
+        compiler->addFunctionCall(&showList);
+}
+
+void ListBlocks::compileHideList(Compiler *compiler)
+{
+    Field *field = compiler->field(LIST);
+    assert(field);
+    List *var = static_cast<List *>(field->valuePtr().get());
+    assert(var);
+
+    compiler->addConstValue(var->id());
+
+    if (var->target() == static_cast<Target *>(compiler->engine()->stage()))
+        compiler->addFunctionCall(&hideGlobalList);
+    else
+        compiler->addFunctionCall(&hideList);
+}
+
+void ListBlocks::setListVisible(std::shared_ptr<List> list, bool visible)
+{
+    if (list) {
+        assert(list->monitor());
+        list->monitor()->setVisible(visible);
+    }
+}
+
+unsigned int ListBlocks::showGlobalList(VirtualMachine *vm)
+{
+    if (Stage *target = vm->engine()->stage()) {
+        int index = target->findListById(vm->getInput(0, 1)->toString());
+        setListVisible(target->listAt(index), true);
+    }
+
+    return 1;
+}
+
+unsigned int ListBlocks::showList(VirtualMachine *vm)
+{
+    if (Target *target = vm->target()) {
+        int index = target->findListById(vm->getInput(0, 1)->toString());
+        setListVisible(target->listAt(index), true);
+    }
+
+    return 1;
+}
+
+unsigned int ListBlocks::hideGlobalList(VirtualMachine *vm)
+{
+    if (Stage *target = vm->engine()->stage()) {
+        int index = target->findListById(vm->getInput(0, 1)->toString());
+        setListVisible(target->listAt(index), false);
+    }
+
+    return 1;
+}
+
+unsigned int ListBlocks::hideList(VirtualMachine *vm)
+{
+    if (Target *target = vm->target()) {
+        int index = target->findListById(vm->getInput(0, 1)->toString());
+        setListVisible(target->listAt(index), false);
+    }
+
+    return 1;
+}
+
 const std::string &ListBlocks::listContentsMonitorName(Block *block)
 {
     List *list = dynamic_cast<List *>(block->findFieldById(LIST)->valuePtr().get());
diff --git a/src/blocks/listblocks.h b/src/blocks/listblocks.h
index 678725f6..1fdcbd12 100644
--- a/src/blocks/listblocks.h
+++ b/src/blocks/listblocks.h
@@ -8,6 +8,7 @@ namespace libscratchcpp
 {
 
 class Compiler;
+class List;
 
 /*! \brief The ListBlocks class contains the implementation of list blocks. */
 class ListBlocks : public IBlockSection
@@ -39,6 +40,15 @@ class ListBlocks : public IBlockSection
         static void compileItemNumberInList(Compiler *compiler);
         static void compileLengthOfList(Compiler *compiler);
         static void compileListContainsItem(Compiler *compiler);
+        static void compileShowList(Compiler *compiler);
+        static void compileHideList(Compiler *compiler);
+
+        static void setListVisible(std::shared_ptr<List> list, bool visible);
+
+        static unsigned int showGlobalList(VirtualMachine *vm);
+        static unsigned int showList(VirtualMachine *vm);
+        static unsigned int hideGlobalList(VirtualMachine *vm);
+        static unsigned int hideList(VirtualMachine *vm);
 
         static const std::string &listContentsMonitorName(Block *block);
 
diff --git a/src/blocks/looksblocks.cpp b/src/blocks/looksblocks.cpp
index d56cb4d1..0072ed45 100644
--- a/src/blocks/looksblocks.cpp
+++ b/src/blocks/looksblocks.cpp
@@ -12,10 +12,12 @@
 
 #include "looksblocks.h"
 #include "../engine/internal/randomgenerator.h"
+#include "../engine/internal/clock.h"
 
 using namespace libscratchcpp;
 
 IRandomGenerator *LooksBlocks::rng = nullptr;
+IClock *LooksBlocks::clock = nullptr;
 
 std::string LooksBlocks::name() const
 {
@@ -25,6 +27,10 @@ std::string LooksBlocks::name() const
 void LooksBlocks::registerBlocks(IEngine *engine)
 {
     // Blocks
+    engine->addCompileFunction(this, "looks_sayforsecs", &compileSayForSecs);
+    engine->addCompileFunction(this, "looks_say", &compileSay);
+    engine->addCompileFunction(this, "looks_thinkforsecs", &compileThinkForSecs);
+    engine->addCompileFunction(this, "looks_think", &compileThink);
     engine->addCompileFunction(this, "looks_show", &compileShow);
     engine->addCompileFunction(this, "looks_hide", &compileHide);
     engine->addCompileFunction(this, "looks_changeeffectby", &compileChangeEffectBy);
@@ -49,6 +55,8 @@ void LooksBlocks::registerBlocks(IEngine *engine)
     engine->addMonitorNameFunction(this, "looks_size", &sizeMonitorName);
 
     // Inputs
+    engine->addInput(this, "MESSAGE", MESSAGE);
+    engine->addInput(this, "SECS", SECS);
     engine->addInput(this, "CHANGE", CHANGE);
     engine->addInput(this, "SIZE", SIZE);
     engine->addInput(this, "COSTUME", COSTUME);
@@ -77,6 +85,34 @@ void LooksBlocks::registerBlocks(IEngine *engine)
     engine->addFieldValue(this, "backward", Backward);
 }
 
+void LooksBlocks::compileSayForSecs(Compiler *compiler)
+{
+    compiler->addInput(MESSAGE);
+    compiler->addInput(SECS);
+    compiler->addFunctionCall(&startSayForSecs);
+    compiler->addFunctionCall(&sayForSecs);
+}
+
+void LooksBlocks::compileSay(Compiler *compiler)
+{
+    compiler->addInput(MESSAGE);
+    compiler->addFunctionCall(&say);
+}
+
+void LooksBlocks::compileThinkForSecs(Compiler *compiler)
+{
+    compiler->addInput(MESSAGE);
+    compiler->addInput(SECS);
+    compiler->addFunctionCall(&startThinkForSecs);
+    compiler->addFunctionCall(&thinkForSecs);
+}
+
+void LooksBlocks::compileThink(Compiler *compiler)
+{
+    compiler->addInput(MESSAGE);
+    compiler->addFunctionCall(&think);
+}
+
 void LooksBlocks::compileShow(Compiler *compiler)
 {
     compiler->addFunctionCall(&show);
@@ -179,7 +215,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
             if (!m_colorEffect)
                 m_colorEffect = ScratchConfiguration::getGraphicsEffect("color");
 
-            compiler->addInput(CHANGE);
+            compiler->addInput(VALUE);
             compiler->addFunctionCall(&setColorEffectTo);
             break;
 
@@ -187,7 +223,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
             if (!m_fisheyeEffect)
                 m_fisheyeEffect = ScratchConfiguration::getGraphicsEffect("fisheye");
 
-            compiler->addInput(CHANGE);
+            compiler->addInput(VALUE);
             compiler->addFunctionCall(&setFisheyeEffectTo);
             break;
 
@@ -195,7 +231,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
             if (!m_whirlEffect)
                 m_whirlEffect = ScratchConfiguration::getGraphicsEffect("whirl");
 
-            compiler->addInput(CHANGE);
+            compiler->addInput(VALUE);
             compiler->addFunctionCall(&setWhirlEffectTo);
             break;
 
@@ -203,7 +239,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
             if (!m_pixelateEffect)
                 m_pixelateEffect = ScratchConfiguration::getGraphicsEffect("pixelate");
 
-            compiler->addInput(CHANGE);
+            compiler->addInput(VALUE);
             compiler->addFunctionCall(&setPixelateEffectTo);
             break;
 
@@ -211,7 +247,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
             if (!m_mosaicEffect)
                 m_mosaicEffect = ScratchConfiguration::getGraphicsEffect("mosaic");
 
-            compiler->addInput(CHANGE);
+            compiler->addInput(VALUE);
             compiler->addFunctionCall(&setMosaicEffectTo);
             break;
 
@@ -219,7 +255,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
             if (!m_brightnessEffect)
                 m_brightnessEffect = ScratchConfiguration::getGraphicsEffect("brightness");
 
-            compiler->addInput(CHANGE);
+            compiler->addInput(VALUE);
             compiler->addFunctionCall(&setBrightnessEffectTo);
             break;
 
@@ -227,7 +263,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
             if (!m_ghostEffect)
                 m_ghostEffect = ScratchConfiguration::getGraphicsEffect("ghost");
 
-            compiler->addInput(CHANGE);
+            compiler->addInput(VALUE);
             compiler->addFunctionCall(&setGhostEffectTo);
             break;
 
@@ -245,7 +281,7 @@ void LooksBlocks::compileSetEffectTo(Compiler *compiler)
                     index = it - m_customGraphicsEffects.begin();
 
                 compiler->addConstValue(index);
-                compiler->addInput(CHANGE);
+                compiler->addInput(VALUE);
                 compiler->addFunctionCall(&setEffectTo);
             }
 
@@ -515,6 +551,114 @@ const std::string &LooksBlocks::sizeMonitorName(Block *block)
     return name;
 }
 
+void LooksBlocks::startWait(VirtualMachine *vm, double secs)
+{
+    if (!clock)
+        clock = Clock::instance().get();
+
+    auto currentTime = clock->currentSteadyTime();
+    m_timeMap[vm] = { currentTime, secs * 1000 };
+    vm->engine()->requestRedraw();
+}
+
+bool LooksBlocks::wait(VirtualMachine *vm)
+{
+    if (!clock)
+        clock = Clock::instance().get();
+
+    auto currentTime = clock->currentSteadyTime();
+    assert(m_timeMap.count(vm) == 1);
+
+    if (std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - m_timeMap[vm].first).count() >= m_timeMap[vm].second) {
+        m_timeMap.erase(vm);
+        vm->stop(true, true, false);
+        return true;
+    } else {
+        vm->stop(true, true, true);
+        return false;
+    }
+}
+
+void LooksBlocks::showBubble(VirtualMachine *vm, Target::BubbleType type, const std::string &text)
+{
+    Target *target = vm->target();
+
+    if (target) {
+        target->setBubbleType(type);
+        target->setBubbleText(text);
+        m_waitingBubbles.erase(target);
+    }
+}
+
+void LooksBlocks::hideBubble(Target *target)
+{
+    if (!target)
+        return;
+
+    target->setBubbleText("");
+    m_waitingBubbles.erase(target);
+}
+
+unsigned int LooksBlocks::startSayForSecs(VirtualMachine *vm)
+{
+    Target *target = vm->target();
+
+    if (target) {
+        showBubble(vm, Target::BubbleType::Say, vm->getInput(0, 2)->toString());
+        m_waitingBubbles[target] = vm;
+        startWait(vm, vm->getInput(1, 2)->toDouble());
+    }
+
+    return 2;
+}
+
+unsigned int LooksBlocks::sayForSecs(VirtualMachine *vm)
+{
+    if (wait(vm)) {
+        Target *target = vm->target();
+
+        if (target) {
+            auto it = m_waitingBubbles.find(target);
+
+            // Clear bubble if it hasn't been changed
+            if (it != m_waitingBubbles.cend() && it->second == vm)
+                hideBubble(vm->target());
+        }
+    }
+
+    return 0;
+}
+
+unsigned int LooksBlocks::say(VirtualMachine *vm)
+{
+    showBubble(vm, Target::BubbleType::Say, vm->getInput(0, 1)->toString());
+    return 1;
+}
+
+unsigned int LooksBlocks::startThinkForSecs(VirtualMachine *vm)
+{
+    Target *target = vm->target();
+
+    if (target) {
+        showBubble(vm, Target::BubbleType::Think, vm->getInput(0, 2)->toString());
+        m_waitingBubbles[target] = vm;
+        startWait(vm, vm->getInput(1, 2)->toDouble());
+    }
+
+    return 2;
+}
+
+unsigned int LooksBlocks::thinkForSecs(VirtualMachine *vm)
+{
+    return sayForSecs(vm); // there isn't any difference
+}
+
+unsigned int LooksBlocks::think(VirtualMachine *vm)
+{
+    showBubble(vm, Target::BubbleType::Think, vm->getInput(0, 1)->toString());
+    return 1;
+}
+
 unsigned int LooksBlocks::show(VirtualMachine *vm)
 {
     Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
@@ -537,11 +681,11 @@ unsigned int LooksBlocks::hide(VirtualMachine *vm)
 
 unsigned int LooksBlocks::changeEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite) {
+    if (target) {
         IGraphicsEffect *effect = m_customGraphicsEffects[vm->getInput(0, 2)->toLong()];
-        sprite->setGraphicsEffectValue(effect, sprite->graphicsEffectValue(effect) + vm->getInput(1, 2)->toDouble());
+        target->setGraphicsEffectValue(effect, target->graphicsEffectValue(effect) + vm->getInput(1, 2)->toDouble());
     }
 
     return 2;
@@ -549,160 +693,160 @@ unsigned int LooksBlocks::changeEffectBy(VirtualMachine *vm)
 
 unsigned int LooksBlocks::changeColorEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_colorEffect, sprite->graphicsEffectValue(m_colorEffect) + vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_colorEffect, target->graphicsEffectValue(m_colorEffect) + vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::changeFisheyeEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_fisheyeEffect, sprite->graphicsEffectValue(m_fisheyeEffect) + vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_fisheyeEffect, target->graphicsEffectValue(m_fisheyeEffect) + vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::changeWhirlEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_whirlEffect, sprite->graphicsEffectValue(m_whirlEffect) + vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_whirlEffect, target->graphicsEffectValue(m_whirlEffect) + vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::changePixelateEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_pixelateEffect, sprite->graphicsEffectValue(m_pixelateEffect) + vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_pixelateEffect, target->graphicsEffectValue(m_pixelateEffect) + vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::changeMosaicEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_mosaicEffect, sprite->graphicsEffectValue(m_mosaicEffect) + vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_mosaicEffect, target->graphicsEffectValue(m_mosaicEffect) + vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::changeBrightnessEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_brightnessEffect, sprite->graphicsEffectValue(m_brightnessEffect) + vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_brightnessEffect, target->graphicsEffectValue(m_brightnessEffect) + vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::changeGhostEffectBy(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_ghostEffect, sprite->graphicsEffectValue(m_ghostEffect) + vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_ghostEffect, target->graphicsEffectValue(m_ghostEffect) + vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::setEffectTo(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_customGraphicsEffects[vm->getInput(0, 2)->toLong()], vm->getInput(1, 2)->toDouble());
+    if (target)
+        target->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());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_colorEffect, vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_colorEffect, vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::setFisheyeEffectTo(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_fisheyeEffect, vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_fisheyeEffect, vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::setWhirlEffectTo(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_whirlEffect, vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_whirlEffect, vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::setPixelateEffectTo(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_pixelateEffect, vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_pixelateEffect, vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::setMosaicEffectTo(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_mosaicEffect, vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_mosaicEffect, vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::setBrightnessEffectTo(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_brightnessEffect, vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_brightnessEffect, vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::setGhostEffectTo(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->setGraphicsEffectValue(m_ghostEffect, vm->getInput(0, 1)->toDouble());
+    if (target)
+        target->setGraphicsEffectValue(m_ghostEffect, vm->getInput(0, 1)->toDouble());
 
     return 1;
 }
 
 unsigned int LooksBlocks::clearGraphicEffects(VirtualMachine *vm)
 {
-    Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
+    Target *target = vm->target();
 
-    if (sprite)
-        sprite->clearGraphicsEffects();
+    if (target)
+        target->clearGraphicsEffects();
 
     return 0;
 }
diff --git a/src/blocks/looksblocks.h b/src/blocks/looksblocks.h
index 585fc0ec..91a598b4 100644
--- a/src/blocks/looksblocks.h
+++ b/src/blocks/looksblocks.h
@@ -3,16 +3,19 @@
 #pragma once
 
 #include <scratchcpp/iblocksection.h>
+#include <scratchcpp/target.h>
 #include <vector>
+#include <unordered_map>
+#include <chrono>
 
 namespace libscratchcpp
 {
 
-class Target;
 class Stage;
 class Value;
 class IGraphicsEffect;
 class IRandomGenerator;
+class IClock;
 
 /*! \brief The LooksBlocks class contains the implementation of looks blocks. */
 class LooksBlocks : public IBlockSection
@@ -20,6 +23,8 @@ class LooksBlocks : public IBlockSection
     public:
         enum Inputs
         {
+            MESSAGE,
+            SECS,
             CHANGE,
             SIZE,
             COSTUME,
@@ -57,6 +62,10 @@ class LooksBlocks : public IBlockSection
 
         void registerBlocks(IEngine *engine) override;
 
+        static void compileSayForSecs(Compiler *compiler);
+        static void compileSay(Compiler *compiler);
+        static void compileThinkForSecs(Compiler *compiler);
+        static void compileThink(Compiler *compiler);
         static void compileShow(Compiler *compiler);
         static void compileHide(Compiler *compiler);
         static void compileChangeEffectBy(Compiler *compiler);
@@ -79,6 +88,19 @@ class LooksBlocks : public IBlockSection
         static const std::string &backdropNumberNameMonitorName(Block *block);
         static const std::string &sizeMonitorName(Block *block);
 
+        static void startWait(VirtualMachine *vm, double secs);
+        static bool wait(VirtualMachine *vm);
+        static void showBubble(VirtualMachine *vm, Target::BubbleType type, const std::string &text);
+        static void hideBubble(Target *target);
+
+        static unsigned int startSayForSecs(VirtualMachine *vm);
+        static unsigned int sayForSecs(VirtualMachine *vm);
+        static unsigned int say(VirtualMachine *vm);
+
+        static unsigned int startThinkForSecs(VirtualMachine *vm);
+        static unsigned int thinkForSecs(VirtualMachine *vm);
+        static unsigned int think(VirtualMachine *vm);
+
         static unsigned int show(VirtualMachine *vm);
         static unsigned int hide(VirtualMachine *vm);
 
@@ -141,6 +163,9 @@ class LooksBlocks : public IBlockSection
         static unsigned int backdropNumber(VirtualMachine *vm);
         static unsigned int backdropName(VirtualMachine *vm);
 
+        static inline std::unordered_map<VirtualMachine *, std::pair<std::chrono::steady_clock::time_point, int>> m_timeMap;
+        static inline std::unordered_map<Target *, VirtualMachine *> m_waitingBubbles;
+
         static inline std::vector<IGraphicsEffect *> m_customGraphicsEffects;
         static inline IGraphicsEffect *m_colorEffect = nullptr;
         static inline IGraphicsEffect *m_fisheyeEffect = nullptr;
@@ -151,6 +176,7 @@ class LooksBlocks : public IBlockSection
         static inline IGraphicsEffect *m_ghostEffect = nullptr;
 
         static IRandomGenerator *rng;
+        static IClock *clock;
 };
 
 } // namespace libscratchcpp
diff --git a/src/blocks/motionblocks.cpp b/src/blocks/motionblocks.cpp
index 8c38001f..c49a3dd0 100644
--- a/src/blocks/motionblocks.cpp
+++ b/src/blocks/motionblocks.cpp
@@ -275,8 +275,7 @@ unsigned int MotionBlocks::moveSteps(VirtualMachine *vm)
     if (sprite) {
         double dir = sprite->direction();
         double steps = vm->getInput(0, 1)->toDouble();
-        sprite->setX(sprite->x() + std::sin(dir * pi / 180) * steps);
-        sprite->setY(sprite->y() + std::cos(dir * pi / 180) * steps);
+        sprite->setPosition(sprite->x() + std::sin(dir * pi / 180) * steps, sprite->y() + std::cos(dir * pi / 180) * steps);
     }
 
     return 1;
@@ -392,8 +391,7 @@ unsigned int MotionBlocks::goToXY(VirtualMachine *vm)
     Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
 
     if (sprite) {
-        sprite->setX(vm->getInput(0, 2)->toDouble());
-        sprite->setY(vm->getInput(1, 2)->toDouble());
+        sprite->setPosition(vm->getInput(0, 2)->toDouble(), vm->getInput(1, 2)->toDouble());
     }
 
     return 2;
@@ -408,26 +406,22 @@ unsigned int MotionBlocks::goTo(VirtualMachine *vm)
 
     std::string value = vm->getInput(0, 1)->toString();
 
-    if (value == "_mouse_") {
-        sprite->setX(vm->engine()->mouseX());
-        sprite->setY(vm->engine()->mouseY());
-    } else if (value == "_random_") {
+    if (value == "_mouse_")
+        sprite->setPosition(vm->engine()->mouseX(), vm->engine()->mouseY());
+    else if (value == "_random_") {
         const unsigned int stageWidth = vm->engine()->stageWidth();
         const unsigned int stageHeight = vm->engine()->stageHeight();
 
         if (!rng)
             rng = RandomGenerator::instance().get();
 
-        sprite->setX(rng->randint(-static_cast<int>(stageWidth / 2), stageWidth / 2));
-        sprite->setY(rng->randint(-static_cast<int>(stageHeight / 2), stageHeight / 2));
+        sprite->setPosition(rng->randint(-static_cast<int>(stageWidth / 2), stageWidth / 2), rng->randint(-static_cast<int>(stageHeight / 2), stageHeight / 2));
     } else {
         Target *target = vm->engine()->targetAt(vm->engine()->findTarget(value));
         Sprite *targetSprite = dynamic_cast<Sprite *>(target);
 
-        if (targetSprite) {
-            sprite->setX(targetSprite->x());
-            sprite->setY(targetSprite->y());
-        }
+        if (targetSprite)
+            sprite->setPosition(targetSprite->x(), targetSprite->y());
     }
 
     return 1;
@@ -439,10 +433,8 @@ unsigned int MotionBlocks::goToByIndex(VirtualMachine *vm)
     Target *target = vm->engine()->targetAt(vm->getInput(0, 1)->toInt());
     Sprite *targetSprite = dynamic_cast<Sprite *>(target);
 
-    if (sprite && targetSprite) {
-        sprite->setX(targetSprite->x());
-        sprite->setY(targetSprite->y());
-    }
+    if (sprite && targetSprite)
+        sprite->setPosition(targetSprite->x(), targetSprite->y());
 
     return 1;
 }
@@ -451,10 +443,8 @@ unsigned int MotionBlocks::goToMousePointer(VirtualMachine *vm)
 {
     Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
 
-    if (sprite) {
-        sprite->setX(vm->engine()->mouseX());
-        sprite->setY(vm->engine()->mouseY());
-    }
+    if (sprite)
+        sprite->setPosition(vm->engine()->mouseX(), vm->engine()->mouseY());
 
     return 0;
 }
@@ -470,8 +460,7 @@ unsigned int MotionBlocks::goToRandomPosition(VirtualMachine *vm)
         if (!rng)
             rng = RandomGenerator::instance().get();
 
-        sprite->setX(rng->randint(-static_cast<int>(stageWidth / 2), stageWidth / 2));
-        sprite->setY(rng->randint(-static_cast<int>(stageHeight / 2), stageHeight / 2));
+        sprite->setPosition(rng->randint(-static_cast<int>(stageWidth / 2), stageWidth / 2), rng->randint(-static_cast<int>(stageHeight / 2), stageHeight / 2));
     }
 
     return 0;
@@ -485,10 +474,8 @@ void MotionBlocks::startGlidingToPos(VirtualMachine *vm, double x, double y, dou
         return;
 
     if (secs <= 0) {
-        if (sprite) {
-            sprite->setX(x);
-            sprite->setY(y);
-        }
+        if (sprite)
+            sprite->setPosition(x, y);
 
         return;
     }
@@ -516,10 +503,8 @@ void MotionBlocks::continueGliding(VirtualMachine *vm)
     double y = m_glideMap[vm].second.second;
 
     if (elapsedTime >= maxTime) {
-        if (sprite) {
-            sprite->setX(x);
-            sprite->setY(y);
-        }
+        if (sprite)
+            sprite->setPosition(x, y);
 
         m_timeMap.erase(vm);
         m_glideMap.erase(vm);
@@ -530,8 +515,7 @@ void MotionBlocks::continueGliding(VirtualMachine *vm)
             double factor = elapsedTime / static_cast<double>(maxTime);
             assert(factor >= 0 && factor < 1);
 
-            sprite->setX(startX + (x - startX) * factor);
-            sprite->setY(startY + (y - startY) * factor);
+            sprite->setPosition(startX + (x - startX) * factor, startY + (y - startY) * factor);
         }
 
         vm->stop(true, true, true);
@@ -750,8 +734,7 @@ unsigned int MotionBlocks::ifOnEdgeBounce(VirtualMachine *vm)
     // Keep within the stage
     double fencedX, fencedY;
     sprite->keepInFence(sprite->x(), sprite->y(), &fencedX, &fencedY);
-    sprite->setX(fencedX);
-    sprite->setY(fencedY);
+    sprite->setPosition(fencedX, fencedY);
 
     return 0;
 }
diff --git a/src/blocks/sensingblocks.cpp b/src/blocks/sensingblocks.cpp
index 0af82ae8..869d7657 100644
--- a/src/blocks/sensingblocks.cpp
+++ b/src/blocks/sensingblocks.cpp
@@ -28,6 +28,8 @@ void SensingBlocks::registerBlocks(IEngine *engine)
 {
     // Blocks
     engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo);
+    engine->addCompileFunction(this, "sensing_askandwait", &compileAskAndWait);
+    engine->addCompileFunction(this, "sensing_answer", &compileAnswer);
     engine->addCompileFunction(this, "sensing_keypressed", &compileKeyPressed);
     engine->addCompileFunction(this, "sensing_mousedown", &compileMouseDown);
     engine->addCompileFunction(this, "sensing_mousex", &compileMouseX);
@@ -49,6 +51,7 @@ void SensingBlocks::registerBlocks(IEngine *engine)
 
     // Inputs
     engine->addInput(this, "DISTANCETOMENU", DISTANCETOMENU);
+    engine->addInput(this, "QUESTION", QUESTION);
     engine->addInput(this, "KEY_OPTION", KEY_OPTION);
     engine->addInput(this, "OBJECT", OBJECT);
 
@@ -77,6 +80,9 @@ void SensingBlocks::registerBlocks(IEngine *engine)
     engine->addFieldValue(this, "background #", BackdropNumber); // Scratch 1.4 support
     engine->addFieldValue(this, "backdrop #", BackdropNumber);
     engine->addFieldValue(this, "backdrop name", BackdropName);
+
+    // Callbacks
+    engine->setQuestionAnswered(&onAnswer);
 }
 
 void SensingBlocks::compileDistanceTo(Compiler *compiler)
@@ -100,6 +106,17 @@ void SensingBlocks::compileDistanceTo(Compiler *compiler)
     }
 }
 
+void SensingBlocks::compileAskAndWait(Compiler *compiler)
+{
+    compiler->addInput(QUESTION);
+    compiler->addFunctionCall(&askAndWait);
+}
+
+void SensingBlocks::compileAnswer(Compiler *compiler)
+{
+    compiler->addFunctionCall(&answer);
+}
+
 void SensingBlocks::compileKeyPressed(Compiler *compiler)
 {
     compiler->addInput(KEY_OPTION);
@@ -488,6 +505,45 @@ unsigned int SensingBlocks::distanceToMousePointer(VirtualMachine *vm)
     return 0;
 }
 
+void SensingBlocks::onAnswer(const std::string &answer)
+{
+    // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115
+    m_answer = answer;
+
+    if (!m_questionList.empty()) {
+        Question *question = m_questionList.front().get();
+        VirtualMachine *vm = question->vm;
+        assert(vm);
+        assert(vm->target());
+
+        // If the target was visible when asked, hide the say bubble unless the target was the stage
+        if (question->wasVisible && !question->wasStage)
+            vm->target()->setBubbleText("");
+
+        m_questionList.erase(m_questionList.begin());
+        vm->resolvePromise();
+        askNextQuestion();
+    }
+}
+
+unsigned int SensingBlocks::askAndWait(VirtualMachine *vm)
+{
+    const bool isQuestionAsked = !m_questionList.empty();
+    enqueueAsk(vm->getInput(0, 1)->toString(), vm);
+
+    if (!isQuestionAsked)
+        askNextQuestion();
+
+    vm->promise();
+    return 1;
+}
+
+unsigned int SensingBlocks::answer(VirtualMachine *vm)
+{
+    vm->addReturnValue(m_answer);
+    return 0;
+}
+
 unsigned int SensingBlocks::timer(VirtualMachine *vm)
 {
     vm->addReturnValue(vm->engine()->timer()->value());
@@ -822,3 +878,44 @@ unsigned int SensingBlocks::daysSince2000(VirtualMachine *vm)
 
     return 0;
 }
+
+void SensingBlocks::enqueueAsk(const std::string &question, VirtualMachine *vm)
+{
+    // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L117-L119
+    assert(vm);
+    Target *target = vm->target();
+    assert(target);
+    bool visible = true;
+    bool isStage = target->isStage();
+
+    if (!isStage) {
+        Sprite *sprite = static_cast<Sprite *>(target);
+        visible = sprite->visible();
+    }
+
+    m_questionList.push_back(std::make_unique<Question>(question, vm, visible, isStage));
+}
+
+void SensingBlocks::askNextQuestion()
+{
+    // https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L121-L133
+    if (m_questionList.empty())
+        return;
+
+    Question *question = m_questionList.front().get();
+    Target *target = question->vm->target();
+    auto ask = question->vm->engine()->questionAsked();
+
+    // If the target is visible, emit a blank question and show
+    // a bubble unless the target was the stage
+    if (question->wasVisible && !question->wasStage) {
+        target->setBubbleType(Target::BubbleType::Say);
+        target->setBubbleText(question->question);
+
+        if (ask)
+            ask("");
+    } else {
+        if (ask)
+            ask(question->question);
+    }
+}
diff --git a/src/blocks/sensingblocks.h b/src/blocks/sensingblocks.h
index b79e3ca6..01d8a608 100644
--- a/src/blocks/sensingblocks.h
+++ b/src/blocks/sensingblocks.h
@@ -3,11 +3,16 @@
 #pragma once
 
 #include <scratchcpp/iblocksection.h>
+#include <scratchcpp/value.h>
+#include <vector>
+
 #include "../engine/internal/clock.h"
 
 namespace libscratchcpp
 {
 
+class Target;
+
 /*! \brief The SensingBlocks class contains the implementation of sensing blocks. */
 class SensingBlocks : public IBlockSection
 {
@@ -15,6 +20,7 @@ class SensingBlocks : public IBlockSection
         enum Inputs
         {
             DISTANCETOMENU,
+            QUESTION,
             KEY_OPTION,
             OBJECT
         };
@@ -53,6 +59,8 @@ class SensingBlocks : public IBlockSection
         void registerBlocks(IEngine *engine) override;
 
         static void compileDistanceTo(Compiler *compiler);
+        static void compileAskAndWait(Compiler *compiler);
+        static void compileAnswer(Compiler *compiler);
         static void compileKeyPressed(Compiler *compiler);
         static void compileMouseDown(Compiler *compiler);
         static void compileMouseX(Compiler *compiler);
@@ -83,6 +91,10 @@ class SensingBlocks : public IBlockSection
         static unsigned int distanceToByIndex(VirtualMachine *vm);
         static unsigned int distanceToMousePointer(VirtualMachine *vm);
 
+        static void onAnswer(const std::string &answer);
+        static unsigned int askAndWait(VirtualMachine *vm);
+        static unsigned int answer(VirtualMachine *vm);
+
         static unsigned int timer(VirtualMachine *vm);
         static unsigned int resetTimer(VirtualMachine *vm);
 
@@ -116,6 +128,29 @@ class SensingBlocks : public IBlockSection
         static unsigned int daysSince2000(VirtualMachine *vm);
 
         static IClock *clock;
+
+    private:
+        struct Question
+        {
+                Question(const std::string &question, VirtualMachine *vm, bool wasVisible, bool wasStage) :
+                    question(question),
+                    vm(vm),
+                    wasVisible(wasVisible),
+                    wasStage(wasStage)
+                {
+                }
+
+                std::string question;
+                VirtualMachine *vm = nullptr;
+                bool wasVisible = false;
+                bool wasStage = false;
+        };
+
+        static void enqueueAsk(const std::string &question, VirtualMachine *vm);
+        static void askNextQuestion();
+
+        static inline std::vector<std::unique_ptr<Question>> m_questionList;
+        static inline Value m_answer;
 };
 
 } // namespace libscratchcpp
diff --git a/src/blocks/variableblocks.cpp b/src/blocks/variableblocks.cpp
index f9907dfb..668bd56b 100644
--- a/src/blocks/variableblocks.cpp
+++ b/src/blocks/variableblocks.cpp
@@ -5,6 +5,8 @@
 #include <scratchcpp/field.h>
 #include <scratchcpp/block.h>
 #include <scratchcpp/variable.h>
+#include <scratchcpp/stage.h>
+#include <scratchcpp/monitor.h>
 
 #include "variableblocks.h"
 
@@ -21,6 +23,8 @@ void VariableBlocks::registerBlocks(IEngine *engine)
     engine->addCompileFunction(this, "data_variable", &compileVariable);
     engine->addCompileFunction(this, "data_setvariableto", &compileSetVariable);
     engine->addCompileFunction(this, "data_changevariableby", &compileChangeVariableBy);
+    engine->addCompileFunction(this, "data_showvariable", &compileShowVariable);
+    engine->addCompileFunction(this, "data_hidevariable", &compileHideVariable);
 
     // Monitor names
     engine->addMonitorNameFunction(this, "data_variable", &variableMonitorName);
@@ -53,6 +57,84 @@ void VariableBlocks::compileChangeVariableBy(Compiler *compiler)
     compiler->addInstruction(vm::OP_CHANGE_VAR, { compiler->variableIndex(compiler->field(VARIABLE)->valuePtr()) });
 }
 
+void VariableBlocks::compileShowVariable(Compiler *compiler)
+{
+    Field *field = compiler->field(VARIABLE);
+    assert(field);
+    Variable *var = static_cast<Variable *>(field->valuePtr().get());
+    assert(var);
+
+    compiler->addConstValue(var->id());
+
+    if (var->target() == static_cast<Target *>(compiler->engine()->stage()))
+        compiler->addFunctionCall(&showGlobalVariable);
+    else
+        compiler->addFunctionCall(&showVariable);
+}
+
+void VariableBlocks::compileHideVariable(Compiler *compiler)
+{
+    Field *field = compiler->field(VARIABLE);
+    assert(field);
+    Variable *var = static_cast<Variable *>(field->valuePtr().get());
+    assert(var);
+
+    compiler->addConstValue(var->id());
+
+    if (var->target() == static_cast<Target *>(compiler->engine()->stage()))
+        compiler->addFunctionCall(&hideGlobalVariable);
+    else
+        compiler->addFunctionCall(&hideVariable);
+}
+
+void VariableBlocks::setVarVisible(std::shared_ptr<Variable> var, bool visible)
+{
+    if (var) {
+        assert(var->monitor());
+        var->monitor()->setVisible(visible);
+    }
+}
+
+unsigned int VariableBlocks::showGlobalVariable(VirtualMachine *vm)
+{
+    if (Stage *target = vm->engine()->stage()) {
+        int index = target->findVariableById(vm->getInput(0, 1)->toString());
+        setVarVisible(target->variableAt(index), true);
+    }
+
+    return 1;
+}
+
+unsigned int VariableBlocks::showVariable(VirtualMachine *vm)
+{
+    if (Target *target = vm->target()) {
+        int index = target->findVariableById(vm->getInput(0, 1)->toString());
+        setVarVisible(target->variableAt(index), true);
+    }
+
+    return 1;
+}
+
+unsigned int VariableBlocks::hideGlobalVariable(VirtualMachine *vm)
+{
+    if (Stage *target = vm->engine()->stage()) {
+        int index = target->findVariableById(vm->getInput(0, 1)->toString());
+        setVarVisible(target->variableAt(index), false);
+    }
+
+    return 1;
+}
+
+unsigned int VariableBlocks::hideVariable(VirtualMachine *vm)
+{
+    if (Target *target = vm->target()) {
+        int index = target->findVariableById(vm->getInput(0, 1)->toString());
+        setVarVisible(target->variableAt(index), false);
+    }
+
+    return 1;
+}
+
 const std::string &VariableBlocks::variableMonitorName(Block *block)
 {
     Variable *var = dynamic_cast<Variable *>(block->findFieldById(VARIABLE)->valuePtr().get());
diff --git a/src/blocks/variableblocks.h b/src/blocks/variableblocks.h
index 620cf35e..1d19a5be 100644
--- a/src/blocks/variableblocks.h
+++ b/src/blocks/variableblocks.h
@@ -8,6 +8,7 @@ namespace libscratchcpp
 {
 
 class Compiler;
+class Variable;
 
 /*! \brief The VariableBlocks class contains the implementation of variable blocks. */
 class VariableBlocks : public IBlockSection
@@ -30,6 +31,15 @@ class VariableBlocks : public IBlockSection
         static void compileVariable(Compiler *compiler);
         static void compileSetVariable(Compiler *compiler);
         static void compileChangeVariableBy(Compiler *compiler);
+        static void compileShowVariable(Compiler *compiler);
+        static void compileHideVariable(Compiler *compiler);
+
+        static void setVarVisible(std::shared_ptr<Variable> var, bool visible);
+
+        static unsigned int showGlobalVariable(VirtualMachine *vm);
+        static unsigned int showVariable(VirtualMachine *vm);
+        static unsigned int hideGlobalVariable(VirtualMachine *vm);
+        static unsigned int hideVariable(VirtualMachine *vm);
 
         static const std::string &variableMonitorName(Block *block);
         static void changeVariableMonitorValue(Block *block, const Value &newValue);
diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp
index ac11c763..8734cc41 100644
--- a/src/engine/internal/engine.cpp
+++ b/src/engine/internal/engine.cpp
@@ -34,11 +34,8 @@
 using namespace libscratchcpp;
 
 const std::unordered_map<Engine::HatType, bool> Engine::m_hatRestartExistingThreads = {
-    { HatType::GreenFlag, true },
-    { HatType::BroadcastReceived, true },
-    { HatType::BackdropChanged, true },
-    { HatType::CloneInit, false },
-    { HatType::KeyPressed, false }
+    { HatType::GreenFlag, true },  { HatType::BroadcastReceived, true }, { HatType::BackdropChanged, true },
+    { HatType::CloneInit, false }, { HatType::KeyPressed, false },       { HatType::TargetClicked, true }
 };
 
 Engine::Engine() :
@@ -138,7 +135,7 @@ void Engine::resolveIds()
         if (container)
             block->setCompileFunction(container->resolveBlockCompileFunc(block->opcode()));
 
-        monitor->impl->blockSection = blockSection(monitor->opcode());
+        monitor->setBlockSection(blockSection(monitor->opcode()));
 
         const auto &fields = block->fields();
         Target *target;
@@ -161,10 +158,35 @@ void Engine::resolveIds()
             }
 
             // TODO: Move field information out of Engine
-            if (field->name() == "VARIABLE")
-                field->setValuePtr(target->variableAt(target->findVariable(field->value().toString())));
-            else if (field->name() == "LIST")
-                field->setValuePtr(target->listAt(target->findList(field->value().toString())));
+            if (field->name() == "VARIABLE") {
+                std::string name = field->value().toString();
+                int index = target->findVariable(name);
+                std::shared_ptr<Variable> var;
+
+                if (index == -1) {
+                    var = std::make_shared<Variable>(monitor->id(), name);
+                    index = target->addVariable(var);
+                } else
+                    var = target->variableAt(index);
+
+                field->setValuePtr(target->variableAt(index));
+                assert(var);
+                var->setMonitor(monitor.get());
+            } else if (field->name() == "LIST") {
+                std::string name = field->value().toString();
+                int index = target->findList(name);
+                std::shared_ptr<List> list;
+
+                if (index == -1) {
+                    list = std::make_shared<List>(monitor->id(), name);
+                    index = target->addList(list);
+                } else
+                    list = target->listAt(index);
+
+                field->setValuePtr(target->listAt(index));
+                assert(list);
+                list->setMonitor(monitor.get());
+            }
         }
 
         block->updateInputMap();
@@ -232,7 +254,7 @@ void Engine::compile()
             MonitorNameFunc nameFunc = container->resolveMonitorNameFunc(block->opcode());
 
             if (nameFunc)
-                monitor->impl->name = nameFunc(block.get());
+                monitor->setName(nameFunc(block.get()));
 
             MonitorChangeFunc changeFunc = container->resolveMonitorChangeFunc(block->opcode());
             monitor->setValueChangeFunction(changeFunc);
@@ -629,6 +651,18 @@ void Engine::setAnyKeyPressed(bool pressed)
         startHats(HatType::KeyPressed, { { HatField::KeyOption, "any" } }, nullptr);
 }
 
+void Engine::mouseWheelUp()
+{
+    // Start "when up arrow pressed" scripts
+    startHats(HatType::KeyPressed, { { HatField::KeyOption, "up arrow" } }, nullptr);
+}
+
+void Engine::mouseWheelDown()
+{
+    // Start "when down arrow pressed" scripts
+    startHats(HatType::KeyPressed, { { HatField::KeyOption, "down arrow" } }, nullptr);
+}
+
 double Engine::mouseX() const
 {
     return m_mouseX;
@@ -661,7 +695,7 @@ void Engine::setMousePressed(bool pressed)
 
 void Engine::clickTarget(Target *target)
 {
-    // TODO: Implement this (#92, #93)
+    startHats(HatType::TargetClicked, {}, target);
 }
 
 unsigned int Engine::stageWidth() const
@@ -944,6 +978,11 @@ void Engine::addKeyPressScript(std::shared_ptr<Block> hatBlock, int fieldId)
     addHatField(script, HatField::KeyOption, fieldId);
 }
 
+void Engine::addTargetClickScript(std::shared_ptr<Block> hatBlock)
+{
+    addHatToMap(m_whenTargetClickedHats, m_scripts[hatBlock].get());
+}
+
 const std::vector<std::shared_ptr<Target>> &Engine::targets() const
 {
     return m_targets;
@@ -1131,6 +1170,26 @@ void Engine::setRemoveMonitorHandler(const std::function<void(Monitor *, IMonito
     m_removeMonitorHandler = handler;
 }
 
+const std::function<void(const std::string &)> &Engine::questionAsked() const
+{
+    return m_questionAsked;
+}
+
+void Engine::setQuestionAsked(const std::function<void(const std::string &)> &f)
+{
+    m_questionAsked = f;
+}
+
+const std::function<void(const std::string &)> &Engine::questionAnswered() const
+{
+    return m_questionAnswered;
+}
+
+void Engine::setQuestionAnswered(const std::function<void(const std::string &)> &f)
+{
+    m_questionAnswered = f;
+}
+
 const std::vector<std::string> &Engine::extensions() const
 {
     return m_extensions;
@@ -1329,6 +1388,9 @@ const std::vector<Script *> &Engine::getHats(Target *target, HatType type)
         case HatType::KeyPressed:
             return m_whenKeyPressedHats[target];
 
+        case HatType::TargetClicked:
+            return m_whenTargetClickedHats[target];
+
         default: {
             static const std::vector<Script *> empty = {};
             return empty;
@@ -1368,6 +1430,16 @@ BlockSectionContainer *Engine::blockSectionContainer(IBlockSection *section) con
     return nullptr;
 }
 
+const std::string &Engine::userAgent() const
+{
+    return m_userAgent;
+}
+
+void Engine::setUserAgent(const std::string &agent)
+{
+    m_userAgent = agent;
+}
+
 void Engine::finalize()
 {
     m_eventLoopMutex.lock();
@@ -1465,14 +1537,14 @@ void Engine::addVarOrListMonitor(std::shared_ptr<Monitor> monitor, Target *targe
     if (!target->isStage())
         monitor->setSprite(dynamic_cast<Sprite *>(target));
 
-    monitor->impl->blockSection = blockSection(monitor->opcode());
+    monitor->setBlockSection(blockSection(monitor->opcode()));
     auto container = blockSectionContainer(monitor->opcode());
 
     if (container) {
         MonitorNameFunc nameFunc = container->resolveMonitorNameFunc(monitor->opcode());
 
         if (nameFunc)
-            monitor->impl->name = nameFunc(monitor->block().get());
+            monitor->setName(nameFunc(monitor->block().get()));
 
         MonitorChangeFunc changeFunc = container->resolveMonitorChangeFunc(monitor->opcode());
         monitor->setValueChangeFunction(changeFunc);
diff --git a/src/engine/internal/engine.h b/src/engine/internal/engine.h
index fc945e9a..26800535 100644
--- a/src/engine/internal/engine.h
+++ b/src/engine/internal/engine.h
@@ -64,6 +64,9 @@ class Engine : public IEngine
         void setKeyState(const KeyEvent &event, bool pressed) override;
         void setAnyKeyPressed(bool pressed) override;
 
+        void mouseWheelUp() override;
+        void mouseWheelDown() override;
+
         double mouseX() const override;
         void setMouseX(double x) override;
 
@@ -120,6 +123,7 @@ class Engine : public IEngine
         void addBackdropChangeScript(std::shared_ptr<Block> hatBlock, int fieldId) override;
         void addCloneInitScript(std::shared_ptr<Block> hatBlock) override;
         void addKeyPressScript(std::shared_ptr<Block> hatBlock, int fieldId) override;
+        void addTargetClickScript(std::shared_ptr<Block> hatBlock) override;
 
         const std::vector<std::shared_ptr<Target>> &targets() const override;
         void setTargets(const std::vector<std::shared_ptr<Target>> &newTargets) override;
@@ -139,6 +143,12 @@ class Engine : public IEngine
         void setAddMonitorHandler(const std::function<void(Monitor *)> &handler) override;
         void setRemoveMonitorHandler(const std::function<void(Monitor *, IMonitorHandler *)> &handler) override;
 
+        const std::function<void(const std::string &)> &questionAsked() const override;
+        void setQuestionAsked(const std::function<void(const std::string &)> &f) override;
+
+        const std::function<void(const std::string &)> &questionAnswered() const override;
+        void setQuestionAnswered(const std::function<void(const std::string &)> &f) override;
+
         const std::vector<std::string> &extensions() const override;
         void setExtensions(const std::vector<std::string> &newExtensions) override;
 
@@ -147,6 +157,9 @@ class Engine : public IEngine
         BlockSectionContainer *blockSectionContainer(const std::string &opcode) const;
         BlockSectionContainer *blockSectionContainer(IBlockSection *section) const;
 
+        const std::string &userAgent() const override;
+        void setUserAgent(const std::string &agent) override;
+
         IClock *m_clock = nullptr;
 
     private:
@@ -156,7 +169,8 @@ class Engine : public IEngine
             BroadcastReceived,
             BackdropChanged,
             CloneInit,
-            KeyPressed
+            KeyPressed,
+            TargetClicked
         };
 
         enum class HatField
@@ -216,12 +230,14 @@ class Engine : public IEngine
         std::unordered_map<std::shared_ptr<Block>, std::shared_ptr<Script>> m_scripts;
         std::vector<BlockFunc> m_functions;
         std::recursive_mutex m_eventLoopMutex;
+        std::string m_userAgent;
 
         std::unordered_map<Target *, std::vector<Script *>> m_greenFlagHats;
         std::unordered_map<Target *, std::vector<Script *>> m_backdropChangeHats;
         std::unordered_map<Target *, std::vector<Script *>> m_broadcastHats;
         std::unordered_map<Target *, std::vector<Script *>> m_cloneInitHats;
         std::unordered_map<Target *, std::vector<Script *>> m_whenKeyPressedHats;
+        std::unordered_map<Target *, std::vector<Script *>> m_whenTargetClickedHats;
 
         std::unordered_map<Script *, std::unordered_map<HatField, int>> m_scriptHatFields; // HatField, field ID from the block implementation
 
@@ -249,6 +265,8 @@ class Engine : public IEngine
 
         std::function<void(Monitor *)> m_addMonitorHandler = nullptr;
         std::function<void(Monitor *, IMonitorHandler *)> m_removeMonitorHandler = nullptr;
+        std::function<void(const std::string &)> m_questionAsked = nullptr;
+        std::function<void(const std::string &)> m_questionAnswered = nullptr;
 };
 
 } // namespace libscratchcpp
diff --git a/src/engine/virtualmachine.cpp b/src/engine/virtualmachine.cpp
index f066e46e..98b12248 100644
--- a/src/engine/virtualmachine.cpp
+++ b/src/engine/virtualmachine.cpp
@@ -166,6 +166,9 @@ void VirtualMachine::run()
 {
     impl->running = true;
 
+    if (impl->promisePending)
+        return;
+
     unsigned int *ret = impl->run(impl->pos);
     assert(ret);
 
@@ -187,6 +190,7 @@ void VirtualMachine::reset()
 {
     impl->pos = impl->bytecode;
     impl->atEnd = false;
+    impl->promisePending = false;
 
     if (!impl->running) // Registers will be freed when the script stops running
         impl->regCount = 0;
@@ -216,6 +220,22 @@ void VirtualMachine::stop(bool savePos, bool breakFrame, bool goBack)
     impl->goBack = goBack;
 }
 
+/*!
+ * Use this to pause the execution of the script.
+ * The execution will continue after calling resolvePromise()
+ */
+void VirtualMachine::promise()
+{
+    impl->promisePending = true;
+    stop();
+}
+
+/*! Resolves the promise so that the VM can continue with the execution. */
+void VirtualMachine::resolvePromise()
+{
+    impl->promisePending = false;
+}
+
 /*! Returns true if the VM has reached the vm::OP_HALT instruction. */
 bool VirtualMachine::atEnd() const
 {
diff --git a/src/engine/virtualmachine_p.cpp b/src/engine/virtualmachine_p.cpp
index 92c381f0..d6c93591 100644
--- a/src/engine/virtualmachine_p.cpp
+++ b/src/engine/virtualmachine_p.cpp
@@ -10,7 +10,14 @@
 #include "virtualmachine_p.h"
 #include "internal/randomgenerator.h"
 
+#ifdef ENABLE_COMPUTED_GOTO
 #define DISPATCH() goto *dispatch_table[*++pos]
+#define OP(opcode) DO_##opcode
+#else
+#define DISPATCH() break
+#define OP(opcode) case Opcode::OP_##opcode
+#endif
+
 #define FREE_REGS(count) regCount -= count
 #define ADD_RET_VALUE(value)                                                                                                                                                                           \
     if (regCount + 1 >= regsVector.size()) {                                                                                                                                                           \
@@ -127,71 +134,74 @@ VirtualMachinePrivate::~VirtualMachinePrivate()
 
 unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
 {
+#ifdef ENABLE_COMPUTED_GOTO
     static const void *dispatch_table[] = {
         nullptr,
-        &&do_halt,
-        &&do_const,
-        &&do_null,
-        &&do_checkpoint,
-        &&do_if,
-        &&do_else,
-        &&do_endif,
-        &&do_forever_loop,
-        &&do_repeat_loop,
-        &&do_repeat_loop_index,
-        &&do_repeat_loop_index1,
-        &&do_until_loop,
-        &&do_begin_until_loop,
-        &&do_loop_end,
-        &&do_print,
-        &&do_add,
-        &&do_subtract,
-        &&do_multiply,
-        &&do_divide,
-        &&do_mod,
-        &&do_random,
-        &&do_round,
-        &&do_abs,
-        &&do_floor,
-        &&do_ceil,
-        &&do_sqrt,
-        &&do_sin,
-        &&do_cos,
-        &&do_tan,
-        &&do_asin,
-        &&do_acos,
-        &&do_atan,
-        &&do_greater_than,
-        &&do_less_than,
-        &&do_equals,
-        &&do_and,
-        &&do_or,
-        &&do_not,
-        &&do_set_var,
-        &&do_change_var,
-        &&do_read_var,
-        &&do_read_list,
-        &&do_list_append,
-        &&do_list_del,
-        &&do_list_del_all,
-        &&do_list_insert,
-        &&do_list_replace,
-        &&do_list_get_item,
-        &&do_list_index_of,
-        &&do_list_length,
-        &&do_list_contains,
-        &&do_str_concat,
-        &&do_str_at,
-        &&do_str_length,
-        &&do_str_contains,
-        &&do_exec,
-        &&do_init_procedure,
-        &&do_call_procedure,
-        &&do_add_arg,
-        &&do_read_arg,
-        &&do_break_frame,
-        &&do_warp
+        &&DO_HALT,
+        &&DO_CONST,
+        &&DO_NULL,
+        &&DO_CHECKPOINT,
+        &&DO_IF,
+        &&DO_ELSE,
+        &&DO_ENDIF,
+        &&DO_FOREVER_LOOP,
+        &&DO_REPEAT_LOOP,
+        &&DO_REPEAT_LOOP_INDEX,
+        &&DO_REPEAT_LOOP_INDEX1,
+        &&DO_UNTIL_LOOP,
+        &&DO_BEGIN_UNTIL_LOOP,
+        &&DO_LOOP_END,
+        &&DO_PRINT,
+        &&DO_ADD,
+        &&DO_SUBTRACT,
+        &&DO_MULTIPLY,
+        &&DO_DIVIDE,
+        &&DO_MOD,
+        &&DO_RANDOM,
+        &&DO_ROUND,
+        &&DO_ABS,
+        &&DO_FLOOR,
+        &&DO_CEIL,
+        &&DO_SQRT,
+        &&DO_SIN,
+        &&DO_COS,
+        &&DO_TAN,
+        &&DO_ASIN,
+        &&DO_ACOS,
+        &&DO_ATAN,
+        &&DO_GREATER_THAN,
+        &&DO_LESS_THAN,
+        &&DO_EQUALS,
+        &&DO_AND,
+        &&DO_OR,
+        &&DO_NOT,
+        &&DO_SET_VAR,
+        &&DO_CHANGE_VAR,
+        &&DO_READ_VAR,
+        &&DO_READ_LIST,
+        &&DO_LIST_APPEND,
+        &&DO_LIST_DEL,
+        &&DO_LIST_DEL_ALL,
+        &&DO_LIST_INSERT,
+        &&DO_LIST_REPLACE,
+        &&DO_LIST_GET_ITEM,
+        &&DO_LIST_INDEX_OF,
+        &&DO_LIST_LENGTH,
+        &&DO_LIST_CONTAINS,
+        &&DO_STR_CONCAT,
+        &&DO_STR_AT,
+        &&DO_STR_LENGTH,
+        &&DO_STR_CONTAINS,
+        &&DO_EXEC,
+        &&DO_INIT_PROCEDURE,
+        &&DO_CALL_PROCEDURE,
+        &&DO_ADD_ARG,
+        &&DO_READ_ARG,
+        &&DO_BREAK_FRAME,
+        &&DO_WARP
     };
+#endif // ENABLE_COMPUTED_GOTO
+
     assert(pos);
     unsigned int *loopStart;
     unsigned int *loopEnd;
@@ -201,20 +211,26 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
         noBreak = true;
         warp = false;
     }
+
+#ifdef ENABLE_COMPUTED_GOTO
     DISPATCH();
+#else
+    while (true)
+        switch (*++pos) {
+#endif
 
-do_halt:
-    if (regCount > 0) {
+    OP(HALT) :
+        if (regCount > 0)
+    {
         std::cout << "warning: VM: " << regCount << " registers were leaked by the script; this is most likely a bug in the VM or in the compiler" << std::endl;
     }
     if (callTree.empty()) {
         atEnd = true;
         return pos;
     } else {
-        if (callTree.size() == 1)
-            warp = false;
-
-        pos = callTree.back();
+        const auto &oldState = callTree.back();
+        pos = oldState.first;
+        warp = oldState.second;
         callTree.pop_back();
         procedureArgTree.pop_back();
         if (procedureArgTree.empty())
@@ -224,22 +240,41 @@ unsigned int *VirtualMachinePrivate::run(unsigned int *pos, bool reset)
         DISPATCH();
     }
 
-do_const:
-    ADD_RET_VALUE(GET_NEXT_ARG());
+    OP(CONST) :
+        ADD_RET_VALUE(GET_NEXT_ARG());
     DISPATCH();
 
-do_null:
-    ADD_RET_VALUE(Value());
+    OP(NULL) :
+        ADD_RET_VALUE(Value());
     DISPATCH();
 
-do_checkpoint:
-    checkpoint = pos - 1;
+    OP(CHECKPOINT) :
+        checkpoint = pos - 1;
     DISPATCH();
 
-do_if : {
-    if (!READ_LAST_REG()->toBool()) {
+    OP(IF) :
+    {
+        if (!READ_LAST_REG()->toBool()) {
+            unsigned int ifCounter = 1;
+            while (!((*pos == OP_ELSE && ifCounter == 1) || (*pos == OP_ENDIF && ifCounter == 0))) {
+                pos += instruction_arg_count[*pos++];
+
+                if ((*pos == OP_IF) || (*pos == OP_FOREVER_LOOP) || (*pos == OP_REPEAT_LOOP) || (*pos == OP_UNTIL_LOOP))
+                    ifCounter++;
+                else if ((*pos == OP_ENDIF) || (*pos == OP_LOOP_END)) {
+                    assert(ifCounter > 0);
+                    ifCounter--;
+                }
+            }
+        }
+        FREE_REGS(1);
+        DISPATCH();
+    }
+
+    OP(ELSE) :
+    {
         unsigned int ifCounter = 1;
-        while (!((*pos == OP_ELSE && ifCounter == 1) || (*pos == OP_ENDIF && ifCounter == 0))) {
+        while (!(*pos == OP_ENDIF && ifCounter == 0)) {
             pos += instruction_arg_count[*pos++];
 
             if ((*pos == OP_IF) || (*pos == OP_FOREVER_LOOP) || (*pos == OP_REPEAT_LOOP) || (*pos == OP_UNTIL_LOOP))
@@ -248,41 +283,24 @@ do_if : {
                 assert(ifCounter > 0);
                 ifCounter--;
             }
-        }
-    }
-    FREE_REGS(1);
-    DISPATCH();
-}
-
-do_else : {
-    unsigned int ifCounter = 1;
-    while (!(*pos == OP_ENDIF && ifCounter == 0)) {
-        pos += instruction_arg_count[*pos++];
 
-        if ((*pos == OP_IF) || (*pos == OP_FOREVER_LOOP) || (*pos == OP_REPEAT_LOOP) || (*pos == OP_UNTIL_LOOP))
-            ifCounter++;
-        else if ((*pos == OP_ENDIF) || (*pos == OP_LOOP_END)) {
-            assert(ifCounter > 0);
-            ifCounter--;
+            assert(!(*pos == OP_ELSE && ifCounter == 1));
         }
-
-        assert(!(*pos == OP_ELSE && ifCounter == 1));
     }
-}
 
-do_endif:
-    DISPATCH();
+    OP(ENDIF) :
+        DISPATCH();
 
-do_forever_loop:
-    Loop l;
+    OP(FOREVER_LOOP) :
+        Loop l;
     l.isRepeatLoop = true;
     l.start = pos;
     l.index = -1;
     loops.push_back(l);
     DISPATCH();
 
-do_repeat_loop:
-    loopCount = std::round(READ_LAST_REG()->toDouble());
+    OP(REPEAT_LOOP) :
+        loopCount = std::round(READ_LAST_REG()->toDouble());
     FREE_REGS(1);
     if (loopCount <= 0) {
         loopEnd = pos;
@@ -308,24 +326,26 @@ do_else : {
     }
     DISPATCH();
 
-do_repeat_loop_index : {
-    assert(!loops.empty());
-    Loop &l = loops.back();
-    assert(l.isRepeatLoop);
-    ADD_RET_VALUE(static_cast<long>(l.index));
-    DISPATCH();
-}
+    OP(REPEAT_LOOP_INDEX) :
+    {
+        assert(!loops.empty());
+        Loop &l = loops.back();
+        assert(l.isRepeatLoop);
+        ADD_RET_VALUE(static_cast<long>(l.index));
+        DISPATCH();
+    }
 
-do_repeat_loop_index1 : {
-    assert(!loops.empty());
-    Loop &l = loops.back();
-    assert(l.isRepeatLoop);
-    ADD_RET_VALUE(static_cast<long>(l.index + 1));
-    DISPATCH();
-}
+    OP(REPEAT_LOOP_INDEX1) :
+    {
+        assert(!loops.empty());
+        Loop &l = loops.back();
+        assert(l.isRepeatLoop);
+        ADD_RET_VALUE(static_cast<long>(l.index + 1));
+        DISPATCH();
+    }
 
-do_until_loop:
-    loopStart = run(pos, false);
+    OP(UNTIL_LOOP) :
+        loopStart = run(pos, false);
     if (!READ_LAST_REG()->toBool()) {
         Loop l;
         l.isRepeatLoop = false;
@@ -349,440 +369,460 @@ do_repeat_loop_index1 : {
     FREE_REGS(1);
     DISPATCH();
 
-do_begin_until_loop:
-    return pos;
+    OP(BEGIN_UNTIL_LOOP) :
+        return pos;
 
-do_loop_end : {
-    assert(!loops.empty());
-    Loop &l = loops.back();
-    if (l.isRepeatLoop) {
-        if ((l.index == -1) || (++l.index < l.max))
-            pos = l.start;
-        else
-            loops.pop_back();
-        if (!noBreak && !warp)
-            return pos;
-        DISPATCH();
-    } else {
-        if (!noBreak && !warp)
-            return pos - 1;
-        loopStart = run(l.start, false);
-        if (!READ_LAST_REG()->toBool())
-            pos = loopStart;
-        else
-            loops.pop_back();
-        FREE_REGS(1);
-        DISPATCH();
+    OP(LOOP_END) :
+    {
+        assert(!loops.empty());
+        Loop &l = loops.back();
+        if (l.isRepeatLoop) {
+            if ((l.index == -1) || (++l.index < l.max))
+                pos = l.start;
+            else
+                loops.pop_back();
+            if (!noBreak && !warp)
+                return pos;
+            DISPATCH();
+        } else {
+            if (!noBreak && !warp)
+                return pos - 1;
+            loopStart = run(l.start, false);
+            if (!READ_LAST_REG()->toBool())
+                pos = loopStart;
+            else
+                loops.pop_back();
+            FREE_REGS(1);
+            DISPATCH();
+        }
     }
-}
 
-do_print:
-    std::cout << READ_LAST_REG()->toString() << std::endl;
+    OP(PRINT) :
+        std::cout << READ_LAST_REG()->toString() << std::endl;
     FREE_REGS(1);
     DISPATCH();
 
-do_add:
-    READ_REG(0, 2)->add(*READ_REG(1, 2));
+    OP(ADD) :
+        READ_REG(0, 2)->add(*READ_REG(1, 2));
     FREE_REGS(1);
     DISPATCH();
 
-do_subtract:
-    READ_REG(0, 2)->subtract(*READ_REG(1, 2));
+    OP(SUBTRACT) :
+        READ_REG(0, 2)->subtract(*READ_REG(1, 2));
     FREE_REGS(1);
     DISPATCH();
 
-do_multiply:
-    READ_REG(0, 2)->multiply(*READ_REG(1, 2));
+    OP(MULTIPLY) :
+        READ_REG(0, 2)->multiply(*READ_REG(1, 2));
     FREE_REGS(1);
     DISPATCH();
 
-do_divide:
-    READ_REG(0, 2)->divide(*READ_REG(1, 2));
+    OP(DIVIDE) :
+        READ_REG(0, 2)->divide(*READ_REG(1, 2));
     FREE_REGS(1);
     DISPATCH();
 
-do_mod:
-    READ_REG(0, 2)->mod(*READ_REG(1, 2));
+    OP(MOD) :
+        READ_REG(0, 2)->mod(*READ_REG(1, 2));
     FREE_REGS(1);
     DISPATCH();
 
-do_random:
-    if ((READ_REG(0, 2)->type() == Value::Type::Integer) && (READ_REG(1, 2)->type() == Value::Type::Integer))
-        REPLACE_RET_VALUE(rng->randint(READ_REG(0, 2)->toInt(), READ_REG(1, 2)->toInt()), 2);
-    else
-        REPLACE_RET_VALUE(rng->randintDouble(READ_REG(0, 2)->toDouble(), READ_REG(1, 2)->toDouble()), 2);
+    OP(RANDOM) :
+        if ((READ_REG(0, 2)->type() == Value::Type::Integer) && (READ_REG(1, 2)->type() == Value::Type::Integer)) REPLACE_RET_VALUE(rng->randint(READ_REG(0, 2)->toInt(), READ_REG(1, 2)->toInt()), 2);
+    else REPLACE_RET_VALUE(rng->randintDouble(READ_REG(0, 2)->toDouble(), READ_REG(1, 2)->toDouble()), 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_round : {
-    const Value *v = READ_REG(0, 1);
-    if (!v->isInfinity() && !v->isNegativeInfinity()) {
-        if (v->toDouble() < 0) {
-            REPLACE_RET_VALUE(static_cast<long>(std::floor(v->toDouble() + 0.5)), 1);
-        } else
-            REPLACE_RET_VALUE(static_cast<long>(v->toDouble() + 0.5), 1);
+    OP(ROUND) :
+    {
+        const Value *v = READ_REG(0, 1);
+        if (!v->isInfinity() && !v->isNegativeInfinity()) {
+            if (v->toDouble() < 0) {
+                REPLACE_RET_VALUE(static_cast<long>(std::floor(v->toDouble() + 0.5)), 1);
+            } else
+                REPLACE_RET_VALUE(static_cast<long>(v->toDouble() + 0.5), 1);
+        }
+        DISPATCH();
     }
-    DISPATCH();
-}
-
-do_abs : {
-    const Value *v = READ_REG(0, 1);
-    if (v->isNegativeInfinity())
-        REPLACE_RET_VALUE(Value(Value::SpecialValue::Infinity), 1);
-    else if (!v->isInfinity())
-        REPLACE_RET_VALUE(std::abs(v->toDouble()), 1);
-    DISPATCH();
-}
 
-do_floor : {
-    const Value *v = READ_REG(0, 1);
-    if (!v->isInfinity() && !v->isNegativeInfinity())
-        REPLACE_RET_VALUE(std::floor(v->toDouble()), 1);
-    DISPATCH();
-}
-
-do_ceil : {
-    const Value *v = READ_REG(0, 1);
-    if (!v->isInfinity() && !v->isNegativeInfinity())
-        REPLACE_RET_VALUE(std::ceil(v->toDouble()), 1);
-    DISPATCH();
-}
+    OP(ABS) :
+    {
+        const Value *v = READ_REG(0, 1);
+        if (v->isNegativeInfinity())
+            REPLACE_RET_VALUE(Value(Value::SpecialValue::Infinity), 1);
+        else if (!v->isInfinity())
+            REPLACE_RET_VALUE(std::abs(v->toDouble()), 1);
+        DISPATCH();
+    }
 
-do_sqrt : {
-    const Value &v = *READ_REG(0, 1);
-    if (v < 0)
-        REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
-    else if (!v.isInfinity())
-        REPLACE_RET_VALUE(std::sqrt(v.toDouble()), 1);
-    DISPATCH();
-}
+    OP(FLOOR) :
+    {
+        const Value *v = READ_REG(0, 1);
+        if (!v->isInfinity() && !v->isNegativeInfinity())
+            REPLACE_RET_VALUE(std::floor(v->toDouble()), 1);
+        DISPATCH();
+    }
 
-do_sin : {
-    const Value *v = READ_REG(0, 1);
-    if (v->isInfinity() || v->isNegativeInfinity())
-        REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
-    else
-        REPLACE_RET_VALUE(std::sin(v->toDouble() * pi / 180), 1);
-    DISPATCH();
-}
+    OP(CEIL) :
+    {
+        const Value *v = READ_REG(0, 1);
+        if (!v->isInfinity() && !v->isNegativeInfinity())
+            REPLACE_RET_VALUE(std::ceil(v->toDouble()), 1);
+        DISPATCH();
+    }
 
-do_cos : {
-    const Value *v = READ_REG(0, 1);
-    if (v->isInfinity() || v->isNegativeInfinity())
-        REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
-    else
-        REPLACE_RET_VALUE(std::cos(v->toDouble() * pi / 180), 1);
-    DISPATCH();
-}
+    OP(SQRT) :
+    {
+        const Value &v = *READ_REG(0, 1);
+        if (v < 0)
+            REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
+        else if (!v.isInfinity())
+            REPLACE_RET_VALUE(std::sqrt(v.toDouble()), 1);
+        DISPATCH();
+    }
 
-do_tan : {
-    const Value *v = READ_REG(0, 1);
-    if (v->isInfinity() || v->isNegativeInfinity())
-        REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
-    else {
-        long mod;
-        if (v->toLong() < 0)
-            mod = (v->toLong() + 360) % 360;
+    OP(SIN) :
+    {
+        const Value *v = READ_REG(0, 1);
+        if (v->isInfinity() || v->isNegativeInfinity())
+            REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
         else
-            mod = v->toLong() % 360;
-        if (mod == 90)
-            REPLACE_RET_VALUE(Value(Value::SpecialValue::Infinity), 1);
-        else if (mod == 270)
-            REPLACE_RET_VALUE(Value(Value::SpecialValue::NegativeInfinity), 1);
+            REPLACE_RET_VALUE(std::sin(v->toDouble() * pi / 180), 1);
+        DISPATCH();
+    }
+
+    OP(COS) :
+    {
+        const Value *v = READ_REG(0, 1);
+        if (v->isInfinity() || v->isNegativeInfinity())
+            REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
         else
-            REPLACE_RET_VALUE(std::tan(v->toDouble() * pi / 180), 1);
+            REPLACE_RET_VALUE(std::cos(v->toDouble() * pi / 180), 1);
+        DISPATCH();
     }
-    DISPATCH();
-}
 
-do_asin : {
-    const Value &v = *READ_REG(0, 1);
-    if (v < -1 || v > 1)
-        REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
-    else
-        REPLACE_RET_VALUE(std::asin(v.toDouble()) * 180 / pi, 1);
-    DISPATCH();
-}
+    OP(TAN) :
+    {
+        const Value *v = READ_REG(0, 1);
+        if (v->isInfinity() || v->isNegativeInfinity())
+            REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
+        else {
+            long mod;
+            if (v->toLong() < 0)
+                mod = (v->toLong() + 360) % 360;
+            else
+                mod = v->toLong() % 360;
+            if (mod == 90)
+                REPLACE_RET_VALUE(Value(Value::SpecialValue::Infinity), 1);
+            else if (mod == 270)
+                REPLACE_RET_VALUE(Value(Value::SpecialValue::NegativeInfinity), 1);
+            else
+                REPLACE_RET_VALUE(std::tan(v->toDouble() * pi / 180), 1);
+        }
+        DISPATCH();
+    }
 
-do_acos : {
-    const Value &v = *READ_REG(0, 1);
-    if (v < -1 || v > 1)
-        REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
-    else
-        REPLACE_RET_VALUE(std::acos(v.toDouble()) * 180 / pi, 1);
-    DISPATCH();
-}
+    OP(ASIN) :
+    {
+        const Value &v = *READ_REG(0, 1);
+        if (v < -1 || v > 1)
+            REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
+        else
+            REPLACE_RET_VALUE(std::asin(v.toDouble()) * 180 / pi, 1);
+        DISPATCH();
+    }
 
-do_atan : {
-    const Value &v = *READ_REG(0, 1);
-    if (v.isInfinity())
-        REPLACE_RET_VALUE(90, 1);
-    else if (v.isNegativeInfinity())
-        REPLACE_RET_VALUE(-90, 1);
-    else
-        REPLACE_RET_VALUE(std::atan(v.toDouble()) * 180 / pi, 1);
-    DISPATCH();
-}
+    OP(ACOS) :
+    {
+        const Value &v = *READ_REG(0, 1);
+        if (v < -1 || v > 1)
+            REPLACE_RET_VALUE(Value(Value::SpecialValue::NaN), 1);
+        else
+            REPLACE_RET_VALUE(std::acos(v.toDouble()) * 180 / pi, 1);
+        DISPATCH();
+    }
+
+    OP(ATAN) :
+    {
+        const Value &v = *READ_REG(0, 1);
+        if (v.isInfinity())
+            REPLACE_RET_VALUE(90, 1);
+        else if (v.isNegativeInfinity())
+            REPLACE_RET_VALUE(-90, 1);
+        else
+            REPLACE_RET_VALUE(std::atan(v.toDouble()) * 180 / pi, 1);
+        DISPATCH();
+    }
 
-do_greater_than:
-    REPLACE_RET_VALUE(*READ_REG(0, 2) > *READ_REG(1, 2), 2);
+    OP(GREATER_THAN) :
+        REPLACE_RET_VALUE(*READ_REG(0, 2) > *READ_REG(1, 2), 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_less_than:
-    REPLACE_RET_VALUE(*READ_REG(0, 2) < *READ_REG(1, 2), 2);
+    OP(LESS_THAN) :
+        REPLACE_RET_VALUE(*READ_REG(0, 2) < *READ_REG(1, 2), 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_equals:
-    REPLACE_RET_VALUE(*READ_REG(0, 2) == *READ_REG(1, 2), 2);
+    OP(EQUALS) :
+        REPLACE_RET_VALUE(*READ_REG(0, 2) == *READ_REG(1, 2), 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_and:
-    REPLACE_RET_VALUE(READ_REG(0, 2)->toBool() && READ_REG(1, 2)->toBool(), 2);
+    OP(AND) :
+        REPLACE_RET_VALUE(READ_REG(0, 2)->toBool() && READ_REG(1, 2)->toBool(), 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_or:
-    REPLACE_RET_VALUE(READ_REG(0, 2)->toBool() || READ_REG(1, 2)->toBool(), 2);
+    OP(OR) :
+        REPLACE_RET_VALUE(READ_REG(0, 2)->toBool() || READ_REG(1, 2)->toBool(), 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_not:
-    REPLACE_RET_VALUE(!READ_LAST_REG()->toBool(), 1);
+    OP(NOT) :
+        REPLACE_RET_VALUE(!READ_LAST_REG()->toBool(), 1);
     DISPATCH();
 
-do_set_var:
-    *variables[*++pos] = *READ_LAST_REG();
+    OP(SET_VAR) :
+        *variables[*++pos] = *READ_LAST_REG();
     FREE_REGS(1);
     DISPATCH();
 
-do_read_var:
-    ADD_RET_VALUE(*variables[*++pos]);
+    OP(READ_VAR) :
+        ADD_RET_VALUE(*variables[*++pos]);
     DISPATCH();
 
-do_change_var:
-    variables[*++pos]->add(*READ_LAST_REG());
+    OP(CHANGE_VAR) :
+        variables[*++pos]->add(*READ_LAST_REG());
     FREE_REGS(1);
     DISPATCH();
 
-do_read_list:
-    ADD_RET_VALUE(lists[*++pos]->toString());
+    OP(READ_LIST) :
+        ADD_RET_VALUE(lists[*++pos]->toString());
     DISPATCH();
 
-do_list_append:
-    lists[*++pos]->push_back(*READ_LAST_REG());
+    OP(LIST_APPEND) :
+        lists[*++pos]->push_back(*READ_LAST_REG());
     FREE_REGS(1);
     DISPATCH();
 
-do_list_del : {
-    const Value *indexValue = READ_LAST_REG();
-    size_t index;
-    List *list = lists[*++pos];
-    if (indexValue->isString()) {
-        const std::string &str = indexValue->toString();
-        if (str == "last") {
-            index = list->size();
-        } else if (str == "all") {
-            list->clear();
-            index = 0;
-        } else if (str == "random") {
-            size_t size = list->size();
-            index = size == 0 ? 0 : rng->randint(1, size);
-        } else
-            index = 0;
-    } else {
-        index = indexValue->toLong();
-        FIX_LIST_INDEX(index, list->size());
+    OP(LIST_DEL) :
+    {
+        const Value *indexValue = READ_LAST_REG();
+        size_t index;
+        List *list = lists[*++pos];
+        if (indexValue->isString()) {
+            const std::string &str = indexValue->toString();
+            if (str == "last") {
+                index = list->size();
+            } else if (str == "all") {
+                list->clear();
+                index = 0;
+            } else if (str == "random") {
+                size_t size = list->size();
+                index = size == 0 ? 0 : rng->randint(1, size);
+            } else
+                index = 0;
+        } else {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        }
+        if (index != 0)
+            list->removeAt(index - 1);
+        FREE_REGS(1);
+        DISPATCH();
     }
-    if (index != 0)
-        list->removeAt(index - 1);
-    FREE_REGS(1);
-    DISPATCH();
-}
 
-do_list_del_all:
-    lists[*++pos]->clear();
-    DISPATCH();
-
-do_list_insert : {
-    const Value *indexValue = READ_REG(1, 2);
-    size_t index;
-    List *list = lists[*++pos];
-    if (indexValue->isString()) {
-        const std::string &str = indexValue->toString();
-        if (str == "last") {
-            list->push_back(*READ_REG(0, 2));
-            index = 0;
-        } else if (str == "random") {
-            size_t size = list->size();
-            index = size == 0 ? 1 : rng->randint(1, size);
-        } else
-            index = 0;
-    } else {
-        index = indexValue->toLong();
-        FIX_LIST_INDEX(index, list->size());
-    }
-    if ((index != 0) || list->empty()) {
-        if (list->empty())
-            list->push_back(*READ_REG(0, 2));
-        else
-            list->insert(index - 1, *READ_REG(0, 2));
-    }
-    FREE_REGS(2);
+    OP(LIST_DEL_ALL) :
+        lists[*++pos]->clear();
     DISPATCH();
-}
 
-do_list_replace : {
-    const Value *indexValue = READ_REG(0, 2);
-    size_t index;
-    List *list = lists[*++pos];
-    if (indexValue->isString()) {
-        std::string str = indexValue->toString();
-        if (str == "last")
-            index = list->size();
-        else if (str == "random") {
-            size_t size = list->size();
-            index = size == 0 ? 0 : rng->randint(1, size);
-        } else
-            index = 0;
-    } else {
-        index = indexValue->toLong();
-        FIX_LIST_INDEX(index, list->size());
+    OP(LIST_INSERT) :
+    {
+        const Value *indexValue = READ_REG(1, 2);
+        size_t index;
+        List *list = lists[*++pos];
+        if (indexValue->isString()) {
+            const std::string &str = indexValue->toString();
+            if (str == "last") {
+                list->push_back(*READ_REG(0, 2));
+                index = 0;
+            } else if (str == "random") {
+                size_t size = list->size();
+                index = size == 0 ? 1 : rng->randint(1, size);
+            } else
+                index = 0;
+        } else {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        }
+        if ((index != 0) || list->empty()) {
+            if (list->empty())
+                list->push_back(*READ_REG(0, 2));
+            else
+                list->insert(index - 1, *READ_REG(0, 2));
+        }
+        FREE_REGS(2);
+        DISPATCH();
     }
-    if (index != 0)
-        list->operator[](index - 1) = *READ_REG(1, 2);
-    FREE_REGS(2);
-    DISPATCH();
-}
 
-do_list_get_item : {
-    const Value *indexValue = READ_LAST_REG();
-    size_t index;
-    List *list = lists[*++pos];
-    if (indexValue->isString()) {
-        std::string str = indexValue->toString();
-        if (str == "last")
-            index = list->size();
-        else if (str == "random") {
-            size_t size = list->size();
-            index = size == 0 ? 0 : rng->randint(1, size);
-        } else
-            index = 0;
-    } else {
-        index = indexValue->toLong();
-        FIX_LIST_INDEX(index, list->size());
+    OP(LIST_REPLACE) :
+    {
+        const Value *indexValue = READ_REG(0, 2);
+        size_t index;
+        List *list = lists[*++pos];
+        if (indexValue->isString()) {
+            std::string str = indexValue->toString();
+            if (str == "last")
+                index = list->size();
+            else if (str == "random") {
+                size_t size = list->size();
+                index = size == 0 ? 0 : rng->randint(1, size);
+            } else
+                index = 0;
+        } else {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        }
+        if (index != 0)
+            list->operator[](index - 1) = *READ_REG(1, 2);
+        FREE_REGS(2);
+        DISPATCH();
     }
-    if (index == 0) {
-        REPLACE_RET_VALUE("", 1);
-    } else {
-        REPLACE_RET_VALUE(list->operator[](index - 1), 1);
+
+    OP(LIST_GET_ITEM) :
+    {
+        const Value *indexValue = READ_LAST_REG();
+        size_t index;
+        List *list = lists[*++pos];
+        if (indexValue->isString()) {
+            std::string str = indexValue->toString();
+            if (str == "last")
+                index = list->size();
+            else if (str == "random") {
+                size_t size = list->size();
+                index = size == 0 ? 0 : rng->randint(1, size);
+            } else
+                index = 0;
+        } else {
+            index = indexValue->toLong();
+            FIX_LIST_INDEX(index, list->size());
+        }
+        if (index == 0) {
+            REPLACE_RET_VALUE("", 1);
+        } else {
+            REPLACE_RET_VALUE(list->operator[](index - 1), 1);
+        }
+        DISPATCH();
     }
-    DISPATCH();
-}
 
-do_list_index_of:
-    // TODO: Add size_t support to Value and remove the static_cast
-    REPLACE_RET_VALUE(static_cast<long>(lists[*++pos]->indexOf(*READ_LAST_REG()) + 1), 1);
+    OP(LIST_INDEX_OF) :
+        // TODO: Add size_t support to Value and remove the static_cast
+        REPLACE_RET_VALUE(static_cast<long>(lists[*++pos]->indexOf(*READ_LAST_REG()) + 1), 1);
     DISPATCH();
 
-do_list_length:
-    // TODO: Add size_t support to Value and remove the static_cast
-    ADD_RET_VALUE(static_cast<long>(lists[*++pos]->size()));
+    OP(LIST_LENGTH) :
+        // TODO: Add size_t support to Value and remove the static_cast
+        ADD_RET_VALUE(static_cast<long>(lists[*++pos]->size()));
     DISPATCH();
 
-do_list_contains:
-    REPLACE_RET_VALUE(lists[*++pos]->contains(*READ_LAST_REG()), 1);
+    OP(LIST_CONTAINS) :
+        REPLACE_RET_VALUE(lists[*++pos]->contains(*READ_LAST_REG()), 1);
     DISPATCH();
 
-do_str_concat:
-    REPLACE_RET_VALUE(READ_REG(0, 2)->toString() + READ_REG(1, 2)->toString(), 2);
+    OP(STR_CONCAT) :
+        REPLACE_RET_VALUE(READ_REG(0, 2)->toString() + READ_REG(1, 2)->toString(), 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_str_at : {
-    size_t index = READ_REG(1, 2)->toLong() - 1;
+    OP(STR_AT) :
     {
-        std::u16string str = READ_REG(0, 2)->toUtf16();
-        if (index < 0 || index >= str.size())
-            REPLACE_RET_VALUE("", 2);
-        else
-            REPLACE_RET_VALUE(utf8::utf16to8(std::u16string({ str[index] })), 2);
-        FREE_REGS(1);
+        size_t index = READ_REG(1, 2)->toLong() - 1;
+        {
+            std::u16string str = READ_REG(0, 2)->toUtf16();
+            if (index < 0 || index >= str.size())
+                REPLACE_RET_VALUE("", 2);
+            else
+                REPLACE_RET_VALUE(utf8::utf16to8(std::u16string({ str[index] })), 2);
+            FREE_REGS(1);
+        }
+        DISPATCH();
     }
-    DISPATCH();
-}
 
-do_str_length:
-    REPLACE_RET_VALUE(static_cast<long>(READ_REG(0, 1)->toUtf16().size()), 1);
+    OP(STR_LENGTH) :
+        REPLACE_RET_VALUE(static_cast<long>(READ_REG(0, 1)->toUtf16().size()), 1);
     DISPATCH();
 
-do_str_contains:
-    REPLACE_RET_VALUE(READ_REG(0, 2)->toUtf16().find(READ_REG(1, 2)->toUtf16()) != std::u16string::npos, 2);
+    OP(STR_CONTAINS) :
+        REPLACE_RET_VALUE(READ_REG(0, 2)->toUtf16().find(READ_REG(1, 2)->toUtf16()) != std::u16string::npos, 2);
     FREE_REGS(1);
     DISPATCH();
 
-do_exec : {
-    auto ret = functions[*++pos](vm);
-    if (updatePos) {
-        pos = this->pos;
-        updatePos = false;
-    }
-    if (stop) {
-        stop = false;
-        if (goBack) {
-            goBack = false;
-            pos -= instruction_arg_count[OP_EXEC] + 1;
-            // NOTE: Going back leaks all registers for the next time the same function is called.
-            // This is for example used in the wait block (to call it again with the same time value).
-        } else
-            FREE_REGS(ret);
-
-        if (!warp) // TODO: This should always return if there's a "warp timer" enabled
-            return pos;
-
-        DISPATCH(); // this avoids freeing registers after "stopping" a warp script
+    OP(EXEC) :
+    {
+        auto ret = functions[*++pos](vm);
+        if (updatePos) {
+            pos = this->pos;
+            updatePos = false;
+        }
+        if (stop) {
+            stop = false;
+            if (goBack) {
+                goBack = false;
+                pos -= instruction_arg_count[OP_EXEC] + 1;
+                // NOTE: Going back leaks all registers for the next time the same function is called.
+                // This is for example used in the wait block (to call it again with the same time value).
+            } else
+                FREE_REGS(ret);
+
+            if (!warp) // TODO: This should always return if there's a "warp timer" enabled
+                return pos;
+
+            DISPATCH(); // this avoids freeing registers after "stopping" a warp script
+        }
+        FREE_REGS(ret);
+        DISPATCH();
     }
-    FREE_REGS(ret);
-    DISPATCH();
-}
 
-do_init_procedure:
-    procedureArgTree.push_back({});
+    OP(INIT_PROCEDURE) :
+        procedureArgTree.push_back({});
     if (procedureArgTree.size() >= 2)
         procedureArgs = &procedureArgTree[procedureArgTree.size() - 2];
     nextProcedureArgs = &procedureArgTree.back();
     DISPATCH();
 
-do_call_procedure : {
-    unsigned int *procedurePos = procedures[pos[1]];
+    OP(CALL_PROCEDURE) :
+    {
+        unsigned int *procedurePos = procedures[pos[1]];
 
-    if (procedurePos) {
-        callTree.push_back(++pos);
-        procedureArgs = nextProcedureArgs;
-        nextProcedureArgs = nullptr;
-        pos = procedurePos;
-    } else
-        pos++;
+        if (procedurePos) {
+            callTree.push_back({ ++pos, warp });
+            procedureArgs = nextProcedureArgs;
+            nextProcedureArgs = nullptr;
+            pos = procedurePos;
+        } else
+            pos++;
 
-    DISPATCH();
-}
+        DISPATCH();
+    }
 
-do_add_arg:
-    nextProcedureArgs->push_back(*READ_LAST_REG());
+    OP(ADD_ARG) :
+        nextProcedureArgs->push_back(*READ_LAST_REG());
     FREE_REGS(1);
     DISPATCH();
 
-do_read_arg:
-    ADD_RET_VALUE(procedureArgs->operator[](*++pos));
+    OP(READ_ARG) :
+        ADD_RET_VALUE(procedureArgs->operator[](*++pos));
     DISPATCH();
 
-do_break_frame:
-    noBreak = false;
+    OP(BREAK_FRAME) :
+        noBreak = false;
     DISPATCH();
 
-do_warp:
-    warp = true;
+    OP(WARP) :
+        warp = true;
     DISPATCH();
+#if !defined(ENABLE_COMPUTED_GOTO)
+}
+#endif
 }
diff --git a/src/engine/virtualmachine_p.h b/src/engine/virtualmachine_p.h
index da5dc7d1..7b1f1c6c 100644
--- a/src/engine/virtualmachine_p.h
+++ b/src/engine/virtualmachine_p.h
@@ -46,7 +46,7 @@ struct VirtualMachinePrivate
         bool running = false;
         bool atEnd = false;
         std::vector<Loop> loops;
-        std::vector<unsigned int *> callTree;
+        std::vector<std::pair<unsigned int *, bool>> callTree;
         std::vector<std::vector<Value>> procedureArgTree;
         std::vector<Value> *procedureArgs = nullptr;
         std::vector<Value> *nextProcedureArgs = nullptr;
@@ -56,6 +56,7 @@ struct VirtualMachinePrivate
         bool savePos = true;
         bool goBack = false;
         bool updatePos = false;
+        bool promisePending = false;
 
         unsigned int **procedures = nullptr;
         BlockFunc *functions = nullptr;
diff --git a/src/internal/iprojectreader.h b/src/internal/iprojectreader.h
index d74b8b5f..dec3b38d 100644
--- a/src/internal/iprojectreader.h
+++ b/src/internal/iprojectreader.h
@@ -35,6 +35,7 @@ class IProjectReader
         virtual const std::vector<std::shared_ptr<Broadcast>> &broadcasts() = 0;
         virtual const std::vector<std::shared_ptr<Monitor>> &monitors() = 0;
         virtual const std::vector<std::string> &extensions() = 0;
+        virtual const std::string &userAgent() const = 0;
 
     protected:
         virtual void printErr(const std::string &errStr) final { std::cerr << "Failed to read project: " << errStr << std::endl; }
diff --git a/src/internal/reader_common.h b/src/internal/reader_common.h
index 7c84f1f9..09815d01 100644
--- a/src/internal/reader_common.h
+++ b/src/internal/reader_common.h
@@ -13,9 +13,18 @@ Value jsonToValue(nlohmann::json value)
 {
     if (value.is_string())
         return value.get<std::string>();
-    else if (value.is_number())
-        return std::stod(value.dump());
-    else if (value.is_boolean())
+    else if (value.is_number()) {
+        // Set locale to C to avoid conversion issues
+        std::string oldLocale = std::setlocale(LC_NUMERIC, nullptr);
+        std::setlocale(LC_NUMERIC, "C");
+
+        double converted = std::stod(value.dump());
+
+        // Restore old locale
+        std::setlocale(LC_NUMERIC, oldLocale.c_str());
+
+        return converted;
+    } else if (value.is_boolean())
         return value.get<bool>();
     else
         return value.dump();
diff --git a/src/internal/scratch3reader.cpp b/src/internal/scratch3reader.cpp
index c747a023..14f6065b 100644
--- a/src/internal/scratch3reader.cpp
+++ b/src/internal/scratch3reader.cpp
@@ -404,12 +404,16 @@ bool Scratch3Reader::load()
             }
 
             // width
-            READER_STEP(step, "monitor -> width");
-            monitor->setWidth(jsonMonitor["width"]);
+            if (jsonMonitor.contains("width")) {
+                READER_STEP(step, "monitor -> width");
+                monitor->setWidth(jsonMonitor["width"]);
+            }
 
             // height
-            READER_STEP(step, "monitor -> height");
-            monitor->setHeight(jsonMonitor["height"]);
+            if (jsonMonitor.contains("height")) {
+                READER_STEP(step, "monitor -> height");
+                monitor->setHeight(jsonMonitor["height"]);
+            }
 
             // x
             READER_STEP(step, "monitor -> x");
@@ -449,6 +453,13 @@ bool Scratch3Reader::load()
         auto extensions = project["extensions"];
         for (auto extension : extensions)
             m_extensions.push_back(extension);
+
+        // meta
+        READER_STEP(step, "meta");
+        auto meta = project["meta"];
+        READER_STEP(step, "meta -> agent");
+        m_userAgent = meta["agent"];
+
     } catch (std::exception &e) {
         if (strcmp(step, "") == 0)
             printErr("could not parse project JSON file", e.what());
@@ -518,6 +529,11 @@ const std::vector<std::string> &Scratch3Reader::extensions()
     return m_extensions;
 }
 
+const std::string &Scratch3Reader::userAgent() const
+{
+    return m_userAgent;
+}
+
 void Scratch3Reader::read()
 {
     if (fileName().empty())
diff --git a/src/internal/scratch3reader.h b/src/internal/scratch3reader.h
index f003151b..1f9d11a0 100644
--- a/src/internal/scratch3reader.h
+++ b/src/internal/scratch3reader.h
@@ -21,6 +21,7 @@ class Scratch3Reader : public IProjectReader
         const std::vector<std::shared_ptr<Broadcast>> &broadcasts() override;
         const std::vector<std::shared_ptr<Monitor>> &monitors() override;
         const std::vector<std::string> &extensions() override;
+        const std::string &userAgent() const override;
 
     private:
         void read();
@@ -30,6 +31,7 @@ class Scratch3Reader : public IProjectReader
         std::vector<std::shared_ptr<Broadcast>> m_broadcasts;
         std::vector<std::shared_ptr<Monitor>> m_monitors;
         std::vector<std::string> m_extensions;
+        std::string m_userAgent;
 };
 
 } // namespace libscratchcpp
diff --git a/src/project_p.cpp b/src/project_p.cpp
index 95658c83..f581f117 100644
--- a/src/project_p.cpp
+++ b/src/project_p.cpp
@@ -129,6 +129,7 @@ bool ProjectPrivate::load()
     engine->setBroadcasts(reader->broadcasts());
     engine->setMonitors(reader->monitors());
     engine->setExtensions(reader->extensions());
+    engine->setUserAgent(reader->userAgent());
     engine->compile();
     return true;
 }
diff --git a/src/scratch/list.cpp b/src/scratch/list.cpp
index 22e4af74..f2e2c16b 100644
--- a/src/scratch/list.cpp
+++ b/src/scratch/list.cpp
@@ -38,6 +38,18 @@ void List::setTarget(Target *target)
     impl->target = target;
 }
 
+/*! Returns the monitor of this list. */
+Monitor *List::monitor() const
+{
+    return impl->monitor;
+}
+
+/*! Sets the monitor of this list. */
+void List::setMonitor(Monitor *monitor)
+{
+    impl->monitor = monitor;
+}
+
 /*! 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 95a4196c..7fed21d9 100644
--- a/src/scratch/list_p.h
+++ b/src/scratch/list_p.h
@@ -8,6 +8,7 @@ namespace libscratchcpp
 {
 
 class Target;
+class Monitor;
 
 struct ListPrivate
 {
@@ -15,6 +16,7 @@ struct ListPrivate
 
         std::string name;
         Target *target = nullptr;
+        Monitor *monitor = nullptr;
 };
 
 } // namespace libscratchcpp
diff --git a/src/scratch/monitor.cpp b/src/scratch/monitor.cpp
index ae28e691..47b9bd1f 100644
--- a/src/scratch/monitor.cpp
+++ b/src/scratch/monitor.cpp
@@ -41,6 +41,12 @@ const std::string &Monitor::name() const
     return impl->name;
 }
 
+/*! Sets the name of this monitor. */
+void Monitor::setName(const std::string &name)
+{
+    impl->name = name;
+}
+
 /*! Returns the monitor's mode. */
 Monitor::Mode Monitor::mode() const
 {
@@ -80,6 +86,12 @@ std::shared_ptr<IBlockSection> Monitor::blockSection() const
     return impl->blockSection;
 }
 
+/*! Sets the block section of this monitor. */
+void Monitor::setBlockSection(std::shared_ptr<IBlockSection> blockSection)
+{
+    impl->blockSection = blockSection;
+}
+
 /*! Convenience method which calls block()->target(). */
 Sprite *Monitor::sprite() const
 {
diff --git a/src/scratch/monitor_p.h b/src/scratch/monitor_p.h
index 04e7d010..0e0509a2 100644
--- a/src/scratch/monitor_p.h
+++ b/src/scratch/monitor_p.h
@@ -22,7 +22,7 @@ struct MonitorPrivate
         std::shared_ptr<Script> script;
         std::shared_ptr<IBlockSection> blockSection;
         std::shared_ptr<Block> block; // Compiler needs shared_ptr
-        MonitorChangeFunc changeFunc;
+        MonitorChangeFunc changeFunc = nullptr;
         VirtualMachine changeValueVM;
         unsigned int width = 0;
         unsigned int height = 0;
diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp
index eef2ffdb..609241af 100644
--- a/src/scratch/sprite.cpp
+++ b/src/scratch/sprite.cpp
@@ -29,6 +29,12 @@ void Sprite::setInterface(ISpriteHandler *newInterface)
     impl->iface->init(this);
 }
 
+/*! Returns the sprite interface. */
+ISpriteHandler *Sprite::getInterface() const
+{
+    return impl->iface;
+}
+
 /*! Creates a clone of the sprite. */
 std::shared_ptr<Sprite> Sprite::clone()
 {
@@ -184,6 +190,17 @@ void Sprite::setY(double newY)
         impl->iface->onYChanged(impl->y);
 }
 
+/* Sets the position of the sprite. */
+void Sprite::setPosition(double x, double y)
+{
+    setXY(x, y);
+
+    if (impl->iface) {
+        impl->iface->onXChanged(impl->x);
+        impl->iface->onYChanged(impl->y);
+    }
+}
+
 /*! Returns the size. */
 double Sprite::size() const
 {
@@ -328,7 +345,7 @@ void Sprite::setLayerOrder(int newLayerOrder)
 Rect Sprite::boundingRect() const
 {
     if (!impl->iface)
-        return Rect();
+        return Rect(impl->x, impl->y, impl->x, impl->y);
 
     return impl->iface->boundingRect();
 }
@@ -412,6 +429,31 @@ void Sprite::clearGraphicsEffects()
         impl->iface->onGraphicsEffectsCleared();
 }
 
+/*! Overrides Target#setBubbleType(). */
+void Sprite::setBubbleType(BubbleType type)
+{
+    Target::setBubbleType(type);
+
+    if (impl->iface)
+        impl->iface->onBubbleTypeChanged(type);
+}
+
+/*! Overrides Target#setBubbleText(). */
+void Sprite::setBubbleText(const std::string &text)
+{
+    Target::setBubbleText(impl->visible ? text : "");
+
+    if (impl->visible && !text.empty()) {
+        IEngine *eng = engine();
+
+        if (eng)
+            eng->requestRedraw();
+    }
+
+    if (impl->iface)
+        impl->iface->onBubbleTextChanged(impl->visible ? text : "");
+}
+
 Target *Sprite::dataSource() const
 {
     return impl->cloneSprite;
@@ -421,6 +463,9 @@ void Sprite::setXY(double x, double y)
 {
     IEngine *eng = engine();
 
+    double oldX = impl->x;
+    double oldY = impl->y;
+
     if (eng && !eng->spriteFencingEnabled()) {
         impl->x = x;
         impl->y = y;
@@ -433,4 +478,7 @@ void Sprite::setXY(double x, double y)
         if (eng)
             eng->requestRedraw();
     }
+
+    if (impl->iface)
+        impl->iface->onMoved(oldX, oldY, impl->x, impl->y);
 }
diff --git a/src/scratch/stage.cpp b/src/scratch/stage.cpp
index 21c6bbf0..7f55b70a 100644
--- a/src/scratch/stage.cpp
+++ b/src/scratch/stage.cpp
@@ -24,6 +24,12 @@ void Stage::setInterface(IStageHandler *newInterface)
     impl->iface->init(this);
 }
 
+/*! Returns the stage interface. */
+IStageHandler *Stage::getInterface() const
+{
+    return impl->iface;
+}
+
 /*! Returns true. */
 bool Stage::isStage() const
 {
@@ -150,3 +156,28 @@ void Stage::clearGraphicsEffects()
     if (impl->iface)
         impl->iface->onGraphicsEffectsCleared();
 }
+
+/*! Overrides Target#setBubbleType(). */
+void Stage::setBubbleType(BubbleType type)
+{
+    Target::setBubbleType(type);
+
+    if (impl->iface)
+        impl->iface->onBubbleTypeChanged(type);
+}
+
+/*! Overrides Target#setBubbleText(). */
+void Stage::setBubbleText(const std::string &text)
+{
+    Target::setBubbleText(text);
+
+    if (!text.empty()) {
+        IEngine *eng = engine();
+
+        if (eng)
+            eng->requestRedraw();
+    }
+
+    if (impl->iface)
+        impl->iface->onBubbleTextChanged(text);
+}
diff --git a/src/scratch/target.cpp b/src/scratch/target.cpp
index e0bce348..3392bef2 100644
--- a/src/scratch/target.cpp
+++ b/src/scratch/target.cpp
@@ -439,6 +439,36 @@ void Target::clearGraphicsEffects()
     impl->graphicsEffects.clear();
 }
 
+/*! Returns the type of the bubble (say or think). */
+Target::BubbleType Target::bubbleType() const
+{
+    return impl->bubbleType;
+}
+
+/*! Sets the type of the bubble (say or think). */
+void Target::setBubbleType(BubbleType type)
+{
+    impl->bubbleType = type;
+}
+
+/*!
+ * Returns the text of the bubble.
+ * \note If the text is an empty string, the bubble is supposed to be hidden.
+ */
+const std::string &Target::bubbleText() const
+{
+    return impl->bubbleText;
+}
+
+/*!
+ * Sets the text of the bubble.
+ * \note If the text is an empty string, the bubble is supposed to be hidden.
+ */
+void Target::setBubbleText(const std::string &text)
+{
+    impl->bubbleText = text;
+}
+
 /*! Returns the engine. */
 IEngine *Target::engine() const
 {
diff --git a/src/scratch/target_p.h b/src/scratch/target_p.h
index fc920922..73db4087 100644
--- a/src/scratch/target_p.h
+++ b/src/scratch/target_p.h
@@ -8,6 +8,7 @@
 #include <unordered_map>
 #include <scratchcpp/costume.h>
 #include <scratchcpp/sound.h>
+#include <scratchcpp/target.h>
 
 namespace libscratchcpp
 {
@@ -36,6 +37,8 @@ struct TargetPrivate
         int layerOrder = 0;
         double volume = 100;
         std::unordered_map<IGraphicsEffect *, double> graphicsEffects;
+        Target::BubbleType bubbleType = Target::BubbleType::Say;
+        std::string bubbleText;
 };
 
 } // namespace libscratchcpp
diff --git a/src/scratch/variable.cpp b/src/scratch/variable.cpp
index 13f47996..bf1c995e 100644
--- a/src/scratch/variable.cpp
+++ b/src/scratch/variable.cpp
@@ -61,6 +61,18 @@ void Variable::setTarget(Target *target)
     impl->target = target;
 }
 
+/*! Returns the monitor of this variable. */
+Monitor *Variable::monitor() const
+{
+    return impl->monitor;
+}
+
+/*! Sets the monitor of this variable. */
+void Variable::setMonitor(Monitor *monitor)
+{
+    impl->monitor = monitor;
+}
+
 /*! 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 ac5edd8f..9c7dcb22 100644
--- a/src/scratch/variable_p.h
+++ b/src/scratch/variable_p.h
@@ -8,6 +8,7 @@ namespace libscratchcpp
 {
 
 class Target;
+class Monitor;
 
 struct VariablePrivate
 {
@@ -18,6 +19,7 @@ struct VariablePrivate
         Value value;
         bool isCloudVariable;
         Target *target = nullptr;
+        Monitor *monitor = nullptr;
 };
 
 } // namespace libscratchcpp
diff --git a/test/blocks/event_blocks_test.cpp b/test/blocks/event_blocks_test.cpp
index 60cbb47b..717930c8 100644
--- a/test/blocks/event_blocks_test.cpp
+++ b/test/blocks/event_blocks_test.cpp
@@ -84,6 +84,8 @@ TEST_F(EventBlocksTest, RegisterBlocks)
 {
     // Blocks
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenflagclicked", &EventBlocks::compileWhenFlagClicked));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenthisspriteclicked", &EventBlocks::compileWhenThisSpriteClicked));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenstageclicked", &EventBlocks::compileWhenStageClicked));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_broadcast", &EventBlocks::compileBroadcast));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_broadcastandwait", &EventBlocks::compileBroadcastAndWait));
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "event_whenbroadcastreceived", &EventBlocks::compileWhenBroadcastReceived));
@@ -121,6 +123,46 @@ TEST_F(EventBlocksTest, WhenFlagClicked)
     ASSERT_TRUE(compiler.lists().empty());
 }
 
+TEST_F(EventBlocksTest, WhenThisSpriteClicked)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = createEventBlock("a", "event_whenthisspriteclicked");
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, addTargetClickScript(block));
+    compiler.setBlock(block);
+    EventBlocks::compileWhenThisSpriteClicked(&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());
+}
+
+TEST_F(EventBlocksTest, WhenStageClicked)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = createEventBlock("a", "event_whenstageclicked");
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, addTargetClickScript(block));
+    compiler.setBlock(block);
+    EventBlocks::compileWhenStageClicked(&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());
+}
+
 TEST_F(EventBlocksTest, Broadcast)
 {
     Compiler compiler(&m_engineMock);
diff --git a/test/blocks/list_blocks_test.cpp b/test/blocks/list_blocks_test.cpp
index 028c2502..40225cbc 100644
--- a/test/blocks/list_blocks_test.cpp
+++ b/test/blocks/list_blocks_test.cpp
@@ -3,6 +3,8 @@
 #include <scratchcpp/input.h>
 #include <scratchcpp/field.h>
 #include <scratchcpp/list.h>
+#include <scratchcpp/stage.h>
+#include <scratchcpp/monitor.h>
 #include <enginemock.h>
 
 #include "../common.h"
@@ -11,6 +13,8 @@
 
 using namespace libscratchcpp;
 
+using ::testing::Return;
+
 class ListBlocksTest : public testing::Test
 {
     public:
@@ -104,6 +108,8 @@ TEST_F(ListBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_itemnumoflist", &ListBlocks::compileItemNumberInList)).Times(1);
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_lengthoflist", &ListBlocks::compileLengthOfList)).Times(1);
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_listcontainsitem", &ListBlocks::compileListContainsItem)).Times(1);
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_showlist", &ListBlocks::compileShowList)).Times(1);
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_hidelist", &ListBlocks::compileHideList)).Times(1);
 
     // Monitor names
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "data_listcontents", &ListBlocks::listContentsMonitorName));
@@ -435,3 +441,209 @@ TEST_F(ListBlocksTest, ListContainsItem)
             list2.get(),
         }));
 }
+
+TEST_F(ListBlocksTest, ShowList)
+{
+    Compiler compiler(&m_engineMock);
+    Stage stage;
+    Target target;
+
+    // show list [list1]
+    auto list1 = std::make_shared<List>("b", "list1");
+    list1->setTarget(&stage);
+    auto block1 = createListBlock("a", "data_showlist", list1);
+
+    // show list [list2]
+    auto list2 = std::make_shared<List>("d", "list2");
+    list2->setTarget(&target);
+    auto block2 = createListBlock("c", "data_showlist", list2);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&ListBlocks::showGlobalList)).WillOnce(Return(0));
+    compiler.init();
+    compiler.setBlock(block1);
+    ListBlocks::compileShowList(&compiler);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&ListBlocks::showList)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    ListBlocks::compileShowList(&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, 1, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "b", "d" }));
+    ASSERT_TRUE(compiler.variables().empty());
+    ASSERT_TRUE(compiler.lists().empty());
+}
+
+TEST_F(ListBlocksTest, ShowListImpl)
+{
+    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[] = { &ListBlocks::showGlobalList, &ListBlocks::showList };
+    static Value constValues[] = { "a", "b", "c", "d" };
+
+    auto list1 = std::make_shared<List>("b", "");
+    Monitor monitor1("b", "");
+    monitor1.setVisible(false);
+    list1->setMonitor(&monitor1);
+
+    auto list2 = std::make_shared<List>("d", "");
+    Monitor monitor2("d", "");
+    monitor2.setVisible(false);
+    list2->setMonitor(&monitor2);
+
+    Stage stage;
+    stage.addList(list1);
+
+    Target target;
+    target.addList(list2);
+
+    // Global
+    VirtualMachine vm1(&stage, &m_engineMock, nullptr);
+    vm1.setBytecode(bytecode1);
+    vm1.setFunctions(functions);
+    vm1.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.reset();
+    vm1.setBytecode(bytecode2);
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+
+    monitor1.setVisible(false);
+
+    // Local
+    VirtualMachine vm2(&target, &m_engineMock, nullptr);
+    vm2.setBytecode(bytecode3);
+    vm2.setFunctions(functions);
+    vm2.setConstValues(constValues);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+
+    vm2.reset();
+    vm2.setBytecode(bytecode4);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+}
+
+TEST_F(ListBlocksTest, HideList)
+{
+    Compiler compiler(&m_engineMock);
+    Stage stage;
+    Target target;
+
+    // hide list [list1]
+    auto list1 = std::make_shared<List>("b", "list1");
+    list1->setTarget(&stage);
+    auto block1 = createListBlock("a", "data_hidelist", list1);
+
+    // hide list [list2]
+    auto list2 = std::make_shared<List>("d", "list2");
+    list2->setTarget(&target);
+    auto block2 = createListBlock("c", "data_hidelist", list2);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&ListBlocks::hideGlobalList)).WillOnce(Return(0));
+    compiler.init();
+    compiler.setBlock(block1);
+    ListBlocks::compileHideList(&compiler);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&ListBlocks::hideList)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    ListBlocks::compileHideList(&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, 1, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "b", "d" }));
+    ASSERT_TRUE(compiler.variables().empty());
+    ASSERT_TRUE(compiler.lists().empty());
+}
+
+TEST_F(ListBlocksTest, HideListImpl)
+{
+    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[] = { &ListBlocks::hideGlobalList, &ListBlocks::hideList };
+    static Value constValues[] = { "a", "b", "c", "d" };
+
+    auto list1 = std::make_shared<List>("b", "");
+    Monitor monitor1("b", "");
+    monitor1.setVisible(true);
+    list1->setMonitor(&monitor1);
+
+    auto list2 = std::make_shared<List>("d", "");
+    Monitor monitor2("d", "");
+    monitor2.setVisible(true);
+    list2->setMonitor(&monitor2);
+
+    Stage stage;
+    stage.addList(list1);
+
+    Target target;
+    target.addList(list2);
+
+    // Global
+    VirtualMachine vm1(&stage, &m_engineMock, nullptr);
+    vm1.setBytecode(bytecode1);
+    vm1.setFunctions(functions);
+    vm1.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.reset();
+    vm1.setBytecode(bytecode2);
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+
+    monitor1.setVisible(true);
+
+    // Local
+    VirtualMachine vm2(&target, &m_engineMock, nullptr);
+    vm2.setBytecode(bytecode3);
+    vm2.setFunctions(functions);
+    vm2.setConstValues(constValues);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+
+    vm2.reset();
+    vm2.setBytecode(bytecode4);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+}
diff --git a/test/blocks/looks_blocks_test.cpp b/test/blocks/looks_blocks_test.cpp
index e21820dd..288c4781 100644
--- a/test/blocks/looks_blocks_test.cpp
+++ b/test/blocks/looks_blocks_test.cpp
@@ -9,12 +9,14 @@
 #include <enginemock.h>
 #include <randomgeneratormock.h>
 #include <graphicseffectmock.h>
+#include <clockmock.h>
 
 #include "../common.h"
 #include "blocks/looksblocks.h"
 #include "blocks/operatorblocks.h"
 #include "engine/internal/engine.h"
 #include "engine/internal/randomgenerator.h"
+#include "engine/internal/clock.h"
 
 using namespace libscratchcpp;
 
@@ -104,6 +106,10 @@ TEST_F(LooksBlocksTest, CategoryVisible)
 TEST_F(LooksBlocksTest, RegisterBlocks)
 {
     // Blocks
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_sayforsecs", &LooksBlocks::compileSayForSecs));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_say", &LooksBlocks::compileSay));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_thinkforsecs", &LooksBlocks::compileThinkForSecs));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "looks_think", &LooksBlocks::compileThink));
     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));
@@ -128,6 +134,8 @@ TEST_F(LooksBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "looks_size", &LooksBlocks::sizeMonitorName));
 
     // Inputs
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "MESSAGE", LooksBlocks::MESSAGE));
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SECS", LooksBlocks::SECS));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "CHANGE", LooksBlocks::CHANGE));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SIZE", LooksBlocks::SIZE));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "COSTUME", LooksBlocks::COSTUME));
@@ -158,6 +166,418 @@ TEST_F(LooksBlocksTest, RegisterBlocks)
     m_section->registerBlocks(&m_engineMock);
 }
 
+TEST_F(LooksBlocksTest, SayForSecs)
+{
+    Compiler compiler(&m_engineMock);
+
+    // say "Hello!" for 3.5 seconds
+    auto block = std::make_shared<Block>("a", "looks_sayforsecs");
+    addValueInput(block, "MESSAGE", LooksBlocks::MESSAGE, "Hello!");
+    addValueInput(block, "SECS", LooksBlocks::SECS, 3.5);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::startSayForSecs)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::sayForSecs)).WillOnce(Return(1));
+
+    compiler.init();
+    compiler.setBlock(block);
+    LooksBlocks::compileSayForSecs(&compiler);
+    compiler.end();
+
+    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_EXEC, 1, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "Hello!", 3.5 }));
+}
+
+TEST_F(LooksBlocksTest, SayForSecsImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 2, vm::OP_HALT };
+    static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &LooksBlocks::startSayForSecs, &LooksBlocks::sayForSecs, &LooksBlocks::say };
+    static Value constValues[] = { "test", 5.5, "hello" };
+
+    Target target;
+    target.setBubbleType(Target::BubbleType::Think);
+    VirtualMachine vm(&target, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+    vm.setBytecode(bytecode1);
+
+    ClockMock clock;
+    LooksBlocks::clock = &clock;
+
+    std::chrono::steady_clock::time_point startTime(std::chrono::milliseconds(1000));
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    std::chrono::steady_clock::time_point time1(std::chrono::milliseconds(6450));
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time1));
+    target.setBubbleType(Target::BubbleType::Think);
+    target.setBubbleText("another");
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "another");
+
+    std::chrono::steady_clock::time_point time2(std::chrono::milliseconds(6500));
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2));
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_TRUE(target.bubbleText().empty());
+
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_TRUE(target.bubbleText().empty());
+
+    // Run the say block while waiting
+    VirtualMachine vm2(&target, &m_engineMock, nullptr);
+    vm2.setFunctions(functions);
+    vm2.setConstValues(constValues);
+    vm2.setBytecode(bytecode2);
+
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    vm2.run();
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2));
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    // Run the say for secs block while waiting
+    vm2.reset();
+    vm2.setBytecode(bytecode3);
+
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm2.reset();
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm2) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm2.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm.reset();
+    vm.run();
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2));
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm2) == LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm2.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm2) == LooksBlocks::m_timeMap.cend());
+    ASSERT_TRUE(vm2.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    LooksBlocks::clock = Clock::instance().get();
+}
+
+TEST_F(LooksBlocksTest, Say)
+{
+    Compiler compiler(&m_engineMock);
+
+    // say "Hello!"
+    auto block = std::make_shared<Block>("a", "looks_say");
+    addValueInput(block, "MESSAGE", LooksBlocks::MESSAGE, "Hello!");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::say)).WillOnce(Return(0));
+
+    compiler.init();
+    compiler.setBlock(block);
+    LooksBlocks::compileSay(&compiler);
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues().size(), 1);
+    ASSERT_EQ(compiler.constValues()[0].toString(), "Hello!");
+}
+
+TEST_F(LooksBlocksTest, SayImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &LooksBlocks::say };
+    static Value constValues[] = { "test" };
+
+    Target target;
+    VirtualMachine vm(&target, nullptr, nullptr);
+    vm.setBytecode(bytecode);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    target.setBubbleType(Target::BubbleType::Think);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "test");
+}
+
+TEST_F(LooksBlocksTest, ThinkForSecs)
+{
+    Compiler compiler(&m_engineMock);
+
+    // think "Hmm..." for 3.5 seconds
+    auto block = std::make_shared<Block>("a", "looks_thinkforsecs");
+    addValueInput(block, "MESSAGE", LooksBlocks::MESSAGE, "Hmm...");
+    addValueInput(block, "SECS", LooksBlocks::SECS, 3.5);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::startThinkForSecs)).WillOnce(Return(0));
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::thinkForSecs)).WillOnce(Return(1));
+
+    compiler.init();
+    compiler.setBlock(block);
+    LooksBlocks::compileThinkForSecs(&compiler);
+    compiler.end();
+
+    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_EXEC, 1, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "Hmm...", 3.5 }));
+}
+
+TEST_F(LooksBlocksTest, ThinkForSecsImpl)
+{
+    static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_HALT };
+    static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 2, vm::OP_HALT };
+    static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &LooksBlocks::startThinkForSecs, &LooksBlocks::thinkForSecs, &LooksBlocks::think };
+    static Value constValues[] = { "test", 5.5, "hello" };
+
+    Target target;
+    target.setBubbleType(Target::BubbleType::Say);
+    VirtualMachine vm(&target, &m_engineMock, nullptr);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+    vm.setBytecode(bytecode1);
+
+    ClockMock clock;
+    LooksBlocks::clock = &clock;
+
+    std::chrono::steady_clock::time_point startTime(std::chrono::milliseconds(1000));
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    std::chrono::steady_clock::time_point time1(std::chrono::milliseconds(6450));
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time1));
+    target.setBubbleType(Target::BubbleType::Say);
+    target.setBubbleText("another");
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleText(), "another");
+
+    std::chrono::steady_clock::time_point time2(std::chrono::milliseconds(6500));
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2));
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_TRUE(target.bubbleText().empty());
+
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+    ASSERT_TRUE(target.bubbleText().empty());
+
+    // Run the say block while waiting
+    VirtualMachine vm2(&target, &m_engineMock, nullptr);
+    vm2.setFunctions(functions);
+    vm2.setConstValues(constValues);
+    vm2.setBytecode(bytecode2);
+
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    vm2.run();
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2));
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm) == LooksBlocks::m_timeMap.cend());
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    // Run the say for secs block while waiting
+    vm2.reset();
+    vm2.setBytecode(bytecode3);
+
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm2.reset();
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm2) != LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm2.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(startTime));
+    EXPECT_CALL(m_engineMock, requestRedraw());
+    vm.reset();
+    vm.run();
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2));
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm2) == LooksBlocks::m_timeMap.cend());
+    ASSERT_FALSE(vm2.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(LooksBlocks::m_timeMap.find(&vm2) == LooksBlocks::m_timeMap.cend());
+    ASSERT_TRUE(vm2.atEnd());
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    LooksBlocks::clock = Clock::instance().get();
+}
+
+TEST_F(LooksBlocksTest, Think)
+{
+    Compiler compiler(&m_engineMock);
+
+    // say "Hmm..."
+    auto block = std::make_shared<Block>("a", "looks_think");
+    addValueInput(block, "MESSAGE", LooksBlocks::MESSAGE, "Hmm...");
+
+    EXPECT_CALL(m_engineMock, functionIndex(&LooksBlocks::think)).WillOnce(Return(0));
+
+    compiler.init();
+    compiler.setBlock(block);
+    LooksBlocks::compileThink(&compiler);
+    compiler.end();
+
+    ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues().size(), 1);
+    ASSERT_EQ(compiler.constValues()[0].toString(), "Hmm...");
+}
+
+TEST_F(LooksBlocksTest, ThinkImpl)
+{
+    static unsigned int bytecode[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
+    static BlockFunc functions[] = { &LooksBlocks::think };
+    static Value constValues[] = { "test" };
+
+    Target target;
+    VirtualMachine vm(&target, nullptr, nullptr);
+    vm.setBytecode(bytecode);
+    vm.setFunctions(functions);
+    vm.setConstValues(constValues);
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "test");
+
+    target.setBubbleType(Target::BubbleType::Say);
+    vm.reset();
+    vm.run();
+
+    ASSERT_EQ(vm.registerCount(), 0);
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleText(), "test");
+}
+
 TEST_F(LooksBlocksTest, Show)
 {
     Compiler compiler(&m_engineMock);
@@ -403,6 +823,36 @@ TEST_F(LooksBlocksTest, ChangeEffectBy)
 
 static void initEffects()
 {
+    // Create and register fake graphics effects
+    auto colorEffect = std::make_shared<GraphicsEffectMock>();
+    auto fisheyeEffect = std::make_shared<GraphicsEffectMock>();
+    auto whirlEffect = std::make_shared<GraphicsEffectMock>();
+    auto pixelateEffect = std::make_shared<GraphicsEffectMock>();
+    auto mosaicEffect = std::make_shared<GraphicsEffectMock>();
+    auto brightnessEffect = std::make_shared<GraphicsEffectMock>();
+    auto ghostEffect = std::make_shared<GraphicsEffectMock>();
+
+    EXPECT_CALL(*colorEffect, name()).WillOnce(Return("color"));
+    ScratchConfiguration::registerGraphicsEffect(colorEffect);
+
+    EXPECT_CALL(*fisheyeEffect, name()).WillOnce(Return("fisheye"));
+    ScratchConfiguration::registerGraphicsEffect(fisheyeEffect);
+
+    EXPECT_CALL(*whirlEffect, name()).WillOnce(Return("whirl"));
+    ScratchConfiguration::registerGraphicsEffect(whirlEffect);
+
+    EXPECT_CALL(*pixelateEffect, name()).WillOnce(Return("pixelate"));
+    ScratchConfiguration::registerGraphicsEffect(pixelateEffect);
+
+    EXPECT_CALL(*mosaicEffect, name()).WillOnce(Return("mosaic"));
+    ScratchConfiguration::registerGraphicsEffect(mosaicEffect);
+
+    EXPECT_CALL(*brightnessEffect, name()).WillOnce(Return("brightness"));
+    ScratchConfiguration::registerGraphicsEffect(brightnessEffect);
+
+    EXPECT_CALL(*ghostEffect, name()).WillOnce(Return("ghost"));
+    ScratchConfiguration::registerGraphicsEffect(ghostEffect);
+
     LooksBlocks::m_colorEffect = ScratchConfiguration::getGraphicsEffect("color");
     LooksBlocks::m_fisheyeEffect = ScratchConfiguration::getGraphicsEffect("fisheye");
     LooksBlocks::m_whirlEffect = ScratchConfiguration::getGraphicsEffect("whirl");
@@ -412,6 +862,17 @@ static void initEffects()
     LooksBlocks::m_ghostEffect = ScratchConfiguration::getGraphicsEffect("ghost");
 }
 
+static void removeEffects()
+{
+    ScratchConfiguration::removeGraphicsEffect("color");
+    ScratchConfiguration::removeGraphicsEffect("fisheye");
+    ScratchConfiguration::removeGraphicsEffect("whirl");
+    ScratchConfiguration::removeGraphicsEffect("pixelate");
+    ScratchConfiguration::removeGraphicsEffect("mosaic");
+    ScratchConfiguration::removeGraphicsEffect("brightness");
+    ScratchConfiguration::removeGraphicsEffect("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 };
@@ -431,12 +892,12 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
 
     GraphicsEffectMock effect1, effect2;
 
-    Sprite sprite;
-    sprite.setGraphicsEffectValue(&effect1, 12.5);
-    sprite.setGraphicsEffectValue(&effect2, -100.48);
+    Target target;
+    target.setGraphicsEffectValue(&effect1, 12.5);
+    target.setGraphicsEffectValue(&effect2, -100.48);
 
     // custom1
-    VirtualMachine vm(&sprite, nullptr, nullptr);
+    VirtualMachine vm(&target, nullptr, nullptr);
     LooksBlocks::m_customGraphicsEffects.clear();
     LooksBlocks::m_customGraphicsEffects.push_back(&effect1);
     vm.setBytecode(bytecode1);
@@ -445,7 +906,7 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 67.65);
+    ASSERT_EQ(target.graphicsEffectValue(&effect1), 67.65);
 
     // custom2
     LooksBlocks::m_customGraphicsEffects.push_back(&effect2);
@@ -454,26 +915,25 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), -141.02);
+    ASSERT_EQ(target.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)
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color"), 12.4);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye"), -8.45);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl"), 50.15);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate"), -45.21);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic"), 0.2);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness"), -0.01);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost"), 45.78);
+
     // color
-    /*vm.reset();
+    vm.reset();
     vm.setBytecode(bytecode3);
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color")), 13.6);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color")), 13.6);
 
     // fisheye
     vm.reset();
@@ -481,7 +941,7 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye")), -6.15);
+    ASSERT_EQ(std::round(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye")) * 100) / 100, -6.15);
 
     // whirl
     vm.reset();
@@ -489,7 +949,7 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl")), 46.75);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl")), 46.75);
 
     // pixelate
     vm.reset();
@@ -497,7 +957,7 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate")), -49.71);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate")), -49.71);
 
     // mosaic
     vm.reset();
@@ -505,7 +965,7 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic")), 0.7);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic")), 0.7);
 
     // brightness
     vm.reset();
@@ -513,7 +973,7 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness")), -8.55);
+    ASSERT_EQ(std::round(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness")) * 100) / 100, -8.55);
 
     // ghost
     vm.reset();
@@ -521,7 +981,9 @@ TEST_F(LooksBlocksTest, ChangeEffectByImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost")), 45.79);*/
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost")), 45.79);
+
+    removeEffects();
 }
 
 TEST_F(LooksBlocksTest, SetEffectTo)
@@ -531,47 +993,47 @@ TEST_F(LooksBlocksTest, SetEffectTo)
     // 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);
+    addValueInput(block1, "VALUE", LooksBlocks::VALUE, 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);
+    addValueInput(block2, "VALUE", LooksBlocks::VALUE, -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);
+    addValueInput(block3, "VALUE", LooksBlocks::VALUE, 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);
+    addValueInput(block4, "VALUE", LooksBlocks::VALUE, -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);
+    addValueInput(block5, "VALUE", LooksBlocks::VALUE, 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);
+    addValueInput(block6, "VALUE", LooksBlocks::VALUE, -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);
+    addValueInput(block7, "VALUE", LooksBlocks::VALUE, 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);
+    addValueInput(block8, "VALUE", LooksBlocks::VALUE, 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);
+    addValueInput(block9, "VALUE", LooksBlocks::VALUE, 50.04);
 
     auto effect1 = std::make_shared<GraphicsEffectMock>();
     auto effect2 = std::make_shared<GraphicsEffectMock>();
@@ -706,12 +1168,12 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
 
     GraphicsEffectMock effect1, effect2;
 
-    Sprite sprite;
-    sprite.setGraphicsEffectValue(&effect1, 12.5);
-    sprite.setGraphicsEffectValue(&effect2, -100.48);
+    Target target;
+    target.setGraphicsEffectValue(&effect1, 12.5);
+    target.setGraphicsEffectValue(&effect2, -100.48);
 
     // custom1
-    VirtualMachine vm(&sprite, nullptr, nullptr);
+    VirtualMachine vm(&target, nullptr, nullptr);
     LooksBlocks::m_customGraphicsEffects.clear();
     LooksBlocks::m_customGraphicsEffects.push_back(&effect1);
     vm.setBytecode(bytecode1);
@@ -720,7 +1182,7 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 55.15);
+    ASSERT_EQ(target.graphicsEffectValue(&effect1), 55.15);
 
     // custom2
     LooksBlocks::m_customGraphicsEffects.push_back(&effect2);
@@ -729,26 +1191,25 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(&effect2), -40.54);
+    ASSERT_EQ(target.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)
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color"), 12.4);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye"), -8.45);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl"), 50.15);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate"), -45.21);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic"), 0.2);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness"), -0.01);
+    target.setGraphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost"), 45.78);
+
     // color
-    /*vm.reset();
+    vm.reset();
     vm.setBytecode(bytecode3);
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color")), 1.2);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("color")), 1.2);
 
     // fisheye
     vm.reset();
@@ -756,7 +1217,7 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye")), 2.3);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("fisheye")), 2.3);
 
     // whirl
     vm.reset();
@@ -764,7 +1225,7 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl")), -3.4);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("whirl")), -3.4);
 
     // pixelate
     vm.reset();
@@ -772,7 +1233,7 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate")), -4.5);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("pixelate")), -4.5);
 
     // mosaic
     vm.reset();
@@ -780,7 +1241,7 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic")), 0.5);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("mosaic")), 0.5);
 
     // brightness
     vm.reset();
@@ -788,7 +1249,7 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness")), -8.54);
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("brightness")), -8.54);
 
     // ghost
     vm.reset();
@@ -796,7 +1257,9 @@ TEST_F(LooksBlocksTest, SetEffectToImpl)
     vm.run();
 
     ASSERT_EQ(vm.registerCount(), 0);
-    ASSERT_EQ(sprite.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost")), 0.01);*/
+    ASSERT_EQ(target.graphicsEffectValue(ScratchConfiguration::getGraphicsEffect("ghost")), 0.01);
+
+    removeEffects();
 }
 
 TEST_F(LooksBlocksTest, ClearGraphicEffects)
@@ -821,19 +1284,19 @@ 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;
+    Target target;
     GraphicsEffectMock effect1, effect2;
-    sprite.setGraphicsEffectValue(&effect1, 48.21);
-    sprite.setGraphicsEffectValue(&effect2, -107.08);
+    target.setGraphicsEffectValue(&effect1, 48.21);
+    target.setGraphicsEffectValue(&effect2, -107.08);
 
-    VirtualMachine vm(&sprite, nullptr, nullptr);
+    VirtualMachine vm(&target, 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);
+    ASSERT_EQ(target.graphicsEffectValue(&effect1), 0);
+    ASSERT_EQ(target.graphicsEffectValue(&effect2), 0);
 }
 
 TEST_F(LooksBlocksTest, ChangeSizeBy)
diff --git a/test/blocks/motion_blocks_test.cpp b/test/blocks/motion_blocks_test.cpp
index beb8f2d4..399b2030 100644
--- a/test/blocks/motion_blocks_test.cpp
+++ b/test/blocks/motion_blocks_test.cpp
@@ -1132,12 +1132,13 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
     vm.setBytecode(bytecode);
     vm.setFunctions(functions);
 
-    EXPECT_CALL(m_engineMock, stageWidth()).Times(9).WillRepeatedly(Return(480));
-    EXPECT_CALL(m_engineMock, stageHeight()).Times(9).WillRepeatedly(Return(360));
+    EXPECT_CALL(m_engineMock, stageWidth()).WillRepeatedly(Return(480));
+    EXPECT_CALL(m_engineMock, stageHeight()).WillRepeatedly(Return(360));
 
     // No edge
     EXPECT_CALL(m_engineMock, requestRedraw()).Times(3);
     EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(2).WillRepeatedly(Return(false));
+    EXPECT_CALL(handler, onMoved).Times(2);
     EXPECT_CALL(handler, onXChanged);
     EXPECT_CALL(handler, onYChanged);
     EXPECT_CALL(handler, onDirectionChanged);
@@ -1153,8 +1154,9 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
     ASSERT_EQ(sprite.direction(), -45);
 
     // Left edge
-    EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
-    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
+    EXPECT_CALL(m_engineMock, requestRedraw()).Times(4);
+    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(3).WillRepeatedly(Return(false));
+    EXPECT_CALL(handler, onMoved).Times(3);
     EXPECT_CALL(handler, onXChanged).Times(2);
     EXPECT_CALL(handler, onYChanged).Times(2);
     EXPECT_CALL(handler, onDirectionChanged);
@@ -1170,8 +1172,9 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
     ASSERT_EQ(std::round(sprite.direction() * 100) / 100, 45);
 
     // Top edge
-    EXPECT_CALL(m_engineMock, requestRedraw()).Times(6);
-    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
+    EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
+    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(3).WillRepeatedly(Return(false));
+    EXPECT_CALL(handler, onMoved).Times(3);
     EXPECT_CALL(handler, onXChanged).Times(2);
     EXPECT_CALL(handler, onYChanged).Times(2);
     EXPECT_CALL(handler, onDirectionChanged).Times(2);
@@ -1188,8 +1191,9 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
     ASSERT_EQ(sprite.direction(), 135);
 
     // Right edge
-    EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
-    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
+    EXPECT_CALL(m_engineMock, requestRedraw()).Times(4);
+    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(3).WillRepeatedly(Return(false));
+    EXPECT_CALL(handler, onMoved).Times(3);
     EXPECT_CALL(handler, onXChanged).Times(2);
     EXPECT_CALL(handler, onYChanged).Times(2);
     EXPECT_CALL(handler, onDirectionChanged);
@@ -1205,8 +1209,9 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
     ASSERT_EQ(sprite.direction(), -135);
 
     // Bottom edge
-    EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
-    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
+    EXPECT_CALL(m_engineMock, requestRedraw()).Times(4);
+    EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(3).WillRepeatedly(Return(false));
+    EXPECT_CALL(handler, onMoved).Times(3);
     EXPECT_CALL(handler, onXChanged).Times(2);
     EXPECT_CALL(handler, onYChanged).Times(2);
     EXPECT_CALL(handler, onDirectionChanged);
diff --git a/test/blocks/sensing_blocks_test.cpp b/test/blocks/sensing_blocks_test.cpp
index ddeb5a31..c287ba4b 100644
--- a/test/blocks/sensing_blocks_test.cpp
+++ b/test/blocks/sensing_blocks_test.cpp
@@ -18,6 +18,9 @@
 using namespace libscratchcpp;
 
 using ::testing::Return;
+using ::testing::ReturnRef;
+using ::testing::SaveArg;
+using ::testing::_;
 
 class SensingBlocksTest : public testing::Test
 {
@@ -92,6 +95,19 @@ class SensingBlocksTest : public testing::Test
         ClockMock m_clockMock;
 };
 
+struct QuestionSpy
+{
+        MOCK_METHOD(void, asked, (const std::string &), ());
+};
+
+template<typename T, typename... U>
+size_t getAddress(std::function<T(U...)> f)
+{
+    typedef T(fnType)(U...);
+    fnType **fnPointer = f.template target<fnType *>();
+    return (size_t)*fnPointer;
+}
+
 TEST_F(SensingBlocksTest, Name)
 {
     ASSERT_EQ(m_section->name(), "Sensing");
@@ -106,6 +122,8 @@ 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_askandwait", &SensingBlocks::compileAskAndWait));
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_answer", &SensingBlocks::compileAnswer));
     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));
@@ -127,6 +145,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
 
     // Inputs
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "DISTANCETOMENU", SensingBlocks::DISTANCETOMENU));
+    EXPECT_CALL(m_engineMock, addInput(m_section.get(), "QUESTION", SensingBlocks::QUESTION));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "KEY_OPTION", SensingBlocks::KEY_OPTION));
     EXPECT_CALL(m_engineMock, addInput(m_section.get(), "OBJECT", SensingBlocks::OBJECT));
 
@@ -156,7 +175,14 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "backdrop #", SensingBlocks::BackdropNumber));
     EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "backdrop name", SensingBlocks::BackdropName));
 
+    // Callbacks
+    std::function<void(const std::string &)> questionAnsweredRef = &SensingBlocks::onAnswer;
+    std::function<void(const std::string &)> questionAnswered;
+    EXPECT_CALL(m_engineMock, setQuestionAnswered(_)).WillOnce(SaveArg<0>(&questionAnswered));
+
     m_section->registerBlocks(&m_engineMock);
+    ASSERT_TRUE(questionAnswered);
+    ASSERT_EQ(getAddress(questionAnsweredRef), getAddress(questionAnswered));
 }
 
 TEST_F(SensingBlocksTest, DistanceTo)
@@ -290,6 +316,162 @@ TEST_F(SensingBlocksTest, DistanceToImpl)
     ASSERT_EQ(std::round(vm.getInput(0, 1)->toDouble() * 10000) / 10000, 261.0096);
 }
 
+TEST_F(SensingBlocksTest, AskAndWait)
+{
+    Compiler compiler(&m_engineMock);
+
+    // ask "test" and wait
+    auto block1 = std::make_shared<Block>("a", "sensing_askandwait");
+    addDropdownInput(block1, "QUESTION", SensingBlocks::QUESTION, "test");
+
+    // ask (null block) and wait
+    auto block2 = std::make_shared<Block>("b", "sensing_askandwait");
+    addDropdownInput(block2, "QUESTION", SensingBlocks::QUESTION, "", createNullBlock("c"));
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::askAndWait)).WillOnce(Return(0));
+    compiler.setBlock(block1);
+    SensingBlocks::compileAskAndWait(&compiler);
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::askAndWait)).WillOnce(Return(0));
+    compiler.setBlock(block2);
+    SensingBlocks::compileAskAndWait(&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, 0, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues().size(), 1);
+    ASSERT_EQ(compiler.constValues()[0].toString(), "test");
+}
+
+TEST_F(SensingBlocksTest, Answer)
+{
+    Compiler compiler(&m_engineMock);
+
+    auto block = std::make_shared<Block>("a", "sensing_answer");
+
+    compiler.init();
+
+    EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::answer)).WillOnce(Return(0));
+    compiler.setBlock(block);
+    SensingBlocks::compileAnswer(&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, AskAndWaitAndAnswerImpl)
+{
+    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, 0, vm::OP_HALT };
+    static unsigned int bytecode4[] = { vm::OP_START, vm::OP_EXEC, 1, vm::OP_HALT };
+    static BlockFunc functions[] = { &SensingBlocks::askAndWait, &SensingBlocks::answer };
+    static Value constValues[] = { "test1", "test2", "test3" };
+
+    Sprite sprite;
+    sprite.setBubbleType(Target::BubbleType::Think);
+    Stage stage;
+    QuestionSpy spy;
+    std::function<void(const std::string &)> asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1);
+
+    VirtualMachine vm1(&sprite, &m_engineMock, nullptr);
+    vm1.setFunctions(functions);
+    vm1.setConstValues(constValues);
+
+    // Ask 3 questions (2 where the sprite is visible and 1 where it's invisible)
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(spy, asked(""));
+    sprite.setVisible(true);
+    vm1.setBytecode(bytecode1);
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_FALSE(vm1.atEnd());
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(sprite.bubbleText(), "test1");
+
+    EXPECT_CALL(m_engineMock, questionAsked).Times(0);
+    vm1.reset();
+    vm1.setBytecode(bytecode2);
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_FALSE(vm1.atEnd());
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(sprite.bubbleText(), "test1");
+
+    EXPECT_CALL(m_engineMock, questionAsked).Times(0);
+    sprite.setVisible(false);
+    vm1.reset();
+    vm1.setBytecode(bytecode3);
+    vm1.run();
+    sprite.setVisible(true);
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_FALSE(vm1.atEnd());
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(sprite.bubbleText(), "test1");
+
+    // Ask a question from the stage
+    VirtualMachine vm2(&stage, &m_engineMock, nullptr);
+    vm2.setFunctions(functions);
+    vm2.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, questionAsked).Times(0);
+    vm2.setBytecode(bytecode2);
+    vm2.run();
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_FALSE(vm2.atEnd());
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(sprite.bubbleText(), "test1");
+
+    // Answer the questions
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(spy, asked(""));
+    SensingBlocks::onAnswer("hi");
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
+    ASSERT_EQ(sprite.bubbleText(), "test2");
+
+    vm1.reset();
+    vm1.setBytecode(bytecode4);
+    vm1.run();
+    ASSERT_EQ(vm1.registerCount(), 1);
+    ASSERT_EQ(vm1.getInput(0, 1)->toString(), "hi");
+
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(spy, asked("test3"));
+    SensingBlocks::onAnswer("hello");
+    ASSERT_TRUE(sprite.bubbleText().empty());
+
+    vm1.reset();
+    vm1.run();
+    ASSERT_EQ(vm1.registerCount(), 1);
+    ASSERT_EQ(vm1.getInput(0, 1)->toString(), "hello");
+
+    EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
+    EXPECT_CALL(spy, asked("test2"));
+    SensingBlocks::onAnswer("world");
+    ASSERT_TRUE(sprite.bubbleText().empty());
+    ASSERT_TRUE(stage.bubbleText().empty());
+
+    vm1.reset();
+    vm1.run();
+    ASSERT_EQ(vm1.registerCount(), 1);
+    ASSERT_EQ(vm1.getInput(0, 1)->toString(), "world");
+
+    EXPECT_CALL(m_engineMock, questionAsked).Times(0);
+    SensingBlocks::onAnswer("test");
+
+    vm1.reset();
+    vm1.run();
+    ASSERT_EQ(vm1.registerCount(), 1);
+    ASSERT_EQ(vm1.getInput(0, 1)->toString(), "test");
+}
+
 TEST_F(SensingBlocksTest, KeyPressed)
 {
     Compiler compiler(&m_engineMock);
diff --git a/test/blocks/variable_blocks_test.cpp b/test/blocks/variable_blocks_test.cpp
index 02f09fa9..079397ff 100644
--- a/test/blocks/variable_blocks_test.cpp
+++ b/test/blocks/variable_blocks_test.cpp
@@ -3,6 +3,8 @@
 #include <scratchcpp/input.h>
 #include <scratchcpp/field.h>
 #include <scratchcpp/variable.h>
+#include <scratchcpp/stage.h>
+#include <scratchcpp/monitor.h>
 #include <enginemock.h>
 
 #include "../common.h"
@@ -11,6 +13,8 @@
 
 using namespace libscratchcpp;
 
+using ::testing::Return;
+
 class VariableBlocksTest : public testing::Test
 {
     public:
@@ -70,6 +74,8 @@ TEST_F(VariableBlocksTest, RegisterBlocks)
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_variable", &VariableBlocks::compileVariable)).Times(1);
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_setvariableto", &VariableBlocks::compileSetVariable)).Times(1);
     EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_changevariableby", &VariableBlocks::compileChangeVariableBy)).Times(1);
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_showvariable", &VariableBlocks::compileShowVariable)).Times(1);
+    EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "data_hidevariable", &VariableBlocks::compileHideVariable)).Times(1);
 
     // Monitor names
     EXPECT_CALL(m_engineMock, addMonitorNameFunction(m_section.get(), "data_variable", &VariableBlocks::variableMonitorName));
@@ -208,3 +214,209 @@ TEST_F(VariableBlocksTest, ChangeVariableBy)
         }));
     ASSERT_TRUE(compiler.lists().empty());
 }
+
+TEST_F(VariableBlocksTest, ShowVariable)
+{
+    Compiler compiler(&m_engineMock);
+    Stage stage;
+    Target target;
+
+    // show variable [var1]
+    auto var1 = std::make_shared<Variable>("b", "var1");
+    var1->setTarget(&stage);
+    auto block1 = createVariableBlock("a", "data_showvariable", var1);
+
+    // show variable [var2]
+    auto var2 = std::make_shared<Variable>("d", "var2");
+    var2->setTarget(&target);
+    auto block2 = createVariableBlock("c", "data_showvariable", var2);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&VariableBlocks::showGlobalVariable)).WillOnce(Return(0));
+    compiler.init();
+    compiler.setBlock(block1);
+    VariableBlocks::compileShowVariable(&compiler);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&VariableBlocks::showVariable)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    VariableBlocks::compileShowVariable(&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, 1, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "b", "d" }));
+    ASSERT_TRUE(compiler.variables().empty());
+    ASSERT_TRUE(compiler.lists().empty());
+}
+
+TEST_F(VariableBlocksTest, ShowVariableImpl)
+{
+    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[] = { &VariableBlocks::showGlobalVariable, &VariableBlocks::showVariable };
+    static Value constValues[] = { "a", "b", "c", "d" };
+
+    auto var1 = std::make_shared<Variable>("b", "");
+    Monitor monitor1("b", "");
+    monitor1.setVisible(false);
+    var1->setMonitor(&monitor1);
+
+    auto var2 = std::make_shared<Variable>("d", "");
+    Monitor monitor2("d", "");
+    monitor2.setVisible(false);
+    var2->setMonitor(&monitor2);
+
+    Stage stage;
+    stage.addVariable(var1);
+
+    Target target;
+    target.addVariable(var2);
+
+    // Global
+    VirtualMachine vm1(&stage, &m_engineMock, nullptr);
+    vm1.setBytecode(bytecode1);
+    vm1.setFunctions(functions);
+    vm1.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.reset();
+    vm1.setBytecode(bytecode2);
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+
+    monitor1.setVisible(false);
+
+    // Local
+    VirtualMachine vm2(&target, &m_engineMock, nullptr);
+    vm2.setBytecode(bytecode3);
+    vm2.setFunctions(functions);
+    vm2.setConstValues(constValues);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+
+    vm2.reset();
+    vm2.setBytecode(bytecode4);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+}
+
+TEST_F(VariableBlocksTest, HideVariable)
+{
+    Compiler compiler(&m_engineMock);
+    Stage stage;
+    Target target;
+
+    // hide variable [var1]
+    auto var1 = std::make_shared<Variable>("b", "var1");
+    var1->setTarget(&stage);
+    auto block1 = createVariableBlock("a", "data_hidevariable", var1);
+
+    // hide variable [var2]
+    auto var2 = std::make_shared<Variable>("d", "var2");
+    var2->setTarget(&target);
+    auto block2 = createVariableBlock("c", "data_hidevariable", var2);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&VariableBlocks::hideGlobalVariable)).WillOnce(Return(0));
+    compiler.init();
+    compiler.setBlock(block1);
+    VariableBlocks::compileHideVariable(&compiler);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    EXPECT_CALL(m_engineMock, functionIndex(&VariableBlocks::hideVariable)).WillOnce(Return(1));
+    compiler.setBlock(block2);
+    VariableBlocks::compileHideVariable(&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, 1, vm::OP_HALT }));
+    ASSERT_EQ(compiler.constValues(), std::vector<Value>({ "b", "d" }));
+    ASSERT_TRUE(compiler.variables().empty());
+    ASSERT_TRUE(compiler.lists().empty());
+}
+
+TEST_F(VariableBlocksTest, HideVariableImpl)
+{
+    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[] = { &VariableBlocks::hideGlobalVariable, &VariableBlocks::hideVariable };
+    static Value constValues[] = { "a", "b", "c", "d" };
+
+    auto var1 = std::make_shared<Variable>("b", "");
+    Monitor monitor1("b", "");
+    monitor1.setVisible(true);
+    var1->setMonitor(&monitor1);
+
+    auto var2 = std::make_shared<Variable>("d", "");
+    Monitor monitor2("d", "");
+    monitor2.setVisible(true);
+    var2->setMonitor(&monitor2);
+
+    Stage stage;
+    stage.addVariable(var1);
+
+    Target target;
+    target.addVariable(var2);
+
+    // Global
+    VirtualMachine vm1(&stage, &m_engineMock, nullptr);
+    vm1.setBytecode(bytecode1);
+    vm1.setFunctions(functions);
+    vm1.setConstValues(constValues);
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+
+    EXPECT_CALL(m_engineMock, stage()).WillOnce(Return(&stage));
+    vm1.reset();
+    vm1.setBytecode(bytecode2);
+    vm1.run();
+
+    ASSERT_EQ(vm1.registerCount(), 0);
+    ASSERT_FALSE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+
+    monitor1.setVisible(true);
+
+    // Local
+    VirtualMachine vm2(&target, &m_engineMock, nullptr);
+    vm2.setBytecode(bytecode3);
+    vm2.setFunctions(functions);
+    vm2.setConstValues(constValues);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_TRUE(monitor2.visible());
+
+    vm2.reset();
+    vm2.setBytecode(bytecode4);
+    vm2.run();
+
+    ASSERT_EQ(vm2.registerCount(), 0);
+    ASSERT_TRUE(monitor1.visible());
+    ASSERT_FALSE(monitor2.visible());
+}
diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp
index 5d5aa5f6..73d83254 100644
--- a/test/engine/engine_test.cpp
+++ b/test/engine/engine_test.cpp
@@ -51,6 +51,14 @@ class AddRemoveMonitorMock
         MOCK_METHOD(void, monitorRemoved, (Monitor *, IMonitorHandler *));
 };
 
+template<typename T, typename... U>
+size_t getAddress(std::function<T(U...)> f)
+{
+    typedef T(fnType)(U...);
+    fnType **fnPointer = f.template target<fnType *>();
+    return (size_t)*fnPointer;
+}
+
 TEST(EngineTest, Clock)
 {
     Engine engine;
@@ -706,6 +714,67 @@ TEST(EngineTest, WhenKeyPressed)
     ASSERT_EQ(GET_VAR(stage, "4_pressed")->value().toInt(), 1);
 }
 
+TEST(EngineTest, MouseWheel)
+{
+    Project p("mouse_wheel.sb3");
+    ASSERT_TRUE(p.load());
+
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    // Initial state
+    ASSERT_VAR(stage, "up");
+    ASSERT_EQ(GET_VAR(stage, "up")->value().toInt(), 0);
+    ASSERT_VAR(stage, "down");
+    ASSERT_EQ(GET_VAR(stage, "down")->value().toInt(), 0);
+    ASSERT_VAR(stage, "any");
+    ASSERT_EQ(GET_VAR(stage, "any")->value().toInt(), 0);
+
+    // Up
+    engine->mouseWheelUp();
+    ASSERT_FALSE(engine->keyPressed("up arrow"));
+    engine->step();
+    ASSERT_VAR(stage, "up");
+    ASSERT_EQ(GET_VAR(stage, "up")->value().toInt(), 1);
+    ASSERT_VAR(stage, "down");
+    ASSERT_EQ(GET_VAR(stage, "down")->value().toInt(), 0);
+    ASSERT_VAR(stage, "any");
+    ASSERT_EQ(GET_VAR(stage, "any")->value().toInt(), 0);
+
+    engine->mouseWheelUp();
+    ASSERT_FALSE(engine->keyPressed("up arrow"));
+    engine->step();
+    ASSERT_VAR(stage, "up");
+    ASSERT_EQ(GET_VAR(stage, "up")->value().toInt(), 2);
+    ASSERT_VAR(stage, "down");
+    ASSERT_EQ(GET_VAR(stage, "down")->value().toInt(), 0);
+    ASSERT_VAR(stage, "any");
+    ASSERT_EQ(GET_VAR(stage, "any")->value().toInt(), 0);
+
+    // Down
+    engine->mouseWheelDown();
+    ASSERT_FALSE(engine->keyPressed("down arrow"));
+    engine->step();
+    ASSERT_VAR(stage, "up");
+    ASSERT_EQ(GET_VAR(stage, "up")->value().toInt(), 2);
+    ASSERT_VAR(stage, "down");
+    ASSERT_EQ(GET_VAR(stage, "down")->value().toInt(), 1);
+    ASSERT_VAR(stage, "any");
+    ASSERT_EQ(GET_VAR(stage, "any")->value().toInt(), 0);
+
+    engine->mouseWheelDown();
+    ASSERT_FALSE(engine->keyPressed("down arrow"));
+    engine->step();
+    ASSERT_VAR(stage, "up");
+    ASSERT_EQ(GET_VAR(stage, "up")->value().toInt(), 2);
+    ASSERT_VAR(stage, "down");
+    ASSERT_EQ(GET_VAR(stage, "down")->value().toInt(), 2);
+    ASSERT_VAR(stage, "any");
+    ASSERT_EQ(GET_VAR(stage, "any")->value().toInt(), 0);
+}
+
 TEST(EngineTest, MouseX)
 {
     Engine engine;
@@ -994,6 +1063,87 @@ TEST(EngineTest, Broadcasts)
     ASSERT_EQ(engine.findBroadcastById("c"), 2);
 }
 
+TEST(EngineTest, TargetClickScripts)
+{
+    Project p("target_click_scripts.sb3");
+    ASSERT_TRUE(p.load());
+    p.run();
+
+    auto engine = p.engine();
+
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+
+    // Initial state
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 0);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 0);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
+
+    // Sprite1
+    Target *sprite = engine->targetAt(engine->findTarget("Sprite1"));
+    ASSERT_TRUE(sprite);
+    engine->clickTarget(sprite);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 1);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 0);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
+
+    engine->clickTarget(sprite);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 2);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 0);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
+
+    // Sprite2
+    sprite = engine->targetAt(engine->findTarget("Sprite2"));
+    ASSERT_TRUE(sprite);
+    engine->clickTarget(sprite);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 2);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 1);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
+
+    engine->clickTarget(sprite);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 2);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 2);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 0);
+
+    // Stage
+    engine->clickTarget(stage);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 2);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 2);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 1);
+
+    engine->clickTarget(stage);
+    engine->step();
+    ASSERT_VAR(stage, "1");
+    ASSERT_EQ(GET_VAR(stage, "1")->value().toInt(), 2);
+    ASSERT_VAR(stage, "2");
+    ASSERT_EQ(GET_VAR(stage, "2")->value().toInt(), 2);
+    ASSERT_VAR(stage, "stage");
+    ASSERT_EQ(GET_VAR(stage, "stage")->value().toInt(), 2);
+}
+
 TEST(EngineTest, Targets)
 {
     Engine engine;
@@ -1400,6 +1550,30 @@ TEST(EngineTest, CreateMissingMonitors)
     }
 }
 
+void questionFunction(const std::string &)
+{
+}
+
+TEST(EngineTest, QuestionAsked)
+{
+    Engine engine;
+    ASSERT_EQ(engine.questionAsked(), nullptr);
+
+    static const std::function<void(const std::string &)> f = &questionFunction;
+    engine.setQuestionAsked(&questionFunction);
+    ASSERT_EQ(getAddress(engine.questionAsked()), getAddress(f));
+}
+
+TEST(EngineTest, QuestionAnswered)
+{
+    Engine engine;
+    ASSERT_EQ(engine.questionAnswered(), nullptr);
+
+    static const std::function<void(const std::string &)> f = &questionFunction;
+    engine.setQuestionAnswered(f);
+    ASSERT_EQ(getAddress(engine.questionAnswered()), getAddress(f));
+}
+
 TEST(EngineTest, Clones)
 {
     Project p("clones.sb3");
@@ -1593,6 +1767,15 @@ TEST(EngineTest, StopOtherScriptsInSprite)
     ASSERT_EQ(GET_VAR(stage, "l")->value().toInt(), 110);
 }
 
+TEST(EngineTest, UserAgent)
+{
+    Engine engine;
+    ASSERT_TRUE(engine.userAgent().empty());
+
+    engine.setUserAgent("test");
+    ASSERT_EQ(engine.userAgent(), "test");
+}
+
 TEST(EngineTest, NoCrashAfterStop)
 {
     // Regtest for #186
@@ -1702,3 +1885,27 @@ TEST(EngineTest, StopBeforeStarting)
     ASSERT_VAR(stage, "test");
     ASSERT_TRUE(GET_VAR(stage, "test")->value().toBool());
 }
+
+TEST(EngineTest, NoCrashWhenLoadingUndefinedVariableOrListMonitor)
+{
+    // Regtest for #445
+    Project p("regtest_projects/445_undefined_variable_or_list_monitor_crash.sb3");
+    ASSERT_TRUE(p.load());
+
+    auto engine = p.engine();
+
+    // The undefined variable and list should now be defined
+    Stage *stage = engine->stage();
+    ASSERT_TRUE(stage);
+    ASSERT_VAR(stage, "test");
+    auto var = GET_VAR(stage, "test");
+    ASSERT_EQ(var->id(), "E,r`5qYWCdXa~yj7nDS]");
+    ASSERT_EQ(var->value(), Value());
+
+    Target *sprite = engine->targetAt(engine->findTarget("Sprite1"));
+    ASSERT_TRUE(sprite);
+    ASSERT_LIST(sprite, "test");
+    auto list = GET_LIST(sprite, "test");
+    ASSERT_EQ(list->id(), "7a5rAs|X2_[1APT7@B1V");
+    ASSERT_TRUE(list->empty());
+}
diff --git a/test/load_project/load_project_test.cpp b/test/load_project/load_project_test.cpp
index f95c338a..dbacec3d 100644
--- a/test/load_project/load_project_test.cpp
+++ b/test/load_project/load_project_test.cpp
@@ -72,6 +72,8 @@ TEST(LoadProjectTest, EmptyProject)
                 backdrop->dataSize()),
             0);
 
+        ASSERT_EQ(engine->userAgent(), "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Scratux/1.4.1 Chrome/76.0.3809.146 Electron/6.1.7 Safari/537.36");
+
         i++;
     }
 }
@@ -460,6 +462,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "LIST");
         ASSERT_EQ(field->value(), "list1");
         ASSERT_EQ(field->valuePtr(), GET_LIST(stage, "list1"));
+        ASSERT_EQ(GET_LIST(stage, "list1")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), nullptr);
         ASSERT_EQ(monitor->width(), 167);
         ASSERT_EQ(monitor->height(), 93);
@@ -478,6 +481,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "LIST");
         ASSERT_EQ(field->value(), "list2");
         ASSERT_EQ(field->valuePtr(), GET_LIST(engine->targetAt(engine->findTarget("Sprite1")), "list2"));
+        ASSERT_EQ(GET_LIST(engine->targetAt(engine->findTarget("Sprite1")), "list2")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), dynamic_cast<Sprite *>(engine->targetAt(engine->findTarget("Sprite1"))));
         ASSERT_EQ(monitor->width(), 0);
         ASSERT_EQ(monitor->height(), 0);
@@ -496,6 +500,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "LIST");
         ASSERT_EQ(field->value(), "list2");
         ASSERT_EQ(field->valuePtr(), GET_LIST(engine->targetAt(engine->findTarget("Balloon1")), "list2"));
+        ASSERT_EQ(GET_LIST(engine->targetAt(engine->findTarget("Balloon1")), "list2")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), dynamic_cast<Sprite *>(engine->targetAt(engine->findTarget("Balloon1"))));
         ASSERT_EQ(monitor->width(), 0);
         ASSERT_EQ(monitor->height(), 0);
@@ -514,6 +519,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "VARIABLE");
         ASSERT_EQ(field->value(), "var1");
         ASSERT_EQ(field->valuePtr(), GET_VAR(stage, "var1"));
+        ASSERT_EQ(GET_VAR(stage, "var1")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), nullptr);
         ASSERT_EQ(monitor->width(), 0);
         ASSERT_EQ(monitor->height(), 0);
@@ -532,6 +538,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "VARIABLE");
         ASSERT_EQ(field->value(), "var2");
         ASSERT_EQ(field->valuePtr(), GET_VAR(engine->targetAt(engine->findTarget("Sprite1")), "var2"));
+        ASSERT_EQ(GET_VAR(engine->targetAt(engine->findTarget("Sprite1")), "var2")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), dynamic_cast<Sprite *>(engine->targetAt(engine->findTarget("Sprite1"))));
         ASSERT_EQ(monitor->width(), 0);
         ASSERT_EQ(monitor->height(), 0);
@@ -550,6 +557,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "VARIABLE");
         ASSERT_EQ(field->value(), "var2");
         ASSERT_EQ(field->valuePtr(), GET_VAR(engine->targetAt(engine->findTarget("Balloon1")), "var2"));
+        ASSERT_EQ(GET_VAR(engine->targetAt(engine->findTarget("Balloon1")), "var2")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), dynamic_cast<Sprite *>(engine->targetAt(engine->findTarget("Balloon1"))));
         ASSERT_EQ(monitor->width(), 0);
         ASSERT_EQ(monitor->height(), 0);
@@ -599,6 +607,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "VARIABLE");
         ASSERT_EQ(field->value(), "var4");
         ASSERT_EQ(field->valuePtr(), GET_VAR(stage, "var4"));
+        ASSERT_EQ(GET_VAR(stage, "var4")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), nullptr);
         ASSERT_EQ(monitor->width(), 0);
         ASSERT_EQ(monitor->height(), 0);
@@ -617,6 +626,7 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(field->name(), "VARIABLE");
         ASSERT_EQ(field->value(), "var5");
         ASSERT_EQ(field->valuePtr(), GET_VAR(stage, "var5"));
+        ASSERT_EQ(GET_VAR(stage, "var5")->monitor(), monitor.get());
         ASSERT_EQ(monitor->sprite(), nullptr);
         ASSERT_EQ(monitor->width(), 0);
         ASSERT_EQ(monitor->height(), 0);
@@ -624,6 +634,9 @@ TEST(LoadProjectTest, LoadTestProject)
         ASSERT_EQ(monitor->y(), 280);
         ASSERT_TRUE(monitor->visible());
 
+        // User agent
+        ASSERT_TRUE(engine->userAgent().empty());
+
         i++;
     }
 }
@@ -701,3 +714,29 @@ TEST(LoadProjectTest, ProjectInvalidTest)
     ASSERT_EQ(p.scratchVersion(), ScratchVersion::Invalid);
     ASSERT_FALSE(p.load());
 }
+
+TEST(LoadProjectTest, LoadNullDimensionMonitor)
+{
+    // Regtest for #448
+    std::string name = "regtest_projects/448_null_monitor_dimension.sb3";
+    Project p(name);
+    ASSERT_TRUE(p.load());
+}
+
+TEST(LoadProjectTest, LoadDoubleValue)
+{
+    // Regtest for #437
+    std::string oldLocale = std::setlocale(LC_NUMERIC, nullptr);
+    std::setlocale(LC_NUMERIC, "sk_SK.UTF-8");
+
+    std::string name = "regtest_projects/437_load_double_values.sb3";
+    Project p(name);
+    ASSERT_TRUE(p.load());
+
+    auto stage = p.engine()->stage();
+    ASSERT_TRUE(stage);
+    ASSERT_VAR(stage, "test");
+    ASSERT_EQ(GET_VAR(stage, "test")->value().toDouble(), 5.6654);
+
+    std::setlocale(LC_NUMERIC, oldLocale.c_str());
+}
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index feda73db..b7a89755 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -47,6 +47,9 @@ class EngineMock : public IEngine
         MOCK_METHOD(void, setKeyState, (const KeyEvent &, bool), (override));
         MOCK_METHOD(void, setAnyKeyPressed, (bool), (override));
 
+        MOCK_METHOD(void, mouseWheelUp, (), (override));
+        MOCK_METHOD(void, mouseWheelDown, (), (override));
+
         MOCK_METHOD(double, mouseX, (), (const, override));
         MOCK_METHOD(void, setMouseX, (double x), (override));
 
@@ -101,6 +104,7 @@ class EngineMock : public IEngine
         MOCK_METHOD(void, addBackdropChangeScript, (std::shared_ptr<Block>, int), (override));
         MOCK_METHOD(void, addCloneInitScript, (std::shared_ptr<Block>), (override));
         MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr<Block>, int), (override));
+        MOCK_METHOD(void, addTargetClickScript, (std::shared_ptr<Block>), (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));
@@ -120,10 +124,19 @@ class EngineMock : public IEngine
         MOCK_METHOD(void, setAddMonitorHandler, (const std::function<void(Monitor *)> &), (override));
         MOCK_METHOD(void, setRemoveMonitorHandler, (const std::function<void(Monitor *, IMonitorHandler *)> &), (override));
 
+        MOCK_METHOD(const std::function<void(const std::string &)> &, questionAsked, (), (const, override));
+        MOCK_METHOD(void, setQuestionAsked, (const std::function<void(const std::string &)> &), (override));
+
+        MOCK_METHOD(const std::function<void(const std::string &)> &, questionAnswered, (), (const, override));
+        MOCK_METHOD(void, setQuestionAnswered, (const std::function<void(const std::string &)> &), (override));
+
         MOCK_METHOD(std::vector<std::string> &, extensions, (), (const, override));
         MOCK_METHOD(void, setExtensions, (const std::vector<std::string> &), (override));
 
         MOCK_METHOD(const ScriptMap &, scripts, (), (const, override));
+
+        MOCK_METHOD(const std::string &, userAgent, (), (const, override));
+        MOCK_METHOD(void, setUserAgent, (const std::string &), (override));
 };
 
 } // namespace libscratchcpp
diff --git a/test/mocks/spritehandlermock.h b/test/mocks/spritehandlermock.h
index 7a3c3de0..5b9642ba 100644
--- a/test/mocks/spritehandlermock.h
+++ b/test/mocks/spritehandlermock.h
@@ -18,12 +18,15 @@ class SpriteHandlerMock : public ISpriteHandler
         MOCK_METHOD(void, onVisibleChanged, (bool), (override));
         MOCK_METHOD(void, onXChanged, (double), (override));
         MOCK_METHOD(void, onYChanged, (double), (override));
+        MOCK_METHOD(void, onMoved, (double, double, double, double), (override));
         MOCK_METHOD(void, onSizeChanged, (double), (override));
         MOCK_METHOD(void, onDirectionChanged, (double), (override));
         MOCK_METHOD(void, onRotationStyleChanged, (Sprite::RotationStyle), (override));
         MOCK_METHOD(void, onLayerOrderChanged, (int), (override));
         MOCK_METHOD(void, onGraphicsEffectChanged, (IGraphicsEffect *, double), (override));
         MOCK_METHOD(void, onGraphicsEffectsCleared, (), (override));
+        MOCK_METHOD(void, onBubbleTypeChanged, (Target::BubbleType), (override));
+        MOCK_METHOD(void, onBubbleTextChanged, (const std::string &), (override));
 
         MOCK_METHOD(Rect, boundingRect, (), (const, override));
 };
diff --git a/test/mocks/stagehandlermock.h b/test/mocks/stagehandlermock.h
index 4188eb8d..d00c569c 100644
--- a/test/mocks/stagehandlermock.h
+++ b/test/mocks/stagehandlermock.h
@@ -17,4 +17,6 @@ class StageHandlerMock : public IStageHandler
         MOCK_METHOD(void, onVideoTransparencyChanged, (int), (override));
         MOCK_METHOD(void, onGraphicsEffectChanged, (IGraphicsEffect *, double), (override));
         MOCK_METHOD(void, onGraphicsEffectsCleared, (), (override));
+        MOCK_METHOD(void, onBubbleTypeChanged, (Target::BubbleType), (override));
+        MOCK_METHOD(void, onBubbleTextChanged, (const std::string &), (override));
 };
diff --git a/test/mouse_wheel.sb3 b/test/mouse_wheel.sb3
new file mode 100644
index 00000000..c08fc6e9
Binary files /dev/null and b/test/mouse_wheel.sb3 differ
diff --git a/test/regtest_projects/437_load_double_values.sb3 b/test/regtest_projects/437_load_double_values.sb3
new file mode 100644
index 00000000..918a7ae1
Binary files /dev/null and b/test/regtest_projects/437_load_double_values.sb3 differ
diff --git a/test/regtest_projects/445_undefined_variable_or_list_monitor_crash.sb3 b/test/regtest_projects/445_undefined_variable_or_list_monitor_crash.sb3
new file mode 100644
index 00000000..87a9adc1
Binary files /dev/null and b/test/regtest_projects/445_undefined_variable_or_list_monitor_crash.sb3 differ
diff --git a/test/regtest_projects/448_null_monitor_dimension.sb3 b/test/regtest_projects/448_null_monitor_dimension.sb3
new file mode 100644
index 00000000..c52a77f2
Binary files /dev/null and b/test/regtest_projects/448_null_monitor_dimension.sb3 differ
diff --git a/test/scratch_classes/CMakeLists.txt b/test/scratch_classes/CMakeLists.txt
index 9098c9df..f7ab3cfd 100644
--- a/test/scratch_classes/CMakeLists.txt
+++ b/test/scratch_classes/CMakeLists.txt
@@ -222,6 +222,8 @@ gtest_discover_tests(comment_test)
 add_executable(
   monitor_test
   monitor_test.cpp
+  testsection.cpp
+  testsection.h
 )
 
 target_link_libraries(
diff --git a/test/scratch_classes/list_test.cpp b/test/scratch_classes/list_test.cpp
index 93a22670..2c01ec9a 100644
--- a/test/scratch_classes/list_test.cpp
+++ b/test/scratch_classes/list_test.cpp
@@ -1,5 +1,6 @@
 #include <scratchcpp/list.h>
 #include <scratchcpp/target.h>
+#include <scratchcpp/monitor.h>
 
 #include "../common.h"
 
@@ -29,6 +30,16 @@ TEST(ListTest, Target)
     ASSERT_EQ(list.target(), &target);
 }
 
+TEST(ListTest, Monitor)
+{
+    List list("", "");
+    ASSERT_EQ(list.monitor(), nullptr);
+
+    Monitor monitor("", "");
+    list.setMonitor(&monitor);
+    ASSERT_EQ(list.monitor(), &monitor);
+}
+
 TEST(ListTest, IndexOf)
 {
     List list("", "test list");
diff --git a/test/scratch_classes/monitor_test.cpp b/test/scratch_classes/monitor_test.cpp
index c8244240..618811ad 100644
--- a/test/scratch_classes/monitor_test.cpp
+++ b/test/scratch_classes/monitor_test.cpp
@@ -9,6 +9,7 @@
 #include <randomgeneratormock.h>
 
 #include "../common.h"
+#include "testsection.h"
 
 using namespace libscratchcpp;
 
@@ -47,6 +48,15 @@ TEST(MonitorTest, Interface)
     monitor.setInterface(&iface);
 }
 
+TEST(MonitorTest, Name)
+{
+    Monitor monitor("", "");
+    ASSERT_TRUE(monitor.name().empty());
+
+    monitor.setName("test");
+    ASSERT_EQ(monitor.name(), "test");
+}
+
 TEST(MonitorTest, Mode)
 {
     Monitor monitor("", "");
@@ -75,6 +85,16 @@ TEST(MonitorTest, Script)
     ASSERT_EQ(monitor.script(), script);
 }
 
+TEST(MonitorTest, BlockSection)
+{
+    Monitor monitor("", "");
+    ASSERT_EQ(monitor.blockSection(), nullptr);
+
+    auto section = std::make_shared<TestSection>();
+    monitor.setBlockSection(section);
+    ASSERT_EQ(monitor.blockSection(), section);
+}
+
 TEST(MonitorTest, Sprite)
 {
     Monitor monitor("", "");
diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp
index ac964315..0d9f1067 100644
--- a/test/scratch_classes/sprite_test.cpp
+++ b/test/scratch_classes/sprite_test.cpp
@@ -23,6 +23,17 @@ TEST(SpriteTest, IsStage)
     ASSERT_FALSE(sprite.isStage());
 }
 
+TEST(SpriteTest, Interface)
+{
+    Sprite sprite;
+    ASSERT_EQ(sprite.getInterface(), nullptr);
+
+    SpriteHandlerMock handler;
+    EXPECT_CALL(handler, init);
+    sprite.setInterface(&handler);
+    ASSERT_EQ(sprite.getInterface(), &handler);
+}
+
 TEST(SpriteTest, Visible)
 {
     Sprite sprite;
@@ -233,7 +244,7 @@ TEST(SpriteTest, XY)
 
     EngineMock engine;
     sprite.setEngine(&engine);
-    EXPECT_CALL(engine, requestRedraw()).Times(17);
+    EXPECT_CALL(engine, requestRedraw()).Times(18);
     EXPECT_CALL(engine, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
 
     sprite.setX(-53.25);
@@ -261,6 +272,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(319);
     ASSERT_EQ(sprite.x(), 319);
@@ -269,6 +281,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(75);
     ASSERT_EQ(sprite.x(), 75);
@@ -277,11 +290,13 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(400);
     ASSERT_EQ(sprite.x(), 344);
 
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(400);
     ASSERT_EQ(sprite.x(), 400);
@@ -290,11 +305,13 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(-400);
     ASSERT_EQ(sprite.x(), 155);
 
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onXChanged);
     sprite.setX(-400);
     ASSERT_EQ(sprite.x(), -400);
@@ -303,6 +320,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(150);
     ASSERT_EQ(sprite.y(), 150);
@@ -311,6 +329,7 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(-103);
     ASSERT_EQ(sprite.y(), -103);
@@ -319,11 +338,13 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(340);
     ASSERT_EQ(sprite.y(), 62);
 
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(340);
     ASSERT_EQ(sprite.y(), 340);
@@ -332,14 +353,24 @@ TEST(SpriteTest, XY)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     EXPECT_CALL(handler, boundingRect()).WillOnce(Return(rect));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(-340);
     ASSERT_EQ(sprite.y(), 86);
 
     EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    EXPECT_CALL(handler, onMoved);
     EXPECT_CALL(handler, onYChanged);
     sprite.setY(-340);
     ASSERT_EQ(sprite.y(), -340);
+
+    EXPECT_CALL(engine, spriteFencingEnabled()).WillOnce(Return(false));
+    EXPECT_CALL(handler, onMoved);
+    EXPECT_CALL(handler, onXChanged);
+    EXPECT_CALL(handler, onYChanged);
+    sprite.setPosition(56, -23);
+    ASSERT_EQ(sprite.x(), 56);
+    ASSERT_EQ(sprite.y(), -23);
 }
 
 TEST(SpriteTest, Size)
@@ -525,6 +556,7 @@ TEST(SpriteTest, KeepInFence)
 
     EXPECT_CALL(engine, requestRedraw()).Times(2);
     EXPECT_CALL(engine, spriteFencingEnabled()).Times(2).WillRepeatedly(Return(false));
+    EXPECT_CALL(handler, onMoved).Times(2);
     EXPECT_CALL(handler, onXChanged);
     EXPECT_CALL(handler, onYChanged);
     sprite.setX(100);
@@ -558,6 +590,18 @@ TEST(SpriteTest, KeepInFence)
     ASSERT_EQ(std::round(fencedY * 100) / 100, 150.9);
 }
 
+TEST(SpriteTest, DefaultBoundingRect)
+{
+    Sprite sprite;
+    sprite.setX(65.5);
+    sprite.setY(-45.01);
+    Rect rect = sprite.boundingRect();
+    ASSERT_EQ(rect.left(), 65.5);
+    ASSERT_EQ(rect.top(), -45.01);
+    ASSERT_EQ(rect.right(), 65.5);
+    ASSERT_EQ(rect.bottom(), -45.01);
+}
+
 TEST(SpriteTest, GraphicsEffects)
 {
     Sprite sprite;
@@ -588,3 +632,42 @@ TEST(SpriteTest, GraphicsEffects)
     ASSERT_EQ(sprite.graphicsEffectValue(&effect1), 0);
     ASSERT_EQ(sprite.graphicsEffectValue(&effect2), 0);
 }
+
+TEST(SpriteTest, BubbleType)
+{
+    Sprite sprite;
+    EngineMock engine;
+    sprite.setEngine(&engine);
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
+
+    EXPECT_CALL(engine, requestRedraw).Times(0);
+
+    sprite.setBubbleType(Target::BubbleType::Think);
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Think);
+
+    sprite.setBubbleType(Target::BubbleType::Say);
+    ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
+}
+
+TEST(SpriteTest, BubbleText)
+{
+    Sprite sprite;
+    EngineMock engine;
+    sprite.setVisible(true);
+    sprite.setEngine(&engine);
+    ASSERT_TRUE(sprite.bubbleText().empty());
+
+    EXPECT_CALL(engine, requestRedraw());
+    sprite.setBubbleText("hello");
+    ASSERT_EQ(sprite.bubbleText(), "hello");
+
+    EXPECT_CALL(engine, requestRedraw());
+    sprite.setBubbleText("world");
+    ASSERT_EQ(sprite.bubbleText(), "world");
+
+    sprite.setVisible(false);
+
+    EXPECT_CALL(engine, requestRedraw).Times(0);
+    sprite.setBubbleText("test");
+    ASSERT_TRUE(sprite.bubbleText().empty());
+}
diff --git a/test/scratch_classes/stage_test.cpp b/test/scratch_classes/stage_test.cpp
index 7f808d53..2a0a6048 100644
--- a/test/scratch_classes/stage_test.cpp
+++ b/test/scratch_classes/stage_test.cpp
@@ -2,6 +2,7 @@
 #include <scratchcpp/costume.h>
 #include <enginemock.h>
 #include <graphicseffectmock.h>
+#include <stagehandlermock.h>
 
 #include "../common.h"
 
@@ -13,6 +14,17 @@ TEST(StageTest, IsStage)
     ASSERT_TRUE(stage.isStage());
 }
 
+TEST(SpriteTest, Interface)
+{
+    Stage stage;
+    ASSERT_EQ(stage.getInterface(), nullptr);
+
+    StageHandlerMock handler;
+    EXPECT_CALL(handler, init);
+    stage.setInterface(&handler);
+    ASSERT_EQ(stage.getInterface(), &handler);
+}
+
 TEST(SpriteTest, CostumeIndex)
 {
     Stage stage;
@@ -129,6 +141,30 @@ TEST(StageTest, GraphicsEffects)
     ASSERT_EQ(stage.graphicsEffectValue(&effect2), 0);
 }
 
+TEST(StageTest, BubbleType)
+{
+    Stage stage;
+    ASSERT_EQ(stage.bubbleType(), Target::BubbleType::Say);
+
+    stage.setBubbleType(Target::BubbleType::Think);
+    ASSERT_EQ(stage.bubbleType(), Target::BubbleType::Think);
+
+    stage.setBubbleType(Target::BubbleType::Say);
+    ASSERT_EQ(stage.bubbleType(), Target::BubbleType::Say);
+}
+
+TEST(StageTest, BubbleText)
+{
+    Stage stage;
+    ASSERT_TRUE(stage.bubbleText().empty());
+
+    stage.setBubbleText("hello");
+    ASSERT_EQ(stage.bubbleText(), "hello");
+
+    stage.setBubbleText("world");
+    ASSERT_EQ(stage.bubbleText(), "world");
+}
+
 TEST(StageTest, LayerOrder)
 {
     Stage stage;
diff --git a/test/scratch_classes/target_test.cpp b/test/scratch_classes/target_test.cpp
index 9feabeb6..c0e2ffec 100644
--- a/test/scratch_classes/target_test.cpp
+++ b/test/scratch_classes/target_test.cpp
@@ -581,6 +581,30 @@ TEST(TargetTest, GraphicsEffects)
     ASSERT_EQ(target.graphicsEffectValue(&effect2), 0);
 }
 
+TEST(TargetTest, BubbleType)
+{
+    Target target;
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+
+    target.setBubbleType(Target::BubbleType::Think);
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Think);
+
+    target.setBubbleType(Target::BubbleType::Say);
+    ASSERT_EQ(target.bubbleType(), Target::BubbleType::Say);
+}
+
+TEST(TargetTest, BubbleText)
+{
+    Target target;
+    ASSERT_TRUE(target.bubbleText().empty());
+
+    target.setBubbleText("hello");
+    ASSERT_EQ(target.bubbleText(), "hello");
+
+    target.setBubbleText("world");
+    ASSERT_EQ(target.bubbleText(), "world");
+}
+
 TEST(TargetTest, Engine)
 {
     Target target;
diff --git a/test/scratch_classes/testsection.cpp b/test/scratch_classes/testsection.cpp
new file mode 100644
index 00000000..255e6938
--- /dev/null
+++ b/test/scratch_classes/testsection.cpp
@@ -0,0 +1,14 @@
+#include <scratchcpp/iengine.h>
+
+#include "testsection.h"
+
+using namespace libscratchcpp;
+
+std::string TestSection::name() const
+{
+    return "Test";
+}
+
+void TestSection::registerBlocks(IEngine *engine)
+{
+}
diff --git a/test/scratch_classes/testsection.h b/test/scratch_classes/testsection.h
new file mode 100644
index 00000000..78a0507d
--- /dev/null
+++ b/test/scratch_classes/testsection.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <scratchcpp/iblocksection.h>
+
+namespace libscratchcpp
+{
+
+class TestSection : public IBlockSection
+{
+    public:
+        std::string name() const override;
+
+        void registerBlocks(IEngine *engine) override;
+};
+
+} // namespace libscratchcpp
diff --git a/test/scratch_classes/value_test.cpp b/test/scratch_classes/value_test.cpp
index d2c2c71a..c46be444 100644
--- a/test/scratch_classes/value_test.cpp
+++ b/test/scratch_classes/value_test.cpp
@@ -1809,6 +1809,8 @@ TEST(ValueTest, EqualityOperators)
         Value v5 = " 5.25 ";
         Value v6 = "  5.25  ";
         Value v7 = "5.26";
+        Value v8 = 5;
+        Value v9 = "5  ";
 
         ASSERT_TRUE(v1 == v2);
         ASSERT_FALSE(v1 != v2);
@@ -1842,6 +1844,9 @@ TEST(ValueTest, EqualityOperators)
 
         ASSERT_FALSE(v6 == v7);
         ASSERT_TRUE(v6 != v7);
+
+        ASSERT_TRUE(v8 == v9);
+        ASSERT_FALSE(v8 != v9);
     }
 
     {
diff --git a/test/scratch_classes/variable_test.cpp b/test/scratch_classes/variable_test.cpp
index e669d8eb..c3abadfb 100644
--- a/test/scratch_classes/variable_test.cpp
+++ b/test/scratch_classes/variable_test.cpp
@@ -1,5 +1,6 @@
 #include <scratchcpp/variable.h>
 #include <scratchcpp/target.h>
+#include <scratchcpp/monitor.h>
 
 #include "../common.h"
 
@@ -68,6 +69,16 @@ TEST(VariableTest, Target)
     ASSERT_EQ(var.target(), &target);
 }
 
+TEST(VariableTest, Monitor)
+{
+    Variable var("", "");
+    ASSERT_EQ(var.monitor(), nullptr);
+
+    Monitor monitor("", "");
+    var.setMonitor(&monitor);
+    ASSERT_EQ(var.monitor(), &monitor);
+}
+
 TEST(VariableTest, Clone)
 {
     std::shared_ptr<Variable> clone;
diff --git a/test/target_click_scripts.sb3 b/test/target_click_scripts.sb3
new file mode 100644
index 00000000..ba8a0233
Binary files /dev/null and b/test/target_click_scripts.sb3 differ
diff --git a/test/target_interfaces/ispritehandler_test.cpp b/test/target_interfaces/ispritehandler_test.cpp
index 42bd6fd9..777fb2ed 100644
--- a/test/target_interfaces/ispritehandler_test.cpp
+++ b/test/target_interfaces/ispritehandler_test.cpp
@@ -63,6 +63,7 @@ TEST_F(ISpriteHandlerTest, Visible)
 
 TEST_F(ISpriteHandlerTest, X)
 {
+    EXPECT_CALL(m_handler, onMoved(0, 0, 189.46, 0));
     EXPECT_CALL(m_handler, onXChanged(189.46)).Times(1);
     EXPECT_CALL(m_handler, boundingRect()).WillOnce(Return(Rect()));
     EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(true));
@@ -71,6 +72,7 @@ TEST_F(ISpriteHandlerTest, X)
     EXPECT_CALL(m_engine, requestRedraw());
     m_sprite.setX(189.46);
 
+    EXPECT_CALL(m_handler, onMoved(189.46, 0, 284.61, 0));
     EXPECT_CALL(m_handler, onXChanged(284.61)).Times(1);
     EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(false));
     EXPECT_CALL(m_engine, requestRedraw());
@@ -79,6 +81,7 @@ TEST_F(ISpriteHandlerTest, X)
 
 TEST_F(ISpriteHandlerTest, Y)
 {
+    EXPECT_CALL(m_handler, onMoved(0, 0, 0, -153.7)).Times(1);
     EXPECT_CALL(m_handler, onYChanged(-153.7)).Times(1);
     EXPECT_CALL(m_handler, boundingRect()).WillOnce(Return(Rect()));
     EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(true));
@@ -87,12 +90,30 @@ TEST_F(ISpriteHandlerTest, Y)
     EXPECT_CALL(m_engine, requestRedraw());
     m_sprite.setY(-153.7);
 
+    EXPECT_CALL(m_handler, onMoved(0, -153.7, 0, 207.08)).Times(1);
     EXPECT_CALL(m_handler, onYChanged(207.08)).Times(1);
     EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(false));
     EXPECT_CALL(m_engine, requestRedraw());
     m_sprite.setY(207.08);
 }
 
+TEST_F(ISpriteHandlerTest, Position)
+{
+    EXPECT_CALL(m_handler, onMoved(0, 0, 284.61, 207.08)).Times(1);
+    EXPECT_CALL(m_handler, onXChanged(284.61));
+    EXPECT_CALL(m_handler, onYChanged(207.08));
+    EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(false));
+    EXPECT_CALL(m_engine, requestRedraw());
+    m_sprite.setPosition(284.61, 207.08);
+
+    EXPECT_CALL(m_handler, onMoved(284.61, 207.08, -45.9, -20.05)).Times(1);
+    EXPECT_CALL(m_handler, onXChanged(-45.9));
+    EXPECT_CALL(m_handler, onYChanged(-20.05));
+    EXPECT_CALL(m_engine, spriteFencingEnabled()).WillOnce(Return(false));
+    EXPECT_CALL(m_engine, requestRedraw());
+    m_sprite.setPosition(-45.9, -20.05);
+}
+
 TEST_F(ISpriteHandlerTest, Size)
 {
     EXPECT_CALL(m_handler, onSizeChanged(53.8)).Times(1);
@@ -153,6 +174,22 @@ TEST_F(ISpriteHandlerTest, GraphicsEffects)
     m_sprite.clearGraphicsEffects();
 }
 
+TEST_F(ISpriteHandlerTest, BubbleType)
+{
+    EXPECT_CALL(m_handler, onBubbleTypeChanged(Target::BubbleType::Say));
+    m_sprite.setBubbleType(Target::BubbleType::Say);
+
+    EXPECT_CALL(m_handler, onBubbleTypeChanged(Target::BubbleType::Think));
+    m_sprite.setBubbleType(Target::BubbleType::Think);
+}
+
+TEST_F(ISpriteHandlerTest, BubbleText)
+{
+    EXPECT_CALL(m_handler, onBubbleTextChanged("test"));
+    EXPECT_CALL(m_engine, requestRedraw());
+    m_sprite.setBubbleText("test");
+}
+
 TEST_F(ISpriteHandlerTest, BoundingRect)
 {
     EXPECT_CALL(m_handler, boundingRect()).WillOnce(Return(Rect(-44.6, 89.1, 20.5, -0.48)));
diff --git a/test/target_interfaces/istagehandler_test.cpp b/test/target_interfaces/istagehandler_test.cpp
index 38119aa1..a109624a 100644
--- a/test/target_interfaces/istagehandler_test.cpp
+++ b/test/target_interfaces/istagehandler_test.cpp
@@ -66,3 +66,19 @@ TEST_F(IStageHandlerTest, GraphicsEffects)
     EXPECT_CALL(m_engine, requestRedraw());
     m_stage.clearGraphicsEffects();
 }
+
+TEST_F(IStageHandlerTest, BubbleType)
+{
+    EXPECT_CALL(m_handler, onBubbleTypeChanged(Target::BubbleType::Say));
+    m_stage.setBubbleType(Target::BubbleType::Say);
+
+    EXPECT_CALL(m_handler, onBubbleTypeChanged(Target::BubbleType::Think));
+    m_stage.setBubbleType(Target::BubbleType::Think);
+}
+
+TEST_F(IStageHandlerTest, BubbleText)
+{
+    EXPECT_CALL(m_handler, onBubbleTextChanged("test"));
+    EXPECT_CALL(m_engine, requestRedraw());
+    m_stage.setBubbleText("test");
+}
diff --git a/test/virtual_machine/virtual_machine_test.cpp b/test/virtual_machine/virtual_machine_test.cpp
index 587d562b..27819ba2 100644
--- a/test/virtual_machine/virtual_machine_test.cpp
+++ b/test/virtual_machine/virtual_machine_test.cpp
@@ -1466,6 +1466,27 @@ TEST(VirtualMachineTest, RunProcedures)
     ASSERT_EQ(vm.registerCount(), 0);
 }
 
+TEST(VirtualMachineTest, RunNestedWarpProcedures)
+{
+    static unsigned int bytecode[] = { OP_START, OP_INIT_PROCEDURE, OP_CALL_PROCEDURE, 0, OP_HALT };
+    static unsigned int procedure1[] = { OP_START, OP_INIT_PROCEDURE, OP_CALL_PROCEDURE, 1, OP_CONST, 0, OP_REPEAT_LOOP, OP_BREAK_FRAME, OP_LOOP_END, OP_HALT };
+    static unsigned int procedure2[] = { OP_START, OP_WARP, OP_CONST, 0, OP_REPEAT_LOOP, OP_BREAK_FRAME, OP_LOOP_END, OP_HALT };
+    static unsigned int *procedures[] = { procedure1, procedure2 };
+    static Value constValues[] = { 1 };
+
+    VirtualMachine vm;
+    vm.setBytecode(bytecode);
+    vm.setProcedures(procedures);
+    vm.setConstValues(constValues);
+    vm.run();
+    ASSERT_FALSE(vm.atEnd()); // the procedure should still run in warp mode even after running a warp procedure (#470)
+    ASSERT_EQ(vm.registerCount(), 0);
+
+    vm.run();
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 0);
+}
+
 TEST(VirtualMachineTest, RunUndefinedProcedure)
 {
     static unsigned int bytecode[] = {
@@ -1639,6 +1660,35 @@ TEST(VirtualMachineTest, ResetAndKill)
     ASSERT_TRUE(vm.atEnd());
 }
 
+unsigned int promiseTest(VirtualMachine *vm)
+{
+    vm->promise();
+    return 1;
+}
+
+TEST(VirtualMachineTest, Promise)
+{
+    static unsigned int bytecode[] = { OP_START, OP_NULL, OP_EXEC, 0, vm::OP_NULL, vm::OP_NULL, vm::OP_NULL, OP_HALT };
+    static BlockFunc functions[] = { &promiseTest };
+
+    VirtualMachine vm;
+    vm.setBytecode(bytecode);
+    vm.setFunctions(functions);
+
+    vm.run();
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 0);
+
+    vm.run();
+    ASSERT_FALSE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 0);
+
+    vm.resolvePromise();
+    vm.run();
+    ASSERT_TRUE(vm.atEnd());
+    ASSERT_EQ(vm.registerCount(), 3);
+}
+
 TEST(VirtualMachineTest, NoCrashWhenRepeatingZeroTimes)
 {
     // Regtest for #362
