From c914b0c59036ac59036de504b260c4978e22b912 Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Sun, 22 Mar 2026 10:50:37 +0100 Subject: [PATCH] Add prestige --- core/game_state.gd | 105 +++++++ core/generator/currency_generator.gd | 38 ++- core/prestige/TECH_SPEC.md | 179 +++++++++++ core/prestige/prestige_config.gd | 77 +++++ core/prestige/prestige_config.gd.uid | 1 + core/prestige/prestige_manager.gd | 416 ++++++++++++++++++++++++++ core/prestige/prestige_manager.gd.uid | 1 + core/prestige/prestige_panel.gd | 90 ++++++ core/prestige/prestige_panel.gd.uid | 1 + core/prestige/prestige_panel.tscn | 81 +++++ docs/gyms/generator_gym.tscn | 8 + generator_container.gd | 2 +- idles/goals/magic_350k_wood_20k.tres | 14 +- idles/prestige/primary_prestige.tres | 25 ++ project.godot | 1 + 15 files changed, 1032 insertions(+), 7 deletions(-) create mode 100644 core/prestige/TECH_SPEC.md create mode 100644 core/prestige/prestige_config.gd create mode 100644 core/prestige/prestige_config.gd.uid create mode 100644 core/prestige/prestige_manager.gd create mode 100644 core/prestige/prestige_manager.gd.uid create mode 100644 core/prestige/prestige_panel.gd create mode 100644 core/prestige/prestige_panel.gd.uid create mode 100644 core/prestige/prestige_panel.tscn create mode 100644 idles/prestige/primary_prestige.tres diff --git a/core/game_state.gd b/core/game_state.gd index 2d85956..034653e 100644 --- a/core/game_state.gd +++ b/core/game_state.gd @@ -14,10 +14,12 @@ signal generator_buff_unlocked_changed(generator_id: StringName, buff_id: String # ========================================== 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 last_save_time: int = 0 # Unix timestamp @@ -35,6 +37,7 @@ const GENERATOR_BUFF_UNLOCKED_KEY: String = "generator_buff_unlocked" 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: @@ -100,6 +103,7 @@ func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void: 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) @@ -130,6 +134,12 @@ func get_total_currency_acquired(currency: Resource) -> BigNumber: 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(): @@ -293,6 +303,35 @@ func get_generator_buff_unlocked(generator_id: StringName) -> Dictionary: func get_generator_buff_levels(generator_id: StringName) -> Dictionary: return _get_generator_buff_levels_ref(generator_id).duplicate(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) -> void: + for currency_id in _collect_currency_ids_for_save(): + _set_current_currency(currency_id, BigNumber.from_float(0.0)) + currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) + 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() + # ========================================== # PERSISTENCE (Save/Load) # ========================================== @@ -305,6 +344,17 @@ func save_game() -> void: 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)) @@ -318,6 +368,7 @@ func load_game() -> void: var parsed: Variant = JSON.parse_string(file.get_as_text()) if parsed is Dictionary: var parsed_data: Dictionary = parsed + _external_save_sections.clear() if parsed_data.has(CURRENCIES_KEY): _load_currency_balances(parsed_data.get(CURRENCIES_KEY, {})) @@ -325,6 +376,16 @@ func load_game() -> void: generator_buff_levels = _deserialize_generator_buff_levels(parsed_data.get(GENERATOR_BUFF_LEVELS_KEY, {})) generator_buff_unlocked = _deserialize_generator_buff_unlocked(parsed_data.get(GENERATOR_BUFF_UNLOCKED_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 [CURRENCIES_KEY, GENERATOR_STATES_KEY, GENERATOR_BUFF_LEVELS_KEY, GENERATOR_BUFF_UNLOCKED_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): @@ -343,8 +404,10 @@ func _load_currency_balances(raw_currencies: Variant) -> void: 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) @@ -361,6 +424,21 @@ func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) 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) @@ -375,6 +453,7 @@ func _serialize_currency_balances() -> Dictionary: 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 @@ -388,6 +467,12 @@ func _collect_currency_ids_for_save() -> Array[StringName]: 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): @@ -402,6 +487,7 @@ func _initialize_currency_maps() -> void: 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() @@ -435,6 +521,19 @@ func _get_total_currency_ref(currency_id: StringName) -> BigNumber: _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 == &"": @@ -447,6 +546,12 @@ func _set_total_currency(currency_id: StringName, value: BigNumber) -> void: 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 { diff --git a/core/generator/currency_generator.gd b/core/generator/currency_generator.gd index 4f10aa8..5111362 100644 --- a/core/generator/currency_generator.gd +++ b/core/generator/currency_generator.gd @@ -87,6 +87,8 @@ var cycle_progress_seconds: float = 0.0 var _generator_id: StringName = &"" ## Prevents duplicate state registration in GameState. var _is_registered: bool = false +## Prevents re-entrant registration while GameState emits synchronous callbacks. +var _is_registering: bool = false ## Resolves and registers this generator state before gameplay updates. func _ready() -> void: @@ -127,7 +129,7 @@ func _grant_cycle_income(cycle_count: int) -> void: if data == null or cycle_count <= 0: return - var effective_run_multiplier: float = run_multiplier * get_automatic_production_multiplier() + var effective_run_multiplier: float = get_effective_auto_run_multiplier() var per_cycle: float = data.production_per_cycle(owned, effective_run_multiplier, purchased_count) if per_cycle <= 0.0: return @@ -308,6 +310,19 @@ func is_buff_maxed(buff_id: StringName) -> bool: func get_automatic_production_multiplier() -> float: return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER) +func get_effective_auto_run_multiplier() -> float: + return run_multiplier * get_automatic_production_multiplier() * _get_prestige_multiplier() + +func reset_runtime_state_for_prestige() -> void: + _is_registered = false + _is_registering = false + cycle_progress_seconds = 0.0 + _remaining_click_cooldown_seconds = 0.0 + _ensure_registered() + _evaluate_generator_unlock_goal(false) + _evaluate_buff_unlock_goals() + visible = is_available_to_player() + func get_manual_click_multiplier() -> float: return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.MANUAL_CLICK_MULTIPLIER) @@ -369,6 +384,21 @@ func _get_click_cooldown_seconds() -> float: func _grants_click_while_hovering() -> bool: return grants_click_while_hovering +func _get_prestige_multiplier() -> float: + var manager: Node = get_node_or_null("/root/PrestigeManager") + if manager == null: + return 1.0 + if not manager.has_method("get_total_multiplier"): + return 1.0 + + var raw_multiplier: Variant = manager.call("get_total_multiplier") + if raw_multiplier is float: + return maxf(raw_multiplier, 0.0) + if raw_multiplier is int: + return maxf(float(raw_multiplier), 0.0) + + return 1.0 + ## Returns this generator's resolved state id. func get_generator_id() -> StringName: _ensure_registered() @@ -522,7 +552,7 @@ func _resolve_generator_id() -> StringName: ## Ensures this generator has a state entry in GameState. func _ensure_registered() -> void: - if _is_registered: + if _is_registered or _is_registering: return if _generator_id == &"": @@ -530,6 +560,7 @@ func _ensure_registered() -> void: if _generator_id == &"": return + _is_registering = true GameState.register_generator( _generator_id, _default_unlocked_state(), @@ -538,6 +569,7 @@ func _ensure_registered() -> void: ) _register_buffs() _is_registered = true + _is_registering = false func _register_buffs() -> void: for buff in get_buffs(): @@ -632,4 +664,4 @@ func _on_area_2d_mouse_exited() -> void: func _on_generated_state_changed(generator_id: StringName, _state: Dictionary) -> void: if generator_id == _generator_id: - visible = true + visible = is_available_to_player() diff --git a/core/prestige/TECH_SPEC.md b/core/prestige/TECH_SPEC.md new file mode 100644 index 0000000..2636952 --- /dev/null +++ b/core/prestige/TECH_SPEC.md @@ -0,0 +1,179 @@ +# Prestige System - Technical Specification + +## Document Status +1. Version: 1.0 +2. Date: 2026-03-21 +3. Scope: Add a configurable prestige reset loop with editor-authored curve math and permanent run multipliers. + +## Problem Statement +The prototype currently has growth, buffs, and unlock goals, but no reset loop that converts late-run progress into long-term power. +Without prestige, the economy has no designed "ladder climb" cycle and balancing eventually relies only on direct cost/production tuning. + +## Design Goals +1. Support a prestige reset that grants a permanent currency based on player progress. +2. Keep prestige math data-driven and editable in the Godot inspector for balancing iteration. +3. Support both major prestige archetypes from Kongregate's analysis: + - Lifetime or max-stat based curves (diminishing gains when resetting at the same point repeatedly). + - Since-reset/run-only curves (same run depth gives similar rewards each reset). +4. Integrate with existing systems (`GameState`, `BigNumber`, generator/buff runtime) without hardcoding one formula. +5. Persist all prestige state in `user://idle_save.json` alongside existing runtime data. + +## Non-Goals (For First Iteration) +1. Multiple prestige currencies. +2. Prestige skill tree/shop UI. +3. Offline simulation changes beyond applying the resulting permanent multiplier. + +## Kongregate-Informed Curve Model +From "The Math of Idle Games, Part III", prestige formulas can be modeled as transformed progress stats with fractional exponents. + +Core variables: +1. `delta_p`: prestige currency gained on reset. +2. `p`: total prestige currency owned. +3. `c_L`: lifetime currency earned. +4. `c_M`: max currency reached in a run. +5. `c_R`: currency earned since last reset. + +Supported formula families for this system: +1. Power curve (generic): `f(x) = scale * (x / threshold)^exponent`. +2. Triangular inverse sqrt (Realm Grinder style): solve `x = threshold * p(p+1)/2` for `p`. +3. Log-like compression (future option, disabled by default). + +Behavior groups: +1. Cumulative-stat formulas (`c_L`, `c_M`) compute target total prestige then grant `target_total - current_total`. +2. Run-only formulas (`c_R`) compute direct gain per run. + +## Proposed Runtime Architecture +1. Add `PrestigeManager` autoload (`core/prestige/prestige_manager.gd`) as the single authority for prestige state and reset flow. +2. Add `PrestigeConfig` resource (`core/prestige/prestige_config.gd`) authored in inspector and loaded from data assets. +3. `GameState` remains owner of currencies/generator state; `PrestigeManager` orchestrates reset and applies permanent multiplier. +4. Generators read the active prestige multiplier through one accessor (`PrestigeManager.get_total_multiplier()`), then multiply existing `run_multiplier` behavior. + +## Data Model + +### PrestigeConfig (Resource) +Editor-authored resource fields (all exported): +1. `id: StringName`. +2. `display_name: String`. +3. `prestige_currency_id: StringName` (logical id, not regular wallet currency). +4. `source_currency_id: StringName` (which gameplay currency powers prestige calculation). +5. `basis: BasisType` enum: + - `LIFETIME_TOTAL` + - `RUN_TOTAL` + - `RUN_MAX` +6. `formula: FormulaType` enum: + - `POWER` + - `TRIANGULAR_INVERSE` +7. `threshold_mantissa: float` and `threshold_exponent: int` (BigNumber-like threshold). +8. `scale: float`. +9. `exponent: float` (used by `POWER`). +10. `flat_bonus: float`. +11. `minimum_gain: int`. +12. `rounding_mode: RoundingMode` enum (`FLOOR`, `ROUND`, `CEIL`). +13. `allow_prestige_without_gain: bool`. +14. Multiplier tuning: + - `multiplier_mode: MultiplierMode` enum (`ADDITIVE`, `MULTIPLICATIVE_POWER`). + - `base_multiplier: float`. + - `multiplier_per_prestige: float`. + - `multiplier_exponent: float`. + +### Prestige Runtime State +Persisted by `PrestigeManager`: +1. `total_prestige_earned: BigNumber`. +2. `current_prestige_unspent: BigNumber`. +3. `prestige_resets_count: int`. +4. `run_start_total_source_currency: BigNumber`. +5. `run_peak_source_currency: BigNumber`. +6. `last_reset_time: int`. + +## Formula Specification + +### 1) Basis Value Extraction +Given configured `source_currency_id`: +1. `LIFETIME_TOTAL`: `basis_value = GameState.get_total_currency_acquired_by_id(source_currency_id)`. +2. `RUN_TOTAL`: `basis_value = lifetime_now - run_start_total_source_currency`. +3. `RUN_MAX`: `basis_value = run_peak_source_currency`. + +### 2) Raw Gain +1. POWER: `raw = scale * pow(basis_value / threshold, exponent) + flat_bonus`. +2. TRIANGULAR_INVERSE: + - `k = basis_value / threshold` + - `target = (sqrt(1 + 8k) - 1) / 2` + - if basis is cumulative (`LIFETIME_TOTAL`, `RUN_MAX` interpreted as cumulative target), `raw = target - total_prestige_earned` + - if basis is run-only, `raw = target` + +### 3) Post-Processing +1. Clamp negative to zero. +2. Apply configured rounding. +3. Enforce `minimum_gain` when basis passed threshold and gain > 0. + +### 4) Multiplier Application +`get_total_multiplier()` returns one of: +1. Additive mode: `base_multiplier + total_prestige_earned * multiplier_per_prestige`. +2. Multiplicative power mode: `base_multiplier * pow(1 + multiplier_per_prestige, pow(total_prestige_earned, multiplier_exponent))`. + +Implementation note: compute large-number powers via `log10` transforms against `BigNumber` to avoid float overflow. + +## Reset Flow +1. Player presses prestige button. +2. `PrestigeManager.calculate_pending_gain()` computes gain from configured curve. +3. If gain is zero and `allow_prestige_without_gain` is false, block reset. +4. On confirm: + - Add gain to `total_prestige_earned` and `current_prestige_unspent`. + - Increment `prestige_resets_count`. + - Reset runtime progression (`GameState` currencies, generator owned counts, generator availability that should restart locked). + - Preserve permanent unlocks only if explicitly marked as persistent in future extensions. + - Reinitialize run-tracking values (`run_start_total_source_currency`, `run_peak_source_currency`). + - Emit `prestige_performed(gain, total_prestige)` signal. + +## GameState Integration +1. Add APIs for reset-safe state initialization (single call from `PrestigeManager` instead of duplicating clear logic). +2. Keep total-acquired currency behavior explicit: + - Option A (recommended): reset current currencies and generator ownership, keep lifetime totals for `LIFETIME_TOTAL` basis. + - Option B: clear totals too and rely on dedicated lifetime accumulator in prestige state. +3. Ensure generator/buff registrations are idempotent after reset (`_ensure_registered` paths already support this pattern). + +## Editor Authoring And Balancing Workflow +1. Create `idles/prestige/primary_prestige.tres` using `PrestigeConfig`. +2. Designers tune only exported fields in inspector (no code edits). +3. Provide presets matching Kongregate article examples: + - Realm Grinder-like: `basis=RUN_MAX`, `formula=TRIANGULAR_INVERSE`, `threshold=1e12`. + - AdCap-like: `basis=LIFETIME_TOTAL`, `formula=POWER`, `scale=150`, `threshold=1e15`, `exponent=0.5`. + - Cookie Clicker-like: `basis=LIFETIME_TOTAL`, `formula=POWER`, `threshold=1e12`, `exponent=0.333333`. + - Egg Inc-like: `basis=RUN_TOTAL`, `formula=POWER`, `threshold=1e6`, `exponent=0.14`. +4. Balance target guidance: + - For sqrt-like curves, doubling prestige generally needs roughly 3x-4x deeper runs. + - For cube-root curves, doubling prestige tends toward ~8x deeper runs. + - For ~1/7 exponent run curves, doubling prestige needs ~128x deeper runs. + +## UI Requirements +1. Prestige panel must display: + - current prestige total + - pending gain (`delta_p`) + - resulting multiplier after reset + - basis stat currently used (`c_L`, `c_R`, or `c_M` equivalent) +2. Reset CTA disabled when pending gain is zero and config disallows zero-gain resets. +3. Confirmation dialog explicitly lists what resets and what is kept. + +## Persistence +1. Extend save payload with `prestige_state` object. +2. Serialize all `BigNumber` values with existing `BigNumber.serialize()` pattern. +3. During load, validate dictionaries and apply defaults if fields are missing. +4. Maintain backward compatibility with pre-prestige saves by creating default prestige state when absent. + +## Validation Plan +1. Headless parse: `"$GODOT_BIN" --headless --path . --quit`. +2. Script checks once implemented: + - `"$GODOT_BIN" --headless --check-only --script res://core/prestige/prestige_manager.gd` + - `"$GODOT_BIN" --headless --check-only --script res://core/prestige/prestige_config.gd` +3. Manual smoke cases: + - same reset point in cumulative mode yields reduced/zero incremental gain. + - same reset point in run-only mode yields roughly same gain. + - multiplier updates production immediately after reset. + - prestige state survives save/load. + +## Implementation Milestones +1. Introduce data classes (`PrestigeConfig`) and one authored `.tres` profile. +2. Implement `PrestigeManager` with gain math + persistence. +3. Add reset integration entrypoint in `GameState`. +4. Wire generator production multiplier to prestige. +5. Add initial prestige UI panel and confirmation flow. diff --git a/core/prestige/prestige_config.gd b/core/prestige/prestige_config.gd new file mode 100644 index 0000000..b7f3915 --- /dev/null +++ b/core/prestige/prestige_config.gd @@ -0,0 +1,77 @@ +class_name PrestigeConfig +extends Resource + +enum BasisType { + LIFETIME_TOTAL, + RUN_TOTAL, + RUN_MAX, +} + +enum FormulaType { + POWER, + TRIANGULAR_INVERSE, +} + +enum RoundingMode { + FLOOR, + ROUND, + CEIL, +} + +enum MultiplierMode { + ADDITIVE, + MULTIPLICATIVE_POWER, +} + +@export var id: StringName = &"primary" +@export var display_name: String = "Ascension" +@export var prestige_currency_id: StringName = &"prestige" +@export var source_currency_id: StringName = &"magic" +@export var basis: BasisType = BasisType.LIFETIME_TOTAL +@export var formula: FormulaType = FormulaType.POWER +@export var threshold_mantissa: float = 1.0 +@export var threshold_exponent: int = 4 +@export var scale: float = 1.0 +@export var exponent: float = 0.5 +@export var flat_bonus: float = 0.0 +@export var minimum_gain: int = 0 +@export var rounding_mode: RoundingMode = RoundingMode.FLOOR +@export var allow_prestige_without_gain: bool = false +@export var multiplier_mode: MultiplierMode = MultiplierMode.ADDITIVE +@export var base_multiplier: float = 1.0 +@export var multiplier_per_prestige: float = 0.05 +@export var multiplier_exponent: float = 1.0 + +func get_threshold() -> BigNumber: + return BigNumber.new(threshold_mantissa, threshold_exponent) + +func is_valid() -> bool: + if String(id).strip_edges().is_empty(): + return false + if String(source_currency_id).strip_edges().is_empty(): + return false + if threshold_mantissa <= 0.0: + return false + if scale < 0.0: + return false + if formula == FormulaType.POWER and exponent <= 0.0: + return false + if base_multiplier < 0.0: + return false + + return true + +func is_cumulative_basis() -> bool: + return basis != BasisType.RUN_TOTAL + +func round_value(value: float) -> float: + if not is_finite(value): + return value + + match rounding_mode: + RoundingMode.CEIL: + return ceilf(value) + RoundingMode.ROUND: + return roundf(value) + _: + return floorf(value) diff --git a/core/prestige/prestige_config.gd.uid b/core/prestige/prestige_config.gd.uid new file mode 100644 index 0000000..b40a639 --- /dev/null +++ b/core/prestige/prestige_config.gd.uid @@ -0,0 +1 @@ +uid://w34t58rpkoui diff --git a/core/prestige/prestige_manager.gd b/core/prestige/prestige_manager.gd new file mode 100644 index 0000000..381ea96 --- /dev/null +++ b/core/prestige/prestige_manager.gd @@ -0,0 +1,416 @@ +extends Node + +signal prestige_state_changed(total_prestige: BigNumber, pending_gain: BigNumber) +signal prestige_performed(gain: BigNumber, total_prestige: 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://idles/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()) + +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) + _reinitialize_generators_after_prestige_reset() + _initialize_run_tracking_from_current_state() + _save_state_to_game_state() + GameState.save_game() + + var total: BigNumber = get_total_prestige() + prestige_performed.emit(gain, total) + prestige_state_changed.emit(total, calculate_pending_gain()) + return true + +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() + prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain()) + +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 _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 diff --git a/core/prestige/prestige_manager.gd.uid b/core/prestige/prestige_manager.gd.uid new file mode 100644 index 0000000..21f6405 --- /dev/null +++ b/core/prestige/prestige_manager.gd.uid @@ -0,0 +1 @@ +uid://srkiu4qe8s2m diff --git a/core/prestige/prestige_panel.gd b/core/prestige/prestige_panel.gd new file mode 100644 index 0000000..2994d04 --- /dev/null +++ b/core/prestige/prestige_panel.gd @@ -0,0 +1,90 @@ +class_name PrestigePanel +extends PanelContainer + +@onready var _total_value: Label = $MarginContainer/VBoxContainer/StatsGrid/TotalValue +@onready var _pending_value: Label = $MarginContainer/VBoxContainer/StatsGrid/PendingValue +@onready var _multiplier_value: Label = $MarginContainer/VBoxContainer/StatsGrid/MultiplierValue +@onready var _basis_label: Label = $MarginContainer/VBoxContainer/StatsGrid/BasisValue +@onready var _reset_button: Button = $MarginContainer/VBoxContainer/Buttons/ResetButton +@onready var _save_button: Button = $MarginContainer/VBoxContainer/Buttons/SaveButton +@onready var _confirm_dialog: ConfirmationDialog = $ConfirmDialog + +var _is_waiting_for_confirm: bool = false + +func _ready() -> void: + var manager: Node = _get_prestige_manager() + if manager != null and manager.has_signal("prestige_state_changed"): + manager.prestige_state_changed.connect(_on_prestige_state_changed) + if manager != null and manager.has_signal("prestige_performed"): + manager.prestige_performed.connect(_on_prestige_performed) + if not GameState.currency_changed.is_connected(_on_currency_changed): + GameState.currency_changed.connect(_on_currency_changed) + + _refresh_ui() + +func _on_reset_button_pressed() -> void: + var manager: Node = _get_prestige_manager() + if manager == null or not manager.has_method("can_prestige"): + return + + if not bool(manager.call("can_prestige")): + return + + var pending: BigNumber = manager.call("calculate_pending_gain") + var source_currency_id: StringName = manager.call("get_source_currency_id") + var source_currency_name: String = GameState.get_currency_name(source_currency_id) + var confirm_message: String = "Reset this run for +%s prestige?\n\nThis resets currencies, owned generators, and buff levels. Lifetime totals and prestige are kept." % pending.to_string_suffix(2) + if not source_currency_name.is_empty(): + confirm_message = "%s\n\nBased on %s progression." % [confirm_message, source_currency_name] + + _confirm_dialog.dialog_text = confirm_message + _is_waiting_for_confirm = true + _confirm_dialog.popup_centered_ratio(0.4) + +func _on_save_button_pressed() -> void: + GameState.save_game() + +func _on_prestige_state_changed(_total: BigNumber, _pending: BigNumber) -> void: + _refresh_ui() + +func _on_prestige_performed(_gain: BigNumber, _total: BigNumber) -> void: + _refresh_ui() + +func _on_currency_changed(_currency_id: StringName, _new_amount: BigNumber) -> void: + _refresh_ui() + +func _on_confirm_dialog_confirmed() -> void: + if not _is_waiting_for_confirm: + return + + _is_waiting_for_confirm = false + var manager: Node = _get_prestige_manager() + if manager == null: + return + + manager.call("perform_prestige") + +func _on_confirm_dialog_canceled() -> void: + _is_waiting_for_confirm = false + +func _refresh_ui() -> void: + var manager: Node = _get_prestige_manager() + if manager == null: + visible = false + return + + visible = true + var total: BigNumber = manager.call("get_total_prestige") + var pending: BigNumber = manager.call("calculate_pending_gain") + var multiplier: float = float(manager.call("get_total_multiplier")) + var basis_text: String = String(manager.call("get_basis_label")) + var basis_value: BigNumber = manager.call("get_basis_value_for_display") + + _total_value.text = total.to_string_suffix(2) + _pending_value.text = pending.to_string_suffix(2) + _multiplier_value.text = "x%.2f" % multiplier + _basis_label.text = "%s (%s)" % [basis_text, basis_value.to_string_suffix(2)] + _reset_button.disabled = not bool(manager.call("can_prestige")) + +func _get_prestige_manager() -> Node: + return get_node_or_null("/root/PrestigeManager") diff --git a/core/prestige/prestige_panel.gd.uid b/core/prestige/prestige_panel.gd.uid new file mode 100644 index 0000000..cd0eb96 --- /dev/null +++ b/core/prestige/prestige_panel.gd.uid @@ -0,0 +1 @@ +uid://dmsmmgtbrul1t diff --git a/core/prestige/prestige_panel.tscn b/core/prestige/prestige_panel.tscn new file mode 100644 index 0000000..57aecd1 --- /dev/null +++ b/core/prestige/prestige_panel.tscn @@ -0,0 +1,81 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://core/prestige/prestige_panel.gd" id="1_7fdwq"] + +[node name="PrestigePanel" type="PanelContainer"] +offset_right = 420.0 +offset_bottom = 206.0 +script = ExtResource("1_7fdwq") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Prestige" + +[node name="StatsGrid" type="GridContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +columns = 2 + +[node name="TotalTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "Total" + +[node name="TotalValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "0" + +[node name="PendingTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "Pending" + +[node name="PendingValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "0" + +[node name="MultiplierTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "Multiplier" + +[node name="MultiplierValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "x1.00" + +[node name="BasisTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "Basis" + +[node name="BasisValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +layout_mode = 2 +text = "-" + +[node name="Buttons" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="ResetButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons"] +layout_mode = 2 +text = "Prestige Reset" + +[node name="SaveButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons"] +layout_mode = 2 +text = "Save" + +[node name="ConfirmDialog" type="ConfirmationDialog" parent="."] +title = "Prestige Reset" +ok_button_text = "Prestige" +dialog_text = "Reset this run?" + +[connection signal="pressed" from="MarginContainer/VBoxContainer/Buttons/ResetButton" to="." method="_on_reset_button_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Buttons/SaveButton" to="." method="_on_save_button_pressed"] +[connection signal="canceled" from="ConfirmDialog" to="." method="_on_confirm_dialog_canceled"] +[connection signal="confirmed" from="ConfirmDialog" to="." method="_on_confirm_dialog_confirmed"] diff --git a/docs/gyms/generator_gym.tscn b/docs/gyms/generator_gym.tscn index 2a78f8a..ef73e49 100644 --- a/docs/gyms/generator_gym.tscn +++ b/docs/gyms/generator_gym.tscn @@ -8,6 +8,7 @@ [ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="5_dl3gy"] [ext_resource type="Resource" uid="uid://df5k58yu1g6rf" path="res://idles/generators/forestry.tres" id="5_u3cug"] [ext_resource type="PackedScene" uid="uid://dirvi76rkoowf" path="res://goals_debug_ui.tscn" id="7_7i63j"] +[ext_resource type="PackedScene" path="res://core/prestige/prestige_panel.tscn" id="9_q5vce"] [node name="GeneratorGym" type="Node2D" unique_id=1219373683] @@ -60,3 +61,10 @@ offset_left = 1253.0 offset_top = 817.0 offset_right = 1913.0 offset_bottom = 1076.0 + +[node name="PrestigePanel" parent="UI" unique_id=401213142 instance=ExtResource("9_q5vce")] +layout_mode = 0 +offset_left = 12.0 +offset_top = 822.0 +offset_right = 432.0 +offset_bottom = 1028.0 diff --git a/generator_container.gd b/generator_container.gd index 4384765..0e9b82d 100644 --- a/generator_container.gd +++ b/generator_container.gd @@ -162,7 +162,7 @@ func _refresh_generator_ui() -> void: if _generator.data != null and can_interact: production_per_second = _generator.data.production_per_second( _generator.owned, - _generator.run_multiplier * _generator.get_automatic_production_multiplier(), + _generator.get_effective_auto_run_multiplier(), _generator.purchased_count ) diff --git a/idles/goals/magic_350k_wood_20k.tres b/idles/goals/magic_350k_wood_20k.tres index 8262d37..227e33a 100644 --- a/idles/goals/magic_350k_wood_20k.tres +++ b/idles/goals/magic_350k_wood_20k.tres @@ -3,14 +3,22 @@ [ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_pj6se"] [ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_lsxf0"] [ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_hyry2"] +[ext_resource type="Resource" uid="uid://cythfovqgqlyh" path="res://idles/currencies/wood.tres" id="3_lsxf0"] [sub_resource type="Resource" id="Resource_tvl3d"] script = ExtResource("1_pj6se") currency = ExtResource("2_lsxf0") -amount_mantissa = 1.3 +amount_mantissa = 3.5 +amount_exponent = 5 + +[sub_resource type="Resource" id="Resource_hyry2"] +script = ExtResource("1_pj6se") +currency = ExtResource("3_lsxf0") +amount_mantissa = 2.0 amount_exponent = 4 +metadata/_custom_type_script = "uid://r4js5eajolio" [resource] script = ExtResource("3_hyry2") -id = &"magic_total_13k" -requirements = Array[ExtResource("1_pj6se")]([SubResource("Resource_tvl3d")]) +id = &"magic_350k_wood_20k" +requirements = Array[ExtResource("1_pj6se")]([SubResource("Resource_tvl3d"), SubResource("Resource_hyry2")]) diff --git a/idles/prestige/primary_prestige.tres b/idles/prestige/primary_prestige.tres new file mode 100644 index 0000000..19a8794 --- /dev/null +++ b/idles/prestige/primary_prestige.tres @@ -0,0 +1,25 @@ +[gd_resource type="Resource" script_class="PrestigeConfig" format=3] + +[ext_resource type="Script" path="res://core/prestige/prestige_config.gd" id="1_3tg3a"] + +[resource] +script = ExtResource("1_3tg3a") +id = &"ascension" +display_name = "Ascension" +prestige_currency_id = &"ascension" +source_currency_id = &"magic" +basis = 0 +formula = 0 +threshold_mantissa = 1.0 +threshold_exponent = 4 +scale = 1.0 +exponent = 0.5 +flat_bonus = 0.0 +minimum_gain = 0 +rounding_mode = 0 +allow_prestige_without_gain = false +multiplier_mode = 0 +base_multiplier = 1.0 +multiplier_per_prestige = 0.05 +multiplier_exponent = 1.0 +metadata/_custom_type_script = "res://core/prestige/prestige_config.gd" diff --git a/project.godot b/project.godot index d6c4d6f..14f2d76 100644 --- a/project.godot +++ b/project.godot @@ -19,6 +19,7 @@ config/icon="res://icon.svg" CurrencyDatabase="*res://core/currency_database.gd" GameState="*uid://d2j7tvlgxr2jp" +PrestigeManager="*res://core/prestige/prestige_manager.gd" [display]