From 623ade6291adcede59313ebbc9080a6c1280023c Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Sun, 19 Apr 2026 10:57:42 +0200 Subject: [PATCH] Minor fixes --- core/generator/currency_generator.gd | 7 +- core/generator/generator_container.gd | 14 - core/level_game_state.gd | 496 ++++++++++++-------------- 3 files changed, 234 insertions(+), 283 deletions(-) diff --git a/core/generator/currency_generator.gd b/core/generator/currency_generator.gd index 8d1370f..f914ce3 100644 --- a/core/generator/currency_generator.gd +++ b/core/generator/currency_generator.gd @@ -680,12 +680,7 @@ func _register_buffs() -> void: var starts_unlocked: bool = buff.unlocked or persisted_level > 0 game_state.set_buff_unlocked(buff_id, starts_unlocked) - _try_unlock_buff_from_goal(buff) - -func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void: - if game_state == null: - return - game_state._try_unlock_buff_from_goal(buff) + game_state.try_unlock_buff_from_goal(buff) func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void: if game_state == null: diff --git a/core/generator/generator_container.gd b/core/generator/generator_container.gd index 3aec810..9b6787e 100644 --- a/core/generator/generator_container.gd +++ b/core/generator/generator_container.gd @@ -41,8 +41,6 @@ func _ready() -> void: if _generator.game_state: _generator.game_state.currency_changed.connect(_on_currency_changed) _generator.game_state.generator_state_changed.connect(_on_generator_state_changed) - _generator.game_state.generator_buff_level_changed.connect(_on_generator_buff_level_changed) - _generator.game_state.generator_buff_unlocked_changed.connect(_on_generator_buff_unlocked_changed) _name_label.text = _generator.data.name if _generator.data != null else "Generator" _build_buff_rows() @@ -71,18 +69,6 @@ func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) - visible = true _refresh_generator_ui() -func _on_generator_buff_level_changed(generator_id: StringName, _buff_id: StringName, _new_level: int) -> void: - if generator_id != _generator_id: - return - - _refresh_generator_ui() - -func _on_generator_buff_unlocked_changed(generator_id: StringName, _buff_id: StringName, _unlocked: bool) -> void: - if generator_id != _generator_id: - return - - _refresh_generator_ui() - func _on_buy_buff_pressed(buff_id: StringName) -> void: _generator.buy_buff(buff_id) _refresh_generator_ui() diff --git a/core/level_game_state.gd b/core/level_game_state.gd index ab1612c..b837b62 100644 --- a/core/level_game_state.gd +++ b/core/level_game_state.gd @@ -1,82 +1,130 @@ +## Central state authority for the idle game. Manages currencies, generators, buffs, goals, research, and persistence. +## Acts as the single source of truth for all gameplay state, emitting signals for UI and other systems to react. class_name LevelGameState extends Node -# ========================================== +# ============================================================================== # SIGNALS -# ========================================== +# ============================================================================== +## Emitted when any currency balance changes. [param currency_id] identifies the currency, [param new_amount] is the updated balance. signal currency_changed(currency_id: StringName, new_amount: BigNumber) +## Emitted when a generator's state changes (owned count, unlocked status, etc.). 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) + +## Emitted when a global buff level changes. signal buff_level_changed(buff_id: StringName, new_level: int) +## Emitted when a global buff unlock state changes. signal buff_unlocked_changed(buff_id: StringName, unlocked: bool) +## Emitted when a goal is completed. signal goal_completed(goal_id: StringName) +## Emitted when goal progress changes (for UI progress bars). signal goal_progress_changed(goal_id: StringName, progress: float) +## Emitted when research XP changes. signal research_xp_changed(research_id: StringName, new_xp: BigNumber) +## Emitted when a research track levels up. signal research_level_up(research_id: StringName, old_level: int, new_level: int) -# ========================================== +# ============================================================================== # EXPORTED REFERENCES -# ========================================== +# ============================================================================== +## Catalog of all defined currencies in the game. @export var currency_catalogue: CurrencyCatalogue +## Catalog of all defined buffs in the game. @export var buff_catalogue: BuffCatalogue +## Catalog of all defined goals in the game. @export var goal_catalogue: GoalCatalogue +## Catalog of all defined research tracks in the game. @export var research_catalogue: ResearchCatalogue +## File path for save/load persistence. Defaults to user directory. @export var save_file_path: String = "user://level_save.json" -# ========================================== +# ============================================================================== # STATE VARIABLES -# ========================================== +# ============================================================================== +## Current balance for each currency (resets on prestige). var _current_currency: Dictionary = {} +## Total currency acquired this run (resets on prestige). var _total_currency_acquired: Dictionary = {} +## All-time currency acquired (never resets, persists across prestige). var _all_time_currency_acquired: Dictionary = {} +## Generator states (owned count, unlocked status, availability). var generator_states: Dictionary = {} -var generator_buff_levels: Dictionary = {} -var generator_buff_unlocked: Dictionary = {} + +## External save data sections for other systems (e.g., PrestigeManager). var _external_save_sections: Dictionary = {} +## Global buff definitions by ID. var _buff_definitions: Dictionary = {} +## Global buff levels (shared across all targets). var _buff_levels: Dictionary = {} +## Global buff unlock states. var _buff_unlocked: Dictionary = {} +## Global buff active states (true when unlocked and active). var _buff_active: Dictionary = {} +## Goal definitions by ID. var _goal_definitions: Dictionary = {} +## Goal completion state (true if completed). var _goal_completed: Dictionary = {} +## Research XP by research ID (BigNumber). var research_xp: Dictionary = {} +## Research levels by research ID. var research_levels: Dictionary = {} +## Unix timestamp of last save. var last_save_time: int = 0 -# ========================================== -# Save / Load -# ========================================== +# ============================================================================== +# SAVE / LOAD CONSTANTS +# ============================================================================== +## JSON key for save format version. const SAVE_FORMAT_VERSION_KEY: String = "save_format_version" +## Current save format version (5 includes research XP BigNumber serialization). const CURRENT_SAVE_FORMAT_VERSION: int = 5 +## JSON key for generator owned count. const GENERATOR_OWNED_KEY: String = "owned" +## JSON key for generator purchased count. const GENERATOR_PURCHASED_COUNT_KEY: String = "purchased_count" +## JSON key for generator unlocked status. const GENERATOR_UNLOCKED_KEY: String = "unlocked" +## JSON key for generator availability. const GENERATOR_AVAILABLE_KEY: String = "available" +## JSON key for generator states section. 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" + +## JSON key for buff definitions section. const BUFF_DEFINITIONS_KEY: String = "buff_definitions" +## JSON key for buff levels section. const BUFF_LEVELS_KEY: String = "buff_levels" +## JSON key for buff unlocked section. const BUFF_UNLOCKED_KEY: String = "buff_unlocked" +## JSON key for buff active section. const BUFF_ACTIVE_KEY: String = "buff_active" +## JSON key for goals section. const GOALS_KEY: String = "goals" +## JSON key for goal completed array. const GOAL_COMPLETED_KEY: String = "completed" +## JSON key for currencies section. const CURRENCIES_KEY: String = "currencies" +## JSON key for current currency balance. const CURRENT_KEY: String = "current" +## JSON key for total currency acquired. const TOTAL_KEY: String = "total" +## JSON key for all-time currency acquired. const ALL_TIME_KEY: String = "all_time" +## JSON key for last save timestamp. const LAST_SAVE_TIME_KEY: String = "last_save_time" +## JSON key for research XP section. const RESEARCH_XP_KEY: String = "research_xp" +## JSON key for research levels section. const RESEARCH_LEVELS_KEY: String = "research_levels" +## Reference to the PrestigeManager autoload node (set in _ready). var prestige_manager: PrestigeManager +## Called when the node enters the scene tree. Initializes all game systems, loads saved data, and restores state. func _ready() -> void: prestige_manager = find_child("PrestigeManager") assert(prestige_manager != null) @@ -88,29 +136,36 @@ func _ready() -> void: _try_unlock_all_buffs_from_goals() _initialize_research() -# ========================================== -# STATE MODIFIERS -# ========================================== +#region currency + +# ============================================================================== +# CURRENCY API +# ============================================================================== +## Returns the normalized ID from a Currency resource. func get_currency_id(currency: Resource) -> StringName: return _normalize_currency_id(currency.id if currency else &"") +## Parses and normalizes a currency ID from raw input (handles special cases like "gem" -> "gems"). func parse_currency_id(raw_currency: Variant) -> StringName: var currency_text: String = String(raw_currency).to_lower().strip_edges() if currency_text == "gem": currency_text = "gems" return _normalize_currency_id(StringName(currency_text)) +## Checks if a currency ID is known in the catalogue. func is_known_currency_id(currency_id: StringName) -> bool: if not currency_catalogue: return false var ids: Array[StringName] = currency_catalogue.get_all_ids() return ids.has(_normalize_currency_id(currency_id)) +## Returns the Currency resource for a given ID, or null if not found. func get_currency_resource(currency_id: StringName) -> Currency: if not currency_catalogue: return null return currency_catalogue.get_currency_by_id(_normalize_currency_id(currency_id)) +## Returns the display name for a currency ID, falling back to a humanized ID if not found. func get_currency_name(currency_id: StringName) -> String: var currency: Currency = get_currency_resource(currency_id) if currency != null: @@ -119,6 +174,7 @@ func get_currency_name(currency_id: StringName) -> String: return display_name return _humanize_currency_id(currency_id) +## Returns the icon texture for a currency ID, or null if not found. func get_currency_icon(currency_id: StringName) -> Texture2D: var currency: Resource = get_currency_resource(currency_id) if currency == null: @@ -128,6 +184,7 @@ func get_currency_icon(currency_id: StringName) -> Texture2D: return raw_icon return null +## Adds currency from a Currency resource. Emits [signal currency_changed] on success. func add_currency(currency: Resource, amount: BigNumber) -> void: var currency_id: StringName = get_currency_id(currency) if currency_id == &"": @@ -135,6 +192,7 @@ func add_currency(currency: Resource, amount: BigNumber) -> void: return add_currency_by_id(currency_id, amount) +## Spends currency from a Currency resource. Returns false if insufficient funds. func spend_currency(currency: Resource, cost: BigNumber) -> bool: var currency_id: StringName = get_currency_id(currency) if currency_id == &"": @@ -142,6 +200,7 @@ func spend_currency(currency: Resource, cost: BigNumber) -> bool: return false return spend_currency_by_id(currency_id, cost) +## Adds currency by ID. Updates current, total, and all-time tracking. Emits [signal currency_changed]. 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 == &"": @@ -155,6 +214,7 @@ func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void: current.add_in_place(amount) currency_changed.emit(normalized_currency_id, current) +## Spends currency by ID. Returns true if successful, false if insufficient funds. Emits [signal currency_changed]. 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 == &"": @@ -168,24 +228,38 @@ func spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool: return true return false +## Returns the current balance for a Currency resource. func get_currency_amount(currency: Resource) -> BigNumber: return get_currency_amount_by_id(get_currency_id(currency)) +## Returns the current balance for a currency ID. func get_currency_amount_by_id(currency_id: StringName) -> BigNumber: return _get_current_currency_ref(currency_id) +## Returns the total acquired this run for a Currency resource. func get_total_currency_acquired(currency: Resource) -> BigNumber: return get_total_currency_acquired_by_id(get_currency_id(currency)) +## Returns the total acquired this run for a currency ID (resets on prestige). func get_total_currency_acquired_by_id(currency_id: StringName) -> BigNumber: return _get_total_currency_ref(currency_id) +## Returns the all-time total for a Currency resource. func get_all_time_currency_acquired(currency: Resource) -> BigNumber: return get_all_time_currency_acquired_by_id(get_currency_id(currency)) +## Returns the all-time total for a currency ID (never resets). func get_all_time_currency_acquired_by_id(currency_id: StringName) -> BigNumber: return _get_all_time_currency_ref(currency_id) +#endregion + +#region generator +# ============================================================================== +# GENERATOR STATE API +# ============================================================================== +## Registers a generator with the game state. Creates default state if not exists, sanitizes existing state otherwise. +## Emits [signal generator_state_changed] on registration or update. 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(): @@ -202,13 +276,16 @@ func register_generator(generator_id: StringName, unlocked: bool = false, availa generator_states[key] = new_state generator_state_changed.emit(StringName(key), new_state.duplicate(true)) +## Returns a copy of the full state dictionary for a generator. func get_generator_state(generator_id: StringName) -> Dictionary: return _get_generator_state(generator_id).duplicate(true) +## Returns the owned count for a generator. func get_generator_owned(generator_id: StringName) -> int: var state: Dictionary = _get_generator_state(generator_id) return int(state.get(GENERATOR_OWNED_KEY, 0)) +## Sets the owned count for a generator. Also updates purchased_count to be at least the new owned value. 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) @@ -219,10 +296,12 @@ func set_generator_owned(generator_id: StringName, value: int) -> void: state[GENERATOR_PURCHASED_COUNT_KEY] = maxi(int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)), next_owned) _set_generator_state(generator_id, state) +## Returns the total purchased count for a generator (lifetime purchases, never decreases). 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)) +## Sets the purchased count for a generator. Ensures it never drops below owned count. 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)) @@ -233,10 +312,12 @@ func set_generator_purchased_count(generator_id: StringName, value: int) -> void state[GENERATOR_PURCHASED_COUNT_KEY] = next_purchased _set_generator_state(generator_id, state) +## Returns true if a generator is unlocked (visible and accessible to player). func is_generator_unlocked(generator_id: StringName) -> bool: var state: Dictionary = _get_generator_state(generator_id) return bool(state.get(GENERATOR_UNLOCKED_KEY, false)) +## Sets the unlocked state for a generator. Emits [signal generator_state_changed]. 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: @@ -245,10 +326,12 @@ func set_generator_unlocked(generator_id: StringName, value: bool) -> void: state[GENERATOR_UNLOCKED_KEY] = value _set_generator_state(generator_id, state) +## Returns true if a generator is available (may differ from unlocked for goal-locked generators). func is_generator_available(generator_id: StringName) -> bool: var state: Dictionary = _get_generator_state(generator_id) return bool(state.get(GENERATOR_AVAILABLE_KEY, false)) +## Sets the availability state for a generator. Emits [signal generator_state_changed]. 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: @@ -257,98 +340,14 @@ func set_generator_available(generator_id: StringName, value: bool) -> void: 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 +#endregion - 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) +#region buff +# ============================================================================== +# BUFF API +# ============================================================================== +## Registers a global buff definition for runtime lookup. Initializes level, unlock, and active state. func register_buff(buff: GeneratorBuffData) -> void: if buff == null: return @@ -365,9 +364,11 @@ func register_buff(buff: GeneratorBuffData) -> void: if not _buff_active.has(buff_id): _buff_active[buff_id] = false +## Returns the buff definition for a given ID, or null if not registered. func get_buff(buff_id: StringName) -> GeneratorBuffData: return _buff_definitions.get(buff_id, null) +## Returns all registered global buffs as an array. func get_all_buffs() -> Array[GeneratorBuffData]: var result: Array[GeneratorBuffData] = [] for buff in _buff_definitions.values(): @@ -375,9 +376,11 @@ func get_all_buffs() -> Array[GeneratorBuffData]: result.append(buff) return result +## Returns the current level of a global buff (0 if not registered). func get_buff_level(buff_id: StringName) -> int: return _buff_levels.get(buff_id, 0) +## Sets the level of a global buff. Emits [signal buff_level_changed]. func set_buff_level(buff_id: StringName, level: int) -> void: if level < 0: level = 0 @@ -389,9 +392,11 @@ func set_buff_level(buff_id: StringName, level: int) -> void: _buff_levels[buff_id] = level buff_level_changed.emit(buff_id, level) +## Returns true if a global buff is unlocked. func is_buff_unlocked(buff_id: StringName) -> bool: return _buff_unlocked.get(buff_id, false) +## Sets the unlock state for a global buff. Emits [signal buff_unlocked_changed] and activates the buff if unlocked. func set_buff_unlocked(buff_id: StringName, unlocked: bool) -> void: if bool(_buff_unlocked.get(buff_id, false)) == unlocked: return @@ -402,15 +407,18 @@ func set_buff_unlocked(buff_id: StringName, unlocked: bool) -> void: if unlocked and not _buff_active.get(buff_id, false): _buff_active[buff_id] = true +## Returns true if a global buff is currently active (unlocked and applied). func is_buff_active(buff_id: StringName) -> bool: return _buff_active.get(buff_id, false) +## Returns the list of generators targeted by a global buff. 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() +## Returns all buffs that affect a specific generator. func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]: if buff_catalogue: return buff_catalogue.get_buffs_for_generator(generator_id) @@ -421,6 +429,7 @@ func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffDat result.append(buff) return result +## Calculates the effective production multiplier for a generator by combining all active buffs of the specified kind. func get_effective_multiplier(generator_id: StringName, kind: int) -> float: var multiplier: float = 1.0 for buff in get_buffs_for_generator(generator_id): @@ -439,42 +448,35 @@ func get_effective_multiplier(generator_id: StringName, kind: int) -> float: multiplier *= buff_multiplier return multiplier +#endregion +#region research +# ============================================================================== +# RESEARCH API +# ============================================================================== +## Initializes all research tracks from the catalogue on game start. func _initialize_research() -> void: if research_catalogue: for research in research_catalogue.get_all_research(): register_research(research.id) -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) - +## Registers a research track, initializing XP and level storage. func register_research(research_id: StringName) -> void: if not research_xp.has(research_id): research_xp[research_id] = BigNumber.new(0.0, 0) if not research_levels.has(research_id): research_levels[research_id] = 0 +## Returns the current XP for a research track. func get_research_xp(research_id: StringName) -> BigNumber: var val: Variant = research_xp.get(research_id, BigNumber.new(0.0, 0)) return val as BigNumber +## Returns the current level for a research track. func get_research_level(research_id: StringName) -> int: return research_levels.get(research_id, 0) +## Returns the production multiplier provided by a research track at its current level. func get_research_multiplier(research_id: StringName) -> float: var level: int = get_research_level(research_id) var research: ResearchData = _get_research_data(research_id) @@ -482,6 +484,7 @@ func get_research_multiplier(research_id: StringName) -> float: return 1.0 return research.get_multiplier_for_level(level) +## Adds XP to a research track, applying buff modifiers and emitting signals on level up. func add_research_xp(research_id: StringName, xp: BigNumber) -> void: var actual_xp: BigNumber = ResearchBuffCalculator.apply_buffs(self, research_id, xp) @@ -499,11 +502,18 @@ func add_research_xp(research_id: StringName, xp: BigNumber) -> void: research_xp_changed.emit(research_id, research_xp[research_id]) +## Retrieves the ResearchData resource for a given ID from the catalogue. func _get_research_data(research_id: StringName) -> ResearchData: if research_catalogue == null: return null return research_catalogue.get_research_by_id(research_id) +#endregion + +# ============================================================================== +# EXTERNAL SAVE DATA API +# ============================================================================== +## Stores external save data from other systems (e.g., PrestigeManager). Creates or overwrites the section. func set_external_save_data(section_key: StringName, payload: Dictionary) -> void: var key: String = String(section_key).strip_edges() if key.is_empty(): @@ -511,6 +521,7 @@ func set_external_save_data(section_key: StringName, payload: Dictionary) -> voi _external_save_sections[key] = payload.duplicate(true) +## Retrieves a saved data section by key. Returns empty dict if not found. func get_external_save_data(section_key: StringName) -> Dictionary: var key: String = String(section_key).strip_edges() if key.is_empty(): @@ -522,6 +533,10 @@ func get_external_save_data(section_key: StringName) -> Dictionary: return {} +# ============================================================================== +# PRESTIGE RESET API +# ============================================================================== +## Resets all progress for a prestige reset. Optionally resets total currency, preserves specified currencies, and controls signal emission. func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signals: bool = true, preserve_currency_ids: Array[StringName] = []) -> void: var currency_ids: Array[StringName] = _collect_currency_ids_for_save() for currency_id in currency_ids: @@ -532,8 +547,6 @@ func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signal _set_total_currency(currency_id, BigNumber.from_float(0.0)) generator_states.clear() - generator_buff_levels.clear() - generator_buff_unlocked.clear() for goal_id in _goal_definitions.keys(): _goal_completed[goal_id] = false @@ -551,6 +564,7 @@ func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signal for currency_id in currency_ids: currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) +## Internal helper: Emits currency_changed signals for all currencies to refresh UI after a prestige load. 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)) @@ -558,6 +572,7 @@ func emit_currency_changed_for_all() -> void: # ========================================== # PERSISTENCE (Save/Load) # ========================================== +## Saves all game state to disk as JSON. func save_game() -> void: if save_file_path.is_empty(): push_warning("LevelGameState: save_file_path is empty; skipping save.") @@ -593,6 +608,7 @@ func save_game() -> void: else: push_error("LevelGameState: Failed to open save file: %s" % save_file_path) +## Loads all game state from disk JSON. func load_game() -> void: if not FileAccess.file_exists(save_file_path): return @@ -636,6 +652,7 @@ func load_game() -> void: if section_payload is Dictionary: _external_save_sections[save_key] = section_payload.duplicate(true) +## Internal helper: Loads currency balances from deserialized save data. func _load_currency_balances(raw_currencies: Variant) -> void: if not (raw_currencies is Dictionary): return @@ -658,8 +675,9 @@ func _load_currency_balances(raw_currencies: Variant) -> void: _set_total_currency(currency_id, total_amount) _set_all_time_currency(currency_id, all_time_amount) +## Internal helper: Deserializes total currency from save data with validation. func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) -> BigNumber: - var minimum_total: BigNumber = _copy_big_number(current_amount) + var minimum_total: BigNumber = BigNumber.new(current_amount.mantissa, current_amount.exponent) if minimum_total.mantissa < 0.0: minimum_total = BigNumber.from_float(0.0) @@ -673,8 +691,9 @@ func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) return minimum_total return parsed_total +## Internal helper: Deserializes all-time currency from save data with validation. func _deserialize_all_time_currency(raw_all_time: Variant, total_amount: BigNumber) -> BigNumber: - var minimum_all_time: BigNumber = _copy_big_number(total_amount) + var minimum_all_time: BigNumber = BigNumber.new(total_amount.mantissa, total_amount.exponent) if minimum_all_time.mantissa < 0.0: minimum_all_time = BigNumber.from_float(0.0) @@ -688,9 +707,7 @@ func _deserialize_all_time_currency(raw_all_time: Variant, total_amount: BigNumb return minimum_all_time return parsed_all_time -func _copy_big_number(value: BigNumber) -> BigNumber: - return BigNumber.new(value.mantissa, value.exponent) - +## Internal helper: Serializes all currency balances for saving. func _serialize_currency_balances() -> Dictionary: var result: Dictionary = {} @@ -707,6 +724,7 @@ func _serialize_currency_balances() -> Dictionary: return result +## Internal helper: Collects all currency IDs for serialization. func _collect_currency_ids_for_save() -> Array[StringName]: var ids: Array[StringName] = [] @@ -731,6 +749,7 @@ func _collect_currency_ids_for_save() -> Array[StringName]: return ids +## Internal helper: Initializes all currency maps from the catalogue. func _initialize_currency_maps() -> void: if currency_catalogue: for currency_id in currency_catalogue.get_all_ids(): @@ -740,6 +759,7 @@ func _initialize_currency_maps() -> void: _set_total_currency(currency_id, BigNumber.from_float(0.0)) _set_all_time_currency(currency_id, BigNumber.from_float(0.0)) +## Internal helper: Initializes all catalogues (buffs, goals). func _initialize_catalogues() -> void: if currency_catalogue: for currency in currency_catalogue.currencies: @@ -758,6 +778,7 @@ func _initialize_catalogues() -> void: continue register_goal(goal) +## Internal helper: Loads goal definitions from the catalogue. func _load_catalogue_goals() -> void: if goal_catalogue: for goal in goal_catalogue.goals: @@ -770,6 +791,10 @@ func _load_catalogue_goals() -> void: _goal_definitions[goal.id] = goal _goal_completed[goal.id] = false +# ============================================================================== +# GOAL EVALUATION HELPERS +# ============================================================================== +## Internal helper: Checks if a goal requirement's currency amount has been reached. func is_goal_requirement_met(requirement: GoalRequirementData) -> bool: if requirement == null: return false @@ -787,6 +812,7 @@ func is_goal_requirement_met(requirement: GoalRequirementData) -> bool: return not total_amount.is_less_than(required_amount) +## Internal helper: Returns progress (0.0-1.0) for a goal requirement using logarithmic scaling for large numbers. func get_goal_requirement_progress(requirement: GoalRequirementData) -> float: if requirement == null: return 0.0 @@ -820,6 +846,7 @@ func get_goal_requirement_progress(requirement: GoalRequirementData) -> float: var progress: float = current_log / required_log return clampf(progress, 0.0, 1.0) +## Internal helper: Returns a human-readable summary string for a goal requirement (amount + currency name). func get_goal_requirement_summary(requirement: GoalRequirementData) -> String: if requirement == null: return "" @@ -835,6 +862,7 @@ func get_goal_requirement_summary(requirement: GoalRequirementData) -> String: var currency_name: String = get_currency_name(currency_id) return "%s %s" % [requirement.get_amount().to_string_suffix(2), currency_name] +## Internal helper: Returns true if all requirements for a goal are met. func is_goal_met(goal: GoalData) -> bool: if goal == null: return false @@ -845,6 +873,7 @@ func is_goal_met(goal: GoalData) -> bool: return true +## Internal helper: Returns the overall progress (0.0-1.0) for a goal based on its slowest requirement. func get_goal_progress(goal: GoalData) -> float: if goal == null: return 0.0 @@ -864,12 +893,14 @@ func get_goal_progress(goal: GoalData) -> float: return min_progress +## Internal helper: Normalizes a currency ID (lowercase, trimmed). 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) +## Internal helper: Gets or creates a reference to the current currency balance. func _get_current_currency_ref(currency_id: StringName) -> BigNumber: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": @@ -883,6 +914,7 @@ func _get_current_currency_ref(currency_id: StringName) -> BigNumber: _current_currency[normalized_currency_id] = fallback return fallback +## Internal helper: Gets or creates a reference to the total currency acquired this run. func _get_total_currency_ref(currency_id: StringName) -> BigNumber: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": @@ -896,6 +928,7 @@ func _get_total_currency_ref(currency_id: StringName) -> BigNumber: _total_currency_acquired[normalized_currency_id] = fallback return fallback +## Internal helper: Gets or creates a reference to the all-time currency acquired. func _get_all_time_currency_ref(currency_id: StringName) -> BigNumber: var normalized_currency_id: StringName = _normalize_currency_id(currency_id) if normalized_currency_id == &"": @@ -909,24 +942,31 @@ func _get_all_time_currency_ref(currency_id: StringName) -> BigNumber: _all_time_currency_acquired[normalized_currency_id] = fallback return fallback +## Internal helper: Sets the current currency balance. 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) + _current_currency[normalized_currency_id] = BigNumber.new(value.mantissa, value.exponent) +## Internal helper: Sets the total currency acquired this run. 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) + _total_currency_acquired[normalized_currency_id] = BigNumber.new(value.mantissa, value.exponent) +## Internal helper: Sets the all-time currency acquired. 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) + _all_time_currency_acquired[normalized_currency_id] = BigNumber.new(value.mantissa, value.exponent) +# ============================================================================== +# GENERATOR STATE HELPERS +# ============================================================================== +## Internal helper: Creates a default generator state dictionary with the specified values. func _default_generator_state(unlocked: bool, available: bool, owned: int = 0) -> Dictionary: var owned_value: int = _to_non_negative_int(owned, 0) return { @@ -936,6 +976,7 @@ func _default_generator_state(unlocked: bool, available: bool, owned: int = 0) - GENERATOR_AVAILABLE_KEY: available, } +## Internal helper: Gets or creates the state dictionary for a generator, ensuring it exists and is valid. func _get_generator_state(generator_id: StringName) -> Dictionary: var key: String = String(generator_id) if key.is_empty(): @@ -958,6 +999,7 @@ func _get_generator_state(generator_id: StringName) -> Dictionary: generator_states[key] = fallback return fallback +## Internal helper: Updates a generator's state dictionary and emits the change signal. func _set_generator_state(generator_id: StringName, state: Dictionary) -> void: var key: String = String(generator_id) if key.is_empty(): @@ -970,62 +1012,10 @@ func _set_generator_state(generator_id: StringName, state: Dictionary) -> void: 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 - - 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 - +# ============================================================================== +# GENERATOR SERIALIZATION HELPERS +# ============================================================================== +## Internal helper: Serializes all generator states for saving. func _serialize_generator_states() -> Dictionary: var result: Dictionary = {} for key_variant in generator_states.keys(): @@ -1038,6 +1028,7 @@ func _serialize_generator_states() -> Dictionary: result[key] = _serialize_generator_state_entry(value) return result +## Internal helper: Deserializes generator states from save data. func _deserialize_generator_states(raw_value: Variant) -> Dictionary: var result: Dictionary = {} if not (raw_value is Dictionary): @@ -1053,76 +1044,8 @@ func _deserialize_generator_states(raw_value: Variant) -> 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 +## Internal helper: Serializes a single generator state entry for saving. 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) @@ -1139,6 +1062,7 @@ func _serialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: return serialized_state +## Internal helper: Deserializes a single generator state entry from save data. 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) @@ -1155,6 +1079,7 @@ func _deserialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: return parsed_state +## Internal helper: Sanitizes a generator state dictionary with default values. 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) @@ -1167,6 +1092,7 @@ func _sanitize_generator_state(raw_state: Dictionary, default_unlocked: bool, de GENERATOR_AVAILABLE_KEY: _to_bool(raw_state.get(GENERATOR_AVAILABLE_KEY, default_available), default_available), } +## Internal helper: Converts a value to a non-negative integer. func _to_non_negative_int(value: Variant, fallback: int = 0) -> int: if value is int: return maxi(value, 0) @@ -1174,17 +1100,20 @@ func _to_non_negative_int(value: Variant, fallback: int = 0) -> int: return maxi(int(value), 0) return maxi(fallback, 0) +## Internal helper: Converts a value to a boolean with a fallback. func _to_bool(value: Variant, fallback: bool) -> bool: if value is bool: return value return fallback +## Internal helper: Serializes all global buff levels for saving. func _serialize_buff_levels() -> Dictionary: var result: Dictionary = {} for buff_id in _buff_levels.keys(): result[buff_id] = _buff_levels[buff_id] return result +## Internal helper: Serializes all global buff unlock states for saving. func _serialize_buff_unlocked() -> Dictionary: var result: Dictionary = {} for buff_id in _buff_unlocked.keys(): @@ -1192,6 +1121,7 @@ func _serialize_buff_unlocked() -> Dictionary: result[buff_id] = true return result +## Internal helper: Serializes all global buff active states for saving. func _serialize_buff_active() -> Dictionary: var result: Dictionary = {} for buff_id in _buff_active.keys(): @@ -1199,6 +1129,7 @@ func _serialize_buff_active() -> Dictionary: result[buff_id] = true return result +## Internal helper: Deserializes all global buff states from save data. func _deserialize_buff_states(parsed_data: Dictionary) -> void: _buff_levels.clear() _buff_unlocked.clear() @@ -1230,9 +1161,10 @@ func _deserialize_buff_states(parsed_data: Dictionary) -> void: if not key.is_empty(): _buff_active[key] = true -# ========================================== -# GOALS -# ========================================== +# ============================================================================== +# GOALS API +# ============================================================================== +## Registers a goal definition and initializes its completion state. func register_goal(goal: GoalData) -> void: if goal == null: return @@ -1245,9 +1177,11 @@ func register_goal(goal: GoalData) -> void: _goal_definitions[goal_id] = goal _goal_completed[goal_id] = false +## Returns the goal definition for a given ID, or null if not registered. func get_goal(goal_id: StringName) -> GoalData: return _goal_definitions.get(goal_id, null) +## Returns all registered goals as an array. func get_all_goals() -> Array[GoalData]: var result: Array[GoalData] = [] for goal in _goal_definitions.values(): @@ -1255,15 +1189,30 @@ func get_all_goals() -> Array[GoalData]: result.append(goal) return result +## Returns true if a goal has been completed. func is_goal_completed(goal_id: StringName) -> bool: return _goal_completed.get(goal_id, false) +## Returns the progress (0.0-1.0) for a goal by ID. func get_goal_progress_by_id(goal_id: StringName) -> float: var goal: GoalData = get_goal(goal_id) if goal == null: return 0.0 return get_goal_progress(goal) +## Unlocks a single buff if its unlock goal is completed. +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) + +## Evaluates all goals for completion and unlocks buffs from completed goals. Emits [signal goal_progress_changed]. func evaluate_all_goals() -> void: for goal_id in _goal_completed.keys(): if not _goal_completed[goal_id]: @@ -1273,6 +1222,7 @@ func evaluate_all_goals() -> void: goal_progress_changed.emit(&"", 0.0) +## Internal helper: Attempts to complete a goal if its requirements are met and unlock behavior allows auto-completion. func _try_complete_goal(goal_id: StringName) -> void: var goal: GoalData = _goal_definitions.get(goal_id, null) if goal == null: @@ -1285,6 +1235,7 @@ func _try_complete_goal(goal_id: StringName) -> void: _goal_completed[goal_id] = true goal_completed.emit(goal_id) +## Internal helper: Manually completes a goal (for MANUAL unlock behavior). Emits [signal goal_completed]. func _complete_goal_manually(goal_id: StringName) -> void: var goal: GoalData = _goal_definitions.get(goal_id, null) if goal == null: @@ -1296,6 +1247,19 @@ func _complete_goal_manually(goal_id: StringName) -> void: _goal_completed[goal_id] = true goal_completed.emit(goal_id) +## Internal helper: Unlocks all buffs from completed goals. +func _try_unlock_all_buffs_from_goals() -> void: + for buff in get_all_buffs(): + if buff == null: + continue + try_unlock_buff_from_goal(buff) + + + +# ============================================================================== +# PERSISTENCE HELPERS +# ============================================================================== +## Serializes goal completion state for saving. func _serialize_goals() -> Dictionary: var result: Dictionary = {} var completed_goals: Array[StringName] = [] @@ -1305,6 +1269,7 @@ func _serialize_goals() -> Dictionary: result[GOAL_COMPLETED_KEY] = completed_goals return result +## Deserializes goal completion state from save data. func _deserialize_goals(raw_value: Variant) -> void: _goal_completed.clear() @@ -1322,6 +1287,7 @@ func _deserialize_goals(raw_value: Variant) -> void: if not _goal_completed.has(goal_id): _goal_completed[goal_id] = false +## Internal helper: Migrates goal completion state from save format version 2 (generator unlock-based) to current format. func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void: _goal_completed.clear() @@ -1343,6 +1309,7 @@ func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void: if not _goal_completed.has(goal_id): _goal_completed[goal_id] = false +## Internal helper: Finds the goal ID that unlocks a specific generator (used in save migration). func _get_unlock_goal_id_for_generator(generator_id: String) -> StringName: if goal_catalogue == null: return &"" @@ -1362,12 +1329,14 @@ func _get_unlock_goal_id_for_generator(generator_id: String) -> StringName: return &"" +## Internal helper: Converts a currency ID to a human-readable display name. func _humanize_currency_id(currency_id: StringName) -> String: var normalized: String = String(_normalize_currency_id(currency_id)) if normalized.is_empty(): return "Unknown" return normalized.replace("_", " ").capitalize() +## Serializes research XP data for saving (includes BigNumber mantissa/exponent). func _serialize_research_xp() -> Dictionary: var result: Dictionary = {} for research_id in research_xp.keys(): @@ -1375,6 +1344,7 @@ func _serialize_research_xp() -> Dictionary: result[research_id] = xp.serialize() return result +## Deserializes research XP data from save data. func _deserialize_research_xp(raw: Variant) -> Dictionary: var result: Dictionary = {} if raw is Dictionary: