extends GdUnitTestSuite var runner: GdUnitSceneRunner var world: World func before(): runner = scene_runner("res://addons/gecs/tests/test_scene.tscn") world = runner.get_property("world") ECS.world = world func after_test(): world.purge(false) func test_observer_receive_component_changed(): world.add_system(TestASystem.new()) var test_a_observer = TestAObserver.new() world.add_observer(test_a_observer) # Create entities with the required components var entity_a = TestA.new() entity_a.name = "a" entity_a.add_component(C_TestA.new()) var entity_b = TestB.new() entity_b.name = "b" entity_b.add_component(C_TestA.new()) entity_b.add_component(C_TestB.new()) # issue #43 var entity_a2 = TestA.new() entity_a2.name = "a" entity_a2.add_component(C_TestA.new()) world.get_node(world.entity_nodes_root).add_child(entity_a2) world.add_entity(entity_a2, null, false) assert_int(test_a_observer.added_count).is_equal(1) # Add some entities before systems world.add_entities([entity_a, entity_b]) assert_int(test_a_observer.added_count).is_equal(3) # Run the systems once print('process 1st') world.process(0.1) # Check the event_count assert_int(test_a_observer.event_count).is_equal(2) # Run the systems again print('process 2nd') world.process(0.1) # Check the event_count assert_int(test_a_observer.event_count).is_equal(4) ## Test that observers detect when a component is added to an entity func test_observer_on_component_added(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create an entity without the component var entity = Entity.new() world.add_entity(entity) # Verify observer hasn't fired yet assert_int(observer.added_count).is_equal(0) # Add the watched component var component = C_ObserverTest.new() entity.add_component(component) # Verify observer detected the addition assert_int(observer.added_count).is_equal(1) assert_object(observer.last_added_entity).is_equal(entity) ## Test that observers detect when a component is removed from an entity func test_observer_on_component_removed(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create an entity with the component var entity = Entity.new() var component = C_ObserverTest.new() entity.add_component(component) world.add_entity(entity) # Verify observer detected the addition assert_int(observer.added_count).is_equal(1) # Reset and remove the component observer.reset() entity.remove_component(C_ObserverTest) # Verify observer detected the removal assert_int(observer.removed_count).is_equal(1) assert_object(observer.last_removed_entity).is_equal(entity) assert_int(observer.added_count).is_equal(0) # Should remain 0 after reset ## Test that observers detect property changes on watched components func test_observer_on_component_changed(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create an entity with the component var entity = Entity.new() var component = C_ObserverTest.new(0, "initial") entity.add_component(component) world.add_entity(entity) # Reset the observer (it may have fired on add) observer.reset() # Change the value property (this will emit property_changed signal) component.value = 42 # Verify observer detected the change assert_int(observer.changed_count).is_equal(1) assert_object(observer.last_changed_entity).is_equal(entity) assert_str(observer.last_changed_property).is_equal("value") assert_int(observer.last_old_value).is_equal(0) assert_int(observer.last_new_value).is_equal(42) # Change another property component.name_prop = "changed" # Verify observer detected the second change assert_int(observer.changed_count).is_equal(2) assert_str(observer.last_changed_property).is_equal("name_prop") assert_str(observer.last_old_value).is_equal("initial") assert_str(observer.last_new_value).is_equal("changed") ## Test that observers respect query filters (only match entities that pass the query) func test_observer_respects_query_filter(): var health_observer = O_HealthObserver.new() world.add_observer(health_observer) # Create entity with only health component (should NOT match - needs both components) var entity_only_health = Entity.new() entity_only_health.add_component(C_ObserverHealth.new()) world.add_entity(entity_only_health) # Observer should NOT have fired (doesn't match query) assert_int(health_observer.health_added_count).is_equal(0) # Create entity with both components (should match) var entity_both = Entity.new() entity_both.add_component(C_ObserverTest.new()) entity_both.add_component(C_ObserverHealth.new()) world.add_entity(entity_both) # Observer should have fired now (matches query) assert_int(health_observer.health_added_count).is_equal(1) ## Test that multiple observers can watch the same component func test_multiple_observers_same_component(): var observer1 = O_ObserverTest.new() var observer2 = O_ObserverTest.new() world.add_observer(observer1) world.add_observer(observer2) # Create an entity with the component var entity = Entity.new() var component = C_ObserverTest.new() entity.add_component(component) world.add_entity(entity) # Both observers should have detected the addition assert_int(observer1.added_count).is_equal(1) assert_int(observer2.added_count).is_equal(1) # Change the component observer1.reset() observer2.reset() component.value = 100 # Both observers should have detected the change assert_int(observer1.changed_count).is_equal(1) assert_int(observer2.changed_count).is_equal(1) ## Test that observers can track multiple property changes func test_observer_tracks_multiple_changes(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create an entity with the component var entity = Entity.new() var component = C_ObserverTest.new(0, "start") entity.add_component(component) world.add_entity(entity) observer.reset() # Make multiple changes component.value = 10 component.value = 20 component.name_prop = "middle" component.value = 30 # Should have detected all 4 changes assert_int(observer.changed_count).is_equal(4) ## Test observer with health component and query matching func test_observer_health_low_health_alert(): var health_observer = O_HealthObserver.new() world.add_observer(health_observer) # Create entity with both components var entity = Entity.new() entity.add_component(C_ObserverTest.new()) var health = C_ObserverHealth.new(100) entity.add_component(health) world.add_entity(entity) health_observer.reset() # Reduce health gradually health.health = 50 assert_int(health_observer.health_changed_count).is_equal(1) assert_int(health_observer.low_health_alerts.size()).is_equal(0) health.health = 25 # Below threshold assert_int(health_observer.health_changed_count).is_equal(2) assert_int(health_observer.low_health_alerts.size()).is_equal(1) assert_object(health_observer.low_health_alerts[0]).is_equal(entity) ## Test that observer doesn't fire when entity doesn't match query func test_observer_ignores_non_matching_entities(): var health_observer = O_HealthObserver.new() world.add_observer(health_observer) # Create entity with only C_ObserverTest (not both components) var entity = Entity.new() entity.add_component(C_ObserverTest.new()) world.add_entity(entity) # Try to add C_ObserverHealth to a different entity that doesn't have C_ObserverTest var entity2 = Entity.new() entity2.add_component(C_ObserverHealth.new()) world.add_entity(entity2) # Observer should not have fired (entity2 doesn't match query) assert_int(health_observer.health_added_count).is_equal(0) ## Test observer detects component addition before entity is added to world func test_observer_component_added_before_entity_added(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create entity and add component BEFORE adding to world var entity = Entity.new() var component = C_ObserverTest.new() entity.add_component(component) # Observer shouldn't have fired yet assert_int(observer.added_count).is_equal(0) # Now add to world world.add_entity(entity) # Observer should fire now assert_int(observer.added_count).is_equal(1) ## Test observer with component replacement func test_observer_component_replacement(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create entity with component var entity = Entity.new() var component1 = C_ObserverTest.new(10, "first") entity.add_component(component1) world.add_entity(entity) assert_int(observer.added_count).is_equal(1) # Replace the component (add_component on same type replaces) var component2 = C_ObserverTest.new(20, "second") entity.add_component(component2) # Should trigger both removed and added assert_int(observer.removed_count).is_equal(1) assert_int(observer.added_count).is_equal(2) ## Test that property changes without signal emission don't trigger observer func test_observer_ignores_direct_property_changes(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create entity with component var entity = Entity.new() var component = C_ObserverTest.new() entity.add_component(component) world.add_entity(entity) observer.reset() # Directly set the property WITHOUT using the setter # This bypasses the property_changed signal # Note: In GDScript, using the property name always calls the setter, # so we need to access the internal variable directly # For this test, we're verifying that ONLY setters that emit signals work # Using the setter (should trigger) component.value = 42 assert_int(observer.changed_count).is_equal(1) # The framework correctly requires explicit signal emission in setters ## Test observer with entity that starts matching query after component addition func test_observer_entity_becomes_matching(): var health_observer = O_HealthObserver.new() world.add_observer(health_observer) # Create entity with only one component var entity = Entity.new() entity.add_component(C_ObserverTest.new()) world.add_entity(entity) # Health observer shouldn't fire (needs both components) assert_int(health_observer.health_added_count).is_equal(0) # Add the second component entity.add_component(C_ObserverHealth.new()) # Now health observer should fire assert_int(health_observer.health_added_count).is_equal(1) ## Test removing observer from world func test_remove_observer(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create entity with component var entity = Entity.new() entity.add_component(C_ObserverTest.new()) world.add_entity(entity) assert_int(observer.added_count).is_equal(1) # Remove the observer world.remove_observer(observer) # Add another entity - observer should not fire var entity2 = Entity.new() entity2.add_component(C_ObserverTest.new()) world.add_entity(entity2) # Count should still be 1 (not 2) assert_int(observer.added_count).is_equal(1) ## Test observer with multiple entities func test_observer_with_multiple_entities(): var observer = O_ObserverTest.new() world.add_observer(observer) # Create multiple entities for i in range(5): var entity = Entity.new() entity.add_component(C_ObserverTest.new(i)) world.add_entity(entity) # Should have detected all 5 additions assert_int(observer.added_count).is_equal(5) observer.reset() # Get all entities and modify their components var entities = world.query.with_all([C_ObserverTest]).execute() for entity in entities: var comp = entity.get_component(C_ObserverTest) comp.value = comp.value + 100 # Should have detected all 5 changes assert_int(observer.changed_count).is_equal(5)