diff --git a/big_number_museum.gd b/big_number_museum.gd index 66d9de5..e52cdc6 100644 --- a/big_number_museum.gd +++ b/big_number_museum.gd @@ -1,10 +1,49 @@ extends Control +@onready var _generator: CurrencyGenerator = $VBoxContainer2/HBoxContainerGenerators/BuyOneButton +@onready var _name_label: Label = $VBoxContainer2/Label +@onready var _owned_value: Label = $VBoxContainer2/GeneratorStatsGrid/OwnedValue +@onready var _next_cost_value: Label = $VBoxContainer2/GeneratorStatsGrid/NextCostValue +@onready var _production_value: Label = $VBoxContainer2/GeneratorStatsGrid/ProductionValue -# Called when the node enters the scene tree for the first time. func _ready() -> void: - pass + _generator.purchase_completed.connect(_on_generator_updated) + _generator.production_tick.connect(_on_generator_updated) + _generator.purchase_failed.connect(_on_generator_updated) -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: - pass + match _generator.currency: + GameState.CurrencyType.gold: + GameState.gold_changed.connect(_on_currency_changed) + GameState.CurrencyType.gems: + GameState.gems_changed.connect(_on_currency_changed) + + _name_label.text = _generator.data.name + _refresh_generator_ui() + +func _on_buy_max_pressed() -> void: + _generator.buy_max() + _refresh_generator_ui() + +func _on_debug_income_pressed() -> void: + _generator.grant_currency(BigNumber.from_float(100.0)) + _refresh_generator_ui() + +func _on_currency_changed(_new_value: BigNumber) -> void: + _refresh_generator_ui() + +func _on_generator_updated(_a = null, _b = null, _c = null, _d = null) -> void: + _refresh_generator_ui() + +func _refresh_generator_ui() -> void: + _owned_value.text = str(_generator.owned) + _next_cost_value.text = _generator.get_next_cost().to_string_suffix(2) + + var production_per_second: float = 0.0 + if _generator.data != null: + production_per_second = _generator.data.production_per_second( + _generator.owned, + _generator.run_multiplier, + _generator.purchased_count + ) + + _production_value.text = BigNumber.from_float(production_per_second).to_string_suffix(2) diff --git a/big_number_museum.tscn b/big_number_museum.tscn index be39812..f09d8d6 100644 --- a/big_number_museum.tscn +++ b/big_number_museum.tscn @@ -1,9 +1,9 @@ [gd_scene format=3 uid="uid://b11eevc8qj18p"] [ext_resource type="Script" uid="uid://dtbxopw6ulhl8" path="res://generator/currency_generator.gd" id="1_h2bxk"] -[ext_resource type="PackedScene" uid="uid://clu4rn1tl1s8t" path="res://big_number_progress_bar.tscn" id="1_lni8d"] [ext_resource type="Script" uid="uid://coasop1lyw5rh" path="res://big_number_museum.gd" id="1_muwei"] [ext_resource type="Script" uid="uid://sp67wcb6s5nw" path="res://currency_label.gd" id="2_a7k0t"] +[ext_resource type="Resource" uid="uid://co0mcc2kvcpo5" path="res://generator/primary_generator.tres" id="3_m5m0g"] [node name="BigNumberMuseum" type="Control" unique_id=1249411984] layout_mode = 3 @@ -16,28 +16,79 @@ script = ExtResource("1_muwei") [node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1937517070] layout_mode = 0 -offset_right = 264.0 -offset_bottom = 111.0 +offset_right = 404.0 +offset_bottom = 267.0 -[node name="Label" type="Label" parent="VBoxContainer" unique_id=1357448839] +[node name="HBoxContainerCurrency" type="HBoxContainer" parent="VBoxContainer" unique_id=93448794] layout_mode = 2 -text = "Primary currency" +theme_override_constants/separation = 12 -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" unique_id=1592054467] +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainerCurrency" unique_id=1951729878] layout_mode = 2 -theme_override_constants/separation = 126 +text = "Gold:" -[node name="Button" type="Button" parent="VBoxContainer/HBoxContainer" unique_id=2126082317] +[node name="CurrencyValueLabel" type="Label" parent="VBoxContainer/HBoxContainerCurrency" unique_id=1036299287] layout_mode = 2 -text = "Get Currency" -script = ExtResource("1_h2bxk") - -[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=1036299287] -layout_mode = 2 -text = "000" +text = "0" script = ExtResource("2_a7k0t") -[node name="HBoxContainerAuto" parent="VBoxContainer" unique_id=1174903091 instance=ExtResource("1_lni8d")] +[node name="DebugIncomeButton" type="Button" parent="VBoxContainer/HBoxContainerCurrency" unique_id=780351266] layout_mode = 2 +text = "+100 Gold" -[connection signal="pressed" from="VBoxContainer/HBoxContainer/Button" to="VBoxContainer/HBoxContainer/Button" method="_on_pressed"] +[node name="VBoxContainer2" type="VBoxContainer" parent="." unique_id=1163554471] +layout_mode = 0 +offset_left = 2.0 +offset_top = 138.0 +offset_right = 266.0 +offset_bottom = 303.0 + +[node name="Label" type="Label" parent="VBoxContainer2" unique_id=2116890422] +layout_mode = 2 +text = "Generator" + +[node name="HBoxContainerGenerators" type="HBoxContainer" parent="VBoxContainer2" unique_id=1759912851] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="BuyOneButton" type="Button" parent="VBoxContainer2/HBoxContainerGenerators" unique_id=2126082317] +layout_mode = 2 +text = "Buy x1" +script = ExtResource("1_h2bxk") +data = ExtResource("3_m5m0g") + +[node name="BuyMaxButton" type="Button" parent="VBoxContainer2/HBoxContainerGenerators" unique_id=171532995] +layout_mode = 2 +text = "Buy Max" + +[node name="GeneratorStatsGrid" type="GridContainer" parent="VBoxContainer2" unique_id=88353659] +layout_mode = 2 +columns = 2 + +[node name="OwnedTitle" type="Label" parent="VBoxContainer2/GeneratorStatsGrid" unique_id=1743705746] +layout_mode = 2 +text = "Owned" + +[node name="OwnedValue" type="Label" parent="VBoxContainer2/GeneratorStatsGrid" unique_id=890804662] +layout_mode = 2 +text = "0" + +[node name="NextCostTitle" type="Label" parent="VBoxContainer2/GeneratorStatsGrid" unique_id=990792129] +layout_mode = 2 +text = "Next Cost" + +[node name="NextCostValue" type="Label" parent="VBoxContainer2/GeneratorStatsGrid" unique_id=1296617914] +layout_mode = 2 +text = "0" + +[node name="ProductionTitle" type="Label" parent="VBoxContainer2/GeneratorStatsGrid" unique_id=698748759] +layout_mode = 2 +text = "Production / sec" + +[node name="ProductionValue" type="Label" parent="VBoxContainer2/GeneratorStatsGrid" unique_id=1059520776] +layout_mode = 2 +text = "0" + +[connection signal="pressed" from="VBoxContainer/HBoxContainerCurrency/DebugIncomeButton" to="." method="_on_debug_income_pressed"] +[connection signal="pressed" from="VBoxContainer2/HBoxContainerGenerators/BuyOneButton" to="VBoxContainer2/HBoxContainerGenerators/BuyOneButton" method="_on_pressed"] +[connection signal="pressed" from="VBoxContainer2/HBoxContainerGenerators/BuyMaxButton" to="." method="_on_buy_max_pressed"] diff --git a/currency_label.gd b/currency_label.gd index 2cb67ea..faeca72 100644 --- a/currency_label.gd +++ b/currency_label.gd @@ -11,6 +11,6 @@ func _ready() -> void: GameState.gems_changed.connect(_on_currency_changed) func _on_currency_changed(currency: BigNumber) -> void: - text = currency.to_string_sci(0) + text = currency.to_string_suffix(2) diff --git a/generator/currency_generator.gd b/generator/currency_generator.gd index b22392f..29005a2 100644 --- a/generator/currency_generator.gd +++ b/generator/currency_generator.gd @@ -1,20 +1,162 @@ class_name CurrencyGenerator extends Node -@export var currency: GameState.CurrencyType +signal purchase_completed(amount: int, total_owned: int, total_purchased: int, total_cost: BigNumber) +signal purchase_failed(requested_amount: int, required_cost: BigNumber, available_currency: BigNumber) +signal production_tick(amount: BigNumber, cycle_count: int, total_owned: int) + +const HUGE_COST_EXPONENT: int = 1000000 + +@export var currency: GameState.CurrencyType = GameState.CurrencyType.gold @export var data: CurrencyGeneratorData +@export var is_automatic: bool = true +@export var press_buys_generator: bool = true +@export var run_multiplier: float = 1.0 + +## Manual click fallback when no data is assigned. +@export var click_mantissa: float = 1.0 +@export var click_exponent: int = 0 + +var owned: int = 0 +var purchased_count: int = 0 +var cycle_progress_seconds: float = 0.0 -# Called when the node enters the scene tree for the first time. func _ready() -> void: - pass # Replace with function body. + cycle_progress_seconds = 0.0 -# Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: - pass + if not is_automatic: + return + if data == null or owned <= 0: + return + + var cycle_seconds: float = maxf(data.initial_time, 0.0001) + cycle_progress_seconds += maxf(delta, 0.0) + if cycle_progress_seconds < cycle_seconds: + return + + var completed_cycles: int = floori(cycle_progress_seconds / cycle_seconds) + cycle_progress_seconds -= cycle_seconds * float(completed_cycles) + _grant_cycle_income(completed_cycles) + +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) + if per_cycle <= 0.0: + return + + var produced: BigNumber = BigNumber.from_float(per_cycle).multiply(BigNumber.from_float(float(cycle_count))) + _add_currency(produced) + production_tick.emit(produced, cycle_count, owned) + +func get_next_cost() -> BigNumber: + return get_cost_for_amount(1) + +func get_cost_for_amount(amount: int = 1) -> BigNumber: + if data == null: + return BigNumber.from_float(0.0) + + var safe_amount: int = maxi(amount, 1) + var raw_cost: float = data.cost_for_amount(owned, safe_amount) + return _float_to_big_number(raw_cost) + +func can_buy(amount: int = 1) -> bool: + var cost: BigNumber = get_cost_for_amount(amount) + var current: BigNumber = _get_currency_amount() + return current.is_greater_than(cost) or current.is_equal_to(cost) + +func buy(amount: int = 1) -> bool: + if data == null: + return false + + var safe_amount: int = maxi(amount, 1) + var total_cost: BigNumber = get_cost_for_amount(safe_amount) + if not _spend_currency(total_cost): + purchase_failed.emit(safe_amount, total_cost, _get_currency_amount()) + return false + + owned += safe_amount + purchased_count += safe_amount + purchase_completed.emit(safe_amount, owned, purchased_count, total_cost) + return true + +func buy_max() -> int: + if data == null: + return 0 + + var available_currency: float = _big_number_to_float(_get_currency_amount()) + var estimated_max: int = data.max_affordable(owned, available_currency) + if estimated_max <= 0: + return 0 + + # Keep max-buy accurate by validating affordability with BigNumber comparisons. + var low: int = 1 + var high: int = estimated_max + var best: int = 0 + while low <= high: + var mid: int = floori((float(low) + float(high)) * 0.5) + if can_buy(mid): + best = mid + low = mid + 1 + else: + high = mid - 1 + + if best <= 0: + return 0 + return best if buy(best) else 0 func _on_pressed() -> void: + if data != null and press_buys_generator: + buy(1) + return + + _add_currency(BigNumber.new(click_mantissa, click_exponent)) + +func grant_currency(amount: BigNumber) -> void: + _add_currency(amount) + +func _add_currency(amount: BigNumber) -> void: match currency: - GameState.CurrencyType.gold: GameState.add_gold(BigNumber.new(1, 0)) - GameState.CurrencyType.gems: GameState.add_gems(BigNumber.new(1, 0)) - _: print("c") - pass + GameState.CurrencyType.gold: + GameState.add_gold(amount) + GameState.CurrencyType.gems: + GameState.add_gems(amount) + _: + push_error("Unknown currency type in CurrencyGenerator._add_currency") + +func _spend_currency(cost: BigNumber) -> bool: + match currency: + GameState.CurrencyType.gold: + return GameState.spend_gold(cost) + GameState.CurrencyType.gems: + return GameState.spend_gems(cost) + _: + push_error("Unknown currency type in CurrencyGenerator._spend_currency") + return false + +func _get_currency_amount() -> BigNumber: + match currency: + GameState.CurrencyType.gold: + return GameState.gold + GameState.CurrencyType.gems: + return GameState.gems + _: + return BigNumber.from_float(0.0) + +func _float_to_big_number(value: float) -> BigNumber: + if value <= 0.0: + return BigNumber.from_float(0.0) + if not is_finite(value): + return BigNumber.new(1.0, HUGE_COST_EXPONENT) + return BigNumber.from_float(value) + +func _big_number_to_float(value: BigNumber) -> float: + if value.mantissa <= 0.0: + return 0.0 + if value.exponent > 308: + return INF + if value.exponent < -308: + return 0.0 + return value.mantissa * pow(10.0, float(value.exponent)) diff --git a/generator/currency_generator_data.gd b/generator/currency_generator_data.gd index b2d052a..d6806ba 100644 --- a/generator/currency_generator_data.gd +++ b/generator/currency_generator_data.gd @@ -1,18 +1,127 @@ class_name CurrencyGeneratorData extends Resource -@export var id: GameState.CurrencyType -@export var name: String -@export var initial_cost: float -@export var coefficient: float -@export var initial_time: float -@export var initial_revenue: float -@export var initial_productivity: float +@export var id: StringName = &"" +@export var name: String = "" -## Returns cost of next generator +## Base cost and exponential growth (part I): cost_next = b * r^owned +@export var initial_cost: float = 10.0 +@export var coefficient: float = 1.15 + +## Generator timing and output. +@export var initial_time: float = 1.0 +@export var initial_revenue: float = 1.0 +@export var initial_productivity: float = 1.0 + +## Milestone boosts (part I), e.g. every 25 purchased grant x2. +@export var milestone_step: int = 25 +@export var milestone_multiplier: float = 2.0 + +## Optional purchased-only boost from derivative-style models (part II). +## Example: 0.05 means each purchased unit adds +0.05% to output. +@export var purchased_boost_percent: float = 0.0 + +## Returns cost of next generator func cost_next(owned: int) -> float: - return initial_cost * pow(coefficient, owned) + return cost_for_amount(owned, 1) + +## Geometric-series bulk-buy cost (part I): +## total = b * r^k * ((r^n - 1) / (r - 1)) +func cost_for_amount(owned: int, amount: int = 1) -> float: + var safe_owned: int = maxi(owned, 0) + var safe_amount: int = maxi(amount, 0) + if safe_amount == 0: + return 0.0 + if initial_cost <= 0.0: + return 0.0 + if coefficient <= 0.0: + return INF + + if is_equal_approx(coefficient, 1.0): + return initial_cost * float(safe_amount) + + var r_to_owned: float = pow(coefficient, float(safe_owned)) + var r_to_amount: float = pow(coefficient, float(safe_amount)) + return initial_cost * r_to_owned * ((r_to_amount - 1.0) / (coefficient - 1.0)) + +## Closed-form max-buy estimate from available currency (part I): +## max = floor(log((c(r-1)/(b*r^k))+1) / log(r)) +func max_affordable(owned: int, currency: float) -> int: + if currency <= 0.0: + return 0 + if initial_cost <= 0.0: + return 0 + if coefficient <= 0.0: + return 0 + + var safe_owned: int = maxi(owned, 0) + if is_equal_approx(coefficient, 1.0): + return maxi(0, floori(currency / initial_cost)) + + var denominator: float = initial_cost * pow(coefficient, float(safe_owned)) + if denominator <= 0.0: + return 0 + + var inside: float = (currency * (coefficient - 1.0) / denominator) + 1.0 + if inside <= 1.0: + return 0 + + return maxi(0, floori(log(inside) / log(coefficient))) + +func milestone_count(owned: int) -> int: + if milestone_step <= 0: + return 0 + return maxi(owned, 0) / milestone_step + +func milestone_multiplier_total(owned: int) -> float: + if milestone_multiplier <= 1.0: + return 1.0 + var count: int = milestone_count(owned) + if count <= 0: + return 1.0 + return pow(milestone_multiplier, float(count)) + +func purchased_multiplier(purchased: int) -> float: + if purchased_boost_percent <= 0.0: + return 1.0 + return 1.0 + (float(maxi(purchased, 0)) * purchased_boost_percent * 0.01) + +func production_per_cycle(owned: int, run_multiplier: float = 1.0, purchased: int = -1) -> float: + var safe_owned: int = maxi(owned, 0) + if safe_owned == 0: + return 0.0 + + var purchased_count: int = safe_owned if purchased < 0 else maxi(purchased, 0) + var milestone_mult: float = milestone_multiplier_total(safe_owned) + var purchased_mult: float = purchased_multiplier(purchased_count) + + return initial_productivity * float(safe_owned) * run_multiplier * milestone_mult * purchased_mult + +func production_per_second(owned: int, run_multiplier: float = 1.0, purchased: int = -1) -> float: + return production_per_cycle(owned, run_multiplier, purchased) / maxf(initial_time, 0.0001) ## Returns value produced func production_total(owned: int, multipliers: float) -> float: - return (initial_productivity * owned) * multipliers + return production_per_second(owned, multipliers) + +func payback_seconds(owned: int, amount: int = 1, run_multiplier: float = 1.0, purchased: int = -1) -> float: + var safe_amount: int = maxi(amount, 1) + var upgrade_cost: float = cost_for_amount(owned, safe_amount) + if not is_finite(upgrade_cost): + return INF + + var next_owned: int = maxi(owned, 0) + safe_amount + var projected_rate: float = production_per_second(next_owned, run_multiplier, purchased) + if projected_rate <= 0.0: + return INF + + return upgrade_cost / projected_rate + +func income_to_cost_ratio(owned: int, amount: int = 1, run_multiplier: float = 1.0, purchased: int = -1) -> float: + var safe_amount: int = maxi(amount, 1) + var upgrade_cost: float = cost_for_amount(owned, safe_amount) + if upgrade_cost <= 0.0 or not is_finite(upgrade_cost): + return 0.0 + + var projected_rate: float = production_per_second(maxi(owned, 0) + safe_amount, run_multiplier, purchased) + return projected_rate / upgrade_cost diff --git a/generator/primary_generator.tres b/generator/primary_generator.tres new file mode 100644 index 0000000..e96e49b --- /dev/null +++ b/generator/primary_generator.tres @@ -0,0 +1,11 @@ +[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"] + +[resource] +script = ExtResource("1_c6y77") +id = &"Gold" +name = "Gold Mine" +initial_time = 0.6 +initial_productivity = 1.67 +metadata/_custom_type_script = "uid://b00tqsuhxdy0d"