Skip to content

Commit 2be16db

Browse files
authored
Reset decorators correctly (#392)
Reset decorators correctly (cooldown, delayer)
1 parent d75656a commit 2be16db

File tree

12 files changed

+167
-7
lines changed

12 files changed

+167
-7
lines changed

addons/beehave/nodes/decorators/cooldown.gd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class_name CooldownDecorator
66
## The Cooldown Decorator will return 'FAILURE' for a set amount of time
77
## after executing its child.
88
## The timer resets the next time its child is executed and it is not `RUNNING`
9+
## or when the node is interrupted (such as when the behavior tree changes branches).
910

1011
## The wait time in seconds
1112
@export var wait_time := 0.0
@@ -48,3 +49,9 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
4849
blackboard.set_value(cache_key, wait_time, str(actor.get_instance_id()))
4950

5051
return response
52+
53+
func interrupt(actor: Node, blackboard: Blackboard) -> void:
54+
# Reset the cooldown when the branch changes
55+
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))
56+
57+
super(actor, blackboard)

addons/beehave/nodes/decorators/delayer.gd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class_name DelayDecorator
66
## The Delay Decorator will return 'RUNNING' for a set amount of time
77
## before executing its child.
88
## The timer resets when both it and its child are not `RUNNING`
9+
## or when the node is interrupted (such as when the behavior tree changes branches).
910

1011
## The wait time in seconds
1112
@export var wait_time := 0.0
@@ -48,3 +49,9 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
4849
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))
4950

5051
return response
52+
53+
func interrupt(actor: Node, blackboard: Blackboard) -> void:
54+
# Reset the delay timer when the branch changes
55+
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))
56+
57+
super(actor, blackboard)

addons/beehave/nodes/decorators/limiter.gd

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class_name LimiterDecorator extends Decorator
55
## The limiter will execute its `RUNNING` child `x` amount of times. When the number of
66
## maximum ticks is reached, it will return a `FAILURE` status code.
77
## The count resets the next time that a child is not `RUNNING`
8+
## or when the node is interrupted (such as when the behavior tree changes branches).
89

910
@onready var cache_key = "limiter_%s" % self.get_instance_id()
1011

@@ -28,11 +29,13 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
2829
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
2930
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
3031

31-
if child is ActionLeaf and response == RUNNING:
32+
if response == RUNNING:
3233
running_child = child
33-
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
34-
35-
if response != RUNNING:
34+
if child is ActionLeaf:
35+
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
36+
else:
37+
# If the child is no longer running, reset the counter for next time
38+
_reset_counter(actor, blackboard)
3639
child.after_run(actor, blackboard)
3740

3841
return response
@@ -43,11 +46,25 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
4346

4447

4548
func before_run(actor: Node, blackboard: Blackboard) -> void:
46-
blackboard.set_value(cache_key, 0, str(actor.get_instance_id()))
49+
# Initialize the counter to 0 when we first start running
50+
_reset_counter(actor, blackboard)
4751
if get_child_count() > 0:
4852
get_child(0).before_run(actor, blackboard)
4953

5054

55+
func interrupt(actor: Node, blackboard: Blackboard) -> void:
56+
# The tree is changing branches, so the count should reset
57+
_reset_counter(actor, blackboard)
58+
59+
# Call super, which may affect our blackboard values
60+
super(actor, blackboard)
61+
62+
63+
# Resets the counter in the blackboard
64+
func _reset_counter(actor: Node, blackboard: Blackboard) -> void:
65+
blackboard.set_value(cache_key, 0, str(actor.get_instance_id()))
66+
67+
5168
func get_class_name() -> Array[StringName]:
5269
var classes := super()
5370
classes.push_back(&"LimiterDecorator")

addons/beehave/nodes/decorators/repeater.gd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## The repeater will execute its child until it returns `SUCCESS` a certain amount of times.
22
## When the number of maximum ticks is reached, it will return a `SUCCESS` status code.
33
## If the child returns `FAILURE`, the repeater will return `FAILURE` immediately.
4+
## The counter resets when the node is interrupted (such as when the behavior tree changes branches).
45
@tool
56
@icon("../../icons/repeater.svg")
67
class_name RepeaterDecorator extends Decorator
@@ -52,6 +53,13 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
5253
return SUCCESS
5354

5455

56+
func interrupt(actor: Node, blackboard: Blackboard) -> void:
57+
# Reset the internal counter when the node is interrupted
58+
current_count = 0
59+
60+
super(actor, blackboard)
61+
62+
5563
func get_class_name() -> Array[StringName]:
5664
var classes := super()
5765
classes.push_back(&"LimiterDecorator")

addons/beehave/nodes/decorators/time_limiter.gd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class_name TimeLimiterDecorator extends Decorator
55
## The Time Limit Decorator will give its `RUNNING` child a set amount of time to finish
66
## before interrupting it and return a `FAILURE` status code.
77
## The timer resets the next time that a child is not `RUNNING`
8+
## or when the node is interrupted (such as when the behavior tree changes branches).
89

910
@export var wait_time := 0.0
1011

@@ -48,6 +49,13 @@ func before_run(actor: Node, blackboard: Blackboard) -> void:
4849
get_child(0).before_run(actor, blackboard)
4950

5051

52+
func interrupt(actor: Node, blackboard: Blackboard) -> void:
53+
# Reset the timer when the node is interrupted
54+
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))
55+
56+
super(actor, blackboard)
57+
58+
5159
func get_class_name() -> Array[StringName]:
5260
var classes := super()
5361
classes.push_back(&"TimeLimiterDecorator")

test/composites/mock_composite.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ func interrupt(actor: Node, blackboard: Blackboard) -> void:
99
interrupted.emit(actor, blackboard)
1010

1111
func tick(_actor: Node, _blackboard: Blackboard) -> int:
12-
return final_result
12+
return final_result
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://ddeb0yatovjv

test/nodes/decorators/cooldown_test.gd

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,20 @@ func test_after_run_not_called_during_cooldown() -> void:
129129
# Wait a bit but not enough to complete cooldown
130130
await runner.simulate_frames(1, 500)
131131
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)
132-
assert_bool(action.after_run_called).is_false() # Should not call after_run during cooldown
132+
assert_bool(action.after_run_called).is_false() # Should not call after_run during cooldown
133+
134+
func test_cooldown_reset_on_interrupt() -> void:
135+
cooldown.wait_time = 1.0
136+
action.final_result = BeehaveNode.SUCCESS
137+
138+
# First tick should execute child and start cooldown
139+
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)
140+
141+
# Second tick should be in cooldown
142+
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)
143+
144+
# Interrupt should reset the cooldown
145+
tree.interrupt()
146+
147+
# After interrupt, the cooldown should be reset and the action should execute normally
148+
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)

test/nodes/decorators/delayer_test.gd

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,36 @@ func test_after_run_not_called_during_delay() -> void:
9494
await runner.simulate_frames(1, 500)
9595
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
9696
assert_bool(action.after_run_called).is_false()
97+
98+
func test_delay_reset_on_interrupt() -> void:
99+
delayer.wait_time = 1.0
100+
action.final_result = BeehaveNode.SUCCESS
101+
102+
# First tick should start the delay
103+
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
104+
105+
# Simulate partial progress through the delay
106+
await runner.simulate_frames(1, 500)
107+
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
108+
109+
# Interrupt should reset the delay
110+
tree.interrupt()
111+
112+
# Verify the blackboard value is reset
113+
var cache_key = "time_limiter_%s" % delayer.get_instance_id()
114+
var blackboard_value = tree.blackboard.get_value(cache_key, -1, str(tree.actor.get_instance_id()))
115+
assert_that(blackboard_value).is_equal(0.0) # This should pass if our reset works
116+
117+
# First tick after interrupt should start a new delay from the beginning
118+
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
119+
120+
# Set running_child to null to force a reinitialize on next tick
121+
# This simulates what would happen in a real tree when switching branches
122+
delayer.running_child = null
123+
124+
# Manually "complete" the delay by setting it past the wait time
125+
cache_key = "time_limiter_%s" % delayer.get_instance_id()
126+
tree.blackboard.set_value(cache_key, 1.1, str(tree.actor.get_instance_id())) # Set it past wait_time
127+
128+
# Now when we tick, it should execute the child and return SUCCESS
129+
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)

test/nodes/decorators/limiter_test.gd

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,26 @@ func test_clear_running_child_after_run() -> void:
6262
tree.tick()
6363
assert_that(action.count).is_equal(2)
6464
assert_that(limiter.running_child).is_equal(null)
65+
66+
func test_counter_reset_on_interrupt() -> void:
67+
limiter.max_count = 3
68+
action.status = BeehaveNode.RUNNING
69+
70+
# Tick once to advance the counter
71+
var tree_status = tree.tick()
72+
# Verify counter was advanced
73+
assert_that(tree_status).is_equal(BeehaveNode.RUNNING)
74+
assert_that(action.count).is_equal(1)
75+
76+
# Interrupt should reset the counter
77+
tree.interrupt()
78+
79+
# Verify the counter was reset by checking if we can tick again
80+
# If counter was reset, we should be able to tick max_count times again
81+
action.status = BeehaveNode.RUNNING
82+
for i in range(limiter.max_count):
83+
var status = tree.tick()
84+
assert_that(status).is_equal(BeehaveNode.RUNNING)
85+
86+
# After max_count ticks, it should now fail (proving counter started from 0)
87+
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)

0 commit comments

Comments
 (0)