Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delay and Cooldown Decorators #276

Merged
merged 20 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions addons/beehave/icons/cooldown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions addons/beehave/icons/delayer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions addons/beehave/nodes/decorators/cooldown.gd
Original file line number Diff line number Diff line change
@@ -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


49 changes: 49 additions & 0 deletions addons/beehave/nodes/decorators/delayer.gd
Original file line number Diff line number Diff line change
@@ -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

10 changes: 10 additions & 0 deletions docs/manual/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
42 changes: 42 additions & 0 deletions test/nodes/decorators/cooldown_test.gd
Original file line number Diff line number Diff line change
@@ -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)
56 changes: 56 additions & 0 deletions test/nodes/decorators/delayer_test.gd
Original file line number Diff line number Diff line change
@@ -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)
Loading