Files
idle/core/prestige/prestige_manager.gd

472 lines
15 KiB
GDScript

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 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: Resource
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/prestige/primary_prestige.tres") as Resource
if not _is_config_valid():
push_warning("PrestigeManager has invalid or missing config; prestige is disabled.")
return
var loaded_state: Dictionary = GameState.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()
GameState.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() -> Resource:
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_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 = _big_number_to_float(total_prestige_earned)
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)
prestige_resets_count += 1
last_reset_time = Time.get_unix_time_from_system()
GameState.reset_for_prestige(true, false)
_reset_all_buff_levels()
_reinitialize_generators_after_prestige_reset()
GameState.emit_currency_changed_for_all()
_initialize_run_tracking_from_current_state()
_save_state_to_game_state()
GameState.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)
match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
BASIS_RUN_TOTAL:
return _get_config_threshold()
_:
var current_total: float = _big_number_to_float(total_prestige_earned)
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 "-"
match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
BASIS_RUN_TOTAL:
return "Run Total"
BASIS_RUN_MAX:
return "Run Max"
_:
return "Lifetime Total"
func _on_currency_changed(changed_currency_id: StringName, new_amount: BigNumber) -> void:
if config == null:
return
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 = _big_number_to_float(_get_config_threshold())
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 = _big_number_to_float(_get_config_threshold())
if threshold_value <= 0.0:
return 0.0
var basis_float: float = _big_number_to_float(basis_value)
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 - _big_number_to_float(total_prestige_earned)
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 - _big_number_to_float(total_prestige_earned)
return value
func _get_basis_value() -> BigNumber:
if config == null:
return BigNumber.from_float(0.0)
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
return BigNumber.from_float(0.0)
match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
BASIS_RUN_TOTAL:
var lifetime_now: BigNumber = GameState.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)
_:
return _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))
func _initialize_run_tracking_from_current_state() -> void:
if config == null:
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
run_start_total_source_currency = _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))
run_peak_source_currency = _copy_big_number(GameState.get_currency_amount_by_id(source_currency_id))
_save_state_to_game_state()
func _validate_loaded_run_tracking() -> void:
if config == null:
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
var current_currency: BigNumber = GameState.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 = GameState.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:
GameState.set_external_save_data(SAVE_KEY, _serialize_state())
func _reset_all_buff_levels() -> void:
for buff_id in GameState._buff_levels.keys():
GameState.set_buff_level(buff_id, 0)
GameState.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 _big_number_to_float(value: BigNumber) -> float:
if value.mantissa <= 0.0:
return 0.0
if value.exponent > 308:
return INF
if value.exponent < -308:
return 0.0
return value.mantissa * pow(10.0, float(value.exponent))
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
if not config.has_method("is_valid"):
return false
return bool(config.call("is_valid"))
func _is_cumulative_basis() -> bool:
if config != null and config.has_method("is_cumulative_basis"):
return bool(config.call("is_cumulative_basis"))
return _get_config_int("basis", BASIS_LIFETIME_TOTAL) != BASIS_RUN_TOTAL
func _round_gain(value: float) -> float:
if config != null and config.has_method("round_value"):
var rounded: Variant = config.call("round_value", value)
if rounded is float:
return rounded
if rounded is int:
return float(rounded)
return floorf(value)
func _get_config_threshold() -> BigNumber:
if config != null and config.has_method("get_threshold"):
var threshold: Variant = config.call("get_threshold")
if threshold is BigNumber:
return 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