feature: add scripted transition condition protocol (#11479) 853b2a08b5
* feature: add scripted transition condition protocol

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index a1a4e90..93a602c 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-3253c0beae3bdc2d68db7cf52eb4490e17276c58
+853b2a08b5743038ac40d2d4b80697d8983a23df
diff --git a/dev/defs/animation/scripted_transition_condition.json b/dev/defs/animation/scripted_transition_condition.json
new file mode 100644
index 0000000..3ffba7e
--- /dev/null
+++ b/dev/defs/animation/scripted_transition_condition.json
@@ -0,0 +1,20 @@
+{
+  "name": "ScriptedTransitionCondition",
+  "key": {
+    "int": 647,
+    "string": "scriptedtransitioncondition"
+  },
+  "extends": "animation/transition_condition.json",
+  "properties": {
+    "scriptAssetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 931,
+        "string": "scriptassetid"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/include/rive/animation/scripted_listener_action.hpp b/include/rive/animation/scripted_listener_action.hpp
index b464337..32e615f 100644
--- a/include/rive/animation/scripted_listener_action.hpp
+++ b/include/rive/animation/scripted_listener_action.hpp
@@ -30,6 +30,7 @@
     Component* component() override { return nullptr; }
     StatusCode import(ImportStack& importStack) override;
     Core* clone() const override;
+    ScriptedObject* cloneScriptedObject() const override;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/scripted_transition_condition.hpp b/include/rive/animation/scripted_transition_condition.hpp
new file mode 100644
index 0000000..e26683a
--- /dev/null
+++ b/include/rive/animation/scripted_transition_condition.hpp
@@ -0,0 +1,32 @@
+#ifndef _RIVE_SCRIPTED_TRANSITION_CONDITION_HPP_
+#define _RIVE_SCRIPTED_TRANSITION_CONDITION_HPP_
+#include "rive/generated/animation/scripted_transition_condition_base.hpp"
+#include "rive/scripted/scripted_object.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ScriptedTransitionCondition : public ScriptedTransitionConditionBase,
+                                    public ScriptedObject
+{
+public:
+    bool evaluate(const StateMachineInstance* stateMachineInstance,
+                  StateMachineLayerInstance* layerInstance) const override;
+    bool evaluateStateful(const StateMachineInstance* stateMachineInstance,
+                          StateMachineLayerInstance* layerInstance) const;
+    uint32_t assetId() override { return scriptAssetId(); }
+    bool addScriptedDirt(ComponentDirt value, bool recurse = false) override
+    {
+        return false;
+    }
+    ScriptProtocol scriptProtocol() override
+    {
+        return ScriptProtocol::transitionCondition;
+    }
+    Component* component() override { return nullptr; }
+    StatusCode import(ImportStack& importStack) override;
+    Core* clone() const override;
+    ScriptedObject* cloneScriptedObject() const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine.hpp b/include/rive/animation/state_machine.hpp
index 258fe61..ff441ca 100644
--- a/include/rive/animation/state_machine.hpp
+++ b/include/rive/animation/state_machine.hpp
@@ -10,6 +10,7 @@
 class StateMachineInput;
 class StateMachineListener;
 class StateMachineImporter;
+class ScriptedObject;
 class DataBind;
 class StateMachine : public StateMachineBase
 {
@@ -20,6 +21,7 @@
     std::vector<std::unique_ptr<StateMachineInput>> m_Inputs;
     std::vector<std::unique_ptr<StateMachineListener>> m_Listeners;
     std::vector<std::unique_ptr<DataBind>> m_dataBinds;
+    std::vector<ScriptedObject*> m_scriptedObjects;
 
     void addLayer(std::unique_ptr<StateMachineLayer>);
     void addInput(std::unique_ptr<StateMachineInput>);
@@ -36,6 +38,11 @@
     size_t inputCount() const { return m_Inputs.size(); }
     size_t listenerCount() const { return m_Listeners.size(); }
     size_t dataBindCount() const { return m_dataBinds.size(); }
+    void addScriptedObject(ScriptedObject* object);
+    std::vector<ScriptedObject*> scriptedObjects() const
+    {
+        return m_scriptedObjects;
+    }
 
     const StateMachineInput* input(std::string name) const;
     const StateMachineInput* input(size_t index) const;
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index 641630c..18f3437 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -206,7 +206,7 @@
     bool hasListeners() { return m_hitComponents.size() > 0; }
     void clearDataContext();
     void internalDataContext(DataContext* dataContext);
-    ScriptedObject* scriptedObject(const ScriptedObject*);
+    ScriptedObject* scriptedObject(const ScriptedObject*) const;
 #ifdef TESTING
     size_t hitComponentsCount() { return m_hitComponents.size(); };
     HitComponent* hitComponent(size_t index)
diff --git a/include/rive/assets/script_asset.hpp b/include/rive/assets/script_asset.hpp
index 6de0360..d62f942 100644
--- a/include/rive/assets/script_asset.hpp
+++ b/include/rive/assets/script_asset.hpp
@@ -26,7 +26,8 @@
     layout,
     converter,
     pathEffect,
-    listenerAction
+    listenerAction,
+    transitionCondition
 };
 
 #ifdef WITH_RIVE_SCRIPTING
diff --git a/include/rive/generated/animation/scripted_transition_condition_base.hpp b/include/rive/generated/animation/scripted_transition_condition_base.hpp
new file mode 100644
index 0000000..01d08b5
--- /dev/null
+++ b/include/rive/generated/animation/scripted_transition_condition_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_SCRIPTED_TRANSITION_CONDITION_BASE_HPP_
+#define _RIVE_SCRIPTED_TRANSITION_CONDITION_BASE_HPP_
+#include "rive/animation/transition_condition.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ScriptedTransitionConditionBase : public TransitionCondition
+{
+protected:
+    typedef TransitionCondition Super;
+
+public:
+    static const uint16_t typeKey = 647;
+
+    /// Helper to quickly determine if a core object extends another without
+    /// RTTI at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ScriptedTransitionConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t scriptAssetIdPropertyKey = 931;
+
+protected:
+    uint32_t m_ScriptAssetId = -1;
+
+public:
+    inline uint32_t scriptAssetId() const { return m_ScriptAssetId; }
+    void scriptAssetId(uint32_t value)
+    {
+        if (m_ScriptAssetId == value)
+        {
+            return;
+        }
+        m_ScriptAssetId = value;
+        scriptAssetIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ScriptedTransitionConditionBase& object)
+    {
+        m_ScriptAssetId = object.m_ScriptAssetId;
+        TransitionCondition::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case scriptAssetIdPropertyKey:
+                m_ScriptAssetId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TransitionCondition::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void scriptAssetIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index 4a541ae..1bd5d13 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -51,6 +51,7 @@
 #include "rive/animation/nested_state_machine.hpp"
 #include "rive/animation/nested_trigger.hpp"
 #include "rive/animation/scripted_listener_action.hpp"
+#include "rive/animation/scripted_transition_condition.hpp"
 #include "rive/animation/state_machine.hpp"
 #include "rive/animation/state_machine_bool.hpp"
 #include "rive/animation/state_machine_component.hpp"
@@ -508,6 +509,8 @@
                 return new ListenerBoolChange();
             case ListenerAlignTargetBase::typeKey:
                 return new ListenerAlignTarget();
+            case ScriptedTransitionConditionBase::typeKey:
+                return new ScriptedTransitionCondition();
             case TransitionNumberConditionBase::typeKey:
                 return new TransitionNumberCondition();
             case TransitionValueBooleanComparatorBase::typeKey:
@@ -1242,6 +1245,10 @@
             case ListenerAlignTargetBase::targetIdPropertyKey:
                 object->as<ListenerAlignTargetBase>()->targetId(value);
                 break;
+            case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey:
+                object->as<ScriptedTransitionConditionBase>()->scriptAssetId(
+                    value);
+                break;
             case TransitionValueConditionBase::opValuePropertyKey:
                 object->as<TransitionValueConditionBase>()->opValue(value);
                 break;
@@ -2775,6 +2782,9 @@
                 return object->as<ListenerBoolChangeBase>()->value();
             case ListenerAlignTargetBase::targetIdPropertyKey:
                 return object->as<ListenerAlignTargetBase>()->targetId();
+            case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey:
+                return object->as<ScriptedTransitionConditionBase>()
+                    ->scriptAssetId();
             case TransitionValueConditionBase::opValuePropertyKey:
                 return object->as<TransitionValueConditionBase>()->opValue();
             case TransitionViewModelConditionBase::opValuePropertyKey:
@@ -3749,6 +3759,7 @@
             case KeyFrameIdBase::valuePropertyKey:
             case ListenerBoolChangeBase::valuePropertyKey:
             case ListenerAlignTargetBase::targetIdPropertyKey:
+            case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey:
             case TransitionValueConditionBase::opValuePropertyKey:
             case TransitionViewModelConditionBase::opValuePropertyKey:
             case BlendState1DInputBase::inputIdPropertyKey:
@@ -4416,6 +4427,8 @@
                 return object->is<ListenerBoolChangeBase>();
             case ListenerAlignTargetBase::targetIdPropertyKey:
                 return object->is<ListenerAlignTargetBase>();
+            case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey:
+                return object->is<ScriptedTransitionConditionBase>();
             case TransitionValueConditionBase::opValuePropertyKey:
                 return object->is<TransitionValueConditionBase>();
             case TransitionViewModelConditionBase::opValuePropertyKey:
diff --git a/include/rive/importers/state_machine_importer.hpp b/include/rive/importers/state_machine_importer.hpp
index dc08236..2d8c402 100644
--- a/include/rive/importers/state_machine_importer.hpp
+++ b/include/rive/importers/state_machine_importer.hpp
@@ -10,6 +10,7 @@
 class StateMachineListener;
 class StateMachine;
 class DataBind;
+class ScriptedObject;
 class StateMachineImporter : public ImportStackObject
 {
 private:
@@ -23,6 +24,7 @@
     void addInput(std::unique_ptr<StateMachineInput>);
     void addListener(std::unique_ptr<StateMachineListener>);
     void addDataBind(std::unique_ptr<DataBind>);
+    void addScriptedObject(ScriptedObject* object);
 
     StatusCode resolve() override;
     bool readNullObject() override;
diff --git a/include/rive/scripted/scripted_object.hpp b/include/rive/scripted/scripted_object.hpp
index c712716..18e9182 100644
--- a/include/rive/scripted/scripted_object.hpp
+++ b/include/rive/scripted/scripted_object.hpp
@@ -63,6 +63,7 @@
     virtual ScriptProtocol scriptProtocol() = 0;
     int self() { return m_self; }
     virtual Component* component() = 0;
+    virtual ScriptedObject* cloneScriptedObject() const { return nullptr; }
 };
 } // namespace rive
 
diff --git a/src/animation/scripted_listener_action.cpp b/src/animation/scripted_listener_action.cpp
index 4b3b959..db217cc 100644
--- a/src/animation/scripted_listener_action.cpp
+++ b/src/animation/scripted_listener_action.cpp
@@ -1,5 +1,6 @@
 #include "rive/animation/scripted_listener_action.hpp"
 #include "rive/animation/state_machine_instance.hpp"
+#include "rive/importers/state_machine_importer.hpp"
 
 using namespace rive;
 
@@ -66,6 +67,14 @@
     {
         return result;
     }
+
+    auto stateMachineImporter =
+        importStack.latest<StateMachineImporter>(StateMachine::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    stateMachineImporter->addScriptedObject(this);
     return Super::import(importStack);
 }
 
@@ -83,4 +92,11 @@
         twin->addProperty(clonedValue);
     }
     return twin;
+}
+
+ScriptedObject* ScriptedListenerAction::cloneScriptedObject() const
+{
+    auto clonedScriptedObject = clone()->as<ScriptedListenerAction>();
+    clonedScriptedObject->reinit();
+    return clonedScriptedObject;
 }
\ No newline at end of file
diff --git a/src/animation/scripted_transition_condition.cpp b/src/animation/scripted_transition_condition.cpp
new file mode 100644
index 0000000..8ea3eda
--- /dev/null
+++ b/src/animation/scripted_transition_condition.cpp
@@ -0,0 +1,101 @@
+#include "rive/animation/scripted_transition_condition.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+
+using namespace rive;
+
+// Note: performStateful is the actual instance of the ScriptedListenerAction
+// that will run the script. perform itself will look for the map between the
+// stateless and the stateful instances of this class.
+bool ScriptedTransitionCondition::evaluateStateful(
+    const StateMachineInstance* stateMachineInstance,
+    StateMachineLayerInstance* layerInstance) const
+{
+    bool result = false;
+#ifdef WITH_RIVE_SCRIPTING
+    if (m_state)
+    {
+        // Stack: []
+        rive_lua_pushRef(m_state, m_self);
+        // Stack: [self]
+        lua_getfield(m_state, -1, "evaluate");
+
+        // Stack: [self, field]
+        lua_insert(m_state, -2); // Swap self and field
+
+        // Stack: [field, self]
+        if (static_cast<lua_Status>(rive_lua_pcall(m_state, 1, 1)) == LUA_OK)
+        {
+            if (lua_isboolean(m_state, -1))
+            {
+                result = lua_toboolean(m_state, -1);
+            }
+            // Stack: [result]
+            rive_lua_pop(m_state, 1);
+        }
+        else
+        {
+            // Stack: [status]
+            rive_lua_pop(m_state, 1);
+        }
+    }
+#endif
+    return result;
+}
+
+bool ScriptedTransitionCondition::evaluate(
+    const StateMachineInstance* stateMachineInstance,
+    StateMachineLayerInstance* layerInstance) const
+{
+#ifdef WITH_RIVE_SCRIPTING
+    auto scriptedObject = stateMachineInstance->scriptedObject(this);
+    if (scriptedObject != nullptr)
+    {
+        auto statefulListenerAction =
+            static_cast<ScriptedTransitionCondition*>(scriptedObject);
+        return statefulListenerAction->evaluateStateful(stateMachineInstance,
+                                                        layerInstance);
+    }
+#endif
+    return false;
+}
+
+StatusCode ScriptedTransitionCondition::import(ImportStack& importStack)
+{
+    auto result = registerReferencer(importStack);
+    if (result != StatusCode::Ok)
+    {
+        return result;
+    }
+    auto stateMachineImporter =
+        importStack.latest<StateMachineImporter>(StateMachine::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    stateMachineImporter->addScriptedObject(this);
+    return Super::import(importStack);
+}
+
+Core* ScriptedTransitionCondition::clone() const
+{
+    ScriptedTransitionCondition* twin = ScriptedTransitionConditionBase::clone()
+                                            ->as<ScriptedTransitionCondition>();
+    if (m_fileAsset != nullptr)
+    {
+        twin->setAsset(m_fileAsset);
+    }
+    for (auto prop : m_customProperties)
+    {
+        auto clonedValue = prop->clone()->as<CustomProperty>();
+        twin->addProperty(clonedValue);
+    }
+    return twin;
+}
+
+ScriptedObject* ScriptedTransitionCondition::cloneScriptedObject() const
+{
+    auto clonedScriptedObject = clone()->as<ScriptedTransitionCondition>();
+    clonedScriptedObject->reinit();
+    return clonedScriptedObject;
+}
\ No newline at end of file
diff --git a/src/animation/state_machine.cpp b/src/animation/state_machine.cpp
index dbea78e..be9cc59 100644
--- a/src/animation/state_machine.cpp
+++ b/src/animation/state_machine.cpp
@@ -4,6 +4,7 @@
 #include "rive/animation/state_machine_layer.hpp"
 #include "rive/animation/state_machine_input.hpp"
 #include "rive/animation/state_machine_listener.hpp"
+#include "rive/scripted/scripted_object.hpp"
 #include "rive/data_bind/data_bind.hpp"
 
 using namespace rive;
@@ -156,4 +157,9 @@
         return m_dataBinds[index].get();
     }
     return nullptr;
+}
+
+void StateMachine::addScriptedObject(ScriptedObject* object)
+{
+    m_scriptedObjects.push_back(object);
 }
\ No newline at end of file
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index 964b7a2..226bb45 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -8,6 +8,7 @@
 #include "rive/animation/layer_state_flags.hpp"
 #include "rive/animation/nested_linear_animation.hpp"
 #include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/scripted_transition_condition.hpp"
 #include "rive/animation/state_instance.hpp"
 #include "rive/animation/state_machine_bool.hpp"
 #include "rive/animation/state_machine_input_instance.hpp"
@@ -1482,32 +1483,18 @@
             this);
         m_hitComponents.push_back(std::move(hc));
     }
-    // Initialize local instances of ScriptedListenerActions
-    for (std::size_t i = 0; i < machine->listenerCount(); i++)
-    {
-        auto listener = machine->listener(i);
 
-        for (std::size_t j = 0; j < listener->actionCount(); j++)
-        {
-            auto action = listener->action(j);
-            if (action->is<ScriptedListenerAction>())
-            {
-                auto scriptedListenerAction =
-                    action->as<ScriptedListenerAction>();
-                auto scriptedListenerActionClone =
-                    static_cast<ScriptedListenerAction*>(
-                        scriptedListenerAction->clone());
-                scriptedListenerActionClone->reinit();
-                m_scriptedListenerActionsMap[scriptedListenerAction] =
-                    scriptedListenerActionClone;
-            }
-        }
+    // Initialize local instances of ScriptedObjects
+    for (auto& scriptedOb : machine->scriptedObjects())
+    {
+        m_scriptedListenerActionsMap[scriptedOb] =
+            scriptedOb->cloneScriptedObject();
     }
     sortHitComponents();
 }
 
 ScriptedObject* StateMachineInstance::scriptedObject(
-    const ScriptedObject* source)
+    const ScriptedObject* source) const
 {
     auto itr = m_scriptedListenerActionsMap.find(source);
     if (itr != m_scriptedListenerActionsMap.end())
diff --git a/src/assets/script_asset.cpp b/src/assets/script_asset.cpp
index 9642b5b..bca0765 100644
--- a/src/assets/script_asset.cpp
+++ b/src/assets/script_asset.cpp
@@ -67,7 +67,8 @@
         scriptProtocol == ScriptProtocol::layout ||
         scriptProtocol == ScriptProtocol::converter ||
         scriptProtocol == ScriptProtocol::pathEffect ||
-        scriptProtocol == ScriptProtocol::listenerAction)
+        scriptProtocol == ScriptProtocol::listenerAction ||
+        scriptProtocol == ScriptProtocol::transitionCondition)
     {
         if (static_cast<lua_Type>(lua_getfield(state, -1, "update")) ==
             LUA_TFUNCTION)
diff --git a/src/generated/animation/scripted_transition_condition_base.cpp b/src/generated/animation/scripted_transition_condition_base.cpp
new file mode 100644
index 0000000..d4bd6d1
--- /dev/null
+++ b/src/generated/animation/scripted_transition_condition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/scripted_transition_condition_base.hpp"
+#include "rive/animation/scripted_transition_condition.hpp"
+
+using namespace rive;
+
+Core* ScriptedTransitionConditionBase::clone() const
+{
+    auto cloned = new ScriptedTransitionCondition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/importers/state_machine_importer.cpp b/src/importers/state_machine_importer.cpp
index de29f24..67eac58 100644
--- a/src/importers/state_machine_importer.cpp
+++ b/src/importers/state_machine_importer.cpp
@@ -39,4 +39,9 @@
     return true;
 }
 
+void StateMachineImporter::addScriptedObject(ScriptedObject* object)
+{
+    m_StateMachine->addScriptedObject(object);
+}
+
 StatusCode StateMachineImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/tests/unit_tests/assets/scripted_transition_condition.riv b/tests/unit_tests/assets/scripted_transition_condition.riv
new file mode 100644
index 0000000..2dbecd7
--- /dev/null
+++ b/tests/unit_tests/assets/scripted_transition_condition.riv
Binary files differ
diff --git a/tests/unit_tests/runtime/scripting/scripting_transition_condition_test.cpp b/tests/unit_tests/runtime/scripting/scripting_transition_condition_test.cpp
new file mode 100644
index 0000000..985df3b
--- /dev/null
+++ b/tests/unit_tests/runtime/scripting/scripting_transition_condition_test.cpp
@@ -0,0 +1,48 @@
+
+#include "catch.hpp"
+#include "scripting_test_utilities.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/lua/rive_lua_libs.hpp"
+#include "rive/viewmodel/viewmodel_instance_string.hpp"
+#include "rive/viewmodel/viewmodel_instance_boolean.hpp"
+#include "rive_file_reader.hpp"
+
+using namespace rive;
+
+TEST_CASE("Scripted transition condition", "[silver]")
+{
+    rive::SerializingFactory silver;
+    auto file =
+        ReadRiveFile("assets/scripted_transition_condition.riv", &silver);
+    auto artboard = file->artboardDefault();
+
+    silver.frameSize(artboard->width(), artboard->height());
+    REQUIRE(artboard != nullptr);
+    auto stateMachine = artboard->stateMachineAt(0);
+
+    auto vmi = file->createViewModelInstance(artboard.get());
+    stateMachine->bindViewModelInstance(vmi);
+    stateMachine->advanceAndApply(0.1f);
+
+    auto tlBool =
+        vmi->propertyValue("timelineBool")->as<ViewModelInstanceBoolean>();
+    auto anyBool =
+        vmi->propertyValue("anyStateBool")->as<ViewModelInstanceBoolean>();
+
+    auto renderer = silver.makeRenderer();
+    artboard->draw(renderer.get());
+
+    silver.addFrame();
+
+    tlBool->propertyValue(true);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+
+    silver.addFrame();
+
+    anyBool->propertyValue(true);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+
+    CHECK(silver.matches("scripted_transition_condition"));
+}
\ No newline at end of file
diff --git a/tests/unit_tests/silvers/scripted_transition_condition.sriv b/tests/unit_tests/silvers/scripted_transition_condition.sriv
new file mode 100644
index 0000000..a0ae9bb
--- /dev/null
+++ b/tests/unit_tests/silvers/scripted_transition_condition.sriv
Binary files differ