RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
arcade_powerup_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
7
8const DataCacheManager = preload("res://scripts/systems/dataCacheManager/data_cache_manager.gd")
9
10var definitions:Dictionary = {}
11var spawn_config := {
12 "max_active": 2,
13 "lifetime": 10.0,
14 "min_delay": 1.0,
15 "max_delay": 2.0,
16 "followup_min_delay": 0.7,
17 "followup_max_delay": 1.5,
18 "pace": 1.0,
19 "collect_radius": 12.0,
20 "x_margin_ratio": 0.25,
21 "y_margin": 28.0
22}
23var active_powerups:Array = []
24var spawn_timer:float = 0.0
25var data_cache_manager = DataCacheManager.new()
26
27
28
29func load_definitions(file_path:String, options:Dictionary = {}) -> Array:
30 var parsed = data_cache_manager.load_json(file_path)
31 if not parsed is Dictionary:
32 configure([], options)
33 return []
34 configure(parsed.get("powerups", []), options)
35 return get_powerup_ids()
36
37
38
39func get_cache_stats() -> Dictionary:
40 return data_cache_manager.get_cache_stats()
41
42
43
44func configure(next_definitions, options:Dictionary = {}) -> void:
45 definitions.clear()
46 if next_definitions is Array:
47 for row in next_definitions:
48 if row is Dictionary:
49 var id:String = str(row.get("id", ""))
50 if id != "":
51 definitions[id] = row.duplicate(true)
52 else:
53 definitions[str(row)] = {"id": str(row)}
54 elif next_definitions is Dictionary:
55 for id in next_definitions.keys():
56 var value = next_definitions[id]
57 definitions[str(id)] = value.duplicate(true) if value is Dictionary else {"id": str(id)}
58 for key in options.keys():
59 spawn_config[key] = options[key]
60
61
62func get_powerup_ids() -> Array:
63 var ids := definitions.keys()
64 ids.sort()
65 return ids
66
67
68func get_definition(powerup_id:String) -> Dictionary:
69 return definitions.get(powerup_id, {"id": powerup_id, "label": powerup_id.capitalize()}).duplicate(true)
70
71
72func get_label(powerup_id:String) -> String:
73 return str(get_definition(powerup_id).get("label", powerup_id.capitalize()))
74
75
76func get_color(powerup_id:String, fallback:Color = Color.WHITE) -> Color:
77 var definition := get_definition(powerup_id)
78 if definition.has("color"):
79 return Color(str(definition.get("color", "#ffffff")))
80 return fallback
81
82
83
85func apply_effects(powerup_id:String, context:Dictionary = {}, handlers:Dictionary = {}) -> Array:
86 var operations:Array = []
87 var definition:Dictionary = get_definition(powerup_id)
88 for effect in definition.get("effects", []):
89 if not effect is Dictionary:
90 continue
91 var operation := normalize_effect(powerup_id, effect, context)
92 if operation.is_empty():
93 continue
94 operations.append(operation)
95 _dispatch_effect(operation, handlers)
96 return operations
97
98
99
100func normalize_effect(powerup_id:String, effect:Dictionary, context:Dictionary = {}) -> Dictionary:
101 var operation:Dictionary = effect.duplicate(true)
102 var effect_type:String = str(operation.get("type", ""))
103 if effect_type == "":
104 return {}
105 operation["type"] = effect_type
106 operation["powerup_id"] = powerup_id
107 operation["label"] = str(operation.get("label", get_label(powerup_id)))
108 if context.has("recipient"):
109 operation["recipient"] = str(context.get("recipient", ""))
110 if operation.has("duration"):
111 operation["duration"] = float(operation.get("duration", 0.0))
112 match effect_type:
113 "clear_timer":
114 operation["type"] = "set_timer"
115 operation["duration"] = 0.0
116 "effect_label":
117 operation["duration"] = float(operation.get("duration", 5.0))
118 "slow_opponent":
119 var slow_recipient:String = str(context.get("recipient", "player"))
120 operation["timer"] = "opponent_slow" if slow_recipient == "player" else "player_slow"
121 operation["type"] = "set_timer"
122 operation["duration"] = float(operation.get("duration", 10.0))
123 "curve_boost":
124 var curve_recipient:String = str(context.get("recipient", "player"))
125 operation["timer"] = "player_curve_boost" if curve_recipient == "player" else "opponent_curve_boost"
126 operation["type"] = "set_timer"
127 operation["duration"] = float(operation.get("duration", 10.0))
128 return operation
129
130
131
132func update(delta:float, playfield:Rect2, collectors:Array, options:Dictionary = {}) -> Dictionary:
133 for key in options.keys():
134 spawn_config[key] = options[key]
135 var events:Dictionary = {"spawned": [], "collected": [], "expired": []}
136 spawn_timer -= delta
137 if active_powerups.size() < int(spawn_config.get("max_active", 2)) and spawn_timer <= 0.0:
138 var spawned:Dictionary = spawn(playfield)
139 if not spawned.is_empty():
140 events["spawned"].append(spawned)
141 spawn_timer = next_spawn_delay(active_powerups.size() < int(spawn_config.get("max_active", 2)))
142 var remaining:Array = []
143 for powerup in active_powerups:
144 var next_powerup:Dictionary = powerup.duplicate(true)
145 next_powerup["age"] = float(next_powerup.get("age", 0.0)) + delta
146 if float(next_powerup.get("age", 0.0)) >= float(spawn_config.get("lifetime", 10.0)):
147 events["expired"].append(next_powerup)
148 spawn_timer = min(spawn_timer, next_spawn_delay(true) * 0.7)
149 continue
150 var collector:Dictionary = find_collector(next_powerup, collectors)
151 if not collector.is_empty():
152 next_powerup["collector"] = str(collector.get("owner", ""))
153 next_powerup["collector_id"] = str(collector.get("id", ""))
154 events["collected"].append(next_powerup)
155 spawn_timer = min(spawn_timer, next_spawn_delay(true))
156 else:
157 remaining.append(next_powerup)
158 active_powerups = remaining
159 return events
160
161
162
163func spawn(playfield:Rect2, powerup_id:String = "") -> Dictionary:
164 var ids:Array = definitions.keys()
165 if ids.is_empty():
166 return {}
167 var id:String = powerup_id if powerup_id != "" else str(ids[randi() % ids.size()])
168 var x_margin:float = playfield.size.x * float(spawn_config.get("x_margin_ratio", 0.25))
169 var y_margin:float = float(spawn_config.get("y_margin", 28.0))
170 var row:Dictionary = {
171 "id": "%s_%d" % [id, Time.get_ticks_msec()],
172 "type": id,
173 "position": Vector2(
174 randf_range(playfield.position.x + x_margin, playfield.end.x - x_margin),
175 randf_range(playfield.position.y + y_margin, playfield.end.y - y_margin)
176 ),
177 "age": 0.0
178 }
179 active_powerups.append(row)
180 return row.duplicate(true)
181
182
183
184func find_collector(powerup:Dictionary, collectors:Array) -> Dictionary:
185 var powerup_position:Vector2 = _vector(powerup.get("position", Vector2.ZERO))
186 for collector in collectors:
187 if not collector is Dictionary:
188 continue
189 var radius:float = float(collector.get("radius", 8.0)) + float(spawn_config.get("collect_radius", 12.0))
190 if _vector(collector.get("position", Vector2.ZERO)).distance_to(powerup_position) <= radius:
191 return collector.duplicate(true)
192 return {}
193
194
195func next_spawn_delay(wants_followup:bool) -> float:
196 var min_delay:float = float(spawn_config.get("followup_min_delay" if wants_followup else "min_delay", 0.7))
197 var max_delay:float = float(spawn_config.get("followup_max_delay" if wants_followup else "max_delay", 1.5))
198 var pace:float = maxf(0.1, float(spawn_config.get("pace", 1.0)))
199 return maxf(0.25, randf_range(min_delay, max_delay) / pace)
200
201
202func clear() -> void:
203 active_powerups.clear()
204 spawn_timer = 0.0
205
206
207func get_active() -> Array:
208 return active_powerups.duplicate(true)
209
210
211func set_active(rows:Array) -> void:
212 active_powerups = rows.duplicate(true)
213
214
215func get_state() -> Dictionary:
216 return {
217 "active_powerups": active_powerups.duplicate(true),
218 "spawn_timer": spawn_timer,
219 "spawn_config": spawn_config.duplicate(true)
220 }
221
222
223func apply_state(state:Dictionary) -> void:
224 active_powerups = state.get("active_powerups", []).duplicate(true)
225 spawn_timer = float(state.get("spawn_timer", spawn_timer))
226 var saved_config = state.get("spawn_config", {})
227 if saved_config is Dictionary:
228 for key in saved_config.keys():
229 spawn_config[key] = saved_config[key]
230
231
232func _dispatch_effect(operation:Dictionary, handlers:Dictionary) -> void:
233 var effect_type:String = str(operation.get("type", ""))
234 var handler_key:String = effect_type
235 if not handlers.has(handler_key):
236 handler_key = "custom_effect"
237 var handler = handlers.get(handler_key, Callable())
238 if handler is Callable and handler.is_valid():
239 if handler_key == "custom_effect":
240 handler.call(operation)
241 else:
242 handler.call(operation)
243
244
245func _vector(value) -> Vector2:
246 if value is Vector2:
247 return value
248 if value is Vector2i:
249 return Vector2(value.x, value.y)
250 if value is Array and value.size() >= 2:
251 return Vector2(float(value[0]), float(value[1]))
252 if value is Dictionary and value.has("x") and value.has("y"):
253 return Vector2(float(value.get("x", 0.0)), float(value.get("y", 0.0)))
254 return Vector2.ZERO
Vector2
Return the first collector intersecting a field power-up.