buffs are now decoupled from generators

This commit is contained in:
2026-03-29 17:50:39 +02:00
parent 85bbe51956
commit f1e08c848e
9 changed files with 604 additions and 73 deletions

47
core/buff_database.gd Normal file
View File

@@ -0,0 +1,47 @@
extends Node
## Auto-discovers and registers all buff resources from res://sandbox/buffs/
const BUFF_DIRECTORY_PATH: String = "res://sandbox/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

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

View File

@@ -8,6 +8,8 @@ signal currency_changed(currency_id: StringName, new_amount: BigNumber)
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)
signal buff_level_changed(buff_id: StringName, new_level: int)
signal buff_unlocked_changed(buff_id: StringName, unlocked: bool)
# ==========================================
# STATE VARIABLES
@@ -21,12 +23,19 @@ var generator_buff_levels: Dictionary = {}
var generator_buff_unlocked: Dictionary = {}
var _external_save_sections: Dictionary = {}
var _buff_definitions: Dictionary = {}
var _buff_levels: Dictionary = {}
var _buff_unlocked: Dictionary = {}
var _buff_active: Dictionary = {}
var last_save_time: int = 0 # Unix timestamp
# ==========================================
# 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 = 2
const GENERATOR_OWNED_KEY: String = "owned"
const GENERATOR_PURCHASED_COUNT_KEY: String = "purchased_count"
const GENERATOR_UNLOCKED_KEY: String = "unlocked"
@@ -34,6 +43,10 @@ const GENERATOR_AVAILABLE_KEY: String = "available"
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"
const BUFF_DEFINITIONS_KEY: String = "buff_definitions"
const BUFF_LEVELS_KEY: String = "buff_levels"
const BUFF_UNLOCKED_KEY: String = "buff_unlocked"
const BUFF_ACTIVE_KEY: String = "buff_active"
const CURRENCIES_KEY: String = "currencies"
const CURRENT_KEY: String = "current"
const TOTAL_KEY: String = "total"
@@ -303,6 +316,104 @@ func get_generator_buff_unlocked(generator_id: StringName) -> Dictionary:
func get_generator_buff_levels(generator_id: StringName) -> Dictionary:
return _get_generator_buff_levels_ref(generator_id).duplicate(true)
func register_buff(buff: GeneratorBuffData) -> void:
if buff == null:
return
var buff_id: StringName = buff.id
if buff_id == &"":
push_warning("Attempted to register buff with invalid id")
return
_buff_definitions[buff_id] = buff
if not _buff_levels.has(buff_id):
_buff_levels[buff_id] = 0
if not _buff_unlocked.has(buff_id):
_buff_unlocked[buff_id] = false
if not _buff_active.has(buff_id):
_buff_active[buff_id] = false
func get_buff(buff_id: StringName) -> GeneratorBuffData:
return _buff_definitions.get(buff_id, null)
func get_all_buffs() -> Array[GeneratorBuffData]:
var result: Array[GeneratorBuffData] = []
for buff in _buff_definitions.values():
if buff is GeneratorBuffData:
result.append(buff)
return result
func get_buff_level(buff_id: StringName) -> int:
return _buff_levels.get(buff_id, 0)
func set_buff_level(buff_id: StringName, level: int) -> void:
if level < 0:
level = 0
var current_level: int = _buff_levels.get(buff_id, 0)
if current_level == level:
return
_buff_levels[buff_id] = level
buff_level_changed.emit(buff_id, level)
func is_buff_unlocked(buff_id: StringName) -> bool:
return _buff_unlocked.get(buff_id, false)
func set_buff_unlocked(buff_id: StringName, unlocked: bool) -> void:
if bool(_buff_unlocked.get(buff_id, false)) == unlocked:
return
_buff_unlocked[buff_id] = unlocked
buff_unlocked_changed.emit(buff_id, unlocked)
if unlocked and not _buff_active.get(buff_id, false):
_buff_active[buff_id] = true
func is_buff_active(buff_id: StringName) -> bool:
return _buff_active.get(buff_id, false)
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()
func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]:
var result: Array[GeneratorBuffData] = []
for buff in _buff_definitions.values():
if buff is GeneratorBuffData and buff.targets_generator(generator_id):
result.append(buff)
return result
func get_effective_multiplier(generator_id: StringName, kind: int) -> float:
var multiplier: float = 1.0
for buff in get_buffs_for_generator(generator_id):
if buff == null:
continue
if not is_buff_active(buff.id):
continue
if buff.kind != kind:
continue
var level: int = get_buff_level(buff.id)
if level <= 0:
continue
var buff_multiplier: float = buff.get_effect_multiplier(level)
multiplier *= buff_multiplier
return multiplier
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
if buff.is_unlock_goal_met():
set_buff_unlocked(buff.id, true)
func set_external_save_data(section_key: StringName, payload: Dictionary) -> void:
var key: String = String(section_key).strip_edges()
if key.is_empty():
@@ -347,10 +458,12 @@ func emit_currency_changed_for_all() -> void:
# ==========================================
func save_game() -> void:
var save_data = {
SAVE_FORMAT_VERSION_KEY: CURRENT_SAVE_FORMAT_VERSION,
CURRENCIES_KEY: _serialize_currency_balances(),
GENERATOR_STATES_KEY: _serialize_generator_states(),
GENERATOR_BUFF_LEVELS_KEY: _serialize_generator_buff_levels(),
GENERATOR_BUFF_UNLOCKED_KEY: _serialize_generator_buff_unlocked(),
BUFF_LEVELS_KEY: _serialize_buff_levels(),
BUFF_UNLOCKED_KEY: _serialize_buff_unlocked(),
BUFF_ACTIVE_KEY: _serialize_buff_active(),
LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system()
}
@@ -378,19 +491,24 @@ func load_game() -> void:
var parsed: Variant = JSON.parse_string(file.get_as_text())
if parsed is Dictionary:
var parsed_data: Dictionary = parsed
var version: int = int(parsed_data.get(SAVE_FORMAT_VERSION_KEY, 1))
if version < 2:
push_error("Save file version %d too old. Please start fresh." % version)
return
_external_save_sections.clear()
if parsed_data.has(CURRENCIES_KEY):
_load_currency_balances(parsed_data.get(CURRENCIES_KEY, {}))
generator_states = _deserialize_generator_states(parsed_data.get(GENERATOR_STATES_KEY, {}))
generator_buff_levels = _deserialize_generator_buff_levels(parsed_data.get(GENERATOR_BUFF_LEVELS_KEY, {}))
generator_buff_unlocked = _deserialize_generator_buff_unlocked(parsed_data.get(GENERATOR_BUFF_UNLOCKED_KEY, {}))
_deserialize_buff_states(parsed_data)
last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system())
for save_key_variant in parsed_data.keys():
var save_key: String = String(save_key_variant)
if save_key.is_empty():
continue
if save_key in [CURRENCIES_KEY, GENERATOR_STATES_KEY, GENERATOR_BUFF_LEVELS_KEY, GENERATOR_BUFF_UNLOCKED_KEY, LAST_SAVE_TIME_KEY]:
if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, LAST_SAVE_TIME_KEY]:
continue
var section_payload: Variant = parsed_data.get(save_key_variant)
@@ -808,3 +926,54 @@ func _to_bool(value: Variant, fallback: bool) -> bool:
if value is bool:
return value
return fallback
func _serialize_buff_levels() -> Dictionary:
var result: Dictionary = {}
for buff_id in _buff_levels.keys():
result[buff_id] = _buff_levels[buff_id]
return result
func _serialize_buff_unlocked() -> Dictionary:
var result: Dictionary = {}
for buff_id in _buff_unlocked.keys():
if _buff_unlocked[buff_id]:
result[buff_id] = true
return result
func _serialize_buff_active() -> Dictionary:
var result: Dictionary = {}
for buff_id in _buff_active.keys():
if _buff_active[buff_id]:
result[buff_id] = true
return result
func _deserialize_buff_states(parsed_data: Dictionary) -> void:
_buff_levels.clear()
_buff_unlocked.clear()
_buff_active.clear()
if parsed_data.has(BUFF_LEVELS_KEY):
var raw_levels: Variant = parsed_data.get(BUFF_LEVELS_KEY)
if raw_levels is Dictionary:
for key_variant in raw_levels.keys():
var key: String = String(key_variant)
if not key.is_empty():
var value: Variant = raw_levels.get(key_variant)
if value is int:
_buff_levels[key] = value
if parsed_data.has(BUFF_UNLOCKED_KEY):
var raw_unlocked: Variant = parsed_data.get(BUFF_UNLOCKED_KEY)
if raw_unlocked is Dictionary:
for key_variant in raw_unlocked.keys():
var key: String = String(key_variant)
if not key.is_empty():
_buff_unlocked[key] = true
if parsed_data.has(BUFF_ACTIVE_KEY):
var raw_active: Variant = parsed_data.get(BUFF_ACTIVE_KEY)
if raw_active is Dictionary:
for key_variant in raw_active.keys():
var key: String = String(key_variant)
if not key.is_empty():
_buff_active[key] = true

View File

@@ -219,18 +219,18 @@ func buy_max() -> int:
func get_buffs() -> Array[GeneratorBuffData]:
if data == null:
return []
return data.buffs
return GameState.get_buffs_for_generator(_generator_id)
func get_buff_level(buff_id: StringName) -> int:
_ensure_registered()
return GameState.get_generator_buff_level(_generator_id, buff_id)
return GameState.get_buff_level(buff_id)
func is_buff_unlocked(buff_id: StringName) -> bool:
_ensure_registered()
return GameState.is_generator_buff_unlocked(_generator_id, buff_id)
return GameState.is_buff_unlocked(buff_id)
func get_buff_cost(buff_id: StringName) -> BigNumber:
var buff: GeneratorBuffData = _find_buff(buff_id)
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if buff == null:
return BigNumber.from_float(0.0)
if not is_buff_unlocked(buff.id):
@@ -240,7 +240,7 @@ 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 = _find_buff(buff_id)
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if buff == null:
return &""
return _resolve_buff_cost_currency_id(buff)
@@ -249,7 +249,7 @@ func can_buy_buff(buff_id: StringName) -> bool:
if not is_available_to_player():
return false
var buff: GeneratorBuffData = _find_buff(buff_id)
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if buff == null:
return false
@@ -272,14 +272,14 @@ func buy_buff(buff_id: StringName) -> bool:
if not is_available_to_player():
return false
var buff: GeneratorBuffData = _find_buff(buff_id)
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if buff == null:
return false
if not is_buff_unlocked(buff.id):
if not GameState.is_buff_unlocked(buff.id):
return false
var current_level: int = get_buff_level(buff.id)
var current_level: int = GameState.get_buff_level(buff.id)
if not buff.can_purchase_next_level(current_level):
return false
@@ -293,19 +293,19 @@ func buy_buff(buff_id: StringName) -> bool:
return false
var next_level: int = current_level + 1
GameState.set_generator_buff_level(_generator_id, buff.id, next_level)
GameState.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 = _find_buff(buff_id)
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if buff == null:
return true
if not is_buff_unlocked(buff.id):
if not GameState.is_buff_unlocked(buff.id):
return false
return not buff.can_purchase_next_level(get_buff_level(buff.id))
return not buff.can_purchase_next_level(GameState.get_buff_level(buff.id))
func get_automatic_production_multiplier() -> float:
return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER)
@@ -477,11 +477,6 @@ func _big_number_to_float(value: BigNumber) -> float:
return 0.0
return value.mantissa * pow(10.0, float(value.exponent))
func _find_buff(buff_id: StringName) -> GeneratorBuffData:
if data == null:
return null
return data.find_buff(buff_id)
func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName:
if buff == null:
return &""
@@ -509,18 +504,7 @@ func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName:
return GameState.get_currency_id(target_currency)
func _get_multiplier_for_buff_kind(kind: GeneratorBuffData.BuffKind) -> float:
var multiplier: float = 1.0
for buff in get_buffs():
if buff == null:
continue
if buff.kind != kind:
continue
if not is_buff_unlocked(buff.id):
continue
multiplier *= buff.get_effect_multiplier(get_buff_level(buff.id))
return maxf(multiplier, 0.0)
return GameState.get_effective_multiplier(_generator_id, kind)
func _apply_resource_purchase_buff(buff: GeneratorBuffData, level: int) -> void:
if buff == null:
@@ -581,34 +565,21 @@ func _register_buffs() -> void:
continue
var buff_id: StringName = StringName(buff_id_text)
GameState.register_generator_buff(_generator_id, buff_id, 0)
var persisted_level: int = GameState.get_generator_buff_level(_generator_id, buff_id)
var starts_unlocked: bool = buff.unlocked or persisted_level > 0
GameState.register_generator_buff_unlocked(_generator_id, buff_id, starts_unlocked)
GameState.register_buff(buff)
if not GameState.is_buff_unlocked(buff_id):
var persisted_level: int = GameState.get_buff_level(buff_id)
var starts_unlocked: bool = buff.unlocked or persisted_level > 0
GameState.set_buff_unlocked(buff_id, starts_unlocked)
_evaluate_buff_unlock_goals()
_try_unlock_buff_from_goal(buff)
func _evaluate_buff_unlock_goals() -> void:
for buff in get_buffs():
_try_unlock_buff_from_goal(buff)
GameState._try_unlock_buff_from_goal(buff)
func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
if buff == null:
return
var buff_id_text: String = String(buff.id).strip_edges()
if buff_id_text.is_empty():
return
var buff_id: StringName = StringName(buff_id_text)
if GameState.is_generator_buff_unlocked(_generator_id, buff_id):
return
if not buff.has_unlock_goal():
return
if not buff.is_unlock_goal_met():
return
GameState.set_generator_buff_unlocked(_generator_id, buff_id, true)
GameState._try_unlock_buff_from_goal(buff)
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
_evaluate_generator_unlock_goal(false)

View File

@@ -37,9 +37,6 @@ enum UnlockGoalBehavior {
@export var click_exponent: int = 0
@export var click_cooldown_seconds: float = 0.2
## Data-driven buffs purchasable for this generator.
@export var buffs: Array[GeneratorBuffData] = []
func has_unlock_goal() -> bool:
if unlock_goal == null:
return false
@@ -65,19 +62,6 @@ func get_unlock_goal_id() -> StringName:
func unlocks_automatically_from_goal() -> bool:
return unlock_goal_behavior == UnlockGoalBehavior.AUTOMATIC
func find_buff(buff_id: StringName) -> GeneratorBuffData:
var expected_id: String = String(buff_id).strip_edges()
if expected_id.is_empty():
return null
for buff in buffs:
if buff == null:
continue
if String(buff.id) == expected_id:
return buff
return null
## Returns cost of next generator
func cost_next(owned: int) -> float:
return cost_for_amount(owned, 1)

View File

@@ -130,6 +130,7 @@ func perform_prestige() -> bool:
last_reset_time = Time.get_unix_time_from_system()
GameState.reset_for_prestige(true, false)
_reset_all_buff_levels()
_reinitialize_generators_after_prestige_reset()
GameState.emit_currency_changed_for_all()
_initialize_run_tracking_from_current_state()
@@ -280,6 +281,11 @@ func _deserialize_state(raw_state: Dictionary) -> void:
func _save_state_to_game_state() -> void:
GameState.set_external_save_data(SAVE_KEY, _serialize_state())
func _reset_all_buff_levels() -> void:
for buff_id in GameState._buff_levels.keys():
GameState.set_buff_level(buff_id, 0)
GameState.set_buff_unlocked(buff_id, false)
func _reinitialize_generators_after_prestige_reset() -> void:
var scene_root: Node = get_tree().current_scene
if scene_root == null: