11const DEFAULT_BALL := {
12 "position": Vector2.ZERO,
17const DEFAULT_PADDLE := {
18 "position": Vector2.ZERO,
25func create_ball(position:Vector2 = Vector2.ZERO, velocity:Vector2 =
Vector2(220, -120), radius:float = 8.0) -> Dictionary:
26 var ball = DEFAULT_BALL.duplicate(true)
27 ball[
"position"] = position
28 ball[
"velocity"] = velocity
29 ball[
"radius"] = radius
34func create_paddle(position:Vector2 = Vector2.ZERO, size:Vector2 =
Vector2(14, 72), speed:float = 260.0) -> Dictionary:
35 var paddle = DEFAULT_PADDLE.duplicate(true)
36 paddle[
"position"] = position
38 paddle[
"speed"] = speed
43func step_ball(ball:Dictionary, delta:float, playfield:Rect2) -> Dictionary:
44 var next = ball.duplicate(true)
45 var radius =
float(next.get(
"radius", 8.0))
46 var previous_position:Vector2 = _vector(next.get(
"position", Vector2.ZERO))
47 var position = previous_position + next.get(
"velocity", Vector2.ZERO) * delta
48 var velocity = next.get(
"velocity", Vector2.ZERO)
49 if position.y - radius < playfield.position.y:
50 position.y = playfield.position.y + radius
51 velocity.y = abs(velocity.y)
52 elif position.y + radius > playfield.end.y:
53 position.y = playfield.end.y - radius
54 velocity.y = -abs(velocity.y)
55 next[
"previous_position"] = previous_position
56 next[
"position"] = position
57 next[
"velocity"] = velocity
64func step_balls(balls:Array, delta:float, playfield:Rect2, post_step:Callable =
Callable()) -> Array:
65 var stepped:Array = []
66 for index
in range(balls.size()):
67 if not balls[index]
is Dictionary:
69 var next_ball:Dictionary = step_ball(balls[index], delta, playfield)
70 if post_step.is_valid():
71 next_ball = post_step.call(index, next_ball)
72 stepped.append(next_ball)
79func step_ball_group(primary_ball:Dictionary, extra_ball_rows:Array, delta:float, playfield:Rect2, post_step:Callable =
Callable()) -> Dictionary:
80 var next_primary:Dictionary = {}
81 if not primary_ball.is_empty():
82 next_primary = step_ball(primary_ball, delta, playfield)
83 if post_step.is_valid():
84 next_primary = post_step.call(-1, next_primary)
85 var next_extras:Array = step_balls(extra_ball_rows, delta, playfield, post_step)
88 "extra_balls": next_extras,
89 "count": (1
if not next_primary.is_empty()
else 0) + next_extras.size()
96func collect_scoring_balls(balls:Array, playfield:Rect2) -> Dictionary:
97 var scoring_sides:Array = []
98 var active_balls:Array = []
99 for ball_value
in balls:
100 if not ball_value
is Dictionary:
102 var scoring_side:String = get_score_side(ball_value, playfield)
103 if scoring_side ==
"":
104 active_balls.append(ball_value)
106 scoring_sides.append(scoring_side)
108 "scoring_sides": scoring_sides,
109 "active_balls": active_balls
116func collect_ball_group_scoring(primary_ball:Dictionary, extra_ball_rows:Array, playfield:Rect2) -> Dictionary:
117 var primary_scoring_side:String =
""
118 if not primary_ball.is_empty():
119 primary_scoring_side = get_score_side(primary_ball, playfield)
120 var scoring_result:Dictionary = collect_scoring_balls(extra_ball_rows, playfield)
121 var scoring_sides:Array = scoring_result.get(
"scoring_sides", [])
122 if primary_scoring_side !=
"":
123 scoring_sides.push_front(primary_scoring_side)
124 var active_balls:Array = scoring_result.get(
"active_balls", [])
125 var next_ball:Dictionary = primary_ball
126 var next_extra_balls:Array = active_balls
127 if primary_scoring_side !=
"":
128 if not active_balls.is_empty():
129 next_ball = active_balls.pop_front()
130 next_extra_balls = active_balls
134 "primary_scoring_side": primary_scoring_side,
135 "scoring_sides": scoring_sides,
136 "next_ball": next_ball,
137 "active_balls": next_extra_balls,
138 "primary_promoted": primary_scoring_side !=
"" and not next_ball.is_empty()
143func move_paddle(paddle:Dictionary, direction:float, delta:float, playfield:Rect2) -> Dictionary:
144 var next:Dictionary = paddle.duplicate(true)
145 var position:Vector2 = _vector(next.get(
"position", Vector2.ZERO))
146 var size:Vector2 = _vector(next.get(
"size",
Vector2(14, 72)))
147 position.y += clamp(direction, -1.0, 1.0) *
float(next.get(
"speed", 260.0)) * delta
148 position.y = clamp(position.y, playfield.position.y + size.y * 0.5, playfield.end.y - size.y * 0.5)
149 next[
"position"] = position
154func create_projectile(owner:String, position:Vector2, velocity:Vector2, radius:float = 5.0, data:Dictionary = {}) -> Dictionary:
155 var projectile:Dictionary = data.duplicate(true)
156 projectile[
"owner"] = owner
157 projectile[
"position"] = position
158 projectile[
"velocity"] = velocity
159 projectile[
"radius"] = radius
164func create_horizontal_projectile(source:Dictionary, owner:String, speed:float, playfield:Rect2, options:Dictionary = {}) -> Dictionary:
165 if source.is_empty():
167 var direction:float =
float(options.get(
"direction", 1.0
if owner ==
"player" else -1.0))
168 var offset:Vector2 = _vector(options.get(
"offset",
Vector2(direction * 16.0, 0.0)))
169 var radius:float =
float(options.get(
"radius", 5.0))
170 var position:Vector2 = _vector(source.get(
"position", playfield.get_center())) + offset
171 return create_projectile(owner, position,
Vector2(direction * speed, 0.0), radius, options.get(
"data", {}))
175func step_projectile(projectile:Dictionary, delta:float) -> Dictionary:
176 var next:Dictionary = projectile.duplicate(true)
177 next[
"position"] = _vector(next.get(
"position", Vector2.ZERO)) + _vector(next.get(
"velocity", Vector2.ZERO)) * delta
190func step_projectiles(projectiles:Array, delta:float, playfield:Rect2, targets:Array = [], options:Dictionary = {}) -> Dictionary:
191 var active_projectiles:Array = []
193 var out_of_bounds:Array = []
194 var margin:float =
float(options.get(
"margin", 24.0))
195 for projectile_value
in projectiles:
196 if not projectile_value
is Dictionary:
198 var stepped_projectile:Dictionary = step_projectile(projectile_value, delta)
199 var hit_target:Dictionary = _projectile_hit_target(stepped_projectile, targets)
200 if not hit_target.is_empty():
202 "projectile": stepped_projectile,
203 "target_id": str(hit_target.get(
"id",
"")),
204 "target": hit_target.duplicate(true),
205 "owner": str(stepped_projectile.get(
"owner",
""))
208 if projectile_out_of_bounds(stepped_projectile, playfield, margin):
209 out_of_bounds.append(stepped_projectile)
211 active_projectiles.append(stepped_projectile)
213 "active_projectiles": active_projectiles,
215 "out_of_bounds": out_of_bounds
220func projectile_out_of_bounds(projectile:Dictionary, playfield:Rect2, margin:float = 24.0) -> bool:
221 var position:Vector2 = _vector(projectile.get(
"position", Vector2.ZERO))
222 return position.x < playfield.position.x - margin
or position.x > playfield.end.x + margin
or position.y < playfield.position.y - margin
or position.y > playfield.end.y + margin
226func projectile_intersects_paddle(projectile:Dictionary, paddle:Dictionary) -> bool:
227 if paddle.is_empty():
229 var paddle_size:Vector2 = _vector(paddle.get(
"size",
Vector2(12, 70)))
230 var paddle_rect:Rect2 =
Rect2(_vector(paddle.get(
"position", Vector2.ZERO)) - paddle_size * 0.5, paddle_size)
231 return paddle_rect.grow(
float(projectile.get(
"radius", 5.0))).has_point(_vector(projectile.get(
"position", Vector2.ZERO)))
235func create_split_ball(source_ball:Dictionary, options:Dictionary = {}) -> Dictionary:
236 if source_ball.is_empty():
238 var base_velocity:Vector2 = _vector(source_ball.get(
"velocity",
Vector2(220, 0)))
239 var split_velocity:Vector2 =
Vector2(base_velocity.x, -base_velocity.y)
240 var min_vertical:float =
float(options.get(
"min_vertical_speed", 130.0))
241 if abs(split_velocity.y) <
float(options.get(
"vertical_threshold", 90.0)):
242 split_velocity.y = min_vertical
if base_velocity.y <= 0.0
else -min_vertical
243 var min_speed:float =
float(options.get(
"min_speed", 150.0))
244 var speed_ratio:float =
float(options.get(
"speed_ratio", 0.72))
245 var split_speed:float = maxf(min_speed, base_velocity.length() * speed_ratio)
246 if split_velocity.length() > 0.001:
247 split_velocity = split_velocity.normalized() * split_speed
248 var split_ball:Dictionary = create_ball(_vector(source_ball.get(
"position", Vector2.ZERO)), split_velocity,
float(source_ball.get(
"radius", 8.0)))
249 if source_ball.has(
"last_hitter"):
250 split_ball[
"last_hitter"] = str(source_ball.get(
"last_hitter",
""))
256func ball_intersects_paddle(ball:Dictionary, paddle:Dictionary, inset:Vector2 = Vector2.ZERO) -> bool:
257 var position = ball.get(
"position", Vector2.ZERO)
258 var radius =
float(ball.get(
"radius", 8.0))
259 var size = paddle.get(
"size",
Vector2(14, 72))
260 var paddle_position = paddle.get(
"position", Vector2.ZERO)
261 var rect =
Rect2(paddle_position - size * 0.5, size).grow_individual(-inset.x, -inset.y, -inset.x, -inset.y)
263 clamp(position.x, rect.position.x, rect.end.x),
264 clamp(position.y, rect.position.y, rect.end.y)
266 return position.distance_to(closest) <= radius
271func ball_swept_intersects_paddle(ball:Dictionary, paddle:Dictionary, inset:Vector2 = Vector2.ZERO) -> bool:
272 if paddle.is_empty():
274 var current_position:Vector2 = _vector(ball.get(
"position", Vector2.ZERO))
275 var previous_position:Vector2 = _vector(ball.get(
"previous_position", current_position))
276 if previous_position == current_position:
277 return ball_intersects_paddle(ball, paddle, inset)
278 var radius:float =
float(ball.get(
"radius", 8.0))
279 var size:Vector2 = _vector(paddle.get(
"size",
Vector2(14, 72)))
280 var paddle_position:Vector2 = _vector(paddle.get(
"position", Vector2.ZERO))
281 var rect:Rect2 =
Rect2(paddle_position - size * 0.5, size).grow_individual(-inset.x, -inset.y, -inset.x, -inset.y).grow(radius)
282 return _segment_intersects_rect(previous_position, current_position, rect)
286func bounce_from_paddle(ball:Dictionary, paddle:Dictionary, horizontal_sign:float, speedup:float = 1.04) -> Dictionary:
287 var next = ball.duplicate(true)
288 var velocity = next.get(
"velocity", Vector2.ZERO)
289 var paddle_position = paddle.get(
"position", Vector2.ZERO)
290 var paddle_size = paddle.get(
"size",
Vector2(14, 72))
291 var ball_position = next.get(
"position", Vector2.ZERO)
292 var offset = clamp((ball_position.y - paddle_position.y) / max(1.0, paddle_size.y * 0.5), -1.0, 1.0)
293 var speed = max(80.0, velocity.length() * speedup)
294 var direction =
Vector2(sign(horizontal_sign), offset * 0.75).normalized()
295 ball_position.x = paddle_position.x + sign(horizontal_sign) * (paddle_size.x * 0.5 +
float(next.get(
"radius", 8.0)) + 0.5)
296 next[
"position"] = ball_position
297 next[
"previous_position"] = ball_position
298 next[
"velocity"] = direction * speed
303func get_score_side(ball:Dictionary, playfield:Rect2) -> String:
304 var position = ball.get(
"position", Vector2.ZERO)
305 var radius =
float(ball.get(
"radius", 8.0))
306 if position.x + radius < playfield.position.x:
308 if position.x - radius > playfield.end.x:
313func _vector(value) -> Vector2:
316 if value
is Vector2i:
317 return Vector2(value.x, value.y)
318 if value
is Array
and value.size() >= 2:
320 if value
is Dictionary
and value.has(
"x")
and value.has(
"y"):
325func _projectile_hit_target(projectile:Dictionary, targets:Array) -> Dictionary:
326 for target_value
in targets:
327 if not target_value
is Dictionary:
329 var target:Dictionary = target_value
330 if not _projectile_matches_target(projectile, target):
332 var paddle:Dictionary = _dictionary(target.get(
"paddle", {}))
333 if projectile_intersects_paddle(projectile, paddle):
338func _projectile_matches_target(projectile:Dictionary, target:Dictionary) -> bool:
339 var owner:String = str(projectile.get(
"owner",
""))
340 var owner_filters = target.get(
"owners", [])
341 if owner_filters
is Array:
342 var owners:Array = owner_filters
343 if not owners.is_empty()
and not owners.has(owner):
345 var excluded_filters = target.get(
"exclude_owners", [])
346 if excluded_filters
is Array:
347 var excluded_owners:Array = excluded_filters
348 if excluded_owners.has(owner):
353func _dictionary(value) -> Dictionary:
354 if value
is Dictionary:
359func _segment_intersects_rect(start:Vector2, end:Vector2, rect:Rect2) -> bool:
360 if rect.has_point(start)
or rect.has_point(end):
362 var delta:Vector2 = end - start
363 var t_min:float = 0.0
364 var t_max:float = 1.0
365 if abs(delta.x) < 0.00001:
366 if start.x < rect.position.x
or start.x > rect.end.x:
369 var inv_x:float = 1.0 / delta.x
370 var tx1:float = (rect.position.x - start.x) * inv_x
371 var tx2:float = (rect.end.x - start.x) * inv_x
373 var temp_x:float = tx1
376 t_min = maxf(t_min, tx1)
377 t_max = minf(t_max, tx2)
380 if abs(delta.y) < 0.00001:
381 return start.y >= rect.position.y
and start.y <= rect.end.y
382 var inv_y:float = 1.0 / delta.y
383 var ty1:float = (rect.position.y - start.y) * inv_y
384 var ty2:float = (rect.end.y - start.y) * inv_y
386 var temp_y:float = ty1
389 t_min = maxf(t_min, ty1)
390 t_max = minf(t_max, ty2)
391 return t_min <= t_max
Callable
Advance a list of ball dictionaries with the same bounds step.
float
Create a reusable projectile state dictionary for arcade/action games.
Vector2
Create a reusable ball state dictionary.