Files
storybook/docs/reference/12-decorators.md

688 lines
15 KiB
Markdown
Raw Normal View History

# Decorators
Decorators are special nodes that wrap a single child and modify its execution behavior. They enable timing control, retry logic, conditional execution, and result inversion without modifying the child node itself.
## What Are Decorators?
Decorators sit between a parent and child node, transforming the child's behavior:
```
Parent
└─ Decorator
└─ Child
```
The decorator intercepts the child's execution, potentially:
- Repeating it multiple times
- Timing it out
- Inverting its result
- Guarding its execution
- Forcing a specific result
## Decorator Types
Storybook provides 10 decorator types:
| Decorator | Purpose | Example |
|-----------|---------|---------|
| `repeat` | Loop infinitely | `repeat { Patrol }` |
| `repeat(N)` | Loop N times | `repeat(3) { Check }` |
| `repeat(min..max)` | Loop min to max times | `repeat(2..5) { Search }` |
| `invert` | Flip success/failure | `invert { IsEnemy }` |
| `retry(N)` | Retry on failure (max N times) | `retry(5) { Open }` |
| `timeout(duration)` | Fail after duration | `timeout(10s) { Solve }` |
| `cooldown(duration)` | Run at most once per duration | `cooldown(30s) { Fire }` |
| `if(condition)` | Only run if condition true | `if(health > 50) { Attack }` |
| `succeed_always` | Always return success | `succeed_always { Try }` |
| `fail_always` | Always return failure | `fail_always { Test }` |
## Syntax
```bnf
<decorator> ::= <repeat-decorator>
| <invert-decorator>
| <retry-decorator>
| <timeout-decorator>
| <cooldown-decorator>
| <if-decorator>
| <force-result-decorator>
<repeat-decorator> ::= "repeat" <repeat-spec>? "{" <behavior-node> "}"
<repeat-spec> ::= "(" <number> ")"
| "(" <number> ".." <number> ")"
<invert-decorator> ::= "invert" "{" <behavior-node> "}"
<retry-decorator> ::= "retry" "(" <number> ")" "{" <behavior-node> "}"
<timeout-decorator> ::= "timeout" "(" <duration-literal> ")" "{" <behavior-node> "}"
<cooldown-decorator> ::= "cooldown" "(" <duration-literal> ")" "{" <behavior-node> "}"
<if-decorator> ::= "if" "(" <expression> ")" "{" <behavior-node> "}"
<force-result-decorator> ::= ("succeed_always" | "fail_always") "{" <behavior-node> "}"
<duration-literal> ::= <number> ("s" | "m" | "h" | "d")
```
## Repeat Decorators
### Infinite Repeat: `repeat`
Loops the child infinitely. The child is re-executed immediately after completing (success or failure).
```storybook
behavior InfinitePatrol {
repeat {
PatrolRoute
}
}
```
**Execution:**
1. Run child
2. Child completes (success or failure)
3. Immediately run child again
4. Go to step 2 (forever)
**Use cases:**
- Perpetual patrols
- Endless background processes
- Continuous monitoring
**Warning:** Infinite loops never return to parent. Ensure they're appropriate for your use case.
### Repeat N Times: `repeat N`
Repeats the child exactly N times, then returns success.
```storybook
behavior CheckThreeTimes {
repeat(3) {
CheckDoor
}
}
```
**Execution:**
1. Counter = 0
2. Run child
3. Counter++
4. If counter < N, go to step 2
5. Return success
**Use cases:**
- Fixed iteration counts
- "Try three times then give up"
- Deterministic looping
### Repeat Range: `repeat min..max`
Repeats the child between min and max times. At runtime, a specific count is selected within the range.
```storybook
behavior SearchRandomly {
repeat(2..5) {
SearchArea
}
}
```
**Execution:**
1. Select count C randomly from [min, max]
2. Repeat child C times (as in `repeat N`)
**Use cases:**
- Variable behavior
- Procedural variation
- Non-deterministic actions
**Validation:**
- min ≥ 0
- max ≥ min
- Both must be integers
## Invert Decorator
Inverts the child's return value: success becomes failure, failure becomes success.
```storybook
behavior AvoidEnemies {
invert {
IsEnemyNearby // Success if NOT nearby
}
}
```
**Truth table:**
| Child returns | Decorator returns |
|---------------|------------------|
| Success | Failure |
| Failure | Success |
| Running | Running (unchanged) |
**Use cases:**
- Negating conditions ("if NOT X")
- Inverting success criteria
- Converting "found" to "not found"
**Example:**
```storybook
behavior SafeExploration {
choose safe_actions {
// Only explore if NOT dangerous
then explore {
invert { IsDangerous }
ExploreArea
}
// Default: Stay put
Wait
}
}
```
## Retry Decorator
Retries the child up to N times on failure. Returns success if any attempt succeeds, failure if all N attempts fail.
```storybook
behavior PersistentDoor {
retry(5) {
OpenLockedDoor
}
}
```
**Execution:**
1. Attempts = 0
2. Run child
3. If child succeeds → return success
4. If child fails:
- Attempts++
- If attempts < N, go to step 2
- Else return failure
**Use cases:**
- Unreliable actions (lockpicking, persuasion)
- Network/resource operations
- Probabilistic success
**Example with context:**
```storybook
behavior Thief_PickLock {
then attempt_entry {
// Try to pick lock (may fail)
retry(3) {
PickLock
}
// If succeeded, enter
EnterBuilding
// If failed after 3 tries, give up
}
}
```
**Validation:**
- N must be ≥ 1
## Timeout Decorator
Fails the child if it doesn't complete within the specified duration.
```storybook
behavior TimeLimitedPuzzle {
timeout(30s) {
SolvePuzzle
}
}
```
**Execution:**
1. Start timer
2. Run child each tick
3. If child completes before timeout → return child's result
4. If timer expires → return failure (interrupt child)
**Use cases:**
- Time-limited actions
- Preventing infinite loops
- Enforcing deadlines
**Duration formats:**
- `5s` - 5 seconds
- `10m` - 10 minutes
- `2h` - 2 hours
- `3d` - 3 days
**Example:**
```storybook
behavior QuickDecision {
choose timed_choice {
// Give AI 5 seconds to find optimal move
timeout(5s) {
CalculateOptimalStrategy
}
// Fallback: Use simple heuristic
UseQuickHeuristic
}
}
```
**Notes:**
- Timer starts when decorator is first entered
- Timer resets if decorator exits and re-enters
- Child node should handle interruption gracefully
## Cooldown Decorator
Prevents the child from running more than once per cooldown period. Fails immediately if called within cooldown.
```storybook
behavior SpecialAbility {
cooldown(30s) {
FireCannon
}
}
```
**Execution:**
1. Check last execution time
2. If (current_time - last_time) < cooldown → return failure
3. Else:
- Run child
- Record current_time as last_time
- Return child's result
**Use cases:**
- Rate-limiting abilities
- Resource cooldowns (spells, items)
- Preventing spam
**Example:**
```storybook
behavior Mage_SpellCasting {
choose spells {
// Fireball: 10 second cooldown
cooldown(10s) {
CastFireball
}
// Lightning: 5 second cooldown
cooldown(5s) {
CastLightning
}
// Basic attack: No cooldown
MeleeAttack
}
}
```
**State management:**
- Cooldown state persists across behavior tree ticks
- Each cooldown decorator instance has independent state
- Cooldown timers are per-entity (not global)
## If Decorator
Only runs the child if the condition is true. Fails immediately if condition is false.
```storybook
behavior ConditionalAttack {
if(health > 50) {
AggressiveAttack
}
}
```
**Execution:**
1. Evaluate condition expression
2. If true → run child and return its result
3. If false → return failure (do not run child)
**Use cases:**
- Preconditions ("only if X")
- Resource checks ("only if have mana")
- Safety checks ("only if safe")
**Expression syntax:**
See [Expression Language](./17-expressions.md) for complete syntax.
**Examples:**
```storybook
behavior GuardedActions {
choose options {
// Only attack if have weapon and enemy close
if(has_weapon and distance < 10) {
Attack
}
// Only heal if health below 50%
if(health < (max_health * 0.5)) {
Heal
}
// Only flee if outnumbered
if(enemy_count > ally_count) {
Flee
}
}
}
```
**Comparison with bare `if` conditions:**
```storybook
// Using bare 'if' condition (checks every tick, no body)
then approach_and_attack {
if(enemy_nearby)
Approach
Attack
}
// Using 'if' decorator with body (precondition check, fails fast)
if(enemy_nearby) {
then do_attack {
Approach
Attack
}
}
```
The `if` decorator with a body is more efficient for gating expensive subtrees.
## Force Result Decorators
### `succeed_always`
Always returns success, regardless of child's actual result.
```storybook
behavior TryOptionalTask {
succeed_always {
AttemptBonus // Even if fails, we don't care
}
}
```
**Use cases:**
- Optional tasks that shouldn't block progress
- Logging/monitoring actions
- Best-effort operations
**Example:**
```storybook
behavior QuestSequence {
then main_quest {
TalkToNPC
// Try to find secret, but don't fail quest if not found
succeed_always {
SearchForSecretDoor
}
ReturnToQuestGiver
}
}
```
### `fail_always`
Always returns failure, regardless of child's actual result.
```storybook
behavior TestFailure {
fail_always {
AlwaysSucceedsAction // But we force it to fail
}
}
```
**Use cases:**
- Testing/debugging
- Forcing alternative paths
- Disabling branches temporarily
**Example:**
```storybook
behavior UnderConstruction {
choose abilities {
// Temporarily disabled feature
fail_always {
NewExperimentalAbility
}
// Fallback to old ability
ClassicAbility
}
}
```
## Combining Decorators
Decorators can nest to create complex behaviors:
```storybook
behavior ComplexPattern {
// Repeat 3 times, each with 10 second timeout
repeat(3) {
timeout(10s) {
SolveSubproblem
}
}
}
```
**More complex nesting:**
```storybook
behavior ResilientAction {
// If: Only if health > 30
if(health > 30) {
// Timeout: Must complete in 20 seconds
timeout(20s) {
// Retry: Try up to 5 times
retry(5) {
// Cooldown: Can only run once per minute
cooldown(1m) {
PerformComplexAction
}
}
}
}
}
```
**Execution order:** Outside → Inside
1. If checks condition
2. Timeout starts timer
3. Retry begins first attempt
4. Cooldown checks last execution time
5. Child action runs
## Duration Syntax
Timeout and cooldown decorators use duration literals:
```bnf
<duration-literal> ::= <number> <unit>
<unit> ::= "s" // seconds
| "m" // minutes
| "h" // hours
| "d" // days
<number> ::= <digit>+
```
**Examples:**
- `5s` - 5 seconds
- `30s` - 30 seconds
- `2m` - 2 minutes
- `10m` - 10 minutes
- `1h` - 1 hour
- `24h` - 24 hours
- `7d` - 7 days
**Validation:**
- Number must be positive integer
- No compound durations (use `120s` not `2m` if runtime expects seconds)
- No fractional units (`1.5m` not allowed; use `90s`)
## Comparison Table
| Decorator | Affects Success | Affects Failure | Affects Running | Stateful |
|-----------|----------------|----------------|----------------|----------|
| `repeat` | Repeat | Repeat | Wait | Yes (counter) |
| `repeat N` | Repeat | Repeat | Wait | Yes (counter) |
| `repeat min..max` | Repeat | Repeat | Wait | Yes (counter) |
| `invert` | → Failure | → Success | Unchanged | No |
| `retry N` | → Success | Retry or fail | Wait | Yes (attempts) |
| `timeout dur` | → Success | → Success | → Failure if expired | Yes (timer) |
| `cooldown dur` | → Success | → Success | → Success | Yes (last time) |
| `if(expr)` | → Success | → Success | → Success | No |
| `succeed_always` | → Success | → Success | → Success | No |
| `fail_always` | → Failure | → Failure | → Failure | No |
**Stateful decorators** maintain state across ticks. **Stateless decorators** evaluate fresh every tick.
## Validation Rules
1. **Child required**: All decorators must have exactly one child node
2. **Repeat count**: `repeat N` requires N ≥ 0
3. **Repeat range**: `repeat min..max` requires 0 ≤ min ≤ max
4. **Retry count**: `retry N` requires N ≥ 1
5. **Duration positive**: Timeout/cooldown durations must be > 0
6. **Duration format**: Must match `<number><unit>` (e.g., `10s`, `5m`)
7. **Guard expression**: Guard condition must be valid expression
8. **No empty decorators**: `repeat { }` is invalid (missing child)
## Use Cases by Category
### Timing Control
- **timeout**: Prevent infinite loops, enforce time limits
- **cooldown**: Rate-limit abilities, prevent spam
- **repeat**: Continuous processes, patrols
### Reliability
- **retry**: Handle unreliable actions, probabilistic success
- **if**: Precondition checks, resource validation
- **succeed_always**: Optional tasks, best-effort
### Logic Control
- **invert**: Negate conditions, flip results
- **fail_always**: Disable branches, testing
### Iteration
- **repeat N**: Fixed loops, deterministic behavior
- **repeat min..max**: Variable loops, procedural variation
## Best Practices
### 1. Use Guards for Expensive Checks
**Avoid:**
```storybook
then expensive {
if(complex_condition)
ExpensiveOperation
}
```
**Prefer:**
```storybook
if(complex_condition) {
ExpensiveOperation // Only runs if condition passes
}
```
### 2. Combine Timeout with Retry
**Avoid:** Infinite retry loops
**Prefer:**
```storybook
timeout(30s) {
retry(5) {
UnreliableAction
}
}
```
### 3. Use Cooldowns for Rate Limiting
**Avoid:** Manual timing in actions
**Prefer:**
```storybook
cooldown(10s) {
FireCannon
}
```
### 4. Invert for Readable Conditions
**Avoid:**
```storybook
choose options {
then branch_a {
if(not is_dangerous)
Explore
}
}
```
**Prefer:**
```storybook
choose options {
then branch_a {
invert { IsDangerous }
Explore
}
}
```
### 5. succeed_always for Optional Tasks
**Avoid:**
```storybook
then quest {
MainTask
choose optional {
BonusTask
DoNothing // Awkward fallback
}
NextTask
}
```
**Prefer:**
```storybook
then quest {
MainTask
succeed_always { BonusTask } // Try bonus, don't fail quest
NextTask
}
```
## Cross-References
- [Behavior Trees](./11-behavior-trees.md) - Using decorators in behavior trees
- [Expression Language](./17-expressions.md) - Guard condition syntax
- [Value Types](./18-value-types.md) - Duration literals
- [Design Patterns](../advanced/20-patterns.md) - Common decorator patterns
## Related Concepts
- **Composability**: Decorators can nest for complex control flow
- **Separation of concerns**: Decorators handle control flow, children handle logic
- **State management**: Stateful decorators (repeat, retry, timeout, cooldown) persist across ticks
- **Performance**: Guards prevent unnecessary child execution