fix buff unlock

This commit is contained in:
2026-03-31 01:45:58 +02:00
parent f1e08c848e
commit eaa8f18a4a
6 changed files with 292 additions and 31 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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: