class_name PrestigeManager extends Node signal prestige_state_changed(total_prestige: BigNumber, pending_gain: BigNumber) signal prestige_performed(gain: BigNumber, total_prestige: BigNumber) signal prestige_threshold_changed(next_threshold: BigNumber) const SAVE_KEY: String = "prestige_state" const TOTAL_PRESTIGE_KEY: String = "total_prestige_earned" const CURRENT_PRESTIGE_UNSPENT_KEY: String = "current_prestige_unspent" const PRESTIGE_RESETS_COUNT_KEY: String = "prestige_resets_count" const RUN_START_TOTAL_SOURCE_KEY: String = "run_start_total_source_currency" const RUN_PEAK_SOURCE_KEY: String = "run_peak_source_currency" const LAST_RESET_TIME_KEY: String = "last_reset_time" const BASIS_LIFETIME_TOTAL: int = 0 const BASIS_RUN_TOTAL: int = 1 const BASIS_RUN_MAX: int = 2 const BASIS_ALL_CURRENCIES: int = 3 const FORMULA_POWER: int = 0 const FORMULA_TRIANGULAR_INVERSE: int = 1 const MULTIPLIER_MODE_ADDITIVE: int = 0 const MULTIPLIER_MODE_MULTIPLICATIVE_POWER: int = 1 @export var config: PrestigeConfig @export var game_state: LevelGameState var total_prestige_earned: BigNumber = BigNumber.from_float(0.0) var current_prestige_unspent: BigNumber = BigNumber.from_float(0.0) var prestige_resets_count: int = 0 var run_start_total_source_currency: BigNumber = BigNumber.from_float(0.0) var run_peak_source_currency: BigNumber = BigNumber.from_float(0.0) var last_reset_time: int = 0 func _ready() -> void: if config == null: config = load("res://docs/gyms/tiny_sword/prestige/primary_prestige.tres") as PrestigeConfig if not _is_config_valid(): push_warning("PrestigeManager has invalid or missing config; prestige is disabled.") return if game_state == null: push_warning("PrestigeManager: game_state reference is not set.") return var loaded_state: Dictionary = game_state.get_external_save_data(SAVE_KEY) if loaded_state.is_empty(): _initialize_run_tracking_from_current_state() else: _deserialize_state(loaded_state) _validate_loaded_run_tracking() _save_state_to_game_state() game_state.currency_changed.connect(_on_currency_changed) prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain()) prestige_threshold_changed.emit(get_next_prestige_threshold()) func get_config() -> PrestigeConfig: return config func get_source_currency_id() -> StringName: if not _is_config_valid(): return &"" return _normalize_currency_id(_get_config_string_name("source_currency_id", &"")) func get_prestige_currency_id() -> StringName: if not _is_config_valid(): return &"" return _normalize_currency_id(_get_config_string_name("prestige_currency_id", &"")) func get_tracked_currency_ids() -> Array[StringName]: if not _is_config_valid(): return [] var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) if basis_type == BASIS_ALL_CURRENCIES: var include_ids: Array[StringName] = _get_config_array("include_currency_ids", []) if not include_ids.is_empty(): return include_ids if game_state and game_state.currency_catalogue: return game_state.currency_catalogue.get_all_ids() return [] return [get_source_currency_id()] func _get_config_array(property_name: String, fallback: Array = []) -> Array: if config == null: return fallback var value: Variant = config.get(property_name) if value is Array: return value return fallback func get_total_prestige() -> BigNumber: return _copy_big_number(total_prestige_earned) func get_current_prestige_unspent() -> BigNumber: return _copy_big_number(current_prestige_unspent) func get_total_multiplier() -> float: if not _is_config_valid(): return 1.0 var base: float = maxf(_get_config_float("base_multiplier", 1.0), 0.0) var prestige_value: float = total_prestige_earned.to_float() if prestige_value <= 0.0: return base match _get_config_int("multiplier_mode", MULTIPLIER_MODE_ADDITIVE): MULTIPLIER_MODE_MULTIPLICATIVE_POWER: var growth_base: float = 1.0 + maxf(_get_config_float("multiplier_per_prestige", 0.0), 0.0) if growth_base <= 0.0: return base var effective_power: float = pow(prestige_value, maxf(_get_config_float("multiplier_exponent", 1.0), 0.0)) var mult: float = base * pow(growth_base, effective_power) if not is_finite(mult): return INF return maxf(mult, 0.0) _: return maxf(base + prestige_value * maxf(_get_config_float("multiplier_per_prestige", 0.0), 0.0), 0.0) func calculate_pending_gain() -> BigNumber: if not _is_config_valid(): return BigNumber.from_float(0.0) var basis_value: BigNumber = _get_basis_value() if basis_value.mantissa <= 0.0: return BigNumber.from_float(0.0) var raw_gain: float = _calculate_raw_gain_float(basis_value) if not is_finite(raw_gain): raw_gain = 0.0 raw_gain = maxf(raw_gain, 0.0) var rounded_gain: float = _round_gain(raw_gain) if not is_finite(rounded_gain): rounded_gain = 0.0 rounded_gain = maxf(rounded_gain, 0.0) if rounded_gain > 0.0: rounded_gain = maxf(rounded_gain, float(maxi(_get_config_int("minimum_gain", 0), 0))) return BigNumber.from_float(rounded_gain) func can_prestige() -> bool: if not _is_config_valid(): return false var pending: BigNumber = calculate_pending_gain() if pending.mantissa > 0.0: return true return _get_config_bool("allow_prestige_without_gain", false) func perform_prestige() -> bool: if not can_prestige(): return false var gain: BigNumber = calculate_pending_gain() if gain.mantissa > 0.0: total_prestige_earned.add_in_place(gain) current_prestige_unspent.add_in_place(gain) var prestige_currency_id: StringName = get_prestige_currency_id() if prestige_currency_id != &"" and game_state: game_state.add_currency_by_id(prestige_currency_id, gain) prestige_resets_count += 1 last_reset_time = Time.get_unix_time_from_system() if game_state: var preserve_currencies: Array[StringName] = [] var prestige_currency_id: StringName = get_prestige_currency_id() if prestige_currency_id != &"": preserve_currencies.append(prestige_currency_id) game_state.reset_for_prestige(true, false, preserve_currencies) _reset_all_buff_levels() _reinitialize_generators_after_prestige_reset() if game_state: game_state.emit_currency_changed_for_all() _initialize_run_tracking_from_current_state() _save_state_to_game_state() if game_state: game_state.save_game() var total: BigNumber = get_total_prestige() var next_threshold: BigNumber = get_next_prestige_threshold() prestige_performed.emit(gain, total) prestige_state_changed.emit(total, calculate_pending_gain()) prestige_threshold_changed.emit(next_threshold) return true func get_next_prestige_threshold() -> BigNumber: if not _is_config_valid(): return BigNumber.from_float(0.0) var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) match basis_type: BASIS_RUN_TOTAL, BASIS_ALL_CURRENCIES: return _get_config_threshold() _: var current_total: float = total_prestige_earned.to_float() var next_target: float = _calculate_target_for_prestige(current_total + 1.0) return BigNumber.from_float(next_target) func get_basis_value_for_display() -> BigNumber: return _get_basis_value() func get_basis_label() -> String: if not _is_config_valid(): return "-" var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) match basis_type: BASIS_RUN_TOTAL: return "Run Total" BASIS_RUN_MAX: return "Run Max" BASIS_ALL_CURRENCIES: return "Total Currency" _: return "Lifetime Total" func _on_currency_changed(changed_currency_id: StringName, new_amount: BigNumber) -> void: if config == null: return var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) if basis_type == BASIS_ALL_CURRENCIES: if new_amount.is_greater_than(run_peak_source_currency): run_peak_source_currency = _copy_big_number(new_amount) else: var source_currency_id: StringName = get_source_currency_id() if source_currency_id == &"": return if changed_currency_id != source_currency_id: return if new_amount.is_greater_than(run_peak_source_currency): run_peak_source_currency = _copy_big_number(new_amount) _save_state_to_game_state() var next_threshold: BigNumber = get_next_prestige_threshold() prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain()) prestige_threshold_changed.emit(next_threshold) func _calculate_target_for_prestige(prestige_level: float) -> float: if config == null: return 0.0 var threshold_value: float = _get_config_threshold().to_float() if threshold_value <= 0.0: return 0.0 match _get_config_int("formula", FORMULA_POWER): FORMULA_TRIANGULAR_INVERSE: var target_total: float = prestige_level var k: float = target_total * (target_total + 1.0) * 0.5 return k * threshold_value _: var scale: float = maxf(_get_config_float("scale", 1.0), 0.0001) var exponent: float = maxf(_get_config_float("exponent", 1.0), 0.0001) var flat_bonus: float = _get_config_float("flat_bonus", 0.0) if scale <= 0.0: scale = 0.0001 if exponent <= 0.0: exponent = 0.0001 var ratio: float = pow((prestige_level - flat_bonus) / scale, 1.0 / exponent) if ratio <= 0.0: return threshold_value return ratio * threshold_value func _calculate_raw_gain_float(basis_value: BigNumber) -> float: if config == null: return 0.0 match _get_config_int("formula", FORMULA_POWER): FORMULA_TRIANGULAR_INVERSE: var threshold_value: float = _get_config_threshold().to_float() if threshold_value <= 0.0: return 0.0 var basis_float: float = basis_value.to_float() if basis_float <= 0.0: return 0.0 var k: float = basis_float / threshold_value if k <= 0.0: return 0.0 var target_total: float = (sqrt(1.0 + 8.0 * k) - 1.0) * 0.5 if _is_cumulative_basis(): return target_total - total_prestige_earned.to_float() return target_total _: var ratio: float = _big_number_ratio_to_float(basis_value, _get_config_threshold()) if ratio <= 0.0: return 0.0 var value: float = maxf(_get_config_float("scale", 0.0), 0.0) * pow(ratio, maxf(_get_config_float("exponent", 0.0001), 0.0001)) + _get_config_float("flat_bonus", 0.0) if _is_cumulative_basis(): return value - total_prestige_earned.to_float() return value func _get_basis_value() -> BigNumber: if config == null: return BigNumber.from_float(0.0) if game_state == null: return BigNumber.from_float(0.0) var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) match basis_type: BASIS_ALL_CURRENCIES: return _get_total_all_currencies() BASIS_RUN_TOTAL: var source_currency_id: StringName = get_source_currency_id() if source_currency_id == &"": return BigNumber.from_float(0.0) var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id) var run_total: BigNumber = lifetime_now.subtract(run_start_total_source_currency) if run_total.mantissa < 0.0: return BigNumber.from_float(0.0) return run_total BASIS_RUN_MAX: return _copy_big_number(run_peak_source_currency) _: var source_currency_id: StringName = get_source_currency_id() if source_currency_id == &"": return BigNumber.from_float(0.0) return _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id)) func _get_total_all_currencies() -> BigNumber: var total: BigNumber = BigNumber.from_float(0.0) var currency_ids: Array[StringName] = get_tracked_currency_ids() for currency_id in currency_ids: var currency_total: BigNumber = game_state.get_total_currency_acquired_by_id(currency_id) total.add_in_place(currency_total) return total func _initialize_run_tracking_from_current_state() -> void: if config == null: return var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) if basis_type == BASIS_ALL_CURRENCIES: run_start_total_source_currency = _get_total_all_currencies() run_peak_source_currency = BigNumber.from_float(0.0) var currency_ids: Array[StringName] = get_tracked_currency_ids() for currency_id in currency_ids: var current: BigNumber = game_state.get_currency_amount_by_id(currency_id) if current.is_greater_than(run_peak_source_currency): run_peak_source_currency = _copy_big_number(current) _save_state_to_game_state() return var source_currency_id: StringName = get_source_currency_id() if source_currency_id == &"": run_start_total_source_currency = BigNumber.from_float(0.0) run_peak_source_currency = BigNumber.from_float(0.0) return if game_state == null: run_start_total_source_currency = BigNumber.from_float(0.0) run_peak_source_currency = BigNumber.from_float(0.0) return run_start_total_source_currency = _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id)) run_peak_source_currency = _copy_big_number(game_state.get_currency_amount_by_id(source_currency_id)) _save_state_to_game_state() func _validate_loaded_run_tracking() -> void: if config == null: return var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) if basis_type == BASIS_ALL_CURRENCIES: var currency_ids: Array[StringName] = get_tracked_currency_ids() var peak: BigNumber = BigNumber.from_float(0.0) for currency_id in currency_ids: var current: BigNumber = game_state.get_currency_amount_by_id(currency_id) if current.is_greater_than(peak): peak = _copy_big_number(current) if peak.is_greater_than(run_peak_source_currency): run_peak_source_currency = _copy_big_number(peak) return var source_currency_id: StringName = get_source_currency_id() if source_currency_id == &"": run_start_total_source_currency = BigNumber.from_float(0.0) run_peak_source_currency = BigNumber.from_float(0.0) return if game_state == null: return var current_currency: BigNumber = game_state.get_currency_amount_by_id(source_currency_id) if current_currency.is_greater_than(run_peak_source_currency): run_peak_source_currency = _copy_big_number(current_currency) var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id) if run_start_total_source_currency.is_greater_than(lifetime_now): run_start_total_source_currency = _copy_big_number(lifetime_now) func _serialize_state() -> Dictionary: return { TOTAL_PRESTIGE_KEY: total_prestige_earned.serialize(), CURRENT_PRESTIGE_UNSPENT_KEY: current_prestige_unspent.serialize(), PRESTIGE_RESETS_COUNT_KEY: maxi(prestige_resets_count, 0), RUN_START_TOTAL_SOURCE_KEY: run_start_total_source_currency.serialize(), RUN_PEAK_SOURCE_KEY: run_peak_source_currency.serialize(), LAST_RESET_TIME_KEY: maxi(last_reset_time, 0), } func _deserialize_state(raw_state: Dictionary) -> void: total_prestige_earned = _deserialize_big_number(raw_state.get(TOTAL_PRESTIGE_KEY, {})) current_prestige_unspent = _deserialize_big_number(raw_state.get(CURRENT_PRESTIGE_UNSPENT_KEY, {})) prestige_resets_count = _to_non_negative_int(raw_state.get(PRESTIGE_RESETS_COUNT_KEY, 0), 0) run_start_total_source_currency = _deserialize_big_number(raw_state.get(RUN_START_TOTAL_SOURCE_KEY, {})) run_peak_source_currency = _deserialize_big_number(raw_state.get(RUN_PEAK_SOURCE_KEY, {})) last_reset_time = _to_non_negative_int(raw_state.get(LAST_RESET_TIME_KEY, 0), 0) if current_prestige_unspent.is_greater_than(total_prestige_earned): current_prestige_unspent = _copy_big_number(total_prestige_earned) func _save_state_to_game_state() -> void: if game_state: game_state.set_external_save_data(SAVE_KEY, _serialize_state()) func _reset_all_buff_levels() -> void: if game_state == null: return for buff in game_state.get_all_buffs(): if buff == null: continue game_state.set_buff_level(buff.id, 0) game_state.set_buff_unlocked(buff.id, false) func _reinitialize_generators_after_prestige_reset() -> void: var scene_root: Node = get_tree().current_scene if scene_root == null: return var generator_nodes: Array[Node] = scene_root.find_children("*", "CurrencyGenerator", true, false) for generator_node in generator_nodes: if generator_node == null: continue if not generator_node.has_method("reset_runtime_state_for_prestige"): continue generator_node.call("reset_runtime_state_for_prestige") func _deserialize_big_number(raw_value: Variant) -> BigNumber: if raw_value is Dictionary: return BigNumber.deserialize(raw_value) return BigNumber.from_float(0.0) func _copy_big_number(value: BigNumber) -> BigNumber: return BigNumber.new(value.mantissa, value.exponent) func _big_number_ratio_to_float(a: BigNumber, b: BigNumber) -> float: if b.mantissa <= 0.0: return 0.0 if a.mantissa <= 0.0: return 0.0 var exponent_delta: int = a.exponent - b.exponent if exponent_delta > 308: return INF if exponent_delta < -308: return 0.0 return (a.mantissa / b.mantissa) * pow(10.0, float(exponent_delta)) 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 _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 _is_config_valid() -> bool: if config == null: return false return config.is_valid() func _is_cumulative_basis() -> bool: var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) return basis_type != BASIS_RUN_TOTAL and basis_type != BASIS_ALL_CURRENCIES func _round_gain(value: float) -> float: return config.round_value(value) func _get_config_threshold() -> BigNumber: if config != null: return config.get_threshold() return BigNumber.from_float(1.0) func _get_config_string_name(property_name: String, fallback: StringName = &"") -> StringName: if config == null: return fallback var value: Variant = config.get(property_name) if value is StringName: return value if value is String: return StringName(String(value)) return fallback func _get_config_int(property_name: String, fallback: int = 0) -> int: if config == null: return fallback var value: Variant = config.get(property_name) if value is int: return value if value is float: return int(value) return fallback func _get_config_float(property_name: String, fallback: float = 0.0) -> float: if config == null: return fallback var value: Variant = config.get(property_name) if value is float: return value if value is int: return float(value) return fallback func _get_config_bool(property_name: String, fallback: bool = false) -> bool: if config == null: return fallback var value: Variant = config.get(property_name) if value is bool: return value return fallback