diff --git a/src/features/editScene/lua-examples/behavior_tree_example.lua b/src/features/editScene/lua-examples/behavior_tree_example.lua index 1b69f1b..64e8799 100644 --- a/src/features/editScene/lua-examples/behavior_tree_example.lua +++ b/src/features/editScene/lua-examples/behavior_tree_example.lua @@ -2,17 +2,18 @@ -- Behavior Tree Lua API Examples -- ============================================================================= -- This file demonstrates how to create custom behavior tree nodes using --- Lua functions via the ecs.behavior_tree API. +-- Lua functions via the ecs.behavior_tree API, and how to use the +-- built-in C++ node types via ecs.behavior_tree.create_node(). -- -- The API allows you to: -- 1. Register Lua functions as behavior tree node handlers --- 2. Create behavior tree nodes that invoke those handlers +-- 2. Create behavior tree nodes (both Lua and built-in C++ types) -- 3. Return "success", "failure", or "running" to control tree flow -- 4. Pass parameters from the behavior tree editor to your Lua function -- ============================================================================= -- ============================================================================= --- Registering Node Handlers +-- Registering Lua Node Handlers -- ============================================================================= -- Use ecs.behavior_tree.register_node(name, function) to register a Lua -- function as a behavior tree node handler. @@ -30,7 +31,7 @@ -- ============================================================================= -- Example 1: Simple greeting node -- ============================================================================= --- This node just prints a message and succeeds immediately. +-- Prints a message and succeeds immediately. ecs.behavior_tree.register_node("say_hello", function(entity_id, params) local message = params.message or "Hello!" @@ -41,35 +42,30 @@ end) -- ============================================================================= -- Example 2: Node that checks a blackboard value -- ============================================================================= --- This node checks if a blackboard value meets a threshold. --- It succeeds if the check passes, fails otherwise. +-- Checks if a blackboard integer value meets a minimum threshold. +-- Succeeds if value >= min, fails otherwise. ecs.behavior_tree.register_node("check_blackboard_value", function(entity_id, params) local key = params.key local min_val = tonumber(params.min) or 0 if not key then - print("[BT] check_blackboard_value: no 'key' param provided") return "failure" end local bb = ecs.get_component(entity_id, "GoapBlackboard") if not bb then - print("[BT] Entity " .. entity_id .. " has no GoapBlackboard") return "failure" end local value = bb.values[key] if value == nil then - print("[BT] Blackboard has no key '" .. key .. "'") return "failure" end if value >= min_val then - print("[BT] Check passed: " .. key .. " = " .. value .. " >= " .. min_val) return "success" else - print("[BT] Check failed: " .. key .. " = " .. value .. " < " .. min_val) return "failure" end end) @@ -77,8 +73,8 @@ end) -- ============================================================================= -- Example 3: Node that runs over multiple frames (running state) -- ============================================================================= --- This node waits for a specified duration, storing its progress in the --- blackboard's floatValues. It returns "running" each frame until done. +-- Waits for a specified duration, storing progress in the blackboard's +-- floatValues. Returns "running" each frame until the duration elapses. ecs.behavior_tree.register_node("wait_for_duration", function(entity_id, params) local duration = tonumber(params.duration) or 1.0 @@ -89,35 +85,26 @@ ecs.behavior_tree.register_node("wait_for_duration", function(entity_id, params) return "failure" end - -- Get elapsed time from blackboard (or start at 0) local elapsed = bb.floatValues[timer_key] or 0.0 - - -- Get delta time (requires ecs.get_delta_time to be available) - -- For now, use a fixed step approximation - local dt = 0.016 -- ~60 FPS + local dt = ecs.get_delta_time() or 0.016 elapsed = elapsed + dt bb.floatValues[timer_key] = elapsed - - -- Save updated blackboard ecs.set_component(entity_id, "GoapBlackboard", bb) if elapsed >= duration then - -- Done waiting - clean up timer bb.floatValues[timer_key] = nil ecs.set_component(entity_id, "GoapBlackboard", bb) - print("[BT] Wait complete after " .. duration .. " seconds") return "success" end - -- Still waiting return "running" end) -- ============================================================================= -- Example 4: Node that modifies blackboard values -- ============================================================================= --- This node adds a value to a blackboard integer value. +-- Adds a configurable amount to a blackboard integer value. ecs.behavior_tree.register_node("add_blackboard_value", function(entity_id, params) local key = params.key @@ -134,15 +121,13 @@ ecs.behavior_tree.register_node("add_blackboard_value", function(entity_id, para bb.values[key] = (bb.values[key] or 0) + amount ecs.set_component(entity_id, "GoapBlackboard", bb) - - print("[BT] Added " .. amount .. " to " .. key .. " (now " .. bb.values[key] .. ")") return "success" end) -- ============================================================================= -- Example 5: Node that sets a blackboard bit -- ============================================================================= --- This node sets or clears a named bit in the blackboard. +-- Sets or clears a named bit in the blackboard. ecs.behavior_tree.register_node("set_blackboard_bit", function(entity_id, params) local bit_name = params.bit @@ -159,15 +144,13 @@ ecs.behavior_tree.register_node("set_blackboard_bit", function(entity_id, params bb.bits[bit_name] = value ecs.set_component(entity_id, "GoapBlackboard", bb) - - print("[BT] Set bit '" .. bit_name .. "' to " .. tostring(value)) return "success" end) -- ============================================================================= -- Example 6: Node that checks a blackboard bit -- ============================================================================= --- This node checks if a named bit is set to a specific value. +-- Checks if a named bit is set to a specific value. ecs.behavior_tree.register_node("check_blackboard_bit", function(entity_id, params) local bit_name = params.bit @@ -193,55 +176,222 @@ end) -- ============================================================================= -- Example 7: Random chance node -- ============================================================================= --- This node succeeds with a configurable probability. +-- Succeeds with a configurable probability (0.0 to 1.0). ecs.behavior_tree.register_node("random_chance", function(entity_id, params) local probability = tonumber(params.probability) or 0.5 - - -- Simple pseudo-random using math.random local roll = math.random() if roll < probability then - print("[BT] Random chance succeeded (roll=" .. roll .. " < " .. probability .. ")") return "success" else - print("[BT] Random chance failed (roll=" .. roll .. " >= " .. probability .. ")") return "failure" end end) -- ============================================================================= --- Using the Nodes in Behavior Trees +-- Using Built-in Node Types via create_node() -- ============================================================================= --- Once registered, you can use ecs.behavior_tree.create_node(name, params) --- to create node tables for use in action behavior trees. +-- ecs.behavior_tree.create_node(type, name, params) creates a node table +-- for any built-in C++ node type. If the first argument matches a registered +-- Lua handler name, it creates a luaTask node instead. -- --- The params string uses "key=val,key2=val2" format: --- - Numbers (int or float) are parsed automatically --- - Quoted strings preserve string values: msg="hello world" --- - Unquoted non-numeric values are treated as strings +-- Built-in node types: +-- Control: sequence, selector, invert +-- Animation: setAnimationState, isAnimationEnded +-- Blackboard: setBit, checkBit, setValue, checkValue, blackboardDump +-- Timing: delay +-- Movement: teleportToChild +-- Physics: disablePhysics, enablePhysics +-- Events: sendEvent +-- Inventory: hasItem, hasItemByName, countItem, pickupItem, dropItem, +-- useItem, addItemToInventory +-- Debug: debugPrint +-- Lua: luaTask (auto-detected when name matches a registered handler) -- ============================================================================= --- Example: Action that uses Lua behavior tree nodes -ecs.action_db.add_action("lua_demo_greet", 1, - {}, -- no preconditions - {}, -- no effects +-- ============================================================================= +-- Example 8: Action using built-in animation nodes +-- ============================================================================= +-- Uses setAnimationState and isAnimationEnded to play an animation and +-- wait for it to finish. + +ecs.action_db.add_action("play_walk_animation", 1, + {}, -- preconditions + {}, -- effects { -- behavior tree type = "sequence", children = { - ecs.behavior_tree.create_node("say_hello", "message=Welcome to the game!"), - ecs.behavior_tree.create_node("wait_for_duration", "duration=2.0,timer_key=greet_timer"), - ecs.behavior_tree.create_node("say_hello", "message=How are you today?") + ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"), + ecs.behavior_tree.create_node("isAnimationEnded", "locomotion"), + ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle") } } ) --- Example: Action with conditional logic using Lua nodes -ecs.action_db.add_action("lua_demo_conditional", 2, +-- ============================================================================= +-- Example 9: Action using built-in delay node +-- ============================================================================= +-- Waits for a specified duration using the built-in delay node. + +ecs.action_db.add_action("wait_and_greet", 1, + {}, + {}, { - values = { experience = 10 } + type = "sequence", + children = { + ecs.behavior_tree.create_node("delay", "", "2.0"), + ecs.behavior_tree.create_node("say_hello", "message=Waited 2 seconds!") + } + } +) + +-- ============================================================================= +-- Example 10: Action using built-in blackboard nodes +-- ============================================================================= +-- Sets a blackboard bit, checks it, and sets a value. + +ecs.action_db.add_action("blackboard_demo", 1, + {}, + { + bits = { has_sword = true }, + values = { gold = 100 } }, { - values = { experience = 20 } + type = "sequence", + children = { + ecs.behavior_tree.create_node("setBit", "has_sword", "1"), + ecs.behavior_tree.create_node("checkBit", "has_sword"), + ecs.behavior_tree.create_node("setValue", "gold", "100"), + ecs.behavior_tree.create_node("checkValue", "gold", ">= 50"), + ecs.behavior_tree.create_node("blackboardDump", "After setup") + } + } +) + +-- ============================================================================= +-- Example 11: Action using built-in sendEvent node +-- ============================================================================= +-- Sends an event with parameters. + +ecs.action_db.add_action("trigger_quest_event", 1, + {}, + {}, + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("sendEvent", "quest_started", + "quest_id=the_ancient_sword,quest_giver=elder"), + ecs.behavior_tree.create_node("debugPrint", "Quest event sent") + } + } +) + +-- ============================================================================= +-- Example 12: Action using built-in physics nodes +-- ============================================================================= +-- Disables physics, waits, then re-enables. + +ecs.action_db.add_action("physics_control", 1, + {}, + {}, + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("disablePhysics"), + ecs.behavior_tree.create_node("delay", "", "1.0"), + ecs.behavior_tree.create_node("enablePhysics") + } + } +) + +-- ============================================================================= +-- Example 13: Action using built-in inventory nodes +-- ============================================================================= +-- Adds an item to inventory, checks for it, uses it, then drops it. + +ecs.action_db.add_action("inventory_demo", 1, + {}, + {}, + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("addItemToInventory", "potion_01", + "Health Potion,misc,1,0.5,10"), + ecs.behavior_tree.create_node("hasItem", "potion_01"), + ecs.behavior_tree.create_node("countItem", "potion_01", "1"), + ecs.behavior_tree.create_node("useItem", "potion_01"), + ecs.behavior_tree.create_node("dropItem", "potion_01", "1") + } + } +) + +-- ============================================================================= +-- Example 14: Action using built-in teleport node +-- ============================================================================= +-- Teleports the entity to a named child transform. + +ecs.action_db.add_action("teleport_to_entrance", 1, + {}, + {}, + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("teleportToChild", "entrance"), + ecs.behavior_tree.create_node("debugPrint", "Teleported to entrance") + } + } +) + +-- ============================================================================= +-- Example 15: Complex action mixing Lua and built-in nodes +-- ============================================================================= +-- A selector that first tries to use a sword, and if not available, +-- picks one up and equips it. + +ecs.action_db.add_action("equip_sword", 2, + {}, + { + bits = { has_sword = true } + }, + { + type = "selector", + children = { + -- Try to use existing sword + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("checkBit", "has_sword"), + ecs.behavior_tree.create_node("debugPrint", "Already have a sword") + } + }, + -- Pick up and equip a sword + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"), + ecs.behavior_tree.create_node("delay", "", "3.0"), + ecs.behavior_tree.create_node("addItemToInventory", "sword_01", + "Iron Sword,weapon,1,2.5,50"), + ecs.behavior_tree.create_node("setBit", "has_sword", "1"), + ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"), + ecs.behavior_tree.create_node("debugPrint", "Picked up the sword!") + } + } + } + } +) + +-- ============================================================================= +-- Example 16: Action with conditional logic using Lua nodes +-- ============================================================================= +-- Uses Lua-registered nodes for blackboard checks and modifications. + +ecs.action_db.add_action("gain_experience", 2, + { + values = { experience = 0 } + }, + { + values = { experience = 15 } }, { type = "selector", @@ -249,24 +399,33 @@ ecs.action_db.add_action("lua_demo_conditional", 2, { type = "sequence", children = { - ecs.behavior_tree.create_node("check_blackboard_value", "key=experience,min=50"), - ecs.behavior_tree.create_node("say_hello", "message=You are experienced!"), - ecs.behavior_tree.create_node("add_blackboard_value", "key=experience,amount=10") + ecs.behavior_tree.create_node("check_blackboard_value", + "key=experience,min=50"), + ecs.behavior_tree.create_node("say_hello", + "message=You are experienced!"), + ecs.behavior_tree.create_node("add_blackboard_value", + "key=experience,amount=10") } }, { type = "sequence", children = { - ecs.behavior_tree.create_node("say_hello", "message=You are still learning..."), - ecs.behavior_tree.create_node("add_blackboard_value", "key=experience,amount=5") + ecs.behavior_tree.create_node("say_hello", + "message=You are still learning..."), + ecs.behavior_tree.create_node("add_blackboard_value", + "key=experience,amount=5") } } } } ) --- Example: Action with random outcomes -ecs.action_db.add_action("lua_demo_gamble", 3, +-- ============================================================================= +-- Example 17: Action with random outcomes +-- ============================================================================= +-- Uses the Lua random_chance node for probabilistic behavior. + +ecs.action_db.add_action("try_gamble", 3, {}, { values = { gold = 10 } @@ -281,6 +440,45 @@ ecs.action_db.add_action("lua_demo_gamble", 3, } ) +-- ============================================================================= +-- Example 18: Action with running-state Lua node +-- ============================================================================= +-- Uses the wait_for_duration Lua node which returns "running" each frame. + +ecs.action_db.add_action("wait_and_continue", 1, + {}, + {}, + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("wait_for_duration", + "duration=2.5,timer_key=my_timer"), + ecs.behavior_tree.create_node("say_hello", "message=Done waiting!") + } + } +) + +-- ============================================================================= +-- Example 19: Action with blackboard bit operations via Lua nodes +-- ============================================================================= + +ecs.action_db.add_action("toggle_flag", 1, + {}, + { + bits = { quest_complete = true } + }, + { + type = "sequence", + children = { + ecs.behavior_tree.create_node("set_blackboard_bit", + "bit=quest_complete,value=1"), + ecs.behavior_tree.create_node("check_blackboard_bit", + "bit=quest_complete,value=1"), + ecs.behavior_tree.create_node("debugPrint", "Quest flag set and verified") + } + } +) + -- ============================================================================= -- Managing Registered Nodes -- ============================================================================= diff --git a/src/features/editScene/lua/LuaBehaviorTreeApi.cpp b/src/features/editScene/lua/LuaBehaviorTreeApi.cpp index 00fa9cf..3124b3b 100644 --- a/src/features/editScene/lua/LuaBehaviorTreeApi.cpp +++ b/src/features/editScene/lua/LuaBehaviorTreeApi.cpp @@ -343,25 +343,75 @@ static int luaListNodeHandlers(lua_State *L) } // --------------------------------------------------------------------------- -// Lua: ecs.behavior_tree.create_node(name, params) -> table +// Lua: ecs.behavior_tree.create_node(type, name, params) -> table +// ecs.behavior_tree.create_node(type, params) -> table +// ecs.behavior_tree.create_node(luaHandlerName, params) -> table (backward compat) // --------------------------------------------------------------------------- +// Creates a behavior tree node table suitable for use in action behavior trees. +// +// If the first argument matches a registered Lua node handler name, it creates +// a luaTask node (backward compatible behavior). +// Otherwise, the first argument is treated as the node type string. +// +// Examples: +// ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk") +// -> { type = "setAnimationState", name = "locomotion/walk" } +// +// ecs.behavior_tree.create_node("delay", "2.0") +// -> { type = "delay", params = "2.0" } +// +// ecs.behavior_tree.create_node("delay", "", "2.0") +// -> { type = "delay", params = "2.0" } +// +// ecs.behavior_tree.create_node("say_hello", "message=Hi") +// -> { type = "luaTask", name = "say_hello", params = "message=Hi" } static int luaCreateNode(lua_State *L) { - const char *name = luaL_checkstring(L, 1); - const char *params = luaL_optstring(L, 2, ""); + const char *arg1 = luaL_checkstring(L, 1); + + // Check if arg1 is a registered Lua node handler name (backward compat) + bool isLuaHandler = g_luaNodeHandlers.find(arg1) != + g_luaNodeHandlers.end(); + + // Save arg2 and arg3 before pushing the result table (which shifts indices) + const char *arg2 = NULL; + const char *arg3 = NULL; + if (lua_gettop(L) >= 2 && lua_isstring(L, 2)) + arg2 = lua_tostring(L, 2); + if (lua_gettop(L) >= 3 && lua_isstring(L, 3)) + arg3 = lua_tostring(L, 3); lua_newtable(L); - lua_pushstring(L, "luaTask"); - lua_setfield(L, -2, "type"); + if (isLuaHandler) { + // Backward compatible: create a luaTask node + lua_pushstring(L, "luaTask"); + lua_setfield(L, -2, "type"); - lua_pushstring(L, name); - lua_setfield(L, -2, "name"); + lua_pushstring(L, arg1); + lua_setfield(L, -2, "name"); - if (params && params[0]) { - lua_pushstring(L, params); - lua_setfield(L, -2, "params"); + if (arg2 && arg2[0]) { + lua_pushstring(L, arg2); + lua_setfield(L, -2, "params"); + } + } else { + // arg1 is the node type + lua_pushstring(L, arg1); + lua_setfield(L, -2, "type"); + + // arg2 is either name or params depending on the node type + if (arg2 && arg2[0]) { + lua_pushstring(L, arg2); + lua_setfield(L, -2, "name"); + } + + // arg3 is params (optional) + if (arg3 && arg3[0]) { + lua_pushstring(L, arg3); + lua_setfield(L, -2, "params"); + } } return 1; diff --git a/src/features/editScene/lua/LuaBehaviorTreeApi.hpp b/src/features/editScene/lua/LuaBehaviorTreeApi.hpp index b411ded..120e480 100644 --- a/src/features/editScene/lua/LuaBehaviorTreeApi.hpp +++ b/src/features/editScene/lua/LuaBehaviorTreeApi.hpp @@ -28,29 +28,52 @@ * ecs.behavior_tree.list_nodes() -> table of registered node names * Returns an array of all registered node handler names. * - * ecs.behavior_tree.create_node(name, params) -> table + * ecs.behavior_tree.create_node(type, name, params) -> table * Creates a behavior tree node table suitable for use in * ecs.action_db.add_action() behavior trees. - * Returns { type = "luaTask", name = name, params = params } + * + * If the first argument matches a registered Lua node handler name, + * it creates a luaTask node (backward compatible). + * Otherwise, the first argument is the node type string. + * + * Examples: + * ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk") + * -> { type = "setAnimationState", name = "locomotion/walk" } + * + * ecs.behavior_tree.create_node("delay", "", "2.0") + * -> { type = "delay", params = "2.0" } + * + * ecs.behavior_tree.create_node("say_hello", "message=Hi") + * -> { type = "luaTask", name = "say_hello", params = "message=Hi" } + * + * Supported node types (see BehaviorTree.hpp for full list): + * sequence, selector, invert, task, check, debugPrint, + * setAnimationState, isAnimationEnded, setBit, checkBit, + * setValue, checkValue, blackboardDump, delay, teleportToChild, + * disablePhysics, enablePhysics, sendEvent, hasItem, hasItemByName, + * countItem, pickupItem, dropItem, useItem, addItemToInventory, + * luaTask * * Example: - * -- Register a node handler + * -- Register a Lua node handler * ecs.behavior_tree.register_node("say_hello", function(entity_id, params) * print("Hello from entity " .. entity_id .. "! " .. (params.message or "")) * return "success" * end) * - * -- Use it in an action's behavior tree + * -- Build a behavior tree using both built-in C++ nodes and Lua nodes * ecs.action_db.add_action("greet", 1, {}, {}, { * type = "sequence", * children = { + * ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"), + * ecs.behavior_tree.create_node("delay", "", "2.0"), + * ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"), * ecs.behavior_tree.create_node("say_hello", "message=Hello World") * } * }) * - * -- A node that runs for a while: + * -- A Lua node that runs for a while: * ecs.behavior_tree.register_node("wait_random", function(entity_id, params) - * -- Use the blackboard to store state across frames * local bb = ecs.get_component(entity_id, "GoapBlackboard") * if not bb then return "failure" end * diff --git a/src/features/editScene/tests/behavior_tree_lua_test.cpp b/src/features/editScene/tests/behavior_tree_lua_test.cpp index 65bee42..9d9ba53 100644 --- a/src/features/editScene/tests/behavior_tree_lua_test.cpp +++ b/src/features/editScene/tests/behavior_tree_lua_test.cpp @@ -270,7 +270,13 @@ static int testCreateNodeNoParams(lua_State *L) { TEST("create_node without params"); - bool ok = runLua( + // Register a Lua handler first so create_node creates a luaTask node + bool ok = runLua(L, "ecs.behavior_tree.register_node('simple_node', " + " function(e, p) return 'success' end)\n"); + if (!ok) + FAIL("failed to register simple_node handler"); + + ok = runLua( L, "local node = ecs.behavior_tree.create_node('simple_node')\n" "no_params_type = node.type\n" "no_params_name = node.name\n" @@ -312,26 +318,34 @@ static int testCreateNodeInTree(lua_State *L) { TEST("create_node used in behavior tree structure"); - bool ok = runLua( - L, "local tree = {\n" - " type = 'sequence',\n" - " children = {\n" - " ecs.behavior_tree.create_node('say_hello', " - " 'message=Hello'),\n" - " ecs.behavior_tree.create_node('wait_for_duration', " - " 'duration=2.0,timer_key=test'),\n" - " ecs.behavior_tree.create_node('say_hello', " - " 'message=Done')\n" - " }\n" - "}\n" - "tree_type = tree.type\n" - "child_count = #tree.children\n" - "child0_type = tree.children[1].type\n" - "child0_name = tree.children[1].name\n" - "child0_params = tree.children[1].params\n" - "child1_type = tree.children[2].type\n" - "child1_name = tree.children[2].name\n" - "child1_params = tree.children[2].params\n"); + // Register Lua handlers first so create_node creates luaTask nodes + bool ok = runLua(L, + "ecs.behavior_tree.register_node('say_hello', " + " function(e, p) return 'success' end)\n" + "ecs.behavior_tree.register_node('wait_for_duration', " + " function(e, p) return 'success' end)\n"); + if (!ok) + FAIL("failed to register Lua handlers"); + + ok = runLua(L, "local tree = {\n" + " type = 'sequence',\n" + " children = {\n" + " ecs.behavior_tree.create_node('say_hello', " + " 'message=Hello'),\n" + " ecs.behavior_tree.create_node('wait_for_duration', " + " 'duration=2.0,timer_key=test'),\n" + " ecs.behavior_tree.create_node('say_hello', " + " 'message=Done')\n" + " }\n" + "}\n" + "tree_type = tree.type\n" + "child_count = #tree.children\n" + "child0_type = tree.children[1].type\n" + "child0_name = tree.children[1].name\n" + "child0_params = tree.children[1].params\n" + "child1_type = tree.children[2].type\n" + "child1_name = tree.children[2].name\n" + "child1_params = tree.children[2].params\n"); if (!ok) FAIL("failed to create tree with Lua nodes"); @@ -408,13 +422,17 @@ static int testEmptyList(lua_State *L) { TEST("list_nodes returns empty when no nodes"); - // Unregister all nodes we created - bool ok = runLua(L, "ecs.behavior_tree.unregister_node('test_node')\n" - "ecs.behavior_tree.unregister_node('node_b')\n" - "ecs.behavior_tree.unregister_node('node_c')\n" - "ecs.behavior_tree.unregister_node('replaceable')\n" - "local nodes = ecs.behavior_tree.list_nodes()\n" - "empty_count = #nodes\n"); + // Unregister all nodes we created (including those from tests 6 and 7) + bool ok = runLua( + L, "ecs.behavior_tree.unregister_node('test_node')\n" + "ecs.behavior_tree.unregister_node('node_b')\n" + "ecs.behavior_tree.unregister_node('node_c')\n" + "ecs.behavior_tree.unregister_node('replaceable')\n" + "ecs.behavior_tree.unregister_node('simple_node')\n" + "ecs.behavior_tree.unregister_node('say_hello')\n" + "ecs.behavior_tree.unregister_node('wait_for_duration')\n" + "local nodes = ecs.behavior_tree.list_nodes()\n" + "empty_count = #nodes\n"); if (!ok) FAIL("failed to unregister all nodes"); @@ -631,6 +649,625 @@ static int testFullPipelineUnregistered(lua_State *L) return 0; } +// --------------------------------------------------------------------------- +// Test 17: create_node with built-in node type (setAnimationState) +// --------------------------------------------------------------------------- + +static int testCreateNodeBuiltinType(lua_State *L) +{ + TEST("create_node with built-in setAnimationState type"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('setAnimationState', " + " 'locomotion/walk')\n" + "anim_type = node.type\n" + "anim_name = node.name\n" + "anim_has_params = (node.params ~= nil)\n"); + if (!ok) + FAIL("failed to create setAnimationState node"); + + std::string type = getGlobalString(L, "anim_type"); + std::string name = getGlobalString(L, "anim_name"); + + if (type != "setAnimationState") + FAIL("expected type 'setAnimationState', got '" + type + "'"); + if (name != "locomotion/walk") + FAIL("expected name 'locomotion/walk', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 18: create_node with built-in delay type and params +// --------------------------------------------------------------------------- + +static int testCreateNodeDelay(lua_State *L) +{ + TEST("create_node with built-in delay type and params"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('delay', '', '2.0')\n" + "delay_type = node.type\n" + "delay_params = node.params\n" + "delay_has_name = (node.name ~= nil and node.name ~= '')\n"); + if (!ok) + FAIL("failed to create delay node"); + + std::string type = getGlobalString(L, "delay_type"); + std::string params = getGlobalString(L, "delay_params"); + + if (type != "delay") + FAIL("expected type 'delay', got '" + type + "'"); + if (params != "2.0") + FAIL("expected params '2.0', got '" + params + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 19: create_node with built-in isAnimationEnded type +// --------------------------------------------------------------------------- + +static int testCreateNodeAnimEnded(lua_State *L) +{ + TEST("create_node with built-in isAnimationEnded type"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('isAnimationEnded', " + " 'locomotion')\n" + "anim_end_type = node.type\n" + "anim_end_name = node.name\n"); + if (!ok) + FAIL("failed to create isAnimationEnded node"); + + std::string type = getGlobalString(L, "anim_end_type"); + std::string name = getGlobalString(L, "anim_end_name"); + + if (type != "isAnimationEnded") + FAIL("expected type 'isAnimationEnded', got '" + type + "'"); + if (name != "locomotion") + FAIL("expected name 'locomotion', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 20: create_node with built-in debugPrint type +// --------------------------------------------------------------------------- + +static int testCreateNodeDebugPrint(lua_State *L) +{ + TEST("create_node with built-in debugPrint type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('debugPrint', " + " 'Hello World')\n" + "dbg_type = node.type\n" + "dbg_name = node.name\n"); + if (!ok) + FAIL("failed to create debugPrint node"); + + std::string type = getGlobalString(L, "dbg_type"); + std::string name = getGlobalString(L, "dbg_name"); + + if (type != "debugPrint") + FAIL("expected type 'debugPrint', got '" + type + "'"); + if (name != "Hello World") + FAIL("expected name 'Hello World', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 21: create_node with built-in sendEvent type +// --------------------------------------------------------------------------- + +static int testCreateNodeSendEvent(lua_State *L) +{ + TEST("create_node with built-in sendEvent type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('sendEvent', " + " 'quest_started', 'quest_id=42')\n" + "ev_type = node.type\n" + "ev_name = node.name\n" + "ev_params = node.params\n"); + if (!ok) + FAIL("failed to create sendEvent node"); + + std::string type = getGlobalString(L, "ev_type"); + std::string name = getGlobalString(L, "ev_name"); + std::string params = getGlobalString(L, "ev_params"); + + if (type != "sendEvent") + FAIL("expected type 'sendEvent', got '" + type + "'"); + if (name != "quest_started") + FAIL("expected name 'quest_started', got '" + name + "'"); + if (params != "quest_id=42") + FAIL("expected params 'quest_id=42', got '" + params + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 22: create_node with built-in setBit type +// --------------------------------------------------------------------------- + +static int testCreateNodeSetBit(lua_State *L) +{ + TEST("create_node with built-in setBit type"); + + bool ok = runLua(L, + "local node = ecs.behavior_tree.create_node('setBit', " + " 'has_sword', '1')\n" + "bit_type = node.type\n" + "bit_name = node.name\n" + "bit_params = node.params\n"); + if (!ok) + FAIL("failed to create setBit node"); + + std::string type = getGlobalString(L, "bit_type"); + std::string name = getGlobalString(L, "bit_name"); + std::string params = getGlobalString(L, "bit_params"); + + if (type != "setBit") + FAIL("expected type 'setBit', got '" + type + "'"); + if (name != "has_sword") + FAIL("expected name 'has_sword', got '" + name + "'"); + if (params != "1") + FAIL("expected params '1', got '" + params + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 23: create_node with built-in checkBit type +// --------------------------------------------------------------------------- + +static int testCreateNodeCheckBit(lua_State *L) +{ + TEST("create_node with built-in checkBit type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('checkBit', " + " 'has_sword')\n" + "cbit_type = node.type\n" + "cbit_name = node.name\n"); + if (!ok) + FAIL("failed to create checkBit node"); + + std::string type = getGlobalString(L, "cbit_type"); + std::string name = getGlobalString(L, "cbit_name"); + + if (type != "checkBit") + FAIL("expected type 'checkBit', got '" + type + "'"); + if (name != "has_sword") + FAIL("expected name 'has_sword', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 24: create_node with built-in setValue type +// --------------------------------------------------------------------------- + +static int testCreateNodeSetValue(lua_State *L) +{ + TEST("create_node with built-in setValue type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('setValue', " + " 'gold', '42')\n" + "sv_type = node.type\n" + "sv_name = node.name\n" + "sv_params = node.params\n"); + if (!ok) + FAIL("failed to create setValue node"); + + std::string type = getGlobalString(L, "sv_type"); + std::string name = getGlobalString(L, "sv_name"); + std::string params = getGlobalString(L, "sv_params"); + + if (type != "setValue") + FAIL("expected type 'setValue', got '" + type + "'"); + if (name != "gold") + FAIL("expected name 'gold', got '" + name + "'"); + if (params != "42") + FAIL("expected params '42', got '" + params + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 25: create_node with built-in checkValue type +// --------------------------------------------------------------------------- + +static int testCreateNodeCheckValue(lua_State *L) +{ + TEST("create_node with built-in checkValue type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('checkValue', " + " 'gold', '> 10')\n" + "cv_type = node.type\n" + "cv_name = node.name\n" + "cv_params = node.params\n"); + if (!ok) + FAIL("failed to create checkValue node"); + + std::string type = getGlobalString(L, "cv_type"); + std::string name = getGlobalString(L, "cv_name"); + std::string params = getGlobalString(L, "cv_params"); + + if (type != "checkValue") + FAIL("expected type 'checkValue', got '" + type + "'"); + if (name != "gold") + FAIL("expected name 'gold', got '" + name + "'"); + if (params != "> 10") + FAIL("expected params '> 10', got '" + params + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 26: create_node with built-in teleportToChild type +// --------------------------------------------------------------------------- + +static int testCreateNodeTeleport(lua_State *L) +{ + TEST("create_node with built-in teleportToChild type"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('teleportToChild', " + " 'entrance')\n" + "tel_type = node.type\n" + "tel_name = node.name\n"); + if (!ok) + FAIL("failed to create teleportToChild node"); + + std::string type = getGlobalString(L, "tel_type"); + std::string name = getGlobalString(L, "tel_name"); + + if (type != "teleportToChild") + FAIL("expected type 'teleportToChild', got '" + type + "'"); + if (name != "entrance") + FAIL("expected name 'entrance', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 27: create_node with built-in disablePhysics type +// --------------------------------------------------------------------------- + +static int testCreateNodeDisablePhysics(lua_State *L) +{ + TEST("create_node with built-in disablePhysics type"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('disablePhysics')\n" + "dphys_type = node.type\n"); + if (!ok) + FAIL("failed to create disablePhysics node"); + + std::string type = getGlobalString(L, "dphys_type"); + if (type != "disablePhysics") + FAIL("expected type 'disablePhysics', got '" + type + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 28: create_node with built-in enablePhysics type +// --------------------------------------------------------------------------- + +static int testCreateNodeEnablePhysics(lua_State *L) +{ + TEST("create_node with built-in enablePhysics type"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('enablePhysics')\n" + "ephys_type = node.type\n"); + if (!ok) + FAIL("failed to create enablePhysics node"); + + std::string type = getGlobalString(L, "ephys_type"); + if (type != "enablePhysics") + FAIL("expected type 'enablePhysics', got '" + type + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 29: create_node with built-in blackboardDump type +// --------------------------------------------------------------------------- + +static int testCreateNodeBlackboardDump(lua_State *L) +{ + TEST("create_node with built-in blackboardDump type"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('blackboardDump', " + " 'Debug')\n" + "bbdump_type = node.type\n" + "bbdump_name = node.name\n"); + if (!ok) + FAIL("failed to create blackboardDump node"); + + std::string type = getGlobalString(L, "bbdump_type"); + std::string name = getGlobalString(L, "bbdump_name"); + + if (type != "blackboardDump") + FAIL("expected type 'blackboardDump', got '" + type + "'"); + if (name != "Debug") + FAIL("expected name 'Debug', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 30: create_node with built-in hasItem type +// --------------------------------------------------------------------------- + +static int testCreateNodeHasItem(lua_State *L) +{ + TEST("create_node with built-in hasItem type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('hasItem', " + " 'sword_01')\n" + "hi_type = node.type\n" + "hi_name = node.name\n"); + if (!ok) + FAIL("failed to create hasItem node"); + + std::string type = getGlobalString(L, "hi_type"); + std::string name = getGlobalString(L, "hi_name"); + + if (type != "hasItem") + FAIL("expected type 'hasItem', got '" + type + "'"); + if (name != "sword_01") + FAIL("expected name 'sword_01', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 31: create_node with built-in pickupItem type +// --------------------------------------------------------------------------- + +static int testCreateNodePickupItem(lua_State *L) +{ + TEST("create_node with built-in pickupItem type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('pickupItem', " + " 'sword_01')\n" + "pi_type = node.type\n" + "pi_name = node.name\n"); + if (!ok) + FAIL("failed to create pickupItem node"); + + std::string type = getGlobalString(L, "pi_type"); + std::string name = getGlobalString(L, "pi_name"); + + if (type != "pickupItem") + FAIL("expected type 'pickupItem', got '" + type + "'"); + if (name != "sword_01") + FAIL("expected name 'sword_01', got '" + name + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 32: create_node with built-in addItemToInventory type +// --------------------------------------------------------------------------- + +static int testCreateNodeAddItem(lua_State *L) +{ + TEST("create_node with built-in addItemToInventory type"); + + bool ok = runLua( + L, + "local node = ecs.behavior_tree.create_node('addItemToInventory', " + " 'potion_01', 'Potion of Healing,misc,1,0.5,10')\n" + "ai_type = node.type\n" + "ai_name = node.name\n" + "ai_params = node.params\n"); + if (!ok) + FAIL("failed to create addItemToInventory node"); + + std::string type = getGlobalString(L, "ai_type"); + std::string name = getGlobalString(L, "ai_name"); + std::string params = getGlobalString(L, "ai_params"); + + if (type != "addItemToInventory") + FAIL("expected type 'addItemToInventory', got '" + type + "'"); + if (name != "potion_01") + FAIL("expected name 'potion_01', got '" + name + "'"); + if (params != "Potion of Healing,misc,1,0.5,10") + FAIL("expected params 'Potion of Healing,misc,1,0.5,10', got '" + + params + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 33: create_node with built-in sequence type (composite node) +// --------------------------------------------------------------------------- + +static int testCreateNodeSequence(lua_State *L) +{ + TEST("create_node with built-in sequence type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('sequence')\n" + "seq_type = node.type\n"); + if (!ok) + FAIL("failed to create sequence node"); + + std::string type = getGlobalString(L, "seq_type"); + if (type != "sequence") + FAIL("expected type 'sequence', got '" + type + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 34: create_node with built-in selector type (composite node) +// --------------------------------------------------------------------------- + +static int testCreateNodeSelector(lua_State *L) +{ + TEST("create_node with built-in selector type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('selector')\n" + "sel_type = node.type\n"); + if (!ok) + FAIL("failed to create selector node"); + + std::string type = getGlobalString(L, "sel_type"); + if (type != "selector") + FAIL("expected type 'selector', got '" + type + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 35: create_node with built-in invert type (decorator node) +// --------------------------------------------------------------------------- + +static int testCreateNodeInvert(lua_State *L) +{ + TEST("create_node with built-in invert type"); + + bool ok = runLua( + L, "local node = ecs.behavior_tree.create_node('invert')\n" + "inv_type = node.type\n"); + if (!ok) + FAIL("failed to create invert node"); + + std::string type = getGlobalString(L, "inv_type"); + if (type != "invert") + FAIL("expected type 'invert', got '" + type + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 36: create_node with built-in nodes in a full behavior tree +// --------------------------------------------------------------------------- + +static int testCreateNodeFullTree(lua_State *L) +{ + TEST("create_node with built-in nodes in a full behavior tree"); + + bool ok = runLua( + L, + "local tree = {\n" + " type = 'selector',\n" + " children = {\n" + " {\n" + " type = 'sequence',\n" + " children = {\n" + " ecs.behavior_tree.create_node('checkBit', 'has_sword'),\n" + " ecs.behavior_tree.create_node('debugPrint', 'Already have sword'),\n" + " ecs.behavior_tree.create_node('setAnimationState', 'locomotion/idle')\n" + " }\n" + " },\n" + " {\n" + " type = 'sequence',\n" + " children = {\n" + " ecs.behavior_tree.create_node('setAnimationState', 'locomotion/walk'),\n" + " ecs.behavior_tree.create_node('delay', '', '2.0'),\n" + " ecs.behavior_tree.create_node('addItemToInventory', 'sword_01', " + "'Iron Sword,weapon,1,2.5,50'),\n" + " ecs.behavior_tree.create_node('setAnimationState', 'locomotion/idle'),\n" + " ecs.behavior_tree.create_node('debugPrint', 'Got the sword!')\n" + " }\n" + " }\n" + " }\n" + "}\n" + "full_tree_type = tree.type\n" + "full_tree_children = #tree.children\n" + "full_c0_type = tree.children[1].type\n" + "full_c0_children = #tree.children[1].children\n" + "full_c0_c0_type = tree.children[1].children[1].type\n" + "full_c0_c0_name = tree.children[1].children[1].name\n" + "full_c1_type = tree.children[2].type\n" + "full_c1_children = #tree.children[2].children\n" + "full_c1_c2_type = tree.children[2].children[3].type\n" + "full_c1_c2_name = tree.children[2].children[3].name\n"); + if (!ok) + FAIL("failed to create full behavior tree"); + + std::string treeType = getGlobalString(L, "full_tree_type"); + int childCount = getGlobalInt(L, "full_tree_children"); + + if (treeType != "selector") + FAIL("expected tree type 'selector'"); + if (childCount != 2) + FAIL("expected 2 children"); + + std::string c0type = getGlobalString(L, "full_c0_type"); + int c0children = getGlobalInt(L, "full_c0_children"); + if (c0type != "sequence") + FAIL("expected child 0 type 'sequence'"); + if (c0children != 3) + FAIL("expected child 0 to have 3 children"); + + std::string c0c0type = getGlobalString(L, "full_c0_c0_type"); + std::string c0c0name = getGlobalString(L, "full_c0_c0_name"); + if (c0c0type != "checkBit") + FAIL("expected child 0.0 type 'checkBit'"); + if (c0c0name != "has_sword") + FAIL("expected child 0.0 name 'has_sword'"); + + std::string c1type = getGlobalString(L, "full_c1_type"); + int c1children = getGlobalInt(L, "full_c1_children"); + if (c1type != "sequence") + FAIL("expected child 1 type 'sequence'"); + if (c1children != 5) + FAIL("expected child 1 to have 5 children"); + + std::string c1c2type = getGlobalString(L, "full_c1_c2_type"); + std::string c1c2name = getGlobalString(L, "full_c1_c2_name"); + if (c1c2type != "addItemToInventory") + FAIL("expected child 1.2 type 'addItemToInventory'"); + if (c1c2name != "sword_01") + FAIL("expected child 1.2 name 'sword_01'"); + + PASS(); + return 0; +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -669,6 +1306,26 @@ int main() failures += testFullPipelineParams(L); failures += testFullPipelineEntityId(L); failures += testFullPipelineUnregistered(L); + failures += testCreateNodeBuiltinType(L); + failures += testCreateNodeDelay(L); + failures += testCreateNodeAnimEnded(L); + failures += testCreateNodeDebugPrint(L); + failures += testCreateNodeSendEvent(L); + failures += testCreateNodeSetBit(L); + failures += testCreateNodeCheckBit(L); + failures += testCreateNodeSetValue(L); + failures += testCreateNodeCheckValue(L); + failures += testCreateNodeTeleport(L); + failures += testCreateNodeDisablePhysics(L); + failures += testCreateNodeEnablePhysics(L); + failures += testCreateNodeBlackboardDump(L); + failures += testCreateNodeHasItem(L); + failures += testCreateNodePickupItem(L); + failures += testCreateNodeAddItem(L); + failures += testCreateNodeSequence(L); + failures += testCreateNodeSelector(L); + failures += testCreateNodeInvert(L); + failures += testCreateNodeFullTree(L); // Cleanup lua_close(L);