├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── deno.json ├── example ├── behavior3-ts.b3-workspace ├── hero.json ├── monster.json ├── node-config.b3-setting ├── test-array.json ├── test-listen.json ├── test-once.json ├── test-parallel.json ├── test-race.json ├── test-repeat-until-failure.json ├── test-repeat-until-success.json ├── test-sequence.json ├── test-switch-case.json └── test-timeout.json ├── images ├── behavior3-editor-running.png └── behavior3-editor.png ├── src └── behavior3 │ ├── blackboard.ts │ ├── context.ts │ ├── evaluator.ts │ ├── index.ts │ ├── node.ts │ ├── nodes │ ├── actions │ │ ├── calculate.ts │ │ ├── concat.ts │ │ ├── get-field.ts │ │ ├── index.ts │ │ ├── just-success.ts │ │ ├── let.ts │ │ ├── log.ts │ │ ├── math.ts │ │ ├── now.ts │ │ ├── push.ts │ │ ├── random-index.ts │ │ ├── set-field.ts │ │ ├── wait-for-event.ts │ │ └── wait.ts │ ├── composites │ │ ├── ifelse.ts │ │ ├── parallel.ts │ │ ├── race.ts │ │ ├── selector.ts │ │ ├── sequence.ts │ │ └── switch.ts │ ├── conditions │ │ ├── check.ts │ │ ├── includes.ts │ │ ├── is-null.ts │ │ └── not-null.ts │ └── decorators │ │ ├── always-failure.ts │ │ ├── always-running.ts │ │ ├── always-success.ts │ │ ├── assert.ts │ │ ├── delay.ts │ │ ├── filter.ts │ │ ├── foreach.ts │ │ ├── invert.ts │ │ ├── listen.ts │ │ ├── once.ts │ │ ├── repeat.ts │ │ ├── retry-until-failure.ts │ │ ├── retry-until-success.ts │ │ └── timeout.ts │ ├── stack.ts │ └── tree.ts ├── test ├── main.ts ├── nodes │ ├── attack.ts │ ├── find-enemy.ts │ ├── get-hp.ts │ ├── idle.ts │ ├── is-status.ts │ ├── move-to-pos.ts │ └── move-to-target.ts └── role.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 100, 4 | "overrides": [ 5 | { 6 | "files": "*.json", 7 | "options": { 8 | "tabWidth": 2 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "request": "launch", 9 | "name": "Deno", 10 | "type": "node", 11 | "cwd": "${workspaceFolder}", 12 | "env": {}, 13 | "runtimeExecutable": "deno", 14 | "runtimeArgs": [ 15 | "run", 16 | "--inspect-wait", 17 | "-A", 18 | "test/main.ts", 19 | "--allow-all", 20 | "--build", 21 | "web" 22 | ], 23 | "attachSimplePort": 9229 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": "explicit" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 codetypes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 行为树框架 for typescript/javascript 2 | 3 | ## 用途 4 | 5 | 在游戏中使用行为树,可以大大的降低 AI、技能、BUFF 实现的复杂度,同时提高配置的灵活度,而且更加容易找到游戏行为中的问题。 6 | 7 | 以下是最主要的使用用途: 8 | 9 | - 游戏中的人物的 AI 10 | - 游戏中的技能和 BUFF 11 | - 游戏中 NPC 的工作流程 12 | - 游戏中的新手引导 13 | - 游戏中的业务工作流或决策行为 14 | 15 | ## 节点定义 16 | 17 | ```javascript 18 | export interface NodeDef { 19 | name: string; 20 | /** 21 | * Recommended type used for the node definition: 22 | * + `Action`: No children allowed, returns `success`, `failure` or `running`. 23 | * + `Decorator`: Only one child allowed, returns `success`, `failure` or `running`. 24 | * + `Composite`: Contains more than one child, returns `success`, `failure` or `running`. 25 | * + `Condition`: No children allowed, no output, returns `success` or `failure`. 26 | */ 27 | type: "Action" | "Decorator" | "Condition" | "Composite"; 28 | desc: string; 29 | input?: string[]; // ["input1?", "input2..."] 30 | output?: string[]; // ["output1", "output2..."] 31 | args?: { 32 | name: string, 33 | type: 34 | | "bool" 35 | | "bool?" 36 | | "bool[]" 37 | | "bool[]?" 38 | | "int" 39 | | "int?" 40 | | "int[]" 41 | | "int[]?" 42 | | "float" 43 | | "float?" 44 | | "float[]" 45 | | "float[]?" 46 | | "string" 47 | | "string?" 48 | | "string[]" 49 | | "string[]?" 50 | | "json" 51 | | "json?" 52 | | "json[]" 53 | | "json[]?" 54 | | "expr" 55 | | "expr?" 56 | | "expr[]" 57 | | "expr[]?", 58 | desc: string, 59 | oneof?: string, // Input `value`, only one is allowed between `value` and this arg. 60 | default?: unknown, 61 | options?: { name: string, value: unknown, desc?: string }[], 62 | }[]; 63 | doc?: string; // markdown 64 | } 65 | ``` 66 | 67 | #### 常量 68 | 69 | 通常是固定值,比如范围,类型之类的,常量支持可选和必填。 70 | 71 | #### 输入/输出变量 72 | 73 | 通常情况,一个节点并不能完成自己的工作,需要依赖于上一个节点的输出,或者依赖于外部的输入,所以大多数行为树设计都会提供一个数据结构来记录节点的运行状态,称之为“黑板”,节点可以从黑板中读取变量,也可以写入变量。 74 | 75 | 让我们来看一个简单的例子,在特定的范围(`w`, `h`)内寻找大于指定数量(`count`)的敌人,并把找到的敌人放在变量 (`target`) 中,这个节点的大致实现如下: 76 | 77 | ```typescript 78 | export class FindEnemy extends Node { 79 | declare input: [number]; 80 | declare args: { 81 | readonly w: number; 82 | readonly h: number; 83 | }; 84 | 85 | override onTick(tree: Tree): Status { 86 | const [count] = this.input; 87 | const { w, h } = this.args; 88 | const { x, y } = tree.owner; 89 | const list = tree.context.find((role: Role) => { 90 | if (role === tree.owner) { 91 | return false; 92 | } 93 | const tx = role.x; 94 | const ty = role.y; 95 | return Math.abs(x - tx) <= w && Math.abs(y - ty) <= h; 96 | }); 97 | if (list.length >= count) { 98 | this.output.push(...list); 99 | return "success"; 100 | } else { 101 | return "failure"; 102 | } 103 | } 104 | 105 | static override get descriptor(): NodeDef { 106 | return { 107 | name: "FindEnemy", 108 | type: "Action", 109 | desc: "寻找敌人", 110 | args: [ 111 | { name: "w", type: "int", desc: "宽度" }, 112 | { name: "h", type: "int", desc: "高度" }, 113 | ], 114 | input: ["count"], 115 | output: ["target"], 116 | }; 117 | } 118 | } 119 | ``` 120 | 121 | 上述节点执行成功后,黑板上的 `target` 变量就会被赋值成查找到的敌人列表,这样后面的节点就可以使用 `target` 这个变量作为输入了。 122 | 123 | #### 状态返回 124 | 125 | 我们使用栈来实现行为树的调度,当行为树节点运行的时候,会把节点压入栈,只有当节点返回 `success` 或 `failure` 时,才会出栈,继续执行下一个节点,否则会一直执行栈顶的节点,直到返回 `success` 或 `failure` 为止。 126 | 127 | - success 节点执行成功 128 | - failure 节点执行失败 129 | - running 节点运行中 130 | 131 | ![](images/behavior3-editor-running.png) 132 | 133 | ## 内置节点 134 | 135 | ### 行为节点 136 | 137 | - Calculate 简单的数值公式计算 138 | - Concate 拼接两个数组 139 | - GetField 获取对象的字段值 140 | - Index 获取数组的元素 141 | - Let 定义变量 142 | - Log 打印日志 143 | - Push 向数组中添加元素 144 | - RandomIndex 随机返回数组的一个元素及其索引 145 | - Random 返回一个随机数值 146 | - SetField 设置对象的字段值 147 | - Wait 等待一段时间 148 | 149 | ### 复合节点 150 | 151 | - Race 竞争执行,并行执行所有子节点,优先返回第一个成功的子节点 152 | - Parallel 并行执行, 执行所有子节点 153 | - Sequence 顺序执行,执行所有子节点直到返回 false 154 | - Selector 选择执行,执行所有子节点直到返回 true 155 | - IfElse 条件执行,根据条件执行不同的子节点,并返回子节点执行状态 156 | - Switch 分支执行,根据 `case` 条件执行不同的分支子节点,并返回分支子节点执行状态 157 | 158 | ### 条件节点 159 | 160 | - Check 检查表达式 161 | - IsNull 检查值是否为空 162 | - NotNull 检查值是否不为空 163 | - Includes 判断元素是否在数组中 164 | 165 | ### 装饰节点 166 | 167 | - AlwaysSuccess 永远返回成功 168 | - AlwaysFailure 永远返回失败 169 | - Assert 断言,如果断言失败,抛出异常,一般用于调试测试 170 | - Delay 延时执行子节点 171 | - Filter 返回满足条件的元素 172 | - Foreach 遍历数组 173 | - Invert 反转子节点执行状态 174 | - Listen 监听事件并执行子节点 175 | - Once 只执行一次 176 | - Repeat 重复执行子节点 177 | - RetryUntilFailure 一直尝试执行子节点直到返回失败 178 | - RetryUntilSuccess 一直尝试执行子节点直到返回成功 179 | - Timeout 限定时间内执行子节点 180 | 181 | ## 编辑器 182 | 183 | 基于 antv G6 图形库开发了一个通用的行为树编辑器,感兴趣的同学可以关注一下 [behavior3editor](https://github.com/zhandouxiaojiji/behavior3editor) 184 | ![](images/behavior3-editor.png) 185 | 186 | ## 运行测试用例 187 | 188 | - Deno 安装 189 | 190 | - windows powershell 191 | ```powershell 192 | irm https://deno.land/install.ps1 | iex 193 | ``` 194 | - macos & linux terminal 195 | ```bash 196 | curl -fsSL https://deno.land/install.sh | sh 197 | ``` 198 | - macos 199 | ```bash 200 | brew install deno 201 | ``` 202 | 203 | - 导出节点定义 204 | 205 | ```bash 206 | // fs.writeFileSync("example/node-config.b3-setting", context.exportNodeDefs()); 207 | deno task start 208 | ``` 209 | 210 | - 运行测试 211 | 212 | ```bash 213 | deno task start 214 | ``` 215 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "unstable": ["sloppy-imports"], 3 | "lint": { 4 | "rules": { 5 | "exclude": ["no-sloppy-imports", "no-unused-vars"] 6 | } 7 | }, 8 | "tasks": { 9 | "start": "deno --allow-all test/main.ts", 10 | "check": "deno check ." 11 | }, 12 | "compilerOptions": { 13 | "noImplicitAny": true, 14 | "noImplicitOverride": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "strict": true, 18 | "strictNullChecks": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/behavior3-ts.b3-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "path": "hero.json", 5 | "desc": "英雄测试AI" 6 | }, 7 | { 8 | "path": "monster.json", 9 | "desc": "怪物测试AI" 10 | }, 11 | { 12 | "path": "test-array.json", 13 | "desc": "" 14 | }, 15 | { 16 | "path": "test-listen.json", 17 | "desc": "" 18 | }, 19 | { 20 | "path": "test-once.json", 21 | "desc": "" 22 | }, 23 | { 24 | "path": "test-parallel.json", 25 | "desc": "" 26 | }, 27 | { 28 | "path": "test-race.json", 29 | "desc": "" 30 | }, 31 | { 32 | "path": "test-repeat-until-failure.json", 33 | "desc": "" 34 | }, 35 | { 36 | "path": "test-repeat-until-success.json", 37 | "desc": "" 38 | }, 39 | { 40 | "path": "test-sequence.json", 41 | "desc": "" 42 | }, 43 | { 44 | "path": "test-switch-case.json", 45 | "desc": "" 46 | }, 47 | { 48 | "path": "test-timeout.json", 49 | "desc": "" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /example/hero.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hero", 3 | "root": { 4 | "id": 1, 5 | "name": "Selector", 6 | "desc": "英雄测试AI", 7 | "children": [ 8 | { 9 | "id": 2, 10 | "name": "Once", 11 | "children": [ 12 | { 13 | "id": 3, 14 | "name": "Parallel", 15 | "children": [ 16 | { 17 | "id": 4, 18 | "name": "AlwaysFailure", 19 | "children": [ 20 | { 21 | "id": 5, 22 | "name": "Sequence", 23 | "children": [ 24 | { 25 | "id": 6, 26 | "name": "Log", 27 | "args": { 28 | "message": "B: test sequeue1", 29 | "level": "info" 30 | } 31 | }, 32 | { 33 | "id": 7, 34 | "name": "Log", 35 | "args": { 36 | "message": "B: test sequeue2", 37 | "level": "info" 38 | } 39 | } 40 | ] 41 | } 42 | ] 43 | }, 44 | { 45 | "id": 8, 46 | "name": "AlwaysFailure", 47 | "children": [ 48 | { 49 | "id": 9, 50 | "name": "Sequence", 51 | "debug": true, 52 | "children": [ 53 | { 54 | "id": 10, 55 | "name": "Log", 56 | "args": { 57 | "message": "A: test sequeue1", 58 | "level": "info" 59 | } 60 | }, 61 | { 62 | "id": 11, 63 | "name": "AlwaysFailure", 64 | "children": [ 65 | { 66 | "id": 12, 67 | "name": "Log", 68 | "args": { 69 | "message": "A: test fail", 70 | "level": "info" 71 | } 72 | } 73 | ] 74 | }, 75 | { 76 | "id": 13, 77 | "name": "Log", 78 | "args": { 79 | "message": "A: test sequeue2", 80 | "level": "info" 81 | } 82 | } 83 | ] 84 | } 85 | ] 86 | }, 87 | { 88 | "id": 14, 89 | "name": "AlwaysFailure", 90 | "children": [ 91 | { 92 | "id": 15, 93 | "name": "Selector", 94 | "children": [ 95 | { 96 | "id": 16, 97 | "name": "AlwaysFailure", 98 | "children": [ 99 | { 100 | "id": 17, 101 | "name": "Log", 102 | "args": { 103 | "message": "C: test fail", 104 | "level": "info" 105 | } 106 | } 107 | ] 108 | }, 109 | { 110 | "id": 18, 111 | "name": "Log", 112 | "args": { 113 | "message": "C: test sequeue1", 114 | "level": "info" 115 | } 116 | }, 117 | { 118 | "id": 19, 119 | "name": "Log", 120 | "args": { 121 | "message": "C: test sequeue2", 122 | "level": "info" 123 | } 124 | } 125 | ] 126 | } 127 | ] 128 | }, 129 | { 130 | "id": 20, 131 | "name": "AlwaysFailure", 132 | "children": [ 133 | { 134 | "id": 21, 135 | "name": "Parallel", 136 | "children": [ 137 | { 138 | "id": 22, 139 | "name": "Log", 140 | "args": { 141 | "message": "D: test sequeue1", 142 | "level": "info" 143 | } 144 | }, 145 | { 146 | "id": 23, 147 | "name": "AlwaysFailure", 148 | "children": [ 149 | { 150 | "id": 24, 151 | "name": "Log", 152 | "args": { 153 | "message": "D: test fail", 154 | "level": "info" 155 | } 156 | } 157 | ] 158 | }, 159 | { 160 | "id": 25, 161 | "name": "Log", 162 | "args": { 163 | "message": "D: test sequeue2", 164 | "level": "info" 165 | } 166 | } 167 | ] 168 | } 169 | ] 170 | } 171 | ] 172 | } 173 | ] 174 | }, 175 | { 176 | "id": 26, 177 | "name": "Sequence", 178 | "desc": "攻击", 179 | "children": [ 180 | { 181 | "id": 27, 182 | "name": "FindEnemy", 183 | "args": { 184 | "w": 100, 185 | "h": 50 186 | }, 187 | "output": [ 188 | "enemy" 189 | ], 190 | "debug": true 191 | }, 192 | { 193 | "id": 28, 194 | "name": "Attack", 195 | "input": [ 196 | "enemy" 197 | ] 198 | }, 199 | { 200 | "id": 29, 201 | "name": "Wait", 202 | "args": { 203 | "time": 10 204 | } 205 | } 206 | ] 207 | }, 208 | { 209 | "id": 30, 210 | "name": "Sequence", 211 | "desc": "移动", 212 | "children": [ 213 | { 214 | "id": 31, 215 | "name": "FindEnemy", 216 | "args": { 217 | "w": 1000, 218 | "h": 500 219 | }, 220 | "output": [ 221 | "enemy" 222 | ] 223 | }, 224 | { 225 | "id": 32, 226 | "name": "MoveToTarget", 227 | "input": [ 228 | "enemy" 229 | ] 230 | } 231 | ] 232 | }, 233 | { 234 | "id": 33, 235 | "name": "Sequence", 236 | "desc": "逃跑", 237 | "children": [ 238 | { 239 | "id": 34, 240 | "name": "GetHp", 241 | "output": [ 242 | "hp" 243 | ] 244 | }, 245 | { 246 | "id": 35, 247 | "name": "Check", 248 | "args": { 249 | "value": "hp > 50" 250 | } 251 | }, 252 | { 253 | "id": 36, 254 | "name": "MoveToPos", 255 | "args": { 256 | "x": 0, 257 | "y": 0 258 | } 259 | } 260 | ] 261 | }, 262 | { 263 | "id": 37, 264 | "name": "Idle" 265 | } 266 | ] 267 | }, 268 | "export": true, 269 | "desc": "英雄测试AI" 270 | } -------------------------------------------------------------------------------- /example/monster.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monster", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "desc": "怪物测试AI", 7 | "args": {}, 8 | "children": [ 9 | { 10 | "id": 2, 11 | "name": "Selector", 12 | "args": {}, 13 | "children": [ 14 | { 15 | "id": 3, 16 | "name": "Sequence", 17 | "args": {}, 18 | "children": [ 19 | { 20 | "id": 4, 21 | "name": "GetHp", 22 | "args": {}, 23 | "output": [ 24 | "hp" 25 | ] 26 | }, 27 | { 28 | "id": 5, 29 | "name": "Check", 30 | "args": { 31 | "value": "hp > 50" 32 | } 33 | }, 34 | { 35 | "id": 6, 36 | "name": "Log", 37 | "desc": "攻击", 38 | "args": { 39 | "message": "Attack!", 40 | "level": "info" 41 | } 42 | }, 43 | { 44 | "id": 7, 45 | "name": "Wait", 46 | "args": { 47 | "time": 5 48 | } 49 | } 50 | ] 51 | }, 52 | { 53 | "id": 8, 54 | "name": "Log", 55 | "desc": "逃跑", 56 | "args": { 57 | "message": "Run!", 58 | "level": "info" 59 | } 60 | } 61 | ] 62 | }, 63 | { 64 | "id": 9, 65 | "name": "Log", 66 | "desc": "test", 67 | "args": { 68 | "message": "if true", 69 | "level": "info" 70 | } 71 | } 72 | ] 73 | }, 74 | "export": true, 75 | "desc": "怪物测试AI" 76 | } -------------------------------------------------------------------------------- /example/test-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-array", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Attack", 10 | "args": { 11 | "distance": 1331, 12 | "range": 1, 13 | "enemy": [ 14 | "go1" 15 | ], 16 | "multi": [ 17 | false, 18 | true 19 | ], 20 | "data": [ 21 | { 22 | "a": 1, 23 | "b": [ 24 | "a", 25 | "b" 26 | ] 27 | } 28 | ], 29 | "velocity": [], 30 | "target": [ 31 | "enemy", 32 | "self" 33 | ] 34 | }, 35 | "input": [ 36 | "ddd", 37 | "skill1", 38 | "skill2" 39 | ], 40 | "output": [ 41 | "d", 42 | "damage1", 43 | "damage2" 44 | ] 45 | }, 46 | { 47 | "id": 3, 48 | "name": "Attack", 49 | "args": { 50 | "range": 1, 51 | "enemy": [], 52 | "multi": [], 53 | "data": [], 54 | "velocity": [], 55 | "target": [] 56 | }, 57 | "input": [ 58 | "hello", 59 | "skill2", 60 | "" 61 | ], 62 | "output": [ 63 | "atkout" 64 | ] 65 | } 66 | ] 67 | }, 68 | "export": true, 69 | "desc": "" 70 | } -------------------------------------------------------------------------------- /example/test-listen.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-listen", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Log", 10 | "args": { 11 | "message": "-------------> Listen", 12 | "level": "info" 13 | } 14 | }, 15 | { 16 | "id": 3, 17 | "name": "AlwaysSuccess", 18 | "children": [ 19 | { 20 | "id": 4, 21 | "name": "Once", 22 | "children": [ 23 | { 24 | "id": 5, 25 | "name": "Sequence", 26 | "children": [ 27 | { 28 | "id": 6, 29 | "name": "Listen", 30 | "args": { 31 | "event": "hello" 32 | }, 33 | "input": [ 34 | "" 35 | ], 36 | "output": [ 37 | "args" 38 | ], 39 | "children": [ 40 | { 41 | "id": 7, 42 | "name": "Log", 43 | "args": { 44 | "message": "hello!!!", 45 | "level": "info" 46 | } 47 | } 48 | ] 49 | }, 50 | { 51 | "id": 8, 52 | "name": "Listen", 53 | "args": { 54 | "event": "testOff" 55 | }, 56 | "input": [ 57 | "" 58 | ], 59 | "output": [ 60 | "" 61 | ], 62 | "children": [ 63 | { 64 | "id": 9, 65 | "name": "Log", 66 | "args": { 67 | "message": "off test!", 68 | "level": "info" 69 | } 70 | } 71 | ] 72 | }, 73 | { 74 | "id": 10, 75 | "name": "Listen", 76 | "args": { 77 | "event": "treeAfterTicked" 78 | }, 79 | "input": [ 80 | "" 81 | ], 82 | "output": [ 83 | "", 84 | "" 85 | ], 86 | "children": [ 87 | { 88 | "id": 11, 89 | "name": "Log", 90 | "args": { 91 | "message": "after run", 92 | "level": "info" 93 | } 94 | } 95 | ] 96 | } 97 | ] 98 | } 99 | ] 100 | } 101 | ] 102 | }, 103 | { 104 | "id": 12, 105 | "name": "Log", 106 | "args": { 107 | "message": "<------------- Listen", 108 | "level": "info" 109 | } 110 | } 111 | ] 112 | }, 113 | "export": true, 114 | "desc": "" 115 | } -------------------------------------------------------------------------------- /example/test-once.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-once", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Log", 10 | "args": { 11 | "message": "-------------> Once", 12 | "level": "info" 13 | } 14 | }, 15 | { 16 | "id": 3, 17 | "name": "AlwaysSuccess", 18 | "children": [ 19 | { 20 | "id": 4, 21 | "name": "Once", 22 | "debug": true, 23 | "children": [ 24 | { 25 | "id": 5, 26 | "name": "RetryUntilSuccess", 27 | "args": { 28 | "count": 2 29 | }, 30 | "input": [ 31 | "" 32 | ], 33 | "children": [ 34 | { 35 | "id": 6, 36 | "name": "AlwaysFailure", 37 | "children": [ 38 | { 39 | "id": 7, 40 | "name": "Log", 41 | "args": { 42 | "message": "log once1", 43 | "level": "info" 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | }, 54 | { 55 | "id": 8, 56 | "name": "AlwaysSuccess", 57 | "children": [ 58 | { 59 | "id": 9, 60 | "name": "Once", 61 | "debug": true, 62 | "children": [ 63 | { 64 | "id": 10, 65 | "name": "Log", 66 | "args": { 67 | "message": "log once2", 68 | "level": "info" 69 | } 70 | } 71 | ] 72 | } 73 | ] 74 | }, 75 | { 76 | "id": 11, 77 | "name": "AlwaysSuccess", 78 | "children": [ 79 | { 80 | "id": 12, 81 | "name": "Once", 82 | "debug": true, 83 | "children": [ 84 | { 85 | "id": 13, 86 | "name": "Sequence", 87 | "children": [ 88 | { 89 | "id": 14, 90 | "name": "Log", 91 | "args": { 92 | "message": "log once3", 93 | "level": "info" 94 | } 95 | }, 96 | { 97 | "id": 15, 98 | "name": "Wait", 99 | "args": { 100 | "time": 1 101 | }, 102 | "input": [ 103 | "" 104 | ] 105 | }, 106 | { 107 | "id": 16, 108 | "name": "Log", 109 | "args": { 110 | "message": "log once4", 111 | "level": "info" 112 | } 113 | } 114 | ] 115 | } 116 | ] 117 | } 118 | ] 119 | }, 120 | { 121 | "id": 17, 122 | "name": "Log", 123 | "args": { 124 | "message": "<------------- Once", 125 | "level": "info" 126 | } 127 | } 128 | ] 129 | }, 130 | "export": true, 131 | "desc": "" 132 | } -------------------------------------------------------------------------------- /example/test-parallel.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-parallel", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Log", 10 | "args": { 11 | "message": "-------------> Parallel", 12 | "level": "info" 13 | } 14 | }, 15 | { 16 | "id": 3, 17 | "name": "Parallel", 18 | "children": [ 19 | { 20 | "id": 4, 21 | "name": "Sequence", 22 | "children": [ 23 | { 24 | "id": 5, 25 | "name": "Wait", 26 | "args": { 27 | "time": 1 28 | } 29 | }, 30 | { 31 | "id": 6, 32 | "name": "Log", 33 | "args": { 34 | "message": "wait 1s", 35 | "level": "info" 36 | } 37 | }, 38 | { 39 | "id": 7, 40 | "name": "Let", 41 | "args": { 42 | "value": true 43 | }, 44 | "input": [""], 45 | "output": ["wait1"] 46 | } 47 | ] 48 | }, 49 | { 50 | "id": 8, 51 | "name": "Log", 52 | "args": { 53 | "message": "run parallel!!!", 54 | "level": "info" 55 | } 56 | }, 57 | { 58 | "id": 9, 59 | "name": "Sequence", 60 | "children": [ 61 | { 62 | "id": 10, 63 | "name": "Sequence", 64 | "children": [ 65 | { 66 | "id": 11, 67 | "name": "Wait", 68 | "args": { 69 | "time": 2 70 | } 71 | }, 72 | { 73 | "id": 12, 74 | "name": "Log", 75 | "args": { 76 | "message": "wait 2s", 77 | "level": "info" 78 | } 79 | }, 80 | { 81 | "id": 13, 82 | "name": "Let", 83 | "args": { 84 | "value": true 85 | }, 86 | "input": [""], 87 | "output": ["wait2"] 88 | } 89 | ] 90 | }, 91 | { 92 | "id": 14, 93 | "name": "Log", 94 | "args": { 95 | "message": "wait 2s complete!!!", 96 | "level": "info" 97 | } 98 | } 99 | ] 100 | }, 101 | { 102 | "id": 15, 103 | "name": "Sequence", 104 | "children": [ 105 | { 106 | "id": 16, 107 | "name": "Sequence", 108 | "children": [ 109 | { 110 | "id": 17, 111 | "name": "Wait", 112 | "args": { 113 | "time": 1 114 | }, 115 | "input": [""] 116 | } 117 | ] 118 | }, 119 | { 120 | "id": 18, 121 | "name": "Sequence", 122 | "children": [ 123 | { 124 | "id": 19, 125 | "name": "Sequence", 126 | "children": [ 127 | { 128 | "id": 20, 129 | "name": "Log", 130 | "args": { 131 | "message": "wait going", 132 | "level": "info" 133 | }, 134 | "input": [""] 135 | } 136 | ] 137 | }, 138 | { 139 | "id": 21, 140 | "name": "Sequence", 141 | "children": [ 142 | { 143 | "id": 22, 144 | "name": "Wait", 145 | "args": { 146 | "time": 1 147 | }, 148 | "input": [""] 149 | }, 150 | { 151 | "id": 23, 152 | "name": "Log", 153 | "args": { 154 | "message": "complete going", 155 | "level": "info" 156 | }, 157 | "input": [""] 158 | }, 159 | { 160 | "id": 24, 161 | "name": "Let", 162 | "args": { 163 | "value": true 164 | }, 165 | "input": [""], 166 | "output": ["wait_going"] 167 | } 168 | ] 169 | } 170 | ] 171 | } 172 | ] 173 | } 174 | ] 175 | }, 176 | { 177 | "id": 25, 178 | "name": "Assert", 179 | "args": { 180 | "message": "wait1 not set" 181 | }, 182 | "children": [ 183 | { 184 | "id": 26, 185 | "name": "NotNull", 186 | "input": ["wait1"] 187 | } 188 | ] 189 | }, 190 | { 191 | "id": 27, 192 | "name": "Assert", 193 | "args": { 194 | "message": "wait2 not set" 195 | }, 196 | "children": [ 197 | { 198 | "id": 28, 199 | "name": "NotNull", 200 | "input": ["wait2"] 201 | } 202 | ] 203 | }, 204 | { 205 | "id": 29, 206 | "name": "Assert", 207 | "args": { 208 | "message": "wait_going not set" 209 | }, 210 | "children": [ 211 | { 212 | "id": 30, 213 | "name": "NotNull", 214 | "input": ["wait_going"] 215 | } 216 | ] 217 | }, 218 | { 219 | "id": 31, 220 | "name": "Log", 221 | "args": { 222 | "message": "<------------- Parallel", 223 | "level": "info" 224 | } 225 | } 226 | ] 227 | }, 228 | "export": true, 229 | "desc": "" 230 | } 231 | -------------------------------------------------------------------------------- /example/test-race.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-race", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Sequence", 10 | "children": [ 11 | { 12 | "id": 3, 13 | "name": "AlwaysSuccess", 14 | "children": [ 15 | { 16 | "id": 4, 17 | "name": "Race", 18 | "children": [ 19 | { 20 | "id": 5, 21 | "name": "Sequence", 22 | "children": [ 23 | { 24 | "id": 6, 25 | "name": "Wait", 26 | "args": { 27 | "time": 2 28 | }, 29 | "input": [ 30 | "" 31 | ] 32 | }, 33 | { 34 | "id": 7, 35 | "name": "Let", 36 | "args": { 37 | "value": 1 38 | }, 39 | "input": [ 40 | "" 41 | ], 42 | "output": [ 43 | "value" 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "id": 8, 50 | "name": "Sequence", 51 | "children": [ 52 | { 53 | "id": 9, 54 | "name": "Wait", 55 | "args": { 56 | "time": 3 57 | }, 58 | "input": [ 59 | "" 60 | ] 61 | }, 62 | { 63 | "id": 10, 64 | "name": "Let", 65 | "args": { 66 | "value": 2 67 | }, 68 | "input": [ 69 | "" 70 | ], 71 | "output": [ 72 | "value" 73 | ] 74 | }, 75 | { 76 | "id": 11, 77 | "name": "Log", 78 | "args": { 79 | "message": "hello race success", 80 | "level": "info" 81 | }, 82 | "input": [ 83 | "" 84 | ] 85 | } 86 | ] 87 | }, 88 | { 89 | "id": 12, 90 | "name": "AlwaysFailure", 91 | "children": [ 92 | { 93 | "id": 13, 94 | "name": "Log", 95 | "args": { 96 | "message": "hello race failure", 97 | "level": "info" 98 | }, 99 | "input": [ 100 | "" 101 | ] 102 | } 103 | ] 104 | } 105 | ] 106 | } 107 | ] 108 | }, 109 | { 110 | "id": 14, 111 | "name": "Assert", 112 | "args": { 113 | "message": "race error" 114 | }, 115 | "children": [ 116 | { 117 | "id": 15, 118 | "name": "Check", 119 | "args": { 120 | "value": "value == 1" 121 | } 122 | } 123 | ] 124 | } 125 | ] 126 | }, 127 | { 128 | "id": 16, 129 | "name": "Sequence", 130 | "children": [ 131 | { 132 | "id": 17, 133 | "name": "AlwaysSuccess", 134 | "children": [ 135 | { 136 | "id": 18, 137 | "name": "Race", 138 | "children": [ 139 | { 140 | "id": 19, 141 | "name": "Sequence", 142 | "children": [ 143 | { 144 | "id": 20, 145 | "name": "Wait", 146 | "args": { 147 | "time": 2 148 | }, 149 | "input": [ 150 | "" 151 | ] 152 | }, 153 | { 154 | "id": 21, 155 | "name": "Let", 156 | "args": { 157 | "value": 1 158 | }, 159 | "input": [ 160 | "" 161 | ], 162 | "output": [ 163 | "value" 164 | ] 165 | } 166 | ] 167 | }, 168 | { 169 | "id": 22, 170 | "name": "Sequence", 171 | "children": [ 172 | { 173 | "id": 23, 174 | "name": "Wait", 175 | "args": { 176 | "time": 1 177 | }, 178 | "input": [ 179 | "" 180 | ] 181 | }, 182 | { 183 | "id": 24, 184 | "name": "Let", 185 | "args": { 186 | "value": 2 187 | }, 188 | "input": [ 189 | "" 190 | ], 191 | "output": [ 192 | "value" 193 | ] 194 | }, 195 | { 196 | "id": 25, 197 | "name": "Log", 198 | "args": { 199 | "message": "hello race success", 200 | "level": "info" 201 | }, 202 | "input": [ 203 | "" 204 | ] 205 | } 206 | ] 207 | }, 208 | { 209 | "id": 26, 210 | "name": "AlwaysFailure", 211 | "children": [ 212 | { 213 | "id": 27, 214 | "name": "Log", 215 | "args": { 216 | "message": "hello race failure", 217 | "level": "info" 218 | }, 219 | "input": [ 220 | "" 221 | ] 222 | } 223 | ] 224 | } 225 | ] 226 | } 227 | ] 228 | }, 229 | { 230 | "id": 28, 231 | "name": "Assert", 232 | "args": { 233 | "message": "race error" 234 | }, 235 | "children": [ 236 | { 237 | "id": 29, 238 | "name": "Check", 239 | "args": { 240 | "value": "value == 2" 241 | } 242 | } 243 | ] 244 | } 245 | ] 246 | } 247 | ] 248 | }, 249 | "export": true, 250 | "desc": "" 251 | } -------------------------------------------------------------------------------- /example/test-repeat-until-failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-repeat-until-failure", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Log", 10 | "args": { 11 | "message": "-------------> RetryUntilFailure", 12 | "level": "info" 13 | } 14 | }, 15 | { 16 | "id": 3, 17 | "name": "AlwaysSuccess", 18 | "children": [ 19 | { 20 | "id": 4, 21 | "name": "RetryUntilFailure", 22 | "args": { 23 | "count": 3 24 | }, 25 | "input": [ 26 | "" 27 | ], 28 | "debug": true, 29 | "children": [ 30 | { 31 | "id": 5, 32 | "name": "AlwaysSuccess", 33 | "children": [ 34 | { 35 | "id": 6, 36 | "name": "Log", 37 | "args": { 38 | "message": "[1] run repeat until failure!!!", 39 | "level": "info" 40 | } 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "id": 7, 50 | "name": "AlwaysSuccess", 51 | "children": [ 52 | { 53 | "id": 8, 54 | "name": "Sequence", 55 | "debug": true, 56 | "children": [ 57 | { 58 | "id": 9, 59 | "name": "RetryUntilFailure", 60 | "args": { 61 | "count": 3 62 | }, 63 | "input": [ 64 | "" 65 | ], 66 | "children": [ 67 | { 68 | "id": 10, 69 | "name": "Sequence", 70 | "children": [ 71 | { 72 | "id": 11, 73 | "name": "Sequence", 74 | "children": [ 75 | { 76 | "id": 12, 77 | "name": "Wait", 78 | "args": { 79 | "time": 1 80 | } 81 | }, 82 | { 83 | "id": 13, 84 | "name": "Log", 85 | "args": { 86 | "message": "wait 1s", 87 | "level": "info" 88 | } 89 | } 90 | ] 91 | }, 92 | { 93 | "id": 14, 94 | "name": "AlwaysSuccess", 95 | "children": [ 96 | { 97 | "id": 15, 98 | "name": "Log", 99 | "args": { 100 | "message": "[2] run repeat until failure!!!", 101 | "level": "info" 102 | } 103 | } 104 | ] 105 | } 106 | ] 107 | } 108 | ] 109 | }, 110 | { 111 | "id": 16, 112 | "name": "Log", 113 | "args": { 114 | "message": "DO NOT LOG THIS!!!", 115 | "level": "info" 116 | } 117 | } 118 | ] 119 | } 120 | ] 121 | }, 122 | { 123 | "id": 17, 124 | "name": "Log", 125 | "args": { 126 | "message": "<------------- RetryUntilFailure", 127 | "level": "info" 128 | } 129 | } 130 | ] 131 | }, 132 | "export": true, 133 | "desc": "" 134 | } -------------------------------------------------------------------------------- /example/test-repeat-until-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-repeat-until-success", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Log", 10 | "args": { 11 | "message": "-------------> RetryUntilSuccess", 12 | "level": "info" 13 | } 14 | }, 15 | { 16 | "id": 3, 17 | "name": "AlwaysSuccess", 18 | "children": [ 19 | { 20 | "id": 4, 21 | "name": "RetryUntilSuccess", 22 | "args": { 23 | "count": 3 24 | }, 25 | "input": [ 26 | "" 27 | ], 28 | "debug": true, 29 | "children": [ 30 | { 31 | "id": 5, 32 | "name": "AlwaysFailure", 33 | "children": [ 34 | { 35 | "id": 6, 36 | "name": "Log", 37 | "args": { 38 | "message": "[1] run repeat until success!!!", 39 | "level": "info" 40 | } 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "id": 7, 50 | "name": "AlwaysSuccess", 51 | "children": [ 52 | { 53 | "id": 8, 54 | "name": "Sequence", 55 | "children": [ 56 | { 57 | "id": 9, 58 | "name": "RetryUntilSuccess", 59 | "args": { 60 | "count": 3 61 | }, 62 | "input": [ 63 | "" 64 | ], 65 | "children": [ 66 | { 67 | "id": 10, 68 | "name": "Sequence", 69 | "children": [ 70 | { 71 | "id": 11, 72 | "name": "Sequence", 73 | "children": [ 74 | { 75 | "id": 12, 76 | "name": "Wait", 77 | "args": { 78 | "time": 1 79 | } 80 | }, 81 | { 82 | "id": 13, 83 | "name": "Log", 84 | "args": { 85 | "message": "wait 1s", 86 | "level": "info" 87 | } 88 | } 89 | ] 90 | }, 91 | { 92 | "id": 14, 93 | "name": "AlwaysFailure", 94 | "children": [ 95 | { 96 | "id": 15, 97 | "name": "Log", 98 | "args": { 99 | "message": "[2] run repeat until success!!!", 100 | "level": "info" 101 | } 102 | } 103 | ] 104 | } 105 | ] 106 | } 107 | ] 108 | }, 109 | { 110 | "id": 16, 111 | "name": "Log", 112 | "args": { 113 | "message": "DO NOT LOG THIS!!!", 114 | "level": "info" 115 | } 116 | } 117 | ] 118 | } 119 | ] 120 | }, 121 | { 122 | "id": 17, 123 | "name": "Log", 124 | "args": { 125 | "message": "<------------- RetryUntilSuccess", 126 | "level": "info" 127 | } 128 | } 129 | ] 130 | }, 131 | "export": true, 132 | "desc": "" 133 | } -------------------------------------------------------------------------------- /example/test-sequence.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-sequence", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Assert", 10 | "args": { 11 | "message": "sequence running" 12 | }, 13 | "children": [ 14 | { 15 | "id": 3, 16 | "name": "IsStatus", 17 | "args": { 18 | "status": "running" 19 | }, 20 | "children": [ 21 | { 22 | "id": 4, 23 | "name": "Sequence", 24 | "children": [ 25 | { 26 | "id": 5, 27 | "name": "Wait", 28 | "args": { 29 | "time": 2 30 | }, 31 | "input": [ 32 | "" 33 | ] 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | ] 40 | }, 41 | { 42 | "id": 6, 43 | "name": "Assert", 44 | "args": { 45 | "message": "sequence success" 46 | }, 47 | "children": [ 48 | { 49 | "id": 7, 50 | "name": "IsStatus", 51 | "args": { 52 | "status": "success" 53 | }, 54 | "children": [ 55 | { 56 | "id": 8, 57 | "name": "Sequence", 58 | "children": [ 59 | { 60 | "id": 9, 61 | "name": "Log", 62 | "args": { 63 | "message": "hello sequence success", 64 | "level": "info" 65 | } 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | ] 72 | }, 73 | { 74 | "id": 10, 75 | "name": "Assert", 76 | "args": { 77 | "message": "sequence failure" 78 | }, 79 | "children": [ 80 | { 81 | "id": 11, 82 | "name": "IsStatus", 83 | "args": { 84 | "status": "failure" 85 | }, 86 | "children": [ 87 | { 88 | "id": 12, 89 | "name": "Sequence", 90 | "children": [ 91 | { 92 | "id": 13, 93 | "name": "AlwaysFailure", 94 | "children": [ 95 | { 96 | "id": 14, 97 | "name": "Log", 98 | "args": { 99 | "message": "hello sequence failure", 100 | "level": "info" 101 | } 102 | } 103 | ] 104 | } 105 | ] 106 | } 107 | ] 108 | } 109 | ] 110 | } 111 | ] 112 | }, 113 | "export": true, 114 | "desc": "" 115 | } -------------------------------------------------------------------------------- /example/test-switch-case.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-switch-case", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Log", 10 | "args": { 11 | "message": "-------------> Switch", 12 | "level": "info" 13 | }, 14 | "input": [ 15 | "" 16 | ] 17 | }, 18 | { 19 | "id": 3, 20 | "name": "AlwaysSuccess", 21 | "children": [ 22 | { 23 | "id": 4, 24 | "name": "Switch", 25 | "children": [ 26 | { 27 | "id": 5, 28 | "name": "Case", 29 | "children": [ 30 | { 31 | "id": 6, 32 | "name": "AlwaysFailure", 33 | "children": [ 34 | { 35 | "id": 7, 36 | "name": "Log", 37 | "args": { 38 | "message": "1. case fail", 39 | "level": "info" 40 | }, 41 | "input": [ 42 | "" 43 | ] 44 | } 45 | ] 46 | }, 47 | { 48 | "id": 8, 49 | "name": "Log", 50 | "args": { 51 | "message": "1. none", 52 | "level": "info" 53 | }, 54 | "input": [ 55 | "" 56 | ] 57 | } 58 | ] 59 | }, 60 | { 61 | "id": 9, 62 | "name": "Case", 63 | "children": [ 64 | { 65 | "id": 10, 66 | "name": "RetryUntilSuccess", 67 | "args": {}, 68 | "input": [], 69 | "children": [ 70 | { 71 | "id": 11, 72 | "name": "Sequence", 73 | "children": [ 74 | { 75 | "id": 12, 76 | "name": "Wait", 77 | "args": { 78 | "time": 1 79 | }, 80 | "input": [ 81 | "" 82 | ] 83 | }, 84 | { 85 | "id": 13, 86 | "name": "Log", 87 | "args": { 88 | "message": "2. wait success", 89 | "level": "info" 90 | }, 91 | "input": [ 92 | "" 93 | ] 94 | } 95 | ] 96 | } 97 | ] 98 | }, 99 | { 100 | "id": 14, 101 | "name": "Log", 102 | "args": { 103 | "message": "2. case done", 104 | "level": "info" 105 | }, 106 | "input": [ 107 | "" 108 | ] 109 | } 110 | ] 111 | }, 112 | { 113 | "id": 15, 114 | "name": "Case", 115 | "children": [ 116 | { 117 | "id": 16, 118 | "name": "Log", 119 | "args": { 120 | "message": "3. case never", 121 | "level": "info" 122 | }, 123 | "input": [ 124 | "" 125 | ] 126 | }, 127 | { 128 | "id": 17, 129 | "name": "Assert", 130 | "args": { 131 | "message": "NO!!!" 132 | }, 133 | "children": [ 134 | { 135 | "id": 18, 136 | "name": "AlwaysFailure", 137 | "children": [ 138 | { 139 | "id": 19, 140 | "name": "Log", 141 | "args": { 142 | "message": "neve run", 143 | "level": "info" 144 | }, 145 | "input": [ 146 | "" 147 | ] 148 | } 149 | ] 150 | } 151 | ] 152 | } 153 | ] 154 | } 155 | ] 156 | } 157 | ] 158 | }, 159 | { 160 | "id": 20, 161 | "name": "AlwaysSuccess", 162 | "children": [ 163 | { 164 | "id": 21, 165 | "name": "Switch", 166 | "children": [ 167 | { 168 | "id": 22, 169 | "name": "Case", 170 | "children": [ 171 | { 172 | "id": 23, 173 | "name": "Log", 174 | "args": { 175 | "message": "4. case fail", 176 | "level": "info" 177 | }, 178 | "input": [ 179 | "" 180 | ] 181 | }, 182 | { 183 | "id": 24, 184 | "name": "Log", 185 | "args": { 186 | "message": "4. none", 187 | "level": "info" 188 | }, 189 | "input": [ 190 | "" 191 | ] 192 | } 193 | ] 194 | }, 195 | { 196 | "id": 25, 197 | "name": "Case", 198 | "children": [ 199 | { 200 | "id": 26, 201 | "name": "Log", 202 | "args": { 203 | "message": "5. wait success", 204 | "level": "info" 205 | }, 206 | "input": [ 207 | "" 208 | ] 209 | }, 210 | { 211 | "id": 27, 212 | "name": "Sequence", 213 | "children": [ 214 | { 215 | "id": 28, 216 | "name": "Wait", 217 | "args": { 218 | "time": 1 219 | }, 220 | "input": [ 221 | "" 222 | ] 223 | }, 224 | { 225 | "id": 29, 226 | "name": "Log", 227 | "args": { 228 | "message": "5. case done", 229 | "level": "info" 230 | }, 231 | "input": [ 232 | "" 233 | ] 234 | } 235 | ] 236 | } 237 | ] 238 | }, 239 | { 240 | "id": 30, 241 | "name": "Case", 242 | "children": [ 243 | { 244 | "id": 31, 245 | "name": "Log", 246 | "args": { 247 | "message": "6. case never", 248 | "level": "info" 249 | }, 250 | "input": [ 251 | "" 252 | ] 253 | }, 254 | { 255 | "id": 32, 256 | "name": "Assert", 257 | "args": { 258 | "message": "NO!!!" 259 | }, 260 | "children": [ 261 | { 262 | "id": 33, 263 | "name": "AlwaysFailure", 264 | "children": [ 265 | { 266 | "id": 34, 267 | "name": "Log", 268 | "args": { 269 | "message": "neve run", 270 | "level": "info" 271 | }, 272 | "input": [ 273 | "" 274 | ] 275 | } 276 | ] 277 | } 278 | ] 279 | } 280 | ] 281 | } 282 | ] 283 | } 284 | ] 285 | }, 286 | { 287 | "id": 35, 288 | "name": "AlwaysSuccess", 289 | "children": [ 290 | { 291 | "id": 36, 292 | "name": "Switch", 293 | "children": [ 294 | { 295 | "id": 37, 296 | "name": "Case", 297 | "children": [ 298 | { 299 | "id": 38, 300 | "name": "Log", 301 | "args": { 302 | "message": "7. success", 303 | "level": "info" 304 | }, 305 | "input": [ 306 | "" 307 | ] 308 | }, 309 | { 310 | "id": 39, 311 | "name": "Log", 312 | "args": { 313 | "message": "7. case done", 314 | "level": "info" 315 | }, 316 | "input": [ 317 | "" 318 | ] 319 | } 320 | ] 321 | }, 322 | { 323 | "id": 40, 324 | "name": "Case", 325 | "children": [ 326 | { 327 | "id": 41, 328 | "name": "Log", 329 | "args": { 330 | "message": "8. case never", 331 | "level": "info" 332 | }, 333 | "input": [ 334 | "" 335 | ] 336 | }, 337 | { 338 | "id": 42, 339 | "name": "Assert", 340 | "args": { 341 | "message": "NO!!!" 342 | }, 343 | "children": [ 344 | { 345 | "id": 43, 346 | "name": "AlwaysFailure", 347 | "children": [ 348 | { 349 | "id": 44, 350 | "name": "Log", 351 | "args": { 352 | "message": "neve run", 353 | "level": "info" 354 | }, 355 | "input": [ 356 | "" 357 | ] 358 | } 359 | ] 360 | } 361 | ] 362 | } 363 | ] 364 | } 365 | ] 366 | } 367 | ] 368 | }, 369 | { 370 | "id": 45, 371 | "name": "Log", 372 | "args": { 373 | "message": "<------------- Switch", 374 | "level": "info" 375 | }, 376 | "input": [ 377 | "" 378 | ] 379 | } 380 | ] 381 | }, 382 | "export": true, 383 | "desc": "" 384 | } -------------------------------------------------------------------------------- /example/test-timeout.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-timeout", 3 | "root": { 4 | "id": 1, 5 | "name": "Sequence", 6 | "children": [ 7 | { 8 | "id": 2, 9 | "name": "Log", 10 | "args": { 11 | "message": "-------------> Timeout", 12 | "level": "info" 13 | } 14 | }, 15 | { 16 | "id": 3, 17 | "name": "Now", 18 | "output": [ 19 | "start_time" 20 | ] 21 | }, 22 | { 23 | "id": 4, 24 | "name": "AlwaysSuccess", 25 | "children": [ 26 | { 27 | "id": 5, 28 | "name": "Timeout", 29 | "args": { 30 | "time": 3 31 | }, 32 | "debug": true, 33 | "children": [ 34 | { 35 | "id": 6, 36 | "name": "RetryUntilSuccess", 37 | "args": { 38 | "count": 10 39 | }, 40 | "input": [ 41 | "" 42 | ], 43 | "children": [ 44 | { 45 | "id": 7, 46 | "name": "AlwaysFailure", 47 | "children": [ 48 | { 49 | "id": 8, 50 | "name": "Log", 51 | "args": { 52 | "message": "log timeout", 53 | "level": "info" 54 | } 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | ] 63 | }, 64 | { 65 | "id": 9, 66 | "name": "Now", 67 | "output": [ 68 | "curr_time" 69 | ] 70 | }, 71 | { 72 | "id": 10, 73 | "name": "Assert", 74 | "args": { 75 | "message": "timeout error" 76 | }, 77 | "children": [ 78 | { 79 | "id": 11, 80 | "name": "Check", 81 | "args": { 82 | "value": "curr_time - start_time == 3" 83 | } 84 | } 85 | ] 86 | }, 87 | { 88 | "id": 12, 89 | "name": "AlwaysSuccess", 90 | "children": [ 91 | { 92 | "id": 13, 93 | "name": "Timeout", 94 | "args": { 95 | "time": 3 96 | }, 97 | "debug": true, 98 | "children": [ 99 | { 100 | "id": 14, 101 | "name": "RetryUntilSuccess", 102 | "args": { 103 | "count": 10 104 | }, 105 | "input": [ 106 | "" 107 | ], 108 | "children": [ 109 | { 110 | "id": 15, 111 | "name": "Sequence", 112 | "children": [ 113 | { 114 | "id": 16, 115 | "name": "Wait", 116 | "args": { 117 | "time": 1 118 | }, 119 | "input": [ 120 | "" 121 | ] 122 | }, 123 | { 124 | "id": 17, 125 | "name": "Log", 126 | "args": { 127 | "message": "log wait", 128 | "level": "info" 129 | } 130 | } 131 | ] 132 | } 133 | ] 134 | } 135 | ] 136 | } 137 | ] 138 | }, 139 | { 140 | "id": 18, 141 | "name": "AlwaysSuccess", 142 | "children": [ 143 | { 144 | "id": 19, 145 | "name": "Timeout", 146 | "args": { 147 | "time": 3 148 | }, 149 | "debug": true, 150 | "children": [ 151 | { 152 | "id": 20, 153 | "name": "Log", 154 | "args": { 155 | "message": "log success", 156 | "level": "info" 157 | } 158 | } 159 | ] 160 | } 161 | ] 162 | }, 163 | { 164 | "id": 21, 165 | "name": "AlwaysSuccess", 166 | "children": [ 167 | { 168 | "id": 22, 169 | "name": "Timeout", 170 | "args": { 171 | "time": 3 172 | }, 173 | "debug": true, 174 | "children": [ 175 | { 176 | "id": 23, 177 | "name": "AlwaysFailure", 178 | "children": [ 179 | { 180 | "id": 24, 181 | "name": "Log", 182 | "args": { 183 | "message": "log failure", 184 | "level": "info" 185 | } 186 | } 187 | ] 188 | } 189 | ] 190 | } 191 | ] 192 | }, 193 | { 194 | "id": 25, 195 | "name": "Log", 196 | "args": { 197 | "message": "<------------- Timeout", 198 | "level": "info" 199 | } 200 | } 201 | ] 202 | }, 203 | "export": true, 204 | "desc": "" 205 | } -------------------------------------------------------------------------------- /images/behavior3-editor-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongfq/behavior3-ts/2849ec4948e2ff19392c69c059f4db7faea3b1c4/images/behavior3-editor-running.png -------------------------------------------------------------------------------- /images/behavior3-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongfq/behavior3-ts/2849ec4948e2ff19392c69c059f4db7faea3b1c4/images/behavior3-editor.png -------------------------------------------------------------------------------- /src/behavior3/blackboard.ts: -------------------------------------------------------------------------------- 1 | import type { Context, ObjectType } from "./context"; 2 | import type { Node } from "./node"; 3 | import type { Tree } from "./tree"; 4 | 5 | const PREFIX_PRIVATE = "__PRIVATE_VAR"; 6 | const PREFIX_TEMP = "__TEMP_VAR"; 7 | 8 | export class Blackboard { 9 | protected _values: ObjectType = {}; 10 | protected _tree: Tree; 11 | 12 | constructor(tree: Tree) { 13 | this._tree = tree; 14 | } 15 | 16 | get values() { 17 | return this._values; 18 | } 19 | 20 | eval(code: string) { 21 | return this._tree.context.compileCode(code)(this._values); 22 | } 23 | 24 | get(k: string): T | undefined { 25 | if (k) { 26 | return this._values[k] as T | undefined; 27 | } else { 28 | return undefined; 29 | } 30 | } 31 | 32 | set(k: string, v: unknown) { 33 | if (k) { 34 | if (v === undefined || v === null) { 35 | delete this._values[k]; 36 | } else { 37 | this._values[k] = v; 38 | } 39 | } 40 | } 41 | 42 | clear() { 43 | this._values = {}; 44 | } 45 | 46 | static makePrivateVar(k: string): string; 47 | 48 | static makePrivateVar(node: Node, k: string): string; 49 | 50 | static makePrivateVar(node: Node | string, k?: string) { 51 | if (typeof node === "string") { 52 | return `${PREFIX_PRIVATE}_${node}`; 53 | } else { 54 | return `${PREFIX_PRIVATE}_NODE#${node.id}_${k}`; 55 | } 56 | } 57 | 58 | static isPrivateVar(k: string) { 59 | return k.startsWith(PREFIX_PRIVATE); 60 | } 61 | 62 | static makeTempVar(node: Node, k: string) { 63 | return `${PREFIX_TEMP}_NODE#${node.id}_${k}`; 64 | } 65 | 66 | static isTempVar(k: string) { 67 | return k.startsWith(PREFIX_TEMP); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/behavior3/context.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any ban-types 2 | import { Evaluator, ExpressionEvaluator } from "./evaluator"; 3 | import { Node, NodeData, NodeDef } from "./node"; 4 | import { Index } from "./nodes/actions"; 5 | import { Calculate } from "./nodes/actions/calculate"; 6 | import { Concat } from "./nodes/actions/concat"; 7 | import { GetField } from "./nodes/actions/get-field"; 8 | import { JustSuccess } from "./nodes/actions/just-success"; 9 | import { Let } from "./nodes/actions/let"; 10 | import { Log } from "./nodes/actions/log"; 11 | import { MathNode } from "./nodes/actions/math"; 12 | import { Now } from "./nodes/actions/now"; 13 | import { Push } from "./nodes/actions/push"; 14 | import { RandomIndex } from "./nodes/actions/random-index"; 15 | import { SetField } from "./nodes/actions/set-field"; 16 | import { Wait } from "./nodes/actions/wait"; 17 | import { WaitForEvent } from "./nodes/actions/wait-for-event"; 18 | import { IfElse } from "./nodes/composites/ifelse"; 19 | import { Parallel } from "./nodes/composites/parallel"; 20 | import { Race } from "./nodes/composites/race"; 21 | import { Selector } from "./nodes/composites/selector"; 22 | import { Sequence } from "./nodes/composites/sequence"; 23 | import { Case, Switch } from "./nodes/composites/switch"; 24 | import { Check } from "./nodes/conditions/check"; 25 | import { Includes } from "./nodes/conditions/includes"; 26 | import { IsNull } from "./nodes/conditions/is-null"; 27 | import { NotNull } from "./nodes/conditions/not-null"; 28 | import { AlwaysFailure } from "./nodes/decorators/always-failure"; 29 | import { AlwaysRunning } from "./nodes/decorators/always-running"; 30 | import { AlwaysSuccess } from "./nodes/decorators/always-success"; 31 | import { Assert } from "./nodes/decorators/assert"; 32 | import { Delay } from "./nodes/decorators/delay"; 33 | import { Filter } from "./nodes/decorators/filter"; 34 | import { Foreach } from "./nodes/decorators/foreach"; 35 | import { Invert } from "./nodes/decorators/invert"; 36 | import { Listen } from "./nodes/decorators/listen"; 37 | import { Once } from "./nodes/decorators/once"; 38 | import { Repeat } from "./nodes/decorators/repeat"; 39 | import { RetryUntilFailure } from "./nodes/decorators/retry-until-failure"; 40 | import { RetryUntilSuccess } from "./nodes/decorators/retry-until-success"; 41 | import { Timeout } from "./nodes/decorators/timeout"; 42 | import { TreeData } from "./tree"; 43 | 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | export type Constructor = new (...args: A) => T; 46 | export type NodeContructor = Constructor> & { 47 | descriptor: NodeDef; 48 | }; 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | export type Callback = (...args: A) => unknown; 51 | export type ObjectType = { [k: string]: unknown }; 52 | export type TargetType = object | string | number; 53 | export type TagType = unknown; 54 | 55 | export type DeepReadonly = T extends object 56 | ? { readonly [P in keyof T]: DeepReadonly } 57 | : T; 58 | 59 | type TimerEntry = { 60 | callback: Callback; 61 | tag: TagType; 62 | expired: number; 63 | }; 64 | 65 | export abstract class Context { 66 | readonly nodeDefs: Record> = {}; 67 | readonly nodeCtors: Record> = {}; 68 | readonly trees: Record = {}; 69 | 70 | protected _time: number = 0; 71 | 72 | private readonly _evaluators: Record = {}; 73 | private readonly _timers: TimerEntry[] = []; 74 | private readonly _listeners: Map>> = new Map(); 75 | 76 | constructor() { 77 | this.registerNode(AlwaysFailure); 78 | this.registerNode(AlwaysRunning); 79 | this.registerNode(AlwaysSuccess); 80 | this.registerNode(Assert); 81 | this.registerNode(Calculate); 82 | this.registerNode(Case); 83 | this.registerNode(Check); 84 | this.registerNode(Concat); 85 | this.registerNode(Delay); 86 | this.registerNode(Filter); 87 | this.registerNode(Foreach); 88 | this.registerNode(GetField); 89 | this.registerNode(IfElse); 90 | this.registerNode(Includes); 91 | this.registerNode(Index); 92 | this.registerNode(Invert); 93 | this.registerNode(IsNull); 94 | this.registerNode(JustSuccess); 95 | this.registerNode(Let); 96 | this.registerNode(Listen); 97 | this.registerNode(Log); 98 | this.registerNode(MathNode); 99 | this.registerNode(NotNull); 100 | this.registerNode(Now); 101 | this.registerNode(Once); 102 | this.registerNode(Parallel); 103 | this.registerNode(Push); 104 | this.registerNode(Race); 105 | this.registerNode(RandomIndex); 106 | this.registerNode(Repeat); 107 | this.registerNode(RetryUntilFailure); 108 | this.registerNode(RetryUntilSuccess); 109 | this.registerNode(Selector); 110 | this.registerNode(Sequence); 111 | this.registerNode(SetField); 112 | this.registerNode(Switch); 113 | this.registerNode(Timeout); 114 | this.registerNode(Wait); 115 | this.registerNode(WaitForEvent); 116 | } 117 | 118 | abstract loadTree(path: string): Promise; 119 | 120 | get time() { 121 | return this._time; 122 | } 123 | 124 | /** 125 | * Schedules a callback to be executed after a specified delay, same callback will be replaced. 126 | * 127 | * @param time The delay in seconds before the callback is executed 128 | * @param callback The function to call after the delay 129 | * @param tag The tag used to identify which timers to remove 130 | */ 131 | delay(time: number, callback: Callback, tag: TagType) { 132 | const expired = time + this._time; 133 | const timers = this._timers; 134 | 135 | let idx = timers.findIndex((v) => v.callback === callback); 136 | if (idx >= 0) { 137 | timers.splice(idx, 1); 138 | } 139 | 140 | idx = timers.findIndex((v) => v.expired > expired); 141 | if (idx >= 0) { 142 | timers.splice(idx, 0, { callback, tag, expired }); 143 | } else { 144 | timers.push({ callback, tag, expired }); 145 | } 146 | } 147 | 148 | update(dt: number): void { 149 | this._time += dt; 150 | 151 | const timers = this._timers; 152 | while (timers.length > 0) { 153 | if (timers[0].expired <= this._time) { 154 | const { callback } = timers.shift()!; 155 | callback(); 156 | } else { 157 | break; 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * Registers a listener for an event. 164 | * 165 | * @param event The event name to listen for 166 | * @param callback The function to call when the event occurs 167 | * @param tag The tag used to identify which listeners to remove 168 | */ 169 | on(event: string, callback: Callback, tag: TagType): void; 170 | 171 | /** 172 | * Registers a listener for an event on a specific target. 173 | * 174 | * @param event The event name to listen for 175 | * @param target The target object to listen for the event on 176 | * @param callback The function to call when the event occurs 177 | * @param tag The tag used to identify which listeners to remove 178 | */ 179 | on(event: string, target: TargetType, callback: Callback, tag: TagType): void; 180 | 181 | on( 182 | event: string, 183 | callbackOrTarget: TargetType | Callback, 184 | tagOrCallback: Callback, 185 | tag?: TagType 186 | ) { 187 | let target: TargetType; 188 | let callback: Callback; 189 | if (typeof callbackOrTarget === "function") { 190 | callback = callbackOrTarget as Callback; 191 | tag = tagOrCallback; 192 | target = this as TargetType; 193 | } else { 194 | target = callbackOrTarget as TargetType; 195 | callback = tagOrCallback as Callback; 196 | } 197 | 198 | let listeners = this._listeners.get(event); 199 | if (!listeners) { 200 | listeners = new Map(); 201 | this._listeners.set(event, listeners); 202 | } 203 | let targetListeners = listeners.get(target); 204 | if (!targetListeners) { 205 | targetListeners = new Map(); 206 | listeners.set(target, targetListeners); 207 | } 208 | targetListeners.set(callback, tag); 209 | } 210 | 211 | /** 212 | * Dispatches an event to all listeners registered for the specified event. 213 | * If a target is provided, only listeners registered for that target will be notified. 214 | * Otherwise, listeners registered for the context(default target) will be notified. 215 | * 216 | * @param event The event name to dispatch 217 | * @param target Optional target object that the event is associated with 218 | * @param args Additional arguments to pass to the event listeners 219 | */ 220 | dispatch(event: string, target?: TargetType | this, ...args: unknown[]) { 221 | this._listeners 222 | .get(event) 223 | ?.get(target ?? this) 224 | ?.forEach((_, callback) => { 225 | callback(...args); 226 | }); 227 | } 228 | 229 | /** 230 | * Removes all listeners for the specified event that match the given tag. 231 | * 232 | * @param event The event name to remove listeners from 233 | * @param tag The tag used to identify which listeners to remove 234 | */ 235 | off(event: string, tag: TagType) { 236 | this._listeners.get(event)?.forEach((targetListeners, target, listeners) => { 237 | targetListeners.forEach((value, key) => { 238 | if (value === tag) { 239 | targetListeners.delete(key); 240 | } 241 | }); 242 | if (targetListeners.size === 0) { 243 | listeners.delete(target); 244 | } 245 | }); 246 | } 247 | 248 | /** 249 | * Removes all listeners for the specified tag from the context. 250 | * This includes both event listeners and timers. 251 | * 252 | * @param tag The tag used to identify which listeners to remove 253 | */ 254 | offAll(tag: TagType) { 255 | this._listeners.forEach((listeners) => { 256 | listeners.forEach((targetListeners, target) => { 257 | targetListeners.forEach((value, key) => { 258 | if (value === tag) { 259 | targetListeners.delete(key); 260 | } 261 | }); 262 | if (targetListeners.size === 0) { 263 | listeners.delete(target); 264 | } 265 | }); 266 | }); 267 | 268 | const timers = this._timers; 269 | for (let i = timers.length - 1; i >= 0; i--) { 270 | if (timers[i].tag === tag) { 271 | timers.splice(i, 1); 272 | } 273 | } 274 | } 275 | 276 | compileCode(code: string) { 277 | let evaluator = this._evaluators[code]; 278 | if (!evaluator) { 279 | const expr = new ExpressionEvaluator(code); 280 | if (!expr.dryRun()) { 281 | throw new Error(`invalid expression: ${code}`); 282 | } 283 | evaluator = (envars: ObjectType) => expr.evaluate(envars); 284 | this._evaluators[code] = evaluator; 285 | } 286 | return evaluator; 287 | } 288 | 289 | registerCode(code: string, evaluator: Evaluator) { 290 | this._evaluators[code] = evaluator; 291 | } 292 | 293 | registerNode(cls: NodeContructor) { 294 | const descriptor = cls.descriptor; 295 | if (descriptor.doc) { 296 | let doc = descriptor.doc.replace(/^[\r\n]+/, ""); 297 | const leadingSpace = doc.match(/^ */)?.[0]; 298 | if (leadingSpace) { 299 | doc = doc 300 | .substring(leadingSpace.length) 301 | .replace(new RegExp(`[\r\n]${leadingSpace}`, "g"), "\n") 302 | .replace(/ +$/, ""); 303 | } 304 | descriptor.doc = doc; 305 | } 306 | this.nodeDefs[descriptor.name] = descriptor; 307 | this.nodeCtors[descriptor.name] = cls; 308 | } 309 | 310 | protected _createTree(treeCfg: TreeData) { 311 | const traverse = (cfg: NodeData) => { 312 | cfg.tree = treeCfg; 313 | cfg.input ||= []; 314 | cfg.output ||= []; 315 | cfg.children ||= []; 316 | cfg.args ||= {}; 317 | cfg.children.forEach(traverse); 318 | }; 319 | traverse(treeCfg.root); 320 | 321 | return Node.create(this, treeCfg.root); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/behavior3/evaluator.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import type { ObjectType } from "./context"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export type Evaluator = (envars: any) => unknown; 6 | 7 | // prettier-ignore 8 | enum TokenType { 9 | NUMBER = 0, // number 10 | STRING = 1, // string 11 | BOOLEAN = 2, // boolean 12 | NEGATION = 3, // -N 13 | POSITIVE = 4, // +N 14 | DOT = 5, // . 15 | NOT = 6, // ! 16 | BNOT = 7, // ~ 17 | GT = 8, // > 18 | GE = 9, // >= 19 | EQ = 10, // == 20 | NEQ = 11, // != 21 | LT = 12, // < 22 | LE = 13, // <= 23 | ADD = 14, // + 24 | SUB = 15, // - 25 | MUL = 16, // * 26 | DIV = 17, // / 27 | MOD = 18, // % 28 | QUESTION = 19, // ? 29 | COLON = 20, // : 30 | AND = 21, // && 31 | OR = 22, // || 32 | BAND = 23, // & 33 | BOR = 24, // | 34 | BXOR = 25, // ^ 35 | SHL = 26, // << 36 | SHR = 27, // >> 37 | SHRU = 28, // >>> 38 | SQUARE_BRACKET = 29, // [] 39 | PARENTHESIS = 30, // () 40 | } 41 | 42 | type Token = { 43 | type: TokenType; 44 | precedence: number; 45 | value?: string | number | boolean | null; 46 | }; 47 | 48 | const OP_REGEX = /^(<<|>>>|>>|>=|<=|==|!=|>|<|&&|&|\|\||[-+*%!?/:.|^()[\]])/; 49 | const NUMBER_REGEX = /^(\d+\.\d+|\d+)/; 50 | const WORD_REGEX = /^(\w+)/; 51 | 52 | export class ExpressionEvaluator { 53 | private _postfix: Token[]; 54 | private _args: ObjectType | null = null; 55 | private _expr: string; 56 | 57 | constructor(expression: string) { 58 | this._expr = expression; 59 | this._postfix = this._convertToPostfix(this._parse(this._expr)); 60 | } 61 | 62 | private _parse(expr: string) { 63 | const tokens: string[] = []; 64 | while (expr.length) { 65 | const char = expr[0]; 66 | let token: RegExpMatchArray | null = null; 67 | if (/^\d/.test(char)) { 68 | token = expr.match(NUMBER_REGEX); 69 | } else if (/^\w/.test(char)) { 70 | token = expr.match(WORD_REGEX); 71 | } else if (/^[-+*%/()<>=?&|:^.![\]]/.test(char)) { 72 | token = expr.match(OP_REGEX); 73 | } 74 | if (!token) { 75 | throw new Error(`invalid expression: '${expr}' in '${this._expr}'`); 76 | } 77 | tokens.push(token[1]); 78 | expr = expr.slice(token[1].length).replace(/^\s+/, ""); 79 | } 80 | return tokens; 81 | } 82 | 83 | evaluate(args: ObjectType): unknown { 84 | const stack: unknown[] = []; 85 | 86 | this._args = args; 87 | for (const token of this._postfix) { 88 | const type = token.type; 89 | if ( 90 | type === TokenType.NUMBER || 91 | type === TokenType.BOOLEAN || 92 | type === TokenType.STRING 93 | ) { 94 | stack.push(token.value); 95 | } else if (type === TokenType.QUESTION) { 96 | const condition = stack.pop()!; 97 | const trueValue = stack.pop()!; 98 | const falseValue = stack.pop()!; 99 | stack.push(this._toValue(condition, false) ? trueValue : falseValue); 100 | } else if (type === TokenType.POSITIVE) { 101 | stack.push(this._toValue(stack.pop()!, false)); 102 | } else if (type === TokenType.NEGATION) { 103 | stack.push(-this._toValue(stack.pop()!, false)); 104 | } else if (type === TokenType.NOT) { 105 | stack.push(!this._toValue(stack.pop()!, false)); 106 | } else if (type === TokenType.BNOT) { 107 | stack.push(~this._toValue(stack.pop()!, false)); 108 | } else { 109 | const b = stack.pop()!; 110 | const a = stack.pop()!; 111 | switch (type) { 112 | case TokenType.DOT: { 113 | const obj = this._toObject(a); 114 | stack.push(this._toValue(obj[b as string])); 115 | break; 116 | } 117 | case TokenType.SQUARE_BRACKET: { 118 | const obj = this._toObject(a); 119 | const index = this._toValue(b); 120 | stack.push(this._toObject(obj[index])); 121 | break; 122 | } 123 | case TokenType.GT: 124 | stack.push(this._toValue(a) > this._toValue(b)); 125 | break; 126 | case TokenType.GE: 127 | stack.push(this._toValue(a) >= this._toValue(b)); 128 | break; 129 | case TokenType.EQ: 130 | stack.push( 131 | this._toValue(a, false) === this._toValue(b, false) 132 | ); 133 | break; 134 | case TokenType.NEQ: 135 | stack.push( 136 | this._toValue(a, false) !== this._toValue(b, false) 137 | ); 138 | break; 139 | case TokenType.LT: 140 | stack.push(this._toValue(a) < this._toValue(b)); 141 | break; 142 | case TokenType.LE: 143 | stack.push(this._toValue(a) <= this._toValue(b)); 144 | break; 145 | case TokenType.ADD: 146 | stack.push(this._toValue(a) + this._toValue(b)); 147 | break; 148 | case TokenType.SUB: 149 | stack.push(this._toValue(a) - this._toValue(b)); 150 | break; 151 | case TokenType.MUL: 152 | stack.push(this._toValue(a) * this._toValue(b)); 153 | break; 154 | case TokenType.DIV: 155 | stack.push(this._toValue(a) / this._toValue(b)); 156 | break; 157 | case TokenType.MOD: 158 | stack.push(this._toValue(a) % this._toValue(b)); 159 | break; 160 | case TokenType.COLON: { 161 | stack.push(this._toValue(a)); 162 | stack.push(this._toValue(b)); 163 | break; 164 | } 165 | case TokenType.SHL: 166 | stack.push(this._toValue(a) << this._toValue(b)); 167 | break; 168 | case TokenType.SHR: 169 | stack.push(this._toValue(a) >> this._toValue(b)); 170 | break; 171 | case TokenType.SHRU: 172 | stack.push(this._toValue(a) >>> this._toValue(b)); 173 | break; 174 | case TokenType.BAND: 175 | stack.push(this._toValue(a) & this._toValue(b)); 176 | break; 177 | case TokenType.BOR: 178 | stack.push(this._toValue(a) | this._toValue(b)); 179 | break; 180 | case TokenType.BXOR: 181 | stack.push(this._toValue(a) ^ this._toValue(b)); 182 | break; 183 | case TokenType.AND: 184 | stack.push(this._toValue(a) && this._toValue(b)); 185 | break; 186 | case TokenType.OR: 187 | stack.push(this._toValue(a) || this._toValue(b)); 188 | break; 189 | default: 190 | throw new Error(`unsupport operator: ${token.value}`); 191 | } 192 | } 193 | } 194 | 195 | this._args = null; 196 | 197 | return stack.pop(); 198 | } 199 | 200 | private _toObject(token: unknown) { 201 | if (typeof token === "string") { 202 | const obj = this._args?.[token]; 203 | if (typeof obj === "object") { 204 | return obj as ObjectType; 205 | } else { 206 | throw new Error(`value indexed by '${token}' is not a object`); 207 | } 208 | } else { 209 | return token as ObjectType; 210 | } 211 | } 212 | 213 | private _toValue(token: unknown, isNumber: boolean = true): T { 214 | const type = typeof token; 215 | if (type === "number" || type === "boolean" || token === null) { 216 | return token as T; 217 | } else if (typeof token === "string") { 218 | const value = this._args?.[token]; 219 | if (value === undefined) { 220 | throw new Error(`value indexed by '${token}' is not found`); 221 | } else if (isNumber && typeof value !== "number") { 222 | throw new Error(`value indexed by '${token}' is not a number'`); 223 | } 224 | return value as T; 225 | } else { 226 | throw new Error(`token '${token}' type not support!`); 227 | } 228 | } 229 | 230 | private _makeToken(symbol: string, last: string | undefined): Token { 231 | if (symbol === "-" || symbol === "+") { 232 | if (last === undefined || /^[-+*%<>=!&~^?:(|[]+$/.test(last)) { 233 | return { 234 | type: symbol === "-" ? TokenType.NEGATION : TokenType.POSITIVE, 235 | precedence: 15, 236 | value: `${symbol}N`, 237 | }; 238 | } 239 | } 240 | if (NUMBER_REGEX.test(symbol)) { 241 | return { type: TokenType.NUMBER, precedence: 0, value: parseFloat(symbol) }; 242 | } 243 | if (symbol === "true" || symbol === "false") { 244 | return { type: TokenType.BOOLEAN, precedence: 0, value: symbol === "true" }; 245 | } 246 | if (WORD_REGEX.test(symbol)) { 247 | return { type: TokenType.STRING, precedence: 0, value: symbol }; 248 | } 249 | if (symbol === ".") { 250 | return { type: TokenType.DOT, precedence: 18, value: symbol }; 251 | } 252 | if (symbol === "(" || symbol === ")") { 253 | // parenthesis is not a operator 254 | return { type: TokenType.PARENTHESIS, precedence: 0, value: symbol }; 255 | } 256 | if (symbol === "[" || symbol === "]") { 257 | return { type: TokenType.SQUARE_BRACKET, precedence: 18, value: symbol }; 258 | } 259 | if (symbol === "!") { 260 | return { type: TokenType.NOT, precedence: 15, value: symbol }; 261 | } 262 | if (symbol === "~") { 263 | return { type: TokenType.BNOT, precedence: 15, value: symbol }; 264 | } 265 | if (symbol === "%") { 266 | return { type: TokenType.MOD, precedence: 13, value: symbol }; 267 | } 268 | if (symbol === "*") { 269 | return { type: TokenType.MUL, precedence: 13, value: symbol }; 270 | } 271 | if (symbol === "/") { 272 | return { type: TokenType.DIV, precedence: 13, value: symbol }; 273 | } 274 | if (symbol === "+") { 275 | return { type: TokenType.ADD, precedence: 12, value: symbol }; 276 | } 277 | if (symbol === "-") { 278 | return { type: TokenType.SUB, precedence: 12, value: symbol }; 279 | } 280 | if (symbol === "<<") { 281 | return { type: TokenType.SHL, precedence: 11, value: symbol }; 282 | } 283 | if (symbol === ">>") { 284 | return { type: TokenType.SHR, precedence: 11, value: symbol }; 285 | } 286 | if (symbol === ">>>") { 287 | return { type: TokenType.SHRU, precedence: 11, value: symbol }; 288 | } 289 | if (symbol === ">") { 290 | return { type: TokenType.GT, precedence: 10, value: symbol }; 291 | } 292 | if (symbol === ">=") { 293 | return { type: TokenType.GE, precedence: 10, value: symbol }; 294 | } 295 | if (symbol === "<") { 296 | return { type: TokenType.LT, precedence: 10, value: symbol }; 297 | } 298 | if (symbol === "<=") { 299 | return { type: TokenType.LE, precedence: 10, value: symbol }; 300 | } 301 | if (symbol === "==") { 302 | return { type: TokenType.EQ, precedence: 9, value: symbol }; 303 | } 304 | if (symbol === "!=") { 305 | return { type: TokenType.NEQ, precedence: 9, value: symbol }; 306 | } 307 | if (symbol === "&") { 308 | return { type: TokenType.BAND, precedence: 8, value: symbol }; 309 | } 310 | if (symbol === "&") { 311 | return { type: TokenType.BXOR, precedence: 7, value: symbol }; 312 | } 313 | if (symbol === "|") { 314 | return { type: TokenType.BOR, precedence: 6, value: symbol }; 315 | } 316 | if (symbol === "&&") { 317 | return { type: TokenType.AND, precedence: 5, value: symbol }; 318 | } 319 | if (symbol === "||") { 320 | return { type: TokenType.OR, precedence: 4, value: symbol }; 321 | } 322 | if (symbol === ":") { 323 | return { type: TokenType.COLON, precedence: 2, value: symbol }; 324 | } 325 | if (symbol === "?") { 326 | return { type: TokenType.QUESTION, precedence: 2 - 0.1, value: symbol }; 327 | } 328 | throw new Error(`unsupport token: ${symbol}`); 329 | } 330 | 331 | private _convertToPostfix(infix: string[]) { 332 | const output: Token[] = []; 333 | const operators: Token[] = []; 334 | 335 | for (let i = 0; i < infix.length; i++) { 336 | const token = this._makeToken(infix[i], infix[i - 1]); 337 | if ( 338 | token.type === TokenType.NUMBER || 339 | token.type === TokenType.BOOLEAN || 340 | token.type === TokenType.STRING 341 | ) { 342 | output.push(token); 343 | } else if (token.value === "(" || token.value === "[") { 344 | operators.push(token); 345 | } else if (token.value === ")" || token.value === "]") { 346 | while ( 347 | operators.length && 348 | operators[operators.length - 1].value !== "(" && 349 | operators[operators.length - 1].value !== "[" 350 | ) { 351 | output.push(operators.pop()!); 352 | } 353 | 354 | const last = operators[operators.length - 1]; 355 | if (token.value === ")" && last.value !== "(") { 356 | throw new Error("unmatched parentheses: '('"); 357 | } else if (token.value === "]" && last.value !== "[") { 358 | throw new Error("unmatched parentheses: '['"); 359 | } 360 | if (token.value === "]") { 361 | output.push(operators.pop()!); 362 | } else { 363 | operators.pop(); 364 | } 365 | } else { 366 | while ( 367 | operators.length && 368 | token.precedence <= operators[operators.length - 1].precedence 369 | ) { 370 | output.push(operators.pop()!); 371 | } 372 | operators.push(token); 373 | } 374 | } 375 | 376 | while (operators.length) { 377 | output.push(operators.pop()!); 378 | } 379 | 380 | return output; 381 | } 382 | 383 | /** 384 | * Performs a dry run of the expression evaluation to check if it is syntactically valid. 385 | * Does not actually evaluate values, just verifies operator/operand counts and structure. 386 | * @returns true if expression is valid, false if invalid 387 | */ 388 | dryRun(): boolean { 389 | const stack: Token[] = []; 390 | 391 | try { 392 | for (const token of this._postfix) { 393 | const type = token.type; 394 | if ( 395 | type === TokenType.NUMBER || 396 | type === TokenType.BOOLEAN || 397 | type === TokenType.STRING 398 | ) { 399 | stack.push(token); 400 | } else if (type === TokenType.QUESTION) { 401 | if (stack.length < 3) { 402 | console.error(`not enough operands for ternary operator: ${token.value}`); 403 | return false; 404 | } 405 | stack.pop(); 406 | stack.pop(); 407 | } else if ( 408 | type === TokenType.POSITIVE || 409 | type === TokenType.NEGATION || 410 | type === TokenType.NOT || 411 | type === TokenType.BNOT 412 | ) { 413 | if (stack.length < 1) { 414 | console.error(`not enough operands for unary operator: ${token.value}`); 415 | return false; 416 | } 417 | } else { 418 | if (stack.length < 2) { 419 | console.error(`not enough operands for binary operator: ${token.value}`); 420 | return false; 421 | } 422 | const b = stack.pop()!; // b 423 | const a = stack.pop()!; // a 424 | stack.push(a); 425 | if (type === TokenType.DOT) { 426 | if (a.type !== TokenType.STRING || b.type !== TokenType.STRING) { 427 | console.error( 428 | `invalid operands for dot operator: ${a.value} and ${b.value}` 429 | ); 430 | return false; 431 | } 432 | } else if (type === TokenType.COLON) { 433 | stack.push(b); 434 | } 435 | } 436 | } 437 | if (stack.length !== 1) { 438 | console.error( 439 | `invalid number of operands remaining: ${stack.map((t) => t.value).join(", ")}` 440 | ); 441 | } 442 | return stack.length === 1; 443 | } catch { 444 | return false; 445 | } 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/behavior3/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./blackboard"; 2 | export * from "./context"; 3 | export * from "./evaluator"; 4 | export * from "./node"; 5 | export * from "./stack"; 6 | export * from "./tree"; 7 | -------------------------------------------------------------------------------- /src/behavior3/node.ts: -------------------------------------------------------------------------------- 1 | import { Blackboard } from "./blackboard"; 2 | import type { Context, DeepReadonly, NodeContructor, ObjectType } from "./context"; 3 | import type { Tree, TreeData } from "./tree"; 4 | 5 | export type Status = "success" | "failure" | "running"; 6 | 7 | export interface NodeDef { 8 | name: string; 9 | /** 10 | * Recommended type used for the node definition: 11 | * + `Action`: No children allowed, returns `success`, `failure` or `running`. 12 | * + `Decorator`: Only one child allowed, returns `success`, `failure` or `running`. 13 | * + `Composite`: Contains more than one child, returns `success`, `failure` or `running`. 14 | * + `Condition`: No children allowed, no output, returns `success` or `failure`. 15 | */ 16 | type: "Action" | "Decorator" | "Condition" | "Composite"; 17 | desc: string; 18 | /** ["input1?", "input2..."] */ 19 | input?: string[]; 20 | /** ["output1", "output2..."] */ 21 | output?: string[]; 22 | args?: { 23 | name: string; 24 | type: 25 | | "bool" 26 | | "bool?" 27 | | "bool[]" 28 | | "bool[]?" 29 | | "int" 30 | | "int?" 31 | | "int[]" 32 | | "int[]?" 33 | | "float" 34 | | "float?" 35 | | "float[]" 36 | | "float[]?" 37 | | "string" 38 | | "string?" 39 | | "string[]" 40 | | "string[]?" 41 | | "json" 42 | | "json?" 43 | | "json[]" 44 | | "json[]?" 45 | | "expr" 46 | | "expr?" 47 | | "expr[]" 48 | | "expr[]?"; 49 | desc: string; 50 | /** Input `value`, only one is allowed between `value` and this arg.*/ 51 | oneof?: string; 52 | default?: unknown; 53 | options?: { name: string; value: unknown; desc?: string }[]; 54 | }[]; 55 | doc?: string; 56 | icon?: string; 57 | color?: string; 58 | /** 59 | * Used in Behavior3 Editor, to help editor manage available nodes in file tree. 60 | */ 61 | group?: GroupType[]; 62 | /** 63 | * Used in Behavior3 Editor, to help editor deduce the status of the node. 64 | * 65 | * + `!success` !(child_success|child_success|...) 66 | * + `!failure` !(child_failure|child_failure|...) 67 | * + `|success` child_success|child_success|... 68 | * + `|failure` child_failure|child_failure|... 69 | * + `|running` child_running|child_running|... 70 | * + `&success` child_success&child_success&... 71 | * + `&failure` child_failure&child_failure&... 72 | */ 73 | status?: ( 74 | | "success" 75 | | "failure" 76 | | "running" 77 | | "!success" 78 | | "!failure" 79 | | "|success" 80 | | "|failure" 81 | | "|running" 82 | | "&success" 83 | | "&failure" 84 | )[]; 85 | /** 86 | * Used in Behavior3 Editor, to help editor alert error when the num of children is wrong. 87 | * 88 | * Allowed number of children 89 | * + -1: unlimited 90 | * + 0: no children 91 | * + 1: exactly one 92 | * + 2: exactly two (case) 93 | * + 3: exactly three children (ifelse) 94 | */ 95 | children?: -1 | 0 | 1 | 2 | 3; 96 | } 97 | 98 | export interface NodeData { 99 | id: string; 100 | name: string; 101 | desc: string; 102 | args: { [k: string]: unknown }; 103 | debug?: boolean; 104 | disabled?: boolean; 105 | input: string[]; 106 | output: string[]; 107 | children: NodeData[]; 108 | 109 | tree: TreeData; 110 | } 111 | 112 | export abstract class Node { 113 | readonly args: unknown = {}; 114 | readonly input: unknown[] = []; 115 | readonly output: unknown[] = []; 116 | 117 | protected readonly _context: Context; 118 | 119 | private _parent: Node | null = null; 120 | private _children: Node[] = []; 121 | private _cfg: DeepReadonly; 122 | private _yield?: string; 123 | private _stringifiedArgs: Record | undefined; 124 | 125 | constructor(context: Context, cfg: NodeData) { 126 | this._context = context; 127 | this._cfg = cfg; 128 | Object.keys(cfg.args).forEach((k) => { 129 | const value = cfg.args[k]; 130 | if (value && typeof value === "object") { 131 | this._stringifiedArgs = this._stringifiedArgs ?? {}; 132 | this._stringifiedArgs[k] = JSON.stringify(value); 133 | } else { 134 | (this.args as ObjectType)[k] = value; 135 | } 136 | }); 137 | 138 | for (const childCfg of cfg.children) { 139 | if (!childCfg.disabled) { 140 | const child = Node.create(context, childCfg); 141 | child._parent = this; 142 | this._children.push(child); 143 | } 144 | } 145 | } 146 | 147 | /** @private */ 148 | get __yield() { 149 | return (this._yield ||= Blackboard.makeTempVar(this, "YIELD")); 150 | } 151 | 152 | get cfg() { 153 | return this._cfg; 154 | } 155 | 156 | get id() { 157 | return this.cfg.id; 158 | } 159 | 160 | get name() { 161 | return this.cfg.name; 162 | } 163 | 164 | get parent() { 165 | return this._parent; 166 | } 167 | 168 | get children(): Readonly { 169 | return this._children; 170 | } 171 | 172 | tick(tree: Tree): Status { 173 | const { stack, blackboard } = tree; 174 | const { cfg, input, output, args } = this; 175 | 176 | if (stack.top() !== this) { 177 | stack.push(this); 178 | } 179 | 180 | input.length = 0; 181 | output.length = 0; 182 | 183 | cfg.input.forEach((k, i) => (input[i] = blackboard.get(k))); 184 | 185 | if (this._stringifiedArgs) { 186 | for (const k in this._stringifiedArgs) { 187 | (args as ObjectType)[k] = JSON.parse(this._stringifiedArgs[k]); 188 | } 189 | } 190 | 191 | let status: Status = "failure"; 192 | try { 193 | status = this.onTick(tree, tree.__lastStatus); 194 | } catch (e) { 195 | if (e instanceof Error) { 196 | this.error(`${e.message}\n ${e.stack}`); 197 | } else { 198 | console.error(e); 199 | } 200 | } 201 | 202 | if (tree.__interrupted) { 203 | return "running"; 204 | } else if (status !== "running") { 205 | cfg.output.forEach((k, i) => blackboard.set(k, output[i])); 206 | stack.pop(); 207 | } else if (blackboard.get(this.__yield) === undefined) { 208 | blackboard.set(this.__yield, true); 209 | } 210 | 211 | tree.__lastStatus = status; 212 | 213 | if (cfg.debug || tree.debug) { 214 | let varStr = ""; 215 | for (const k in blackboard.values) { 216 | if (!(Blackboard.isTempVar(k) || Blackboard.isPrivateVar(k))) { 217 | varStr += `${k}:${blackboard.values[k]}, `; 218 | } 219 | } 220 | const indent = tree.debug ? " ".repeat(stack.length) : ""; 221 | console.debug( 222 | `[DEBUG] behavior3 -> ${indent}${this.name}: tree:${this.cfg.tree.name} tree_id:${tree.id}, ` + 223 | `node:${this.id}, status:${status}, values:{${varStr}} args:${JSON.stringify( 224 | cfg.args 225 | )}` 226 | ); 227 | } 228 | 229 | return status; 230 | } 231 | 232 | assert(condition: unknown, msg: string): asserts condition { 233 | if (!condition) { 234 | this.throw(msg); 235 | } 236 | } 237 | 238 | /** 239 | * throw an error 240 | */ 241 | throw(msg: string): never { 242 | throw new Error(`${this.cfg.tree.name}->${this.name}#${this.id}: ${msg}`); 243 | } 244 | 245 | /** 246 | * use console.error to print error message 247 | */ 248 | error(msg: string) { 249 | console.error(`${this.cfg.tree.name}->${this.name}#${this.id}: ${msg}`); 250 | } 251 | 252 | /** 253 | * use console.warn to print warning message 254 | */ 255 | warn(msg: string) { 256 | console.warn(`${this.cfg.tree.name}->${this.name}#${this.id}: ${msg}`); 257 | } 258 | 259 | /** 260 | * use console.debug to print debug message 261 | */ 262 | debug(msg: string) { 263 | console.debug(`${this.cfg.tree.name}->${this.name}#${this.id}: ${msg}`); 264 | } 265 | 266 | /** 267 | * use console.info to print info message 268 | */ 269 | 270 | info(msg: string) { 271 | console.info(`${this.cfg.tree.name}->${this.name}#${this.id}: ${msg}`); 272 | } 273 | 274 | protected _checkOneof(inputIndex: number, argValue: V | undefined, defaultValue?: V) { 275 | const inputValue = this.input[inputIndex]; 276 | const inputName = this.cfg.input[inputIndex]; 277 | let value: V | undefined; 278 | if (inputName) { 279 | if (inputValue === undefined) { 280 | const func = defaultValue === undefined ? this.throw : this.warn; 281 | func.call(this, `missing input '${inputName}'`); 282 | } 283 | value = inputValue as V; 284 | } else { 285 | value = argValue; 286 | } 287 | return (value ?? defaultValue) as V; 288 | } 289 | 290 | /** 291 | * Executes the node's behavior tree logic. 292 | * @param tree The behavior tree instance 293 | * @param status The status of the last node 294 | * @returns The execution status: `success`, `failure`, or `running` 295 | */ 296 | abstract onTick(tree: Tree, status: Status): Status; 297 | 298 | static get descriptor(): NodeDef { 299 | throw new Error(`descriptor not found in '${this.name}'`); 300 | } 301 | 302 | static create(context: Context, cfg: NodeData) { 303 | const NodeCls = context.nodeCtors[cfg.name] as NodeContructor | undefined; 304 | const descriptor = context.nodeDefs[cfg.name] as NodeDef | undefined; 305 | 306 | if (!NodeCls || !descriptor) { 307 | throw new Error(`behavior3: node '${cfg.tree.name}->${cfg.name}' is not registered`); 308 | } 309 | 310 | const node = new NodeCls(context, cfg); 311 | 312 | if (node.tick !== Node.prototype.tick) { 313 | throw new Error("don't override 'tick' function"); 314 | } 315 | 316 | if ( 317 | descriptor.children !== undefined && 318 | descriptor.children !== -1 && 319 | descriptor.children !== node.children.length 320 | ) { 321 | if (descriptor.children === 0) { 322 | node.warn(`no children is required`); 323 | } else if (node.children.length < descriptor.children) { 324 | node.throw(`at least ${descriptor.children} children are required`); 325 | } else { 326 | node.warn(`exactly ${descriptor.children} children`); 327 | } 328 | } 329 | 330 | return node; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/calculate.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeData, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Calculate extends Node { 6 | declare args: { readonly value: string }; 7 | 8 | constructor(context: Context, cfg: NodeData) { 9 | super(context, cfg); 10 | 11 | if (typeof this.args.value !== "string" || this.args.value.length === 0) { 12 | this.throw(`args.value is not a expr string`); 13 | } 14 | context.compileCode(this.args.value); 15 | } 16 | 17 | override onTick(tree: Tree, status: Status): Status { 18 | const value = tree.blackboard.eval(this.args.value); 19 | this.output.push(value); 20 | return "success"; 21 | } 22 | 23 | static override get descriptor(): NodeDef { 24 | return { 25 | name: "Calculate", 26 | type: "Action", 27 | children: 0, 28 | status: ["success"], 29 | desc: "简单的数值公式计算", 30 | args: [{ name: "value", type: "expr", desc: "计算公式" }], 31 | output: ["计算结果"], 32 | doc: ` 33 | + 做简单的数值公式计算,返回结果到输出 34 | `, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/concat.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Concat extends Node { 6 | declare input: [unknown[], unknown[]]; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const [arr1, arr2] = this.input; 10 | if (!Array.isArray(arr1) || !Array.isArray(arr2)) { 11 | return "failure"; 12 | } 13 | this.output.push(arr1.concat(arr2)); 14 | return "success"; 15 | } 16 | 17 | static override get descriptor(): NodeDef { 18 | return { 19 | name: "Concat", 20 | type: "Action", 21 | children: 0, 22 | status: ["success", "failure"], 23 | desc: "将两个输入合并为一个数组,并返回新数组", 24 | input: ["数组1", "数组2"], 25 | output: ["新数组"], 26 | doc: ` 27 | + 如果输入不是数组,则返回 \`failure\` 28 | `, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/get-field.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class GetField extends Node { 6 | declare input: [{ [key: string]: unknown }, string | undefined]; 7 | declare args: { readonly field?: string }; 8 | 9 | override onTick(tree: Tree, status: Status): Status { 10 | const [obj] = this.input; 11 | if (typeof obj !== "object" || !obj) { 12 | this.warn(`invalid object: ${obj}`); 13 | return "failure"; 14 | } 15 | 16 | const args = this.args; 17 | const field = this._checkOneof(1, args.field); 18 | const value = obj[field]; 19 | if (typeof field !== "string" && typeof field !== "number") { 20 | this.warn(`invalid field: ${field}`); 21 | return "failure"; 22 | } else if (value !== undefined && value !== null) { 23 | this.output.push(value); 24 | return "success"; 25 | } else { 26 | return "failure"; 27 | } 28 | } 29 | 30 | static override get descriptor(): NodeDef { 31 | return { 32 | name: "GetField", 33 | type: "Action", 34 | children: 0, 35 | status: ["success", "failure"], 36 | desc: "获取对象的字段值", 37 | args: [ 38 | { 39 | name: "field", 40 | type: "string?", 41 | desc: "字段(field)", 42 | oneof: "字段(field)", 43 | }, 44 | ], 45 | input: ["对象", "字段(field)?"], 46 | output: ["字段值(value)"], 47 | doc: ` 48 | + 合法元素不包括 \`undefined\` 和 \`null\` 49 | + 只有获取到合法元素时候才会返回 \`success\`,否则返回 \`failure\` 50 | `, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/index.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Index extends Node { 6 | declare input: [unknown[], number | undefined]; 7 | declare args: { readonly index: number }; 8 | 9 | override onTick(tree: Tree, status: Status): Status { 10 | const [arr] = this.input; 11 | if (arr instanceof Array) { 12 | const index = this._checkOneof(1, this.args.index); 13 | const value = arr[index]; 14 | if (value !== undefined && value !== null) { 15 | this.output.push(value); 16 | return "success"; 17 | } else if (typeof index !== "number" || isNaN(index)) { 18 | this.warn(`invalid index: ${index}`); 19 | } 20 | } else { 21 | this.warn(`invalid array: ${arr}`); 22 | } 23 | 24 | return "failure"; 25 | } 26 | 27 | static override get descriptor(): NodeDef { 28 | return { 29 | name: "Index", 30 | type: "Action", 31 | children: 0, 32 | status: ["success", "failure"], 33 | desc: "索引输入的数组", 34 | args: [ 35 | { 36 | name: "index", 37 | type: "int?", 38 | desc: "索引", 39 | oneof: "索引", 40 | }, 41 | ], 42 | input: ["数组", "索引?"], 43 | output: ["值"], 44 | doc: ` 45 | + 合法元素不包括 \`undefined\` 和 \`null\` 46 | + 索引数组的时候,第一个元素的索引为 0 47 | + 只有索引到有合法元素时候才会返回 \`success\`,否则返回 \`failure\` 48 | `, 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/just-success.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | // 只返回成功,用来满足一些特殊节点的结构要求 6 | export class JustSuccess extends Node { 7 | override onTick(tree: Tree, status: Status): Status { 8 | return "success"; 9 | } 10 | 11 | static override get descriptor(): NodeDef { 12 | return { 13 | name: "JustSuccess", 14 | type: "Action", 15 | children: 0, 16 | status: ["success"], 17 | desc: "什么都不干,只返回成功", 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/let.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Let extends Node { 6 | declare args: { readonly value?: unknown }; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const value = this._checkOneof(0, this.args.value, null); 10 | this.output.push(value); 11 | return "success"; 12 | } 13 | 14 | static override get descriptor(): NodeDef { 15 | return { 16 | name: "Let", 17 | type: "Action", 18 | children: 0, 19 | status: ["success"], 20 | desc: "定义新的变量名", 21 | input: ["已存在变量名?"], 22 | args: [ 23 | { 24 | name: "value", 25 | type: "json?", 26 | desc: "值(value)", 27 | oneof: "已存在变量名", 28 | }, 29 | ], 30 | output: ["新变量名"], 31 | doc: ` 32 | + 如果有输入变量,则给已有变量重新定义一个名字 33 | + 如果\`值(value)\`为 \`null\`,则清除变量 34 | `, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/log.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | enum LogLevel { 6 | INFO = "info", 7 | DEBUG = "debug", 8 | WARN = "warn", 9 | ERROR = "error", 10 | } 11 | 12 | export class Log extends Node { 13 | declare input: [unknown?]; 14 | declare args: { 15 | readonly message: string; 16 | readonly level: LogLevel; 17 | }; 18 | 19 | override onTick(tree: Tree, status: Status): Status { 20 | const [inputMsg] = this.input; 21 | const args = this.args; 22 | const level = args.level ?? LogLevel.INFO; 23 | let print = console.log; 24 | if (level === LogLevel.INFO) { 25 | print = console.info; 26 | } else if (level === LogLevel.DEBUG) { 27 | print = console.debug; 28 | } else if (level === LogLevel.WARN) { 29 | print = console.warn; 30 | } else if (level === LogLevel.ERROR) { 31 | print = console.error; 32 | } 33 | print.call(console, "behavior3 -> log:", args.message, inputMsg ?? ""); 34 | return "success"; 35 | } 36 | 37 | static override get descriptor(): NodeDef { 38 | return { 39 | name: "Log", 40 | type: "Action", 41 | children: 0, 42 | status: ["success"], 43 | desc: "打印日志", 44 | input: ["日志?"], 45 | args: [ 46 | { 47 | name: "message", 48 | type: "string", 49 | desc: "日志", 50 | }, 51 | { 52 | name: "level", 53 | type: "string", 54 | desc: "日志级别", 55 | default: LogLevel.INFO, 56 | options: [ 57 | { 58 | name: "INFO", 59 | value: LogLevel.INFO, 60 | }, 61 | { 62 | name: "DEBUG", 63 | value: LogLevel.DEBUG, 64 | }, 65 | { 66 | name: "WARN", 67 | value: LogLevel.WARN, 68 | }, 69 | { 70 | name: "ERROR", 71 | value: LogLevel.ERROR, 72 | }, 73 | ], 74 | }, 75 | ], 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/math.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeData, NodeDef, Status } from "../../node"; 3 | import type { Tree } from "../../tree"; 4 | 5 | enum Op { 6 | // 基础运算 7 | abs = 0, 8 | ceil = 1, 9 | floor = 2, 10 | round = 3, 11 | sign = 4, 12 | // 三角函数 13 | sin = 5, 14 | cos = 6, 15 | tan = 7, 16 | atan2 = 8, 17 | // 幂和对数 18 | pow = 9, 19 | sqrt = 10, 20 | log = 11, 21 | // 最值运算 22 | min = 12, 23 | max = 13, 24 | // 随机数 25 | random = 14, 26 | randInt = 15, 27 | randFloat = 16, 28 | // 其他运算 29 | sum = 17, 30 | average = 18, 31 | product = 19, 32 | } 33 | 34 | export class MathNode extends Node { 35 | private _op: Op; 36 | 37 | constructor(context: Context, cfg: NodeData) { 38 | super(context, cfg); 39 | this._op = Op[cfg.args.op as keyof typeof Op]; 40 | if (this._op === undefined) { 41 | throw new Error(`unknown op: ${cfg.args.op}`); 42 | } 43 | } 44 | 45 | override onTick(tree: Tree, status: Status): Status { 46 | const op = this._op; 47 | const args = this.args as { value1?: number; value2?: number }; 48 | const inputValues = this.input.filter((value) => value !== undefined).map(Number); 49 | 50 | // 优先使用常量参数,如果没有则使用输入参数 51 | const values: number[] = []; 52 | if (args.value1 !== undefined) { 53 | values[0] = args.value1; 54 | } else if (inputValues.length > 0) { 55 | values[0] = inputValues[0]; 56 | } 57 | 58 | if (args.value2 !== undefined) { 59 | values[1] = args.value2; 60 | } else if (inputValues.length > 1) { 61 | values[1] = inputValues[1]; 62 | } 63 | 64 | // 对于需要更多参数的运算(min, max, sum等),添加剩余的输入参数 65 | if (inputValues.length > 2) { 66 | values.push(...inputValues.slice(2)); 67 | } 68 | 69 | if (values.length === 0 && op !== Op.random && op !== Op.randInt && op !== Op.randFloat) { 70 | this.error("at least one parameter is required"); 71 | return "failure"; 72 | } 73 | 74 | let result: number; 75 | switch (op) { 76 | case Op.abs: { 77 | if (values.length !== 1) { 78 | this.error("abs operation requires exactly one parameter"); 79 | return "failure"; 80 | } 81 | result = Math.abs(values[0]); 82 | break; 83 | } 84 | case Op.ceil: { 85 | if (values.length !== 1) { 86 | this.error("ceil operation requires exactly one parameter"); 87 | return "failure"; 88 | } 89 | result = Math.ceil(values[0]); 90 | break; 91 | } 92 | case Op.floor: { 93 | if (values.length !== 1) { 94 | this.error("floor operation requires exactly one parameter"); 95 | return "failure"; 96 | } 97 | result = Math.floor(values[0]); 98 | break; 99 | } 100 | case Op.round: { 101 | if (values.length !== 1) { 102 | this.error("round operation requires exactly one parameter"); 103 | return "failure"; 104 | } 105 | result = Math.round(values[0]); 106 | break; 107 | } 108 | case Op.sin: { 109 | if (values.length !== 1) { 110 | this.error("sin operation requires exactly one parameter"); 111 | return "failure"; 112 | } 113 | result = Math.sin(values[0]); 114 | break; 115 | } 116 | case Op.cos: { 117 | if (values.length !== 1) { 118 | this.error("cos operation requires exactly one parameter"); 119 | return "failure"; 120 | } 121 | result = Math.cos(values[0]); 122 | break; 123 | } 124 | case Op.tan: { 125 | if (values.length !== 1) { 126 | this.error("tan operation requires exactly one parameter"); 127 | return "failure"; 128 | } 129 | result = Math.tan(values[0]); 130 | break; 131 | } 132 | case Op.pow: { 133 | if (values.length !== 2) { 134 | this.error("pow operation requires exactly two parameters"); 135 | return "failure"; 136 | } 137 | result = Math.pow(values[0], values[1]); 138 | break; 139 | } 140 | case Op.sqrt: { 141 | if (values.length !== 1) { 142 | this.error("sqrt operation requires exactly one parameter"); 143 | return "failure"; 144 | } 145 | result = Math.sqrt(values[0]); 146 | break; 147 | } 148 | case Op.log: { 149 | if (values.length !== 1) { 150 | this.error("log operation requires exactly one parameter"); 151 | return "failure"; 152 | } 153 | result = Math.log(values[0]); 154 | break; 155 | } 156 | case Op.min: { 157 | result = Math.min(...values); 158 | break; 159 | } 160 | case Op.max: { 161 | result = Math.max(...values); 162 | break; 163 | } 164 | case Op.sum: { 165 | result = values.reduce((a, b) => a + b, 0); 166 | break; 167 | } 168 | case Op.average: { 169 | result = values.reduce((a, b) => a + b, 0) / values.length; 170 | break; 171 | } 172 | case Op.product: { 173 | result = values.reduce((a, b) => a * b, 1); 174 | break; 175 | } 176 | case Op.sign: { 177 | if (values.length !== 1) { 178 | this.error("sign operation requires exactly one parameter"); 179 | return "failure"; 180 | } 181 | result = Math.sign(values[0]); 182 | break; 183 | } 184 | case Op.atan2: { 185 | if (values.length !== 2) { 186 | this.error("atan2 operation requires exactly two parameters"); 187 | return "failure"; 188 | } 189 | result = Math.atan2(values[0], values[1]); 190 | break; 191 | } 192 | case Op.random: { 193 | result = Math.random(); 194 | break; 195 | } 196 | case Op.randInt: { 197 | if (values.length !== 2) { 198 | this.error("randInt operation requires two parameters (min and max)"); 199 | return "failure"; 200 | } 201 | const min = Math.ceil(values[0]); 202 | const max = Math.floor(values[1]); 203 | if (min > max) { 204 | this.error("minimum value cannot be greater than maximum value"); 205 | return "failure"; 206 | } 207 | result = Math.floor(Math.random() * (max - min + 1)) + min; 208 | break; 209 | } 210 | case Op.randFloat: { 211 | if (values.length !== 2) { 212 | this.error("randFloat operation requires two parameters (min and max)"); 213 | return "failure"; 214 | } 215 | if (values[0] > values[1]) { 216 | this.error("minimum value cannot be greater than maximum value"); 217 | return "failure"; 218 | } 219 | result = Math.random() * (values[1] - values[0]) + values[0]; 220 | break; 221 | } 222 | default: { 223 | return "failure"; 224 | } 225 | } 226 | 227 | if (isNaN(result)) { 228 | this.error(`result is NaN: ${result}`); 229 | return "failure"; 230 | } 231 | 232 | this.output.push(result); 233 | return "success"; 234 | } 235 | 236 | static override get descriptor(): NodeDef { 237 | return { 238 | name: "Math", 239 | type: "Action", 240 | desc: "执行数学运算", 241 | input: ["参数..."], 242 | output: ["结果"], 243 | status: ["success", "failure"], 244 | args: [ 245 | { 246 | name: "op", 247 | type: "string", 248 | desc: "数学运算类型", 249 | options: [ 250 | // 基础运算 251 | { name: "绝对值", value: Op[Op.abs] }, 252 | { name: "向上取整", value: Op[Op.ceil] }, 253 | { name: "向下取整", value: Op[Op.floor] }, 254 | { name: "四舍五入", value: Op[Op.round] }, 255 | { name: "符号", value: Op[Op.sign] }, 256 | // 三角函数 257 | { name: "正弦", value: Op[Op.sin] }, 258 | { name: "余弦", value: Op[Op.cos] }, 259 | { name: "正切", value: Op[Op.tan] }, 260 | { name: "反正切2", value: Op[Op.atan2] }, 261 | // 幂和对数 262 | { name: "幂运算", value: Op[Op.pow] }, 263 | { name: "平方根", value: Op[Op.sqrt] }, 264 | { name: "自然对数", value: Op[Op.log] }, 265 | // 最值运算 266 | { name: "最小值", value: Op[Op.min] }, 267 | { name: "最大值", value: Op[Op.max] }, 268 | // 随机数 269 | { name: "随机数", value: Op[Op.random] }, 270 | { name: "随机整数", value: Op[Op.randInt] }, 271 | { name: "随机浮点数", value: Op[Op.randFloat] }, 272 | // 其他运算 273 | { name: "求和", value: Op[Op.sum] }, 274 | { name: "平均值", value: Op[Op.average] }, 275 | { name: "乘积", value: Op[Op.product] }, 276 | ], 277 | }, 278 | { 279 | name: "value1", 280 | type: "float?", 281 | desc: "参数1(优先使用)", 282 | }, 283 | { 284 | name: "value2", 285 | type: "float?", 286 | desc: "参数2(优先使用)", 287 | }, 288 | ], 289 | }; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/now.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Now extends Node { 6 | override onTick(tree: Tree, status: Status): Status { 7 | this.output.push(tree.context.time); 8 | return "success"; 9 | } 10 | 11 | static override get descriptor(): NodeDef { 12 | return { 13 | name: "Now", 14 | type: "Action", 15 | children: 0, 16 | status: ["success"], 17 | desc: "获取当前时间", 18 | output: ["当前时间"], 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/push.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Push extends Node { 6 | declare input: [unknown[], unknown]; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const [arr, element] = this.input; 10 | if (!Array.isArray(arr)) { 11 | return "failure"; 12 | } 13 | arr.push(element); 14 | return "success"; 15 | } 16 | 17 | static override get descriptor(): NodeDef { 18 | return { 19 | name: "Push", 20 | type: "Action", 21 | children: 0, 22 | status: ["success", "failure"], 23 | desc: "向数组中添加元素", 24 | input: ["数组", "元素"], 25 | doc: ` 26 | + 当变量\`数组\`不是数组类型时返回 \`failure\` 27 | + 其余返回 \`success\` 28 | `, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/random-index.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class RandomIndex extends Node { 6 | declare input: [unknown[]]; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const [arr] = this.input; 10 | if (!(arr instanceof Array) || arr.length === 0) { 11 | return "failure"; 12 | } 13 | 14 | const idx = Math.floor(Math.random() * arr.length); 15 | const value = arr[idx]; 16 | if (value !== undefined && value !== null) { 17 | this.output.push(value); 18 | this.output.push(idx); 19 | return "success"; 20 | } else { 21 | return "failure"; 22 | } 23 | } 24 | 25 | static override get descriptor(): NodeDef { 26 | return { 27 | name: "RandomIndex", 28 | type: "Action", 29 | children: 0, 30 | status: ["success", "failure"], 31 | desc: "随机返回输入的其中一个!", 32 | input: ["输入目标"], 33 | output: ["随机目标", "索引?"], 34 | doc: ` 35 | + 合法元素不包括 \`undefined\` 和 \`null\` 36 | + 在输入数组中,随机返回其中一个 37 | + 当输入数组为空时,或者没有合法元素,返回 \`failure\` 38 | `, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/set-field.ts: -------------------------------------------------------------------------------- 1 | import type { Context, ObjectType } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class SetField extends Node { 6 | declare input: [ObjectType, string?, unknown?]; 7 | declare args: { 8 | readonly field?: string; 9 | readonly value?: unknown; 10 | }; 11 | 12 | override onTick(tree: Tree, status: Status): Status { 13 | const [obj] = this.input; 14 | if (typeof obj !== "object" || !obj) { 15 | this.warn(`invalid object: ${obj}`); 16 | return "failure"; 17 | } 18 | 19 | const args = this.args; 20 | const field = this._checkOneof(1, args.field); 21 | const value = this._checkOneof(2, args.value, null); 22 | 23 | if (typeof field !== "string" && typeof field !== "number") { 24 | this.warn(`invalid field: ${field}`); 25 | return "failure"; 26 | } else if (typeof obj[field] === "function") { 27 | this.warn(`not allowed to overwrite function ${field}`); 28 | return "failure"; 29 | } else if (value === null || value === undefined) { 30 | delete obj[field]; 31 | return "success"; 32 | } else { 33 | obj[field] = value; 34 | return "success"; 35 | } 36 | } 37 | 38 | static override get descriptor(): NodeDef { 39 | return { 40 | name: "SetField", 41 | type: "Action", 42 | children: 0, 43 | status: ["success", "failure"], 44 | desc: "设置对象字段值", 45 | input: ["输入对象", "字段(field)?", "值(value)?"], 46 | args: [ 47 | { name: "field", type: "string?", desc: "字段(field)", oneof: "字段(field)" }, 48 | { name: "value", type: "json?", desc: "值(value)", oneof: "值(value)" }, 49 | ], 50 | doc: ` 51 | + 对输入对象设置 \`field\` 和 \`value\` 52 | + 输入参数1必须为对象,否则返回 \`failure\` 53 | + 如果 \`field\` 不为 \`string\`, 也返回 \`failure\` 54 | + 如果 \`value\` 为 \`undefined\` 或 \`null\`, 则删除 \`field\` 的值 55 | `, 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/wait-for-event.ts: -------------------------------------------------------------------------------- 1 | import { Blackboard } from "../../blackboard"; 2 | import type { Context } from "../../context"; 3 | import { Node, NodeData, NodeDef, Status } from "../../node"; 4 | import { Tree, TreeEvent } from "../../tree"; 5 | 6 | export class WaitForEvent extends Node { 7 | declare args: { 8 | readonly event: string; 9 | }; 10 | 11 | private _triggerKey!: string; 12 | private _expiredKey!: string; 13 | 14 | constructor(context: Context, cfg: NodeData) { 15 | super(context, cfg); 16 | 17 | this._triggerKey = Blackboard.makeTempVar(this, "trigger"); 18 | this._expiredKey = Blackboard.makeTempVar(this, "expired"); 19 | } 20 | 21 | override onTick(tree: Tree): Status { 22 | const triggerKey = this._triggerKey; 23 | const triggered = tree.blackboard.get(triggerKey); 24 | const expiredKey = this._expiredKey; 25 | const expired = tree.blackboard.get(expiredKey) ?? tree.context.time; 26 | if (triggered === true) { 27 | tree.blackboard.set(triggerKey, undefined); 28 | tree.blackboard.set(expiredKey, undefined); 29 | return "success"; 30 | } else if (triggered === undefined) { 31 | tree.blackboard.set(triggerKey, false); 32 | tree.blackboard.set(expiredKey, tree.context.time + 5); 33 | tree.context.on( 34 | this.args.event, 35 | () => { 36 | tree.blackboard.set(triggerKey, true); 37 | tree.context.off(this.args.event, tree); 38 | tree.context.off(TreeEvent.INTERRUPTED, tree); 39 | }, 40 | tree 41 | ); 42 | tree.context.on( 43 | TreeEvent.INTERRUPTED, 44 | () => { 45 | tree.blackboard.set(triggerKey, undefined); 46 | tree.blackboard.set(expiredKey, undefined); 47 | tree.context.off(this.args.event, tree); 48 | tree.context.off(TreeEvent.INTERRUPTED, tree); 49 | }, 50 | tree 51 | ); 52 | } else if (tree.context.time >= expired) { 53 | tree.blackboard.set(expiredKey, tree.context.time + 5); 54 | this.debug(`wait for event: ${this.args.event}`); 55 | } 56 | 57 | return "running"; 58 | } 59 | 60 | static override get descriptor(): NodeDef { 61 | return { 62 | name: "WaitForEvent", 63 | type: "Action", 64 | children: 0, 65 | status: ["success", "running"], 66 | desc: "等待事件触发", 67 | args: [ 68 | { 69 | name: "event", 70 | type: "string", 71 | desc: "事件名称", 72 | }, 73 | ], 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/behavior3/nodes/actions/wait.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Wait extends Node { 6 | declare args: { 7 | readonly time: number; 8 | readonly random?: number; 9 | }; 10 | 11 | override onTick(tree: Tree, status: Status): Status { 12 | const t: number | undefined = tree.resume(this); 13 | if (typeof t === "number") { 14 | if (tree.context.time >= t) { 15 | return "success"; 16 | } else { 17 | return "running"; 18 | } 19 | } else { 20 | const args = this.args; 21 | let time = this._checkOneof(0, args.time, 0); 22 | if (args.random) { 23 | time += (Math.random() - 0.5) * args.random; 24 | } 25 | return tree.yield(this, tree.context.time + time); 26 | } 27 | } 28 | 29 | static override get descriptor(): NodeDef { 30 | return { 31 | name: "Wait", 32 | type: "Action", 33 | children: 0, 34 | status: ["success", "running"], 35 | desc: "等待", 36 | input: ["等待时间?"], 37 | args: [ 38 | { 39 | name: "time", 40 | type: "float?", 41 | desc: "等待时间", 42 | oneof: "等待时间", 43 | }, 44 | { 45 | name: "random", 46 | type: "float?", 47 | desc: "随机范围", 48 | }, 49 | ], 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/behavior3/nodes/composites/ifelse.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | /** 6 | * IfElse node executes different child nodes based on a condition. 7 | * 8 | * The node requires exactly 3 children: 9 | * 1. The condition node that determines which branch to execute 10 | * 2. The node to execute if condition returns `success` 11 | * 3. The node to execute if condition returns `failure` 12 | * 13 | * The execution flow is: 14 | * 1. Execute condition node (first child) 15 | * 2. If condition returns `success`, execute second child 16 | * 3. If condition returns `failure`, execute third child 17 | * 4. Return the status of whichever branch was executed 18 | */ 19 | export class IfElse extends Node { 20 | private _ifelse(tree: Tree, status: Exclude) { 21 | const i = status === "success" ? 1 : 2; 22 | const childStatus = this.children[i].tick(tree); 23 | if (childStatus === "running") { 24 | return tree.yield(this, i); 25 | } else { 26 | return childStatus; 27 | } 28 | } 29 | 30 | override onTick(tree: Tree, status: Status): Status { 31 | const i: number | undefined = tree.resume(this); 32 | if (i !== undefined) { 33 | if (status === "running") { 34 | this.throw(`unexpected status error`); 35 | } else if (i === 0) { 36 | return this._ifelse(tree, status); 37 | } else { 38 | return status; 39 | } 40 | return "failure"; 41 | } 42 | 43 | status = this.children[0].tick(tree); 44 | if (status === "running") { 45 | return tree.yield(this, 0); 46 | } else { 47 | return this._ifelse(tree, status); 48 | } 49 | } 50 | 51 | static override get descriptor(): NodeDef { 52 | return { 53 | name: "IfElse", 54 | type: "Composite", 55 | children: 3, 56 | status: ["|success", "|failure", "|running"], 57 | desc: "条件执行", 58 | doc: ` 59 | + 必须有三个子节点 60 | + 第一个子节点为条件节点 61 | + 第二个子节点为条件为 \`success\` 时执行的节点 62 | + 第三个子节点为条件为 \`failure\` 时执行的节点, 63 | `, 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/behavior3/nodes/composites/parallel.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Stack } from "../../stack"; 4 | import { Tree } from "../../tree"; 5 | 6 | const EMPTY_STACK: Stack = new Stack(null!); 7 | 8 | /** 9 | * Parallel node executes all child nodes simultaneously. 10 | * 11 | * The execution flow is: 12 | * 1. Execute all child nodes in parallel 13 | * 2. If any child returns `running`, store its state and continue next tick 14 | * 3. Return `running` until all children complete 15 | * 4. When all children complete, return `success` 16 | * 17 | * Each child's execution state is tracked independently, allowing true parallel behavior. 18 | * The node only succeeds when all children have completed successfully. 19 | */ 20 | export class Parallel extends Node { 21 | override onTick(tree: Tree, status: Status): Status { 22 | const last: Stack[] = tree.resume(this) ?? []; 23 | const stack = tree.stack; 24 | const level = stack.length; 25 | const children = this.children; 26 | let count = 0; 27 | 28 | for (let i = 0; i < children.length; i++) { 29 | let childStack = last[i]; 30 | let status: Status | undefined; 31 | if (childStack === undefined) { 32 | status = children[i].tick(tree); 33 | } else if (childStack.length > 0) { 34 | childStack.move(stack, 0, childStack.length); 35 | while (stack.length > level) { 36 | status = stack.top()!.tick(tree); 37 | if (status === "running") { 38 | break; 39 | } 40 | } 41 | } else { 42 | status = "success"; 43 | } 44 | 45 | if (status === "running") { 46 | if (childStack === undefined) { 47 | childStack = new Stack(tree); 48 | } 49 | stack.move(childStack, level, stack.length - level); 50 | } else { 51 | count++; 52 | childStack = EMPTY_STACK; 53 | } 54 | 55 | last[i] = childStack; 56 | } 57 | 58 | if (count === children.length) { 59 | return "success"; 60 | } else { 61 | return tree.yield(this, last); 62 | } 63 | } 64 | 65 | static override get descriptor(): NodeDef { 66 | return { 67 | name: "Parallel", 68 | type: "Composite", 69 | status: ["success", "|running"], 70 | children: -1, 71 | desc: "并行执行", 72 | doc: ` 73 | + 并行执行所有子节点 74 | + 当有子节点返回 \`running\` 时,返回 \`running\` 状态 75 | + 执行完所有子节点后,返回 \`success\``, 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/behavior3/nodes/composites/race.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Stack } from "../../stack"; 4 | import { Tree } from "../../tree"; 5 | 6 | const EMPTY_STACK: Stack = new Stack(null!); 7 | 8 | /** 9 | * Race node executes all child nodes simultaneously until one succeeds or all fail. 10 | * 11 | * The execution flow is: 12 | * 1. Execute all child nodes in parallel 13 | * 2. If any child returns `success`, immediately return `success` and cancel other running children 14 | * 3. If any child returns `running`, store its state and continue next tick 15 | * 4. Return `failure` only when all children have failed 16 | * 17 | * Each child's execution state is tracked independently, allowing parallel execution. 18 | * The first child to succeed "wins the race" and causes the node to succeed. 19 | * The node only fails if all children fail. 20 | */ 21 | export class Race extends Node { 22 | override onTick(tree: Tree, status: Status): Status { 23 | const last: Stack[] = tree.resume(this) ?? []; 24 | const stack = tree.stack; 25 | const level = stack.length; 26 | const children = this.children; 27 | let count = 0; 28 | 29 | for (let i = 0; i < children.length; i++) { 30 | let childStack = last[i]; 31 | status = "failure"; 32 | if (childStack === undefined) { 33 | status = children[i].tick(tree); 34 | } else if (childStack.length > 0) { 35 | childStack.move(stack, 0, childStack.length); 36 | while (stack.length > level) { 37 | status = stack.top()!.tick(tree); 38 | if (status === "running") { 39 | break; 40 | } 41 | } 42 | } 43 | 44 | if (status === "running") { 45 | if (childStack === undefined) { 46 | childStack = new Stack(tree); 47 | } 48 | stack.move(childStack, level, stack.length - level); 49 | } else if (status === "success") { 50 | last.forEach((v) => v !== EMPTY_STACK && v.clear()); 51 | return "success"; 52 | } else { 53 | count++; 54 | childStack = EMPTY_STACK; 55 | } 56 | 57 | last[i] = childStack; 58 | } 59 | 60 | if (count === children.length) { 61 | return "failure"; 62 | } else { 63 | return tree.yield(this, last); 64 | } 65 | } 66 | 67 | static override get descriptor(): NodeDef { 68 | return { 69 | name: "Race", 70 | type: "Composite", 71 | status: ["|success", "&failure", "|running"], 72 | children: -1, 73 | desc: "竞争执行", 74 | doc: ` 75 | + 并行执行所有子节点 76 | + 当有子节点返回 \`success\` 时,立即返回 \`success\` 状态,并中断其他子节点 77 | + 如果所有子节点返回 \`failure\` 则返回 \`failure\``, 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/behavior3/nodes/composites/selector.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | /** 6 | * Selector composite node that executes children in order. 7 | * Returns: 8 | * - `success`: if any child returns `success` 9 | * - `failure`: when all children return `failure` 10 | * - `running`: if a child returns `running` 11 | * 12 | * Executes children sequentially until one succeeds or all fail. 13 | * If a child returns `running`, the selector will yield and resume 14 | * from that child on next tick. 15 | */ 16 | export class Selector extends Node { 17 | override onTick(tree: Tree, status: Status): Status { 18 | const last: number | undefined = tree.resume(this); 19 | const children = this.children; 20 | let i = 0; 21 | 22 | if (typeof last === "number") { 23 | if (status === "failure") { 24 | i = last + 1; 25 | } else if (status === "success") { 26 | return "success"; 27 | } else { 28 | this.throw(`unexpected status error`); 29 | } 30 | } 31 | 32 | for (; i < children.length; i++) { 33 | status = children[i].tick(tree); 34 | if (status === "success") { 35 | return "success"; 36 | } else if (status === "running") { 37 | return tree.yield(this, i); 38 | } 39 | } 40 | 41 | return "failure"; 42 | } 43 | 44 | static override get descriptor(): NodeDef { 45 | return { 46 | name: "Selector", 47 | type: "Composite", 48 | children: -1, 49 | desc: "选择执行", 50 | status: ["|success", "&failure", "|running"], 51 | doc: ` 52 | + 一直往下执行,直到有子节点返回 \`success\` 则返回 \`success\` 53 | + 若全部节点返回 \`failure\` 则返回 \`failure\``, 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/behavior3/nodes/composites/sequence.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | 4 | import { Tree } from "../../tree"; 5 | 6 | /** 7 | * Sequence composite node that executes children in order. 8 | * Returns: 9 | * - `success`: when all children return `success` 10 | * - `failure`: if any child returns `failure` 11 | * - `running`: if a child returns `running` 12 | * 13 | * Executes children sequentially until one fails or all succeed. 14 | * If a child returns `running`, the sequence will yield and resume 15 | * from that child on next tick. 16 | */ 17 | export class Sequence extends Node { 18 | override onTick(tree: Tree, status: Status): Status { 19 | const last: number | undefined = tree.resume(this); 20 | const children = this.children; 21 | let i = 0; 22 | 23 | if (typeof last === "number") { 24 | if (status === "success") { 25 | i = last + 1; 26 | } else if (status === "failure") { 27 | return "failure"; 28 | } else { 29 | this.throw(`unexpected status error: ${status}`); 30 | } 31 | } 32 | 33 | for (; i < children.length; i++) { 34 | status = children[i].tick(tree); 35 | if (status === "failure") { 36 | return "failure"; 37 | } else if (status === "running") { 38 | return tree.yield(this, i); 39 | } 40 | } 41 | 42 | return "success"; 43 | } 44 | 45 | static override get descriptor(): NodeDef { 46 | return { 47 | name: "Sequence", 48 | type: "Composite", 49 | children: -1, 50 | status: ["&success", "|failure", "|running"], 51 | desc: "顺序执行", 52 | doc: ` 53 | + 一直往下执行,只有当所有子节点都返回 \`success\`, 才返回 \`success\` 54 | + 若子节点返回 \`failure\`,则直接返回 \`failure\` 状态 55 | + 其余情况返回 \`running\` 状态 56 | `, 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/behavior3/nodes/composites/switch.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeData, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | /** 6 | * Switch node that executes cases in order until one matches. 7 | * 8 | * Each child must be a Case node with exactly 2 children: 9 | * 1. The condition node that determines if this case matches 10 | * 2. The action node to execute if condition returns `success` 11 | * 12 | * The execution flow is: 13 | * 1. Execute first child (condition) of each case in order 14 | * 2. If condition returns `success`, execute second child (action) 15 | * 3. Return the status of the action node 16 | * 4. If no case matches, return `failure` 17 | * 18 | * Similar to a switch statement, cases are evaluated in order 19 | * until a matching condition is found. Only the action of the 20 | * first matching case is executed. 21 | */ 22 | export class Switch extends Node { 23 | constructor(context: Context, cfg: NodeData) { 24 | super(context, cfg); 25 | 26 | this.children.forEach((v) => { 27 | if (v.name !== "Case") { 28 | this.throw(`only allow Case node`); 29 | } 30 | }); 31 | } 32 | 33 | override onTick(tree: Tree, status: Status): Status { 34 | let step: number | undefined = tree.resume(this); 35 | const children = this.children; 36 | 37 | if (typeof step === "number") { 38 | if (status === "running") { 39 | this.throw(`unexpected status error`); 40 | } 41 | if (step % 2 === 0) { 42 | if (status === "success") { 43 | step += 1; // do second node in case 44 | } else { 45 | step += 2; // next case 46 | } 47 | } else { 48 | return status; 49 | } 50 | } else { 51 | step = 0; 52 | } 53 | 54 | for (let i = step >>> 1; i < children.length; i++) { 55 | const [first, second] = children[i].children; 56 | if (step % 2 === 0) { 57 | status = first.tick(tree); 58 | if (status === "running") { 59 | return tree.yield(this, step); 60 | } else if (status === "success") { 61 | step = i * 2 + 1; 62 | } else { 63 | step = i * 2 + 2; 64 | } 65 | } 66 | if (step % 2 === 1) { 67 | status = second.tick(tree); 68 | if (status === "running") { 69 | return tree.yield(this, step); 70 | } 71 | return status; 72 | } 73 | } 74 | 75 | return "failure"; 76 | } 77 | 78 | static override get descriptor(): NodeDef { 79 | return { 80 | name: "Switch", 81 | type: "Composite", 82 | children: -1, 83 | status: ["|success", "|failure", "|running"], 84 | desc: "分支执行", 85 | doc: ` 86 | + 按顺序测试 \`Case\` 节点的判断条件(第一个子节点) 87 | + 若测试返回 \`success\` 则执行 \`Case\` 第二个子节点,并返回子节点的执行状态 88 | + 若没有判断为 \`success\` 的 \`Case\` 节点,则返回 \`failure\` 89 | `, 90 | }; 91 | } 92 | } 93 | 94 | export class Case extends Node { 95 | override onTick(tree: Tree, status: Status): Status { 96 | throw new Error("tick children by Switch"); 97 | } 98 | 99 | static override get descriptor(): NodeDef { 100 | return { 101 | name: "Case", 102 | type: "Composite", 103 | children: 2, 104 | status: ["&success", "|failure", "|running"], 105 | desc: "分支选择", 106 | doc: ` 107 | + 必须有两个子节点 108 | + 第一个子节点为判断条件 109 | + 第二个子节点为判断为 \`success\` 时执行的节点 110 | + 此节点不会真正意义的执行,而是交由 \`Switch\` 节点来执行 111 | `, 112 | }; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/behavior3/nodes/conditions/check.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeData, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Check extends Node { 6 | declare args: { readonly value: string }; 7 | 8 | constructor(context: Context, cfg: NodeData) { 9 | super(context, cfg); 10 | 11 | if (typeof this.args.value !== "string" || this.args.value.length === 0) { 12 | this.throw(`args.value is not a expr string`); 13 | } 14 | context.compileCode(this.args.value); 15 | } 16 | 17 | override onTick(tree: Tree, status: Status): Status { 18 | const value = tree.blackboard.eval(this.args.value); 19 | return value ? "success" : "failure"; 20 | } 21 | 22 | static override get descriptor(): NodeDef { 23 | return { 24 | name: "Check", 25 | type: "Condition", 26 | children: 0, 27 | status: ["success", "failure"], 28 | desc: "检查True或False", 29 | args: [{ name: "value", type: "expr", desc: "值" }], 30 | doc: ` 31 | + 做简单数值公式判定,返回 \`success\` 或 \`failure\` 32 | `, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/behavior3/nodes/conditions/includes.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Includes extends Node { 6 | declare input: [unknown, unknown[]]; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const [arr, element] = this.input; 10 | if (!Array.isArray(arr) || element === undefined || element === null) { 11 | return "failure"; 12 | } 13 | const index = arr.indexOf(element); 14 | return index >= 0 ? "success" : "failure"; 15 | } 16 | 17 | static override get descriptor(): NodeDef { 18 | return { 19 | name: "Includes", 20 | type: "Condition", 21 | children: 0, 22 | status: ["success", "failure"], 23 | desc: "判断元素是否在数组中", 24 | input: ["数组", "元素"], 25 | doc: ` 26 | + 若输入的元素不合法,返回 \`failure\` 27 | + 只有数组包含元素时返回 \`success\`,否则返回 \`failure\` 28 | `, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/behavior3/nodes/conditions/is-null.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class IsNull extends Node { 6 | declare input: [unknown]; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const [value] = this.input; 10 | if (value === undefined || value === null) { 11 | return "success"; 12 | } else { 13 | return "failure"; 14 | } 15 | } 16 | 17 | static override get descriptor(): NodeDef { 18 | return { 19 | name: "IsNull", 20 | type: "Condition", 21 | children: 0, 22 | status: ["success", "failure"], 23 | desc: "判断变量是否不存在", 24 | input: ["判断的变量"], 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/behavior3/nodes/conditions/not-null.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class NotNull extends Node { 6 | declare input: [unknown]; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const [value] = this.input; 10 | if (value === undefined || value === null) { 11 | return "failure"; 12 | } else { 13 | return "success"; 14 | } 15 | } 16 | 17 | static override get descriptor(): NodeDef { 18 | return { 19 | name: "NotNull", 20 | type: "Condition", 21 | children: 0, 22 | status: ["success", "failure"], 23 | desc: "判断变量是否存在", 24 | input: ["判断的变量"], 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/always-failure.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class AlwaysFailure extends Node { 6 | override onTick(tree: Tree, status: Status): Status { 7 | const isYield: boolean | undefined = tree.resume(this); 8 | if (typeof isYield === "boolean") { 9 | if (status === "running") { 10 | this.throw(`unexpected status error`); 11 | } 12 | return "failure"; 13 | } 14 | status = this.children[0].tick(tree); 15 | if (status === "running") { 16 | return tree.yield(this); 17 | } 18 | return "failure"; 19 | } 20 | 21 | static override get descriptor(): NodeDef { 22 | return { 23 | name: "AlwaysFailure", 24 | type: "Decorator", 25 | children: 1, 26 | status: ["failure", "|running"], 27 | desc: "始终返回失败", 28 | doc: ` 29 | + 只能有一个子节点,多个仅执行第一个 30 | + 当子节点返回 \`running\` 时,返回 \`running\` 31 | + 其它情况,不管子节点是否成功都返回 \`failure\` 32 | `, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/always-running.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class AlwaysRunning extends Node { 6 | override onTick(tree: Tree, status: Status): Status { 7 | this.children[0].tick(tree); 8 | return "running"; 9 | } 10 | 11 | static override get descriptor(): NodeDef { 12 | return { 13 | name: "AlwaysRunning", 14 | type: "Decorator", 15 | children: 1, 16 | status: ["running"], 17 | desc: "始终返回运行中状态", 18 | doc: ` 19 | + 只能有一个子节点,多个仅执行第一个 20 | + 始终返回 \`running\` 21 | `, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/always-success.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class AlwaysSuccess extends Node { 6 | override onTick(tree: Tree, status: Status): Status { 7 | const isYield: boolean | undefined = tree.resume(this); 8 | if (typeof isYield === "boolean") { 9 | if (status === "running") { 10 | this.throw(`unexpected status error`); 11 | } 12 | return "success"; 13 | } 14 | status = this.children[0].tick(tree); 15 | if (status === "running") { 16 | return tree.yield(this); 17 | } 18 | return "success"; 19 | } 20 | 21 | static override get descriptor(): NodeDef { 22 | return { 23 | name: "AlwaysSuccess", 24 | type: "Decorator", 25 | children: 1, 26 | status: ["success", "|running"], 27 | desc: "始终返回成功", 28 | doc: ` 29 | + 只能有一个子节点,多个仅执行第一个 30 | + 当子节点返回 \`running\` 时,返回 \`running\` 31 | + 其它情况,不管子节点是否成功都返回 \`success\` 32 | `, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/assert.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Assert extends Node { 6 | declare args: { readonly message: string }; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const args = this.args; 10 | const isYield: boolean | undefined = tree.resume(this); 11 | if (typeof isYield === "boolean") { 12 | if (status === "running") { 13 | this.throw(`unexpected status error`); 14 | } 15 | 16 | if (status === "success") { 17 | return "success"; 18 | } else { 19 | this.throw(args.message); 20 | } 21 | } 22 | 23 | status = this.children[0].tick(tree); 24 | if (status === "success") { 25 | return "success"; 26 | } else if (status === "running") { 27 | return tree.yield(this); 28 | } else { 29 | this.throw(args.message); 30 | } 31 | 32 | return "success"; 33 | } 34 | 35 | static override get descriptor(): NodeDef { 36 | return { 37 | name: "Assert", 38 | type: "Decorator", 39 | children: 1, 40 | status: ["success"], 41 | desc: "断言", 42 | args: [ 43 | { 44 | name: "message", 45 | type: "string", 46 | desc: "消息", 47 | }, 48 | ], 49 | doc: ` 50 | + 只能有一个子节点,多个仅执行第一个 51 | + 当子节点返回 \`failure\` 时,抛出异常 52 | + 其余情况返回子节点的执行状态 53 | `, 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/delay.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Delay extends Node { 6 | declare args: { 7 | readonly delay: number; 8 | readonly cacheVars?: Readonly; 9 | }; 10 | 11 | override onTick(tree: Tree, status: Status): Status { 12 | const args = this.args; 13 | const delay = this._checkOneof(0, args.delay, 0); 14 | const blackboard = tree.blackboard; 15 | const keys = args.cacheVars ?? []; 16 | const cacheArgs: unknown[] = keys.map((key) => blackboard.get(key)); 17 | 18 | tree.context.delay( 19 | delay, 20 | () => { 21 | const cacheOldArgs: unknown[] = keys.map((key) => blackboard.get(key)); 22 | keys.forEach((key, i) => blackboard.set(key, cacheArgs[i])); 23 | const level = tree.stack.length; 24 | status = this.children[0].tick(tree); 25 | if (status === "running") { 26 | tree.stack.popTo(level); 27 | } 28 | keys.forEach((key, i) => blackboard.set(key, cacheOldArgs[i])); 29 | }, 30 | tree 31 | ); 32 | return "success"; 33 | } 34 | 35 | static override get descriptor(): NodeDef { 36 | return { 37 | name: "Delay", 38 | type: "Decorator", 39 | children: 1, 40 | status: ["success"], 41 | desc: "延时执行子节点", 42 | input: ["延时时间?"], 43 | args: [ 44 | { 45 | name: "delay", 46 | type: "float?", 47 | desc: "延时时间", 48 | oneof: "延时时间", 49 | }, 50 | { 51 | name: "cacheVars", 52 | type: "string[]?", 53 | desc: "暂存环境变量", 54 | }, 55 | ], 56 | doc: ` 57 | + 当延时触发时,执行第一个子节点,多个仅执行第一个 58 | + 如果子节点返回 \`running\`,会中断执行并清理执行栈`, 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/filter.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Filter extends Node { 6 | declare input: [unknown[]]; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const [arr] = this.input; 10 | if (!(arr instanceof Array) || arr.length === 0) { 11 | return "failure"; 12 | } 13 | 14 | let last: [number, unknown[]] | undefined = tree.resume(this); 15 | let i; 16 | let newArr: unknown[]; 17 | if (last instanceof Array) { 18 | [i, newArr] = last; 19 | if (status === "running") { 20 | this.throw(`unexpected status error`); 21 | } else if (status === "success") { 22 | newArr.push(arr[i]); 23 | } 24 | i++; 25 | } else { 26 | i = 0; 27 | newArr = []; 28 | } 29 | 30 | const filter = this.children[0]; 31 | 32 | for (i = 0; i < arr.length; i++) { 33 | tree.blackboard.set(this.cfg.output[0], arr[i]); 34 | status = filter.tick(tree); 35 | if (status === "running") { 36 | if (last instanceof Array) { 37 | last[0] = i; 38 | last[1] = newArr; 39 | } else { 40 | last = [i, newArr]; 41 | } 42 | return tree.yield(this, last); 43 | } else if (status === "success") { 44 | newArr.push(arr[i]); 45 | } 46 | } 47 | 48 | this.output.push(undefined, newArr); 49 | 50 | return newArr.length === 0 ? "failure" : "success"; 51 | } 52 | 53 | static override get descriptor(): NodeDef { 54 | return { 55 | name: "Filter", 56 | type: "Decorator", 57 | children: 1, 58 | status: ["success", "failure", "|running"], 59 | desc: "返回满足条件的元素", 60 | input: ["输入数组"], 61 | output: ["变量", "新数组"], 62 | doc: ` 63 | + 只能有一个子节点,多个仅执行第一个 64 | + 遍历输入数组,将当前元素写入\`变量\`,满足条件的元素放入新数组 65 | + 只有当新数组不为空时,才返回 \`success\` 66 | `, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/foreach.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Foreach extends Node { 6 | declare input: [unknown[]]; 7 | declare output: [unknown, number?]; 8 | 9 | override onTick(tree: Tree, status: Status): Status { 10 | const [arr] = this.input; 11 | const [varname, idx] = this.cfg.output; 12 | let i: number | undefined = tree.resume(this); 13 | if (i !== undefined) { 14 | if (status === "running") { 15 | this.throw(`unexpected status error`); 16 | } else if (status === "failure") { 17 | return "failure"; 18 | } 19 | i++; 20 | } else { 21 | i = 0; 22 | } 23 | 24 | for (; i < arr.length; i++) { 25 | tree.blackboard.set(varname, arr[i]); 26 | if (idx) { 27 | tree.blackboard.set(idx, i); 28 | } 29 | status = this.children[0].tick(tree); 30 | if (status === "running") { 31 | return tree.yield(this, i); 32 | } else if (status === "failure") { 33 | return "failure"; 34 | } 35 | } 36 | 37 | return "success"; 38 | } 39 | 40 | static override get descriptor(): NodeDef { 41 | return { 42 | name: "ForEach", 43 | type: "Decorator", 44 | children: 1, 45 | status: ["success", "|running", "|failure"], 46 | desc: "遍历数组", 47 | input: ["数组"], 48 | output: ["变量", "索引?"], 49 | doc: ` 50 | + 只能有一个子节点,多个仅执行第一个 51 | + 遍历输入数组,将当前元素写入\`变量\` 52 | + 当子节点返回 \`failure\` 时,退出遍历并返回 \`failure\` 状态 53 | + 执行完所有子节点后,返回 \`success\``, 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/invert.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Invert extends Node { 6 | override onTick(tree: Tree, status: Status): Status { 7 | const isYield: boolean | undefined = tree.resume(this); 8 | if (typeof isYield === "boolean") { 9 | if (status === "running") { 10 | this.throw(`unexpected status error`); 11 | } 12 | return this._invert(status); 13 | } 14 | status = this.children[0].tick(tree); 15 | if (status === "running") { 16 | return tree.yield(this); 17 | } 18 | return this._invert(status); 19 | } 20 | 21 | private _invert(status: Status): Status { 22 | return status === "failure" ? "success" : "failure"; 23 | } 24 | 25 | static override get descriptor(): NodeDef { 26 | return { 27 | name: "Invert", 28 | type: "Decorator", 29 | children: 1, 30 | status: ["!success", "!failure", "|running"], 31 | desc: "反转子节点运行结果", 32 | doc: ` 33 | + 只能有一个子节点,多个仅执行第一个 34 | + 当子节点返回 \`success\` 时返回 \`failure\` 35 | + 当子节点返回 \`failure\` 时返回 \`success\` 36 | `, 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/listen.ts: -------------------------------------------------------------------------------- 1 | import type { Context, TargetType } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree, TreeEvent } from "../../tree"; 4 | 5 | const builtinEventOptions = [ 6 | { name: "行为树被中断", value: TreeEvent.INTERRUPTED }, 7 | { name: "行为树开始执行前", value: TreeEvent.BEFORE_TICKED }, 8 | { name: "行为树执行完成后", value: TreeEvent.AFTER_TICKED }, 9 | { name: "行为树执行成功后", value: TreeEvent.TICKED_SUCCESS }, 10 | { name: "行为树执行失败后", value: TreeEvent.TICKED_FAILURE }, 11 | { name: "行为树被清理", value: TreeEvent.CLEANED }, 12 | ]; 13 | 14 | export class Listen extends Node { 15 | declare args: { readonly event: string }; 16 | declare input: [TargetType | TargetType[] | undefined]; 17 | declare output: [ 18 | target?: string, 19 | arg0?: string, 20 | arg1?: string 21 | // argN?:string 22 | ]; 23 | 24 | protected _isBuiltinEvent(event: string): boolean { 25 | return !!builtinEventOptions.find((e) => e.value === event); 26 | } 27 | 28 | protected _processOutput( 29 | tree: Tree, 30 | eventTarget?: TargetType, 31 | ...eventArgs: unknown[] 32 | ) { 33 | const [eventTargetKey] = this.cfg.output; 34 | if (eventTargetKey) { 35 | tree.blackboard.set(eventTargetKey, eventTarget); 36 | } 37 | for (let i = 1; i < this.cfg.output.length; i++) { 38 | const key = this.cfg.output[i]; 39 | if (key) { 40 | tree.blackboard.set(key, eventArgs[i - 1]); 41 | } 42 | } 43 | } 44 | 45 | override onTick(tree: Tree, status: Status): Status { 46 | let [target] = this.input; 47 | const args = this.args; 48 | 49 | if (this._isBuiltinEvent(args.event)) { 50 | if (target !== undefined) { 51 | this.warn(`invalid target ${target} for builtin event ${args.event}`); 52 | } 53 | target = tree as TargetType; 54 | } 55 | 56 | const callback = (eventTarget?: TargetType) => { 57 | return (...eventArgs: unknown[]) => { 58 | this._processOutput(tree, eventTarget, ...eventArgs); 59 | const level = tree.stack.length; 60 | status = this.children[0].tick(tree); 61 | if (status === "running") { 62 | tree.stack.popTo(level); 63 | } 64 | }; 65 | }; 66 | if (target !== undefined) { 67 | if (target instanceof Array) { 68 | target.forEach((v) => { 69 | tree.context.on(args.event, v, callback(v), tree); 70 | }); 71 | } else { 72 | tree.context.on(args.event, target, callback(target), tree); 73 | } 74 | } else { 75 | tree.context.on(args.event, callback(undefined), tree); 76 | } 77 | 78 | return "success"; 79 | } 80 | 81 | static override get descriptor(): NodeDef { 82 | return { 83 | name: "Listen", 84 | type: "Decorator", 85 | children: 1, 86 | status: ["success"], 87 | desc: "侦听事件", 88 | input: ["目标对象?"], 89 | output: ["事件目标?", "事件参数..."], 90 | args: [ 91 | { 92 | name: "event", 93 | type: "string", 94 | desc: "事件", 95 | options: builtinEventOptions.slice(), 96 | }, 97 | ], 98 | doc: ` 99 | + 当事件触发时,执行第一个子节点,多个仅执行第一个 100 | + 如果子节点返回 \`running\`,会中断执行并清理执行栈`, 101 | }; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/once.ts: -------------------------------------------------------------------------------- 1 | import { Blackboard } from "../../blackboard"; 2 | import type { Context } from "../../context"; 3 | import { Node, NodeData, NodeDef, Status } from "../../node"; 4 | import { Tree } from "../../tree"; 5 | 6 | export class Once extends Node { 7 | private _onceKey!: string; 8 | 9 | constructor(context: Context, cfg: NodeData) { 10 | super(context, cfg); 11 | 12 | this._onceKey = Blackboard.makePrivateVar(this, "ONCE"); 13 | } 14 | 15 | override onTick(tree: Tree, status: Status): Status { 16 | const onceKey = this._onceKey; 17 | if (tree.blackboard.get(onceKey) === true) { 18 | return "failure"; 19 | } 20 | 21 | const isYield: boolean | undefined = tree.resume(this); 22 | if (typeof isYield === "boolean") { 23 | if (status === "running") { 24 | this.throw(`unexpected status error`); 25 | } 26 | tree.blackboard.set(onceKey, true); 27 | return "success"; 28 | } 29 | 30 | status = this.children[0].tick(tree); 31 | if (status === "running") { 32 | return tree.yield(this); 33 | } 34 | tree.blackboard.set(onceKey, true); 35 | return "success"; 36 | } 37 | 38 | static override get descriptor(): NodeDef { 39 | return { 40 | name: "Once", 41 | type: "Decorator", 42 | children: 1, 43 | status: ["success", "failure", "|running"], 44 | desc: "只执行一次", 45 | doc: ` 46 | + 只能有一个子节点,多个仅执行第一个 47 | + 第一次执行完全部子节点时返回 \`success\`,之后永远返回 \`failure\``, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/repeat.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class Repeat extends Node { 6 | declare args: { readonly count: number }; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const count = this._checkOneof(0, this.args.count, Number.MAX_SAFE_INTEGER); 10 | let i: number | undefined = tree.resume(this); 11 | 12 | if (i !== undefined) { 13 | if (status === "running") { 14 | this.throw(`unexpected status error`); 15 | } else if (status === "failure") { 16 | return "failure"; 17 | } 18 | i++; 19 | } else { 20 | i = 0; 21 | } 22 | 23 | for (; i < count; i++) { 24 | status = this.children[0].tick(tree); 25 | if (status === "running") { 26 | return tree.yield(this, i); 27 | } else if (status === "failure") { 28 | return "failure"; 29 | } 30 | } 31 | return "success"; 32 | } 33 | 34 | static override get descriptor(): NodeDef { 35 | return { 36 | name: "Repeat", 37 | type: "Decorator", 38 | children: 1, 39 | status: ["success", "|running", "|failure"], 40 | desc: "循环执行", 41 | input: ["循环次数?"], 42 | args: [ 43 | { 44 | name: "count", 45 | type: "int?", 46 | desc: "循环次数", 47 | oneof: "循环次数", 48 | }, 49 | ], 50 | doc: ` 51 | + 只能有一个子节点,多个仅执行第一个 52 | + 当子节点返回 \`failure\` 时,退出遍历并返回 \`failure\` 状态 53 | + 执行完所有子节点后,返回 \`success\` 54 | `, 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/retry-until-failure.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class RetryUntilFailure extends Node { 6 | declare args: { readonly count?: number }; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const maxTryTimes = this._checkOneof(0, this.args.count, Number.MAX_SAFE_INTEGER); 10 | let count: number | undefined = tree.resume(this); 11 | 12 | if (typeof count === "number") { 13 | if (status === "failure") { 14 | return "success"; 15 | } else if (count >= maxTryTimes) { 16 | return "failure"; 17 | } else { 18 | count++; 19 | } 20 | } else { 21 | count = 1; 22 | } 23 | 24 | status = this.children[0].tick(tree); 25 | if (status === "failure") { 26 | return "success"; 27 | } else { 28 | return tree.yield(this, count); 29 | } 30 | } 31 | 32 | static override get descriptor(): NodeDef { 33 | return { 34 | name: "RetryUntilFailure", 35 | type: "Decorator", 36 | children: 1, 37 | status: ["!success", "failure", "|running"], 38 | desc: "一直尝试直到子节点返回失败", 39 | input: ["最大尝试次数?"], 40 | args: [ 41 | { 42 | name: "count", 43 | type: "int?", 44 | desc: "最大尝试次数", 45 | }, 46 | ], 47 | doc: ` 48 | + 只能有一个子节点,多个仅执行第一个 49 | + 只有当子节点返回 \`failure\` 时,才返回 \`success\`,其它情况返回 \`running\` 状态 50 | + 如果设定了尝试次数,超过指定次数则返回 \`failure\``, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/retry-until-success.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Tree } from "../../tree"; 4 | 5 | export class RetryUntilSuccess extends Node { 6 | declare args: { readonly count?: number }; 7 | 8 | override onTick(tree: Tree, status: Status): Status { 9 | const maxTryTimes = this._checkOneof(0, this.args.count, Number.MAX_SAFE_INTEGER); 10 | let count: number | undefined = tree.resume(this); 11 | 12 | if (typeof count === "number") { 13 | if (status === "success") { 14 | return "success"; 15 | } else if (count >= maxTryTimes) { 16 | return "failure"; 17 | } else { 18 | count++; 19 | } 20 | } else { 21 | count = 1; 22 | } 23 | 24 | status = this.children[0].tick(tree); 25 | if (status === "success") { 26 | return "success"; 27 | } else { 28 | return tree.yield(this, count); 29 | } 30 | } 31 | 32 | static override get descriptor(): NodeDef { 33 | return { 34 | name: "RetryUntilSuccess", 35 | type: "Decorator", 36 | children: 1, 37 | status: ["|success", "failure", "|running"], 38 | desc: "一直尝试直到子节点返回成功", 39 | input: ["最大尝试次数?"], 40 | args: [ 41 | { 42 | name: "count", 43 | type: "int?", 44 | desc: "最大尝试次数", 45 | }, 46 | ], 47 | doc: ` 48 | + 只能有一个子节点,多个仅执行第一个 49 | + 只有当子节点返回 \`success\` 时,才返回 \`success\`,其它情况返回 \`running\` 状态 50 | + 如果设定了尝试次数,超过指定次数则返回 \`failure\``, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/behavior3/nodes/decorators/timeout.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context"; 2 | import { Node, NodeDef, Status } from "../../node"; 3 | import { Stack } from "../../stack"; 4 | import { Tree } from "../../tree"; 5 | 6 | interface NodeYield { 7 | stack: Stack; 8 | expired: number; 9 | } 10 | 11 | export class Timeout extends Node { 12 | declare args: { readonly time?: number }; 13 | 14 | override onTick(tree: Tree, status: Status): Status { 15 | const { stack, context } = tree; 16 | const level = stack.length; 17 | let last: NodeYield | undefined = tree.resume(this); 18 | status = "failure"; 19 | if (last === undefined) { 20 | status = this.children[0].tick(tree); 21 | } else if (context.time >= last.expired) { 22 | last.stack.clear(); 23 | return "failure"; 24 | } else { 25 | last.stack.move(stack, 0, last.stack.length); 26 | while (stack.length > level) { 27 | const child = stack.top()!; 28 | status = child.tick(tree); 29 | if (status === "running") { 30 | break; 31 | } 32 | } 33 | } 34 | 35 | if (status === "running") { 36 | if (last === undefined) { 37 | const time = this._checkOneof(0, this.args.time, 0); 38 | last = { 39 | stack: new Stack(tree), 40 | expired: context.time + time, 41 | }; 42 | } 43 | stack.move(last.stack, level, stack.length - level); 44 | return tree.yield(this, last); 45 | } else { 46 | return status; 47 | } 48 | } 49 | 50 | static override get descriptor(): NodeDef { 51 | return { 52 | name: "Timeout", 53 | type: "Decorator", 54 | children: 1, 55 | status: ["|success", "|running", "failure"], 56 | desc: "超时", 57 | input: ["超时时间?"], 58 | args: [ 59 | { 60 | name: "time", 61 | type: "float?", 62 | desc: "超时时间", 63 | oneof: "超时时间", 64 | }, 65 | ], 66 | doc: ` 67 | + 只能有一个子节点,多个仅执行第一个 68 | + 当子节点执行超时或返回 \`failure\` 时,返回 \`failure\` 69 | + 其余情况返回子节点的执行状态 70 | `, 71 | }; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/behavior3/stack.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "./context"; 2 | import type { Node } from "./node"; 3 | import { Tree } from "./tree"; 4 | 5 | export class Stack { 6 | private _nodes: Node[] = []; 7 | private _tree: Tree; 8 | 9 | constructor(tree: Tree) { 10 | this._tree = tree; 11 | } 12 | 13 | get length() { 14 | return this._nodes.length; 15 | } 16 | 17 | indexOf(node: Node) { 18 | return this._nodes.indexOf(node); 19 | } 20 | 21 | top(): Node | undefined { 22 | const nodes = this._nodes; 23 | return nodes[nodes.length - 1]; 24 | } 25 | 26 | push(node: Node) { 27 | this._nodes.push(node); 28 | } 29 | 30 | pop(): Node | undefined { 31 | const node = this._nodes.pop(); 32 | if (node) { 33 | this._tree.blackboard.set(node.__yield, undefined); 34 | } 35 | return node; 36 | } 37 | 38 | popTo(index: number) { 39 | while (this._nodes.length > index) { 40 | this.pop(); 41 | } 42 | } 43 | 44 | move(dest: Stack, start: number, count: number) { 45 | dest._nodes.push(...this._nodes.splice(start, count)); 46 | } 47 | 48 | clear() { 49 | this.popTo(0); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/behavior3/tree.ts: -------------------------------------------------------------------------------- 1 | import { Blackboard } from "./blackboard"; 2 | import { Context } from "./context"; 3 | import { Node, NodeData, Status } from "./node"; 4 | import { Stack } from "./stack"; 5 | 6 | export interface TreeData { 7 | readonly name: string; 8 | readonly desc: string; 9 | readonly root: NodeData; 10 | } 11 | 12 | export const enum TreeEvent { 13 | CLEANED = "treeCleaned", 14 | INTERRUPTED = "treeInterrupted", 15 | BEFORE_TICKED = "treeBeforeTicked", 16 | AFTER_TICKED = "treeAfterTicked", 17 | TICKED_SUCCESS = "treeTickedSuccess", 18 | TICKED_FAILURE = "treeTickedFailure", 19 | } 20 | 21 | export type TreeStatus = Status | "interrupted"; 22 | 23 | let treeId = 0; 24 | 25 | export class Tree { 26 | readonly context: C; 27 | readonly path: string; 28 | readonly blackboard: Blackboard; 29 | readonly stack: Stack; 30 | readonly id: number = ++treeId; 31 | 32 | debug: boolean = false; 33 | 34 | protected _ticking: boolean = false; 35 | protected _owner: Owner; 36 | 37 | /** @private */ 38 | __lastStatus: Status = "success"; 39 | 40 | /** @private */ 41 | __interrupted: boolean = false; 42 | 43 | private _root?: Node; 44 | private _status: TreeStatus = "success"; 45 | 46 | constructor(context: C, owner: Owner, path: string) { 47 | this.context = context; 48 | this.path = path; 49 | this.blackboard = new Blackboard(this); 50 | this.stack = new Stack(this); 51 | this.context.loadTree(this.path); 52 | this._owner = owner; 53 | } 54 | 55 | get owner() { 56 | return this._owner; 57 | } 58 | 59 | get root(): Node | undefined { 60 | return (this._root ||= this.context.trees[this.path]); 61 | } 62 | 63 | get ready() { 64 | return !!this.root; 65 | } 66 | 67 | get status() { 68 | return this._status; 69 | } 70 | 71 | get ticking() { 72 | return this._ticking; 73 | } 74 | 75 | clear() { 76 | // force run clear 77 | const interrupted = this.__interrupted; 78 | this.__interrupted = false; 79 | this.context.dispatch(TreeEvent.CLEANED, this); 80 | this.__interrupted = interrupted; 81 | 82 | this.interrupt(); 83 | this.debug = false; 84 | this.__interrupted = false; 85 | this._status = "success"; 86 | this.stack.clear(); 87 | this.blackboard.clear(); 88 | this.context.offAll(this); 89 | } 90 | 91 | interrupt() { 92 | if (this._status === "running" || this._ticking) { 93 | this.context.dispatch(TreeEvent.INTERRUPTED, this); 94 | this.__interrupted = true; 95 | if (!this._ticking) { 96 | this._doInterrupt(); 97 | } 98 | } 99 | } 100 | 101 | yield(node: Node, value?: V): Status { 102 | this.blackboard.set(node.__yield, value ?? true); 103 | return "running"; 104 | } 105 | 106 | resume(node: Node): V | undefined { 107 | return this.blackboard.get(node.__yield) as V; 108 | } 109 | 110 | tick(): TreeStatus { 111 | const { context, stack, root } = this; 112 | 113 | if (!root) { 114 | return "failure"; 115 | } 116 | 117 | if (this.debug) { 118 | console.debug(`---------------- debug ai: ${this.path} --------------------`); 119 | } 120 | 121 | if (this._ticking) { 122 | const node = stack.top(); 123 | throw new Error(`tree '${this.path}' already ticking: ${node?.name}#${node?.id}`); 124 | } 125 | 126 | this._ticking = true; 127 | 128 | if (stack.length > 0) { 129 | let node = stack.top(); 130 | while (node) { 131 | this._status = node.tick(this); 132 | if (this._status === "running") { 133 | break; 134 | } else { 135 | node = stack.top(); 136 | } 137 | } 138 | } else { 139 | context.dispatch(TreeEvent.BEFORE_TICKED, this); 140 | this._status = root.tick(this); 141 | } 142 | 143 | if (this._status === "success") { 144 | context.dispatch(TreeEvent.AFTER_TICKED, this); 145 | context.dispatch(TreeEvent.TICKED_SUCCESS, this); 146 | } else if (this._status === "failure") { 147 | context.dispatch(TreeEvent.AFTER_TICKED, this); 148 | context.dispatch(TreeEvent.TICKED_FAILURE, this); 149 | } 150 | 151 | if (this.__interrupted) { 152 | this._doInterrupt(); 153 | } 154 | 155 | this._ticking = false; 156 | 157 | return this._status; 158 | } 159 | 160 | private _doInterrupt() { 161 | const values = this.blackboard.values; 162 | this._status = "interrupted"; 163 | this.stack.clear(); 164 | for (const key in values) { 165 | if (Blackboard.isTempVar(key)) { 166 | delete values[key]; 167 | } 168 | } 169 | this.__interrupted = false; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /test/main.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | import * as fs from "node:fs"; 3 | import { Tree } from "../src/behavior3"; 4 | import { ExpressionEvaluator } from "../src/behavior3/evaluator"; 5 | import { Role, RoleContext } from "./role"; 6 | 7 | assert(new ExpressionEvaluator("(idx + dir +8) % 8").dryRun() === true); 8 | assert(new ExpressionEvaluator("p.x > 3 ? -1.0 : a.x").dryRun() === true); 9 | assert(new ExpressionEvaluator("p.x > 3 ? -1.0 : a.2").dryRun() === false); 10 | assert(new ExpressionEvaluator("x || 2.0").dryRun() === true); 11 | assert(new ExpressionEvaluator("x.helo && 2.0").dryRun() === true); 12 | assert(new ExpressionEvaluator("x.helo && 2.0 |").dryRun() !== true); 13 | assert(new ExpressionEvaluator("x[1] + -2.0").dryRun() === true); 14 | assert(new ExpressionEvaluator("x[1].x + x.y").dryRun() === true); 15 | assert(new ExpressionEvaluator("a.x > -1.2 ? -1.2 : (b.x + 1.2)").dryRun() === true); 16 | assert(new ExpressionEvaluator("x - 1").dryRun() === true); 17 | assert(new ExpressionEvaluator("x > 1").dryRun() === true); 18 | assert( 19 | new ExpressionEvaluator("arr[0].x + a.y").evaluate({ 20 | arr: [{ x: 1 }], 21 | a: { y: 2 }, 22 | }) === 3 23 | ); 24 | 25 | const context = new RoleContext(); 26 | 27 | fs.writeFileSync("example/node-config.b3-setting", context.exportNodeDefs()); 28 | 29 | context.avators.push({ x: 200, y: 0, hp: 100 }); 30 | context.avators.push({ x: 0, y: 0, hp: 100 }); 31 | 32 | const createTree = (owner: Role, treePath: string) => { 33 | context.loadTree(treePath); 34 | return new Tree(context, owner, treePath); 35 | }; 36 | 37 | console.log("====================test hero============================="); 38 | // -- test hero 39 | const heroAi = createTree(context.avators[1], "./example/hero.json"); 40 | heroAi.tick(); 41 | heroAi.tick(); 42 | heroAi.tick(); 43 | heroAi.tick(); 44 | heroAi.tick(); 45 | heroAi.tick(); 46 | 47 | //后摇; 48 | heroAi.tick(); 49 | heroAi.interrupt(); 50 | heroAi.tick(); 51 | context.time = 20; 52 | heroAi.tick(); 53 | 54 | console.log("====================test monster============================="); 55 | const monsterAi = createTree(context.avators[0], "./example/monster.json"); 56 | monsterAi.owner.hp = 100; 57 | monsterAi.tick(); 58 | 59 | monsterAi.owner.hp = 20; 60 | monsterAi.tick(); 61 | monsterAi.context.time = 40; 62 | monsterAi.tick(); 63 | monsterAi.tick(); 64 | 65 | console.log("run end"); 66 | 67 | console.log("====================test api============================="); 68 | const testTree = ( 69 | name: string, 70 | onTick?: (i: number, runner: Tree) => boolean 71 | ) => { 72 | const tree = createTree({ hp: 100, x: 0, y: 0 }, `./example/${name}.json`); 73 | let i = 0; 74 | while (i < 100) { 75 | context.update(1); 76 | tree.tick(); 77 | if (onTick) { 78 | if (!onTick(i, tree)) { 79 | break; 80 | } 81 | } else if (tree.status === "success") { 82 | break; 83 | } 84 | i++; 85 | } 86 | assert(tree.status === "success", `tree ${name} failed`); 87 | console.log(""); 88 | }; 89 | testTree("test-sequence"); 90 | testTree("test-parallel"); 91 | testTree("test-repeat-until-success"); 92 | testTree("test-repeat-until-failure"); 93 | testTree("test-timeout"); 94 | testTree("test-once"); 95 | testTree("test-listen", (i, tree) => { 96 | if (i === 0) { 97 | context.dispatch("hello", undefined, "world"); 98 | context.dispatch("testOff"); 99 | context.off("testOff", tree); 100 | return true; 101 | } else if (i === 1) { 102 | context.dispatch("hello", undefined, "world"); 103 | context.dispatch("testOff"); 104 | context.offAll(tree); 105 | return true; 106 | } else { 107 | context.dispatch("hello", undefined, "world"); 108 | context.dispatch("testOff"); 109 | return false; 110 | } 111 | }); 112 | testTree("test-switch-case"); 113 | testTree("test-race"); 114 | console.log("====================test api end======================"); 115 | -------------------------------------------------------------------------------- /test/nodes/attack.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef, Status, Tree } from "../../src/behavior3"; 2 | import { Role, RoleContext } from "../role"; 3 | 4 | export class Attack extends Node { 5 | declare input: [Role | undefined]; 6 | 7 | override onTick(tree: Tree): Status { 8 | const [enemy] = this.input; 9 | if (!enemy) { 10 | return "failure"; 11 | } 12 | console.log("Do Attack"); 13 | enemy.hp -= 100; 14 | tree.blackboard.set("ATTACKING", true); 15 | return "success"; 16 | } 17 | 18 | static override get descriptor(): Readonly { 19 | return { 20 | name: "Attack", 21 | type: "Action", 22 | desc: "攻击", 23 | input: ["目标敌人"], 24 | args: [], 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/nodes/find-enemy.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef, Status, Tree } from "../../src/behavior3"; 2 | import { Role, RoleContext } from "../role"; 3 | 4 | export class FindEnemy extends Node { 5 | declare args: { 6 | w: number; 7 | h: number; 8 | count?: number; 9 | }; 10 | 11 | override onTick(tree: Tree): Status { 12 | const owner = tree.owner; 13 | const args = this.args; 14 | const x = owner.x; 15 | const y = owner.y; 16 | const w = args.w; 17 | const h = args.h; 18 | const list = tree.context.find((role: Role) => { 19 | if (role === owner) { 20 | return false; 21 | } 22 | const tx = role.x; 23 | const ty = role.y; 24 | return Math.abs(x - tx) <= w && Math.abs(y - ty) <= h; 25 | }, args.count ?? -1); 26 | if (list.length) { 27 | this.output.push(...list); 28 | return "success"; 29 | } else { 30 | return "failure"; 31 | } 32 | } 33 | 34 | static override get descriptor(): Readonly { 35 | return { 36 | name: "FindEnemy", 37 | type: "Action", 38 | desc: "寻找敌人", 39 | output: ["敌人"], 40 | args: [ 41 | { name: "w", type: "int", desc: "宽度" }, 42 | { name: "h", type: "int", desc: "高度" }, 43 | { name: "count", type: "int?", desc: "数量" }, 44 | ], 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/nodes/get-hp.ts: -------------------------------------------------------------------------------- 1 | import { Context, Node, NodeDef, Status, Tree } from "../../src/behavior3"; 2 | import { Role } from "../role"; 3 | 4 | export class GetHp extends Node { 5 | override onTick(tree: Tree): Status { 6 | const owner = tree.owner; 7 | this.output.push(owner.hp); 8 | return "success"; 9 | } 10 | 11 | static override get descriptor(): Readonly { 12 | return { 13 | name: "GetHp", 14 | type: "Action", 15 | desc: "获取生命值", 16 | output: ["hp"], 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/nodes/idle.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef, Status, Tree } from "../../src/behavior3"; 2 | import { RoleContext } from "../role"; 3 | 4 | export class Idle extends Node { 5 | override onTick(tree: Tree): Status { 6 | console.log("Do Idle"); 7 | return "success"; 8 | } 9 | 10 | static override get descriptor(): Readonly { 11 | return { 12 | name: "Idle", 13 | type: "Action", 14 | desc: "待机", 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/nodes/is-status.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from "../../src/behavior3"; 2 | import { Node, NodeDef, Status } from "../../src/behavior3/node"; 3 | import { Role, RoleContext } from "../role"; 4 | 5 | export class IsStatus extends Node { 6 | declare args: { 7 | readonly status: Status; 8 | }; 9 | 10 | override onTick(tree: Tree): Status { 11 | const owner = tree.owner; 12 | const args = this.args; 13 | const level = tree.stack.length; 14 | const status = this.children[0].tick(tree); 15 | if (status === "running") { 16 | tree.stack.popTo(level); 17 | } 18 | return status === args.status ? "success" : "failure"; 19 | } 20 | 21 | static override get descriptor(): Readonly { 22 | return { 23 | name: "IsStatus", 24 | type: "Condition", 25 | children: 1, 26 | status: ["success", "failure"], 27 | desc: "检查子节点状态", 28 | args: [ 29 | { 30 | name: "status", 31 | type: "string", 32 | desc: "执行状态", 33 | options: [ 34 | { name: "成功", value: "success" }, 35 | { name: "失败", value: "failure" }, 36 | { name: "运行中", value: "running" }, 37 | ], 38 | }, 39 | ], 40 | doc: ` 41 | + 只能有一个子节点,多个仅执行第一个 42 | + 只有当子节点的执行状态与指定状态相同时才返回 \`success\`,其余返回失败 43 | + 若子节点返回 \`running\` 状态,将中断子节点并清理子节点的执行栈`, 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/nodes/move-to-pos.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef, Status, Tree } from "../../src/behavior3"; 2 | import { Role, RoleContext } from "../role"; 3 | 4 | export class MoveToPos extends Node { 5 | declare args: { 6 | readonly x: number; 7 | readonly y: number; 8 | }; 9 | 10 | override onTick(tree: Tree): Status { 11 | const owner = tree.owner; 12 | const args = this.args; 13 | owner.x = args.x; 14 | owner.y = args.y; 15 | return "success"; 16 | } 17 | 18 | static override get descriptor(): Readonly { 19 | return { 20 | name: "MoveToPos", 21 | type: "Action", 22 | desc: "移动到位置", 23 | args: [ 24 | { name: "x", type: "float", desc: "x坐标" }, 25 | { name: "y", type: "float", desc: "y坐标" }, 26 | ], 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/nodes/move-to-target.ts: -------------------------------------------------------------------------------- 1 | import { Context, Node, NodeDef, Status, Tree } from "../../src/behavior3"; 2 | import { Role } from "../role"; 3 | 4 | export class MoveToTarget extends Node { 5 | static SPEED = 50; 6 | 7 | declare input: [Role | undefined]; 8 | 9 | override onTick(tree: Tree): Status { 10 | const owner = tree.owner; 11 | const [target] = this.input; 12 | if (!target) { 13 | return "failure"; 14 | } 15 | const { x, y } = owner; 16 | const { x: tx, y: ty } = target; 17 | if (Math.abs(x - tx) < MoveToTarget.SPEED && Math.abs(y - ty) < MoveToTarget.SPEED) { 18 | console.log("Moving reach target"); 19 | return "success"; 20 | } 21 | 22 | console.log(`Moving (${x}, ${y}) => (${tx}, ${ty})`); 23 | 24 | if (Math.abs(x - tx) >= MoveToTarget.SPEED) { 25 | owner.x = owner.x + MoveToTarget.SPEED * (tx > x ? 1 : -1); 26 | } 27 | 28 | if (Math.abs(y - ty) >= MoveToTarget.SPEED) { 29 | owner.y = owner.y + MoveToTarget.SPEED * (ty > x ? 1 : -1); 30 | } 31 | 32 | return tree.yield(this); 33 | } 34 | 35 | static override get descriptor(): Readonly { 36 | return { 37 | name: "MoveToTarget", 38 | type: "Action", 39 | desc: "移动到目标", 40 | input: ["目标"], 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/role.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import { Context, Node, NodeDef, TreeData } from "../src/behavior3"; 3 | import { DeepReadonly } from "../src/behavior3/context"; 4 | import { Attack } from "./nodes/attack"; 5 | import { FindEnemy } from "./nodes/find-enemy"; 6 | import { GetHp } from "./nodes/get-hp"; 7 | import { Idle } from "./nodes/idle"; 8 | import { IsStatus } from "./nodes/is-status"; 9 | import { MoveToPos } from "./nodes/move-to-pos"; 10 | import { MoveToTarget } from "./nodes/move-to-target"; 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | // deno-lint-ignore no-explicit-any 14 | export type Callback = (...args: any[]) => void; 15 | 16 | export interface Role { 17 | hp: number; 18 | x: number; 19 | y: number; 20 | } 21 | 22 | export interface Position { 23 | x: number; 24 | y: number; 25 | } 26 | 27 | export class RoleContext extends Context { 28 | avators: Role[] = []; 29 | 30 | constructor() { 31 | super(); 32 | this.registerNode(Attack); 33 | this.registerNode(FindEnemy); 34 | this.registerNode(GetHp); 35 | this.registerNode(Idle); 36 | this.registerNode(IsStatus); 37 | this.registerNode(MoveToPos); 38 | this.registerNode(MoveToTarget); 39 | 40 | // 用于加速执行表达式,此代码可以通过脚本扫描所有行为树,预先生成代码,然后注册到 Context 中 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | // deno-lint-ignore no-explicit-any 43 | this.registerCode("hp > 50", (envars: any) => { 44 | return envars.hp > 50; 45 | }); 46 | } 47 | 48 | override loadTree(path: string): Promise { 49 | const treeData = JSON.parse(fs.readFileSync(path, "utf-8")) as TreeData; 50 | const rootNode = this._createTree(treeData); 51 | this.trees[path] = rootNode; 52 | return Promise.resolve(rootNode); 53 | } 54 | 55 | override get time() { 56 | return this._time; 57 | } 58 | 59 | override set time(value: number) { 60 | this._time = value; 61 | } 62 | 63 | find(func: Callback, _count: number) { 64 | return this.avators.filter((value) => func(value)); 65 | } 66 | 67 | exportNodeDefs() { 68 | const defs: DeepReadonly[] = []; 69 | Object.values(this.nodeDefs).forEach((descriptor) => { 70 | defs.push(descriptor); 71 | if (descriptor.name === "Listen") { 72 | (descriptor as NodeDef).args?.[0].options?.push( 73 | ...[ 74 | { 75 | name: "testOff", 76 | value: "testOff", 77 | }, 78 | { 79 | name: "hello", 80 | value: "hello", 81 | }, 82 | ] 83 | ); 84 | } 85 | }); 86 | defs.sort((a, b) => a.name.localeCompare(b.name)); 87 | return JSON.stringify(defs, null, 2); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "noImplicitAny": true, 9 | "noImplicitOverride": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "skipLibCheck": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "target": "ESNext" 17 | }, 18 | "include": ["src/", "test/"], 19 | "ts-node": { 20 | "esm": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------