gdharness 1.1.13 → 1.1.15
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 +742 -361
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +19 -1
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +8 -51
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +97 -0
- package/build/godot/addons/gdharness_runtime/runtime_says.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +54 -12
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +4 -4
- package/build/index.js +623 -245
- package/build/keeper.js +111 -10
- package/package.json +1 -1
|
@@ -39,6 +39,11 @@ var _values: Values
|
|
|
39
39
|
var _pointer: Vector2 = Vector2.ZERO
|
|
40
40
|
var _pointer_placed: bool = false
|
|
41
41
|
|
|
42
|
+
## When the pointer last arrived, and how long it took over the arrival before that, in seconds: what
|
|
43
|
+
## turns a motion's distance into the speed a real one carries.
|
|
44
|
+
var _pointer_at_usec: int = 0
|
|
45
|
+
var _arrival_seconds: float = 1.0 / 60.0
|
|
46
|
+
|
|
42
47
|
|
|
43
48
|
func _init(host: Node, values: Values) -> void:
|
|
44
49
|
_host = host
|
|
@@ -46,9 +51,15 @@ func _init(host: Node, values: Values) -> void:
|
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
## The movement a pointer arriving at [param position] carries, which is the distance from where
|
|
49
|
-
## the last injected event put it, and none at all for the first. The arrival is recorded
|
|
54
|
+
## the last injected event put it, and none at all for the first. The arrival is recorded, with how
|
|
55
|
+
## long it took: at least a frame, since two motions sent in one frame did not cross the screen in
|
|
56
|
+
## no time at all.
|
|
50
57
|
func _arrive(position: Vector2) -> Vector2:
|
|
58
|
+
var now: int = Time.get_ticks_usec()
|
|
51
59
|
var relative: Vector2 = position - _pointer if _pointer_placed else Vector2.ZERO
|
|
60
|
+
var took: float = float(now - _pointer_at_usec) / 1_000_000.0 if _pointer_placed else 0.0
|
|
61
|
+
_arrival_seconds = maxf(took, 1.0 / 60.0)
|
|
62
|
+
_pointer_at_usec = now
|
|
52
63
|
_pointer = position
|
|
53
64
|
_pointer_placed = true
|
|
54
65
|
return relative
|
|
@@ -839,12 +850,19 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
839
850
|
## The buttons a real pointer event carries are the ones held as it happens, and Input keeps that
|
|
840
851
|
## from every button event it is given, injected ones included, so a motion between a held click
|
|
841
852
|
## and its release is a drag to a control that reads the mask rather than remembering the click.
|
|
853
|
+
##
|
|
854
|
+
## And the speed, the distance over the time the pointer took, which only the platform fills in. A
|
|
855
|
+
## menu ignores a motion with none, so that one opening under a resting pointer lights nothing: a
|
|
856
|
+
## motion over an open drop-down's item answered as injected and the menu highlighted nothing. A
|
|
857
|
+
## motion that goes nowhere still has none, as a resting pointer does.
|
|
842
858
|
func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
|
|
843
859
|
var event: InputEventMouseMotion = InputEventMouseMotion.new()
|
|
844
860
|
event.position = position
|
|
845
861
|
event.global_position = position
|
|
846
862
|
event.relative = relative
|
|
847
863
|
event.screen_relative = relative
|
|
864
|
+
event.velocity = relative / _arrival_seconds
|
|
865
|
+
event.screen_velocity = event.velocity
|
|
848
866
|
event.button_mask = Input.get_mouse_button_mask()
|
|
849
867
|
return event
|
|
850
868
|
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
|
|
6
6
|
const Paths = preload("runtime_paths.gd")
|
|
7
7
|
const Read = preload("reading.gd")
|
|
8
|
+
const Says = preload("runtime_says.gd")
|
|
8
9
|
const Screen = preload("runtime_screen.gd")
|
|
9
10
|
const Values = preload("runtime_values.gd")
|
|
10
11
|
const Words = preload("runtime_words.gd")
|
|
@@ -103,13 +104,13 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
103
104
|
var truncated: bool = false
|
|
104
105
|
# Counted while the tree is already being walked, for the answer below: how many nodes this
|
|
105
106
|
# find would have matched if the name had been read the way it was probably meant.
|
|
106
|
-
var literal: bool =
|
|
107
|
+
var literal: bool = Says.is_plain(wanted["name"])
|
|
107
108
|
var nearly: int = 0
|
|
108
109
|
# The same for the words: a glob is matched against the whole text, and one written as a prefix
|
|
109
110
|
# by habit, "Taken together*", missed the label whose sentence those words are in the middle of.
|
|
110
111
|
# Nothing found then read as the words not being on screen, which is the wrong conclusion when
|
|
111
112
|
# checking that a change to the text landed.
|
|
112
|
-
var widened_says: String =
|
|
113
|
+
var widened_says: String = Says.widened(wanted["says"])
|
|
113
114
|
var widened: Dictionary[String, String] = {}
|
|
114
115
|
for filter: String in wanted:
|
|
115
116
|
widened[filter] = widened_says if filter == "says" else wanted[filter]
|
|
@@ -241,50 +242,6 @@ static func _named(node: Node, pattern: String) -> bool:
|
|
|
241
242
|
return pattern.is_empty() or str(node.name).matchn(pattern)
|
|
242
243
|
|
|
243
244
|
|
|
244
|
-
## Whether [param pattern] is a name written as a whole word rather than as a glob, which is how a
|
|
245
|
-
## caller writes one when they mean "contains". [method String.matchn] answers nothing to it, and
|
|
246
|
-
## nothing is also what a name that is simply not there answers.
|
|
247
|
-
static func _is_literal(pattern: String) -> bool:
|
|
248
|
-
return not pattern.is_empty() and not pattern.contains("*") and not pattern.contains("?")
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
## Whether [param said] is what a find asked for in [param wanted].
|
|
252
|
-
##
|
|
253
|
-
## A plain word is a contains, which is what somebody looking for the row about a person means. A
|
|
254
|
-
## pattern is a glob, because [code]namePattern[/code] beside it is one and nobody writes `*Still*`
|
|
255
|
-
## in one field meaning a glob and in the other meaning those characters. Written as a contains
|
|
256
|
-
## only, a glob matched nothing at all and an empty answer reads as a control that is not on the
|
|
257
|
-
## screen: twice in one session here, over a button that was.
|
|
258
|
-
static func _says(said: String, wanted: String) -> bool:
|
|
259
|
-
var words: String = as_said(wanted)
|
|
260
|
-
return said.containsn(words) if _is_literal(words) else said.matchn(words)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
## Whether [param node]'s own words are what [param wanted] asks for, by the rules a find uses, so a
|
|
264
|
-
## click that names its control by its words finds the one a find would have answered with.
|
|
265
|
-
static func says(node: Node, wanted: String) -> bool:
|
|
266
|
-
return _says(Words.said_by(node), wanted)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
## [param wanted], a glob, open at both ends so it matches its words anywhere in a text; "" when
|
|
270
|
-
## it is not a glob, or already open at both ends, and so has nothing to suggest.
|
|
271
|
-
static func _widened(wanted: String) -> String:
|
|
272
|
-
if wanted.is_empty() or _is_literal(wanted):
|
|
273
|
-
return ""
|
|
274
|
-
var open: String = "*" + wanted.lstrip("*").rstrip("*") + "*"
|
|
275
|
-
return "" if open == wanted else open
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
## [param wanted] as words on a screen: a backslash followed by n is a line break.
|
|
279
|
-
##
|
|
280
|
-
## A button with two lines on it was asked for with the break written as the two characters, the
|
|
281
|
-
## way it is typed into a JSON string one escape short, and was answered as not there: 0 found,
|
|
282
|
-
## which reads as a control that is not on the screen. Nothing on a screen says a backslash and
|
|
283
|
-
## an n, so the two characters mean the break to everybody who writes them.
|
|
284
|
-
static func as_said(wanted: String) -> String:
|
|
285
|
-
return wanted.replace("\\n", "\n")
|
|
286
|
-
|
|
287
|
-
|
|
288
245
|
## Every filter but the name, so a find that came back empty can say how many nodes the name was
|
|
289
246
|
## the only thing standing between it and.
|
|
290
247
|
func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) -> bool:
|
|
@@ -293,7 +250,7 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
|
|
|
293
250
|
# What this node says, rather than everything said underneath it. A row is then found by the
|
|
294
251
|
# label in it, and the path answered is that label's, which is where the words a caller is
|
|
295
252
|
# looking at actually are: matching every container above it would answer with the screen.
|
|
296
|
-
if not wanted["says"].is_empty() and not
|
|
253
|
+
if not wanted["says"].is_empty() and not Says.says(node, wanted["says"]):
|
|
297
254
|
return false
|
|
298
255
|
var script: Variant = node.get_script()
|
|
299
256
|
if not wanted["script"].is_empty():
|
|
@@ -327,10 +284,6 @@ static func _script_is(attached: Script, wanted: String) -> bool:
|
|
|
327
284
|
return false
|
|
328
285
|
|
|
329
286
|
|
|
330
|
-
## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
|
|
331
|
-
## is drawn in, in both the canvas coordinates the node reports and the window pixels input
|
|
332
|
-
## arrives in. The two differ whenever the project stretches its viewport, which is what made a
|
|
333
|
-
## rect unusable for a click.
|
|
334
287
|
## Every line of text under a node, in the order somebody reads the screen.
|
|
335
288
|
##
|
|
336
289
|
## The question a caller asks most often, and the one that cost the most to answer: reading a panel
|
|
@@ -414,6 +367,10 @@ static func _drawn(node: Node) -> bool:
|
|
|
414
367
|
return window == null or window.visible
|
|
415
368
|
|
|
416
369
|
|
|
370
|
+
## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
|
|
371
|
+
## is drawn in, in both the canvas coordinates the node reports and the window pixels input
|
|
372
|
+
## arrives in. The two differ whenever the project stretches its viewport, which is what made a
|
|
373
|
+
## rect unusable for a click.
|
|
417
374
|
func get_rect(params: Dictionary) -> Dictionary:
|
|
418
375
|
var node_path: String = str(params.get("path", ""))
|
|
419
376
|
if node_path.is_empty():
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Whether a node says what a caller asked for in `says`: the one set of rules a find, a click and a
|
|
4
|
+
## wait all match words by, so the three never disagree about which control a word names.
|
|
5
|
+
|
|
6
|
+
const Words = preload("runtime_words.gd")
|
|
7
|
+
|
|
8
|
+
## Stands in for a bar written as `\|` while `says` is split into its alternatives: a character from
|
|
9
|
+
## the private use area, which no game's text uses.
|
|
10
|
+
const ESCAPED_BAR: String = ""
|
|
11
|
+
|
|
12
|
+
## How many splits [method alternatives] keeps. A find asks about two at once, the words it was given
|
|
13
|
+
## and the same words widened, and anything past a handful is a caller that has moved on.
|
|
14
|
+
const SPLITS_KEPT: int = 8
|
|
15
|
+
|
|
16
|
+
## Each `says` split into alternatives, by what was written; see [method alternatives].
|
|
17
|
+
static var _splits: Dictionary[String, Array] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Whether [param node]'s own words are what [param wanted] asks for.
|
|
21
|
+
static func says(node: Node, wanted: String) -> bool:
|
|
22
|
+
return matches(Words.said_by(node), wanted)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Whether [param said] is what [param wanted] asks for.
|
|
26
|
+
##
|
|
27
|
+
## A plain word is a contains, which is what somebody looking for the row about a person means. A
|
|
28
|
+
## pattern is a glob, because [code]namePattern[/code] beside it is one and nobody writes `*Still*`
|
|
29
|
+
## in one field meaning a glob and in the other meaning those characters. Written as a contains
|
|
30
|
+
## only, a glob matched nothing at all and an empty answer reads as a control that is not on the
|
|
31
|
+
## screen: twice in one session here, over a button that was.
|
|
32
|
+
##
|
|
33
|
+
## Several alternatives separated by `|` match when any one of them does, each by those rules: a
|
|
34
|
+
## wait for the end of a turn is a wait for whichever of "won", "lost" or the next turn's words comes
|
|
35
|
+
## first, and read as one glob with the bars in it, it waited out its whole timeout for words no
|
|
36
|
+
## screen says.
|
|
37
|
+
static func matches(said: String, wanted: String) -> bool:
|
|
38
|
+
for words: String in alternatives(wanted):
|
|
39
|
+
if said.containsn(words) if is_plain(words) else said.matchn(words):
|
|
40
|
+
return true
|
|
41
|
+
return false
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## The alternatives [param wanted] names, each as words on a screen: split at every `|` not written
|
|
45
|
+
## as `\|`, which stays a bar in the words. An empty alternative, a stray bar at either end, is
|
|
46
|
+
## dropped: it names no words, and a find that came back empty widened it to `**` and suggested a
|
|
47
|
+
## pattern that matches every node there is.
|
|
48
|
+
##
|
|
49
|
+
## Kept once split, since a find, a click or a wait asks about the same words for every node on the
|
|
50
|
+
## screen: split afresh for each of them, one look over a hall of twenty thousand nodes took 210ms
|
|
51
|
+
## rather than 155ms. The array handed back is shared and not to be changed.
|
|
52
|
+
static func alternatives(wanted: String) -> Array[String]:
|
|
53
|
+
var kept: Variant = _splits.get(wanted)
|
|
54
|
+
if kept != null:
|
|
55
|
+
var split: Array[String] = kept
|
|
56
|
+
return split
|
|
57
|
+
var held: String = as_said(wanted).replace("\\|", ESCAPED_BAR)
|
|
58
|
+
var found: Array[String] = []
|
|
59
|
+
for part: String in held.split("|"):
|
|
60
|
+
var words: String = part.replace(ESCAPED_BAR, "|")
|
|
61
|
+
if not words.is_empty():
|
|
62
|
+
found.append(words)
|
|
63
|
+
if _splits.size() >= SPLITS_KEPT:
|
|
64
|
+
_splits.clear()
|
|
65
|
+
_splits[wanted] = found
|
|
66
|
+
return found
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## Whether [param pattern] is words written whole rather than as a glob, which is how a caller
|
|
70
|
+
## writes them when they mean "contains". [method String.matchn] answers nothing to it, and nothing
|
|
71
|
+
## is also what words that are simply not there answer.
|
|
72
|
+
static func is_plain(pattern: String) -> bool:
|
|
73
|
+
return not pattern.is_empty() and not pattern.contains("*") and not pattern.contains("?")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## [param wanted], a glob, open at both ends so it matches its words anywhere in a text; "" when
|
|
77
|
+
## it is not a glob, or already open at both ends, and so has nothing to suggest.
|
|
78
|
+
static func widened(wanted: String) -> String:
|
|
79
|
+
var open_ended: Array[String] = []
|
|
80
|
+
var changed: bool = false
|
|
81
|
+
for words: String in alternatives(wanted):
|
|
82
|
+
var open: String = words
|
|
83
|
+
if not is_plain(words):
|
|
84
|
+
open = "*" + words.lstrip("*").rstrip("*") + "*"
|
|
85
|
+
changed = changed or open != words
|
|
86
|
+
open_ended.append(open.replace("|", "\\|"))
|
|
87
|
+
return "|".join(open_ended) if changed else ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## [param wanted] as words on a screen: a backslash followed by n is a line break.
|
|
91
|
+
##
|
|
92
|
+
## A button with two lines on it was asked for with the break written as the two characters, the
|
|
93
|
+
## way it is typed into a JSON string one escape short, and was answered as not there: 0 found,
|
|
94
|
+
## which reads as a control that is not on the screen. Nothing on a screen says a backslash and
|
|
95
|
+
## an n, so the two characters mean the break to everybody who writes them.
|
|
96
|
+
static func as_said(wanted: String) -> String:
|
|
97
|
+
return wanted.replace("\\n", "\n")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://cx6i38yml0dy7
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
|
|
5
5
|
const Queries = preload("runtime_queries.gd")
|
|
6
6
|
const Read = preload("reading.gd")
|
|
7
|
+
const Says = preload("runtime_says.gd")
|
|
7
8
|
const Values = preload("runtime_values.gd")
|
|
8
9
|
const Words = preload("runtime_words.gd")
|
|
9
10
|
|
|
@@ -43,7 +44,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
43
44
|
var pending: Array[Node] = [reached["node"]]
|
|
44
45
|
while not pending.is_empty():
|
|
45
46
|
var node: Node = pending.pop_front()
|
|
46
|
-
if node is Control and
|
|
47
|
+
if node is Control and Says.says(node, wanted):
|
|
47
48
|
if not Queries.shown(node):
|
|
48
49
|
hidden += 1
|
|
49
50
|
else:
|
|
@@ -142,20 +143,35 @@ static func _rank(target: Control, whole: bool) -> int:
|
|
|
142
143
|
return (2 if target is BaseButton else 0) + (1 if whole else 0)
|
|
143
144
|
|
|
144
145
|
|
|
145
|
-
## Whether [param
|
|
146
|
-
## whole text already, so every match of one is whole.
|
|
146
|
+
## Whether one of [param wanted]'s alternatives is the whole of [param said], or the whole of one of
|
|
147
|
+
## its lines. A glob is matched against the whole text already, so every match of one is whole.
|
|
148
|
+
##
|
|
149
|
+
## A line counts because a card is a title over a description: "WARD" over "wards 3" is the card
|
|
150
|
+
## named WARD, and read as its words run together it said WARD only as part of more, level with
|
|
151
|
+
## "WARDSPITE" over its own description, and a click on WARD was refused as not clear.
|
|
147
152
|
static func _says_exactly(said: String, wanted: String) -> bool:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
for words: String in Says.alternatives(wanted):
|
|
154
|
+
if not Says.is_plain(words):
|
|
155
|
+
if said.matchn(words):
|
|
156
|
+
return true
|
|
157
|
+
continue
|
|
158
|
+
var looked_for: String = words.strip_edges()
|
|
159
|
+
if said.strip_edges().nocasecmp_to(looked_for) == 0:
|
|
160
|
+
return true
|
|
161
|
+
for line: String in said.split("\n"):
|
|
162
|
+
if line.strip_edges().nocasecmp_to(looked_for) == 0:
|
|
163
|
+
return true
|
|
164
|
+
return false
|
|
152
165
|
|
|
153
166
|
|
|
154
167
|
## [param count] matches of [param rank], as the start of a sentence.
|
|
155
168
|
static func _described(count: int, rank: int, wanted: String, under: String) -> String:
|
|
156
169
|
var what: String = "button" if rank >= 2 else "control"
|
|
157
170
|
var how: String = '"%s"' % wanted
|
|
158
|
-
|
|
171
|
+
var plain: bool = true
|
|
172
|
+
for words: String in Says.alternatives(wanted):
|
|
173
|
+
plain = plain and Says.is_plain(words)
|
|
174
|
+
if plain:
|
|
159
175
|
how = ('exactly "%s"' if rank % 2 == 1 else '"%s" as part of more') % wanted
|
|
160
176
|
if count == 1:
|
|
161
177
|
return "the one %s on screen%s saying %s" % [what, under, how]
|
|
@@ -247,9 +263,12 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
247
263
|
##
|
|
248
264
|
## The engine's own order for which control takes the pointer: an embedded window over everything
|
|
249
265
|
## in the viewport that holds it, then the higher canvas layer, then the later in the tree. A
|
|
250
|
-
## control holding the target or held by it is part of the same click.
|
|
251
|
-
##
|
|
252
|
-
##
|
|
266
|
+
## control holding the target or held by it is part of the same click.
|
|
267
|
+
##
|
|
268
|
+
## A target whose centre its own container clips away is judged where the click will put it, since
|
|
269
|
+
## the click scrolls it into view first: what is drawn over its place now is not what it will land
|
|
270
|
+
## under. Judged not covered at all instead, a button below the fold of a roster that a full-screen
|
|
271
|
+
## page covered was picked over the ones in view, scrolled up under the page and pressed there.
|
|
253
272
|
##
|
|
254
273
|
## [param areas] holds the box around each of [param drawn], so everything whose box misses the
|
|
255
274
|
## point is passed over before anything costlier is asked of it.
|
|
@@ -257,7 +276,7 @@ static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2])
|
|
|
257
276
|
var viewport: Viewport = target.get_viewport()
|
|
258
277
|
var point: Vector2 = target.get_global_transform_with_canvas() * (target.size * 0.5)
|
|
259
278
|
if _clipped_away(target, point):
|
|
260
|
-
|
|
279
|
+
point = _where_it_lands(target, point)
|
|
261
280
|
var layer: int = _layer_of(target)
|
|
262
281
|
for at: int in drawn.size():
|
|
263
282
|
if not areas[at].has_point(point):
|
|
@@ -296,6 +315,29 @@ static func _holds(control: Control, point: Vector2) -> bool:
|
|
|
296
315
|
return Rect2(Vector2.ZERO, control.size).has_point(placed.affine_inverse() * point)
|
|
297
316
|
|
|
298
317
|
|
|
318
|
+
## Where [param target]'s centre, now at [param point], will be once the click has scrolled it into
|
|
319
|
+
## view: moved as little as puts the whole of it inside each container clipping it, innermost first,
|
|
320
|
+
## which is how a scroll container brings a control into view. A target larger than a container is
|
|
321
|
+
## centred in it.
|
|
322
|
+
static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
|
|
323
|
+
var drawn: Transform2D = target.get_global_transform_with_canvas()
|
|
324
|
+
var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
|
|
325
|
+
var landed: Vector2 = point
|
|
326
|
+
var walk: Node = target.get_parent()
|
|
327
|
+
while walk != null:
|
|
328
|
+
var holder: Control = walk as Control
|
|
329
|
+
if holder != null and holder.clip_contents:
|
|
330
|
+
var shown: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
|
|
331
|
+
var low: Vector2 = shown.position + half
|
|
332
|
+
var high: Vector2 = shown.end - half
|
|
333
|
+
landed = Vector2(
|
|
334
|
+
shown.get_center().x if low.x > high.x else clampf(landed.x, low.x, high.x),
|
|
335
|
+
shown.get_center().y if low.y > high.y else clampf(landed.y, low.y, high.y)
|
|
336
|
+
)
|
|
337
|
+
walk = walk.get_parent()
|
|
338
|
+
return landed
|
|
339
|
+
|
|
340
|
+
|
|
299
341
|
## Whether a container clipping what it holds cuts [param point] off from [param control].
|
|
300
342
|
static func _clipped_away(control: Control, point: Vector2) -> bool:
|
|
301
343
|
var walk: Node = control.get_parent()
|
|
@@ -6,6 +6,7 @@ extends RefCounted
|
|
|
6
6
|
const Paths = preload("runtime_paths.gd")
|
|
7
7
|
const Queries = preload("runtime_queries.gd")
|
|
8
8
|
const Read = preload("reading.gd")
|
|
9
|
+
const Says = preload("runtime_says.gd")
|
|
9
10
|
const Values = preload("runtime_values.gd")
|
|
10
11
|
|
|
11
12
|
## The longest one wait may last, whatever the request says: past this the server has long
|
|
@@ -249,7 +250,6 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
|
|
|
249
250
|
## caller sees what it cost.
|
|
250
251
|
func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_hidden: bool) -> Dictionary:
|
|
251
252
|
var started: int = Time.get_ticks_msec()
|
|
252
|
-
var words: String = Queries.as_said(said)
|
|
253
253
|
var frames: int = 0
|
|
254
254
|
var looks: int = 0
|
|
255
255
|
var next_look: int = 0
|
|
@@ -257,7 +257,7 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
257
257
|
while true:
|
|
258
258
|
if Time.get_ticks_usec() >= next_look:
|
|
259
259
|
var looking: int = Time.get_ticks_usec()
|
|
260
|
-
found = _anything_says(node_path,
|
|
260
|
+
found = _anything_says(node_path, said, include_hidden)
|
|
261
261
|
looks += 1
|
|
262
262
|
next_look = Time.get_ticks_usec() + (Time.get_ticks_usec() - looking) * LOOKS_APART
|
|
263
263
|
if found or Time.get_ticks_msec() - started >= timeout_ms:
|
|
@@ -267,7 +267,7 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
267
267
|
# One more look at the end, so words that arrived between two paced looks are not answered as
|
|
268
268
|
# never having come.
|
|
269
269
|
if not found:
|
|
270
|
-
found = _anything_says(node_path,
|
|
270
|
+
found = _anything_says(node_path, said, include_hidden)
|
|
271
271
|
looks += 1
|
|
272
272
|
|
|
273
273
|
return {
|
|
@@ -293,7 +293,7 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
|
|
|
293
293
|
var pending: Array[Node] = [root]
|
|
294
294
|
while not pending.is_empty():
|
|
295
295
|
var node: Node = pending.pop_back()
|
|
296
|
-
if
|
|
296
|
+
if Says.says(node, said) and (include_hidden or Queries.shown(node)):
|
|
297
297
|
return true
|
|
298
298
|
pending.append_array(node.get_children(true))
|
|
299
299
|
return false
|