From eaa8f18a4a547adf51caaab918a416fbac8bca31 Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Tue, 31 Mar 2026 01:45:58 +0200 Subject: [PATCH] fix buff unlock --- core/game_state.gd | 210 +++++++++++++++++++++++++- core/generator/currency_generator.gd | 14 +- core/generator/generator_buff_data.gd | 8 +- core/goals/goal_data.gd | 16 ++ core/goals/goal_requirement_data.gd | 25 +++ goals_debug_ui.gd | 50 +++--- 6 files changed, 292 insertions(+), 31 deletions(-) diff --git a/core/game_state.gd b/core/game_state.gd index 21b918a..8a4501f 100644 --- a/core/game_state.gd +++ b/core/game_state.gd @@ -10,6 +10,8 @@ signal generator_buff_level_changed(generator_id: StringName, buff_id: StringNam signal generator_buff_unlocked_changed(generator_id: StringName, buff_id: StringName, unlocked: bool) signal buff_level_changed(buff_id: StringName, new_level: int) signal buff_unlocked_changed(buff_id: StringName, unlocked: bool) +signal goal_completed(goal_id: StringName) +signal goal_progress_changed(goal_id: StringName, progress: float) # ========================================== # STATE VARIABLES @@ -28,6 +30,9 @@ var _buff_levels: Dictionary = {} var _buff_unlocked: Dictionary = {} var _buff_active: Dictionary = {} +var _goal_definitions: Dictionary = {} +var _goal_completed: Dictionary = {} + var last_save_time: int = 0 # Unix timestamp # ========================================== @@ -35,7 +40,7 @@ var last_save_time: int = 0 # Unix timestamp # ========================================== static var file_name: String = "user://idle_save.json" const SAVE_FORMAT_VERSION_KEY: String = "save_format_version" -const CURRENT_SAVE_FORMAT_VERSION: int = 2 +const CURRENT_SAVE_FORMAT_VERSION: int = 3 const GENERATOR_OWNED_KEY: String = "owned" const GENERATOR_PURCHASED_COUNT_KEY: String = "purchased_count" const GENERATOR_UNLOCKED_KEY: String = "unlocked" @@ -47,6 +52,8 @@ const BUFF_DEFINITIONS_KEY: String = "buff_definitions" const BUFF_LEVELS_KEY: String = "buff_levels" const BUFF_UNLOCKED_KEY: String = "buff_unlocked" const BUFF_ACTIVE_KEY: String = "buff_active" +const GOALS_KEY: String = "goals" +const GOAL_COMPLETED_KEY: String = "completed" const CURRENCIES_KEY: String = "currencies" const CURRENT_KEY: String = "current" const TOTAL_KEY: String = "total" @@ -55,6 +62,7 @@ const LAST_SAVE_TIME_KEY: String = "last_save_time" func _ready() -> void: _initialize_currency_maps() + _initialize_goals() load_game() # ========================================== @@ -404,6 +412,12 @@ func get_effective_multiplier(generator_id: StringName, kind: int) -> float: return multiplier +func _try_unlock_all_buffs_from_goals() -> void: + for buff in get_all_buffs(): + if buff == null: + continue + _try_unlock_buff_from_goal(buff) + func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void: if buff == null: return @@ -411,7 +425,8 @@ func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void: return if is_buff_unlocked(buff.id): return - if buff.is_unlock_goal_met(): + var goal_id: StringName = buff.unlock_goal.id + if not goal_id.is_empty() and is_goal_completed(goal_id): set_buff_unlocked(buff.id, true) func set_external_save_data(section_key: StringName, payload: Dictionary) -> void: @@ -442,6 +457,7 @@ func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signal generator_states.clear() generator_buff_levels.clear() generator_buff_unlocked.clear() + _goal_completed.clear() if not emit_currency_signals: return @@ -464,6 +480,7 @@ func save_game() -> void: BUFF_LEVELS_KEY: _serialize_buff_levels(), BUFF_UNLOCKED_KEY: _serialize_buff_unlocked(), BUFF_ACTIVE_KEY: _serialize_buff_active(), + GOALS_KEY: _serialize_goals(), LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system() } @@ -497,18 +514,22 @@ func load_game() -> void: push_error("Save file version %d too old. Please start fresh." % version) return + if version < 3: + _migrate_goals_from_version_2(parsed_data) + _external_save_sections.clear() if parsed_data.has(CURRENCIES_KEY): _load_currency_balances(parsed_data.get(CURRENCIES_KEY, {})) generator_states = _deserialize_generator_states(parsed_data.get(GENERATOR_STATES_KEY, {})) _deserialize_buff_states(parsed_data) + _deserialize_goals(parsed_data.get(GOALS_KEY, {})) last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system()) for save_key_variant in parsed_data.keys(): var save_key: String = String(save_key_variant) if save_key.is_empty(): continue - if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, LAST_SAVE_TIME_KEY]: + if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, GOALS_KEY, LAST_SAVE_TIME_KEY]: continue var section_payload: Variant = parsed_data.get(save_key_variant) @@ -977,3 +998,186 @@ func _deserialize_buff_states(parsed_data: Dictionary) -> void: var key: String = String(key_variant) if not key.is_empty(): _buff_active[key] = true + +# ========================================== +# GOALS +# ========================================== +func _initialize_goals() -> void: + _goal_definitions.clear() + _goal_completed.clear() + + var goal_paths: PackedStringArray = _discover_goal_paths() + for goal_path in goal_paths: + var goal_resource: Resource = load(goal_path) + if goal_resource == null: + push_warning("Failed to load goal resource at '%s'." % goal_path) + continue + if not (goal_resource is GoalData): + continue + + var goal: GoalData = goal_resource + if not goal.has_id(): + push_warning("Skipping goal with missing id: %s" % goal_path) + continue + if _goal_definitions.has(goal.id): + push_warning("Duplicate goal id '%s' found at %s. Keeping first occurrence." % [String(goal.id), goal_path]) + continue + + _goal_definitions[goal.id] = goal + _goal_completed[goal.id] = false + +func _discover_goal_paths() -> PackedStringArray: + var result: PackedStringArray = [] + const GOAL_DIRECTORY_PATH: String = "res://sandbox/goals" + const GOAL_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"] + + var directory: DirAccess = DirAccess.open(GOAL_DIRECTORY_PATH) + if directory == null: + push_warning("Unable to open goals directory: %s" % GOAL_DIRECTORY_PATH) + return result + + directory.list_dir_begin() + var entry_name: String = directory.get_next() + while not entry_name.is_empty(): + if directory.current_is_dir() or entry_name.begins_with("."): + entry_name = directory.get_next() + continue + + var extension: String = entry_name.get_extension().to_lower() + if GOAL_RESOURCE_EXTENSIONS.has(extension): + result.append("%s/%s" % [GOAL_DIRECTORY_PATH, entry_name]) + + entry_name = directory.get_next() + directory.list_dir_end() + + result.sort() + return result + +func register_goal(goal: GoalData) -> void: + if goal == null: + return + var goal_id: StringName = goal.id + if goal_id == &"": + push_warning("Attempted to register goal with invalid id") + return + if _goal_definitions.has(goal_id): + return + _goal_definitions[goal_id] = goal + _goal_completed[goal_id] = false + +func get_goal(goal_id: StringName) -> GoalData: + return _goal_definitions.get(goal_id, null) + +func get_all_goals() -> Array[GoalData]: + var result: Array[GoalData] = [] + for goal in _goal_definitions.values(): + if goal is GoalData: + result.append(goal) + return result + +func is_goal_completed(goal_id: StringName) -> bool: + return _goal_completed.get(goal_id, false) + +func get_goal_progress(goal_id: StringName) -> float: + var goal: GoalData = get_goal(goal_id) + if goal == null: + return 0.0 + return goal.get_progress() + +func evaluate_all_goals() -> void: + for goal_id in _goal_completed.keys(): + if not _goal_completed[goal_id]: + _try_complete_goal(goal_id) + + _try_unlock_all_buffs_from_goals() + +func _try_complete_goal(goal_id: StringName) -> void: + var goal: GoalData = _goal_definitions.get(goal_id, null) + if goal == null: + return + if _goal_completed[goal_id]: + return + if goal.is_met(): + _goal_completed[goal_id] = true + goal_completed.emit(goal_id) + +func _serialize_goals() -> Dictionary: + var result: Dictionary = {} + var completed_goals: Array[StringName] = [] + for goal_id in _goal_completed.keys(): + if _goal_completed[goal_id]: + completed_goals.append(goal_id) + result[GOAL_COMPLETED_KEY] = completed_goals + return result + +func _deserialize_goals(raw_value: Variant) -> void: + _goal_completed.clear() + + if not (raw_value is Dictionary): + return + + var raw_completed: Variant = raw_value.get(GOAL_COMPLETED_KEY, []) + if raw_completed is Array: + for goal_id_variant in raw_completed: + var goal_id: String = String(goal_id_variant) + if not goal_id.is_empty(): + _goal_completed[goal_id] = true + + for goal_id in _goal_definitions.keys(): + if not _goal_completed.has(goal_id): + _goal_completed[goal_id] = false + +func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void: + _goal_completed.clear() + + var generator_states_data: Dictionary = parsed_data.get(GENERATOR_STATES_KEY, {}) + for gen_id_variant in generator_states_data.keys(): + var gen_id: String = String(gen_id_variant) + if gen_id.is_empty(): + continue + + var state: Dictionary = generator_states_data.get(gen_id_variant, {}) + if not state.get(GENERATOR_UNLOCKED_KEY, false): + continue + + var generator_data: CurrencyGeneratorData = _load_generator_data_from_id(gen_id) + if generator_data == null or not generator_data.has_unlock_goal(): + continue + + var goal_id: StringName = generator_data.get_unlock_goal_id() + if not goal_id.is_empty(): + _goal_completed[goal_id] = true + + for goal_id in _goal_definitions.keys(): + if not _goal_completed.has(goal_id): + _goal_completed[goal_id] = false + +func _load_generator_data_from_id(generator_id: String) -> CurrencyGeneratorData: + const GENERATORS_DIRECTORY_PATH: String = "res://sandbox/generators" + const GENERATOR_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"] + + var directory: DirAccess = DirAccess.open(GENERATORS_DIRECTORY_PATH) + if directory == null: + return null + + directory.list_dir_begin() + var entry_name: String = directory.get_next() + while not entry_name.is_empty(): + if directory.current_is_dir() or entry_name.begins_with("."): + entry_name = directory.get_next() + continue + + var extension: String = entry_name.get_extension().to_lower() + if GENERATOR_RESOURCE_EXTENSIONS.has(extension): + var generator_path: String = "%s/%s" % [GENERATORS_DIRECTORY_PATH, entry_name] + var generator_resource: Resource = load(generator_path) + if generator_resource is CurrencyGeneratorData: + var gen_data: CurrencyGeneratorData = generator_resource + if String(gen_data.id) == generator_id: + directory.list_dir_end() + return gen_data + + entry_name = directory.get_next() + directory.list_dir_end() + + return null diff --git a/core/generator/currency_generator.gd b/core/generator/currency_generator.gd index 27ffe6d..6f8ac60 100644 --- a/core/generator/currency_generator.gd +++ b/core/generator/currency_generator.gd @@ -320,7 +320,7 @@ func reset_runtime_state_for_prestige() -> void: _remaining_click_cooldown_seconds = 0.0 _ensure_registered() _evaluate_generator_unlock_goal(false) - _evaluate_buff_unlock_goals() + #_evaluate_buff_unlock_goals() visible = is_available_to_player() func get_manual_click_multiplier() -> float: @@ -574,16 +574,12 @@ func _register_buffs() -> void: _try_unlock_buff_from_goal(buff) -func _evaluate_buff_unlock_goals() -> void: - for buff in get_buffs(): - GameState._try_unlock_buff_from_goal(buff) - func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void: GameState._try_unlock_buff_from_goal(buff) func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void: + GameState.evaluate_all_goals() _evaluate_generator_unlock_goal(false) - _evaluate_buff_unlock_goals() func try_unlock_from_goal() -> bool: return _evaluate_generator_unlock_goal(true) @@ -597,12 +593,14 @@ func _evaluate_generator_unlock_goal(allow_manual_unlock: bool) -> bool: return false if not data.unlocks_automatically_from_goal() and not allow_manual_unlock: return false - if not data.is_unlock_goal_met(): + + var goal_id: StringName = data.get_unlock_goal_id() + if goal_id.is_empty() or not GameState.is_goal_completed(goal_id): return false GameState.set_generator_unlocked(_generator_id, true) GameState.set_generator_available(_generator_id, true) - goal_achieved.emit(_generator_id, data.get_unlock_goal_id()) + goal_achieved.emit(_generator_id, goal_id) return true ## Default unlocked state loaded from generator data. diff --git a/core/generator/generator_buff_data.gd b/core/generator/generator_buff_data.gd index 535ea83..4c00adf 100644 --- a/core/generator/generator_buff_data.gd +++ b/core/generator/generator_buff_data.gd @@ -87,8 +87,12 @@ func get_unlock_goal_currency() -> Currency: func is_unlock_goal_met() -> bool: if unlock_goal == null: return false - - return bool(unlock_goal.is_met()) + + var goal_id: StringName = unlock_goal.id + if goal_id.is_empty(): + return false + + return GameState.is_goal_completed(goal_id) func can_purchase_next_level(current_level: int) -> bool: if max_level < 0: diff --git a/core/goals/goal_data.gd b/core/goals/goal_data.gd index 2ffa672..31ae374 100644 --- a/core/goals/goal_data.gd +++ b/core/goals/goal_data.gd @@ -41,3 +41,19 @@ func get_first_requirement_summary() -> String: if valid_requirements.is_empty(): return "" return String(valid_requirements[0].get_summary_text()) + +func get_progress() -> float: + var valid_requirements: Array[GoalRequirementData] = get_valid_requirements() + if valid_requirements.is_empty(): + return 0.0 + + var min_progress: float = 1.0 + for requirement in valid_requirements: + if requirement == null or not requirement.is_valid(): + continue + + var requirement_progress: float = requirement.get_progress() + if requirement_progress < min_progress: + min_progress = requirement_progress + + return min_progress diff --git a/core/goals/goal_requirement_data.gd b/core/goals/goal_requirement_data.gd index eca58ec..b627a54 100644 --- a/core/goals/goal_requirement_data.gd +++ b/core/goals/goal_requirement_data.gd @@ -40,3 +40,28 @@ func get_summary_text() -> String: var currency_name: String = GameState.get_currency_name(get_currency_id()) return "%s %s" % [get_amount().to_string_suffix(2), currency_name] + +func get_progress() -> float: + if not is_valid(): + return 0.0 + + var current_amount: BigNumber = GameState.get_total_currency_acquired_by_id(get_currency_id()) + var required_amount: BigNumber = get_amount() + + if required_amount.mantissa <= 0.0: + return 1.0 + + if current_amount.is_greater_than(required_amount) or current_amount.is_equal_to(required_amount): + return 1.0 + + if current_amount.mantissa <= 0.0: + return 0.0 + + var current_log: float = log(maxf(current_amount.mantissa, 0.0001)) / log(10) + float(current_amount.exponent) + var required_log: float = log(maxf(required_amount.mantissa, 0.0001)) / log(10) + float(required_amount.exponent) + + if required_log <= 0.0: + return 1.0 + + var progress: float = current_log / required_log + return clampf(progress, 0.0, 1.0) diff --git a/goals_debug_ui.gd b/goals_debug_ui.gd index 6cfa94e..c628481 100644 --- a/goals_debug_ui.gd +++ b/goals_debug_ui.gd @@ -38,12 +38,16 @@ func _ready() -> void: _build_goal_rows() GameState.currency_changed.connect(_on_currency_changed) GameState.generator_state_changed.connect(_on_generator_state_changed) + GameState.goal_completed.connect(_on_goal_completed) _refresh_ui() _refresh_ui.call_deferred() func _on_currency_changed(_currency_id: StringName, _new_amount: BigNumber) -> void: _refresh_ui() +func _on_goal_completed(goal_id: StringName) -> void: + _refresh_ui() + func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) -> void: var generator_key: String = String(generator_id) if not generator_key.is_empty(): @@ -58,7 +62,7 @@ func _load_goals() -> void: var scene_root: Node = get_tree().current_scene var search_root: Node = scene_root if scene_root != null else self var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false) - var seen_goal_ids: Dictionary = {} + for generator_node in generator_nodes: var generator: CurrencyGenerator = generator_node as CurrencyGenerator if generator == null: @@ -69,23 +73,21 @@ func _load_goals() -> void: continue _generators_by_id[String(generator_id)] = generator - if generator.data == null: + var seen_goal_ids: Dictionary = {} + for goal in GameState.get_all_goals(): + if goal == null or not goal.has_id(): continue - if not generator.data.has_unlock_goal(): - continue - - var resolved_goal: GoalData = generator.data.unlock_goal - if resolved_goal == null: - continue - if not bool(resolved_goal.is_valid()): - continue - var goal_key: String = String(resolved_goal.get("id")) + + var goal_key: String = String(goal.id) if seen_goal_ids.has(goal_key): - push_warning("Goals debug UI: duplicate goal id '%s' skipped" % goal_key) continue - + + var target_generator_id: StringName = _find_generator_for_goal(goal) + if target_generator_id.is_empty(): + continue + seen_goal_ids[goal_key] = true - _loaded_goals.append(GoalDefinition.new(generator_id, resolved_goal)) + _loaded_goals.append(GoalDefinition.new(target_generator_id, goal)) func _build_goal_rows() -> void: _rows_by_goal_id.clear() @@ -188,10 +190,9 @@ func _is_goal_met(goal: GoalDefinition) -> bool: return bool(goal.goal.is_met()) func _is_goal_completed(goal: GoalDefinition) -> bool: - return ( - GameState.is_generator_unlocked(goal.target_generator_id) - and GameState.is_generator_available(goal.target_generator_id) - ) + if goal.goal == null or goal.goal.id.is_empty(): + return false + return GameState.is_goal_completed(goal.goal.id) func _on_unlock_pressed(goal_id: StringName) -> void: var row: GoalRow = _rows_by_goal_id.get(String(goal_id)) @@ -276,6 +277,16 @@ func _get_generator_for_goal(goal: GoalDefinition) -> CurrencyGenerator: return _generators_by_id.get(key) as CurrencyGenerator +func _find_generator_for_goal(goal: GoalData) -> StringName: + for generator_id in _generators_by_id.keys(): + var generator: CurrencyGenerator = _generators_by_id[generator_id] + if generator == null or generator.data == null: + continue + if generator.data.has_unlock_goal(): + if String(generator.data.get_unlock_goal_id()) == String(goal.id): + return StringName(generator_id) + return &"" + func _is_manual_goal_generator(generator: CurrencyGenerator) -> bool: if generator == null: return false @@ -311,6 +322,9 @@ func _get_requirements_text(goal: GoalDefinition) -> String: required_amount.to_string_suffix(2) ] ) + + if parts.is_empty(): + return "No requirements" return " | ".join(parts) func _currency_label(currency_id: StringName) -> String: