extends Base ## Achievement handler to check and unlock achievements ## ## An achievement handler that will check the available achievements defined in ## both the game files and the mods. When called with triggers will check to see ## if that trigger value unlocks achievements. signal achievement_unlocked(achievement: String) const CACHE_TIME = 60000 const POPUP_POP_TIME = 1000 const POPUP_LINGER_TIME = 5000 const POPUP_HIDE_TIME = 1000 const POPUP_BREAK = 1000 const POPUP_HOVER_TIME = 250 @onready var popup = $"CanvasLayer/Popup" @onready var popup_title = $"CanvasLayer/Popup/Popup/Title" @onready var popup_body = $"CanvasLayer/Popup/Popup/Body" @onready var audio = $"AudioStreamPlayer" @onready var audio2 = $"AudioStreamPlayer2" enum AchievementState { ENTERING, SHOWN, HOVERED, EXITING, HIDDEN } var queue = [] var cached_percents = {} var last_percent_pull: int var showing_popup = false var starting_popup_pos var starting_popup_size var popup_tween var popup_timer = 0 var popup_time = 6 #var entering = false #var exiting = false #var hovered = false var achievement_state = AchievementState.HIDDEN var hovered = false func _ready(): #_log_category = "ACH" #_log_icon = "res://components/Logger/scroll-text.svg" #_log_color = "#a166a1" super() _info("Achievements is active") starting_popup_pos = popup.position starting_popup_size = popup.size popup.position.x = starting_popup_pos.x + starting_popup_size.x popup.modulate = Color.TRANSPARENT popup.scale = Vector2(0.5, 0.5) if _triggerer: _triggerer.listen("any", _on_trigger) func _on_trigger(data: Dictionary) -> void: _check_trigger(data.trigger, "Trigger") func _process(delta): popup_timer += delta if popup_timer > popup_time and achievement_state == AchievementState.SHOWN: _exit_popup() if not queue.is_empty() and not showing_popup: _show_popup(queue.pop_front()) if popup_tween and not popup_tween.is_running(): if hovered and achievement_state == AchievementState.SHOWN: achievement_state = AchievementState.HOVERED popup_tween = create_tween() popup_tween.set_parallel() popup_tween.set_trans(Tween.TRANS_QUAD) popup_tween.tween_property(popup, "scale", Vector2(1.05, 1.05), float(POPUP_HOVER_TIME) / 1000) popup_tween.tween_property(popup, "position:x", starting_popup_pos.x - 20, float(POPUP_HOVER_TIME) / 1000) if not hovered and achievement_state == AchievementState.HOVERED: achievement_state = AchievementState.SHOWN popup_tween = create_tween() popup_tween.set_parallel() popup_tween.set_trans(Tween.TRANS_QUAD) popup_tween.tween_property(popup, "scale", Vector2(1, 1), float(POPUP_HOVER_TIME) / 1000) popup_tween.tween_property(popup, "position:x", starting_popup_pos.x, float(POPUP_HOVER_TIME) / 1000) func _exit_popup(): achievement_state = AchievementState.EXITING audio2.play() if popup_tween: popup_tween.kill() popup_tween = create_tween() popup_tween.set_ease(Tween.EASE_IN) popup_tween.set_trans(Tween.TRANS_BACK) popup_tween.set_parallel() popup_tween.tween_property(popup, "modulate", Color.TRANSPARENT, POPUP_POP_TIME / 1000) popup_tween.tween_property(popup, "scale", Vector2(0.5, 0.5), POPUP_POP_TIME / 1000) popup_tween.tween_property(popup, "position:x", starting_popup_pos.x + starting_popup_size.x, POPUP_HIDE_TIME / 1000) popup_tween.tween_interval(POPUP_BREAK / 1000) popup_tween.chain() popup_tween.tween_callback(func(): showing_popup = false) _info("Hiding achievement popup") func _on_data_persisted(key: String, value, category: PersisterEnums.Scope): _check_trigger(key, "Data", value) ## Get all of the achievement info func get_achievements() -> Dictionary: if not _data: if (_logger): _logger.warn("Could not find data autoload when getting achievements") return {} if not _data.data.has("achievements"): if (_logger): _logger.warn("Could not find achievement data") return {} var local_ach_data = _data.data.achievements.duplicate() var percents = _get_achievement_percents() for achievement in _data.data.achievements: if percents.keys().has(achievement): local_ach_data.percent = percents[achievement] return _data.data.achievements ## Get achievement info given the achievement slug func get_achievement(achievement: String) -> Dictionary: if not _data: if _logger: _logger.warn("Could not find data autoload when getting achievement %s" % [achievement]) return {} if not _data.data.has("achievements"): if _logger: _logger.warn("Could not find achievement data") return {} if not _data.data.achievements.has(achievement): if _logger: _logger.warn("Could not find achievement %s" % [achievement]) return {} var achievement_data = _data.data.achievements[achievement].duplicate() # TODO: Add in achievement percent return achievement_data func _show_popup(achievement: String) -> void: var achievement_data = get_achievement(achievement) achievement_state = AchievementState.ENTERING popup_timer = 0 if achievement_data.is_empty(): if _logger: _logger.error("Could not get achievement %s info when trying to show" % [achievement]) return popup_title.text = achievement_data.name popup_body.text = achievement_data.description showing_popup = true popup.position.x = starting_popup_pos.x + starting_popup_size.x popup.modulate = Color.TRANSPARENT popup.scale = Vector2(0.5, 0.5) #popup.position.y = starting_popup_pos.y + starting_popup_size.y if popup_tween: popup_tween.kill() audio.play() popup_tween = create_tween() popup_tween.set_parallel() popup_tween.set_ease(Tween.EASE_OUT) popup_tween.set_trans(Tween.TRANS_BACK) popup_tween.tween_property(popup, "modulate", Color.WHITE, POPUP_POP_TIME / 1000) popup_tween.tween_property(popup, "scale", Vector2(1, 1), POPUP_POP_TIME / 1000) popup_tween.tween_property(popup, "position:x", starting_popup_pos.x, POPUP_POP_TIME / 1000) popup_tween.chain() popup_tween.tween_callback(func(): achievement_state = AchievementState.SHOWN ) _info("Showing achievement %s popup" % [achievement]) func _get_achievement_percents() -> Dictionary: if not last_percent_pull: return _get_percents_from_db() if last_percent_pull + CACHE_TIME <= Time.get_ticks_msec(): return cached_percents return _get_percents_from_db() func _get_percents_from_db() -> Dictionary: last_percent_pull = Time.get_ticks_msec() return {} func _send_to_db(achievement: String) -> void: pass func _check_trigger(key: String, type: String, value = null): if not _data: if (_logger): _logger.warn("Could not find data autoload when checking trigger") return if not _data.data.has("achievements"): return for achievement in _data.data.achievements: # If the persister does not have the achievement already if not _persister.get_value(achievement): var ach_data = _data.data.achievements[achievement] var valid = false match type: "Data": if ach_data.has("key") and ach_data.key == key: valid = ach_data.value <= value "Trigger": if ach_data.has("trigger") and ach_data.trigger == key: valid = true if valid: achievement_unlocked.emit(achievement) _persister.persist_data(achievement, true, PersisterEnums.Scope.PERMANENT) _info("Added achievement %s to queue" % [achievement]) queue.push_back(achievement) _send_to_db(achievement) func _on_mouse_handler_hovered(): hovered = true func _on_mouse_handler_clicked(): if achievement_state != AchievementState.EXITING: _exit_popup() func _on_mouse_handler_unhovered(): hovered = false