diff --git a/public/icons/Action.svg b/public/icons/Action.svg index 96808ad..3e6376e 100644 --- a/public/icons/Action.svg +++ b/public/icons/Action.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/icons/Composite.svg b/public/icons/Composite.svg index be106d7..981bd44 100644 --- a/public/icons/Composite.svg +++ b/public/icons/Composite.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/locales/en.json b/public/locales/en.json index c7ff136..2a1048b 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -94,41 +94,5 @@ "node.subtree": "Subtree", "node.subtreePathError": "You should save the subtree inside the current workspace", - "node.unknown.desc": "New node", - "node.alwaysFail.desc": "Always return failure", - "node.alwaysFail.doc": "+ Only one child node, multiple child nodes will be executed first\n+ If any child node fails, return failure", - "node.alwaysSuccess.desc": "Always return success", - "node.alwaysSuccess.doc": "+ Only one child node, multiple child nodes will be executed first\n+ If any child node fails, return success", - "node.check.arg1.desc": "Expression", - "node.check.desc": "Evaluate expression", - "node.check.doc": "+ Do simple calculation, return success or failure", - "node.clear.desc": "Clear variable", - "node.clear.output1": "Variable name", - "node.forEach.desc": "Iterate array", - "node.forEach.input1": "Array", - "node.forEach.output1": "Variable name", - "node.forEach.doc": "+ Before executing each child node, set the current variable to the output\n+ Execute all child nodes\n+ Return success or running", - "node.getTime.desc": "Get current time", - "node.getTime.output1": "Current time", - "node.isNull.desc": "Check variable is null", - "node.isNull.input1": "Variable", - "node.log.desc": "Print log", - "node.log.arg1.desc": "Log content", - "node.loop.desc": "Loop execute", - "node.loop.arg1.desc": "Loop times", - "node.loop.input1": "Loop times", - "node.not.desc": "Invert", - "node.not.doc": "+ Invert the return value", - "node.notNull.desc": "Check variable is not null", - "node.notNull.input1": "Variable", - "node.once.desc": "Execute once", - "node.once.doc": "+ Execute all child nodes\n+ Return success or running", - "node.parallel.desc": "Parallel execute", - "node.parallel.doc": "+ Execute all child nodes\n+ Return success", - "node.selector.desc": "Selector execute", - "node.selector.doc": "+ Execute all child nodes\n+ Return success or failure", - "node.sequence.desc": "Sequence execute", - "node.sequence.doc": "+ Execute all child nodes\n+ Return success or failure", - "node.wait.desc": "Wait", - "node.wait.arg1.desc": "Wait time" + "node.unknown.desc": "New node" } diff --git a/public/locales/zh.json b/public/locales/zh.json index f9db29f..51497a8 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -94,41 +94,5 @@ "node.subtree": "子树", "node.subtreePathError": "子树必须保存在当前工作区的目录内", - "node.unknown.desc": "新建节点", - "node.alwaysFail.desc": "始终返回失败", - "node.alwaysFail.doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回失败", - "node.alwaysSuccess.desc": "始终返回成功", - "node.alwaysSuccess.doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回成功", - "node.check.arg1.desc": "表达式", - "node.check.desc": "对表达式进行求值", - "node.check.doc": "+ 做简单数值公式判定,返回成功或失败", - "node.clear.desc": "清除变量", - "node.clear.output1": "清除的变量名", - "node.forEach.desc": "遍历数组", - "node.forEach.input1": "数组", - "node.forEach.output1": "变量名", - "node.forEach.doc": "+ 每次执行子节点前会设置当前遍历到的变量\n+ 会执行所有子节点\n+ 永远返回成功/正在运行", - "node.getTime.desc": "获取当前时间", - "node.getTime.output1": "当前时间", - "node.isNull.desc": "判断变量是否为空", - "node.isNull.input1": "判断的变量", - "node.log.desc": "打印日志", - "node.log.arg1.desc": "日志内容", - "node.loop.desc": "循环执行", - "node.loop.arg1.desc": "循环次数", - "node.loop.input1": "循环次数", - "node.not.desc": "取反", - "node.not.doc": "+ 将子节点的返回值取反", - "node.notNull.desc": "判断变量是否存在", - "node.notNull.input1": "判断的变量", - "node.once.desc": "只执行一次", - "node.once.doc": "+ 可以接多个子节点,子节点默认全部执行\n+ 被打断后该节点后的子节点依旧不会执行\n+ 该节点执行后永远返回成功", - "node.parallel.desc": "并行执行", - "node.parallel.doc": "+ 执行所有子节点并返回成功", - "node.selector.desc": "选择执行", - "node.selector.doc": "+ 一直往下执行,有子节点返回成功则返回成功,若全部节点返回失败则返回失败\n+ 子节点是或 (OR) 的关系", - "node.sequence.desc": "顺序执行", - "node.sequence.doc": "+ 一直往下执行,只有当所有子节点都返回成功, 才返回成功\n+ 子节点是与(AND)的关系", - "node.wait.desc": "等待", - "node.wait.arg1.desc": "等待时间" + "node.unknown.desc": "新建节点" } diff --git a/src/components/register-node.ts b/src/components/register-node.ts index 06a5d42..bcfc817 100644 --- a/src/components/register-node.ts +++ b/src/components/register-node.ts @@ -7,10 +7,10 @@ import Path from "@/misc/path"; import G6 from "@antv/g6"; const NODE_COLORS: any = { - ["Composite"]: "#5aed20", - ["Decorator"]: "#faad14", + ["Composite"]: "#34d800", + ["Decorator"]: "#ff6700", ["Condition"]: "#e4148b", - ["Action"]: "#5b90f9", + ["Action"]: "#1668dc", ["Other"]: "#707070", ["Error"]: "#ff0000", }; @@ -193,9 +193,9 @@ G6.registerNode( addShape("image", { attrs: { x: 5, - y: 3.5, - height: 16, - width: 16, + y: 3, + height: 18, + width: 18, img, }, name: "node-icon", @@ -205,7 +205,7 @@ G6.registerNode( addShape("text", { attrs: { textBaseline: "top", - x: 25, + x: 26, y: 5, fontWeight: 800, text: cfg.name, diff --git a/src/components/workspace.tsx b/src/components/workspace.tsx index 4f3df38..cfa1a44 100644 --- a/src/components/workspace.tsx +++ b/src/components/workspace.tsx @@ -65,6 +65,7 @@ export const Workspace: FC = () => { workspace.close(workspace.editing.path); } } + keysRef.current?.focus(); } else if (isHotkeyPressed(Hotkey.SearchTree)) { event.preventDefault(); workspace.onShowingSearch(true); diff --git a/src/misc/b3util.ts b/src/misc/b3util.ts index 662258f..bec3031 100644 --- a/src/misc/b3util.ts +++ b/src/misc/b3util.ts @@ -4,6 +4,7 @@ import * as fs from "fs"; import { message } from "./hooks"; import i18n from "./i18n"; import Path from "./path"; +import { zhNodeDef } from "./template"; export const isSubtreeRoot = (data: TreeGraphData) => { return data.path && data.id.toString() !== "1"; @@ -415,139 +416,7 @@ export const cutWordTo = (str: string, maxlen: number) => { }; export const createProject = (path: string) => { - fs.writeFileSync( - Path.dirname(path) + "/node-config.b3-setting", - JSON.stringify( - [ - { - name: "AlwaysFail", - type: "Decorator", - desc: i18n.t("node.alwaysFail.desc"), - doc: i18n.t("node.alwaysFail.doc"), - }, - { - name: "AlwaysSuccess", - type: "Decorator", - desc: i18n.t("node.alwaysSuccess.desc"), - doc: i18n.t("node.alwaysSuccess.doc"), - }, - { - name: "Check", - type: "Condition", - desc: i18n.t("node.check.desc"), - args: [ - { - name: "value", - type: "code", - desc: i18n.t("node.check.arg1.desc"), - }, - ], - doc: i18n.t("node.check.doc"), - }, - { - name: "Clear", - type: "Action", - desc: i18n.t("node.clear.desc"), - output: [i18n.t("node.clear.output1")], - }, - { - name: "ForEach", - type: "Composite", - desc: i18n.t("node.forEach.desc"), - input: [i18n.t("node.forEach.input1")], - output: [i18n.t("node.forEach.output1")], - doc: i18n.t("node.forEach.doc"), - }, - { - name: "GetTime", - type: "Action", - desc: i18n.t("node.getTime.desc"), - output: [i18n.t("node.getTime.output1")], - }, - { - name: "IsNull", - type: "Condition", - desc: i18n.t("node.isNull.desc"), - input: [i18n.t("node.isNull.input1")], - }, - { - name: "Log", - type: "Action", - desc: i18n.t("node.log.desc"), - args: [ - { - name: "message", - type: "string", - desc: i18n.t("node.log.arg1.desc"), - }, - ], - }, - { - name: "Loop", - type: "Composite", - desc: i18n.t("node.loop.desc"), - args: [ - { - name: "count", - type: "int?", - desc: i18n.t("node.loop.arg1.desc"), - }, - ], - input: [i18n.t("node.loop.input1") + "?"], - }, - { - name: "Not", - type: "Decorator", - desc: i18n.t("node.not.desc"), - doc: i18n.t("node.not.doc"), - }, - { - name: "NotNull", - type: "Condition", - desc: i18n.t("node.notNull.desc"), - input: [i18n.t("node.notNull.input")], - }, - { - name: "Once", - type: "Composite", - desc: i18n.t("node.once.desc"), - doc: i18n.t("node.once.doc"), - }, - { - name: "Parallel", - type: "Composite", - desc: i18n.t("node.parallel.desc"), - doc: i18n.t("node.parallel.doc"), - }, - { - name: "Selector", - type: "Composite", - desc: i18n.t("node.selector.desc"), - doc: i18n.t("node.selector.doc"), - }, - { - name: "Sequence", - type: "Composite", - desc: i18n.t("node.sequence.desc"), - doc: i18n.t("node.sequence.doc"), - }, - { - name: "Wait", - type: "Action", - desc: i18n.t("node.wait.desc"), - args: [ - { - name: "time", - type: "float", - desc: i18n.t("node.wait.arg1.desc"), - }, - ], - }, - ], - null, - 2 - ) - ); + fs.writeFileSync(Path.dirname(path) + "/node-config.b3-setting", zhNodeDef()); fs.writeFileSync( Path.dirname(path) + "/example.json", JSON.stringify( diff --git a/src/misc/template.ts b/src/misc/template.ts new file mode 100644 index 0000000..53e3423 --- /dev/null +++ b/src/misc/template.ts @@ -0,0 +1,323 @@ +type Status = "success" | "failure" | "running"; + +type Constructor = new (...args: unknown[]) => T; + +interface TreeEnv {} +interface Node {} + +interface NodeDef { + name: string; + type: "Action" | "Decorator" | "Condition" | "Composite"; + desc: string; + icon?: string; + color?: string; + input?: string[]; + args?: { + name: string; + type: + | "boolean" + | "boolean?" + | "int" + | "int?" + | "float" + | "float?" + | "enum" + | "enum?" + | "string" + | "string?" + | "code" + | "code?"; + desc: string; + options?: { name: string; value: unknown }[]; + }[]; + output?: string[]; + doc?: string; +} + +abstract class Process { + init(node: Node): void {} + + run(node: Node, env: TreeEnv): Status { + return "success"; + } + + abstract get descriptor(): NodeDef; +} + +namespace zh { + export namespace action { + export class Clear extends Process { + override get descriptor(): NodeDef { + return { + name: "Clear", + type: "Action", + desc: "清除变量", + output: ["清除的变量名"], + }; + } + } + + export class Log extends Process { + override get descriptor(): NodeDef { + return { + name: "Log", + type: "Action", + desc: "打印日志", + args: [{ name: "message", type: "string", desc: "日志" }], + }; + } + } + + export class Now extends Process { + override get descriptor(): NodeDef { + return { + name: "Now", + type: "Action", + desc: "获取当前时间", + output: ["当前时间"], + }; + } + } + + export class Wait extends Process { + override get descriptor(): NodeDef { + return { + name: "Wait", + type: "Action", + desc: "等待", + input: ["等待时间?"], + args: [ + { name: "time", type: "float", desc: "等待时间" }, + { name: "random", type: "float?", desc: "随机范围" }, + ], + }; + } + } + } + + export namespace composite { + export class Foreach extends Process { + override get descriptor(): NodeDef { + return { + name: "ForEach", + type: "Composite", + desc: "遍历数组", + input: ["数组"], + output: ["变量"], + doc: ` + + 每次执行子节点前会设置当前遍历到的变量 + + 会执行所有子节点 + + 永远返回「成功」/「运行中」`, + }; + } + } + + export class Loop extends Process { + override get descriptor(): NodeDef { + return { + name: "Loop", + type: "Composite", + desc: "循环执行", + input: ["循环次数?"], + args: [{ name: "count", type: "int", desc: "循环次数" }], + }; + } + } + + export class Parallel extends Process { + override get descriptor(): NodeDef { + return { + name: "Parallel", + type: "Composite", + desc: "并行执行", + doc: `执行所有子节点并返回「成功」/「运行中」`, + }; + } + } + + export class Selector extends Process { + override get descriptor(): NodeDef { + return { + name: "Selector", + type: "Composite", + desc: "选择执行", + doc: ` + + 一直往下执行,有子节点返回「成功」则返回「成功」 + + 若全部节点返回「失败」则返回「失败」 + + 子节点是或 (OR) 的关系`, + }; + } + } + + export class Sequence extends Process { + override get descriptor(): NodeDef { + return { + name: "Sequence", + type: "Composite", + desc: "顺序执行", + doc: ` + + 一直往下执行,只有当所有子节点都返回「成功」, 才返回「成功」 + + 子节点是与(AND)的关系`, + }; + } + } + } + + export namespace condition { + export class Check extends Process { + override get descriptor(): NodeDef { + return { + name: "Check", + type: "Condition", + desc: "检查True或False", + args: [{ name: "value", type: "code", desc: "值" }], + doc: ` + + 做简单数值公式判定,返回「成功」或「失败」`, + }; + } + } + export class IsNull extends Process { + override get descriptor(): NodeDef { + return { + name: "IsNull", + type: "Condition", + desc: "判断变量是否不存在", + input: ["判断的变量"], + }; + } + } + + export class NotNull extends Process { + override get descriptor(): NodeDef { + return { + name: "NotNull", + type: "Condition", + desc: "判断变量是否存在", + input: ["判断的变量"], + }; + } + } + } + + export namespace decorator { + export class AlwaysFail extends Process { + override get descriptor(): NodeDef { + return { + name: "AlwaysFail", + type: "Decorator", + desc: "始终返回失败", + doc: ` + + 只能有一个子节点,多个仅执行第一个 + + 不管子节点是否成功都返回「失败」`, + }; + } + } + export class AlwaysSuccess extends Process { + override get descriptor(): NodeDef { + return { + name: "AlwaysSuccess", + type: "Decorator", + desc: "始终返回成功", + doc: ` + + 只能有一个子节点,多个仅执行第一个 + + 不管子节点是否成功都返回「成功」`, + }; + } + } + + export class Inverter extends Process { + override get descriptor(): NodeDef { + return { + name: "Inverter", + type: "Decorator", + desc: "反转子节点运行结果", + doc: ` + + 只能有一个子节点,多个仅执行第一个 + + 当子节点返回「成功」时返回「失败」 + + 当子节点返回「失败」时返回「成功」 + + 其余返回「运行中」`, + }; + } + } + + export class Once extends Process { + override get descriptor(): NodeDef { + return { + name: "Once", + type: "Decorator", + desc: "只执行一次", + doc: ` + + 可以接多个子节点,子节点默认全部执行 + + 被打断后该节点后的子节点依旧不会执行 + + 该节点执行后永远返回「成功」`, + }; + } + } + export class RepeatUntilFailure extends Process { + override get descriptor(): NodeDef { + return { + name: "RepeatUntilFailure", + type: "Decorator", + desc: "一直尝试直到子节点返回失败", + input: ["最大循环次数?"], + args: [{ name: "maxLoop", type: "int?", desc: "最大循环次数" }], + doc: ` + + 只能有一个子节点,多个仅执行第一个 + + 只有当子节点返回「失败」时,才返回「成功」,其它情况返回「运行中」状态 + + 如果设定了尝试次数,超过指定次数则返回「失败」`, + }; + } + } + + export class RepeatUntilSuccess extends Process { + override get descriptor(): NodeDef { + return { + name: "RepeatUntilSuccess", + type: "Decorator", + desc: "一直尝试直到子节点返回成功", + input: ["最大循环次数?"], + args: [{ name: "maxLoop", type: "int?", desc: "最大循环次数" }], + doc: ` + + 只能有一个子节点,多个仅执行第一个 + + 只有当子节点返回「成功」时,才返回「成功」,其它情况返回「运行中」状态 + + 如果设定了尝试次数,超过指定次数则返回「失败」`, + }; + } + } + } +} + +const toString = (nodes: Constructor[]) => { + const defs: NodeDef[] = []; + for (const cls of nodes) { + defs.push(new cls().descriptor); + } + defs.sort((a, b) => a.name.localeCompare(b.name)); + let str = JSON.stringify(defs, null, 2); + str = str.replace(/"doc": "\\n +/g, '"doc": "'); + str = str.replace(/\\n +/g, "\\n"); + return str; +}; + +export const zhNodeDef = () => { + return toString([ + zh.action.Clear, + zh.action.Log, + zh.action.Now, + zh.action.Wait, + zh.composite.Foreach, + zh.composite.Loop, + zh.composite.Parallel, + zh.composite.Selector, + zh.composite.Sequence, + zh.condition.Check, + zh.condition.IsNull, + zh.condition.NotNull, + zh.decorator.AlwaysFail, + zh.decorator.AlwaysSuccess, + zh.decorator.Inverter, + zh.decorator.Once, + zh.decorator.RepeatUntilFailure, + zh.decorator.RepeatUntilSuccess, + ]); +};