// ============================================================
// Reusable production policies
// ============================================================
///|
/// A decorator that blocks a completed child for a number of future frames.
pub fn cooldown_node(child : Node, cooldown_ticks : Int) -> Node {
let remaining : Ref[Int] = Ref::new(0)
let tick = fn(bb) {
if remaining.get() > 0 {
remaining.set(remaining.get() - 1)
return Status::BTFailure
}
let status = child.tick(bb)
match status {
Status::BTSuccess | Status::BTFailure =>
remaining.set(if cooldown_ticks > 0 { cooldown_ticks } else { 0 })
Status::BTRunning => ()
}
status
}
let reset = fn() {
remaining.set(0)
child.reset()
}
Node::new(tick, reset)
}
///|
/// Convert a terminal result into Running for one frame before completing.
pub fn defer_terminal_node(child : Node) -> Node {
let deferred : Ref[Status?] = Ref::new(None)
let tick = fn(bb) {
match deferred.get() {
Some(status) => {
deferred.set(None)
status
}
None => {
let status = child.tick(bb)
match status {
Status::BTSuccess | Status::BTFailure => {
deferred.set(Some(status))
Status::BTRunning
}
Status::BTRunning => Status::BTRunning
}
}
}
}
let reset = fn() {
deferred.set(None)
child.reset()
}
Node::new(tick, reset)
}
///|
/// Return Success after `required` consecutive successful child ticks.
pub fn stable_success_node(child : Node, required : Int) -> Node {
let count : Ref[Int] = Ref::new(0)
let needed = if required > 0 { required } else { 1 }
let tick = fn(bb) {
match child.tick(bb) {
Status::BTSuccess => {
count.set(count.get() + 1)
if count.get() >= needed {
count.set(0)
Status::BTSuccess
} else {
Status::BTRunning
}
}
Status::BTFailure => {
count.set(0)
Status::BTFailure
}
Status::BTRunning => Status::BTRunning
}
}
let reset = fn() {
count.set(0)
child.reset()
}
Node::new(tick, reset)
}
///|
/// Return Failure after `required` consecutive failed child ticks.
pub fn stable_failure_node(child : Node, required : Int) -> Node {
let count : Ref[Int] = Ref::new(0)
let needed = if required > 0 { required } else { 1 }
let tick = fn(bb) {
match child.tick(bb) {
Status::BTFailure => {
count.set(count.get() + 1)
if count.get() >= needed {
count.set(0)
Status::BTFailure
} else {
Status::BTRunning
}
}
Status::BTSuccess => {
count.set(0)
Status::BTSuccess
}
Status::BTRunning => Status::BTRunning
}
}
let reset = fn() {
count.set(0)
child.reset()
}
Node::new(tick, reset)
}
///|
/// Retry only when a blackboard predicate says the action is recoverable.
pub fn recoverable_retry_node(
child : Node,
max_retries : Int,
should_retry : (Blackboard) -> Bool,
) -> Node {
let attempts : Ref[Int] = Ref::new(0)
let limit = if max_retries > 0 { max_retries } else { 1 }
let tick = fn(bb) {
match child.tick(bb) {
Status::BTSuccess => {
attempts.set(0)
Status::BTSuccess
}
Status::BTRunning => Status::BTRunning
Status::BTFailure =>
if should_retry(bb) && attempts.get() + 1 < limit {
attempts.set(attempts.get() + 1)
child.reset()
Status::BTRunning
} else {
attempts.set(0)
Status::BTFailure
}
}
}
let reset = fn() {
attempts.set(0)
child.reset()
}
Node::new(tick, reset)
}