Files
storybook/docs/book/reference/12-decorators.html
Sienna Meridian Satterwhite 16deb5d237 release: Storybook v0.2.0 - Major syntax and features update
BREAKING CHANGES:
- Relationship syntax now requires blocks for all participants
- Removed self/other perspective blocks from relationships
- Replaced 'guard' keyword with 'if' for behavior tree decorators

Language Features:
- Add tree-sitter grammar with improved if/condition disambiguation
- Add comprehensive tutorial and reference documentation
- Add SBIR v0.2.0 binary format specification
- Add resource linking system for behaviors and schedules
- Add year-long schedule patterns (day, season, recurrence)
- Add behavior tree enhancements (named nodes, decorators)

Documentation:
- Complete tutorial series (9 chapters) with baker family examples
- Complete reference documentation for all language features
- SBIR v0.2.0 specification with binary format details
- Added locations and institutions documentation

Examples:
- Convert all examples to baker family scenario
- Add comprehensive working examples

Tooling:
- Zed extension with LSP integration
- Tree-sitter grammar for syntax highlighting
- Build scripts and development tools

Version Updates:
- Main package: 0.1.0 → 0.2.0
- Tree-sitter grammar: 0.1.0 → 0.2.0
- Zed extension: 0.1.0 → 0.2.0
- Storybook editor: 0.1.0 → 0.2.0
2026-02-13 21:52:03 +00:00

838 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>