8const DataCacheManager = preload(
"res://scripts/systems/dataCacheManager/data_cache_manager.gd")
10var profiles:Dictionary = {}
11var data_cache_manager = DataCacheManager.new()
15func load_profiles(file_path:String) -> void:
17 var data = data_cache_manager.load_json(file_path)
18 if not data
is Dictionary:
20 for profile
in data.get(
"profiles", []):
21 if not profile
is Dictionary:
23 var id:String = str(profile.get(
"id",
""))
25 profiles[id] = profile.duplicate(true)
29func configure_profiles(next_profiles:Array) -> void:
31 for profile
in next_profiles:
32 if profile
is Dictionary:
33 var id:String = str(profile.get(
"id",
""))
35 profiles[id] = profile.duplicate(true)
39func get_cache_stats() -> Dictionary:
40 return data_cache_manager.get_cache_stats()
44func get_profile(profile_id:String, overrides:Dictionary = {}) -> Dictionary:
45 var profile:Dictionary = profiles.get(profile_id, {}).duplicate(true)
46 for key
in overrides.keys():
47 profile[key] = overrides[key]
52func nearest_target(actor:Dictionary, targets:Array, options:Dictionary = {}) -> Dictionary:
53 var origin:Vector2 = _position(actor)
54 var best:Dictionary = {}
55 var best_distance:float = INF
56 for target
in targets:
57 if not target
is Dictionary:
59 if bool(options.get(
"alive_only", true))
and not bool(target.get(
"alive", true)):
61 var distance:float = origin.distance_to(_position(target))
62 if distance < best_distance:
63 best_distance = distance
65 return best.duplicate(true)
69func choose_paddle_action(paddle:Dictionary, balls:Array, profile:Dictionary = {}) -> Dictionary:
70 var incoming_only:bool =
bool(profile.get(
"incoming_only", true))
71 var side:String = str(profile.get(
"side", paddle.get(
"side",
"right")))
72 var candidates:Array = []
74 if not ball
is Dictionary:
76 var velocity:Vector2 = _vector(ball.get(
"velocity", Vector2.ZERO))
78 if side ==
"right" and velocity.x <= 0.0:
80 if side ==
"left" and velocity.x >= 0.0:
82 candidates.append(ball)
83 if candidates.is_empty():
84 candidates = balls.duplicate(true)
85 var target:Dictionary = nearest_target(paddle, candidates, {
"alive_only": false})
86 var paddle_y:float = _position(paddle).y
87 var target_y:float = _position(target).y
if not target.is_empty()
else paddle_y
88 var deadzone:float =
float(profile.get(
"deadzone", 6.0))
90 if target_y < paddle_y - deadzone:
92 elif target_y > paddle_y + deadzone:
97 "direction": direction,
98 "target_id": str(target.get(
"id",
"")),
99 "confidence":
float(profile.get(
"difficulty", 0.6))
104func choose_pong_paddle_decision(paddle:Dictionary, balls:Array, playfield:Rect2, profile:Dictionary = {}) -> Dictionary:
105 var difficulty:float = clampf(
float(profile.get(
"difficulty", 0.6)), 0.0, 1.0)
106 var target:Dictionary = _nearest_arcade_ball(paddle, balls, profile)
107 var paddle_position:Vector2 = _position(paddle)
108 var velocity:Vector2 = _vector(target.get(
"velocity", Vector2.ZERO))
if not target.is_empty()
else Vector2.ZERO
109 var ball_position:Vector2 = _position(target)
if not target.is_empty()
else playfield.get_center()
110 var side:String = str(profile.get(
"side", paddle.get(
"side",
"right")))
111 var approaching:bool = (side ==
"right" and velocity.x > 0.0)
or (side ==
"left" and velocity.x < 0.0)
112 var vertical_margin:float =
float(profile.get(
"vertical_margin", 38.0))
113 var target_y:float = playfield.get_center().y
114 var update_delay:float =
float(profile.get(
"idle_update_delay", 0.42))
116 var lead_min:float =
float(profile.get(
"lead_time_min", 0.1))
117 var lead_max:float =
float(profile.get(
"lead_time_max", 0.24))
118 var lead_time:float = lerpf(lead_min, lead_max, difficulty)
119 var low_error:float =
float(profile.get(
"tracking_error_low", -34.0))
120 var high_error:float =
float(profile.get(
"tracking_error_high", 24.0))
121 if difficulty <
float(profile.get(
"wide_error_below", 0.4)):
122 low_error =
float(profile.get(
"easy_tracking_error_low", -44.0))
123 high_error =
float(profile.get(
"easy_tracking_error_high", 10.0))
124 var error_scale:float = lerpf(
float(profile.get(
"error_scale_min_difficulty", 1.25)),
float(profile.get(
"error_scale_max_difficulty", 0.35)), difficulty)
125 var tracking_error:float = randf_range(low_error, high_error) * error_scale
126 target_y = ball_position.y + velocity.y * lead_time + tracking_error
127 update_delay = lerpf(
float(profile.get(
"approach_update_delay_slow", 0.24)),
float(profile.get(
"approach_update_delay_fast", 0.11)), difficulty)
129 var idle_wander:float =
float(profile.get(
"idle_wander", 18.0))
130 target_y = playfield.get_center().y + randf_range(-idle_wander, idle_wander)
131 target_y = clampf(target_y, playfield.position.y + vertical_margin, playfield.end.y - vertical_margin)
132 var deadzone:float = lerpf(
float(profile.get(
"approach_deadzone_low", 18.0)),
float(profile.get(
"approach_deadzone_high", 6.0)), difficulty)
if approaching
else float(profile.get(
"idle_deadzone", 42.0))
133 var delta_to_target:float = target_y - paddle_position.y
134 var direction:float = 0.0
if abs(delta_to_target) < deadzone
else sign(delta_to_target)
138 "direction": direction,
139 "target_y": target_y,
140 "target_id": str(target.get(
"id",
"")),
141 "approaching": approaching,
142 "update_delay": update_delay,
143 "deadzone": deadzone,
144 "confidence": difficulty
149func should_use_pong_powerup(actor:Dictionary, ball:Dictionary, playfield:Rect2, profile:Dictionary = {}) -> bool:
152 var side:String = str(profile.get(
"side", actor.get(
"side",
"right")))
153 var owner_required:String = str(profile.get(
"powerup_owner", actor.get(
"id",
"computer")))
154 if str(ball.get(
"owner",
"")) != owner_required:
156 var velocity:Vector2 = _vector(ball.get(
"velocity", Vector2.ZERO))
157 if side ==
"right" and velocity.x >= 0.0:
159 if side ==
"left" and velocity.x <= 0.0:
161 var ball_position:Vector2 = _position(ball)
162 var progress:float = _arcade_ball_progress(ball_position, playfield, side)
163 var action_profile:Dictionary = profile.duplicate(true)
164 action_profile[
"require_after_hit"] =
bool(profile.get(
"require_after_hit", true))
165 action_profile[
"min_progress"] =
float(profile.get(
"min_progress", 0.45))
166 return should_use_action(actor, {
"after_hit": true,
"progress": progress}, action_profile)
170func choose_grid_action(actor:Dictionary, targets:Array, world:Dictionary = {}, profile:Dictionary = {}) -> Dictionary:
171 var target:Dictionary = nearest_target(actor, targets)
172 if target.is_empty():
173 return {
"type":
"wait",
"reason":
"no_target"}
174 var actor_cell:Vector2i = _cell(actor.get(
"cell", [0, 0]))
175 var target_cell:Vector2i = _cell(target.get(
"cell", [0, 0]))
176 var distance:int = abs(actor_cell.x - target_cell.x) + abs(actor_cell.y - target_cell.y)
177 var attack_range:int =
int(profile.get(
"attack_range", actor.get(
"attack_range", 1)))
178 if distance <= attack_range:
181 "target_id": str(target.get(
"id",
"")),
183 "target": target_cell,
184 "damage":
int(profile.get(
"damage", actor.get(
"attack", 1)))
186 var behavior:String = str(profile.get(
"behavior", actor.get(
"behavior",
"chase")))
187 if behavior ==
"flee":
188 return _move_action(actor_cell, _step_away(actor_cell, target_cell, world),
"flee", target)
189 if behavior ==
"patrol":
190 var patrol = profile.get(
"patrol", actor.get(
"patrol", []))
191 if patrol
is Array
and not patrol.is_empty():
192 return _move_action(actor_cell, _next_patrol_cell(actor_cell, patrol, world),
"patrol", target)
193 if behavior ==
"wander":
194 return _move_action(actor_cell, _first_open_neighbor(actor_cell, world),
"wander", target)
195 return _move_action(actor_cell, _step_toward(actor_cell, target_cell, world),
"chase", target)
199func should_use_action(actor:Dictionary, context:Dictionary = {}, profile:Dictionary = {}) -> bool:
200 var chance:float =
float(profile.get(
"use_chance", actor.get(
"use_chance", 0.0)))
201 if bool(profile.get(
"require_after_hit", false))
and not bool(context.get(
"after_hit", false)):
203 if profile.has(
"min_progress")
and float(context.get(
"progress", 0.0)) <
float(profile.get(
"min_progress", 0.0)):
205 return randf() <= clamp(chance, 0.0, 1.0)
208func _move_action(from_cell:Vector2i, to_cell:Vector2i, reason:String, target:Dictionary) -> Dictionary:
209 if to_cell == from_cell:
210 return {
"type":
"wait",
"reason": reason,
"target_id": str(target.get(
"id",
""))}
216 "target_id": str(target.get(
"id",
""))
220func _step_toward(from_cell:Vector2i, target_cell:Vector2i, world:Dictionary) -> Vector2i:
221 var options:Array = []
222 if target_cell.x != from_cell.x:
223 options.append(from_cell +
Vector2i(sign(target_cell.x - from_cell.x), 0))
224 if target_cell.y != from_cell.y:
225 options.append(from_cell +
Vector2i(0, sign(target_cell.y - from_cell.y)))
226 return _first_open(options, from_cell, world)
229func _step_away(from_cell:Vector2i, target_cell:Vector2i, world:Dictionary) -> Vector2i:
230 var options:Array = []
231 if target_cell.x != from_cell.x:
232 options.append(from_cell -
Vector2i(sign(target_cell.x - from_cell.x), 0))
233 if target_cell.y != from_cell.y:
234 options.append(from_cell -
Vector2i(0, sign(target_cell.y - from_cell.y)))
235 return _first_open(options, from_cell, world)
238func _next_patrol_cell(from_cell:Vector2i, patrol:Array, world:Dictionary) -> Vector2i:
239 for raw_cell
in patrol:
240 var next:Vector2i = _cell(raw_cell)
241 if next != from_cell
and _cell_open(next, world):
246func _first_open_neighbor(from_cell:Vector2i, world:Dictionary) -> Vector2i:
248 from_cell + Vector2i.RIGHT,
249 from_cell + Vector2i.LEFT,
250 from_cell + Vector2i.DOWN,
251 from_cell + Vector2i.UP
255func _first_open(options:Array, fallback:Vector2i, world:Dictionary) -> Vector2i:
257 if _cell_open(cell, world):
262func _cell_open(cell:Vector2i, world:Dictionary) -> bool:
263 var bounds = world.get(
"bounds", Rect2(Vector2.ZERO,
Vector2(99999, 99999)))
265 var tile_size:float =
float(world.get(
"tile_size", 1.0))
266 var center:Vector2 =
Vector2(
float(cell.x) * tile_size + tile_size * 0.5,
float(cell.y) * tile_size + tile_size * 0.5)
267 if not bounds.has_point(center):
269 for blocked
in world.get(
"blocked_cells", []):
270 if _cell(blocked) == cell:
275func _position(value) -> Vector2:
276 if value
is Dictionary:
277 if value.has(
"position"):
278 return _vector(value.get(
"position"))
279 if value.has(
"cell"):
280 var cell:Vector2i = _cell(value.get(
"cell"))
282 return _vector(value)
285func _nearest_arcade_ball(paddle:Dictionary, balls:Array, profile:Dictionary = {}) -> Dictionary:
286 var candidates:Array = []
287 var incoming_only:bool =
bool(profile.get(
"incoming_only", true))
288 var side:String = str(profile.get(
"side", paddle.get(
"side",
"right")))
290 if not ball
is Dictionary:
292 var velocity:Vector2 = _vector(ball.get(
"velocity", Vector2.ZERO))
294 if side ==
"right" and velocity.x <= 0.0:
296 if side ==
"left" and velocity.x >= 0.0:
298 candidates.append(ball)
299 if candidates.is_empty():
300 candidates = balls.duplicate(true)
301 var best:Dictionary = {}
302 var best_score:float = INF
303 var paddle_position:Vector2 = _position(paddle)
304 for candidate
in candidates:
305 if not candidate
is Dictionary:
307 var position:Vector2 = _position(candidate)
308 var velocity:Vector2 = _vector(candidate.get(
"velocity", Vector2.ZERO))
309 var approaching_penalty:float = 0.0
310 if side ==
"right" and velocity.x <= 0.0:
311 approaching_penalty = 9999.0
312 if side ==
"left" and velocity.x >= 0.0:
313 approaching_penalty = 9999.0
314 var score:float = abs(paddle_position.x - position.x) + approaching_penalty
315 if score < best_score:
318 return best.duplicate(true)
321func _arcade_ball_progress(ball_position:Vector2, playfield:Rect2, side:String) -> float:
322 if playfield.size.x <= 0.0:
324 var normalized:float = clampf((ball_position.x - playfield.position.x) / playfield.size.x, 0.0, 1.0)
326 return 1.0 - normalized
330func _vector(value) -> Vector2:
333 if value
is Vector2i:
334 return Vector2(value.x, value.y)
335 if value
is Array
and value.size() >= 2:
337 if value
is Dictionary
and value.has(
"x")
and value.has(
"y"):
342func _cell(value) -> Vector2i:
343 if value
is Vector2i:
347 if value
is Array
and value.size() >= 2:
349 if value
is Dictionary
and value.has(
"cell"):
350 return _cell(value.get(
"cell"))
351 if value
is Dictionary
and value.has(
"x")
and value.has(
"y"):