gopeak 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ @tool
2
+ extends Node
3
+ class_name MCPToolExecutor
4
+
5
+ var _editor_plugin: EditorPlugin = null
6
+
7
+ var _scene_tools: Node = null
8
+ var _resource_tools: Node = null
9
+ var _animation_tools: Node = null
10
+
11
+ var _tool_map: Dictionary = {}
12
+ var _initialized := false
13
+
14
+
15
+ func set_editor_plugin(plugin: EditorPlugin) -> void:
16
+ _editor_plugin = plugin
17
+ _init_tools()
18
+
19
+ if _scene_tools and _scene_tools.has_method("set_editor_plugin"):
20
+ _scene_tools.set_editor_plugin(plugin)
21
+ if _resource_tools and _resource_tools.has_method("set_editor_plugin"):
22
+ _resource_tools.set_editor_plugin(plugin)
23
+ if _animation_tools and _animation_tools.has_method("set_editor_plugin"):
24
+ _animation_tools.set_editor_plugin(plugin)
25
+
26
+
27
+ func _init_tools() -> void:
28
+ if _initialized:
29
+ return
30
+ _initialized = true
31
+
32
+ var base_path: String = get_script().resource_path.get_base_dir()
33
+ var scene_tools_path := "%s/tools/scene_tools.gd" % base_path
34
+ var resource_tools_path := "%s/tools/resource_tools.gd" % base_path
35
+ var animation_tools_path := "%s/tools/animation_tools.gd" % base_path
36
+
37
+ if ResourceLoader.exists(scene_tools_path):
38
+ var scene_script: Script = load(scene_tools_path)
39
+ if scene_script:
40
+ _scene_tools = scene_script.new()
41
+ _scene_tools.name = "SceneTools"
42
+ add_child(_scene_tools)
43
+
44
+ if ResourceLoader.exists(resource_tools_path):
45
+ var resource_script: Script = load(resource_tools_path)
46
+ if resource_script:
47
+ _resource_tools = resource_script.new()
48
+ _resource_tools.name = "ResourceTools"
49
+ add_child(_resource_tools)
50
+
51
+ if ResourceLoader.exists(animation_tools_path):
52
+ var animation_script: Script = load(animation_tools_path)
53
+ if animation_script:
54
+ _animation_tools = animation_script.new()
55
+ _animation_tools.name = "AnimationTools"
56
+ add_child(_animation_tools)
57
+
58
+ _tool_map = {
59
+ # Scene tools
60
+ "create_scene": [_scene_tools, "create_scene"],
61
+ "list_scene_nodes": [_scene_tools, "list_scene_nodes"],
62
+ "add_node": [_scene_tools, "add_node"],
63
+ "delete_node": [_scene_tools, "delete_node"],
64
+ "duplicate_node": [_scene_tools, "duplicate_node"],
65
+ "reparent_node": [_scene_tools, "reparent_node"],
66
+ "set_node_properties": [_scene_tools, "set_node_properties"],
67
+ "get_node_properties": [_scene_tools, "get_node_properties"],
68
+ "load_sprite": [_scene_tools, "load_sprite"],
69
+ "save_scene": [_scene_tools, "save_scene"],
70
+ "connect_signal": [_scene_tools, "connect_signal"],
71
+ "disconnect_signal": [_scene_tools, "disconnect_signal"],
72
+ "list_connections": [_scene_tools, "list_connections"],
73
+
74
+ # Resource tools
75
+ "create_resource": [_resource_tools, "create_resource"],
76
+ "modify_resource": [_resource_tools, "modify_resource"],
77
+ "create_material": [_resource_tools, "create_material"],
78
+ "create_shader": [_resource_tools, "create_shader"],
79
+ "create_tileset": [_resource_tools, "create_tileset"],
80
+ "set_tilemap_cells": [_resource_tools, "set_tilemap_cells"],
81
+ "set_theme_color": [_resource_tools, "set_theme_color"],
82
+ "set_theme_font_size": [_resource_tools, "set_theme_font_size"],
83
+ "apply_theme_shader": [_resource_tools, "apply_theme_shader"],
84
+
85
+ # Animation tools
86
+ "create_animation": [_animation_tools, "create_animation"],
87
+ "add_animation_track": [_animation_tools, "add_animation_track"],
88
+ "create_animation_tree": [_animation_tools, "create_animation_tree"],
89
+ "add_animation_state": [_animation_tools, "add_animation_state"],
90
+ "connect_animation_states": [_animation_tools, "connect_animation_states"],
91
+ "create_navigation_region": [_animation_tools, "create_navigation_region"],
92
+ "create_navigation_agent": [_animation_tools, "create_navigation_agent"]
93
+ }
94
+
95
+
96
+ func execute_tool(tool_name: String, args: Dictionary) -> Dictionary:
97
+ if not _tool_map.has(tool_name):
98
+ return {"ok": false, "error": "Unknown tool: " + tool_name}
99
+
100
+ var handler: Array = _tool_map[tool_name]
101
+ var node: Node = handler[0]
102
+ var method: String = handler[1]
103
+
104
+ if node == null:
105
+ return {"ok": false, "error": "Tool handler unavailable: " + tool_name}
106
+
107
+ if not node.has_method(method):
108
+ return {"ok": false, "error": "Tool method not found: %s.%s" % [node.name, method]}
109
+
110
+ var result = node.call(method, args)
111
+ if result is Dictionary:
112
+ return result
113
+
114
+ return {"ok": false, "error": "Invalid tool result from: " + tool_name}
@@ -0,0 +1,502 @@
1
+ @tool
2
+ extends Node
3
+ class_name MCPAnimationTools
4
+
5
+ var _editor_plugin: EditorPlugin = null
6
+
7
+ func set_editor_plugin(plugin: EditorPlugin) -> void:
8
+ _editor_plugin = plugin
9
+
10
+
11
+ # =============================================================================
12
+ # Shared helpers
13
+ # =============================================================================
14
+ func _ensure_res_path(path: String) -> String:
15
+ if not path.begins_with("res://"): return "res://" + path
16
+ return path
17
+
18
+ func _refresh_and_reload(scene_path: String) -> void:
19
+ _refresh_filesystem()
20
+ _reload_scene_in_editor(scene_path)
21
+
22
+ func _refresh_filesystem() -> void:
23
+ if _editor_plugin:
24
+ EditorInterface.get_resource_filesystem().scan()
25
+
26
+ func _reload_scene_in_editor(scene_path: String) -> void:
27
+ if not _editor_plugin: return
28
+ var edited = EditorInterface.get_edited_scene_root()
29
+ if edited and edited.scene_file_path == scene_path:
30
+ EditorInterface.reload_scene_from_path(scene_path)
31
+
32
+ func _load_scene(scene_path: String) -> Array:
33
+ if not FileAccess.file_exists(scene_path):
34
+ return [null, {"ok": false, "error": "Scene not found: " + scene_path}]
35
+ var packed = load(scene_path) as PackedScene
36
+ if not packed: return [null, {"ok": false, "error": "Failed to load: " + scene_path}]
37
+ var root = packed.instantiate()
38
+ if not root: return [null, {"ok": false, "error": "Failed to instantiate: " + scene_path}]
39
+ return [root, {}]
40
+
41
+ func _save_scene(scene_root: Node, scene_path: String) -> Dictionary:
42
+ var packed = PackedScene.new()
43
+ if packed.pack(scene_root) != OK:
44
+ scene_root.queue_free()
45
+ return {"ok": false, "error": "Failed to pack scene"}
46
+ if ResourceSaver.save(packed, scene_path) != OK:
47
+ scene_root.queue_free()
48
+ return {"ok": false, "error": "Failed to save scene"}
49
+ scene_root.queue_free()
50
+ _refresh_and_reload(scene_path)
51
+ return {}
52
+
53
+ func _find_node(root: Node, path: String) -> Node:
54
+ if path == "." or path.is_empty(): return root
55
+ return root.get_node_or_null(path)
56
+
57
+ func _parse_value(value):
58
+ if typeof(value) == TYPE_DICTIONARY:
59
+ if value.has("type") or value.has("_type"):
60
+ var t = value.get("type", value.get("_type", ""))
61
+ match t:
62
+ "Vector2": return Vector2(value.get("x",0), value.get("y",0))
63
+ "Vector3": return Vector3(value.get("x",0), value.get("y",0), value.get("z",0))
64
+ "Color": return Color(value.get("r",1), value.get("g",1), value.get("b",1), value.get("a",1))
65
+ if typeof(value) == TYPE_ARRAY:
66
+ var result = []
67
+ for item in value:
68
+ result.append(_parse_value(item))
69
+ return result
70
+ return value
71
+
72
+ func _parse_json_maybe(value):
73
+ if typeof(value) != TYPE_STRING:
74
+ return value
75
+ var parsed = JSON.parse_string(value)
76
+ if parsed == null and value != "null":
77
+ return value
78
+ return parsed
79
+
80
+ func _parse_method_args(raw_args: Array) -> Array:
81
+ var parsed_args: Array = []
82
+ for raw_arg in raw_args:
83
+ var parsed = _parse_json_maybe(raw_arg)
84
+ parsed_args.append(_parse_value(parsed))
85
+ return parsed_args
86
+
87
+ func _get_default_animation_library(player: AnimationPlayer) -> AnimationLibrary:
88
+ var anim_lib: AnimationLibrary = player.get_animation_library("")
89
+ if anim_lib:
90
+ return anim_lib
91
+ anim_lib = AnimationLibrary.new()
92
+ var add_lib_err := player.add_animation_library("", anim_lib)
93
+ if add_lib_err != OK:
94
+ return null
95
+ return anim_lib
96
+
97
+ func _get_state_machine(anim_tree: AnimationTree, state_machine_path: String = "") -> AnimationNodeStateMachine:
98
+ if not anim_tree:
99
+ return null
100
+ if state_machine_path.is_empty() or state_machine_path == "root":
101
+ return anim_tree.tree_root as AnimationNodeStateMachine
102
+
103
+ var current = anim_tree.tree_root
104
+ for segment in state_machine_path.split("/", false):
105
+ if String(segment).is_empty():
106
+ continue
107
+ if current == null or not current.has_method("get_node"):
108
+ return null
109
+ current = current.call("get_node", StringName(segment))
110
+ return current as AnimationNodeStateMachine
111
+
112
+
113
+ # =============================================================================
114
+ # create_animation
115
+ # =============================================================================
116
+ func create_animation(args: Dictionary) -> Dictionary:
117
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
118
+ var player_node_path: String = str(args.get("playerNodePath", "."))
119
+ var animation_name: String = str(args.get("animationName", ""))
120
+ var loop_mode_name: String = str(args.get("loopMode", "none"))
121
+
122
+ if scene_path.strip_edges() == "res://":
123
+ return {"ok": false, "error": "Missing scenePath"}
124
+ if animation_name.strip_edges().is_empty():
125
+ return {"ok": false, "error": "Missing animationName"}
126
+
127
+ var loaded := _load_scene(scene_path)
128
+ if not loaded[1].is_empty():
129
+ return loaded[1]
130
+
131
+ var scene_root: Node = loaded[0]
132
+ var player = _find_node(scene_root, player_node_path) as AnimationPlayer
133
+ if not player:
134
+ scene_root.queue_free()
135
+ return {"ok": false, "error": "AnimationPlayer not found at: " + player_node_path}
136
+
137
+ var anim_lib := _get_default_animation_library(player)
138
+ if not anim_lib:
139
+ scene_root.queue_free()
140
+ return {"ok": false, "error": "Failed to create default AnimationLibrary"}
141
+ if anim_lib.has_animation(StringName(animation_name)):
142
+ scene_root.queue_free()
143
+ return {"ok": false, "error": "Animation already exists: " + animation_name}
144
+
145
+ var loop_mode := Animation.LOOP_NONE
146
+ match loop_mode_name:
147
+ "linear": loop_mode = Animation.LOOP_LINEAR
148
+ "pingpong": loop_mode = Animation.LOOP_PINGPONG
149
+ _:
150
+ loop_mode_name = "none"
151
+ loop_mode = Animation.LOOP_NONE
152
+
153
+ var anim = Animation.new()
154
+ anim.length = float(args.get("length", 1.0))
155
+ anim.loop_mode = loop_mode
156
+ anim.step = float(args.get("step", 0.1))
157
+
158
+ var add_err := anim_lib.add_animation(StringName(animation_name), anim)
159
+ if add_err != OK:
160
+ scene_root.queue_free()
161
+ return {"ok": false, "error": "Failed to add animation: " + str(add_err)}
162
+
163
+ var save_err := _save_scene(scene_root, scene_path)
164
+ if not save_err.is_empty():
165
+ return save_err
166
+
167
+ return {
168
+ "ok": true,
169
+ "animationName": animation_name,
170
+ "length": anim.length,
171
+ "loopMode": loop_mode_name,
172
+ }
173
+
174
+
175
+ # =============================================================================
176
+ # add_animation_track
177
+ # =============================================================================
178
+ func add_animation_track(args: Dictionary) -> Dictionary:
179
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
180
+ var player_node_path: String = str(args.get("playerNodePath", "."))
181
+ var animation_name: String = str(args.get("animationName", ""))
182
+ var track: Dictionary = args.get("track", {})
183
+
184
+ if scene_path.strip_edges() == "res://":
185
+ return {"ok": false, "error": "Missing scenePath"}
186
+ if animation_name.strip_edges().is_empty():
187
+ return {"ok": false, "error": "Missing animationName"}
188
+ if track.is_empty():
189
+ return {"ok": false, "error": "Missing track"}
190
+
191
+ var loaded := _load_scene(scene_path)
192
+ if not loaded[1].is_empty():
193
+ return loaded[1]
194
+
195
+ var scene_root: Node = loaded[0]
196
+ var player = _find_node(scene_root, player_node_path) as AnimationPlayer
197
+ if not player:
198
+ scene_root.queue_free()
199
+ return {"ok": false, "error": "AnimationPlayer not found at: " + player_node_path}
200
+
201
+ var anim_lib: AnimationLibrary = player.get_animation_library("")
202
+ if not anim_lib:
203
+ scene_root.queue_free()
204
+ return {"ok": false, "error": "Default AnimationLibrary not found"}
205
+
206
+ var anim: Animation = anim_lib.get_animation(StringName(animation_name))
207
+ if not anim:
208
+ scene_root.queue_free()
209
+ return {"ok": false, "error": "Animation not found: " + animation_name}
210
+
211
+ var track_type: String = str(track.get("type", ""))
212
+ var track_idx := -1
213
+ var keyframes: Array = track.get("keyframes", [])
214
+
215
+ match track_type:
216
+ "property":
217
+ var node_path_str: String = str(track.get("nodePath", ""))
218
+ var prop_name: String = str(track.get("property", ""))
219
+ if prop_name.is_empty():
220
+ scene_root.queue_free()
221
+ return {"ok": false, "error": "track.property is required for property track"}
222
+ track_idx = anim.add_track(Animation.TYPE_VALUE)
223
+ anim.track_set_path(track_idx, NodePath(node_path_str + ":" + prop_name))
224
+ for keyframe in keyframes:
225
+ if typeof(keyframe) != TYPE_DICTIONARY:
226
+ continue
227
+ var raw_value = keyframe.get("value")
228
+ var parsed_value = _parse_json_maybe(raw_value) if typeof(raw_value) == TYPE_STRING else raw_value
229
+ anim.track_insert_key(track_idx, float(keyframe.get("time", 0.0)), _parse_value(parsed_value))
230
+
231
+ "method":
232
+ var method_node_path: String = str(track.get("nodePath", ""))
233
+ var method_name: String = str(track.get("method", ""))
234
+ if method_name.is_empty():
235
+ scene_root.queue_free()
236
+ return {"ok": false, "error": "track.method is required for method track"}
237
+ track_idx = anim.add_track(Animation.TYPE_METHOD)
238
+ anim.track_set_path(track_idx, NodePath(method_node_path))
239
+ for keyframe in keyframes:
240
+ if typeof(keyframe) != TYPE_DICTIONARY:
241
+ continue
242
+ anim.track_insert_key(
243
+ track_idx,
244
+ float(keyframe.get("time", 0.0)),
245
+ {
246
+ "method": method_name,
247
+ "args": _parse_method_args(keyframe.get("args", [])),
248
+ }
249
+ )
250
+
251
+ _:
252
+ scene_root.queue_free()
253
+ return {"ok": false, "error": "Unsupported track.type: " + track_type}
254
+
255
+ var save_err := _save_scene(scene_root, scene_path)
256
+ if not save_err.is_empty():
257
+ return save_err
258
+
259
+ return {"ok": true, "trackType": track_type, "trackIndex": track_idx}
260
+
261
+
262
+ # =============================================================================
263
+ # create_animation_tree
264
+ # =============================================================================
265
+ func create_animation_tree(args: Dictionary) -> Dictionary:
266
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
267
+ var parent_path: String = str(args.get("parentPath", "."))
268
+ var node_name: String = str(args.get("nodeName", "AnimationTree"))
269
+ var anim_player_path: String = str(args.get("animPlayerPath", ""))
270
+ var root_type: String = str(args.get("rootType", "StateMachine"))
271
+
272
+ if scene_path.strip_edges() == "res://":
273
+ return {"ok": false, "error": "Missing scenePath"}
274
+ if anim_player_path.is_empty():
275
+ return {"ok": false, "error": "Missing animPlayerPath"}
276
+
277
+ var loaded := _load_scene(scene_path)
278
+ if not loaded[1].is_empty():
279
+ return loaded[1]
280
+
281
+ var scene_root: Node = loaded[0]
282
+ var parent = _find_node(scene_root, parent_path)
283
+ if not parent:
284
+ scene_root.queue_free()
285
+ return {"ok": false, "error": "Parent node not found: " + parent_path}
286
+
287
+ var anim_tree := AnimationTree.new()
288
+ anim_tree.name = node_name
289
+ anim_tree.anim_player = NodePath(anim_player_path)
290
+
291
+ var root = null
292
+ match root_type:
293
+ "StateMachine": root = AnimationNodeStateMachine.new()
294
+ "BlendTree": root = AnimationNodeBlendTree.new()
295
+ "BlendSpace1D": root = AnimationNodeBlendSpace1D.new()
296
+ "BlendSpace2D": root = AnimationNodeBlendSpace2D.new()
297
+ _:
298
+ scene_root.queue_free()
299
+ return {"ok": false, "error": "Unsupported rootType: " + root_type}
300
+
301
+ anim_tree.tree_root = root
302
+ parent.add_child(anim_tree)
303
+ anim_tree.owner = scene_root
304
+
305
+ var save_err := _save_scene(scene_root, scene_path)
306
+ if not save_err.is_empty():
307
+ return save_err
308
+
309
+ return {"ok": true, "nodeName": node_name, "rootType": root_type}
310
+
311
+
312
+ # =============================================================================
313
+ # add_animation_state
314
+ # =============================================================================
315
+ func add_animation_state(args: Dictionary) -> Dictionary:
316
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
317
+ var anim_tree_path: String = str(args.get("animTreePath", ""))
318
+ var state_name: String = str(args.get("stateName", ""))
319
+ var animation_name: String = str(args.get("animationName", ""))
320
+ var state_machine_path: String = str(args.get("stateMachinePath", ""))
321
+
322
+ if scene_path.strip_edges() == "res://":
323
+ return {"ok": false, "error": "Missing scenePath"}
324
+ if anim_tree_path.is_empty():
325
+ return {"ok": false, "error": "Missing animTreePath"}
326
+ if state_name.is_empty():
327
+ return {"ok": false, "error": "Missing stateName"}
328
+ if animation_name.is_empty():
329
+ return {"ok": false, "error": "Missing animationName"}
330
+
331
+ var loaded := _load_scene(scene_path)
332
+ if not loaded[1].is_empty():
333
+ return loaded[1]
334
+
335
+ var scene_root: Node = loaded[0]
336
+ var anim_tree = _find_node(scene_root, anim_tree_path) as AnimationTree
337
+ if not anim_tree:
338
+ scene_root.queue_free()
339
+ return {"ok": false, "error": "AnimationTree not found at: " + anim_tree_path}
340
+
341
+ var sm := _get_state_machine(anim_tree, state_machine_path)
342
+ if not sm:
343
+ scene_root.queue_free()
344
+ return {"ok": false, "error": "AnimationNodeStateMachine not found"}
345
+
346
+ var anim_node := AnimationNodeAnimation.new()
347
+ anim_node.animation = StringName(animation_name)
348
+ sm.add_node(StringName(state_name), anim_node)
349
+
350
+ var save_err := _save_scene(scene_root, scene_path)
351
+ if not save_err.is_empty():
352
+ return save_err
353
+
354
+ return {"ok": true, "stateName": state_name, "animationName": animation_name}
355
+
356
+
357
+ # =============================================================================
358
+ # connect_animation_states
359
+ # =============================================================================
360
+ func connect_animation_states(args: Dictionary) -> Dictionary:
361
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
362
+ var anim_tree_path: String = str(args.get("animTreePath", ""))
363
+ var from_state: String = str(args.get("fromState", ""))
364
+ var to_state: String = str(args.get("toState", ""))
365
+ var transition_type: String = str(args.get("transitionType", "immediate"))
366
+ var state_machine_path: String = str(args.get("stateMachinePath", ""))
367
+ var advance_condition: String = str(args.get("advanceCondition", ""))
368
+
369
+ if scene_path.strip_edges() == "res://":
370
+ return {"ok": false, "error": "Missing scenePath"}
371
+ if anim_tree_path.is_empty():
372
+ return {"ok": false, "error": "Missing animTreePath"}
373
+ if from_state.is_empty() or to_state.is_empty():
374
+ return {"ok": false, "error": "Missing fromState or toState"}
375
+
376
+ var loaded := _load_scene(scene_path)
377
+ if not loaded[1].is_empty():
378
+ return loaded[1]
379
+
380
+ var scene_root: Node = loaded[0]
381
+ var anim_tree = _find_node(scene_root, anim_tree_path) as AnimationTree
382
+ if not anim_tree:
383
+ scene_root.queue_free()
384
+ return {"ok": false, "error": "AnimationTree not found at: " + anim_tree_path}
385
+
386
+ var sm := _get_state_machine(anim_tree, state_machine_path)
387
+ if not sm:
388
+ scene_root.queue_free()
389
+ return {"ok": false, "error": "AnimationNodeStateMachine not found"}
390
+
391
+ var transition := AnimationNodeStateMachineTransition.new()
392
+ match transition_type:
393
+ "sync": transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC
394
+ "at_end": transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END
395
+ "immediate": transition.switch_mode = AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE
396
+ _:
397
+ scene_root.queue_free()
398
+ return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
399
+
400
+ if not advance_condition.is_empty():
401
+ transition.advance_condition = StringName(advance_condition)
402
+
403
+ sm.add_transition(StringName(from_state), StringName(to_state), transition)
404
+
405
+ var save_err := _save_scene(scene_root, scene_path)
406
+ if not save_err.is_empty():
407
+ return save_err
408
+
409
+ return {"ok": true, "from": from_state, "to": to_state}
410
+
411
+
412
+ # =============================================================================
413
+ # create_navigation_region
414
+ # =============================================================================
415
+ func create_navigation_region(args: Dictionary) -> Dictionary:
416
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
417
+ var parent_path: String = str(args.get("parentPath", "."))
418
+ var node_name: String = str(args.get("nodeName", "NavigationRegion"))
419
+ var is_3d: bool = bool(args.get("is3D", false))
420
+
421
+ if scene_path.strip_edges() == "res://":
422
+ return {"ok": false, "error": "Missing scenePath"}
423
+
424
+ var loaded := _load_scene(scene_path)
425
+ if not loaded[1].is_empty():
426
+ return loaded[1]
427
+
428
+ var scene_root: Node = loaded[0]
429
+ var parent = _find_node(scene_root, parent_path)
430
+ if not parent:
431
+ scene_root.queue_free()
432
+ return {"ok": false, "error": "Parent node not found: " + parent_path}
433
+
434
+ var nav: Node = null
435
+ if is_3d:
436
+ var nav3d := NavigationRegion3D.new()
437
+ nav3d.navigation_mesh = NavigationMesh.new()
438
+ nav = nav3d
439
+ else:
440
+ var nav2d := NavigationRegion2D.new()
441
+ nav2d.navigation_polygon = NavigationPolygon.new()
442
+ nav = nav2d
443
+
444
+ nav.name = node_name
445
+ parent.add_child(nav)
446
+ nav.owner = scene_root
447
+
448
+ var save_err := _save_scene(scene_root, scene_path)
449
+ if not save_err.is_empty():
450
+ return save_err
451
+
452
+ return {"ok": true, "nodeName": node_name, "is3D": is_3d}
453
+
454
+
455
+ # =============================================================================
456
+ # create_navigation_agent
457
+ # =============================================================================
458
+ func create_navigation_agent(args: Dictionary) -> Dictionary:
459
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
460
+ var parent_path: String = str(args.get("parentPath", "."))
461
+ var node_name: String = str(args.get("nodeName", "NavigationAgent"))
462
+ var is_3d: bool = bool(args.get("is3D", false))
463
+
464
+ if scene_path.strip_edges() == "res://":
465
+ return {"ok": false, "error": "Missing scenePath"}
466
+
467
+ var loaded := _load_scene(scene_path)
468
+ if not loaded[1].is_empty():
469
+ return loaded[1]
470
+
471
+ var scene_root: Node = loaded[0]
472
+ var parent = _find_node(scene_root, parent_path)
473
+ if not parent:
474
+ scene_root.queue_free()
475
+ return {"ok": false, "error": "Parent node not found: " + parent_path}
476
+
477
+ var agent: Node = null
478
+ if is_3d:
479
+ var agent3d := NavigationAgent3D.new()
480
+ agent3d.name = node_name
481
+ if args.has("pathDesiredDistance") and args.get("pathDesiredDistance") != null:
482
+ agent3d.path_desired_distance = float(args.get("pathDesiredDistance"))
483
+ if args.has("targetDesiredDistance") and args.get("targetDesiredDistance") != null:
484
+ agent3d.target_desired_distance = float(args.get("targetDesiredDistance"))
485
+ agent = agent3d
486
+ else:
487
+ var agent2d := NavigationAgent2D.new()
488
+ agent2d.name = node_name
489
+ if args.has("pathDesiredDistance") and args.get("pathDesiredDistance") != null:
490
+ agent2d.path_desired_distance = float(args.get("pathDesiredDistance"))
491
+ if args.has("targetDesiredDistance") and args.get("targetDesiredDistance") != null:
492
+ agent2d.target_desired_distance = float(args.get("targetDesiredDistance"))
493
+ agent = agent2d
494
+
495
+ parent.add_child(agent)
496
+ agent.owner = scene_root
497
+
498
+ var save_err := _save_scene(scene_root, scene_path)
499
+ if not save_err.is_empty():
500
+ return save_err
501
+
502
+ return {"ok": true, "nodeName": node_name, "is3D": is_3d}