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
838 lines
33 KiB
HTML
838 lines
33 KiB
HTML
<!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 child’s behavior:</p>
|
||
<pre><code>Parent
|
||
└─ Decorator
|
||
└─ Child
|
||
</code></pre>
|
||
<p>The decorator intercepts the child’s 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 > 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"><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")
|
||
</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 they’re 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 < 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 child’s 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 < 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 doesn’t 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 child’s 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) < cooldown → return failure</li>
|
||
<li>Else:
|
||
<ul>
|
||
<li>Run child</li>
|
||
<li>Record current_time as last_time</li>
|
||
<li>Return child’s 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 > 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 < 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
|
||
}
|
||
}
|
||
}
|
||
</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 child’s 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 shouldn’t 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 child’s 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 > 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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</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"><duration-literal> ::= <number> <unit>
|
||
|
||
<unit> ::= "s" // seconds
|
||
| "m" // minutes
|
||
| "h" // hours
|
||
| "d" // days
|
||
|
||
<number> ::= <digit>+
|
||
</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 > 0</li>
|
||
<li><strong>Duration format</strong>: Must match <code><number><unit></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>
|