diff --git a/addons/beehave/icons/cooldown.svg b/addons/beehave/icons/cooldown.svg new file mode 100644 index 00000000..fbdfd6a8 --- /dev/null +++ b/addons/beehave/icons/cooldown.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/delayer.svg b/addons/beehave/icons/delayer.svg new file mode 100644 index 00000000..21cb6172 --- /dev/null +++ b/addons/beehave/icons/delayer.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/addons/beehave/nodes/decorators/cooldown.gd b/addons/beehave/nodes/decorators/cooldown.gd new file mode 100644 index 00000000..ee084f03 --- /dev/null +++ b/addons/beehave/nodes/decorators/cooldown.gd @@ -0,0 +1,51 @@ +@tool +@icon("../../icons/cooldown.svg") +extends Decorator +class_name CooldownDecorator + +## The Cooldown Decorator will return 'FAILURE' for a set amount of time +## after executing its child. +## The timer resets the next time its child is executed and it is not `RUNNING` + +## The wait time in seconds +@export var wait_time: = 0.0 + +@onready var cache_key = 'cooldown_%s' % self.get_instance_id() + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + var remaining_time = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id())) + var response + + if c != running_child: + c.before_run(actor, blackboard) + + if remaining_time > 0: + response = FAILURE + + remaining_time -= get_physics_process_delta_time() + blackboard.set_value(cache_key, remaining_time, str(actor.get_instance_id())) + + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(self.get_instance_id(), response) + else: + response = c.tick(actor, blackboard) + + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + if response == RUNNING and c is ActionLeaf: + running_child = c + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + + if response != RUNNING: + blackboard.set_value(cache_key, wait_time, str(actor.get_instance_id())) + + return response + + diff --git a/addons/beehave/nodes/decorators/delayer.gd b/addons/beehave/nodes/decorators/delayer.gd new file mode 100644 index 00000000..2fdac858 --- /dev/null +++ b/addons/beehave/nodes/decorators/delayer.gd @@ -0,0 +1,49 @@ +@tool +@icon("../../icons/delayer.svg") +extends Decorator +class_name DelayDecorator + +## The Delay Decorator will return 'RUNNING' for a set amount of time +## before executing its child. +## The timer resets when both it and its child are not `RUNNING` + +## The wait time in seconds +@export var wait_time: = 0.0 + +@onready var cache_key = 'time_limiter_%s' % self.get_instance_id() + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + var total_time = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id())) + var response + + if c != running_child: + c.before_run(actor, blackboard) + + if total_time < wait_time: + response = RUNNING + + total_time += get_physics_process_delta_time() + blackboard.set_value(cache_key, total_time, str(actor.get_instance_id())) + + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(self.get_instance_id(), response) + else: + response = c.tick(actor, blackboard) + + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + if response == RUNNING and c is ActionLeaf: + running_child = c + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + + if response != RUNNING: + blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id())) + + return response + diff --git a/docs/manual/decorators.md b/docs/manual/decorators.md index 23472d97..eb627701 100644 --- a/docs/manual/decorators.md +++ b/docs/manual/decorators.md @@ -29,3 +29,13 @@ The `TimeLimiter` node only gives its `RUNNING` child a set amount of time to fi This note is useful when you want to limit the execution time of a long running action. Once a time limiter reaches its time limit, it will start interrupting its child on every tick. **Example:** A mob aggros and tries to chase you, the chase action will last a maximum of 10 seconds before being aborted if not complete. + +## Delayer +When first executing the `Delayer` node, it will start an internal timer and return `RUNNING` until the timer is complete, after which it will execute its child node. The delayer resets its time after its child returns either `SUCCESS` or `FAILURE`. + +**Example:** You stun a boss mob and it waits a certain amount of time before resuming its attack patterns. + +## Cooldown +The `Cooldown` node executes its child until it either returns `SUCCESS` or `FAILURE`, after which it will start an internal timer and return `FAILURE` until the timer is complete. The cooldown is then able to execute its child again. + +**Example:** A mob attacks you and has to wait before it can attack you again. diff --git a/test/nodes/decorators/cooldown_test.gd b/test/nodes/decorators/cooldown_test.gd new file mode 100644 index 00000000..07d64c5c --- /dev/null +++ b/test/nodes/decorators/cooldown_test.gd @@ -0,0 +1,42 @@ +# GdUnit generated TestSuite +class_name CooldownDecoratorTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/beehave/nodes/decorators/cooldown.gd' +const __action = "res://test/actions/count_up_action.gd" +const __tree = "res://addons/beehave/nodes/beehave_tree.gd" +const __blackboard = "res://addons/beehave/blackboard.gd" + +var tree: BeehaveTree +var action: ActionLeaf +var cooldown: CooldownDecorator +var runner:GdUnitSceneRunner + +func before_test() -> void: + tree = auto_free(load(__tree).new()) + action = auto_free(load(__action).new()) + cooldown = auto_free(load(__source).new()) + + var actor = auto_free(Node2D.new()) + var blackboard = auto_free(load(__blackboard).new()) + + tree.add_child(cooldown) + cooldown.add_child(action) + + tree.actor = actor + tree.blackboard = blackboard + runner = scene_runner(tree) + +func test_running_then_fail() -> void: + cooldown.wait_time = 1.0 + action.status = BeehaveNode.RUNNING + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + action.status = BeehaveNode.SUCCESS + assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS) + action.status = BeehaveNode.RUNNING + assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE) + await runner.simulate_frames(1, 2000) + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) diff --git a/test/nodes/decorators/delayer_test.gd b/test/nodes/decorators/delayer_test.gd new file mode 100644 index 00000000..e7bcaede --- /dev/null +++ b/test/nodes/decorators/delayer_test.gd @@ -0,0 +1,56 @@ +# GdUnit generated TestSuite +class_name DelayDecoratorTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/beehave/nodes/decorators/delayer.gd' +const __action = "res://test/actions/count_up_action.gd" +const __tree = "res://addons/beehave/nodes/beehave_tree.gd" +const __blackboard = "res://addons/beehave/blackboard.gd" + +var tree: BeehaveTree +var action: ActionLeaf +var delayer: DelayDecorator +var runner:GdUnitSceneRunner + +func before_test() -> void: + tree = auto_free(load(__tree).new()) + action = auto_free(load(__action).new()) + delayer = auto_free(load(__source).new()) + + var actor = auto_free(Node2D.new()) + var blackboard = auto_free(load(__blackboard).new()) + + tree.add_child(delayer) + delayer.add_child(action) + + tree.actor = actor + tree.blackboard = blackboard + runner = scene_runner(tree) + +func test_return_success_after_delay() -> void: + delayer.wait_time = get_physics_process_delta_time() + action.status = BeehaveNode.SUCCESS + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS) + # Assure that the delayer properly resets + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS) + +func test_return_running_after_delay() -> void: + delayer.wait_time = 1.0 + action.status = BeehaveNode.RUNNING + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + await runner.simulate_frames(1, 1000) + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + action.status = BeehaveNode.SUCCESS + assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS) + # Assure that the delayer properly resets + action.status = BeehaveNode.RUNNING + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + await runner.simulate_frames(1, 1000) + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + action.status = BeehaveNode.SUCCESS + assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)