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()
26var powerup_ids_cache:Array = []
27var labels_by_id:Dictionary = {}
28var colors_by_id:Dictionary = {}
29var effects_by_id:Dictionary = {}
30var cache_hits:int = 0
31var cache_misses:int = 0
32
33
34
35func load_definitions(file_path:String, options:Dictionary = {}) -> Array:
36 var parsed = data_cache_manager.load_json(file_path)
37 if not parsed is Dictionary:
38 configure([], options)
39 return []
40 configure(parsed.get("powerups", []), options)
41 return get_powerup_ids()
42
43
44
45func get_cache_stats() -> Dictionary:
46 var stats:Dictionary = data_cache_manager.get_cache_stats()
47 stats["definitions"] = definitions.size()
48 stats["cached_ids"] = powerup_ids_cache.size()
49 stats["cached_labels"] = labels_by_id.size()
50 stats["cached_colors"] = colors_by_id.size()
51 stats["cached_effect_lists"] = effects_by_id.size()
52 stats["runtime_cache_hits"] = cache_hits
53 stats["runtime_cache_misses"] = cache_misses
54 return stats
55
56
57
58func configure(next_definitions, options:Dictionary = {}) -> void:
59 definitions.clear()
60 if next_definitions is Array:
61 for row in next_definitions:
62 if row is Dictionary:
63 var id:String = str(row.get("id", ""))
64 if id != "":
65 definitions[id] = row.duplicate(true)
66 else:
67 definitions[str(row)] = {"id": str(row)}
68 elif next_definitions is Dictionary:
69 for id in next_definitions.keys():
70 var value = next_definitions[id]
71 definitions[str(id)] = value.duplicate(true) if value is Dictionary else {"id": str(id)}
72 for key in options.keys():
73 spawn_config[key] = options[key]
74 _rebuild_definition_indexes()
75
76
77func get_powerup_ids() -> Array:
78 cache_hits += 1
79 return powerup_ids_cache.duplicate()
80
81
82func get_definition(powerup_id:String) -> Dictionary:
83 if definitions.has(powerup_id):
84 cache_hits += 1
85 return Dictionary(definitions.get(powerup_id, {})).duplicate(true)
86 cache_misses += 1
87 return definitions.get(powerup_id, {"id": powerup_id, "label": powerup_id.capitalize()}).duplicate(true)
88
89
90func get_label(powerup_id:String) -> String:
91 if labels_by_id.has(powerup_id):
92 cache_hits += 1
93 return str(labels_by_id.get(powerup_id, powerup_id.capitalize()))
94 cache_misses += 1
95 return powerup_id.capitalize()
96
97
98func get_color(powerup_id:String, fallback:Color = Color.WHITE) -> Color:
99 if colors_by_id.has(powerup_id):
100 cache_hits += 1
101 return colors_by_id.get(powerup_id, fallback)
102 cache_misses += 1
103 return fallback
104
105
106
108func apply_effects(powerup_id:String, context:Dictionary = {}, handlers:Dictionary = {}) -> Array:
109 var operations:Array = []
110 var effects:Array = effects_by_id.get(powerup_id, []) if effects_by_id.get(powerup_id, []) is Array else []
111 if effects.is_empty():
112 cache_misses += 1
113 else:
114 cache_hits += 1
115 for effect in effects:
116 if not effect is Dictionary:
117 continue
118 var operation := normalize_effect(powerup_id, effect, context)
119 if operation.is_empty():
120 continue
121 operations.append(operation)
122 _dispatch_effect(operation, handlers)
123 return operations
124
125
126
127func normalize_effect(powerup_id:String, effect:Dictionary, context:Dictionary = {}) -> Dictionary:
128 var operation:Dictionary = effect.duplicate(true)
129 var effect_type:String = str(operation.get("type", ""))
130 if effect_type == "":
131 return {}
132 operation["type"] = effect_type
133 operation["powerup_id"] = powerup_id
134 operation["label"] = str(operation.get("label", get_label(powerup_id)))
135 if context.has("recipient"):
136 operation["recipient"] = str(context.get("recipient", ""))
137 if operation.has("duration"):
138 operation["duration"] = float(operation.get("duration", 0.0))
139 match effect_type:
140 "clear_timer":
141 operation["type"] = "set_timer"
142 operation["duration"] = 0.0
143 "effect_label":
144 operation["duration"] = float(operation.get("duration", 5.0))
145 "slow_opponent":
146 var slow_recipient:String = str(context.get("recipient", "player"))
147 operation["timer"] = "opponent_slow" if slow_recipient == "player" else "player_slow"
148 operation["type"] = "set_timer"
149 operation["duration"] = float(operation.get("duration", 10.0))
150 "curve_boost":
151 var curve_recipient:String = str(context.get("recipient", "player"))
152 operation["timer"] = "player_curve_boost" if curve_recipient == "player" else "opponent_curve_boost"
153 operation["type"] = "set_timer"
154 operation["duration"] = float(operation.get("duration", 10.0))
155 return operation
156
157
158
159func update(delta:float, playfield:Rect2, collectors:Array, options:Dictionary = {}) -> Dictionary:
160 for key in options.keys():
161 spawn_config[key] = options[key]
162 var events:Dictionary = {"spawned": [], "collected": [], "expired": []}
163 spawn_timer -= delta
164 if active_powerups.size() < int(spawn_config.get("max_active", 2)) and spawn_timer <= 0.0:
165 var spawned:Dictionary = spawn(playfield)
166 if not spawned.is_empty():
167 events["spawned"].append(spawned)
168 spawn_timer = next_spawn_delay(active_powerups.size() < int(spawn_config.get("max_active", 2)))
169 var remaining:Array = []
170 for powerup in active_powerups:
171 var next_powerup:Dictionary = powerup.duplicate(true)
172 next_powerup["age"] = float(next_powerup.get("age", 0.0)) + delta
173 if float(next_powerup.get("age", 0.0)) >= float(spawn_config.get("lifetime", 10.0)):
174 events["expired"].append(next_powerup)
175 spawn_timer = min(spawn_timer, next_spawn_delay(true) * 0.7)
176 continue
177 var collector:Dictionary = find_collector(next_powerup, collectors)
178 if not collector.is_empty():
179 next_powerup["collector"] = str(collector.get("owner", ""))
180 next_powerup["collector_id"] = str(collector.get("id", ""))
181 events["collected"].append(next_powerup)
182 spawn_timer = min(spawn_timer, next_spawn_delay(true))
183 else:
184 remaining.append(next_powerup)
185 active_powerups = remaining
186 return events
187
188
189
190func spawn(playfield:Rect2, powerup_id:String = "") -> Dictionary:
191 var ids:Array = definitions.keys()
192 if ids.is_empty():
193 return {}
194 var id:String = powerup_id if powerup_id != "" else str(ids[randi() % ids.size()])
195 var x_margin:float = playfield.size.x * float(spawn_config.get("x_margin_ratio", 0.25))
196 var y_margin:float = float(spawn_config.get("y_margin", 28.0))
197 var row:Dictionary = {
198 "id": "%s_%d" % [id, Time.get_ticks_msec()],
199 "type": id,
200 "position": Vector2(
201 randf_range(playfield.position.x + x_margin, playfield.end.x - x_margin),
202 randf_range(playfield.position.y + y_margin, playfield.end.y - y_margin)
203 ),
204 "age": 0.0
205 }
206 active_powerups.append(row)
207 return row.duplicate(true)
208
209
210
211func find_collector(powerup:Dictionary, collectors:Array) -> Dictionary:
212 var powerup_position:Vector2 = _vector(powerup.get("position", Vector2.ZERO))
213 for collector in collectors:
214 if not collector is Dictionary:
215 continue
216 var radius:float = float(collector.get("radius", 8.0)) + float(spawn_config.get("collect_radius", 12.0))
217 if _vector(collector.get("position", Vector2.ZERO)).distance_to(powerup_position) <= radius:
218 return collector.duplicate(true)
219 return {}
220
221
222func next_spawn_delay(wants_followup:bool) -> float:
223 var min_delay:float = float(spawn_config.get("followup_min_delay" if wants_followup else "min_delay", 0.7))
224 var max_delay:float = float(spawn_config.get("followup_max_delay" if wants_followup else "max_delay", 1.5))
225 var pace:float = maxf(0.1, float(spawn_config.get("pace", 1.0)))
226 return maxf(0.25, randf_range(min_delay, max_delay) / pace)
227
228
229func clear() -> void:
230 active_powerups.clear()
231 spawn_timer = 0.0
232
233
234func get_active() -> Array:
235 return active_powerups.duplicate(true)
236
237
238func set_active(rows:Array) -> void:
239 active_powerups = rows.duplicate(true)
240
241
242func get_state() -> Dictionary:
243 return {
244 "active_powerups": active_powerups.duplicate(true),
245 "spawn_timer": spawn_timer,
246 "spawn_config": spawn_config.duplicate(true)
247 }
248
249
250func apply_state(state:Dictionary) -> void:
251 active_powerups = state.get("active_powerups", []).duplicate(true)
252 spawn_timer = float(state.get("spawn_timer", spawn_timer))
253 var saved_config = state.get("spawn_config", {})
254 if saved_config is Dictionary:
255 for key in saved_config.keys():
256 spawn_config[key] = saved_config[key]
257
258
259func _dispatch_effect(operation:Dictionary, handlers:Dictionary) -> void:
260 var effect_type:String = str(operation.get("type", ""))
261 var handler_key:String = effect_type
262 if not handlers.has(handler_key):
263 handler_key = "custom_effect"
264 var handler = handlers.get(handler_key, Callable())
265 if handler is Callable and handler.is_valid():
266 if handler_key == "custom_effect":
267 handler.call(operation)
268 else:
269 handler.call(operation)
270
271
272func _vector(value) -> Vector2:
273 if value is Vector2:
274 return value
275 if value is Vector2i:
276 return Vector2(value.x, value.y)
277 if value is Array and value.size() >= 2:
278 return Vector2(float(value[0]), float(value[1]))
279 if value is Dictionary and value.has("x") and value.has("y"):
280 return Vector2(float(value.get("x", 0.0)), float(value.get("y", 0.0)))
281 return Vector2.ZERO
282
283
284func _rebuild_definition_indexes() -> void:
285 powerup_ids_cache = definitions.keys()
286 powerup_ids_cache.sort()
287 labels_by_id.clear()
288 colors_by_id.clear()
289 effects_by_id.clear()
290 cache_hits = 0
291 cache_misses = 0
292 for id in powerup_ids_cache:
293 var powerup_id:String = str(id)
294 var definition:Dictionary = definitions.get(powerup_id, {}) if definitions.get(powerup_id, {}) is Dictionary else {}
295 labels_by_id[powerup_id] = str(definition.get("label", powerup_id.capitalize()))
296 if definition.has("color"):
297 colors_by_id[powerup_id] = Color(str(definition.get("color", "#ffffff")))
298 var effects:Array = []
299 for effect in definition.get("effects", []):
300 if effect is Dictionary:
301 effects.append(Dictionary(effect).duplicate(true))
302 effects_by_id[powerup_id] = effects
Vector2
Return the first collector intersecting a field power-up.
Dictionary
Load power-up definitions from JSON containing powerups: [].