gdharness 0.6.0 → 0.6.2

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.
package/build/cli.js CHANGED
@@ -15785,7 +15785,7 @@ var init_tool_definitions = __esm(() => {
15785
15785
  requires: []
15786
15786
  },
15787
15787
  rect: {
15788
- summary: "one node's rectangle or position, in canvas and in window pixels",
15788
+ summary: "one node's rectangle or position, in canvas and in window pixels. A 3D node answers with the point to aim at, which is the middle of what it draws rather than the origin it stands on, the rectangle it covers under covers, the camera that drew it, and behind_camera when it is not in front of one",
15789
15789
  requires: ["nodePath"]
15790
15790
  },
15791
15791
  property: {
@@ -15834,10 +15834,13 @@ var init_tool_definitions = __esm(() => {
15834
15834
  },
15835
15835
  {
15836
15836
  name: "runtime_input",
15837
- description: "Input to the running game: a whole click on a Control named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
15837
+ description: "Input to the running game: a whole click on a Control or a 3D node named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
15838
15838
  parameters: {
15839
15839
  projectPath: RUNNING_PROJECT_PATH,
15840
- nodePath: { type: "string", description: "click: the Control to click, at its centre." },
15840
+ nodePath: {
15841
+ type: "string",
15842
+ description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn."
15843
+ },
15841
15844
  action: { type: "string", description: "action: the InputMap action name." },
15842
15845
  pressed: { type: "boolean", description: "Press or release. Default true." },
15843
15846
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
@@ -15863,7 +15866,7 @@ var init_tool_definitions = __esm(() => {
15863
15866
  requires: [],
15864
15867
  operations: {
15865
15868
  click: {
15866
- summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved",
15869
+ summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
15867
15870
  requires: ["nodePath"]
15868
15871
  },
15869
15872
  action: { summary: "press or release an action", requires: ["action"] },
@@ -38641,6 +38644,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38641
38644
  | What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
38642
38645
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
38643
38646
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
38647
+ | Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
38648
+ | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
38644
38649
  | Wait for something | \`runtime_wait\`, never a sleep |
38645
38650
  | A picture, for a person who asked to see one | \`runtime_capture\` |
38646
38651
 
@@ -1,10 +1,15 @@
1
1
  extends RefCounted
2
2
 
3
3
  ## Input handed to the running game as if a player had given it: actions, keys, the mouse, and
4
- ## a whole click on a Control found by path.
4
+ ## a whole click on a Control or a 3D node found by path.
5
5
 
6
6
  const Values = preload("runtime_values.gd")
7
7
 
8
+ ## Where a 3D node is drawn, which is what a click aimed at one has to work out first. Asked of the
9
+ ## query module rather than worked out again here, so the place this aims at and the place a rect
10
+ ## reports are the same place by construction rather than by agreement.
11
+ const Queries = preload("runtime_queries.gd")
12
+
8
13
  ## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
9
14
  const TO_SMALL: int = 32
10
15
 
@@ -220,6 +225,36 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
220
225
  }
221
226
 
222
227
 
228
+ ## The viewport a click aimed at [param control] has to be pushed into.
229
+ ##
230
+ ## Its own, unless that is an embedded [Window]. A ConfirmationDialog is a Window, and under
231
+ ## `gui_embed_subwindows` a Window is drawn inside its parent rather than given one by the
232
+ ## desktop: pushing into its own viewport delivers nothing, measured, with the pointer reading as
233
+ ## over no control at all. What reaches it is the parent, which is what the engine does with a
234
+ ## real pointer. Walked rather than stepped once, because a dialog can open a dialog.
235
+ static func _clicking_viewport(control: Control) -> Viewport:
236
+ var viewport: Viewport = control.get_viewport()
237
+ var window: Window = viewport as Window
238
+ while window != null and window.is_embedded() and window.get_parent() != null:
239
+ viewport = window.get_parent().get_viewport()
240
+ window = viewport as Window
241
+ return viewport
242
+
243
+
244
+ ## Where [param control]'s centre is in the viewport [method _clicking_viewport] names.
245
+ ##
246
+ ## The transform is against the control's own viewport, so every embedded window between it and
247
+ ## that one contributes its offset. The same walk, because the two answers have to agree about
248
+ ## which viewport they are describing.
249
+ static func _centre_of(control: Control) -> Vector2:
250
+ var centre: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
251
+ var window: Window = control.get_viewport() as Window
252
+ while window != null and window.is_embedded() and window.get_parent() != null:
253
+ centre += Vector2(window.position)
254
+ window = window.get_parent().get_viewport() as Window
255
+ return centre
256
+
257
+
223
258
  ## Scrolls whatever is holding [param control] until it is on screen, and answers whether
224
259
  ## anything moved.
225
260
  ##
@@ -255,14 +290,19 @@ func click(params: Dictionary) -> Dictionary:
255
290
  var node: Node = _host.get_tree().root.get_node_or_null(node_path)
256
291
  if node == null:
257
292
  return {"type": "error", "message": "Node not found: " + node_path}
293
+ if node is Node3D:
294
+ return await _click_in_the_world(node_path, node, params)
258
295
  if not node is Control:
259
- return {"type": "error", "message": "%s is a %s, not a Control" % [node_path, node.get_class()]}
296
+ return {
297
+ "type": "error",
298
+ "message": "%s is a %s, not a Control or a Node3D" % [node_path, node.get_class()]
299
+ }
260
300
  var control: Control = node
261
301
  if not control.is_visible_in_tree():
262
302
  return {"type": "error", "message": "%s is not visible, so nothing can click it" % node_path}
263
303
 
264
- var viewport: Viewport = control.get_viewport()
265
- var centre: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
304
+ var viewport: Viewport = _clicking_viewport(control)
305
+ var centre: Vector2 = _centre_of(control)
266
306
 
267
307
  # A control below the fold of a ScrollContainer is not out of reach, it is one scroll away,
268
308
  # which is what a person does without thinking about it before they click. Refusing it
@@ -282,18 +322,9 @@ func click(params: Dictionary) -> Dictionary:
282
322
  # whatever the project settings say, which is the usual reason to be here and is not
283
323
  # something the caller can read off the rect on its own.
284
324
  if not viewport.get_visible_rect().has_point(centre):
285
- var why: String = ""
325
+ var why: String = _no_window_note(viewport)
286
326
  if scrolled:
287
327
  why = ". It was scrolled as far as what holds it goes and is still out there"
288
- elif not _host.get_tree().root.can_draw():
289
- why = ". This game has no window: run it with a window to reach this control"
290
- # Only where it is true. The rect is printed just above, so claiming 64 by 64 over a
291
- # viewport somebody has resized says two different things in one sentence.
292
- if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
293
- why = (
294
- ". This game has no window, and a game with no window has a 64 by 64 viewport "
295
- + "whatever the project settings say: run it with a window to reach this control"
296
- )
297
328
  return {
298
329
  "type": "error",
299
330
  "message":
@@ -314,7 +345,12 @@ func click(params: Dictionary) -> Dictionary:
314
345
  # that went to it. Read in full here, path included, because nothing about that control
315
346
  # is guaranteed to survive the release: a button that opens the next screen takes the
316
347
  # whole menu out of the tree, and a node that has left the tree has no path to give.
317
- var hovered: Control = viewport.gui_get_hovered_control()
348
+ # Asked of the control's own viewport rather than the one the event went into, and for an
349
+ # embedded window those are two different objects: the parent takes the event and hands it on,
350
+ # and the window keeps the GUI state. Reading the parent reported every dialog as not landed
351
+ # while the button it was aimed at pressed perfectly well, which is the worst shape an answer
352
+ # can have, since the caller believes the miss over what the game just did.
353
+ var hovered: Control = control.get_viewport().gui_get_hovered_control()
318
354
  var hovered_path: Variant = null
319
355
  if hovered != null:
320
356
  hovered_path = str(hovered.get_path())
@@ -348,6 +384,94 @@ func click(params: Dictionary) -> Dictionary:
348
384
  }
349
385
 
350
386
 
387
+ ## What to add to a refusal about a point outside the viewport, when the reason is that nobody
388
+ ## gave this game a window. The rect on its own does not say it, and it is the usual reason.
389
+ ##
390
+ ## The size is only claimed where it is true: the rect is printed beside this, so naming 64 by 64
391
+ ## over a viewport somebody has resized says two different things in one sentence.
392
+ func _no_window_note(viewport: Viewport) -> String:
393
+ if _host.get_tree().root.can_draw():
394
+ return ""
395
+ if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
396
+ return (
397
+ ". This game has no window, and a game with no window has a 64 by 64 viewport whatever "
398
+ + "the project settings say: run it with a window to reach this control"
399
+ )
400
+ return ". This game has no window: run it with a window to reach this control"
401
+
402
+
403
+ ## A whole click aimed at where a 3D node is drawn, for a game that picks with a ray out of the
404
+ ## cursor rather than with a Control.
405
+ ##
406
+ ## The alternative was three calls: read the node's position, find the camera, unproject it, then
407
+ ## push raw mouse events at the answer. Anything that walks has walked by the third, so the click
408
+ ## lands where it used to be, which is a miss that looks exactly like a game that ignored it.
409
+ ##
410
+ ## What this can honestly say is where the click went and whether the interface took it: a Control
411
+ ## under the pointer swallows the press and the room never hears it, and that is the failure worth
412
+ ## naming. Whether the game's own picking then chose this node is the game's rule rather than
413
+ ## anything the engine can be asked, so it is not claimed.
414
+ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) -> Dictionary:
415
+ if not item.is_visible_in_tree():
416
+ return {"type": "error", "message": "%s is not visible, so nothing can click it" % node_path}
417
+
418
+ var found: Dictionary = Queries.in_frame(item)
419
+ if found.is_empty():
420
+ return {
421
+ "type": "error",
422
+ "message":
423
+ "%s is not in a viewport with a current Camera3D, so there is nowhere to click it" % node_path
424
+ }
425
+ if not found.has("aim"):
426
+ return {
427
+ "type": "error",
428
+ "message": "%s is behind the camera drawing it, so it is not on screen to click" % node_path
429
+ }
430
+
431
+ var viewport: Viewport = item.get_viewport()
432
+ var aim: Vector2 = found["aim"]
433
+ if not viewport.get_visible_rect().has_point(aim):
434
+ return {
435
+ "type": "error",
436
+ "message":
437
+ (
438
+ "%s is drawn at %s, outside the viewport %s, so nothing can click it%s"
439
+ % [node_path, aim, viewport.get_visible_rect(), _no_window_note(viewport)]
440
+ )
441
+ }
442
+
443
+ var position: Vector2 = viewport.get_final_transform() * aim
444
+ var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
445
+ var double: bool = bool(params.get("double", false))
446
+
447
+ viewport.push_input(_motion(position, Vector2.ZERO))
448
+ # Read before the press, for the reason the Control click reads it: what the caller needs to
449
+ # know is whether a panel is sitting over the room, and the press is what would change it.
450
+ var hovered: Control = viewport.gui_get_hovered_control()
451
+ var hovered_path: Variant = null
452
+ if hovered != null:
453
+ hovered_path = str(hovered.get_path())
454
+
455
+ viewport.push_input(_button(position, button, true, double))
456
+ await _host.get_tree().process_frame
457
+ viewport.push_input(_button(position, button, false, false))
458
+ await _host.get_tree().process_frame
459
+
460
+ return {
461
+ "type": "clicked",
462
+ "path": node_path,
463
+ "position": _values.serialize(position),
464
+ "button": button,
465
+ "double": double,
466
+ "hovered": hovered_path,
467
+ # The interface did not take it, so it reached the game's own input. As close to "it
468
+ # landed" as anything outside the game can get, and said in the same word the Control
469
+ # click says it in.
470
+ "landed": hovered == null,
471
+ "camera": found["camera"],
472
+ }
473
+
474
+
351
475
  func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
352
476
  var event: InputEventMouseMotion = InputEventMouseMotion.new()
353
477
  event.position = position
@@ -75,7 +75,13 @@ func find_nodes(params: Dictionary) -> Dictionary:
75
75
  truncated = true
76
76
  break
77
77
  found.append(_found(node, wanted_property))
78
- var children: Array[Node] = node.get_children()
78
+ # Internal children included, which they were not. A ConfirmationDialog builds its Yes and
79
+ # its No as internal nodes, and a ScrollContainer its bars, so a find over a screen for
80
+ # every Button came back without the two buttons the player is being asked to press:
81
+ # nothing here could see the dialog at all, and the way past it was to emit `confirmed`.
82
+ # A filtered query carries no cost for including them, because they only appear when they
83
+ # are what was asked for.
84
+ var children: Array[Node] = node.get_children(true)
79
85
  for index: int in range(children.size() - 1, -1, -1):
80
86
  pending.push_front(children[index])
81
87
 
@@ -128,9 +134,10 @@ func _matches(
128
134
  return true
129
135
 
130
136
 
131
- ## Where a node is on screen: a Control's rectangle, or a Node2D's position, in both the
132
- ## canvas coordinates the node reports and the window pixels input arrives in. The two differ
133
- ## whenever the project stretches its viewport, which is what made a rect unusable for a click.
137
+ ## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
138
+ ## is drawn in, in both the canvas coordinates the node reports and the window pixels input
139
+ ## arrives in. The two differ whenever the project stretches its viewport, which is what made a
140
+ ## rect unusable for a click.
134
141
  func get_rect(params: Dictionary) -> Dictionary:
135
142
  var node_path: String = str(params.get("path", ""))
136
143
  if node_path.is_empty():
@@ -162,11 +169,124 @@ func get_rect(params: Dictionary) -> Dictionary:
162
169
  "canvas": _values.serialize(canvas_position),
163
170
  "window": _values.serialize(window_position),
164
171
  }
172
+ if node is Node3D:
173
+ return _in_the_frame(node_path, node)
165
174
  return {
166
175
  "type": "error", "message": "%s is a %s, which has no place on screen" % [node_path, node.get_class()]
167
176
  }
168
177
 
169
178
 
179
+ ## Where a 3D node is in the frame drawing it: the point to aim at, and the rectangle its own
180
+ ## geometry covers, each in canvas coordinates and in window pixels.
181
+ ##
182
+ ## A 3D node had no answer here at all, so placing one meant reading its position, finding the
183
+ ## camera and calling unproject_position by hand. Three calls, and anything that walks has walked
184
+ ## between the first and the third: the aim lands where the thing used to be.
185
+ ##
186
+ ## The camera is named in the answer, because "where is it on screen" is a question about a camera
187
+ ## and a game with two of them has two answers.
188
+ func _in_the_frame(node_path: String, item: Node3D) -> Dictionary:
189
+ var found: Dictionary = in_frame(item)
190
+ if found.is_empty():
191
+ return {
192
+ "type": "error",
193
+ "message": "%s is not in a viewport with a current Camera3D, so nothing is drawing it" % node_path
194
+ }
195
+
196
+ var to_window: Transform2D = item.get_viewport().get_final_transform()
197
+ var answer: Dictionary = {
198
+ "type": "point",
199
+ "path": node_path,
200
+ "visible": item.is_visible_in_tree(),
201
+ "camera": found["camera"],
202
+ "behind_camera": found["behind"],
203
+ }
204
+ if found.has("aim"):
205
+ var aim: Vector2 = found["aim"]
206
+ answer["canvas"] = _values.serialize(aim)
207
+ answer["window"] = _values.serialize(to_window * aim)
208
+ if found.has("rect"):
209
+ var covered: Rect2 = found["rect"]
210
+ answer["covers"] = {
211
+ "canvas": _values.serialize(covered),
212
+ "window": _values.serialize(to_window * covered),
213
+ }
214
+ return answer
215
+
216
+
217
+ ## What anything aiming at a 3D node needs: the camera that draws it, whether it is behind that
218
+ ## camera, the point on screen to aim at, and the rectangle it covers. Empty when no camera is
219
+ ## drawing it at all.
220
+ ##
221
+ ## The aim is the middle of what the node draws rather than its origin, because a person clicking
222
+ ## a character clicks the character and a character's origin is on the floor under their feet. A
223
+ ## node that draws nothing has no middle and falls back to the origin, which is still a place.
224
+ ##
225
+ ## Absent keys rather than nulls: nothing drawn, or behind the camera, and each of those is a
226
+ ## different answer from a coordinate that happens to be zero. Public and static because the click
227
+ ## has to aim at the same point this reports, and two copies of the arithmetic would be two places
228
+ ## on screen for one node the first time either changed.
229
+ static func in_frame(item: Node3D) -> Dictionary:
230
+ var viewport: Viewport = item.get_viewport()
231
+ if viewport == null:
232
+ return {}
233
+ var camera: Camera3D = viewport.get_camera_3d()
234
+ if camera == null:
235
+ return {}
236
+
237
+ var origin: Vector3 = item.global_transform.origin
238
+ var found: Dictionary = {"camera": str(camera.get_path()), "behind": camera.is_position_behind(origin)}
239
+ var box: AABB = drawn_box(item)
240
+ var draws: bool = box.size != Vector3.ZERO
241
+ var middle: Vector3 = box.get_center() if draws else origin
242
+ if not camera.is_position_behind(middle):
243
+ found["aim"] = camera.unproject_position(middle)
244
+ if draws:
245
+ found.merge(_around(box, camera))
246
+ return found
247
+
248
+
249
+ ## The rectangle [param box] covers on screen, under the key `rect`, or nothing when any of it is
250
+ ## behind the camera.
251
+ ##
252
+ ## All eight corners, because a box in space is not a box in the frame: an orthographic camera
253
+ ## looking down a diagonal draws a cube as a hexagon, and the rectangle worth answering is the one
254
+ ## around every corner of it. Nothing rather than a guess when a corner is behind the camera,
255
+ ## because [method Camera3D.unproject_position] mirrors those back into view and a rectangle built
256
+ ## from one is a rectangle somewhere else entirely.
257
+ static func _around(box: AABB, camera: Camera3D) -> Dictionary:
258
+ var seen: Rect2 = Rect2()
259
+ for index: int in 8:
260
+ var corner: Vector3 = box.get_endpoint(index)
261
+ if camera.is_position_behind(corner):
262
+ return {}
263
+ var point: Vector2 = camera.unproject_position(corner)
264
+ seen = Rect2(point, Vector2.ZERO) if index == 0 else seen.expand(point)
265
+ return {"rect": seen}
266
+
267
+
268
+ ## The box everything drawn under [param item] fits in, in world space, or a box with no size when
269
+ ## nothing under it draws.
270
+ ##
271
+ ## Walked rather than asked of [param item] itself, because the node a caller names is the one that
272
+ ## moves and the thing on screen is the mesh hanging off it: a character is a Node3D with a
273
+ ## skeleton and a mesh under it, and the Node3D has no extent of its own at all.
274
+ static func drawn_box(item: Node3D) -> AABB:
275
+ var merged: AABB = AABB()
276
+ var found: bool = false
277
+ var pending: Array[Node] = [item]
278
+ while not pending.is_empty():
279
+ var node: Node = pending.pop_back()
280
+ var visual: VisualInstance3D = node as VisualInstance3D
281
+ if visual != null and visual.is_visible_in_tree():
282
+ var box: AABB = visual.global_transform * visual.get_aabb()
283
+ merged = box if not found else merged.merge(box)
284
+ found = true
285
+ for child: Node in node.get_children(true):
286
+ pending.push_back(child)
287
+ return merged
288
+
289
+
170
290
  func get_property(params: Dictionary) -> Dictionary:
171
291
  var node_path: String = str(params.get("path", ""))
172
292
  var property: String = str(params.get("property", ""))
@@ -286,7 +406,11 @@ func _serialize_node_tree(node: Node, depth: int, max_depth: int, include_proper
286
406
 
287
407
  if depth < max_depth:
288
408
  var children: Array = []
289
- for child: Node in node.get_children():
409
+ # Internal ones too, for the reason `find` takes them: a tree that answers "no children"
410
+ # over a ConfirmationDialog holding a Yes and a No is not tidier than one that says so,
411
+ # it is wrong, and it is what sends somebody looking for another way to press the button.
412
+ # `depth` is what keeps the answer a size worth reading.
413
+ for child: Node in node.get_children(true):
290
414
  children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties))
291
415
  result["children"] = children
292
416
 
package/build/index.js CHANGED
@@ -28299,7 +28299,7 @@ var TOOL_SPECS = [
28299
28299
  requires: []
28300
28300
  },
28301
28301
  rect: {
28302
- summary: "one node's rectangle or position, in canvas and in window pixels",
28302
+ summary: "one node's rectangle or position, in canvas and in window pixels. A 3D node answers with the point to aim at, which is the middle of what it draws rather than the origin it stands on, the rectangle it covers under covers, the camera that drew it, and behind_camera when it is not in front of one",
28303
28303
  requires: ["nodePath"]
28304
28304
  },
28305
28305
  property: {
@@ -28348,10 +28348,13 @@ var TOOL_SPECS = [
28348
28348
  },
28349
28349
  {
28350
28350
  name: "runtime_input",
28351
- description: "Input to the running game: a whole click on a Control named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
28351
+ description: "Input to the running game: a whole click on a Control or a 3D node named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
28352
28352
  parameters: {
28353
28353
  projectPath: RUNNING_PROJECT_PATH,
28354
- nodePath: { type: "string", description: "click: the Control to click, at its centre." },
28354
+ nodePath: {
28355
+ type: "string",
28356
+ description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn."
28357
+ },
28355
28358
  action: { type: "string", description: "action: the InputMap action name." },
28356
28359
  pressed: { type: "boolean", description: "Press or release. Default true." },
28357
28360
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
@@ -28377,7 +28380,7 @@ var TOOL_SPECS = [
28377
28380
  requires: [],
28378
28381
  operations: {
28379
28382
  click: {
28380
- summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved",
28383
+ summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
28381
28384
  requires: ["nodePath"]
28382
28385
  },
28383
28386
  action: { summary: "press or release an action", requires: ["action"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",