11const GLOBAL_ACHIEVEMENT_PATH :=
"user://global_achievements.cfg"
12const DataCacheManager = preload(
"res://scripts/systems/dataCacheManager/data_cache_manager.gd")
17var derived_stats := {}
18var run_summaries := []
20var achievement_unlock_times := {}
22var data_cache_manager = DataCacheManager.new()
27func load_achievements(file_path:String, game_id:String =
"") -> void:
28 load_achievement_data(data_cache_manager.load_json(file_path), game_id)
32func load_achievement_data(data, game_id:String =
"") -> void:
33 if not data
is Dictionary:
35 for achievement
in data.get(
"achievements", []):
36 if not achievement
is Dictionary:
38 var id = str(achievement.get(
"id",
""))
41 var definition = achievement.duplicate(true)
42 definition.erase(
"id")
43 if definition.has(
"conditions")
and definition[
"conditions"]
is Dictionary:
44 for key
in definition[
"conditions"].keys():
45 if not definition.has(key):
46 definition[key] = definition[
"conditions"][key]
47 definition.erase(
"conditions")
48 if game_id !=
"" and not definition.has(
"game_id"):
49 definition[
"game_id"] = game_id
50 register_achievement(id, definition)
54func set_counter(id:String, value:int) -> void:
59func increment(id:String, amount:int = 1) -> int:
60 var value =
int(counters.get(id, 0)) + amount
71func record_events(events:Array, evaluate:bool = true) -> Array:
73 if not event
is Dictionary:
75 var id := str(event.get(
"id",
""))
78 match str(event.get(
"type",
"counter")):
80 increment(id,
int(event.get(
"amount", 1)))
82 set_counter(id,
int(event.get(
"value", 0)))
84 record_high(id,
int(event.get(
"value", 0)))
86 record_low(id,
int(event.get(
"value", 0)))
88 set_flag(id,
bool(event.get(
"value", true)))
89 return evaluate_achievements()
if evaluate
else []
93func get_counter(id:String, default_value:int = 0) -> int:
94 return int(counters.get(id, default_value))
98func record_high(id:String, value:int) -> bool:
99 if value <=
int(records.get(id, value - 1)):
106func record_low(id:String, value:int) -> bool:
107 if records.has(id)
and value >=
int(records[id]):
114func get_record(id:String, default_value:int = 0) -> int:
115 return int(records.get(id, default_value))
119func set_flag(id:String, value:bool = true) -> void:
121 if id.begins_with(
"achievement_"):
122 var achievement_id = id.trim_prefix(
"achievement_")
124 if not achievement_unlock_times.has(achievement_id):
125 achievement_unlock_times[achievement_id] = Time.get_unix_time_from_system()
127 achievement_unlock_times.erase(achievement_id)
131func get_flag(id:String, default_value:bool = false) -> bool:
132 return bool(flags.get(id, default_value))
137func register_derived_stat(id:String, definition:Dictionary) -> void:
138 derived_stats[id] = definition.duplicate(true)
142func get_derived_stat(id:String, default_value = 0):
143 if not derived_stats.has(id):
145 var definition = derived_stats[id]
146 var op = str(definition.get(
"op",
"sum"))
147 var fields = definition.get(
"fields", [])
150 values.append(_get_numeric_stat(str(field)))
158 return (values[0]
if values.size() > 0
else 0) - (values[1]
if values.size() > 1
else 0)
160 var denominator = values[1]
if values.size() > 1
else 0
161 return default_value
if denominator == 0
else float(values[0]) / float(denominator)
163 var denominator = values[1]
if values.size() > 1
else 0
164 return default_value
if denominator == 0
else (float(values[0]) / float(denominator)) * 100.0
166 return values.max()
if not values.is_empty()
else default_value
168 return values.min()
if not values.is_empty()
else default_value
173func add_run_summary(summary:Dictionary, max_entries:int = 20) -> void:
174 var entry = summary.duplicate(true)
175 if not entry.has(
"ended_at"):
176 entry[
"ended_at"] = Time.get_unix_time_from_system()
177 run_summaries.append(entry)
178 while run_summaries.size() > max_entries:
179 run_summaries.pop_front()
183func get_run_summaries() -> Array:
184 return run_summaries.duplicate(true)
189func register_achievement(id:String, definition:Dictionary) -> void:
190 achievements[id] = definition.duplicate(true)
194func get_achievements() -> Dictionary:
195 return achievements.duplicate(true)
202func get_achievement_collection(filters:Dictionary = {}) -> Array:
204 for id
in achievements.keys():
205 var achievement = achievements[id].duplicate(true)
206 achievement[
"id"] = id
207 achievement[
"unlocked"] = get_flag(
"achievement_%s" % id)
208 if achievement_unlock_times.has(id):
209 achievement[
"unlocked_at"] = achievement_unlock_times[id]
210 if not _achievement_matches_filters(achievement, filters):
212 collection.append(achievement)
217func load_global_achievement_state(path:String = GLOBAL_ACHIEVEMENT_PATH) -> void:
218 var file = ConfigFile.new()
219 if file.load(path) != OK:
221 var global_flags = file.get_value(
"achievements",
"flags", {})
222 var global_times = file.get_value(
"achievements",
"unlock_times", {})
223 merge_achievement_state({
"flags": global_flags,
"achievement_unlock_times": global_times})
227func save_global_achievement_state(path:String = GLOBAL_ACHIEVEMENT_PATH) -> void:
228 var file = ConfigFile.new()
230 var global_flags = file.get_value(
"achievements",
"flags", {})
231 var global_times = file.get_value(
"achievements",
"unlock_times", {})
232 if not global_flags
is Dictionary:
234 if not global_times
is Dictionary:
236 for flag_id
in flags.keys():
237 if not str(flag_id).begins_with(
"achievement_")
or not bool(flags[flag_id]):
239 global_flags[str(flag_id)] = true
240 var achievement_id = str(flag_id).trim_prefix(
"achievement_")
241 if achievement_unlock_times.has(achievement_id):
242 global_times[achievement_id] = achievement_unlock_times[achievement_id]
243 elif not global_times.has(achievement_id):
244 global_times[achievement_id] = Time.get_unix_time_from_system()
245 file.set_value(
"achievements",
"flags", global_flags)
246 file.set_value(
"achievements",
"unlock_times", global_times)
251func merge_achievement_state(state:Dictionary) -> void:
252 var source_flags = state.get(
"flags", {})
253 if source_flags
is Dictionary:
254 for flag_id
in source_flags.keys():
255 if str(flag_id).begins_with(
"achievement_")
and bool(source_flags[flag_id]):
256 flags[str(flag_id)] = true
257 var source_times = state.get(
"achievement_unlock_times", {})
258 if source_times
is Dictionary:
259 for achievement_id
in source_times.keys():
260 if int(source_times[achievement_id]) > 0:
261 achievement_unlock_times[str(achievement_id)] = source_times[achievement_id]
265func get_achievement_filter_options() -> Dictionary:
266 var game_ids := {
"all": true}
267 var categories := {
"all": true}
268 for achievement
in achievements.values():
269 if not achievement
is Dictionary:
271 game_ids[str(achievement.get(
"game_id",
"all"))] = true
272 categories[str(achievement.get(
"category",
"general"))] = true
274 "game_ids": game_ids.keys(),
275 "categories": categories.keys(),
276 "states": [
"all",
"locked",
"unlocked"]
281func get_achievement_summary() -> Dictionary:
283 var by_category := {}
284 for achievement
in achievements.values():
285 if not achievement
is Dictionary:
287 var game_id := str(achievement.get(
"game_id",
"all"))
288 var category := str(achievement.get(
"category",
"general"))
289 by_game[game_id] =
int(by_game.get(game_id, 0)) + 1
290 by_category[category] =
int(by_category.get(category, 0)) + 1
292 "total": achievements.size(),
294 "by_category": by_category
299func get_cache_stats() -> Dictionary:
300 return data_cache_manager.get_cache_stats()
304func evaluate_achievements() -> Array:
306 for id
in achievements.keys():
307 var flag_id =
"achievement_%s" % id
308 if get_flag(flag_id):
310 if _achievement_condition_met(achievements[id]):
311 set_flag(flag_id, true)
313 if not unlocked.is_empty():
314 save_global_achievement_state()
319func get_state() -> Dictionary:
321 "counters": counters.duplicate(true),
322 "records": records.duplicate(true),
323 "flags": flags.duplicate(true),
324 "derived_stats": derived_stats.duplicate(true),
325 "run_summaries": run_summaries.duplicate(true),
326 "achievements": achievements.duplicate(true),
327 "achievement_unlock_times": achievement_unlock_times.duplicate(true)
332func apply_state(state:Dictionary) -> void:
333 counters = state.get(
"counters", {}).duplicate(true)
334 records = state.get(
"records", {}).duplicate(true)
335 flags = state.get(
"flags", {}).duplicate(true)
336 derived_stats = state.get(
"derived_stats", {}).duplicate(true)
337 run_summaries = state.get(
"run_summaries", []).duplicate(true)
338 achievement_unlock_times = state.get(
"achievement_unlock_times", {}).duplicate(true)
339 var registered_achievements = achievements.duplicate(true)
340 var saved_achievements = state.get(
"achievements", {})
341 if registered_achievements.is_empty()
and saved_achievements
is Dictionary:
342 achievements = saved_achievements.duplicate(true)
343 elif saved_achievements
is Dictionary:
344 for id
in saved_achievements.keys():
345 if not achievements.has(id):
346 achievements[id] = saved_achievements[id]
354 derived_stats.clear()
355 run_summaries.clear()
357 achievement_unlock_times.clear()
360func _get_numeric_stat(id:String) -> int:
362 return int(counters[id])
364 return int(records[id])
368func _achievement_condition_met(definition:Dictionary) -> bool:
369 for id
in definition.get(
"counter_at_least", {}).keys():
370 if get_counter(str(id)) <
int(definition[
"counter_at_least"][id]):
372 for id
in definition.get(
"record_at_least", {}).keys():
373 if get_record(str(id)) <
int(definition[
"record_at_least"][id]):
375 for id
in definition.get(
"record_at_most", {}).keys():
376 if get_record(str(id), 999999999) >
int(definition[
"record_at_most"][id]):
378 for id
in definition.get(
"flag", []):
379 if not get_flag(str(id)):
384func _achievement_matches_filters(achievement:Dictionary, filters:Dictionary) -> bool:
385 if filters.has(
"game_id")
and not _filter_value_matches(achievement.get(
"game_id",
""), filters[
"game_id"]):
387 if filters.has(
"category")
and not _filter_value_matches(achievement.get(
"category",
""), filters[
"category"]):
389 if filters.has(
"locked")
and bool(filters[
"locked"]) ==
bool(achievement.get(
"unlocked", false)):
391 if filters.has(
"unlocked")
and bool(filters[
"unlocked"]) !=
bool(achievement.get(
"unlocked", false)):
393 if filters.has(
"state"):
394 var state = str(filters[
"state"])
397 if state ==
"locked" and bool(achievement.get(
"unlocked", false)):
399 if state ==
"unlocked" and not bool(achievement.get(
"unlocked", false)):
404func _filter_value_matches(value, filter_value) -> bool:
405 if filter_value
is Array:
406 for accepted
in filter_value:
407 if str(value) == str(accepted):
410 return str(value) == str(filter_value)
int
Increment a numeric counter and return the new value.
bool
Apply a batch of stat events and return newly unlocked achievements.