gdharness 1.3.8 → 1.3.10

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 (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -125,6 +125,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
125
125
  var root: Node = reached["node"]
126
126
 
127
127
  var found: Array[Dictionary] = []
128
+ var found_nodes: Array[Node] = []
128
129
  var pending: Array[Node] = [root]
129
130
  var truncated: bool = false
130
131
  # Counted while the tree is already being walked, for the answer below: how many nodes this
@@ -148,24 +149,28 @@ func find_nodes(params: Dictionary) -> Dictionary:
148
149
  var nearly_hidden: int = 0
149
150
  var nearly_said_hidden: int = 0
150
151
  var hidden: int = 0
152
+ var out_of_view: int = 0
151
153
  while not pending.is_empty():
152
154
  var node: Node = pending.pop_front()
153
155
  var rest: bool = _matches_apart_from_name(node, wanted)
154
156
  if rest and _named(node, names):
155
157
  if not include_hidden and not shown(node):
156
158
  hidden += 1
159
+ elif not include_hidden and not GdhScreen.in_view(node):
160
+ out_of_view += 1
157
161
  elif found.size() >= limit:
158
162
  truncated = true
159
163
  break
160
164
  else:
161
165
  found.append(_found(node, wanted_property))
166
+ found_nodes.append(node)
162
167
  elif rest and _contains_any(str(node.name), plain_names):
163
- if include_hidden or shown(node):
168
+ if include_hidden or readable(node):
164
169
  nearly += 1
165
170
  else:
166
171
  nearly_hidden += 1
167
172
  elif not widened_says.is_empty() and _named(node, names) and _matches_apart_from_name(node, widened):
168
- if include_hidden or shown(node):
173
+ if include_hidden or readable(node):
169
174
  nearly_said += 1
170
175
  else:
171
176
  nearly_said_hidden += 1
@@ -181,6 +186,39 @@ func find_nodes(params: Dictionary) -> Dictionary:
181
186
 
182
187
  var answer: Dictionary = {"type": "nodes", "count": found.size(), "truncated": truncated, "nodes": found}
183
188
  var notes: Array[String] = []
189
+ # Each still listed, since a caller may mean one of them, and marked, so one sign drawn as a label
190
+ # and its outline copies is not read as four signs. The outermost match it copies, since a find
191
+ # under one of the copies has left the label they all copy outside it.
192
+ var copies: int = 0
193
+ for index: int in range(found_nodes.size()):
194
+ var at: int = -1
195
+ for original: Node in GdhWords.copied_through(found_nodes[index]):
196
+ var matched: int = found_nodes.find(original)
197
+ if matched != -1:
198
+ at = matched
199
+ if at != -1:
200
+ found[index]["copyOf"] = found[at]["path"]
201
+ copies += 1
202
+ if copies > 0:
203
+ answer["distinct"] = found.size() - copies
204
+ (
205
+ notes
206
+ . append(
207
+ (
208
+ (
209
+ "%d of the %d matches %s drawn under a match saying the same words, as an outline or a"
210
+ + " glow is drawn, and %s it under copyOf: %d distinct"
211
+ )
212
+ % [
213
+ copies,
214
+ found.size(),
215
+ "is a copy" if copies == 1 else "are copies",
216
+ "names" if copies == 1 else "name",
217
+ found.size() - copies,
218
+ ]
219
+ )
220
+ )
221
+ )
184
222
  # Nothing found is the one answer that cannot be told apart from having asked the wrong
185
223
  # question, and a name written without a wildcard is the way an agent writes "contains".
186
224
  # Said only when it changes the answer, so a genuine nothing stays a plain nothing.
@@ -200,7 +238,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
200
238
  'name is matched as a glob against the whole name; %d %snode %s %s, which "%s"%s would find'
201
239
  % [
202
240
  named_near,
203
- "" if nearly > 0 else "hidden ",
241
+ "" if nearly > 0 else "hidden or out-of-view ",
204
242
  holding,
205
243
  " or ".join(PackedStringArray(near_words)),
206
244
  GdhSays.joined_at_bars(opened),
@@ -220,7 +258,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
220
258
  widened_says,
221
259
  "" if nearly_said > 0 else " with includeHidden true",
222
260
  said_near,
223
- "" if nearly_said > 0 else "hidden ",
261
+ "" if nearly_said > 0 else "hidden or out-of-view ",
224
262
  "" if said_near == 1 else "s",
225
263
  ]
226
264
  )
@@ -236,6 +274,17 @@ func find_nodes(params: Dictionary) -> Dictionary:
236
274
  % [hidden, "" if hidden == 1 else "s"]
237
275
  )
238
276
  )
277
+ if out_of_view > 0:
278
+ answer["out_of_view"] = out_of_view
279
+ notes.append(
280
+ (
281
+ (
282
+ "%d matching node%s drawn outside the viewport or scrolled out of a ScrollContainer"
283
+ + " and left out; includeHidden true answers with them too"
284
+ )
285
+ % [out_of_view, "" if out_of_view == 1 else "s"]
286
+ )
287
+ )
239
288
  if not notes.is_empty():
240
289
  answer["note"] = " ".join(notes)
241
290
  return answer
@@ -268,6 +317,13 @@ static func shown(node: Node) -> bool:
268
317
  return parent == null or shown(parent)
269
318
 
270
319
 
320
+ ## Whether the player can read [param node]: [method shown], and drawn inside its viewport and every
321
+ ## ScrollContainer holding it, see [method GdhScreen.in_view]. A click by words asks [method shown]
322
+ ## alone, since it scrolls what it presses into view.
323
+ static func readable(node: Node) -> bool:
324
+ return shown(node) and GdhScreen.in_view(node)
325
+
326
+
271
327
  ## Whether the canvas [param item] is drawn on is showing: its layer, and the windows around it.
272
328
  static func _on_a_shown_canvas(item: CanvasItem) -> bool:
273
329
  var walk: Node = item.get_parent()
@@ -426,15 +482,46 @@ func read_text(params: Dictionary) -> Dictionary:
426
482
  # and a bare `truncated: true` was looked straight past twice before the limit was suspected.
427
483
  # A number is what sends somebody back.
428
484
  var lines: Array[String] = []
429
- var left_out: int = _read_into(root, include_hidden, limit, lines)
430
- return {
485
+ var tally: Dictionary[String, int] = {"omitted": 0, "out_of_view": 0, "copies": 0}
486
+ _read_into(root, include_hidden, limit, lines, tally)
487
+ var answer: Dictionary = {
431
488
  "type": "text",
432
489
  "root": root_path,
433
490
  "lines": lines,
434
491
  "count": lines.size(),
435
- "truncated": left_out > 0,
436
- "omitted": left_out,
492
+ "truncated": tally["omitted"] > 0,
493
+ "omitted": tally["omitted"],
437
494
  }
495
+ var notes: Array[String] = []
496
+ if tally["out_of_view"] > 0:
497
+ answer["out_of_view"] = tally["out_of_view"]
498
+ notes.append(
499
+ (
500
+ (
501
+ "%d line%s drawn outside the viewport or scrolled out of a ScrollContainer, where the"
502
+ + " player cannot read them, left out; includeHidden true answers with them too"
503
+ )
504
+ % [tally["out_of_view"], "" if tally["out_of_view"] == 1 else "s"]
505
+ )
506
+ )
507
+ if tally["copies"] > 0:
508
+ answer["copies"] = tally["copies"]
509
+ notes.append(
510
+ (
511
+ (
512
+ "%d line%s drawn as a copy under a node saying the same words, as an outline or a glow"
513
+ + " is drawn, read once with the words %s"
514
+ )
515
+ % [
516
+ tally["copies"],
517
+ "" if tally["copies"] == 1 else "s",
518
+ "it copies" if tally["copies"] == 1 else "they copy"
519
+ ]
520
+ )
521
+ )
522
+ if not notes.is_empty():
523
+ answer["note"] = " ".join(notes)
524
+ return answer
438
525
 
439
526
 
440
527
  ## Walks [param node] depth first, which is the order the screen is laid out in and the order a
@@ -445,22 +532,33 @@ func read_text(params: Dictionary) -> Dictionary:
445
532
  ## screen full of forms answered with every label on it and none of the values in it. The hidden
446
533
  ## check covers a [Window] for the same walk: a dropdown's popup is a child that is not drawn
447
534
  ## until it is opened, and reading a closed menu would put every item on the screen.
448
- ## Fills [param into] up to [param most] lines and answers with how many further lines the rest of
449
- ## the subtree says, which is what the caller is told rather than left to infer.
535
+ ## Fills [param into] up to [param most] lines and counts in [param tally] how many further lines the
536
+ ## rest of the subtree says, under omitted, and how many it says out of view, under out_of_view, which
537
+ ## is what the caller is told rather than left to infer.
450
538
  ##
451
539
  ## Asked of each node rather than cut off at the first hidden one, since a hidden panel does not
452
540
  ## hide a label reached through a plain Node: see [method shown].
453
- func _read_into(node: Node, include_hidden: bool, most: int, into: Array[String]) -> int:
454
- var left_out: int = 0
541
+ func _read_into(
542
+ node: Node, include_hidden: bool, most: int, into: Array[String], tally: Dictionary[String, int]
543
+ ) -> void:
455
544
  if include_hidden or shown(node):
456
- for said: String in GdhWords.lines_said_by(node):
457
- if into.size() < most:
458
- into.append(said)
459
- else:
460
- left_out += 1
545
+ var said: Array[String] = GdhWords.lines_said_by(node)
546
+ # A copy drawn under the words it copies, as an outline or a glow is, reads once. Only while
547
+ # what it copies is itself read, so an original hidden or drawn out of view does not take the
548
+ # words with it.
549
+ var original: Node = GdhWords.copied_from(node) if not said.is_empty() else null
550
+ if original != null and (include_hidden or readable(original)):
551
+ tally["copies"] += said.size()
552
+ elif not include_hidden and not GdhScreen.in_view(node):
553
+ tally["out_of_view"] += said.size()
554
+ else:
555
+ for line: String in said:
556
+ if into.size() < most:
557
+ into.append(line)
558
+ else:
559
+ tally["omitted"] += 1
461
560
  for child: Node in node.get_children(true):
462
- left_out += _read_into(child, include_hidden, most, into)
463
- return left_out
561
+ _read_into(child, include_hidden, most, into, tally)
464
562
 
465
563
 
466
564
  ## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
@@ -488,6 +586,7 @@ func get_rect(params: Dictionary) -> Dictionary:
488
586
  "type": "rect",
489
587
  "path": node_path,
490
588
  "visible": control.is_visible_in_tree(),
589
+ "in_view": GdhScreen.in_view(control),
491
590
  "canvas": _values.serialize(canvas_rect),
492
591
  "window":
493
592
  _values.serialize(
@@ -501,13 +600,20 @@ func get_rect(params: Dictionary) -> Dictionary:
501
600
  var item: Node2D = node
502
601
  var canvas_position: Vector2 = item.get_global_transform_with_canvas().origin
503
602
  var window_position: Vector2 = GdhScreen.in_window(item.get_viewport(), canvas_position)
504
- return {
603
+ var at: Dictionary = {
505
604
  "type": "point",
506
605
  "path": node_path,
507
606
  "visible": item.is_visible_in_tree(),
508
607
  "canvas": _values.serialize(canvas_position),
509
608
  "window": _values.serialize(window_position),
510
609
  }
610
+ for local: Rect2 in _drawn_rect(item):
611
+ var covered: Rect2 = item.get_global_transform_with_canvas() * local
612
+ at["covers"] = {
613
+ "canvas": _values.serialize(covered),
614
+ "window": _values.serialize(GdhScreen.rect_in_window(item.get_viewport(), covered)),
615
+ }
616
+ return at
511
617
  if node is Node3D:
512
618
  var spatial: Node3D = node
513
619
  return _in_the_frame(node_path, spatial)
@@ -516,6 +622,43 @@ func get_rect(params: Dictionary) -> Dictionary:
516
622
  }
517
623
 
518
624
 
625
+ ## The rectangle a 2D node's own geometry takes up in its own coordinates, as the one entry of the
626
+ ## list, which is empty for a node that has none of its own: a sprite's texture, a collision shape's
627
+ ## shape, the points of a polygon or a line. A Node2D answered only where it stands, so the extent of
628
+ ## a floor or a sprite to aim inside was a read of its shape and some arithmetic.
629
+ static func _drawn_rect(item: Node2D) -> Array[Rect2]:
630
+ var drawn: Array[Rect2] = []
631
+ var sprite: Sprite2D = item as Sprite2D
632
+ if sprite != null:
633
+ if sprite.texture != null:
634
+ drawn.append(sprite.get_rect())
635
+ return drawn
636
+ var shaped: CollisionShape2D = item as CollisionShape2D
637
+ if shaped != null:
638
+ if shaped.shape != null:
639
+ drawn.append(shaped.shape.get_rect())
640
+ return drawn
641
+ var points: PackedVector2Array = []
642
+ var offset: Vector2 = Vector2.ZERO
643
+ if item is Polygon2D:
644
+ var polygon: Polygon2D = item
645
+ points = polygon.polygon
646
+ offset = polygon.offset
647
+ elif item is CollisionPolygon2D:
648
+ var outline: CollisionPolygon2D = item
649
+ points = outline.polygon
650
+ elif item is Line2D:
651
+ var line: Line2D = item
652
+ points = line.points
653
+ if points.is_empty():
654
+ return drawn
655
+ var bounds: Rect2 = Rect2(points[0] + offset, Vector2.ZERO)
656
+ for point: Vector2 in points:
657
+ bounds = bounds.expand(point + offset)
658
+ drawn.append(bounds)
659
+ return drawn
660
+
661
+
519
662
  ## Where a 3D node is in the frame drawing it: the point to aim at, and the rectangle its own
520
663
  ## geometry covers, each in canvas coordinates and in window pixels.
521
664
  ##
@@ -723,6 +866,8 @@ func get_metrics(params: Dictionary) -> Dictionary:
723
866
  "frame_time": frame_time,
724
867
  "frame_time_max": frame_time_max,
725
868
  "frames_timed": timed,
869
+ "process_frames": Engine.get_process_frames(),
870
+ "physics_frames": Engine.get_physics_frames(),
726
871
  "physics_steps_per_second": physics_steps_per_second,
727
872
  "physics_ticks_per_second": Engine.physics_ticks_per_second,
728
873
  "max_physics_steps_per_frame": Engine.max_physics_steps_per_frame,
@@ -29,9 +29,13 @@ static func steps_out(viewport: Viewport, point: Vector2) -> Array[Dictionary]:
29
29
  var out: Array[Dictionary] = [{"viewport": viewport, "point": point, "through": null}]
30
30
  while true:
31
31
  var window: Window = viewport as Window
32
- if window != null and window.is_embedded() and window.get_parent() != null:
33
- point += Vector2(window.position)
34
- viewport = window.get_parent().get_viewport()
32
+ var host: Viewport = embedder_of(window) if window != null and window.is_embedded() else null
33
+ if host != null:
34
+ # A window's canvas is drawn into its embedder through its own final transform, which is
35
+ # where a content scale goes: one at 4 drew a button at four times the point it was clicked
36
+ # at, and the click missed it.
37
+ point = Vector2(window.position) + window.get_final_transform() * point
38
+ viewport = host
35
39
  out.append({"viewport": viewport, "point": point, "through": window})
36
40
  continue
37
41
  var shown_by: SubViewportContainer = viewport.get_parent() as SubViewportContainer
@@ -45,6 +49,19 @@ static func steps_out(viewport: Viewport, point: Vector2) -> Array[Dictionary]:
45
49
  return out
46
50
 
47
51
 
52
+ ## The viewport [param window] is embedded in and placed by, found as the engine finds it, which
53
+ ## does not offer it to scripts: the nearest viewport above that embeds subwindows. That is not
54
+ ## always its parent's: a window under a window that does not embed is embedded in the one above.
55
+ static func embedder_of(window: Window) -> Viewport:
56
+ var above: Node = window.get_parent()
57
+ # A viewport's get_viewport is itself, so a window's parent that is one is asked first.
58
+ var viewport: Viewport = above.get_viewport() if above != null else null
59
+ while viewport != null and not viewport.is_embedding_subwindows():
60
+ above = viewport.get_parent()
61
+ viewport = above.get_viewport() if above != null else null
62
+ return viewport
63
+
64
+
48
65
  ## Where [param point], in [param viewport]'s own coordinates, is in the pixels of the window the
49
66
  ## game is shown in, which is what a mouse event names.
50
67
  static func in_window(viewport: Viewport, point: Vector2) -> Vector2:
@@ -60,3 +77,32 @@ static func rect_in_window(viewport: Viewport, rect: Rect2) -> Rect2:
60
77
  var start: Vector2 = in_window(viewport, rect.position)
61
78
  var end: Vector2 = in_window(viewport, rect.end)
62
79
  return Rect2(start.min(end), (end - start).abs())
80
+
81
+
82
+ ## Whether any of [param node] is drawn where the player can see it: inside its viewport, and inside
83
+ ## every ScrollContainer between it and the viewport, each of which clips what it holds. A label
84
+ ## placed past the window's edge was read out as written on the screen. Only a Control has a box to
85
+ ## ask about, so anything else is in view.
86
+ static func in_view(node: Node) -> bool:
87
+ var control: Control = node as Control
88
+ return control == null or shown_part(control) != null
89
+
90
+
91
+ ## The part of [param control] the player can see, in its viewport's canvas, or null when there is
92
+ ## none: its drawn box cut to the viewport and to every ScrollContainer between, each cut taken from
93
+ ## what the last one left. A control in view of an inner list and of the outer one can still sit
94
+ ## where the two do not overlap.
95
+ static func shown_part(control: Control) -> Variant:
96
+ var part: Rect2 = control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size)
97
+ var clips: Array[Rect2] = [control.get_viewport().get_visible_rect()]
98
+ var walking: Node = control.get_parent()
99
+ while walking != null and not (walking is Viewport):
100
+ var holder: ScrollContainer = walking as ScrollContainer
101
+ if holder != null:
102
+ clips.append(holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size))
103
+ walking = walking.get_parent()
104
+ for clip: Rect2 in clips:
105
+ if not part.intersects(clip, true):
106
+ return null
107
+ part = part.intersection(clip)
108
+ return part
@@ -13,6 +13,15 @@ const GdhWords = preload("runtime_words.gd")
13
13
  ## How many of the controls saying the same words a refusal names, with the index for each.
14
14
  const LISTED_CANDIDATES: int = 10
15
15
 
16
+ ## Node.ProcessMode as its names, for a refusal to say which one is in force.
17
+ const PROCESS_MODES: Dictionary[int, String] = {
18
+ Node.PROCESS_MODE_INHERIT: "INHERIT",
19
+ Node.PROCESS_MODE_PAUSABLE: "PAUSABLE",
20
+ Node.PROCESS_MODE_WHEN_PAUSED: "WHEN_PAUSED",
21
+ Node.PROCESS_MODE_ALWAYS: "ALWAYS",
22
+ Node.PROCESS_MODE_DISABLED: "DISABLED",
23
+ }
24
+
16
25
  ## What a game started without a window gets whatever the project settings say, and the usual
17
26
  ## reason a control is out of reach. Only worth telling somebody when it is what they have.
18
27
  const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
@@ -753,9 +762,12 @@ static func _moved_by(node: Node, moves: Dictionary[ScrollContainer, Vector2]) -
753
762
  ## target's own viewport, a confirmation over a settings dialog did not cover the dialog's OK, and
754
763
  ## a click by words tied it with the confirmation's own.
755
764
  static func _window_over(target: Control, point: Vector2, window: Window) -> bool:
756
- if window.get_parent() == null or window.is_ancestor_of(target):
765
+ # Not passed over for holding the target in the tree: a window under an embedded window is
766
+ # embedded beside it, not in it, so the one holding it can be stacked over it. The target's own
767
+ # window is the step out below, and is never over it.
768
+ if window.get_parent() == null:
757
769
  return false
758
- var host: Viewport = window.get_parent().get_viewport()
770
+ var host: Viewport = GdhScreen.embedder_of(window)
759
771
  for step: Dictionary in GdhScreen.steps_out(target.get_viewport(), point):
760
772
  if step["viewport"] != host:
761
773
  continue
@@ -954,3 +966,20 @@ static func _out_of_reach_exactly(nodes: Array[Node], why: Array[String]) -> Str
954
966
  if nodes.size() == 1:
955
967
  return "1 control says exactly that and cannot be clicked: %s" % first
956
968
  return "%d controls say exactly that and cannot be clicked, such as %s" % [nodes.size(), first]
969
+
970
+
971
+ ## Which node's process_mode keeps [param control] from processing, and what it is. A node set to
972
+ ## inherit takes its parent's, so the one deciding can be any node above it: the game turning a
973
+ ## whole screen off while a menu shows over it reads, on the button, as a button turned off.
974
+ static func why_it_does_not_process(control: Control, node_path: String) -> String:
975
+ var deciding: Node = control
976
+ while deciding.process_mode == Node.PROCESS_MODE_INHERIT and deciding.get_parent() != null:
977
+ deciding = deciding.get_parent()
978
+ # The root inherits from nothing, which processes as pausable.
979
+ var value: int = deciding.process_mode
980
+ if value == Node.PROCESS_MODE_INHERIT:
981
+ value = Node.PROCESS_MODE_PAUSABLE
982
+ var mode: String = PROCESS_MODES.get(value, str(value))
983
+ if deciding == control:
984
+ return "its process_mode is %s" % mode
985
+ return "%s inherits process_mode %s from %s" % [node_path, mode, deciding.get_path()]