From 278b07350135622cc48bd0702eb3e6ff7d1e2c45 Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Sun, 15 Mar 2026 10:06:43 +0100 Subject: [PATCH] Add generator buffs (additional test required) --- game_state.gd | 116 +++++++++++- generator/buffs/knowledge_auto_treatise.tres | 14 ++ generator/buffs/knowledge_click_notes.tres | 15 ++ generator/buffs/knowledge_grant_program.tres | 19 ++ generator/buffs/magic_auto_flux.tres | 14 ++ generator/buffs/magic_click_focus.tres | 15 ++ generator/buffs/magic_supply_cache.tres | 18 ++ generator/currency_generator.gd | 180 ++++++++++++++++++- generator/currency_generator_data.gd | 16 ++ generator/generator_buff_data.gd | 95 ++++++++++ generator/generator_buff_data.gd.uid | 1 + generator/primary_generator.tres | 4 + generator/secondary_generator.tres | 4 + generator_buff_tile.gd | 48 +++++ generator_buff_tile.gd.uid | 1 + generator_buff_tile.tscn | 41 +++++ generator_container.gd | 86 ++++++++- generator_container.tscn | 11 ++ generator_museum.tscn | 14 +- 19 files changed, 691 insertions(+), 21 deletions(-) create mode 100644 generator/buffs/knowledge_auto_treatise.tres create mode 100644 generator/buffs/knowledge_click_notes.tres create mode 100644 generator/buffs/knowledge_grant_program.tres create mode 100644 generator/buffs/magic_auto_flux.tres create mode 100644 generator/buffs/magic_click_focus.tres create mode 100644 generator/buffs/magic_supply_cache.tres create mode 100644 generator/generator_buff_data.gd create mode 100644 generator/generator_buff_data.gd.uid create mode 100644 generator_buff_tile.gd create mode 100644 generator_buff_tile.gd.uid create mode 100644 generator_buff_tile.tscn diff --git a/game_state.gd b/game_state.gd index 616c4ef..3d506f2 100644 --- a/game_state.gd +++ b/game_state.gd @@ -6,6 +6,7 @@ extends Node # ========================================== 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) # ========================================== # STATE VARIABLES @@ -14,6 +15,7 @@ var _current_currency: Dictionary = {} var _total_currency_acquired: Dictionary = {} var generator_states: Dictionary = {} +var generator_buff_levels: Dictionary = {} var last_save_time: int = 0 # Unix timestamp @@ -25,9 +27,12 @@ const GENERATOR_OWNED_KEY: String = "owned" const GENERATOR_PURCHASED_COUNT_KEY: String = "purchased_count" const GENERATOR_UNLOCKED_KEY: String = "unlocked" const GENERATOR_AVAILABLE_KEY: String = "available" +const GENERATOR_STATES_KEY: String = "generator_states" +const GENERATOR_BUFF_LEVELS_KEY: String = "generator_buff_levels" const CURRENCIES_KEY: String = "currencies" const CURRENT_KEY: String = "current" const TOTAL_KEY: String = "total" +const LAST_SAVE_TIME_KEY: String = "last_save_time" func _ready() -> void: _initialize_currency_maps() @@ -192,14 +197,61 @@ 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 + + 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 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 get_generator_buff_levels(generator_id: StringName) -> Dictionary: + return _get_generator_buff_levels_ref(generator_id).duplicate(true) + # ========================================== # PERSISTENCE (Save/Load) # ========================================== func save_game() -> void: var save_data = { CURRENCIES_KEY: _serialize_currency_balances(), - "generator_states": _serialize_generator_states(), - "last_save_time": Time.get_unix_time_from_system() + GENERATOR_STATES_KEY: _serialize_generator_states(), + GENERATOR_BUFF_LEVELS_KEY: _serialize_generator_buff_levels(), + LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system() } var file: FileAccess = FileAccess.open(file_name, FileAccess.WRITE) @@ -218,8 +270,9 @@ func load_game() -> void: if parsed_data.has(CURRENCIES_KEY): _load_currency_balances(parsed_data.get(CURRENCIES_KEY, {})) - generator_states = _deserialize_generator_states(parsed_data.get("generator_states", {})) - last_save_time = parsed_data.get("last_save_time", Time.get_unix_time_from_system()) + 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, {})) + last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system()) func _load_currency_balances(raw_currencies: Variant) -> void: if not (raw_currencies is Dictionary): @@ -378,6 +431,32 @@ 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 _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 _serialize_generator_states() -> Dictionary: var result: Dictionary = {} for key_variant in generator_states.keys(): @@ -405,6 +484,35 @@ 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_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) diff --git a/generator/buffs/knowledge_auto_treatise.tres b/generator/buffs/knowledge_auto_treatise.tres new file mode 100644 index 0000000..8081c31 --- /dev/null +++ b/generator/buffs/knowledge_auto_treatise.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://b4ne724qqo4d8"] + +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://generator/generator_buff_data.gd" id="1_4w6kt"] +[ext_resource type="Resource" uid="uid://bnhqk8b31mm4e" path="res://currency/knowledge.tres" id="2_hk5d2"] + +[resource] +script = ExtResource("1_4w6kt") +id = &"knowledge_auto_treatise" +text = "Ancient Treatise" +max_level = 35 +effect_increment = 0.2 +cost_currency = ExtResource("2_hk5d2") +base_cost_mantissa = 40.0 +cost_multiplier = 1.75 diff --git a/generator/buffs/knowledge_click_notes.tres b/generator/buffs/knowledge_click_notes.tres new file mode 100644 index 0000000..a91f0ae --- /dev/null +++ b/generator/buffs/knowledge_click_notes.tres @@ -0,0 +1,15 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://l5a300wieavc"] + +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://generator/generator_buff_data.gd" id="1_l4lhm"] +[ext_resource type="Resource" uid="uid://bnhqk8b31mm4e" path="res://currency/knowledge.tres" id="2_9xja7"] + +[resource] +script = ExtResource("1_l4lhm") +id = &"knowledge_click_notes" +kind = 1 +text = "Quick Notes" +max_level = 25 +effect_increment = 0.25 +cost_currency = ExtResource("2_9xja7") +base_cost_mantissa = 15.0 +cost_multiplier = 1.6 diff --git a/generator/buffs/knowledge_grant_program.tres b/generator/buffs/knowledge_grant_program.tres new file mode 100644 index 0000000..af519ce --- /dev/null +++ b/generator/buffs/knowledge_grant_program.tres @@ -0,0 +1,19 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://dxljqpqhdfqn6"] + +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://generator/generator_buff_data.gd" id="1_8ee3d"] +[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://currency/magic.tres" id="2_j7l8a"] +[ext_resource type="Resource" uid="uid://bnhqk8b31mm4e" path="res://currency/knowledge.tres" id="3_ynr4x"] + +[resource] +script = ExtResource("1_8ee3d") +id = &"knowledge_grant_program" +kind = 2 +text = "Research Grant" +max_level = 20 +effect_increment = 0.0 +cost_currency = ExtResource("2_j7l8a") +base_cost_mantissa = 90.0 +cost_multiplier = 1.8 +resource_target_currency = ExtResource("3_ynr4x") +resource_purchase_base_mantissa = 8.0 +resource_purchase_increment_multiplier = 1.35 diff --git a/generator/buffs/magic_auto_flux.tres b/generator/buffs/magic_auto_flux.tres new file mode 100644 index 0000000..69c24c4 --- /dev/null +++ b/generator/buffs/magic_auto_flux.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://ceugcxmassmpk"] + +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://generator/generator_buff_data.gd" id="1_r7ak1"] +[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://currency/magic.tres" id="2_h3we5"] + +[resource] +script = ExtResource("1_r7ak1") +id = &"magic_auto_flux" +text = "Arcane Dynamo" +max_level = 40 +effect_increment = 0.15 +cost_currency = ExtResource("2_h3we5") +base_cost_mantissa = 25.0 +cost_multiplier = 1.7 diff --git a/generator/buffs/magic_click_focus.tres b/generator/buffs/magic_click_focus.tres new file mode 100644 index 0000000..ac53aa6 --- /dev/null +++ b/generator/buffs/magic_click_focus.tres @@ -0,0 +1,15 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://6i3fcygusuqf"] + +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://generator/generator_buff_data.gd" id="1_1wyaq"] +[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://currency/magic.tres" id="2_0j56j"] + +[resource] +script = ExtResource("1_1wyaq") +id = &"magic_click_focus" +kind = 1 +text = "Apprentice Gloves" +max_level = 30 +effect_increment = 0.35 +cost_currency = ExtResource("2_0j56j") +base_cost_mantissa = 8.0 +cost_multiplier = 1.55 diff --git a/generator/buffs/magic_supply_cache.tres b/generator/buffs/magic_supply_cache.tres new file mode 100644 index 0000000..6daeeb6 --- /dev/null +++ b/generator/buffs/magic_supply_cache.tres @@ -0,0 +1,18 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://cluds3cw1d65q"] + +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://generator/generator_buff_data.gd" id="1_fvte4"] +[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://currency/magic.tres" id="2_ea8w5"] + +[resource] +script = ExtResource("1_fvte4") +id = &"magic_supply_cache" +kind = 2 +text = "Mana Cache" +max_level = 25 +effect_increment = 0.0 +cost_currency = ExtResource("2_ea8w5") +base_cost_mantissa = 12.0 +cost_multiplier = 1.6 +resource_target_currency = ExtResource("2_ea8w5") +resource_purchase_base_mantissa = 12.0 +resource_purchase_increment_multiplier = 1.45 diff --git a/generator/currency_generator.gd b/generator/currency_generator.gd index ce0d6b2..b522da7 100644 --- a/generator/currency_generator.gd +++ b/generator/currency_generator.gd @@ -13,6 +13,10 @@ signal purchase_completed(amount: int, total_owned: int, total_purchased: int, t signal purchase_failed(requested_amount: int, required_cost: BigNumber, available_currency: BigNumber) ## Emitted when one or more automatic production cycles complete. signal production_tick(amount: BigNumber, cycle_count: int, total_owned: int) +## Emitted after a successful buff purchase. +signal buff_purchased(buff_id: StringName, new_level: int, cost: BigNumber, cost_currency_id: StringName) +## Emitted when a buff purchase cannot be paid. +signal buff_purchase_failed(buff_id: StringName, required_cost: BigNumber, cost_currency_id: StringName) ## Sentinel exponent used when cost math overflows float range. const HUGE_COST_EXPONENT: int = 1000000 @@ -119,7 +123,8 @@ func _grant_cycle_income(cycle_count: int) -> void: if data == null or cycle_count <= 0: return - var per_cycle: float = data.production_per_cycle(owned, run_multiplier, purchased_count) + var effective_run_multiplier: float = run_multiplier * get_automatic_production_multiplier() + var per_cycle: float = data.production_per_cycle(owned, effective_run_multiplier, purchased_count) if per_cycle <= 0.0: return @@ -195,6 +200,89 @@ func buy_max() -> int: return 0 return best if buy(best) else 0 +func get_buffs() -> Array[GeneratorBuffData]: + if data == null: + return [] + return data.buffs + +func get_buff_level(buff_id: StringName) -> int: + _ensure_registered() + return GameState.get_generator_buff_level(_generator_id, buff_id) + +func get_buff_cost(buff_id: StringName) -> BigNumber: + var buff: GeneratorBuffData = _find_buff(buff_id) + if buff == null: + return BigNumber.from_float(0.0) + + var level: int = get_buff_level(buff.id) + return buff.get_cost_for_level(level) + +func get_buff_cost_currency_id(buff_id: StringName) -> StringName: + var buff: GeneratorBuffData = _find_buff(buff_id) + if buff == null: + return &"" + return _resolve_buff_cost_currency_id(buff) + +func can_buy_buff(buff_id: StringName) -> bool: + if not is_available_to_player(): + return false + + var buff: GeneratorBuffData = _find_buff(buff_id) + if buff == null: + return false + + var current_level: int = get_buff_level(buff.id) + if not buff.can_purchase_next_level(current_level): + return false + + var cost_currency_id: StringName = _resolve_buff_cost_currency_id(buff) + if cost_currency_id == &"": + 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) + 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 = _find_buff(buff_id) + if buff == null: + return false + + var current_level: int = get_buff_level(buff.id) + if not buff.can_purchase_next_level(current_level): + return false + + var cost: BigNumber = buff.get_cost_for_level(current_level) + var cost_currency_id: StringName = _resolve_buff_cost_currency_id(buff) + if cost_currency_id == &"": + return false + + if not _spend_currency_by_id(cost_currency_id, cost): + buff_purchase_failed.emit(buff.id, cost, cost_currency_id) + return false + + var next_level: int = current_level + 1 + GameState.set_generator_buff_level(_generator_id, 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) + if buff == null: + return true + + return not buff.can_purchase_next_level(get_buff_level(buff.id)) + +func get_automatic_production_multiplier() -> float: + return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER) + +func get_manual_click_multiplier() -> float: + return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.MANUAL_CLICK_MULTIPLIER) + ## Handles external "pressed" interaction: buy if configured, otherwise click-grant. func _on_pressed() -> void: if not is_available_to_player(): @@ -235,7 +323,13 @@ func _try_grant_click_currency() -> void: func _get_click_value() -> BigNumber: if data == null: return BigNumber.from_float(0.0) - return BigNumber.new(data.click_mantissa, data.click_exponent) + + var click_multiplier: float = get_manual_click_multiplier() + if click_multiplier <= 0.0: + return BigNumber.from_float(0.0) + + var base_click: BigNumber = BigNumber.new(data.click_mantissa, data.click_exponent) + return base_click.multiply(BigNumber.from_float(click_multiplier)) ## Resolves click cooldown from data. func _get_click_cooldown_seconds() -> float: @@ -268,6 +362,12 @@ func _add_currency(amount: BigNumber) -> void: GameState.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) + ## Attempts to spend target currency; returns false when insufficient. func _spend_currency(cost: BigNumber) -> bool: if currency == null: @@ -276,6 +376,12 @@ func _spend_currency(cost: BigNumber) -> bool: return GameState.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) + ## Returns the current amount for the configured currency type. func _get_currency_amount() -> BigNumber: if currency == null: @@ -300,6 +406,59 @@ 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 &"" + + var cost_currency: Resource = buff.cost_currency if buff.cost_currency != null else currency + if cost_currency == null: + return &"" + + return GameState.get_currency_id(cost_currency) + +func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName: + if buff == null: + return &"" + + var target_currency: Resource = buff.resource_target_currency if buff.resource_target_currency != null else currency + if target_currency == null: + return &"" + + 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 + + multiplier *= buff.get_effect_multiplier(get_buff_level(buff.id)) + + return maxf(multiplier, 0.0) + +func _apply_resource_purchase_buff(buff: GeneratorBuffData, level: int) -> void: + if buff == null: + return + if buff.kind != GeneratorBuffData.BuffKind.RESOURCE_PURCHASE: + return + + var target_currency_id: StringName = _resolve_buff_target_currency_id(buff) + if target_currency_id == &"": + return + + var purchased_amount: BigNumber = buff.get_resource_purchase_amount_for_level(level) + if purchased_amount.mantissa <= 0.0: + return + + _add_currency_by_id(target_currency_id, purchased_amount) + ## Builds a stable state id from data id, node name, or fallback string. func _resolve_generator_id() -> StringName: if data != null: @@ -323,8 +482,20 @@ func _ensure_registered() -> void: return GameState.register_generator(_generator_id, _default_unlocked_state(), _default_available_state()) + _register_buffs() _is_registered = true +func _register_buffs() -> void: + for buff in get_buffs(): + if buff == null: + continue + + var buff_id_text: String = String(buff.id).strip_edges() + if buff_id_text.is_empty(): + continue + + GameState.register_generator_buff(_generator_id, StringName(buff_id_text), 0) + ## Default unlocked state loaded from generator data. func _default_unlocked_state() -> bool: if data == null: @@ -345,9 +516,6 @@ func _on_area_2d_mouse_entered() -> void: func _on_area_2d_mouse_exited() -> void: _mouse_entered = false -func _on_generated_state_changed(generator_id: StringName, state: Dictionary) -> void: +func _on_generated_state_changed(generator_id: StringName, _state: Dictionary) -> void: if generator_id == _generator_id: - print(_generator_id) - print(state) visible = true - diff --git a/generator/currency_generator_data.gd b/generator/currency_generator_data.gd index bc9f67c..14549b9 100644 --- a/generator/currency_generator_data.gd +++ b/generator/currency_generator_data.gd @@ -28,6 +28,22 @@ extends Resource @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 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) diff --git a/generator/generator_buff_data.gd b/generator/generator_buff_data.gd new file mode 100644 index 0000000..595e5dd --- /dev/null +++ b/generator/generator_buff_data.gd @@ -0,0 +1,95 @@ +class_name GeneratorBuffData +extends Resource + +enum BuffKind { + AUTO_PRODUCTION_MULTIPLIER, + MANUAL_CLICK_MULTIPLIER, + RESOURCE_PURCHASE, +} + +const HUGE_EXPONENT: int = 1000000 + +@export var id: StringName = &"" +@export var kind: BuffKind = BuffKind.AUTO_PRODUCTION_MULTIPLIER +@export_multiline var text: String = "" +@export var icon: Texture2D +@export var max_level: int = -1 + +@export_group("Effect") +@export var base_effect: float = 1.0 +@export var effect_increment: float = 0.1 + +@export_group("Cost") +@export var cost_currency: Resource +@export var base_cost_mantissa: float = 10.0 +@export var base_cost_exponent: int = 0 +@export var cost_multiplier: float = 1.5 + +@export_group("Resource Purchase") +@export var resource_target_currency: Resource +@export var resource_purchase_base_mantissa: float = 10.0 +@export var resource_purchase_base_exponent: int = 0 +@export var resource_purchase_increment_multiplier: float = 1.2 + +func can_purchase_next_level(current_level: int) -> bool: + if max_level < 0: + return true + return maxi(current_level, 0) < max_level + +func get_effect_multiplier(level: int) -> float: + var safe_level: int = maxi(level, 0) + return maxf(base_effect + (effect_increment * float(safe_level)), 0.0) + +func get_cost_for_level(level: int) -> BigNumber: + var safe_level: int = maxi(level, 0) + var base_cost: BigNumber = BigNumber.new(base_cost_mantissa, base_cost_exponent) + if base_cost.mantissa <= 0.0: + return BigNumber.from_float(0.0) + + if cost_multiplier <= 0.0: + return BigNumber.new(1.0, HUGE_EXPONENT) + + var growth: float = 1.0 + if not is_equal_approx(cost_multiplier, 1.0): + growth = pow(cost_multiplier, float(safe_level)) + if not is_finite(growth): + return BigNumber.new(1.0, HUGE_EXPONENT) + + return _scale_big_number(base_cost, growth) + +func get_resource_purchase_amount_for_level(level: int) -> BigNumber: + var safe_level: int = maxi(level, 0) + var base_amount: BigNumber = BigNumber.new(resource_purchase_base_mantissa, resource_purchase_base_exponent) + if base_amount.mantissa <= 0.0: + return BigNumber.from_float(0.0) + + if resource_purchase_increment_multiplier <= 0.0: + return BigNumber.from_float(0.0) + + var growth: float = 1.0 + if not is_equal_approx(resource_purchase_increment_multiplier, 1.0): + growth = pow(resource_purchase_increment_multiplier, float(safe_level)) + if not is_finite(growth): + return BigNumber.new(1.0, HUGE_EXPONENT) + + return _scale_big_number(base_amount, growth) + +func get_effect_description(level: int) -> String: + var safe_level: int = maxi(level, 0) + match kind: + BuffKind.AUTO_PRODUCTION_MULTIPLIER, BuffKind.MANUAL_CLICK_MULTIPLIER: + return "x%.2f" % get_effect_multiplier(safe_level) + BuffKind.RESOURCE_PURCHASE: + return "+%s" % get_resource_purchase_amount_for_level(safe_level).to_string_suffix(2) + _: + return "-" + +func _scale_big_number(base_amount: BigNumber, scale: float) -> BigNumber: + if base_amount.mantissa <= 0.0: + return BigNumber.from_float(0.0) + if scale <= 0.0: + return BigNumber.from_float(0.0) + if not is_finite(scale): + return BigNumber.new(1.0, HUGE_EXPONENT) + + return base_amount.multiply(BigNumber.from_float(scale)) diff --git a/generator/generator_buff_data.gd.uid b/generator/generator_buff_data.gd.uid new file mode 100644 index 0000000..0174dc4 --- /dev/null +++ b/generator/generator_buff_data.gd.uid @@ -0,0 +1 @@ +uid://dnhocfsuvmeyb diff --git a/generator/primary_generator.tres b/generator/primary_generator.tres index 65cab5d..07500ae 100644 --- a/generator/primary_generator.tres +++ b/generator/primary_generator.tres @@ -1,6 +1,9 @@ [gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://co0mcc2kvcpo5"] [ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://generator/currency_generator_data.gd" id="1_c6y77"] +[ext_resource type="Resource" path="res://generator/buffs/magic_auto_flux.tres" id="2_x505b"] +[ext_resource type="Resource" path="res://generator/buffs/magic_click_focus.tres" id="3_fsxdm"] +[ext_resource type="Resource" path="res://generator/buffs/magic_supply_cache.tres" id="4_x2byu"] [resource] script = ExtResource("1_c6y77") @@ -8,4 +11,5 @@ id = &"Magic" name = "Magic Orb" initial_time = 0.6 initial_productivity = 1.67 +buffs = [ExtResource("2_x505b"), ExtResource("3_fsxdm"), ExtResource("4_x2byu")] metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/generator/secondary_generator.tres b/generator/secondary_generator.tres index f31f396..1315054 100644 --- a/generator/secondary_generator.tres +++ b/generator/secondary_generator.tres @@ -1,6 +1,9 @@ [gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://04pmc034qupd"] [ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://generator/currency_generator_data.gd" id="1_s3g28"] +[ext_resource type="Resource" path="res://generator/buffs/knowledge_auto_treatise.tres" id="2_kxvfc"] +[ext_resource type="Resource" path="res://generator/buffs/knowledge_click_notes.tres" id="3_q64x8"] +[ext_resource type="Resource" path="res://generator/buffs/knowledge_grant_program.tres" id="4_l77sg"] [resource] script = ExtResource("1_s3g28") @@ -12,4 +15,5 @@ initial_cost = 60.0 initial_time = 3.0 initial_revenue = 60.0 initial_productivity = 20.0 +buffs = [ExtResource("2_kxvfc"), ExtResource("3_q64x8"), ExtResource("4_l77sg")] metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/generator_buff_tile.gd b/generator_buff_tile.gd new file mode 100644 index 0000000..e62b5d4 --- /dev/null +++ b/generator_buff_tile.gd @@ -0,0 +1,48 @@ +class_name GeneratorBuffTile +extends HBoxContainer + +signal buy_pressed(buff_id: StringName) + +@onready var _icon: TextureRect = $Icon +@onready var _text_label: Label = $TextLabel +@onready var _level_label: Label = $LevelLabel +@onready var _effect_label: Label = $EffectLabel +@onready var _cost_label: Label = $CostLabel +@onready var _buy_button: Button = $BuyButton + +var _buff_id: StringName = &"" + +func set_buff(buff: GeneratorBuffData) -> void: + if buff == null: + _buff_id = &"" + _icon.texture = null + _icon.visible = false + _text_label.text = "Unknown Buff" + return + + var buff_id_text: String = String(buff.id).strip_edges() + _buff_id = StringName(buff_id_text) + + _icon.texture = buff.icon + _icon.visible = buff.icon != null + _text_label.text = buff.text if not buff.text.strip_edges().is_empty() else buff_id_text + +func set_runtime(level: int, effect_text: String, cost_text: String, can_buy: bool, is_maxed: bool) -> void: + _level_label.text = "Lv %d" % maxi(level, 0) + _effect_label.text = effect_text + + if is_maxed: + _cost_label.text = "Max level" + _buy_button.text = "Max" + _buy_button.disabled = true + return + + _cost_label.text = cost_text + _buy_button.text = "Buy" + _buy_button.disabled = not can_buy + +func _on_buy_button_pressed() -> void: + if _buff_id == &"": + return + + buy_pressed.emit(_buff_id) diff --git a/generator_buff_tile.gd.uid b/generator_buff_tile.gd.uid new file mode 100644 index 0000000..8f131c8 --- /dev/null +++ b/generator_buff_tile.gd.uid @@ -0,0 +1 @@ +uid://bsu10r1nhmhw1 diff --git a/generator_buff_tile.tscn b/generator_buff_tile.tscn new file mode 100644 index 0000000..a130a8f --- /dev/null +++ b/generator_buff_tile.tscn @@ -0,0 +1,41 @@ +[gd_scene format=3 uid="uid://g1w168ancuxi"] + +[ext_resource type="Script" uid="uid://bsu10r1nhmhw1" path="res://generator_buff_tile.gd" id="1_wdw66"] + +[node name="GeneratorBuffTile" type="HBoxContainer" unique_id=50960329] +size_flags_horizontal = 3 +theme_override_constants/separation = 8 +script = ExtResource("1_wdw66") + +[node name="Icon" type="TextureRect" parent="." unique_id=1130513269] +visible = false +custom_minimum_size = Vector2(18, 18) +layout_mode = 2 +stretch_mode = 5 + +[node name="TextLabel" type="Label" parent="." unique_id=2066010368] +custom_minimum_size = Vector2(140, 0) +layout_mode = 2 +text = "Buff" + +[node name="LevelLabel" type="Label" parent="." unique_id=1101640559] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +text = "Lv 0" + +[node name="EffectLabel" type="Label" parent="." unique_id=976763452] +custom_minimum_size = Vector2(90, 0) +layout_mode = 2 +text = "x1.00" + +[node name="CostLabel" type="Label" parent="." unique_id=246779202] +layout_mode = 2 +size_flags_horizontal = 3 +text = "0" + +[node name="BuyButton" type="Button" parent="." unique_id=1815270585] +custom_minimum_size = Vector2(68, 0) +layout_mode = 2 +text = "Buy" + +[connection signal="pressed" from="BuyButton" to="." method="_on_buy_button_pressed"] diff --git a/generator_container.gd b/generator_container.gd index 9113e07..81b3004 100644 --- a/generator_container.gd +++ b/generator_container.gd @@ -1,5 +1,15 @@ extends VBoxContainer +const GENERATOR_BUFF_TILE_SCENE: PackedScene = preload("res://generator_buff_tile.tscn") + +class BuffRow extends RefCounted: + var buff: GeneratorBuffData + var tile: GeneratorBuffTile + + func _init(row_buff: GeneratorBuffData, row_tile: GeneratorBuffTile) -> void: + buff = row_buff + tile = row_tile + @export var _generator: CurrencyGenerator @onready var _buy_button: Button = $HBoxContainerGenerators/BuyOneButton @@ -8,16 +18,28 @@ extends VBoxContainer @onready var _owned_value: Label = $GeneratorStatsGrid/OwnedValue @onready var _next_cost_value: Label = $GeneratorStatsGrid/NextCostValue @onready var _production_value: Label = $GeneratorStatsGrid/ProductionValue +@onready var _buffs_section: VBoxContainer = $BuffsSection +@onready var _buffs_list: VBoxContainer = $BuffsSection/BuffsList + +var _buff_rows: Array[BuffRow] = [] func _ready() -> void: + if _generator == null: + push_warning("GeneratorContainer '%s' has no generator assigned." % String(name)) + return + _generator.purchase_completed.connect(_on_generator_updated) _generator.production_tick.connect(_on_generator_updated) _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) _name_label.text = _generator.data.name if _generator.data != null else "Generator" + _build_buff_rows() _refresh_generator_ui() func _on_buy_pressed() -> void: @@ -32,9 +54,7 @@ func _on_debug_income_pressed() -> void: _generator.grant_currency(BigNumber.from_float(100.0)) _refresh_generator_ui() -func _on_currency_changed(changed_currency_id: StringName, _new_value: BigNumber) -> void: - if changed_currency_id != _generator.get_currency_id(): - return +func _on_currency_changed(_changed_currency_id: StringName, _new_value: BigNumber) -> void: _refresh_generator_ui() func _on_generator_updated(_a = null, _b = null, _c = null, _d = null) -> void: @@ -44,6 +64,63 @@ func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) - if generator_id == _generator.get_generator_id(): 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.get_generator_id(): + return + + _refresh_generator_ui() + +func _on_buy_buff_pressed(buff_id: StringName) -> void: + _generator.buy_buff(buff_id) + _refresh_generator_ui() + +func _build_buff_rows() -> void: + _buff_rows.clear() + for child in _buffs_list.get_children(): + child.queue_free() + + for buff in _generator.get_buffs(): + if buff == null: + continue + + var buff_id_text: String = String(buff.id).strip_edges() + if buff_id_text.is_empty(): + continue + + var tile: GeneratorBuffTile = GENERATOR_BUFF_TILE_SCENE.instantiate() as GeneratorBuffTile + if tile == null: + push_warning("Failed to instantiate GeneratorBuffTile for buff '%s'." % buff_id_text) + continue + + _buffs_list.add_child(tile) + tile.set_buff(buff) + tile.buy_pressed.connect(_on_buy_buff_pressed) + _buff_rows.append(BuffRow.new(buff, tile)) + +func _refresh_buff_rows(can_interact: bool) -> void: + _buffs_section.visible = not _buff_rows.is_empty() + if _buff_rows.is_empty(): + return + + for row in _buff_rows: + if row == null or row.buff == null or row.tile == null: + continue + + var buff: GeneratorBuffData = row.buff + var level: int = _generator.get_buff_level(buff.id) + var effect_text: String = buff.get_effect_description(level) + + if _generator.is_buff_maxed(buff.id): + row.tile.set_runtime(level, effect_text, "", false, true) + continue + + 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 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) func _refresh_generator_ui() -> void: var can_interact: bool = _generator.is_available_to_player() @@ -57,8 +134,9 @@ func _refresh_generator_ui() -> void: if _generator.data != null and can_interact: production_per_second = _generator.data.production_per_second( _generator.owned, - _generator.run_multiplier, + _generator.run_multiplier * _generator.get_automatic_production_multiplier(), _generator.purchased_count ) _production_value.text = BigNumber.from_float(production_per_second).to_string_suffix(2) + _refresh_buff_rows(can_interact) diff --git a/generator_container.tscn b/generator_container.tscn index 8b8da7e..2e179a3 100644 --- a/generator_container.tscn +++ b/generator_container.tscn @@ -60,5 +60,16 @@ text = "Production / sec" layout_mode = 2 text = "0" +[node name="BuffsSection" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="BuffsTitle" type="Label" parent="BuffsSection"] +layout_mode = 2 +text = "Buffs" + +[node name="BuffsList" type="VBoxContainer" parent="BuffsSection"] +layout_mode = 2 +theme_override_constants/separation = 4 + [connection signal="pressed" from="HBoxContainerGenerators/BuyOneButton" to="." method="_on_buy_pressed"] [connection signal="pressed" from="HBoxContainerGenerators/BuyMaxButton" to="." method="_on_buy_max_pressed"] diff --git a/generator_museum.tscn b/generator_museum.tscn index a504e27..8731dcc 100644 --- a/generator_museum.tscn +++ b/generator_museum.tscn @@ -41,18 +41,18 @@ currency = ExtResource("6_gemsc") [node name="MagicGeneratorContainer" parent="UI" unique_id=1129190041 node_paths=PackedStringArray("_generator") instance=ExtResource("3_pw2a0")] layout_mode = 1 offset_left = -19.0 -offset_top = 53.0 -offset_right = 245.0 -offset_bottom = 218.0 +offset_top = 50.5 +offset_right = 422.0 +offset_bottom = 316.0 _generator = NodePath("../../MagicGenerator") [node name="KnowledgeGeneratorContainer" parent="UI" unique_id=1999544736 node_paths=PackedStringArray("_generator") instance=ExtResource("3_pw2a0")] visible = false layout_mode = 1 -offset_left = -17.0 -offset_top = 215.0 -offset_right = 247.0 -offset_bottom = 380.0 +offset_left = -16.0 +offset_top = 352.0 +offset_right = 417.0 +offset_bottom = 626.0 _generator = NodePath("../../KnowledgeGenerator") [node name="GoalsDebugUI" parent="UI" unique_id=4710697 instance=ExtResource("4_63gjq")]