Decouple everything from globals

This commit is contained in:
2026-04-10 01:09:57 +02:00
parent b758803afe
commit f671ab3419
56 changed files with 993 additions and 1291 deletions

24
core/buff_catalogue.gd Normal file
View File

@@ -0,0 +1,24 @@
class_name BuffCatalogue
extends Resource
@export var buffs: Array[GeneratorBuffData] = []
func get_buff_by_id(id: StringName) -> GeneratorBuffData:
for buff in buffs:
if buff.id == id:
return buff
return null
func get_all_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for buff in buffs:
if buff.id:
ids.append(buff.id)
return ids
func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]:
var result: Array[GeneratorBuffData] = []
for buff in buffs:
if buff.targets_generator(generator_id):
result.append(buff)
return result

View File

@@ -0,0 +1 @@
uid://ctc5yjlnvi0ok

View File

@@ -1,47 +0,0 @@
extends Node
## Auto-discovers and registers all buff resources from res://docs/gyms/buffs/
const BUFF_DIRECTORY_PATH: String = "res://docs/gyms/buffs"
var _buff_by_id: Dictionary = {}
var _initialized: bool = false
func _ready() -> void:
_initialize_buff_catalog()
func _initialize_buff_catalog() -> void:
if _initialized:
return
_buff_by_id.clear()
var dir: DirAccess = DirAccess.open(BUFF_DIRECTORY_PATH)
if dir == null:
push_warning("Unable to open buff directory: %s" % BUFF_DIRECTORY_PATH)
_initialized = true
return
dir.list_dir_begin()
var entry_name: String = dir.get_next()
while not entry_name.is_empty():
if not dir.current_is_dir() and entry_name.ends_with(".tres"):
var path: String = "%s/%s" % [BUFF_DIRECTORY_PATH, entry_name]
var buff: GeneratorBuffData = load(path) as GeneratorBuffData
if buff != null:
_buff_by_id[buff.id] = buff
GameState.register_buff(buff)
entry_name = dir.get_next()
dir.list_dir_end()
_initialized = true
print("BuffDatabase initialized with %d buffs" % _buff_by_id.size())
func get_all_buffs() -> Array[GeneratorBuffData]:
var result: Array[GeneratorBuffData] = []
for buff in _buff_by_id.values():
if buff is GeneratorBuffData:
result.append(buff)
return result
func get_buff(buff_id: StringName) -> GeneratorBuffData:
return _buff_by_id.get(buff_id, null)

View File

@@ -1 +0,0 @@
uid://cjm87u4xh0718

View File

@@ -0,0 +1,17 @@
class_name CurrencyCatalogue
extends Resource
@export var currencies: Array[Currency] = []
func get_currency_by_id(id: StringName) -> Currency:
for currency in currencies:
if currency.id == id:
return currency
return null
func get_all_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for currency in currencies:
if currency.id != &"":
ids.append(currency.id)
return ids

View File

@@ -0,0 +1 @@
uid://621tus0uvbrd

View File

@@ -1,164 +0,0 @@
extends Node
#const GOLD_CURRENCY_ID: StringName = &"gold"
#const GEMS_CURRENCY_ID: StringName = &"gems"
#const LEGACY_CURRENCY_INDEX_TO_ID: Dictionary = {
#0: GOLD_CURRENCY_ID,
#1: GEMS_CURRENCY_ID,
#}
const CURRENCY_DIRECTORY_PATH: String = "res://docs/gyms/currencies"
const CURRENCY_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
var _currency_by_id: Dictionary = {}
var _catalog_initialized: bool = false
func _ready() -> void:
_initialize_currency_catalog()
func get_known_currencies() -> Array[Currency]:
_initialize_currency_catalog_if_needed()
var result: Array[Currency] = []
for raw_currency in _currency_by_id.values():
var currency: Currency = raw_currency as Currency
if currency != null:
result.append(currency)
return result
func get_known_currency_ids() -> Array[StringName]:
_initialize_currency_catalog_if_needed()
var ids: Array[StringName] = []
for raw_currency_id in _currency_by_id.keys():
var currency_id: StringName = _normalize_currency_id(StringName(String(raw_currency_id)))
if currency_id == &"":
continue
ids.append(currency_id)
return ids
func get_currency_id(currency: Currency) -> StringName:
if currency == null:
return &""
var raw_id: Variant = currency.id
if raw_id is StringName:
return _normalize_currency_id(raw_id)
return _normalize_currency_id(StringName(String(raw_id)))
func parse_currency_id(raw_currency: Variant) -> StringName:
#if raw_currency is int:
#return currency_id_from_legacy_index(raw_currency)
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))
#func currency_id_from_legacy_index(currency_index: int) -> StringName:
#return LEGACY_CURRENCY_INDEX_TO_ID.get(currency_index, &"")
func is_known_currency_id(currency_id: StringName) -> bool:
_initialize_currency_catalog_if_needed()
var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
if normalized_currency_id == &"":
return false
return _currency_by_id.has(normalized_currency_id)
func get_currency_resource(currency_id: StringName) -> Currency:
_initialize_currency_catalog_if_needed()
var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
if normalized_currency_id == &"":
return null
var raw_currency: Variant = _currency_by_id.get(normalized_currency_id)
if raw_currency is Currency:
return raw_currency
return null
func get_currency_name(currency_id: StringName) -> String:
var currency: Currency = get_currency_resource(currency_id)
if currency != null:
var display_name: String = String(currency.display_name).strip_edges()
if not display_name.is_empty():
return display_name
return _humanize_currency_id(currency_id)
func get_currency_icon(currency_id: StringName) -> Texture2D:
var currency: Resource = get_currency_resource(currency_id)
if currency == null:
return null
var raw_icon: Variant = currency.icon
if raw_icon is Texture2D:
return raw_icon
return null
func _initialize_currency_catalog_if_needed() -> void:
if _catalog_initialized:
return
_initialize_currency_catalog()
func _initialize_currency_catalog() -> void:
_currency_by_id.clear()
_catalog_initialized = true
var currency_resource_paths: PackedStringArray = _discover_currency_resource_paths()
for resource_path in currency_resource_paths:
var loaded_resource: Resource = load(resource_path)
if loaded_resource == null:
push_warning("Failed to load currency resource at '%s'." % resource_path)
continue
if not (loaded_resource is Currency):
continue
var currency_id: StringName = get_currency_id(loaded_resource)
if currency_id == &"":
push_warning("Skipping currency with missing id: %s" % resource_path)
continue
if _currency_by_id.has(currency_id):
push_warning("Duplicate currency id '%s' found at %s. Keeping first occurrence." % [String(currency_id), resource_path])
continue
_currency_by_id[currency_id] = loaded_resource
if _currency_by_id.is_empty():
push_warning("No currency resources discovered under %s. Ensure currency .tres files exist and are included in export presets." % CURRENCY_DIRECTORY_PATH)
func _discover_currency_resource_paths() -> PackedStringArray:
var result: PackedStringArray = []
var directory: DirAccess = DirAccess.open(CURRENCY_DIRECTORY_PATH)
if directory == null:
push_warning("Unable to open currency directory: %s" % CURRENCY_DIRECTORY_PATH)
return result
directory.list_dir_begin()
var entry_name: String = directory.get_next()
while not entry_name.is_empty():
if directory.current_is_dir() or entry_name.begins_with("."):
entry_name = directory.get_next()
continue
var extension: String = entry_name.get_extension().to_lower()
if CURRENCY_RESOURCE_EXTENSIONS.has(extension):
result.append("%s/%s" % [CURRENCY_DIRECTORY_PATH, entry_name])
entry_name = directory.get_next()
directory.list_dir_end()
result.sort()
return result
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 _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()

View File

@@ -1 +0,0 @@
uid://dmv83fnq26xao

View File

@@ -1 +0,0 @@
uid://d2j7tvlgxr2jp

View File

@@ -44,42 +44,45 @@ var _mouse_entered: bool = false
## Time left until next click/hover grant is allowed.
var _remaining_click_cooldown_seconds: float = 0.0
## Reference to LevelGameState instance.
@onready var game_state: LevelGameState = find_parent("LevelGameState")
## Number of currently owned generators.
## Backed by GameState via this generator's resolved id.
## Backed by LevelGameState via this generator's resolved id.
var owned: int:
get:
_ensure_registered()
return GameState.get_generator_owned(_generator_id)
return game_state.get_generator_owned(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_owned(_generator_id, value)
game_state.set_generator_owned(_generator_id, value)
## Lifetime purchased generators (does not decrease if ownership drops).
var purchased_count: int:
get:
_ensure_registered()
return GameState.get_generator_purchased_count(_generator_id)
return game_state.get_generator_purchased_count(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_purchased_count(_generator_id, value)
game_state.set_generator_purchased_count(_generator_id, value)
## Whether this generator is unlocked for the player.
var is_unlocked: bool:
get:
_ensure_registered()
return GameState.is_generator_unlocked(_generator_id)
return game_state.is_generator_unlocked(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_unlocked(_generator_id, value)
game_state.set_generator_unlocked(_generator_id, value)
## Whether this unlocked generator is currently visible/usable to the player.
var is_available: bool:
get:
_ensure_registered()
return GameState.is_generator_available(_generator_id)
return game_state.is_generator_available(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_available(_generator_id, value)
game_state.set_generator_available(_generator_id, value)
## Accumulated elapsed time toward the next automatic production cycle.
var cycle_progress_seconds: float = 0.0
@@ -98,9 +101,10 @@ func _ready() -> void:
_generator_id = _resolve_generator_id()
cycle_progress_seconds = 0.0
GameState.currency_changed.connect(_on_currency_changed)
GameState.generator_state_changed.connect(_on_generated_state_changed)
GameState.goal_completed.connect(_on_goal_completed)
if game_state:
game_state.currency_changed.connect(_on_currency_changed)
game_state.generator_state_changed.connect(_on_generated_state_changed)
game_state.goal_completed.connect(_on_goal_completed)
_ensure_registered()
@@ -220,18 +224,27 @@ func buy_max() -> int:
func get_buffs() -> Array[GeneratorBuffData]:
if data == null:
return []
return GameState.get_buffs_for_generator(_generator_id)
if game_state == null:
return []
return game_state.get_buffs_for_generator(_generator_id)
func get_buff_level(buff_id: StringName) -> int:
_ensure_registered()
return GameState.get_buff_level(buff_id)
if game_state == null:
return 0
return game_state.get_buff_level(buff_id)
func is_buff_unlocked(buff_id: StringName) -> bool:
_ensure_registered()
return GameState.is_buff_unlocked(buff_id)
if game_state == null:
return false
return game_state.is_buff_unlocked(buff_id)
func get_buff_cost(buff_id: StringName) -> BigNumber:
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return BigNumber.from_float(0.0)
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return BigNumber.from_float(0.0)
if not is_buff_unlocked(buff.id):
@@ -241,7 +254,10 @@ func get_buff_cost(buff_id: StringName) -> BigNumber:
return buff.get_cost_for_level(level)
func get_buff_cost_currency_id(buff_id: StringName) -> StringName:
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return &""
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return &""
return _resolve_buff_cost_currency_id(buff)
@@ -250,7 +266,10 @@ func can_buy_buff(buff_id: StringName) -> bool:
if not is_available_to_player():
return false
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return false
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return false
@@ -266,21 +285,24 @@ func can_buy_buff(buff_id: StringName) -> bool:
return false
var cost: BigNumber = buff.get_cost_for_level(current_level)
var current_amount: BigNumber = GameState.get_currency_amount_by_id(cost_currency_id)
var current_amount: BigNumber = game_state.get_currency_amount_by_id(cost_currency_id)
return current_amount.is_greater_than(cost) or current_amount.is_equal_to(cost)
func buy_buff(buff_id: StringName) -> bool:
if not is_available_to_player():
return false
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return false
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return false
if not GameState.is_buff_unlocked(buff.id):
if not game_state.is_buff_unlocked(buff.id):
return false
var current_level: int = GameState.get_buff_level(buff.id)
var current_level: int = game_state.get_buff_level(buff.id)
if not buff.can_purchase_next_level(current_level):
return false
@@ -294,19 +316,22 @@ func buy_buff(buff_id: StringName) -> bool:
return false
var next_level: int = current_level + 1
GameState.set_buff_level(buff.id, next_level)
game_state.set_buff_level(buff.id, next_level)
_apply_resource_purchase_buff(buff, next_level)
buff_purchased.emit(buff.id, next_level, cost, cost_currency_id)
return true
func is_buff_maxed(buff_id: StringName) -> bool:
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return true
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return true
if not GameState.is_buff_unlocked(buff.id):
if not game_state.is_buff_unlocked(buff.id):
return false
return not buff.can_purchase_next_level(GameState.get_buff_level(buff.id))
return not buff.can_purchase_next_level(game_state.get_buff_level(buff.id))
func get_automatic_production_multiplier() -> float:
return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER)
@@ -384,13 +409,18 @@ func _grants_click_while_hovering() -> bool:
return grants_click_while_hovering
func _get_prestige_multiplier() -> float:
var manager: PrestigeManager = get_node_or_null("/root/PrestigeManager")
if manager == null:
if game_state == null:
return 1.0
if not manager.get_total_multiplier():
var parent: Node = get_parent()
if parent == null:
return 1.0
var raw_multiplier: Variant = manager.get_total_multiplier()
var prestige_manager: PrestigeManager = parent.find_child("PrestigeManager", true, false)
if prestige_manager == null:
return 1.0
var raw_multiplier: Variant = prestige_manager.get_total_multiplier()
if raw_multiplier is float:
return maxf(raw_multiplier, 0.0)
if raw_multiplier is int:
@@ -405,14 +435,16 @@ func get_generator_id() -> StringName:
## Returns the configured currency id.
func get_currency_id() -> StringName:
return GameState.get_currency_id(currency)
if game_state == null:
return &""
return game_state.get_currency_id(currency)
func get_purchase_currency_id() -> StringName:
var purchase_currency: Currency = _resolve_purchase_currency()
if purchase_currency == null:
return &""
return GameState.get_currency_id(purchase_currency)
return game_state.get_currency_id(purchase_currency)
## True when the generator is both unlocked and available.
func is_available_to_player() -> bool:
@@ -424,13 +456,17 @@ func _add_currency(amount: BigNumber) -> void:
push_error("CurrencyGenerator '%s' cannot add currency: currency resource is null." % String(name))
return
GameState.add_currency(currency, amount)
if game_state == null:
return
game_state.add_currency(currency, amount)
func _add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void:
if currency_id == &"":
return
GameState.add_currency_by_id(currency_id, amount)
if game_state == null:
return
game_state.add_currency_by_id(currency_id, amount)
## Attempts to spend target currency; returns false when insufficient.
func _spend_currency(cost: BigNumber) -> bool:
@@ -438,25 +474,33 @@ func _spend_currency(cost: BigNumber) -> bool:
push_error("CurrencyGenerator '%s' cannot spend currency: currency resource is null." % String(name))
return false
return GameState.spend_currency(currency, cost)
if game_state == null:
return false
return game_state.spend_currency(currency, cost)
func _spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool:
if currency_id == &"":
return false
return GameState.spend_currency_by_id(currency_id, cost)
if game_state == null:
return false
return game_state.spend_currency_by_id(currency_id, cost)
## Returns the current amount for the configured currency type.
func _get_currency_amount() -> BigNumber:
if currency == null:
return BigNumber.from_float(0.0)
return GameState.get_currency_amount(currency)
if game_state == null:
return BigNumber.from_float(0.0)
return game_state.get_currency_amount(currency)
func _get_currency_amount_by_id(currency_id: StringName) -> BigNumber:
if currency_id == &"":
return BigNumber.from_float(0.0)
return GameState.get_currency_amount_by_id(currency_id)
if game_state == null:
return BigNumber.from_float(0.0)
return game_state.get_currency_amount_by_id(currency_id)
## Safely converts finite float values into BigNumber costs.
func _float_to_big_number(value: float) -> BigNumber:
@@ -484,7 +528,9 @@ func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName:
if cost_currency == null:
return &""
return GameState.get_currency_id(cost_currency)
if game_state == null:
return &""
return game_state.get_currency_id(cost_currency)
func _resolve_purchase_currency() -> Currency:
if data == null:
@@ -500,10 +546,14 @@ func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName:
if target_currency == null:
return &""
return GameState.get_currency_id(target_currency)
if game_state == null:
return &""
return game_state.get_currency_id(target_currency)
func _get_multiplier_for_buff_kind(kind: GeneratorBuffData.BuffKind) -> float:
return GameState.get_effective_multiplier(_generator_id, kind)
if game_state == null:
return 1.0
return game_state.get_effective_multiplier(_generator_id, kind)
func _apply_resource_purchase_buff(buff: GeneratorBuffData, level: int) -> void:
if buff == null:
@@ -533,7 +583,7 @@ func _resolve_generator_id() -> StringName:
return &"generator"
## Ensures this generator has a state entry in GameState.
## Ensures this generator has a state entry in LevelGameState.
func _ensure_registered() -> void:
if _is_registered or _is_registering:
return
@@ -543,8 +593,11 @@ func _ensure_registered() -> void:
if _generator_id == &"":
return
if game_state == null:
return
_is_registering = true
GameState.register_generator(
game_state.register_generator(
_generator_id,
_default_unlocked_state(),
_default_available_state(),
@@ -555,6 +608,9 @@ func _ensure_registered() -> void:
_is_registering = false
func _register_buffs() -> void:
if game_state == null:
return
for buff in get_buffs():
if buff == null:
continue
@@ -564,20 +620,24 @@ func _register_buffs() -> void:
continue
var buff_id: StringName = StringName(buff_id_text)
GameState.register_buff(buff)
game_state.register_buff(buff)
if not GameState.is_buff_unlocked(buff_id):
var persisted_level: int = GameState.get_buff_level(buff_id)
if not game_state.is_buff_unlocked(buff_id):
var persisted_level: int = game_state.get_buff_level(buff_id)
var starts_unlocked: bool = buff.unlocked or persisted_level > 0
GameState.set_buff_unlocked(buff_id, starts_unlocked)
game_state.set_buff_unlocked(buff_id, starts_unlocked)
_try_unlock_buff_from_goal(buff)
func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
GameState._try_unlock_buff_from_goal(buff)
if game_state == null:
return
game_state._try_unlock_buff_from_goal(buff)
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
GameState.evaluate_all_goals()
if game_state == null:
return
game_state.evaluate_all_goals()
func _on_goal_completed(goal_id: StringName) -> void:
if data == null or not data.has_unlock_goal():
@@ -587,10 +647,12 @@ func _on_goal_completed(goal_id: StringName) -> void:
if is_available_to_player():
return
GameState.set_generator_unlocked(_generator_id, true)
GameState.set_generator_available(_generator_id, true)
if game_state == null:
return
game_state.set_generator_unlocked(_generator_id, true)
game_state.set_generator_available(_generator_id, true)
goal_achieved.emit(_generator_id, goal_id)
_on_generated_state_changed(_generator_id, GameState._get_generator_state(_generator_id))
_on_generated_state_changed(_generator_id, game_state._get_generator_state(_generator_id))
## Default unlocked state loaded from generator data.
func _default_unlocked_state() -> bool:

View File

@@ -58,10 +58,10 @@ func get_unlock_goal_amount() -> BigNumber:
if unlock_goal == null:
return BigNumber.from_float(0.0)
var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements()
if valid_requirements.is_empty():
var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements()
if requirements.is_empty():
return BigNumber.from_float(0.0)
var requirement_resource: GoalRequirementData = valid_requirements[0]
var requirement_resource: GoalRequirementData = requirements[0]
if requirement_resource == null:
return BigNumber.from_float(0.0)
@@ -75,10 +75,10 @@ func get_unlock_goal_currency() -> Currency:
if unlock_goal == null:
return null
var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements()
if valid_requirements.is_empty():
var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements()
if requirements.is_empty():
return null
var requirement_resource: GoalRequirementData = valid_requirements[0]
var requirement_resource: GoalRequirementData = requirements[0]
if requirement_resource == null:
return null

View File

@@ -37,11 +37,12 @@ func _ready() -> void:
_generator.purchase_failed.connect(_on_generator_updated)
_generator.buff_purchased.connect(_on_generator_updated)
_generator.buff_purchase_failed.connect(_on_generator_updated)
GameState.currency_changed.connect(_on_currency_changed)
GameState.generator_state_changed.connect(_on_generator_state_changed)
GameState.generator_buff_level_changed.connect(_on_generator_buff_level_changed)
GameState.generator_buff_unlocked_changed.connect(_on_generator_buff_unlocked_changed)
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()
@@ -123,9 +124,9 @@ func _refresh_buff_rows(can_interact: bool) -> void:
if not buff_unlocked:
var locked_hint: String = "Locked"
var unlock_goal_currency: Resource = buff.get_unlock_goal_currency()
if buff.has_unlock_goal() and unlock_goal_currency != null:
var goal_currency_id: StringName = GameState.get_currency_id(unlock_goal_currency)
var currency_name: String = GameState.get_currency_name(goal_currency_id)
if buff.has_unlock_goal() and unlock_goal_currency != null and _generator.game_state:
var goal_currency_id: StringName = _generator.game_state.get_currency_id(unlock_goal_currency)
var currency_name: String = _generator.game_state.get_currency_name(goal_currency_id)
var required_text: String = buff.get_unlock_goal_amount().to_string_suffix(2)
locked_hint = "Unlock at %s %s" % [required_text, currency_name]
@@ -141,7 +142,7 @@ func _refresh_buff_rows(can_interact: bool) -> void:
var cost: BigNumber = _generator.get_buff_cost(buff.id)
var cost_currency_id: StringName = _generator.get_buff_cost_currency_id(buff.id)
var currency_label: String = GameState.get_currency_name(cost_currency_id)
var currency_label: String = _generator.game_state.get_currency_name(cost_currency_id) if _generator.game_state else "Unknown"
var cost_text: String = "%s %s" % [cost.to_string_suffix(2), currency_label]
var can_buy: bool = can_interact and _generator.can_buy_buff(buff.id)
row.tile.set_runtime(level, effect_text, cost_text, can_buy, false)
@@ -161,7 +162,7 @@ func _refresh_generator_ui() -> void:
_owned_value.text = str(_generator.owned)
if can_interact:
var purchase_currency_id: StringName = _generator.get_purchase_currency_id()
var purchase_currency_name: String = GameState.get_currency_name(purchase_currency_id)
var purchase_currency_name: String = _generator.game_state.get_currency_name(purchase_currency_id) if _generator.game_state else "Unknown"
if purchase_currency_name.is_empty():
purchase_currency_name = "Unconfigured"
_next_cost_value.text = "%s %s" % [_generator.get_next_cost().to_string_suffix(2), purchase_currency_name]

17
core/goal_catalogue.gd Normal file
View File

@@ -0,0 +1,17 @@
class_name GoalCatalogue
extends Resource
@export var goals: Array[GoalData] = []
func get_goal_by_id(id: StringName) -> GoalData:
for goal in goals:
if goal.id == id:
return goal
return null
func get_all_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for goal in goals:
if goal.has_id():
ids.append(goal.id)
return ids

View File

@@ -0,0 +1 @@
uid://bfbp4mo8ys5p8

View File

@@ -38,12 +38,13 @@ enum UnlockBehavior {
### Methods
```gdscript
func is_valid() -> bool # Has id and valid requirements
func is_met() -> bool # All requirements satisfied
func get_progress() -> float # 0.0 to 1.0 based on closest requirement
func get_first_requirement_summary() -> String # "100 Gold" format
func has_id() -> bool # Has non-empty id
func is_valid() -> bool # Has valid id and requirements
func get_requirements() -> Array[GoalRequirementData] # Access requirements
```
**Note:** State-dependent methods (`is_met()`, `get_progress()`, `get_summary_text()`) have been moved to `LevelGameState` for proper decoupling.
## GoalRequirementData
Single condition within a goal.
@@ -62,12 +63,13 @@ extends Resource
### Methods
```gdscript
func get_currency() -> Currency # Currency to track
func get_amount() -> BigNumber # Required amount
func is_met() -> bool # Current >= required
func get_progress() -> float # Log-scaled 0.0-1.0
func get_summary_text() -> String # "100 Gold" format
func has_valid_amount() -> bool # Amount is non-negative
```
**Note:** State-dependent methods (`is_met()`, `get_progress()`, `get_summary_text()`) have been moved to `LevelGameState` for proper decoupling.
### Progress Calculation
Uses **logarithmic scaling** for better visual feedback on huge numbers:
@@ -87,18 +89,18 @@ This means reaching 50% progress on a 1e100 goal requires ~1e50 currency.
### Automatic Checking
`GameState` evaluates goals whenever currency changes:
`LevelGameState` evaluates goals whenever currency changes:
```gdscript
func _on_currency_changed(currency_id, new_amount):
GameState.evaluate_all_goals()
game_state.evaluate_all_goals()
```
### Completion Flow
1. Currency updated → signal emitted
2. `GameState.evaluate_all_goals()` called
3. Each uncompleted goal checked via `is_met()`
2. `LevelGameState.evaluate_all_goals()` called
3. Each uncompleted goal checked via `is_goal_met(goal)`
4. If met → `goal_completed` signal emitted
5. Buffs with unlock goals checked → unlocked if goal complete
@@ -110,6 +112,16 @@ Goals control their own unlock behavior via `unlock_behavior`:
Generators with `unlock_goal` automatically unlock when the goal completes.
### Goal Methods in LevelGameState
```gdscript
func is_goal_requirement_met(requirement: GoalRequirementData) -> bool
func get_goal_requirement_progress(requirement: GoalRequirementData) -> float
func get_goal_requirement_summary(requirement: GoalRequirementData) -> String
func is_goal_met(goal: GoalData) -> bool
func get_goal_progress(goal: GoalData) -> float
```
## Usage Example
### Defining a Goal

View File

@@ -13,53 +13,22 @@ enum UnlockBehavior {
func has_id() -> bool:
return not String(id).strip_edges().is_empty()
func get_valid_requirements() -> Array[GoalRequirementData]:
var result: Array[GoalRequirementData] = []
for requirement_resource in requirements:
if requirement_resource == null:
continue
if not bool(requirement_resource.is_valid()):
continue
result.append(requirement_resource)
return result
func get_requirements() -> Array[GoalRequirementData]:
return requirements.duplicate()
func is_valid() -> bool:
if not has_id():
return false
return not get_valid_requirements().is_empty()
func is_met() -> bool:
var valid_requirements: Array[GoalRequirementData] = get_valid_requirements()
if valid_requirements.is_empty():
if requirements.is_empty():
return false
for requirement_resource in valid_requirements:
if not bool(requirement_resource.is_met()):
return false
return true
func get_first_requirement_summary() -> String:
var valid_requirements: Array[GoalRequirementData] = get_valid_requirements()
if valid_requirements.is_empty():
return ""
return String(valid_requirements[0].get_summary_text())
func get_progress() -> float:
var valid_requirements: Array[GoalRequirementData] = get_valid_requirements()
if valid_requirements.is_empty():
return 0.0
var min_progress: float = 1.0
for requirement in valid_requirements:
if requirement == null or not requirement.is_valid():
for req in requirements:
if req == null:
continue
if req.get_currency() == null:
continue
if not req.has_valid_amount():
continue
var requirement_progress: float = requirement.get_progress()
if requirement_progress < min_progress:
min_progress = requirement_progress
return min_progress
return true

View File

@@ -5,63 +5,11 @@ extends Resource
@export var amount_mantissa: float = 0.0
@export var amount_exponent: int = 0
func get_currency_id() -> StringName:
if currency == null:
return &""
return GameState.get_currency_id(currency)
func get_currency() -> Currency:
return currency
func get_amount() -> BigNumber:
return BigNumber.new(amount_mantissa, amount_exponent)
func has_valid_currency() -> bool:
var currency_id: StringName = get_currency_id()
if currency_id == &"":
return false
return GameState.is_known_currency_id(currency_id)
func has_valid_amount() -> bool:
return get_amount().mantissa >= 0.0
func is_valid() -> bool:
return has_valid_currency() and has_valid_amount()
func is_met() -> bool:
if not is_valid():
return false
var total_amount: BigNumber = GameState.get_total_currency_acquired_by_id(get_currency_id())
return not total_amount.is_less_than(get_amount())
func get_summary_text() -> String:
if not is_valid():
return ""
var currency_name: String = GameState.get_currency_name(get_currency_id())
return "%s %s" % [get_amount().to_string_suffix(2), currency_name]
func get_progress() -> float:
if not is_valid():
return 0.0
var current_amount: BigNumber = GameState.get_total_currency_acquired_by_id(get_currency_id())
var required_amount: BigNumber = get_amount()
if required_amount.mantissa <= 0.0:
return 1.0
if current_amount.is_greater_than(required_amount) or current_amount.is_equal_to(required_amount):
return 1.0
if current_amount.mantissa <= 0.0:
return 0.0
var current_log: float = log(maxf(current_amount.mantissa, 0.0001)) / log(10) + float(current_amount.exponent)
var required_log: float = log(maxf(required_amount.mantissa, 0.0001)) / log(10) + float(required_amount.exponent)
if required_log <= 0.0:
return 1.0
var progress: float = current_log / required_log
return clampf(progress, 0.0, 1.0)

View File

@@ -1,5 +1,5 @@
class_name LevelGameState
extends Node
# This script is added as an Autoload named 'GameState'
# ==========================================
# SIGNALS
@@ -13,6 +13,14 @@ signal buff_unlocked_changed(buff_id: StringName, unlocked: bool)
signal goal_completed(goal_id: StringName)
signal goal_progress_changed(goal_id: StringName, progress: float)
# ==========================================
# EXPORTED REFERENCES
# ==========================================
@export var currency_catalogue: CurrencyCatalogue
@export var buff_catalogue: BuffCatalogue
@export var goal_catalogue: GoalCatalogue
@export var save_file_path: String = "user://level_save.json"
# ==========================================
# STATE VARIABLES
# ==========================================
@@ -33,12 +41,11 @@ var _buff_active: Dictionary = {}
var _goal_definitions: Dictionary = {}
var _goal_completed: Dictionary = {}
var last_save_time: int = 0 # Unix timestamp
var last_save_time: int = 0
# ==========================================
# Save / Load
# ==========================================
static var file_name: String = "user://idle_save.json"
const SAVE_FORMAT_VERSION_KEY: String = "save_format_version"
const CURRENT_SAVE_FORMAT_VERSION: int = 3
const GENERATOR_OWNED_KEY: String = "owned"
@@ -62,51 +69,56 @@ const LAST_SAVE_TIME_KEY: String = "last_save_time"
func _ready() -> void:
_initialize_currency_maps()
_initialize_goals()
_initialize_catalogues()
_load_catalogue_goals()
load_game()
_try_unlock_all_buffs_from_goals()
# ==========================================
# STATE MODIFIERS
# ==========================================
#func add_gold(amount: BigNumber) -> void:
#add_currency_by_id(GOLD_CURRENCY_ID, amount)
#
#func spend_gold(cost: BigNumber) -> bool:
#return spend_currency_by_id(GOLD_CURRENCY_ID, cost)
#
#func add_gems(amount: BigNumber) -> void:
#add_currency_by_id(GEMS_CURRENCY_ID, amount)
#
#func spend_gems(cost: BigNumber) -> bool:
#return spend_currency_by_id(GEMS_CURRENCY_ID, cost)
func get_known_currencies() -> Array[Currency]:
return CurrencyDatabase.get_known_currencies()
func get_currency_id(currency: Resource) -> StringName:
return CurrencyDatabase.get_currency_id(currency)
return _normalize_currency_id(currency.id if currency else &"")
func parse_currency_id(raw_currency: Variant) -> StringName:
return CurrencyDatabase.parse_currency_id(raw_currency)
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))
func is_known_currency_id(currency_id: StringName) -> bool:
return CurrencyDatabase.is_known_currency_id(currency_id)
if not currency_catalogue:
return false
var ids: Array[StringName] = currency_catalogue.get_all_ids()
return ids.has(_normalize_currency_id(currency_id))
func get_currency_resource(currency_id: StringName) -> Resource:
return CurrencyDatabase.get_currency_resource(currency_id)
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))
func get_currency_name(currency_id: StringName) -> String:
return CurrencyDatabase.get_currency_name(currency_id)
var currency: Currency = get_currency_resource(currency_id)
if currency != null:
var display_name: String = String(currency.display_name).strip_edges()
if not display_name.is_empty():
return display_name
return _humanize_currency_id(currency_id)
func get_currency_icon(currency_id: StringName) -> Texture2D:
return CurrencyDatabase.get_currency_icon(currency_id)
var currency: Resource = get_currency_resource(currency_id)
if currency == null:
return null
var raw_icon: Variant = currency.icon
if raw_icon is Texture2D:
return raw_icon
return null
func add_currency(currency: Resource, amount: BigNumber) -> void:
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
push_warning("Attempted to add currency with an invalid Currency resource.")
return
add_currency_by_id(currency_id, amount)
func spend_currency(currency: Resource, cost: BigNumber) -> bool:
@@ -114,7 +126,6 @@ func spend_currency(currency: Resource, cost: BigNumber) -> bool:
if currency_id == &"":
push_warning("Attempted to spend currency with an invalid Currency resource.")
return false
return spend_currency_by_id(currency_id, cost)
func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void:
@@ -387,6 +398,9 @@ func get_generators_targeted_by(buff_id: StringName) -> Array[StringName]:
return buff.get_target_generators()
func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]:
if buff_catalogue:
return buff_catalogue.get_buffs_for_generator(generator_id)
var result: Array[GeneratorBuffData] = []
for buff in _buff_definitions.values():
if buff is GeneratorBuffData and buff.targets_generator(generator_id):
@@ -475,6 +489,10 @@ func emit_currency_changed_for_all() -> void:
# PERSISTENCE (Save/Load)
# ==========================================
func save_game() -> void:
if save_file_path.is_empty():
push_warning("LevelGameState: save_file_path is empty; skipping save.")
return
var save_data = {
SAVE_FORMAT_VERSION_KEY: CURRENT_SAVE_FORMAT_VERSION,
CURRENCIES_KEY: _serialize_currency_balances(),
@@ -497,15 +515,17 @@ func save_game() -> void:
save_data[key] = payload
var file: FileAccess = FileAccess.open(file_name, FileAccess.WRITE)
var file: FileAccess = FileAccess.open(save_file_path, FileAccess.WRITE)
if file:
file.store_string(JSON.stringify(save_data))
else:
push_error("LevelGameState: Failed to open save file: %s" % save_file_path)
func load_game() -> void:
if not FileAccess.file_exists(file_name):
if not FileAccess.file_exists(save_file_path):
return
var file: FileAccess = FileAccess.open(file_name, FileAccess.READ)
var file: FileAccess = FileAccess.open(save_file_path, FileAccess.READ)
if file:
var parsed: Variant = JSON.parse_string(file.get_as_text())
if parsed is Dictionary:
@@ -624,21 +644,147 @@ func _collect_currency_ids_for_save() -> Array[StringName]:
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):
continue
ids.append(normalized_currency_id)
if currency_catalogue:
for known_currency_id in currency_catalogue.get_all_ids():
var normalized_currency_id: StringName = _normalize_currency_id(known_currency_id)
if normalized_currency_id == &"" or ids.has(normalized_currency_id):
continue
ids.append(normalized_currency_id)
return ids
func _initialize_currency_maps() -> void:
for currency_id in CurrencyDatabase.get_known_currency_ids():
if currency_id == &"":
if currency_catalogue:
for currency_id in currency_catalogue.get_all_ids():
if currency_id == &"":
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 _initialize_catalogues() -> void:
if currency_catalogue:
for currency in currency_catalogue.currencies:
if currency == null:
continue
if buff_catalogue:
for buff in buff_catalogue.buffs:
if buff == null:
continue
register_buff(buff)
if goal_catalogue:
for goal in goal_catalogue.goals:
if goal == null:
continue
register_goal(goal)
func _load_catalogue_goals() -> void:
if goal_catalogue:
for goal in goal_catalogue.goals:
if goal == null:
continue
if not goal.has_id():
continue
if _goal_definitions.has(goal.id):
continue
_goal_definitions[goal.id] = goal
_goal_completed[goal.id] = false
func is_goal_requirement_met(requirement: GoalRequirementData) -> bool:
if requirement == null:
return false
var currency: Currency = requirement.get_currency()
if currency == null:
return false
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
return false
var total_amount: BigNumber = get_total_currency_acquired_by_id(currency_id)
var required_amount: BigNumber = requirement.get_amount()
return not total_amount.is_less_than(required_amount)
func get_goal_requirement_progress(requirement: GoalRequirementData) -> float:
if requirement == null:
return 0.0
var currency: Currency = requirement.get_currency()
if currency == null:
return 0.0
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
return 0.0
var current_amount: BigNumber = get_total_currency_acquired_by_id(currency_id)
var required_amount: BigNumber = requirement.get_amount()
if required_amount.mantissa <= 0.0:
return 1.0
if current_amount.is_greater_than(required_amount) or current_amount.is_equal_to(required_amount):
return 1.0
if current_amount.mantissa <= 0.0:
return 0.0
var current_log: float = log(maxf(current_amount.mantissa, 0.0001)) / log(10) + float(current_amount.exponent)
var required_log: float = log(maxf(required_amount.mantissa, 0.0001)) / log(10) + float(required_amount.exponent)
if required_log <= 0.0:
return 1.0
var progress: float = current_log / required_log
return clampf(progress, 0.0, 1.0)
func get_goal_requirement_summary(requirement: GoalRequirementData) -> String:
if requirement == null:
return ""
var currency: Currency = requirement.get_currency()
if currency == null:
return ""
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
return ""
var currency_name: String = get_currency_name(currency_id)
return "%s %s" % [requirement.get_amount().to_string_suffix(2), currency_name]
func is_goal_met(goal: GoalData) -> bool:
if goal == null:
return false
for req in goal.get_requirements():
if not is_goal_requirement_met(req):
return false
return true
func get_goal_progress(goal: GoalData) -> float:
if goal == null:
return 0.0
var requirements: Array[GoalRequirementData] = goal.get_requirements()
if requirements.is_empty():
return 0.0
var min_progress: float = 1.0
for req in requirements:
if req == null:
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))
var req_progress: float = get_goal_requirement_progress(req)
if req_progress < min_progress:
min_progress = req_progress
return min_progress
func _normalize_currency_id(currency_id: StringName) -> StringName:
var normalized: String = String(currency_id).to_lower().strip_edges()
@@ -794,7 +940,6 @@ func _sanitize_generator_buff_unlocked(raw_unlocked: Dictionary) -> Dictionary:
if buff_key.is_empty():
continue
# Save format only stores true values, but keep parser tolerant.
var is_unlocked: bool = _to_bool(raw_unlocked.get(buff_key_variant, false), false)
if not is_unlocked:
continue
@@ -1010,57 +1155,6 @@ func _deserialize_buff_states(parsed_data: Dictionary) -> void:
# ==========================================
# GOALS
# ==========================================
func _initialize_goals() -> void:
_goal_definitions.clear()
_goal_completed.clear()
var goal_paths: PackedStringArray = _discover_goal_paths()
for goal_path in goal_paths:
var goal_resource: Resource = load(goal_path)
if goal_resource == null:
push_warning("Failed to load goal resource at '%s'." % goal_path)
continue
if not (goal_resource is GoalData):
continue
var goal: GoalData = goal_resource
if not goal.has_id():
push_warning("Skipping goal with missing id: %s" % goal_path)
continue
if _goal_definitions.has(goal.id):
push_warning("Duplicate goal id '%s' found at %s. Keeping first occurrence." % [String(goal.id), goal_path])
continue
_goal_definitions[goal.id] = goal
_goal_completed[goal.id] = false
func _discover_goal_paths() -> PackedStringArray:
var result: PackedStringArray = []
const GOAL_DIRECTORY_PATH: String = "res://docs/gyms/goals"
const GOAL_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
var directory: DirAccess = DirAccess.open(GOAL_DIRECTORY_PATH)
if directory == null:
push_warning("Unable to open goals directory: %s" % GOAL_DIRECTORY_PATH)
return result
directory.list_dir_begin()
var entry_name: String = directory.get_next()
while not entry_name.is_empty():
if directory.current_is_dir() or entry_name.begins_with("."):
entry_name = directory.get_next()
continue
var extension: String = entry_name.get_extension().to_lower()
if GOAL_RESOURCE_EXTENSIONS.has(extension):
result.append("%s/%s" % [GOAL_DIRECTORY_PATH, entry_name])
entry_name = directory.get_next()
directory.list_dir_end()
result.sort()
return result
func register_goal(goal: GoalData) -> void:
if goal == null:
return
@@ -1086,11 +1180,11 @@ func get_all_goals() -> Array[GoalData]:
func is_goal_completed(goal_id: StringName) -> bool:
return _goal_completed.get(goal_id, false)
func get_goal_progress(goal_id: StringName) -> float:
func get_goal_progress_by_id(goal_id: StringName) -> float:
var goal: GoalData = get_goal(goal_id)
if goal == null:
return 0.0
return goal.get_progress()
return get_goal_progress(goal)
func evaluate_all_goals() -> void:
for goal_id in _goal_completed.keys():
@@ -1098,6 +1192,8 @@ func evaluate_all_goals() -> void:
_try_complete_goal(goal_id)
_try_unlock_all_buffs_from_goals()
goal_progress_changed.emit(&"", 0.0)
func _try_complete_goal(goal_id: StringName) -> void:
var goal: GoalData = _goal_definitions.get(goal_id, null)
@@ -1105,7 +1201,7 @@ func _try_complete_goal(goal_id: StringName) -> void:
return
if _goal_completed[goal_id]:
return
if goal.is_met():
if is_goal_met(goal):
if goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL:
return
_goal_completed[goal_id] = true
@@ -1117,7 +1213,7 @@ func _complete_goal_manually(goal_id: StringName) -> void:
return
if _goal_completed.get(goal_id, false):
return
if not goal.is_met():
if not is_goal_met(goal):
return
_goal_completed[goal_id] = true
goal_completed.emit(goal_id)
@@ -1161,11 +1257,7 @@ func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void:
if not state.get(GENERATOR_UNLOCKED_KEY, false):
continue
var generator_data: CurrencyGeneratorData = _load_generator_data_from_id(gen_id)
if generator_data == null or not generator_data.has_unlock_goal():
continue
var goal_id: StringName = generator_data.get_unlock_goal_id()
var goal_id: StringName = _get_unlock_goal_id_for_generator(gen_id)
if not goal_id.is_empty():
_goal_completed[goal_id] = true
@@ -1173,32 +1265,27 @@ func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void:
if not _goal_completed.has(goal_id):
_goal_completed[goal_id] = false
func _load_generator_data_from_id(generator_id: String) -> CurrencyGeneratorData:
const GENERATORS_DIRECTORY_PATH: String = "res://docs/gyms/generators"
const GENERATOR_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
func _get_unlock_goal_id_for_generator(generator_id: String) -> StringName:
if goal_catalogue == null:
return &""
var directory: DirAccess = DirAccess.open(GENERATORS_DIRECTORY_PATH)
if directory == null:
return null
directory.list_dir_begin()
var entry_name: String = directory.get_next()
while not entry_name.is_empty():
if directory.current_is_dir() or entry_name.begins_with("."):
entry_name = directory.get_next()
for goal in goal_catalogue.goals:
if goal == null or not goal.has_id():
continue
var extension: String = entry_name.get_extension().to_lower()
if GENERATOR_RESOURCE_EXTENSIONS.has(extension):
var generator_path: String = "%s/%s" % [GENERATORS_DIRECTORY_PATH, entry_name]
var generator_resource: Resource = load(generator_path)
if generator_resource is CurrencyGeneratorData:
var gen_data: CurrencyGeneratorData = generator_resource
if String(gen_data.id) == generator_id:
directory.list_dir_end()
return gen_data
entry_name = directory.get_next()
directory.list_dir_end()
var goal_id: StringName = goal.id
var requirements: Array[GoalRequirementData] = goal.get_requirements()
for req in requirements:
if req == null or req.get_currency() == null:
continue
if String(req.get_currency().id) == generator_id:
return goal_id
return null
return &""
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()

View File

@@ -0,0 +1 @@
uid://cv2132o4hi7q3

View File

@@ -1,3 +1,4 @@
class_name PrestigeManager
extends Node
signal prestige_state_changed(total_prestige: BigNumber, pending_gain: BigNumber)
@@ -23,6 +24,7 @@ const MULTIPLIER_MODE_ADDITIVE: int = 0
const MULTIPLIER_MODE_MULTIPLICATIVE_POWER: int = 1
@export var config: Resource
@export var game_state: LevelGameState
var total_prestige_earned: BigNumber = BigNumber.from_float(0.0)
var current_prestige_unspent: BigNumber = BigNumber.from_float(0.0)
@@ -33,13 +35,17 @@ var last_reset_time: int = 0
func _ready() -> void:
if config == null:
config = load("res://docs/gyms/prestige/primary_prestige.tres") as Resource
config = load("res://docs/gyms/tiny_sword/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 game_state == null:
push_warning("PrestigeManager: game_state reference is not set.")
return
var loaded_state: Dictionary = game_state.get_external_save_data(SAVE_KEY)
if loaded_state.is_empty():
_initialize_run_tracking_from_current_state()
else:
@@ -47,7 +53,7 @@ func _ready() -> void:
_validate_loaded_run_tracking()
_save_state_to_game_state()
GameState.currency_changed.connect(_on_currency_changed)
game_state.currency_changed.connect(_on_currency_changed)
prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain())
prestige_threshold_changed.emit(get_next_prestige_threshold())
@@ -134,13 +140,16 @@ func perform_prestige() -> bool:
prestige_resets_count += 1
last_reset_time = Time.get_unix_time_from_system()
GameState.reset_for_prestige(true, false)
if game_state:
game_state.reset_for_prestige(true, false)
_reset_all_buff_levels()
_reinitialize_generators_after_prestige_reset()
GameState.emit_currency_changed_for_all()
if game_state:
game_state.emit_currency_changed_for_all()
_initialize_run_tracking_from_current_state()
_save_state_to_game_state()
GameState.save_game()
if game_state:
game_state.save_game()
var total: BigNumber = get_total_prestige()
var next_threshold: BigNumber = get_next_prestige_threshold()
@@ -261,9 +270,12 @@ func _get_basis_value() -> BigNumber:
if source_currency_id == &"":
return BigNumber.from_float(0.0)
if game_state == null:
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 lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id)
var run_total: BigNumber = lifetime_now.subtract(run_start_total_source_currency)
if run_total.mantissa < 0.0:
return BigNumber.from_float(0.0)
@@ -271,7 +283,7 @@ func _get_basis_value() -> BigNumber:
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))
return _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id))
func _initialize_run_tracking_from_current_state() -> void:
if config == null:
@@ -283,8 +295,13 @@ func _initialize_run_tracking_from_current_state() -> void:
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))
if game_state == null:
run_start_total_source_currency = BigNumber.from_float(0.0)
run_peak_source_currency = BigNumber.from_float(0.0)
return
run_start_total_source_currency = _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id))
run_peak_source_currency = _copy_big_number(game_state.get_currency_amount_by_id(source_currency_id))
_save_state_to_game_state()
func _validate_loaded_run_tracking() -> void:
@@ -297,11 +314,14 @@ func _validate_loaded_run_tracking() -> void:
run_peak_source_currency = BigNumber.from_float(0.0)
return
var current_currency: BigNumber = GameState.get_currency_amount_by_id(source_currency_id)
if game_state == null:
return
var current_currency: BigNumber = game_state.get_currency_amount_by_id(source_currency_id)
if current_currency.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(current_currency)
var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id)
var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id)
if run_start_total_source_currency.is_greater_than(lifetime_now):
run_start_total_source_currency = _copy_big_number(lifetime_now)
@@ -327,12 +347,16 @@ func _deserialize_state(raw_state: Dictionary) -> void:
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())
if game_state:
game_state.set_external_save_data(SAVE_KEY, _serialize_state())
func _reset_all_buff_levels() -> void:
for buff_id in GameState._buff_levels.keys():
GameState.set_buff_level(buff_id, 0)
GameState.set_buff_unlocked(buff_id, false)
if game_state == null:
return
for buff_id in game_state._buff_levels.keys():
game_state.set_buff_level(buff_id, 0)
game_state.set_buff_unlocked(buff_id, false)
func _reinitialize_generators_after_prestige_reset() -> void:
var scene_root: Node = get_tree().current_scene

View File

@@ -1,6 +1,8 @@
class_name PrestigePanel
extends PanelContainer
@export var game_state: LevelGameState
@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
@@ -18,8 +20,8 @@ func _ready() -> void:
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)
if game_state and not game_state.currency_changed.is_connected(_on_currency_changed):
game_state.currency_changed.connect(_on_currency_changed)
_refresh_ui()
@@ -33,7 +35,7 @@ func _on_reset_button_pressed() -> void:
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 source_currency_name: String = game_state.get_currency_name(source_currency_id) if game_state else ""
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]
@@ -43,7 +45,8 @@ func _on_reset_button_pressed() -> void:
_confirm_dialog.popup_centered_ratio(0.4)
func _on_save_button_pressed() -> void:
GameState.save_game()
if game_state:
game_state.save_game()
func _on_prestige_state_changed(_total: BigNumber, _pending: BigNumber) -> void:
_refresh_ui()

View File

@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://dmsmmgtbrul1t" path="res://core/prestige/prestige_panel.gd" id="1_panel"]
[ext_resource type="PackedScene" path="res://core/prestige/prestige_progress_bar.tscn" id="2_7b6yg"]
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/prestige/primary_prestige.tres" id="3_r8h5p"]
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="3_r8h5p"]
[node name="PrestigePanel" type="PanelContainer" unique_id=789062217]
offset_right = 420.0