Add prestige graph

This commit is contained in:
2026-05-06 23:37:52 +02:00
parent af62e379ee
commit 1ccb498947
32 changed files with 1537 additions and 279 deletions

View File

@@ -27,6 +27,8 @@ signal research_level_up(research_id: StringName, old_level: int, new_level: int
signal worker_count_changed(currency_id: StringName, new_count: int)
## Emitted when research workers change.
signal research_workers_changed(new_count: int)
## Emitted when an prestige buff node is unlocked.
signal prestige_buff_unlocked(buff_id: StringName)
# ==============================================================================
# EXPORTED REFERENCES
@@ -39,6 +41,8 @@ signal research_workers_changed(new_count: int)
@export var goal_catalogue: GoalCatalogue
## Catalog of all defined research tracks in the game.
@export var research_catalogue: ResearchCatalogue
## Catalog of all prestige buff nodes in the graph.
@export var prestige_buff_catalogue: PrestigeBuffCatalogue
## File path for save/load persistence. Defaults to user directory.
@export var save_file_path: String = "user://level_save.json"
@@ -78,6 +82,8 @@ var research_xp: Dictionary = {}
var research_levels: Dictionary = {}
## Workers assigned to research (total across all tracks).
var research_workers: int = 0
## Ascension buff unlock state: maps node_id (StringName) → bool (true if unlocked). Persists across prestige.
var _prestige_buff_unlocked: Dictionary = {}
## Unix timestamp of last save.
var last_save_time: int = 0
@@ -88,7 +94,7 @@ var last_save_time: int = 0
## JSON key for save format version.
const SAVE_FORMAT_VERSION_KEY: String = "save_format_version"
## Current save format version (7 includes research workers tracking).
const CURRENT_SAVE_FORMAT_VERSION: int = 7
const CURRENT_SAVE_FORMAT_VERSION: int = 8
## JSON key for generator owned count.
const GENERATOR_OWNED_KEY: String = "owned"
## JSON key for generator purchased count.
@@ -128,6 +134,8 @@ const RESEARCH_XP_KEY: String = "research_xp"
const RESEARCH_LEVELS_KEY: String = "research_levels"
## JSON key for research workers section.
const RESEARCH_WORKERS_KEY: String = "research_workers"
## JSON key for prestige buff unlocked array.
const PRESTIGE_BUFF_UNLOCKED_KEY: String = "prestige_buff_unlocked"
## Reference to the PrestigeManager autoload node (set in _ready).
var prestige_manager: PrestigeManager
@@ -139,6 +147,7 @@ func _ready() -> void:
_initialize_currency_maps()
_initialize_catalogues()
_initialize_prestige_buffs()
_load_catalogue_goals()
load_game()
_try_unlock_all_buffs_from_goals()
@@ -184,7 +193,7 @@ func get_currency_name(currency_id: StringName) -> String:
## 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)
var currency: Currency = get_currency_resource(currency_id)
if currency == null:
return null
var raw_icon: Variant = currency.icon
@@ -627,6 +636,7 @@ func save_game() -> void:
RESEARCH_XP_KEY: _serialize_research_xp(),
RESEARCH_LEVELS_KEY: research_levels.duplicate(),
RESEARCH_WORKERS_KEY: research_workers,
PRESTIGE_BUFF_UNLOCKED_KEY: _serialize_prestige_buff_unlocked(),
LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system()
}
@@ -682,11 +692,13 @@ func load_game() -> void:
research_workers = int(parsed_data.get(RESEARCH_WORKERS_KEY, 0))
last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system())
if parsed_data.has(PRESTIGE_BUFF_UNLOCKED_KEY):
_deserialize_prestige_buff_unlocked(parsed_data.get(PRESTIGE_BUFF_UNLOCKED_KEY))
for save_key_variant in parsed_data.keys():
var save_key: String = String(save_key_variant)
if save_key.is_empty():
continue
if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, GOALS_KEY, RESEARCH_XP_KEY, RESEARCH_LEVELS_KEY, RESEARCH_WORKERS_KEY, LAST_SAVE_TIME_KEY]:
if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, GOALS_KEY, RESEARCH_XP_KEY, RESEARCH_LEVELS_KEY, RESEARCH_WORKERS_KEY, PRESTIGE_BUFF_UNLOCKED_KEY, LAST_SAVE_TIME_KEY]:
continue
var section_payload: Variant = parsed_data.get(save_key_variant)
@@ -819,6 +831,16 @@ func _initialize_catalogues() -> void:
continue
register_goal(goal)
## Internal helper: Initializes prestige buff unlock state from the catalogue, filling missing node IDs as false.
func _initialize_prestige_buffs() -> void:
if prestige_buff_catalogue == null:
return
for node in prestige_buff_catalogue.nodes:
if node == null or node.id == &"":
continue
if not _prestige_buff_unlocked.has(node.id):
_prestige_buff_unlocked[node.id] = false
## Internal helper: Loads goal definitions from the catalogue.
func _load_catalogue_goals() -> void:
if goal_catalogue:
@@ -1297,6 +1319,99 @@ func _try_unlock_all_buffs_from_goals() -> void:
# ==============================================================================
# PRESTIGE BUFF API
# ==============================================================================
## Returns the ascension currency ID used for purchasing buff nodes.
func get_prestige_currency_id() -> StringName:
return &"ascension"
## Attempts to purchase and unlock an prestige buff node. Returns true on success.
func purchase_prestige_buff(buff_id: StringName) -> bool:
if not can_purchase_prestige_buff(buff_id):
return false
var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id)
if node == null:
return false
var cost: BigNumber = node.get_cost()
var currency_id: StringName = get_prestige_currency_id()
if not spend_currency_by_id(currency_id, cost):
return false
_prestige_buff_unlocked[buff_id] = true
prestige_buff_unlocked.emit(buff_id)
save_game()
return true
## Checks whether an prestige buff node can be purchased (all parents unlocked, not already unlocked, enough currency).
func can_purchase_prestige_buff(buff_id: StringName) -> bool:
var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id)
if node == null:
return false
if is_prestige_buff_unlocked(buff_id):
return false
if not node.all_parents_unlocked(_prestige_buff_unlocked):
return false
var cost: BigNumber = node.get_cost()
var currency_id: StringName = get_prestige_currency_id()
var balance: BigNumber = get_currency_amount_by_id(currency_id)
return balance.is_greater_than(cost) or balance.is_equal_to(cost)
## Returns true if the given prestige buff node is unlocked.
func is_prestige_buff_unlocked(buff_id: StringName) -> bool:
return bool(_prestige_buff_unlocked.get(buff_id, false))
## Returns the fixed BigNumber cost for an prestige buff node.
func get_prestige_buff_cost(buff_id: StringName) -> BigNumber:
var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id)
if node == null:
return BigNumber.from_float(0.0)
return node.get_cost()
## Computes the combined multiplier from all unlocked prestige buff nodes matching the given effect type.
## [param target_id] filters to nodes targeting this ID (or global nodes when empty).
func get_prestige_buff_multiplier(effect_type: int, target_id: StringName = &"") -> float:
var multiplier: float = 1.0
for node_id in _prestige_buff_unlocked.keys():
if not bool(_prestige_buff_unlocked.get(node_id, false)):
continue
var node: PrestigeBuffNode = _get_prestige_buff_node(node_id)
if node == null:
continue
if node.effect_type != effect_type:
continue
if target_id != &"" and node.target_id != &"" and node.target_id != target_id:
continue
multiplier *= node.effect_value
return multiplier
## Returns all prestige buff nodes whose prerequisites are met but are not yet unlocked.
func get_available_prestige_buffs() -> Array[PrestigeBuffNode]:
if prestige_buff_catalogue == null:
return []
var available: Array[PrestigeBuffNode] = []
for node in prestige_buff_catalogue.nodes:
if node == null or node.id == &"":
continue
if is_prestige_buff_unlocked(node.id):
continue
if node.all_parents_unlocked(_prestige_buff_unlocked):
available.append(node)
return available
## Returns the PrestigeBuffNode resource for a given ID, or null if not found.
func _get_prestige_buff_node(buff_id: StringName) -> PrestigeBuffNode:
if prestige_buff_catalogue == null:
return null
return prestige_buff_catalogue.get_node_by_id(buff_id)
# ==============================================================================
# PERSISTENCE HELPERS
# ==============================================================================
@@ -1394,3 +1509,26 @@ func _deserialize_research_xp(raw: Variant) -> Dictionary:
if serialized is Dictionary:
result[research_id] = BigNumber.deserialize(serialized)
return result
## Serializes prestige buff unlock state as an array of unlocked node IDs.
func _serialize_prestige_buff_unlocked() -> Array:
var unlocked: Array = []
for node_id in _prestige_buff_unlocked.keys():
if _prestige_buff_unlocked.get(node_id, false):
unlocked.append(node_id)
return unlocked
## Deserializes prestige buff unlock state from an array of unlocked node ID strings, then fills missing catalogue entries.
func _deserialize_prestige_buff_unlocked(raw: Variant) -> void:
_prestige_buff_unlocked.clear()
if raw is Array:
for element in raw:
var node_id: StringName = StringName(String(element))
if node_id != &"":
_prestige_buff_unlocked[node_id] = true
_initialize_prestige_buffs()
# Notify tiles that were waiting for state to load
for node_id in _prestige_buff_unlocked.keys():
if _prestige_buff_unlocked.get(node_id, false):
prestige_buff_unlocked.emit(node_id)