extends Node # This script is added as an Autoload named 'GameState' # ========================================== # SIGNALS # ========================================== signal currency_changed(currency_id: StringName, new_amount: BigNumber) signal generator_state_changed(generator_id: StringName, state: Dictionary) signal generator_buff_level_changed(generator_id: StringName, buff_id: StringName, new_level: int) 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 # ========================================== var _current_currency: Dictionary = {} var _total_currency_acquired: Dictionary = {} var _all_time_currency_acquired: Dictionary = {} var generator_states: Dictionary = {} var generator_buff_levels: Dictionary = {} var generator_buff_unlocked: Dictionary = {} var _external_save_sections: Dictionary = {} var _buff_definitions: Dictionary = {} 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 # ========================================== # Save / Load # ========================================== static var file_name: String = "user://idle_save.json" const SAVE_FORMAT_VERSION_KEY: String = "save_format_version" 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" const GENERATOR_AVAILABLE_KEY: String = "available" const GENERATOR_STATES_KEY: String = "generator_states" const GENERATOR_BUFF_LEVELS_KEY: String = "generator_buff_levels" const GENERATOR_BUFF_UNLOCKED_KEY: String = "generator_buff_unlocked" 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" const ALL_TIME_KEY: String = "all_time" const LAST_SAVE_TIME_KEY: String = "last_save_time" func _ready() -> void: _initialize_currency_maps() _initialize_goals() load_game() # ========================================== # STATE MODIFIERS # ========================================== #func add_gold(amount: BigNumber) -> void: #add_currency_by_id(GOLD_CURRENCY_ID, amount) # #func spend_gold(cost: BigNumber) -> bool: #return spend_currency_by_id(GOLD_CURRENCY_ID, cost) # #func add_gems(amount: BigNumber) -> void: #add_currency_by_id(GEMS_CURRENCY_ID, amount) # #func spend_gems(cost: BigNumber) -> bool: #return spend_currency_by_id(GEMS_CURRENCY_ID, cost) func get_known_currencies() -> Array[Currency]: return CurrencyDatabase.get_known_currencies() func get_currency_id(currency: Resource) -> StringName: return CurrencyDatabase.get_currency_id(currency) func parse_currency_id(raw_currency: Variant) -> StringName: return CurrencyDatabase.parse_currency_id(raw_currency) func is_known_currency_id(currency_id: StringName) -> bool: return CurrencyDatabase.is_known_currency_id(currency_id) func get_currency_resource(currency_id: StringName) -> Resource: return CurrencyDatabase.get_currency_resource(currency_id) func get_currency_name(currency_id: StringName) -> String: return CurrencyDatabase.get_currency_name(currency_id) func get_currency_icon(currency_id: StringName) -> Texture2D: return CurrencyDatabase.get_currency_icon(currency_id) func add_currency(currency: Resource, amount: BigNumber) -> void: var currency_id: StringName = get_currency_id(currency) if currency_id == &"": push_warning("Attempted to add currency with an invalid Currency resource.") return add_currency_by_id(currency_id, amount) func spend_currency(currency: Resource, cost: BigNumber) -> bool: var currency_id: StringName = get_currency_id(currency) if currency_id == &"": push_warning("Attempted to spend currency with an invalid Currency resource.") return false return spend_currency_by_id(currency_id, cost) func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return if amount.mantissa > 0.0: _get_total_currency_ref(normalized_currency_id).add_in_place(amount) _get_all_time_currency_ref(normalized_currency_id).add_in_place(amount) var current: BigNumber = _get_current_currency_ref(normalized_currency_id) current.add_in_place(amount) currency_changed.emit(normalized_currency_id, current) func spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return false var current: BigNumber = _get_current_currency_ref(normalized_currency_id) if current.is_greater_than(cost) or current.is_equal_to(cost): var negative_cost: BigNumber = BigNumber.new(-cost.mantissa, cost.exponent) current.add_in_place(negative_cost) currency_changed.emit(normalized_currency_id, current) return true return false func get_currency_amount(currency: Resource) -> BigNumber: return get_currency_amount_by_id(get_currency_id(currency)) func get_currency_amount_by_id(currency_id: StringName) -> BigNumber: return _get_current_currency_ref(currency_id) func get_total_currency_acquired(currency: Resource) -> BigNumber: return get_total_currency_acquired_by_id(get_currency_id(currency)) func get_total_currency_acquired_by_id(currency_id: StringName) -> BigNumber: return _get_total_currency_ref(currency_id) func get_all_time_currency_acquired(currency: Resource) -> BigNumber: return get_all_time_currency_acquired_by_id(get_currency_id(currency)) func get_all_time_currency_acquired_by_id(currency_id: StringName) -> BigNumber: return _get_all_time_currency_ref(currency_id) func register_generator(generator_id: StringName, unlocked: bool = false, available: bool = false, owned: int = 0) -> void: var key: String = String(generator_id) if key.is_empty(): return var default_owned: int = _to_non_negative_int(owned, 0) if generator_states.has(key): var existing_raw: Variant = generator_states.get(key) if existing_raw is Dictionary: generator_states[key] = _sanitize_generator_state(existing_raw, unlocked, available, default_owned) return var new_state: Dictionary = _default_generator_state(unlocked, available, default_owned) generator_states[key] = new_state generator_state_changed.emit(StringName(key), new_state.duplicate(true)) func get_generator_state(generator_id: StringName) -> Dictionary: return _get_generator_state(generator_id).duplicate(true) func get_generator_owned(generator_id: StringName) -> int: var state: Dictionary = _get_generator_state(generator_id) return int(state.get(GENERATOR_OWNED_KEY, 0)) func set_generator_owned(generator_id: StringName, value: int) -> void: var state: Dictionary = _get_generator_state(generator_id) var next_owned: int = maxi(value, 0) if int(state.get(GENERATOR_OWNED_KEY, 0)) == next_owned: return state[GENERATOR_OWNED_KEY] = next_owned state[GENERATOR_PURCHASED_COUNT_KEY] = maxi(int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)), next_owned) _set_generator_state(generator_id, state) func get_generator_purchased_count(generator_id: StringName) -> int: var state: Dictionary = _get_generator_state(generator_id) return int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)) func set_generator_purchased_count(generator_id: StringName, value: int) -> void: var state: Dictionary = _get_generator_state(generator_id) var min_purchased: int = int(state.get(GENERATOR_OWNED_KEY, 0)) var next_purchased: int = maxi(value, min_purchased) if int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)) == next_purchased: return state[GENERATOR_PURCHASED_COUNT_KEY] = next_purchased _set_generator_state(generator_id, state) func is_generator_unlocked(generator_id: StringName) -> bool: var state: Dictionary = _get_generator_state(generator_id) return bool(state.get(GENERATOR_UNLOCKED_KEY, false)) func set_generator_unlocked(generator_id: StringName, value: bool) -> void: var state: Dictionary = _get_generator_state(generator_id) if bool(state.get(GENERATOR_UNLOCKED_KEY, false)) == value: return state[GENERATOR_UNLOCKED_KEY] = value _set_generator_state(generator_id, state) func is_generator_available(generator_id: StringName) -> bool: var state: Dictionary = _get_generator_state(generator_id) return bool(state.get(GENERATOR_AVAILABLE_KEY, false)) func set_generator_available(generator_id: StringName, value: bool) -> void: var state: Dictionary = _get_generator_state(generator_id) if bool(state.get(GENERATOR_AVAILABLE_KEY, false)) == value: return state[GENERATOR_AVAILABLE_KEY] = value _set_generator_state(generator_id, state) func register_generator_buff(generator_id: StringName, buff_id: StringName, initial_level: int = 0) -> void: var generator_key: String = String(generator_id) var buff_key: String = String(buff_id) if generator_key.is_empty() or buff_key.is_empty(): return var levels: Dictionary = _get_generator_buff_levels_ref(generator_id) if levels.has(buff_key): var existing_level: int = _to_non_negative_int(levels.get(buff_key, initial_level), initial_level) if int(levels.get(buff_key, initial_level)) != existing_level: levels[buff_key] = existing_level generator_buff_levels[generator_key] = levels generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), existing_level) return var sanitized_level: int = _to_non_negative_int(initial_level, 0) levels[buff_key] = sanitized_level generator_buff_levels[generator_key] = levels generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), sanitized_level) func register_generator_buff_unlocked(generator_id: StringName, buff_id: StringName, initially_unlocked: bool = false) -> void: var generator_key: String = String(generator_id) var buff_key: String = String(buff_id) if generator_key.is_empty() or buff_key.is_empty(): return var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id) if unlocked_map.has(buff_key): var existing_unlocked: bool = _to_bool(unlocked_map.get(buff_key, initially_unlocked), initially_unlocked) if bool(unlocked_map.get(buff_key, initially_unlocked)) != existing_unlocked: unlocked_map[buff_key] = existing_unlocked generator_buff_unlocked[generator_key] = unlocked_map generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), existing_unlocked) return var sanitized_unlocked: bool = _to_bool(initially_unlocked, false) unlocked_map[buff_key] = sanitized_unlocked generator_buff_unlocked[generator_key] = unlocked_map generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), sanitized_unlocked) func get_generator_buff_level(generator_id: StringName, buff_id: StringName) -> int: var buff_key: String = String(buff_id) if buff_key.is_empty(): return 0 var levels: Dictionary = _get_generator_buff_levels_ref(generator_id) return _to_non_negative_int(levels.get(buff_key, 0), 0) func set_generator_buff_level(generator_id: StringName, buff_id: StringName, value: int) -> void: var generator_key: String = String(generator_id) var buff_key: String = String(buff_id) if generator_key.is_empty() or buff_key.is_empty(): return var levels: Dictionary = _get_generator_buff_levels_ref(generator_id) var next_level: int = _to_non_negative_int(value, 0) if int(levels.get(buff_key, -1)) == next_level: return levels[buff_key] = next_level generator_buff_levels[generator_key] = levels generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), next_level) func is_generator_buff_unlocked(generator_id: StringName, buff_id: StringName) -> bool: var buff_key: String = String(buff_id) if buff_key.is_empty(): return false var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id) return _to_bool(unlocked_map.get(buff_key, false), false) func set_generator_buff_unlocked(generator_id: StringName, buff_id: StringName, value: bool) -> void: var generator_key: String = String(generator_id) var buff_key: String = String(buff_id) if generator_key.is_empty() or buff_key.is_empty(): return var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id) var next_unlocked: bool = _to_bool(value, false) if _to_bool(unlocked_map.get(buff_key, false), false) == next_unlocked: return unlocked_map[buff_key] = next_unlocked generator_buff_unlocked[generator_key] = unlocked_map generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), next_unlocked) func get_generator_buff_unlocked(generator_id: StringName) -> Dictionary: return _get_generator_buff_unlocked_ref(generator_id).duplicate(true) func get_generator_buff_levels(generator_id: StringName) -> Dictionary: return _get_generator_buff_levels_ref(generator_id).duplicate(true) func register_buff(buff: GeneratorBuffData) -> void: if buff == null: return var buff_id: StringName = buff.id if buff_id == &"": push_warning("Attempted to register buff with invalid id") return _buff_definitions[buff_id] = buff if not _buff_levels.has(buff_id): _buff_levels[buff_id] = 0 if not _buff_unlocked.has(buff_id): _buff_unlocked[buff_id] = false if not _buff_active.has(buff_id): _buff_active[buff_id] = false func get_buff(buff_id: StringName) -> GeneratorBuffData: return _buff_definitions.get(buff_id, null) func get_all_buffs() -> Array[GeneratorBuffData]: var result: Array[GeneratorBuffData] = [] for buff in _buff_definitions.values(): if buff is GeneratorBuffData: result.append(buff) return result func get_buff_level(buff_id: StringName) -> int: return _buff_levels.get(buff_id, 0) func set_buff_level(buff_id: StringName, level: int) -> void: if level < 0: level = 0 var current_level: int = _buff_levels.get(buff_id, 0) if current_level == level: return _buff_levels[buff_id] = level buff_level_changed.emit(buff_id, level) func is_buff_unlocked(buff_id: StringName) -> bool: return _buff_unlocked.get(buff_id, false) func set_buff_unlocked(buff_id: StringName, unlocked: bool) -> void: if bool(_buff_unlocked.get(buff_id, false)) == unlocked: return _buff_unlocked[buff_id] = unlocked buff_unlocked_changed.emit(buff_id, unlocked) if unlocked and not _buff_active.get(buff_id, false): _buff_active[buff_id] = true func is_buff_active(buff_id: StringName) -> bool: return _buff_active.get(buff_id, false) func get_generators_targeted_by(buff_id: StringName) -> Array[StringName]: var buff: GeneratorBuffData = get_buff(buff_id) if buff == null: return [] return buff.get_target_generators() func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]: var result: Array[GeneratorBuffData] = [] for buff in _buff_definitions.values(): if buff is GeneratorBuffData and buff.targets_generator(generator_id): result.append(buff) return result func get_effective_multiplier(generator_id: StringName, kind: int) -> float: var multiplier: float = 1.0 for buff in get_buffs_for_generator(generator_id): if buff == null: continue if not is_buff_active(buff.id): continue if buff.kind != kind: continue var level: int = get_buff_level(buff.id) if level <= 0: continue var buff_multiplier: float = buff.get_effect_multiplier(level) multiplier *= buff_multiplier 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 if not buff.has_unlock_goal(): return if is_buff_unlocked(buff.id): return 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: var key: String = String(section_key).strip_edges() if key.is_empty(): return _external_save_sections[key] = payload.duplicate(true) func get_external_save_data(section_key: StringName) -> Dictionary: var key: String = String(section_key).strip_edges() if key.is_empty(): return {} var raw_payload: Variant = _external_save_sections.get(key, {}) if raw_payload is Dictionary: return raw_payload.duplicate(true) return {} func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signals: bool = true) -> void: var currency_ids: Array[StringName] = _collect_currency_ids_for_save() for currency_id in currency_ids: _set_current_currency(currency_id, BigNumber.from_float(0.0)) if reset_total_currency: _set_total_currency(currency_id, BigNumber.from_float(0.0)) generator_states.clear() generator_buff_levels.clear() generator_buff_unlocked.clear() _goal_completed.clear() if not emit_currency_signals: return for currency_id in currency_ids: currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) func emit_currency_changed_for_all() -> void: for currency_id in _collect_currency_ids_for_save(): currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) # ========================================== # PERSISTENCE (Save/Load) # ========================================== func save_game() -> void: var save_data = { SAVE_FORMAT_VERSION_KEY: CURRENT_SAVE_FORMAT_VERSION, CURRENCIES_KEY: _serialize_currency_balances(), GENERATOR_STATES_KEY: _serialize_generator_states(), 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() } for section_key in _external_save_sections.keys(): var key: String = String(section_key) if key.is_empty(): continue var payload: Variant = _external_save_sections.get(section_key) if not (payload is Dictionary): continue save_data[key] = payload var file: FileAccess = FileAccess.open(file_name, FileAccess.WRITE) if file: file.store_string(JSON.stringify(save_data)) func load_game() -> void: if not FileAccess.file_exists(file_name): return var file: FileAccess = FileAccess.open(file_name, FileAccess.READ) if file: var parsed: Variant = JSON.parse_string(file.get_as_text()) if parsed is Dictionary: var parsed_data: Dictionary = parsed var version: int = int(parsed_data.get(SAVE_FORMAT_VERSION_KEY, 1)) if version < 2: 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, GOALS_KEY, LAST_SAVE_TIME_KEY]: continue var section_payload: Variant = parsed_data.get(save_key_variant) if section_payload is Dictionary: _external_save_sections[save_key] = section_payload.duplicate(true) func _load_currency_balances(raw_currencies: Variant) -> void: if not (raw_currencies is Dictionary): return var currencies: Dictionary = raw_currencies for raw_currency_id in currencies.keys(): var currency_id: StringName = _normalize_currency_id(StringName(String(raw_currency_id))) if currency_id == &"": continue var raw_entry: Variant = currencies.get(raw_currency_id) if not (raw_entry is Dictionary): continue var entry: Dictionary = raw_entry var current_amount: BigNumber = BigNumber.deserialize(entry.get(CURRENT_KEY, {})) var total_amount: BigNumber = _deserialize_total_currency(entry.get(TOTAL_KEY, null), current_amount) var all_time_amount: BigNumber = _deserialize_all_time_currency(entry.get(ALL_TIME_KEY, null), total_amount) _set_current_currency(currency_id, current_amount) _set_total_currency(currency_id, total_amount) _set_all_time_currency(currency_id, all_time_amount) func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) -> BigNumber: var minimum_total: BigNumber = _copy_big_number(current_amount) if minimum_total.mantissa < 0.0: minimum_total = BigNumber.from_float(0.0) if not (raw_total is Dictionary): return minimum_total var parsed_total: BigNumber = BigNumber.deserialize(raw_total) if parsed_total.mantissa < 0.0: return minimum_total if parsed_total.is_less_than(minimum_total): return minimum_total return parsed_total func _deserialize_all_time_currency(raw_all_time: Variant, total_amount: BigNumber) -> BigNumber: var minimum_all_time: BigNumber = _copy_big_number(total_amount) if minimum_all_time.mantissa < 0.0: minimum_all_time = BigNumber.from_float(0.0) if not (raw_all_time is Dictionary): return minimum_all_time var parsed_all_time: BigNumber = BigNumber.deserialize(raw_all_time) if parsed_all_time.mantissa < 0.0: return minimum_all_time if parsed_all_time.is_less_than(minimum_all_time): return minimum_all_time return parsed_all_time func _copy_big_number(value: BigNumber) -> BigNumber: return BigNumber.new(value.mantissa, value.exponent) func _serialize_currency_balances() -> Dictionary: var result: Dictionary = {} for currency_id in _collect_currency_ids_for_save(): var key: String = String(currency_id) if key.is_empty(): continue result[key] = { CURRENT_KEY: _get_current_currency_ref(currency_id).serialize(), TOTAL_KEY: _get_total_currency_ref(currency_id).serialize(), ALL_TIME_KEY: _get_all_time_currency_ref(currency_id).serialize(), } return result func _collect_currency_ids_for_save() -> Array[StringName]: var ids: Array[StringName] = [] for raw_id in _current_currency.keys(): var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id))) if normalized_currency_id == &"" or ids.has(normalized_currency_id): continue ids.append(normalized_currency_id) for raw_id in _all_time_currency_acquired.keys(): var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id))) if normalized_currency_id == &"" or ids.has(normalized_currency_id): continue ids.append(normalized_currency_id) for known_currency_id in CurrencyDatabase.get_known_currency_ids(): var normalized_currency_id: StringName = _normalize_currency_id(known_currency_id) if normalized_currency_id == &"" or ids.has(normalized_currency_id): continue ids.append(normalized_currency_id) return ids func _initialize_currency_maps() -> void: for currency_id in CurrencyDatabase.get_known_currency_ids(): if currency_id == &"": continue _set_current_currency(currency_id, BigNumber.from_float(0.0)) _set_total_currency(currency_id, BigNumber.from_float(0.0)) _set_all_time_currency(currency_id, BigNumber.from_float(0.0)) func _normalize_currency_id(currency_id: StringName) -> StringName: var normalized: String = String(currency_id).to_lower().strip_edges() if normalized.is_empty(): return &"" return StringName(normalized) func _get_current_currency_ref(currency_id: StringName) -> BigNumber: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return BigNumber.from_float(0.0) var value: Variant = _current_currency.get(normalized_currency_id) if value is BigNumber: return value var fallback: BigNumber = BigNumber.from_float(0.0) _current_currency[normalized_currency_id] = fallback return fallback func _get_total_currency_ref(currency_id: StringName) -> BigNumber: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return BigNumber.from_float(0.0) var value: Variant = _total_currency_acquired.get(normalized_currency_id) if value is BigNumber: return value var fallback: BigNumber = BigNumber.from_float(0.0) _total_currency_acquired[normalized_currency_id] = fallback return fallback func _get_all_time_currency_ref(currency_id: StringName) -> BigNumber: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return BigNumber.from_float(0.0) var value: Variant = _all_time_currency_acquired.get(normalized_currency_id) if value is BigNumber: return value var fallback: BigNumber = BigNumber.from_float(0.0) _all_time_currency_acquired[normalized_currency_id] = fallback return fallback func _set_current_currency(currency_id: StringName, value: BigNumber) -> void: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return _current_currency[normalized_currency_id] = _copy_big_number(value) func _set_total_currency(currency_id: StringName, value: BigNumber) -> void: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return _total_currency_acquired[normalized_currency_id] = _copy_big_number(value) func _set_all_time_currency(currency_id: StringName, value: BigNumber) -> void: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": return _all_time_currency_acquired[normalized_currency_id] = _copy_big_number(value) func _default_generator_state(unlocked: bool, available: bool, owned: int = 0) -> Dictionary: var owned_value: int = _to_non_negative_int(owned, 0) return { GENERATOR_OWNED_KEY: owned_value, GENERATOR_PURCHASED_COUNT_KEY: owned_value, GENERATOR_UNLOCKED_KEY: unlocked, GENERATOR_AVAILABLE_KEY: available, } func _get_generator_state(generator_id: StringName) -> Dictionary: var key: String = String(generator_id) if key.is_empty(): return _default_generator_state(false, false, 0) if not generator_states.has(key): register_generator(generator_id) var existing_raw: Variant = generator_states.get(key, _default_generator_state(false, false, 0)) if existing_raw is Dictionary: var state: Dictionary = existing_raw var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false) var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false) var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0) var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned) generator_states[key] = sanitized return sanitized var fallback: Dictionary = _default_generator_state(false, false, 0) generator_states[key] = fallback return fallback func _set_generator_state(generator_id: StringName, state: Dictionary) -> void: var key: String = String(generator_id) if key.is_empty(): return var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false) var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false) var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0) var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned) generator_states[key] = sanitized generator_state_changed.emit(StringName(key), sanitized.duplicate(true)) func _get_generator_buff_levels_ref(generator_id: StringName) -> Dictionary: var key: String = String(generator_id) if key.is_empty(): return {} var existing_raw: Variant = generator_buff_levels.get(key, {}) if existing_raw is Dictionary: var sanitized: Dictionary = _sanitize_generator_buff_levels(existing_raw) generator_buff_levels[key] = sanitized return sanitized var fallback: Dictionary = {} generator_buff_levels[key] = fallback return fallback func _get_generator_buff_unlocked_ref(generator_id: StringName) -> Dictionary: var key: String = String(generator_id) if key.is_empty(): return {} var existing_raw: Variant = generator_buff_unlocked.get(key, {}) if existing_raw is Dictionary: var sanitized: Dictionary = _sanitize_generator_buff_unlocked(existing_raw) generator_buff_unlocked[key] = sanitized return sanitized var fallback: Dictionary = {} generator_buff_unlocked[key] = fallback return fallback func _sanitize_generator_buff_levels(raw_levels: Dictionary) -> Dictionary: var sanitized_levels: Dictionary = {} for buff_key_variant in raw_levels.keys(): var buff_key: String = String(buff_key_variant) if buff_key.is_empty(): continue sanitized_levels[buff_key] = _to_non_negative_int(raw_levels.get(buff_key_variant, 0), 0) return sanitized_levels func _sanitize_generator_buff_unlocked(raw_unlocked: Dictionary) -> Dictionary: var sanitized_unlocked: Dictionary = {} for buff_key_variant in raw_unlocked.keys(): var buff_key: String = String(buff_key_variant) if buff_key.is_empty(): continue # Save format only stores true values, but keep parser tolerant. var is_unlocked: bool = _to_bool(raw_unlocked.get(buff_key_variant, false), false) if not is_unlocked: continue sanitized_unlocked[buff_key] = true return sanitized_unlocked func _serialize_generator_states() -> Dictionary: var result: Dictionary = {} for key_variant in generator_states.keys(): var key: String = String(key_variant) if key.is_empty(): continue var value: Variant = generator_states.get(key_variant) if value is Dictionary: result[key] = _serialize_generator_state_entry(value) return result func _deserialize_generator_states(raw_value: Variant) -> Dictionary: var result: Dictionary = {} if not (raw_value is Dictionary): return result for key_variant in raw_value.keys(): var key: String = String(key_variant) if key.is_empty(): continue var entry: Variant = raw_value.get(key_variant) if entry is Dictionary: result[key] = _deserialize_generator_state_entry(entry) return result func _serialize_generator_buff_levels() -> Dictionary: var result: Dictionary = {} for generator_key_variant in generator_buff_levels.keys(): var generator_key: String = String(generator_key_variant) if generator_key.is_empty(): continue var raw_levels: Variant = generator_buff_levels.get(generator_key_variant) if raw_levels is Dictionary: result[generator_key] = _sanitize_generator_buff_levels(raw_levels) return result func _deserialize_generator_buff_levels(raw_value: Variant) -> Dictionary: var result: Dictionary = {} if not (raw_value is Dictionary): return result for generator_key_variant in raw_value.keys(): var generator_key: String = String(generator_key_variant) if generator_key.is_empty(): continue var raw_levels: Variant = raw_value.get(generator_key_variant) if raw_levels is Dictionary: result[generator_key] = _sanitize_generator_buff_levels(raw_levels) return result func _serialize_generator_buff_unlocked() -> Dictionary: var result: Dictionary = {} for generator_key_variant in generator_buff_unlocked.keys(): var generator_key: String = String(generator_key_variant) if generator_key.is_empty(): continue var raw_unlocked: Variant = generator_buff_unlocked.get(generator_key_variant) if not (raw_unlocked is Dictionary): continue var sanitized_unlocked: Dictionary = _sanitize_generator_buff_unlocked(raw_unlocked) if sanitized_unlocked.is_empty(): continue result[generator_key] = sanitized_unlocked return result func _deserialize_generator_buff_unlocked(raw_value: Variant) -> Dictionary: var result: Dictionary = {} if not (raw_value is Dictionary): return result for generator_key_variant in raw_value.keys(): var generator_key: String = String(generator_key_variant) if generator_key.is_empty(): continue var raw_unlocked: Variant = raw_value.get(generator_key_variant) if not (raw_unlocked is Dictionary): continue var sanitized_unlocked: Dictionary = _sanitize_generator_buff_unlocked(raw_unlocked) if sanitized_unlocked.is_empty(): continue result[generator_key] = sanitized_unlocked return result func _serialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0) var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) var serialized_state: Dictionary = { GENERATOR_OWNED_KEY: owned_value, GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), } if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool: serialized_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY) if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool: serialized_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY) return serialized_state func _deserialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0) var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) var parsed_state: Dictionary = { GENERATOR_OWNED_KEY: owned_value, GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), } if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool: parsed_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY) if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool: parsed_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY) return parsed_state func _sanitize_generator_state(raw_state: Dictionary, default_unlocked: bool, default_available: bool, default_owned: int = 0) -> Dictionary: var normalized_default_owned: int = _to_non_negative_int(default_owned, 0) var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, normalized_default_owned), normalized_default_owned) var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) return { GENERATOR_OWNED_KEY: owned_value, GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), GENERATOR_UNLOCKED_KEY: _to_bool(raw_state.get(GENERATOR_UNLOCKED_KEY, default_unlocked), default_unlocked), GENERATOR_AVAILABLE_KEY: _to_bool(raw_state.get(GENERATOR_AVAILABLE_KEY, default_available), default_available), } func _to_non_negative_int(value: Variant, fallback: int = 0) -> int: if value is int: return maxi(value, 0) if value is float: return maxi(int(value), 0) return maxi(fallback, 0) func _to_bool(value: Variant, fallback: bool) -> bool: if value is bool: return value return fallback func _serialize_buff_levels() -> Dictionary: var result: Dictionary = {} for buff_id in _buff_levels.keys(): result[buff_id] = _buff_levels[buff_id] return result func _serialize_buff_unlocked() -> Dictionary: var result: Dictionary = {} for buff_id in _buff_unlocked.keys(): if _buff_unlocked[buff_id]: result[buff_id] = true return result func _serialize_buff_active() -> Dictionary: var result: Dictionary = {} for buff_id in _buff_active.keys(): if _buff_active[buff_id]: result[buff_id] = true return result func _deserialize_buff_states(parsed_data: Dictionary) -> void: _buff_levels.clear() _buff_unlocked.clear() _buff_active.clear() if parsed_data.has(BUFF_LEVELS_KEY): var raw_levels: Variant = parsed_data.get(BUFF_LEVELS_KEY) if raw_levels is Dictionary: for key_variant in raw_levels.keys(): var key: String = String(key_variant) if not key.is_empty(): var value: Variant = raw_levels.get(key_variant) if value is int: _buff_levels[key] = value if parsed_data.has(BUFF_UNLOCKED_KEY): var raw_unlocked: Variant = parsed_data.get(BUFF_UNLOCKED_KEY) if raw_unlocked is Dictionary: for key_variant in raw_unlocked.keys(): var key: String = String(key_variant) if not key.is_empty(): _buff_unlocked[key] = true if parsed_data.has(BUFF_ACTIVE_KEY): var raw_active: Variant = parsed_data.get(BUFF_ACTIVE_KEY) if raw_active is Dictionary: for key_variant in raw_active.keys(): 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(): if goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL: return _goal_completed[goal_id] = true goal_completed.emit(goal_id) func _complete_goal_manually(goal_id: StringName) -> void: var goal: GoalData = _goal_definitions.get(goal_id, null) if goal == null: return if _goal_completed[goal_id]: return if not goal.is_met(): return _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