@beremaran/godot-agent-loop 1.0.0

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.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. package/scripts/prepare-package.js +42 -0
@@ -0,0 +1,108 @@
1
+ extends Node
2
+
3
+ # Base class for a runtime command domain.
4
+ #
5
+ # A domain owns the handlers for one Godot subsystem plus any state those
6
+ # handlers keep (held keys, debug-draw objects, sockets), and registers them
7
+ # with the server's command registry. Domains are children of the interaction
8
+ # server node, so get_tree() and get_viewport() resolve exactly as they did
9
+ # when the handlers lived on the server itself.
10
+ #
11
+ # This class is the only place domain code touches the server: handlers call
12
+ # the helpers below and never see sessions, sockets, request IDs, or the
13
+ # registry. That keeps the transport layer free of subsystem knowledge and
14
+ # lets a domain move without changing the protocol implementation.
15
+
16
+ const CommandParams = preload("res://mcp_runtime/command_params.gd")
17
+ const VariantCodec = preload("res://mcp_runtime/variant_codec.gd")
18
+ # The composition root is always the autoload at res://mcp_interaction_server.gd.
19
+ # It loads its domains by path at runtime rather than preloading them, so naming
20
+ # its script here types every transport helper below without a preload cycle.
21
+ const InteractionServer = preload("res://mcp_interaction_server.gd")
22
+
23
+ var _server: InteractionServer
24
+ var _registrar: Callable
25
+ var _codec: VariantCodec
26
+
27
+
28
+ # Called by the composition root before register_commands(). `registrar` is the
29
+ # server's _register_command(command, handler) callable, which is what decides
30
+ # cancellability from the transport's declarative CANCELLABLE_COMMANDS list.
31
+ func setup(server: InteractionServer, registrar: Callable) -> void:
32
+ _server = server
33
+ _registrar = registrar
34
+ _codec = server._codec
35
+
36
+
37
+ # Overridden by each domain to register its commands via register_command().
38
+ func register_commands() -> void:
39
+ pass
40
+
41
+
42
+ func register_command(command: String, handler: Callable) -> void:
43
+ _registrar.call(command, handler)
44
+
45
+
46
+ # --- Transport helpers ---
47
+ # Mirrors of the server-side helpers, so a handler body reads the same whether
48
+ # it lives in a domain or in the composition root.
49
+
50
+ # Completes the active request. A result dictionary is sent as the JSON-RPC
51
+ # result; an {"error": ...} dictionary becomes a standardized command failure.
52
+ func respond(result: Dictionary) -> void:
53
+ _server._send_response(result)
54
+
55
+
56
+ # Sends the standardized -32000 failure and reports whether the handler must
57
+ # stop. Every handler calls this once after reading its parameters.
58
+ func params_invalid(reader: CommandParams) -> bool:
59
+ var invalid: bool = _server._params_invalid(reader)
60
+ return invalid
61
+
62
+
63
+ # The same failure for a handler that failed the reader itself, on a rule the
64
+ # accessors cannot express, and is about to stop.
65
+ func send_params_error(reader: CommandParams) -> void:
66
+ _server._send_params_error(reader)
67
+
68
+
69
+ func require_node(reader: CommandParams, param_name: String = "node_path", default_path: String = "") -> Node:
70
+ var node: Node = _server._require_node(reader, param_name, default_path)
71
+ return node
72
+
73
+
74
+ func godot_error_data(err: int) -> Dictionary:
75
+ var data: Dictionary = _server._godot_error_data(err)
76
+ return data
77
+
78
+
79
+ # JSON-safe encoding and typed decoding are owned by the shared codec service.
80
+ func variant_to_json(value: Variant) -> Variant:
81
+ _codec.configure(_server.max_json_nesting_depth, _server.max_json_collection_items)
82
+ var encoded: Variant = _codec.encode(value)
83
+ return encoded
84
+
85
+ func json_to_variant(value: Variant, type_hint: String = "") -> Variant:
86
+ _codec.configure(_server.max_json_nesting_depth, _server.max_json_collection_items)
87
+ return _codec.decode(value, type_hint)
88
+
89
+
90
+ func json_to_variant_for_property(node: Node, property: String, value: Variant) -> Variant:
91
+ _codec.configure(_server.max_json_nesting_depth, _server.max_json_collection_items)
92
+ return _codec.decode_for_property(node, property, value)
93
+
94
+
95
+ # Async signal waits query cancellation and report timeout through transport
96
+ # helpers without gaining access to sessions or request IDs.
97
+ func cancellation_requested() -> bool:
98
+ return _server._is_active_request_cancelled()
99
+
100
+
101
+ func respond_timeout(message: String, details: Dictionary = {}) -> void:
102
+ _server._send_timeout_response(message, details)
103
+
104
+
105
+ # A capability the active engine build cannot provide (rather than a bad request).
106
+ # Returning this keeps the caller from believing a silently-dropped write landed.
107
+ func respond_limit(message: String, details: Dictionary = {}) -> void:
108
+ _server._send_limit_response(message, details)
@@ -0,0 +1,527 @@
1
+ extends "res://mcp_runtime/runtime_domain.gd"
2
+
3
+ # 2D domain: TileMapLayer cells, canvas layers/modulate, immediate-mode canvas
4
+ # drawing, 2D lights and occluders, parallax backgrounds, Line2D/Polygon2D
5
+ # points, and Path2D curves. Owns the canvas-draw node and its command list.
6
+
7
+ # The draw node is created lazily on first draw and re-created if it was freed
8
+ # with the scene; the accumulated draw commands survive scene changes.
9
+ var _canvas_draw_node: Node2D = null
10
+ var _draw_commands: Array = []
11
+
12
+
13
+ # The draw node is parented into the scene, not under this domain, so it would
14
+ # outlive the server. Free it with the domain that owns it.
15
+ func _exit_tree() -> void:
16
+ _draw_commands.clear()
17
+ if _canvas_draw_node != null and is_instance_valid(_canvas_draw_node):
18
+ _canvas_draw_node.queue_free()
19
+ _canvas_draw_node = null
20
+
21
+
22
+ func register_commands() -> void:
23
+ register_command("tilemap", _cmd_tilemap)
24
+ register_command("canvas", _cmd_canvas)
25
+ register_command("canvas_draw", _cmd_canvas_draw)
26
+ register_command("light_2d", _cmd_light_2d)
27
+ register_command("parallax", _cmd_parallax)
28
+ register_command("shape_2d", _cmd_shape_2d)
29
+ register_command("path_2d", _cmd_path_2d)
30
+
31
+
32
+ # Records a structured failure when the resolved node is not the class the
33
+ # command drives; the caller still routes through params_invalid().
34
+ func _require_class(reader: CommandParams, node: Node, type_name: String) -> void:
35
+ if reader.failed() or node == null:
36
+ return
37
+ reader.fail("Node is not a %s: %s" % [type_name, node.get_class()],
38
+ {"param": "node_path", "reason": "invalid_value", "expected": type_name, "value": node.get_class()})
39
+
40
+
41
+ func _color_from(value: Dictionary) -> Color:
42
+ return Color(
43
+ CommandParams.json_float(value, "r", 1.0),
44
+ CommandParams.json_float(value, "g", 1.0),
45
+ CommandParams.json_float(value, "b", 1.0),
46
+ CommandParams.json_float(value, "a", 1.0),
47
+ )
48
+
49
+
50
+ func _vector2_from(value: Dictionary, default_x: float = 0.0, default_y: float = 0.0) -> Vector2:
51
+ return Vector2(CommandParams.json_float(value, "x", default_x), CommandParams.json_float(value, "y", default_y))
52
+
53
+
54
+ func _apply_optional_name(reader: CommandParams, node: Node) -> void:
55
+ var node_name: String = reader.optional_string("name")
56
+ if not node_name.is_empty():
57
+ node.name = node_name
58
+
59
+
60
+ # Validates that every element of an array parameter is an object; the caller
61
+ # still routes through params_invalid().
62
+ func _require_dictionary_items(reader: CommandParams, param_name: String, items: Array) -> void:
63
+ if reader.failed():
64
+ return
65
+ for item: Variant in items:
66
+ if not item is Dictionary:
67
+ reader.fail("%s must contain objects" % param_name, {"param": param_name, "reason": "invalid_type"})
68
+ return
69
+
70
+
71
+ func _points_from(reader: CommandParams, param_name: String) -> PackedVector2Array:
72
+ var points: Array = reader.optional_array(param_name)
73
+ _require_dictionary_items(reader, param_name, points)
74
+ var packed: PackedVector2Array = PackedVector2Array()
75
+ if reader.failed():
76
+ return packed
77
+ for point: Dictionary in points:
78
+ @warning_ignore("return_value_discarded")
79
+ packed.append(_vector2_from(point))
80
+ return packed
81
+
82
+
83
+ # --- TileMapLayer cells ---
84
+ func _cmd_tilemap(params: Dictionary) -> void:
85
+ var reader: CommandParams = CommandParams.new(params)
86
+ var node: Node = require_node(reader)
87
+ var action: String = reader.optional_enum("action", "get_cell", ["set_cells", "get_cell", "erase_cells", "get_used_cells"])
88
+ if node != null and not node is TileMapLayer:
89
+ _require_class(reader, node, "TileMapLayer")
90
+ if params_invalid(reader):
91
+ return
92
+
93
+ var tilemap: TileMapLayer = node as TileMapLayer
94
+ match action:
95
+ "set_cells":
96
+ var cells: Array = reader.required_array("cells")
97
+ _require_dictionary_items(reader, "cells", cells)
98
+ if params_invalid(reader):
99
+ return
100
+ for cell: Dictionary in cells:
101
+ var pos: Vector2i = Vector2i(CommandParams.json_int(cell, "x"), CommandParams.json_int(cell, "y"))
102
+ var source_id: int = CommandParams.json_int(cell, "source_id")
103
+ var atlas_coords: Vector2i = Vector2i(CommandParams.json_int(cell, "atlas_x"), CommandParams.json_int(cell, "atlas_y"))
104
+ var alt_tile: int = CommandParams.json_int(cell, "alt_tile")
105
+ tilemap.set_cell(pos, source_id, atlas_coords, alt_tile)
106
+ respond({"success": true, "action": "set_cells", "count": cells.size()})
107
+ "get_cell":
108
+ var x: int = reader.optional_int("x", 0)
109
+ var y: int = reader.optional_int("y", 0)
110
+ if params_invalid(reader):
111
+ return
112
+ var pos: Vector2i = Vector2i(x, y)
113
+ respond({
114
+ "success": true, "action": "get_cell",
115
+ "x": x, "y": y,
116
+ "source_id": tilemap.get_cell_source_id(pos),
117
+ "atlas_coords": variant_to_json(tilemap.get_cell_atlas_coords(pos)),
118
+ "alt_tile": tilemap.get_cell_alternative_tile(pos)
119
+ })
120
+ "erase_cells":
121
+ var cells: Array = reader.required_array("cells")
122
+ _require_dictionary_items(reader, "cells", cells)
123
+ if params_invalid(reader):
124
+ return
125
+ for cell: Dictionary in cells:
126
+ tilemap.erase_cell(Vector2i(CommandParams.json_int(cell, "x"), CommandParams.json_int(cell, "y")))
127
+ respond({"success": true, "action": "erase_cells", "count": cells.size()})
128
+ "get_used_cells":
129
+ var source_filter: int = reader.optional_int("source_id", -1)
130
+ if params_invalid(reader):
131
+ return
132
+ var used: Array
133
+ if source_filter >= 0:
134
+ used = tilemap.get_used_cells_by_id(source_filter)
135
+ else:
136
+ used = tilemap.get_used_cells()
137
+ respond({"success": true, "action": "get_used_cells", "cells": variant_to_json(used), "count": used.size()})
138
+
139
+
140
+ # --- CanvasLayer / CanvasModulate ---
141
+ func _cmd_canvas(params: Dictionary) -> void:
142
+ var reader: CommandParams = CommandParams.new(params)
143
+ var action: String = reader.optional_enum("action", "create_layer", ["create_layer", "create_modulate", "configure"])
144
+ if params_invalid(reader):
145
+ return
146
+
147
+ match action:
148
+ "create_layer":
149
+ var parent: Node = require_node(reader, "parent_path", "/root")
150
+ var layer: int = reader.optional_int("layer", 1)
151
+ if params_invalid(reader):
152
+ return
153
+ var cl: CanvasLayer = CanvasLayer.new()
154
+ if reader.has_param("layer"):
155
+ cl.layer = layer
156
+ _apply_optional_name(reader, cl)
157
+ parent.add_child(cl)
158
+ respond({"success": true, "action": "create_layer", "path": str(cl.get_path())})
159
+ "create_modulate":
160
+ var parent: Node = require_node(reader, "parent_path", "/root")
161
+ var color: Dictionary = reader.optional_dictionary("color")
162
+ if params_invalid(reader):
163
+ return
164
+ var cm: CanvasModulate = CanvasModulate.new()
165
+ if reader.has_param("color"):
166
+ cm.color = _color_from(color)
167
+ _apply_optional_name(reader, cm)
168
+ parent.add_child(cm)
169
+ respond({"success": true, "action": "create_modulate", "path": str(cm.get_path())})
170
+ "configure":
171
+ var node: Node = require_node(reader)
172
+ if node != null and not (node is CanvasLayer or node is CanvasModulate):
173
+ _require_class(reader, node, "CanvasLayer or CanvasModulate")
174
+ if params_invalid(reader):
175
+ return
176
+ var applied: Array = []
177
+ if node is CanvasLayer:
178
+ var cl2: CanvasLayer = node as CanvasLayer
179
+ var layer: int = reader.optional_int("layer", cl2.layer)
180
+ var offset: Dictionary = reader.optional_dictionary("offset")
181
+ var layer_visible: bool = reader.optional_bool("visible", cl2.visible)
182
+ if params_invalid(reader):
183
+ return
184
+ if reader.has_param("layer"):
185
+ cl2.layer = layer
186
+ applied.append("layer")
187
+ if reader.has_param("offset"):
188
+ cl2.offset = _vector2_from(offset)
189
+ applied.append("offset")
190
+ if reader.has_param("visible"):
191
+ cl2.visible = layer_visible
192
+ applied.append("visible")
193
+ elif node is CanvasModulate:
194
+ var color: Dictionary = reader.optional_dictionary("color")
195
+ if params_invalid(reader):
196
+ return
197
+ if reader.has_param("color"):
198
+ (node as CanvasModulate).color = _color_from(color)
199
+ applied.append("color")
200
+ respond({"success": true, "action": "configure", "applied": applied})
201
+
202
+
203
+ # --- Immediate-mode canvas drawing ---
204
+ func _cmd_canvas_draw(params: Dictionary) -> void:
205
+ var reader: CommandParams = CommandParams.new(params)
206
+ var action: String = reader.optional_enum("action", "line", ["line", "rect", "circle", "polygon", "text", "clear"])
207
+ if params_invalid(reader):
208
+ return
209
+
210
+ if action == "clear":
211
+ _draw_commands.clear()
212
+ if _canvas_draw_node != null and is_instance_valid(_canvas_draw_node):
213
+ _canvas_draw_node.queue_redraw()
214
+ respond({"success": true, "action": "clear"})
215
+ return
216
+
217
+ var color: Dictionary = reader.optional_dictionary("color", {"r": 1.0, "g": 1.0, "b": 1.0, "a": 1.0})
218
+ match action:
219
+ "line":
220
+ var _from: Dictionary = reader.required_dictionary("from")
221
+ var _to: Dictionary = reader.required_dictionary("to")
222
+ "rect":
223
+ var _rect: Dictionary = reader.required_dictionary("rect")
224
+ "circle":
225
+ var _center: Dictionary = reader.required_dictionary("center")
226
+ var radius: float = reader.required_number("radius", 0.0)
227
+ if not reader.failed() and radius <= 0.0:
228
+ reader.fail("radius must be greater than zero", {"param": "radius", "reason": "out_of_range", "min_exclusive": 0})
229
+ "polygon":
230
+ var points: Array = reader.required_array("points")
231
+ _require_dictionary_items(reader, "points", points)
232
+ if not reader.failed() and points.size() < 3:
233
+ reader.fail("points must contain at least 3 points", {"param": "points", "reason": "out_of_range", "min_items": 3})
234
+ "text":
235
+ var _position: Dictionary = reader.required_dictionary("position")
236
+ var _text: String = reader.required_string("text")
237
+ if params_invalid(reader):
238
+ return
239
+ if _canvas_draw_node == null or not is_instance_valid(_canvas_draw_node):
240
+ var parent: Node = require_node(reader, "parent_path", "/root")
241
+ if params_invalid(reader):
242
+ return
243
+ _canvas_draw_node = Node2D.new()
244
+ _canvas_draw_node.name = "_McpCanvasDraw"
245
+ _canvas_draw_node.set_script(_create_draw_script())
246
+ parent.add_child(_canvas_draw_node)
247
+ _canvas_draw_node.set("draw_commands", _draw_commands)
248
+ _draw_commands.append({"action": action, "params": params, "color": _color_from(color)})
249
+ _canvas_draw_node.set("draw_commands", _draw_commands)
250
+ _canvas_draw_node.queue_redraw()
251
+ respond({"success": true, "action": action})
252
+
253
+
254
+ func _create_draw_script() -> GDScript:
255
+ var s: GDScript = GDScript.new()
256
+ s.source_code = """extends Node2D
257
+ var draw_commands: Array = []
258
+ func _draw():
259
+ for cmd in draw_commands:
260
+ var p = cmd.params
261
+ var c = cmd.color
262
+ match cmd.action:
263
+ "line":
264
+ var f = p.get("from", {})
265
+ var t = p.get("to", {})
266
+ draw_line(Vector2(float(f.get("x",0)),float(f.get("y",0))),Vector2(float(t.get("x",0)),float(t.get("y",0))),c,float(p.get("width",2)))
267
+ "rect":
268
+ var r = p.get("rect", {})
269
+ draw_rect(Rect2(float(r.get("x",0)),float(r.get("y",0)),float(r.get("w",10)),float(r.get("h",10))),c,bool(p.get("filled",true)))
270
+ "circle":
271
+ var ct = p.get("center", {})
272
+ draw_circle(Vector2(float(ct.get("x",0)),float(ct.get("y",0))),float(p.get("radius",10)),c)
273
+ "polygon":
274
+ var pts = p.get("points", [])
275
+ var pv = PackedVector2Array()
276
+ for pt in pts:
277
+ pv.append(Vector2(float(pt.get("x",0)),float(pt.get("y",0))))
278
+ if pv.size() >= 3:
279
+ draw_colored_polygon(pv, c)
280
+ "text":
281
+ var pos = p.get("position", p.get("pos", {}))
282
+ draw_string(ThemeDB.fallback_font, Vector2(float(pos.get("x",0)),float(pos.get("y",0))), str(p.get("text","")), HORIZONTAL_ALIGNMENT_LEFT, -1, int(p.get("font_size",16)), c)
283
+ """
284
+ @warning_ignore("return_value_discarded")
285
+ s.reload()
286
+ return s
287
+
288
+
289
+ # --- 2D lights and occluders ---
290
+ func _cmd_light_2d(params: Dictionary) -> void:
291
+ var reader: CommandParams = CommandParams.new(params)
292
+ var action: String = reader.optional_enum("action", "create_point", ["create_point", "create_directional", "create_occluder"])
293
+ var parent: Node = require_node(reader, "parent_path", "/root")
294
+ if params_invalid(reader):
295
+ return
296
+
297
+ match action:
298
+ "create_point":
299
+ var color: Dictionary = reader.optional_dictionary("color")
300
+ var energy: float = reader.optional_number("energy", 1.0)
301
+ var light_range: float = reader.optional_number("range", 1.0)
302
+ if params_invalid(reader):
303
+ return
304
+ var light: PointLight2D = PointLight2D.new()
305
+ if reader.has_param("color"):
306
+ light.color = _color_from(color)
307
+ if reader.has_param("energy"):
308
+ light.energy = energy
309
+ # Create a simple gradient texture for the light
310
+ var tex: GradientTexture2D = GradientTexture2D.new()
311
+ tex.width = 128
312
+ tex.height = 128
313
+ tex.fill = GradientTexture2D.FILL_RADIAL
314
+ tex.gradient = Gradient.new()
315
+ light.texture = tex
316
+ if reader.has_param("range"):
317
+ light.texture_scale = light_range
318
+ _apply_optional_name(reader, light)
319
+ parent.add_child(light)
320
+ respond({"success": true, "action": "create_point", "path": str(light.get_path())})
321
+ "create_directional":
322
+ var color: Dictionary = reader.optional_dictionary("color")
323
+ var energy: float = reader.optional_number("energy", 1.0)
324
+ if params_invalid(reader):
325
+ return
326
+ var light: DirectionalLight2D = DirectionalLight2D.new()
327
+ if reader.has_param("color"):
328
+ light.color = _color_from(color)
329
+ if reader.has_param("energy"):
330
+ light.energy = energy
331
+ _apply_optional_name(reader, light)
332
+ parent.add_child(light)
333
+ respond({"success": true, "action": "create_directional", "path": str(light.get_path())})
334
+ "create_occluder":
335
+ var points: Array = reader.required_array("points")
336
+ _require_dictionary_items(reader, "points", points)
337
+ if not reader.failed() and points.size() < 3:
338
+ reader.fail("points must contain at least 3 points", {"param": "points", "reason": "out_of_range", "min_items": 3})
339
+ var packed: PackedVector2Array = PackedVector2Array()
340
+ if not reader.failed():
341
+ for point: Dictionary in points:
342
+ @warning_ignore("return_value_discarded")
343
+ packed.append(_vector2_from(point))
344
+ if params_invalid(reader):
345
+ return
346
+ var occ: LightOccluder2D = LightOccluder2D.new()
347
+ var poly: OccluderPolygon2D = OccluderPolygon2D.new()
348
+ poly.polygon = packed
349
+ occ.occluder = poly
350
+ _apply_optional_name(reader, occ)
351
+ parent.add_child(occ)
352
+ respond({"success": true, "action": "create_occluder", "path": str(occ.get_path()), "point_count": packed.size()})
353
+
354
+
355
+ # --- Parallax backgrounds and layers ---
356
+ func _cmd_parallax(params: Dictionary) -> void:
357
+ var reader: CommandParams = CommandParams.new(params)
358
+ var action: String = reader.optional_enum("action", "create_background", ["create_background", "add_layer", "configure"])
359
+ if params_invalid(reader):
360
+ return
361
+
362
+ match action:
363
+ "create_background":
364
+ var parent: Node = require_node(reader, "parent_path", "/root")
365
+ if params_invalid(reader):
366
+ return
367
+ var bg: ParallaxBackground = ParallaxBackground.new()
368
+ _apply_optional_name(reader, bg)
369
+ parent.add_child(bg)
370
+ respond({"success": true, "action": "create_background", "path": str(bg.get_path())})
371
+ "add_layer":
372
+ var parent: Node = require_node(reader, "parent_path")
373
+ if parent != null and not parent is ParallaxBackground:
374
+ reader.fail("Node is not a ParallaxBackground: %s" % parent.get_class(),
375
+ {"param": "parent_path", "reason": "invalid_value", "expected": "ParallaxBackground", "value": parent.get_class()})
376
+ var motion_scale: Dictionary = reader.optional_dictionary("motion_scale")
377
+ var motion_offset: Dictionary = reader.optional_dictionary("motion_offset")
378
+ var mirroring: Dictionary = reader.optional_dictionary("mirroring")
379
+ if params_invalid(reader):
380
+ return
381
+ var layer: ParallaxLayer = ParallaxLayer.new()
382
+ if reader.has_param("motion_scale"):
383
+ layer.motion_scale = _vector2_from(motion_scale, 1, 1)
384
+ if reader.has_param("motion_offset"):
385
+ layer.motion_offset = _vector2_from(motion_offset)
386
+ if reader.has_param("mirroring"):
387
+ layer.motion_mirroring = _vector2_from(mirroring)
388
+ _apply_optional_name(reader, layer)
389
+ parent.add_child(layer)
390
+ respond({"success": true, "action": "add_layer", "path": str(layer.get_path())})
391
+ "configure":
392
+ var node: Node = require_node(reader)
393
+ if node != null and not (node is ParallaxBackground or node is ParallaxLayer):
394
+ _require_class(reader, node, "ParallaxBackground or ParallaxLayer")
395
+ if params_invalid(reader):
396
+ return
397
+ var applied: Array = []
398
+ if node is ParallaxBackground:
399
+ var pbg: ParallaxBackground = node as ParallaxBackground
400
+ var scroll_offset: Dictionary = reader.optional_dictionary("scroll_offset")
401
+ var scroll_base_offset: Dictionary = reader.optional_dictionary("scroll_base_offset")
402
+ if params_invalid(reader):
403
+ return
404
+ if reader.has_param("scroll_offset"):
405
+ pbg.scroll_offset = _vector2_from(scroll_offset)
406
+ applied.append("scroll_offset")
407
+ if reader.has_param("scroll_base_offset"):
408
+ pbg.scroll_base_offset = _vector2_from(scroll_base_offset)
409
+ applied.append("scroll_base_offset")
410
+ elif node is ParallaxLayer:
411
+ var pl: ParallaxLayer = node as ParallaxLayer
412
+ var motion_scale: Dictionary = reader.optional_dictionary("motion_scale")
413
+ var motion_offset: Dictionary = reader.optional_dictionary("motion_offset")
414
+ var mirroring: Dictionary = reader.optional_dictionary("mirroring")
415
+ if params_invalid(reader):
416
+ return
417
+ if reader.has_param("motion_scale"):
418
+ pl.motion_scale = _vector2_from(motion_scale, 1, 1)
419
+ applied.append("motion_scale")
420
+ if reader.has_param("motion_offset"):
421
+ pl.motion_offset = _vector2_from(motion_offset)
422
+ applied.append("motion_offset")
423
+ if reader.has_param("mirroring"):
424
+ pl.motion_mirroring = _vector2_from(mirroring)
425
+ applied.append("mirroring")
426
+ respond({"success": true, "action": "configure", "applied": applied})
427
+
428
+
429
+ # --- Line2D / Polygon2D points ---
430
+ func _cmd_shape_2d(params: Dictionary) -> void:
431
+ var reader: CommandParams = CommandParams.new(params)
432
+ var node: Node = require_node(reader)
433
+ var action: String = reader.optional_enum("action", "get_points", ["add_point", "set_points", "clear", "get_points"])
434
+ if node != null and not (node is Line2D or node is Polygon2D):
435
+ _require_class(reader, node, "Line2D or Polygon2D")
436
+ if params_invalid(reader):
437
+ return
438
+
439
+ match action:
440
+ "add_point":
441
+ var point: Dictionary = reader.required_dictionary("point")
442
+ if params_invalid(reader):
443
+ return
444
+ var pt: Vector2 = _vector2_from(point)
445
+ if node is Line2D:
446
+ (node as Line2D).add_point(pt)
447
+ else:
448
+ var polygon: PackedVector2Array = (node as Polygon2D).polygon
449
+ @warning_ignore("return_value_discarded")
450
+ polygon.append(pt)
451
+ (node as Polygon2D).polygon = polygon
452
+ respond({"success": true, "action": "add_point"})
453
+ "set_points":
454
+ var points: Array = reader.required_array("points")
455
+ _require_dictionary_items(reader, "points", points)
456
+ var packed: PackedVector2Array = PackedVector2Array()
457
+ if not reader.failed():
458
+ for point: Dictionary in points:
459
+ @warning_ignore("return_value_discarded")
460
+ packed.append(_vector2_from(point))
461
+ if params_invalid(reader):
462
+ return
463
+ if node is Line2D:
464
+ (node as Line2D).points = packed
465
+ else:
466
+ (node as Polygon2D).polygon = packed
467
+ respond({"success": true, "action": "set_points", "count": packed.size()})
468
+ "clear":
469
+ if node is Line2D:
470
+ (node as Line2D).clear_points()
471
+ else:
472
+ (node as Polygon2D).polygon = PackedVector2Array()
473
+ respond({"success": true, "action": "clear"})
474
+ "get_points":
475
+ var pts: PackedVector2Array
476
+ if node is Line2D:
477
+ pts = (node as Line2D).points
478
+ else:
479
+ pts = (node as Polygon2D).polygon
480
+ var result: Array = []
481
+ for p: Vector2 in pts:
482
+ result.append({"x": p.x, "y": p.y})
483
+ respond({"success": true, "action": "get_points", "points": result})
484
+
485
+
486
+ # --- Path2D curves ---
487
+ func _cmd_path_2d(params: Dictionary) -> void:
488
+ var reader: CommandParams = CommandParams.new(params)
489
+ var action: String = reader.optional_enum("action", "create", ["create", "add_point", "get_points"])
490
+ if params_invalid(reader):
491
+ return
492
+
493
+ match action:
494
+ "create":
495
+ var parent: Node = require_node(reader, "parent_path", "/root")
496
+ var packed: PackedVector2Array = _points_from(reader, "points")
497
+ if params_invalid(reader):
498
+ return
499
+ var path_node: Path2D = Path2D.new()
500
+ path_node.curve = Curve2D.new()
501
+ _apply_optional_name(reader, path_node)
502
+ for pt: Vector2 in packed:
503
+ path_node.curve.add_point(pt)
504
+ parent.add_child(path_node)
505
+ respond({"success": true, "action": "create", "path": str(path_node.get_path()), "point_count": path_node.curve.point_count})
506
+ "add_point":
507
+ var node: Node = require_node(reader)
508
+ if node != null and not node is Path2D:
509
+ _require_class(reader, node, "Path2D")
510
+ var point: Dictionary = reader.required_dictionary("point")
511
+ if params_invalid(reader):
512
+ return
513
+ var path_node: Path2D = node as Path2D
514
+ path_node.curve.add_point(_vector2_from(point))
515
+ respond({"success": true, "action": "add_point", "point_count": path_node.curve.point_count})
516
+ "get_points":
517
+ var node: Node = require_node(reader)
518
+ if node != null and not node is Path2D:
519
+ _require_class(reader, node, "Path2D")
520
+ if params_invalid(reader):
521
+ return
522
+ var path_node: Path2D = node as Path2D
523
+ var pts: Array = []
524
+ for i: int in path_node.curve.point_count:
525
+ var pt: Vector2 = path_node.curve.get_point_position(i)
526
+ pts.append({"x": pt.x, "y": pt.y})
527
+ respond({"success": true, "action": "get_points", "points": pts})