Files
storybook/docs/book/reference/12-decorators.html

838 lines
33 KiB
HTML
Raw Normal View History

<!DOCTYPE HTML>
<html lang="en" class="light sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Decorators - Storybook Language Guide</title>
<!-- Custom HTML head -->
<meta name="description" content="Comprehensive documentation for the Storybook narrative simulation language">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Start loading toc.js asap -->
<script src="../toc.js"></script>
</head>
<body>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('light')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Storybook Language Guide</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/r3t-studios/storybook" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="decorators"><a class="header" href="#decorators">Decorators</a></h1>
<p>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.</p>
<h2 id="what-are-decorators"><a class="header" href="#what-are-decorators">What Are Decorators?</a></h2>
<p>Decorators sit between a parent and child node, transforming the childs behavior:</p>
<pre><code>Parent
└─ Decorator
└─ Child
</code></pre>
<p>The decorator intercepts the childs execution, potentially:</p>
<ul>
<li>Repeating it multiple times</li>
<li>Timing it out</li>
<li>Inverting its result</li>
<li>Guarding its execution</li>
<li>Forcing a specific result</li>
</ul>
<h2 id="decorator-types"><a class="header" href="#decorator-types">Decorator Types</a></h2>
<p>Storybook provides 10 decorator types:</p>
<div class="table-wrapper"><table><thead><tr><th>Decorator</th><th>Purpose</th><th>Example</th></tr></thead><tbody>
<tr><td><code>repeat</code></td><td>Loop infinitely</td><td><code>repeat { Patrol }</code></td></tr>
<tr><td><code>repeat(N)</code></td><td>Loop N times</td><td><code>repeat(3) { Check }</code></td></tr>
<tr><td><code>repeat(min..max)</code></td><td>Loop min to max times</td><td><code>repeat(2..5) { Search }</code></td></tr>
<tr><td><code>invert</code></td><td>Flip success/failure</td><td><code>invert { IsEnemy }</code></td></tr>
<tr><td><code>retry(N)</code></td><td>Retry on failure (max N times)</td><td><code>retry(5) { Open }</code></td></tr>
<tr><td><code>timeout(duration)</code></td><td>Fail after duration</td><td><code>timeout(10s) { Solve }</code></td></tr>
<tr><td><code>cooldown(duration)</code></td><td>Run at most once per duration</td><td><code>cooldown(30s) { Fire }</code></td></tr>
<tr><td><code>if(condition)</code></td><td>Only run if condition true</td><td><code>if(health &gt; 50) { Attack }</code></td></tr>
<tr><td><code>succeed_always</code></td><td>Always return success</td><td><code>succeed_always { Try }</code></td></tr>
<tr><td><code>fail_always</code></td><td>Always return failure</td><td><code>fail_always { Test }</code></td></tr>
</tbody></table>
</div>
<h2 id="syntax"><a class="header" href="#syntax">Syntax</a></h2>
<pre><code class="language-bnf">&lt;decorator&gt; ::= &lt;repeat-decorator&gt;
| &lt;invert-decorator&gt;
| &lt;retry-decorator&gt;
| &lt;timeout-decorator&gt;
| &lt;cooldown-decorator&gt;
| &lt;if-decorator&gt;
| &lt;force-result-decorator&gt;
&lt;repeat-decorator&gt; ::= "repeat" &lt;repeat-spec&gt;? "{" &lt;behavior-node&gt; "}"
&lt;repeat-spec&gt; ::= "(" &lt;number&gt; ")"
| "(" &lt;number&gt; ".." &lt;number&gt; ")"
&lt;invert-decorator&gt; ::= "invert" "{" &lt;behavior-node&gt; "}"
&lt;retry-decorator&gt; ::= "retry" "(" &lt;number&gt; ")" "{" &lt;behavior-node&gt; "}"
&lt;timeout-decorator&gt; ::= "timeout" "(" &lt;duration-literal&gt; ")" "{" &lt;behavior-node&gt; "}"
&lt;cooldown-decorator&gt; ::= "cooldown" "(" &lt;duration-literal&gt; ")" "{" &lt;behavior-node&gt; "}"
&lt;if-decorator&gt; ::= "if" "(" &lt;expression&gt; ")" "{" &lt;behavior-node&gt; "}"
&lt;force-result-decorator&gt; ::= ("succeed_always" | "fail_always") "{" &lt;behavior-node&gt; "}"
&lt;duration-literal&gt; ::= &lt;number&gt; ("s" | "m" | "h" | "d")
</code></pre>
<h2 id="repeat-decorators"><a class="header" href="#repeat-decorators">Repeat Decorators</a></h2>
<h3 id="infinite-repeat-repeat"><a class="header" href="#infinite-repeat-repeat">Infinite Repeat: <code>repeat</code></a></h3>
<p>Loops the child infinitely. The child is re-executed immediately after completing (success or failure).</p>
<pre><code class="language-storybook">behavior InfinitePatrol {
repeat {
PatrolRoute
}
}
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li>Run child</li>
<li>Child completes (success or failure)</li>
<li>Immediately run child again</li>
<li>Go to step 2 (forever)</li>
</ol>
<p><strong>Use cases:</strong></p>
<ul>
<li>Perpetual patrols</li>
<li>Endless background processes</li>
<li>Continuous monitoring</li>
</ul>
<p><strong>Warning:</strong> Infinite loops never return to parent. Ensure theyre appropriate for your use case.</p>
<h3 id="repeat-n-times-repeat-n"><a class="header" href="#repeat-n-times-repeat-n">Repeat N Times: <code>repeat N</code></a></h3>
<p>Repeats the child exactly N times, then returns success.</p>
<pre><code class="language-storybook">behavior CheckThreeTimes {
repeat(3) {
CheckDoor
}
}
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li>Counter = 0</li>
<li>Run child</li>
<li>Counter++</li>
<li>If counter &lt; N, go to step 2</li>
<li>Return success</li>
</ol>
<p><strong>Use cases:</strong></p>
<ul>
<li>Fixed iteration counts</li>
<li>“Try three times then give up”</li>
<li>Deterministic looping</li>
</ul>
<h3 id="repeat-range-repeat-minmax"><a class="header" href="#repeat-range-repeat-minmax">Repeat Range: <code>repeat min..max</code></a></h3>
<p>Repeats the child between min and max times. At runtime, a specific count is selected within the range.</p>
<pre><code class="language-storybook">behavior SearchRandomly {
repeat(2..5) {
SearchArea
}
}
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li>Select count C randomly from [min, max]</li>
<li>Repeat child C times (as in <code>repeat N</code>)</li>
</ol>
<p><strong>Use cases:</strong></p>
<ul>
<li>Variable behavior</li>
<li>Procedural variation</li>
<li>Non-deterministic actions</li>
</ul>
<p><strong>Validation:</strong></p>
<ul>
<li>min ≥ 0</li>
<li>max ≥ min</li>
<li>Both must be integers</li>
</ul>
<h2 id="invert-decorator"><a class="header" href="#invert-decorator">Invert Decorator</a></h2>
<p>Inverts the childs return value: success becomes failure, failure becomes success.</p>
<pre><code class="language-storybook">behavior AvoidEnemies {
invert {
IsEnemyNearby // Success if NOT nearby
}
}
</code></pre>
<p><strong>Truth table:</strong></p>
<div class="table-wrapper"><table><thead><tr><th>Child returns</th><th>Decorator returns</th></tr></thead><tbody>
<tr><td>Success</td><td>Failure</td></tr>
<tr><td>Failure</td><td>Success</td></tr>
<tr><td>Running</td><td>Running (unchanged)</td></tr>
</tbody></table>
</div>
<p><strong>Use cases:</strong></p>
<ul>
<li>Negating conditions (“if NOT X”)</li>
<li>Inverting success criteria</li>
<li>Converting “found” to “not found”</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code class="language-storybook">behavior SafeExploration {
choose safe_actions {
// Only explore if NOT dangerous
then explore {
invert { IsDangerous }
ExploreArea
}
// Default: Stay put
Wait
}
}
</code></pre>
<h2 id="retry-decorator"><a class="header" href="#retry-decorator">Retry Decorator</a></h2>
<p>Retries the child up to N times on failure. Returns success if any attempt succeeds, failure if all N attempts fail.</p>
<pre><code class="language-storybook">behavior PersistentDoor {
retry(5) {
OpenLockedDoor
}
}
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li>Attempts = 0</li>
<li>Run child</li>
<li>If child succeeds → return success</li>
<li>If child fails:
<ul>
<li>Attempts++</li>
<li>If attempts &lt; N, go to step 2</li>
<li>Else return failure</li>
</ul>
</li>
</ol>
<p><strong>Use cases:</strong></p>
<ul>
<li>Unreliable actions (lockpicking, persuasion)</li>
<li>Network/resource operations</li>
<li>Probabilistic success</li>
</ul>
<p><strong>Example with context:</strong></p>
<pre><code class="language-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
}
}
</code></pre>
<p><strong>Validation:</strong></p>
<ul>
<li>N must be ≥ 1</li>
</ul>
<h2 id="timeout-decorator"><a class="header" href="#timeout-decorator">Timeout Decorator</a></h2>
<p>Fails the child if it doesnt complete within the specified duration.</p>
<pre><code class="language-storybook">behavior TimeLimitedPuzzle {
timeout(30s) {
SolvePuzzle
}
}
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li>Start timer</li>
<li>Run child each tick</li>
<li>If child completes before timeout → return childs result</li>
<li>If timer expires → return failure (interrupt child)</li>
</ol>
<p><strong>Use cases:</strong></p>
<ul>
<li>Time-limited actions</li>
<li>Preventing infinite loops</li>
<li>Enforcing deadlines</li>
</ul>
<p><strong>Duration formats:</strong></p>
<ul>
<li><code>5s</code> - 5 seconds</li>
<li><code>10m</code> - 10 minutes</li>
<li><code>2h</code> - 2 hours</li>
<li><code>3d</code> - 3 days</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code class="language-storybook">behavior QuickDecision {
choose timed_choice {
// Give AI 5 seconds to find optimal move
timeout(5s) {
CalculateOptimalStrategy
}
// Fallback: Use simple heuristic
UseQuickHeuristic
}
}
</code></pre>
<p><strong>Notes:</strong></p>
<ul>
<li>Timer starts when decorator is first entered</li>
<li>Timer resets if decorator exits and re-enters</li>
<li>Child node should handle interruption gracefully</li>
</ul>
<h2 id="cooldown-decorator"><a class="header" href="#cooldown-decorator">Cooldown Decorator</a></h2>
<p>Prevents the child from running more than once per cooldown period. Fails immediately if called within cooldown.</p>
<pre><code class="language-storybook">behavior SpecialAbility {
cooldown(30s) {
FireCannon
}
}
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li>Check last execution time</li>
<li>If (current_time - last_time) &lt; cooldown → return failure</li>
<li>Else:
<ul>
<li>Run child</li>
<li>Record current_time as last_time</li>
<li>Return childs result</li>
</ul>
</li>
</ol>
<p><strong>Use cases:</strong></p>
<ul>
<li>Rate-limiting abilities</li>
<li>Resource cooldowns (spells, items)</li>
<li>Preventing spam</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code class="language-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
}
}
</code></pre>
<p><strong>State management:</strong></p>
<ul>
<li>Cooldown state persists across behavior tree ticks</li>
<li>Each cooldown decorator instance has independent state</li>
<li>Cooldown timers are per-entity (not global)</li>
</ul>
<h2 id="if-decorator"><a class="header" href="#if-decorator">If Decorator</a></h2>
<p>Only runs the child if the condition is true. Fails immediately if condition is false.</p>
<pre><code class="language-storybook">behavior ConditionalAttack {
if(health &gt; 50) {
AggressiveAttack
}
}
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li>Evaluate condition expression</li>
<li>If true → run child and return its result</li>
<li>If false → return failure (do not run child)</li>
</ol>
<p><strong>Use cases:</strong></p>
<ul>
<li>Preconditions (“only if X”)</li>
<li>Resource checks (“only if have mana”)</li>
<li>Safety checks (“only if safe”)</li>
</ul>
<p><strong>Expression syntax:</strong>
See <a href="./17-expressions.html">Expression Language</a> for complete syntax.</p>
<p><strong>Examples:</strong></p>
<pre><code class="language-storybook">behavior GuardedActions {
choose options {
// Only attack if have weapon and enemy close
if(has_weapon and distance &lt; 10) {
Attack
}
// Only heal if health below 50%
if(health &lt; (max_health * 0.5)) {
Heal
}
// Only flee if outnumbered
if(enemy_count &gt; ally_count) {
Flee
}
}
}
</code></pre>
<p><strong>Comparison with bare <code>if</code> conditions:</strong></p>
<pre><code class="language-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
}
}
</code></pre>
<p>The <code>if</code> decorator with a body is more efficient for gating expensive subtrees.</p>
<h2 id="force-result-decorators"><a class="header" href="#force-result-decorators">Force Result Decorators</a></h2>
<h3 id="succeed_always"><a class="header" href="#succeed_always"><code>succeed_always</code></a></h3>
<p>Always returns success, regardless of childs actual result.</p>
<pre><code class="language-storybook">behavior TryOptionalTask {
succeed_always {
AttemptBonus // Even if fails, we don't care
}
}
</code></pre>
<p><strong>Use cases:</strong></p>
<ul>
<li>Optional tasks that shouldnt block progress</li>
<li>Logging/monitoring actions</li>
<li>Best-effort operations</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code class="language-storybook">behavior QuestSequence {
then main_quest {
TalkToNPC
// Try to find secret, but don't fail quest if not found
succeed_always {
SearchForSecretDoor
}
ReturnToQuestGiver
}
}
</code></pre>
<h3 id="fail_always"><a class="header" href="#fail_always"><code>fail_always</code></a></h3>
<p>Always returns failure, regardless of childs actual result.</p>
<pre><code class="language-storybook">behavior TestFailure {
fail_always {
AlwaysSucceedsAction // But we force it to fail
}
}
</code></pre>
<p><strong>Use cases:</strong></p>
<ul>
<li>Testing/debugging</li>
<li>Forcing alternative paths</li>
<li>Disabling branches temporarily</li>
</ul>
<p><strong>Example:</strong></p>
<pre><code class="language-storybook">behavior UnderConstruction {
choose abilities {
// Temporarily disabled feature
fail_always {
NewExperimentalAbility
}
// Fallback to old ability
ClassicAbility
}
}
</code></pre>
<h2 id="combining-decorators"><a class="header" href="#combining-decorators">Combining Decorators</a></h2>
<p>Decorators can nest to create complex behaviors:</p>
<pre><code class="language-storybook">behavior ComplexPattern {
// Repeat 3 times, each with 10 second timeout
repeat(3) {
timeout(10s) {
SolveSubproblem
}
}
}
</code></pre>
<p><strong>More complex nesting:</strong></p>
<pre><code class="language-storybook">behavior ResilientAction {
// If: Only if health &gt; 30
if(health &gt; 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
}
}
}
}
}
</code></pre>
<p><strong>Execution order:</strong> Outside → Inside</p>
<ol>
<li>If checks condition</li>
<li>Timeout starts timer</li>
<li>Retry begins first attempt</li>
<li>Cooldown checks last execution time</li>
<li>Child action runs</li>
</ol>
<h2 id="duration-syntax"><a class="header" href="#duration-syntax">Duration Syntax</a></h2>
<p>Timeout and cooldown decorators use duration literals:</p>
<pre><code class="language-bnf">&lt;duration-literal&gt; ::= &lt;number&gt; &lt;unit&gt;
&lt;unit&gt; ::= "s" // seconds
| "m" // minutes
| "h" // hours
| "d" // days
&lt;number&gt; ::= &lt;digit&gt;+
</code></pre>
<p><strong>Examples:</strong></p>
<ul>
<li><code>5s</code> - 5 seconds</li>
<li><code>30s</code> - 30 seconds</li>
<li><code>2m</code> - 2 minutes</li>
<li><code>10m</code> - 10 minutes</li>
<li><code>1h</code> - 1 hour</li>
<li><code>24h</code> - 24 hours</li>
<li><code>7d</code> - 7 days</li>
</ul>
<p><strong>Validation:</strong></p>
<ul>
<li>Number must be positive integer</li>
<li>No compound durations (use <code>120s</code> not <code>2m</code> if runtime expects seconds)</li>
<li>No fractional units (<code>1.5m</code> not allowed; use <code>90s</code>)</li>
</ul>
<h2 id="comparison-table"><a class="header" href="#comparison-table">Comparison Table</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Decorator</th><th>Affects Success</th><th>Affects Failure</th><th>Affects Running</th><th>Stateful</th></tr></thead><tbody>
<tr><td><code>repeat</code></td><td>Repeat</td><td>Repeat</td><td>Wait</td><td>Yes (counter)</td></tr>
<tr><td><code>repeat N</code></td><td>Repeat</td><td>Repeat</td><td>Wait</td><td>Yes (counter)</td></tr>
<tr><td><code>repeat min..max</code></td><td>Repeat</td><td>Repeat</td><td>Wait</td><td>Yes (counter)</td></tr>
<tr><td><code>invert</code></td><td>→ Failure</td><td>→ Success</td><td>Unchanged</td><td>No</td></tr>
<tr><td><code>retry N</code></td><td>→ Success</td><td>Retry or fail</td><td>Wait</td><td>Yes (attempts)</td></tr>
<tr><td><code>timeout dur</code></td><td>→ Success</td><td>→ Success</td><td>→ Failure if expired</td><td>Yes (timer)</td></tr>
<tr><td><code>cooldown dur</code></td><td>→ Success</td><td>→ Success</td><td>→ Success</td><td>Yes (last time)</td></tr>
<tr><td><code>if(expr)</code></td><td>→ Success</td><td>→ Success</td><td>→ Success</td><td>No</td></tr>
<tr><td><code>succeed_always</code></td><td>→ Success</td><td>→ Success</td><td>→ Success</td><td>No</td></tr>
<tr><td><code>fail_always</code></td><td>→ Failure</td><td>→ Failure</td><td>→ Failure</td><td>No</td></tr>
</tbody></table>
</div>
<p><strong>Stateful decorators</strong> maintain state across ticks. <strong>Stateless decorators</strong> evaluate fresh every tick.</p>
<h2 id="validation-rules"><a class="header" href="#validation-rules">Validation Rules</a></h2>
<ol>
<li><strong>Child required</strong>: All decorators must have exactly one child node</li>
<li><strong>Repeat count</strong>: <code>repeat N</code> requires N ≥ 0</li>
<li><strong>Repeat range</strong>: <code>repeat min..max</code> requires 0 ≤ min ≤ max</li>
<li><strong>Retry count</strong>: <code>retry N</code> requires N ≥ 1</li>
<li><strong>Duration positive</strong>: Timeout/cooldown durations must be &gt; 0</li>
<li><strong>Duration format</strong>: Must match <code>&lt;number&gt;&lt;unit&gt;</code> (e.g., <code>10s</code>, <code>5m</code>)</li>
<li><strong>Guard expression</strong>: Guard condition must be valid expression</li>
<li><strong>No empty decorators</strong>: <code>repeat { }</code> is invalid (missing child)</li>
</ol>
<h2 id="use-cases-by-category"><a class="header" href="#use-cases-by-category">Use Cases by Category</a></h2>
<h3 id="timing-control"><a class="header" href="#timing-control">Timing Control</a></h3>
<ul>
<li><strong>timeout</strong>: Prevent infinite loops, enforce time limits</li>
<li><strong>cooldown</strong>: Rate-limit abilities, prevent spam</li>
<li><strong>repeat</strong>: Continuous processes, patrols</li>
</ul>
<h3 id="reliability"><a class="header" href="#reliability">Reliability</a></h3>
<ul>
<li><strong>retry</strong>: Handle unreliable actions, probabilistic success</li>
<li><strong>if</strong>: Precondition checks, resource validation</li>
<li><strong>succeed_always</strong>: Optional tasks, best-effort</li>
</ul>
<h3 id="logic-control"><a class="header" href="#logic-control">Logic Control</a></h3>
<ul>
<li><strong>invert</strong>: Negate conditions, flip results</li>
<li><strong>fail_always</strong>: Disable branches, testing</li>
</ul>
<h3 id="iteration"><a class="header" href="#iteration">Iteration</a></h3>
<ul>
<li><strong>repeat N</strong>: Fixed loops, deterministic behavior</li>
<li><strong>repeat min..max</strong>: Variable loops, procedural variation</li>
</ul>
<h2 id="best-practices"><a class="header" href="#best-practices">Best Practices</a></h2>
<h3 id="1-use-guards-for-expensive-checks"><a class="header" href="#1-use-guards-for-expensive-checks">1. Use Guards for Expensive Checks</a></h3>
<p><strong>Avoid:</strong></p>
<pre><code class="language-storybook">then expensive {
if(complex_condition)
ExpensiveOperation
}
</code></pre>
<p><strong>Prefer:</strong></p>
<pre><code class="language-storybook">if(complex_condition) {
ExpensiveOperation // Only runs if condition passes
}
</code></pre>
<h3 id="2-combine-timeout-with-retry"><a class="header" href="#2-combine-timeout-with-retry">2. Combine Timeout with Retry</a></h3>
<p><strong>Avoid:</strong> Infinite retry loops</p>
<p><strong>Prefer:</strong></p>
<pre><code class="language-storybook">timeout(30s) {
retry(5) {
UnreliableAction
}
}
</code></pre>
<h3 id="3-use-cooldowns-for-rate-limiting"><a class="header" href="#3-use-cooldowns-for-rate-limiting">3. Use Cooldowns for Rate Limiting</a></h3>
<p><strong>Avoid:</strong> Manual timing in actions</p>
<p><strong>Prefer:</strong></p>
<pre><code class="language-storybook">cooldown(10s) {
FireCannon
}
</code></pre>
<h3 id="4-invert-for-readable-conditions"><a class="header" href="#4-invert-for-readable-conditions">4. Invert for Readable Conditions</a></h3>
<p><strong>Avoid:</strong></p>
<pre><code class="language-storybook">choose options {
then branch_a {
if(not is_dangerous)
Explore
}
}
</code></pre>
<p><strong>Prefer:</strong></p>
<pre><code class="language-storybook">choose options {
then branch_a {
invert { IsDangerous }
Explore
}
}
</code></pre>
<h3 id="5-succeed_always-for-optional-tasks"><a class="header" href="#5-succeed_always-for-optional-tasks">5. succeed_always for Optional Tasks</a></h3>
<p><strong>Avoid:</strong></p>
<pre><code class="language-storybook">then quest {
MainTask
choose optional {
BonusTask
DoNothing // Awkward fallback
}
NextTask
}
</code></pre>
<p><strong>Prefer:</strong></p>
<pre><code class="language-storybook">then quest {
MainTask
succeed_always { BonusTask } // Try bonus, don't fail quest
NextTask
}
</code></pre>
<h2 id="cross-references"><a class="header" href="#cross-references">Cross-References</a></h2>
<ul>
<li><a href="./11-behavior-trees.html">Behavior Trees</a> - Using decorators in behavior trees</li>
<li><a href="./17-expressions.html">Expression Language</a> - Guard condition syntax</li>
<li><a href="./18-value-types.html">Value Types</a> - Duration literals</li>
<li><a href="../advanced/20-patterns.html">Design Patterns</a> - Common decorator patterns</li>
</ul>
<h2 id="related-concepts"><a class="header" href="#related-concepts">Related Concepts</a></h2>
<ul>
<li><strong>Composability</strong>: Decorators can nest for complex control flow</li>
<li><strong>Separation of concerns</strong>: Decorators handle control flow, children handle logic</li>
<li><strong>State management</strong>: Stateful decorators (repeat, retry, timeout, cooldown) persist across ticks</li>
<li><strong>Performance</strong>: Guards prevent unnecessary child execution</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../reference/11-behavior-trees.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../reference/13-life-arcs.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../reference/11-behavior-trees.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../reference/13-life-arcs.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload();
}
};
window.onbeforeunload = function() {
socket.close();
}
</script>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>