Add generator logic
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
## 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
|
||||
|
||||
11
generator/primary_generator.tres
Normal file
11
generator/primary_generator.tres
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user