Change project organization
This commit is contained in:
521
core/generator/currency_generator.gd
Normal file
521
core/generator/currency_generator.gd
Normal file
@@ -0,0 +1,521 @@
|
||||
class_name CurrencyGenerator
|
||||
extends Node2D
|
||||
|
||||
## Handles generator economy behavior:
|
||||
## - automatic production cycles
|
||||
## - manual/hover click rewards with cooldown
|
||||
## - purchasing and max-buy
|
||||
## - generator state access through the GameState autoload
|
||||
|
||||
## Emitted after a successful purchase.
|
||||
signal purchase_completed(amount: int, total_owned: int, total_purchased: int, total_cost: BigNumber)
|
||||
## Emitted when a purchase is attempted but cannot be paid.
|
||||
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
|
||||
|
||||
## Currency target updated by this generator.
|
||||
@export var currency: Resource
|
||||
## Data resource containing balancing values and defaults (required).
|
||||
@export var data: CurrencyGeneratorData
|
||||
## Enables per-cycle automatic production when true.
|
||||
@export var is_automatic: bool = true
|
||||
## If true, `_on_pressed` buys one generator instead of granting click currency.
|
||||
@export var press_buys_generator: bool = true
|
||||
## If true, hovering grants click reward every cooldown without pressing.
|
||||
@export var grants_click_while_hovering: bool = false
|
||||
## Extra multiplier applied to automatic production output.
|
||||
@export var run_multiplier: float = 1.0
|
||||
|
||||
|
||||
|
||||
## True while the pointer is inside the generator Area2D.
|
||||
var _mouse_entered: bool = false
|
||||
## Time left until next click/hover grant is allowed.
|
||||
var _remaining_click_cooldown_seconds: float = 0.0
|
||||
|
||||
## Number of currently owned generators.
|
||||
## Backed by GameState via this generator's resolved id.
|
||||
var owned: int:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.get_generator_owned(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_owned(_generator_id, value)
|
||||
|
||||
## Lifetime purchased generators (does not decrease if ownership drops).
|
||||
var purchased_count: int:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.get_generator_purchased_count(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_purchased_count(_generator_id, value)
|
||||
|
||||
## Whether this generator is unlocked for the player.
|
||||
var is_unlocked: bool:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.is_generator_unlocked(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_unlocked(_generator_id, value)
|
||||
|
||||
## Whether this unlocked generator is currently visible/usable to the player.
|
||||
var is_available: bool:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.is_generator_available(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_available(_generator_id, value)
|
||||
|
||||
## Accumulated elapsed time toward the next automatic production cycle.
|
||||
var cycle_progress_seconds: float = 0.0
|
||||
## Stable id used to store/read generator state in GameState.
|
||||
var _generator_id: StringName = &""
|
||||
## Prevents duplicate state registration in GameState.
|
||||
var _is_registered: bool = false
|
||||
|
||||
|
||||
## Resolves and registers this generator state before gameplay updates.
|
||||
func _ready() -> void:
|
||||
assert(currency != null, "Currency cannot be null")
|
||||
assert(data != null, "Data cannot be null")
|
||||
|
||||
_generator_id = _resolve_generator_id()
|
||||
_ensure_registered()
|
||||
cycle_progress_seconds = 0.0
|
||||
|
||||
GameState.generator_state_changed.connect(_on_generated_state_changed)
|
||||
|
||||
## Updates click cooldown/click grants and runs automatic production cycles.
|
||||
func _process(delta: float) -> void:
|
||||
_remaining_click_cooldown_seconds = maxf(_remaining_click_cooldown_seconds - maxf(delta, 0.0), 0.0)
|
||||
_try_handle_mouse_click()
|
||||
|
||||
if not is_automatic:
|
||||
return
|
||||
if not is_available_to_player():
|
||||
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)
|
||||
|
||||
## Adds automatic production from completed cycles and emits `production_tick`.
|
||||
func _grant_cycle_income(cycle_count: int) -> void:
|
||||
if data == null or cycle_count <= 0:
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
## Convenience helper for the next single-unit purchase cost.
|
||||
func get_next_cost() -> BigNumber:
|
||||
return get_cost_for_amount(1)
|
||||
|
||||
## Returns cumulative cost to buy `amount` units from current ownership.
|
||||
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)
|
||||
|
||||
## Checks whether enough target currency is available for `amount` units.
|
||||
func can_buy(amount: int = 1) -> bool:
|
||||
if not is_available_to_player():
|
||||
return false
|
||||
|
||||
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)
|
||||
|
||||
## Attempts to buy `amount` units and emits purchase success/failure signals.
|
||||
func buy(amount: int = 1) -> bool:
|
||||
if data == null:
|
||||
return false
|
||||
if not is_available_to_player():
|
||||
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
|
||||
|
||||
## Computes and buys the maximum affordable units with current currency.
|
||||
func buy_max() -> int:
|
||||
if data == null:
|
||||
return 0
|
||||
if not is_available_to_player():
|
||||
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 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():
|
||||
return
|
||||
|
||||
if press_buys_generator:
|
||||
buy(1)
|
||||
return
|
||||
|
||||
_try_grant_click_currency()
|
||||
|
||||
## Adds currency directly through the configured currency target.
|
||||
func grant_currency(amount: BigNumber) -> void:
|
||||
_add_currency(amount)
|
||||
|
||||
## Processes pointer interaction while hovering (hold click or hover-only mode).
|
||||
func _try_handle_mouse_click() -> void:
|
||||
if not _mouse_entered:
|
||||
return
|
||||
if not is_available_to_player():
|
||||
return
|
||||
if not (_grants_click_while_hovering() or Input.is_action_pressed("click")):
|
||||
return
|
||||
|
||||
_try_grant_click_currency()
|
||||
|
||||
## Grants one click/hover reward if cooldown has elapsed.
|
||||
func _try_grant_click_currency() -> void:
|
||||
if data == null:
|
||||
return
|
||||
if _remaining_click_cooldown_seconds > 0.0:
|
||||
return
|
||||
|
||||
_add_currency(_get_click_value())
|
||||
_remaining_click_cooldown_seconds = _get_click_cooldown_seconds()
|
||||
|
||||
## Resolves click reward from data.
|
||||
func _get_click_value() -> BigNumber:
|
||||
if data == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
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:
|
||||
if data == null:
|
||||
return 0.0
|
||||
return maxf(data.click_cooldown_seconds, 0.0)
|
||||
|
||||
## Resolves hover-only click-grant mode from node configuration.
|
||||
func _grants_click_while_hovering() -> bool:
|
||||
return grants_click_while_hovering
|
||||
|
||||
## Returns this generator's resolved state id.
|
||||
func get_generator_id() -> StringName:
|
||||
_ensure_registered()
|
||||
return _generator_id
|
||||
|
||||
## Returns the configured currency id.
|
||||
func get_currency_id() -> StringName:
|
||||
return GameState.get_currency_id(currency)
|
||||
|
||||
## True when the generator is both unlocked and available.
|
||||
func is_available_to_player() -> bool:
|
||||
return is_unlocked and is_available
|
||||
|
||||
## Routes positive/negative currency deltas to the configured currency bucket.
|
||||
func _add_currency(amount: BigNumber) -> void:
|
||||
if currency == null:
|
||||
push_error("CurrencyGenerator '%s' cannot add currency: currency resource is null." % String(name))
|
||||
return
|
||||
|
||||
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:
|
||||
push_error("CurrencyGenerator '%s' cannot spend currency: currency resource is null." % String(name))
|
||||
return false
|
||||
|
||||
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:
|
||||
return BigNumber.from_float(0.0)
|
||||
return GameState.get_currency_amount(currency)
|
||||
|
||||
## Safely converts finite float values into BigNumber costs.
|
||||
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)
|
||||
|
||||
## Converts BigNumber to float for approximate estimate math.
|
||||
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))
|
||||
|
||||
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:
|
||||
var data_id: String = String(data.id)
|
||||
if not data_id.is_empty():
|
||||
return StringName(data_id)
|
||||
|
||||
if not String(name).is_empty():
|
||||
return StringName(String(name))
|
||||
|
||||
return &"generator"
|
||||
|
||||
## Ensures this generator has a state entry in GameState.
|
||||
func _ensure_registered() -> void:
|
||||
if _is_registered:
|
||||
return
|
||||
|
||||
if _generator_id == &"":
|
||||
_generator_id = _resolve_generator_id()
|
||||
if _generator_id == &"":
|
||||
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:
|
||||
return false
|
||||
return data.starts_unlocked
|
||||
|
||||
## Default available state loaded from generator data.
|
||||
func _default_available_state() -> bool:
|
||||
if data == null:
|
||||
return false
|
||||
return data.starts_available
|
||||
|
||||
## Area2D callback: pointer entered generator interaction region.
|
||||
func _on_area_2d_mouse_entered() -> void:
|
||||
_mouse_entered = true
|
||||
|
||||
## Area2D callback: pointer exited generator interaction region.
|
||||
func _on_area_2d_mouse_exited() -> void:
|
||||
_mouse_entered = false
|
||||
|
||||
func _on_generated_state_changed(generator_id: StringName, _state: Dictionary) -> void:
|
||||
if generator_id == _generator_id:
|
||||
visible = true
|
||||
1
core/generator/currency_generator.gd.uid
Normal file
1
core/generator/currency_generator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtbxopw6ulhl8
|
||||
25
core/generator/currency_generator.tscn
Normal file
25
core/generator/currency_generator.tscn
Normal file
@@ -0,0 +1,25 @@
|
||||
[gd_scene format=3 uid="uid://jeoiinukrrsp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dtbxopw6ulhl8" path="res://core/generator/currency_generator.gd" id="1_4n4ca"]
|
||||
[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_5tmvy"]
|
||||
[ext_resource type="Resource" uid="uid://co0mcc2kvcpo5" path="res://idles/generators/primary_generator.tres" id="3_wx13b"]
|
||||
[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="4_ruf1h"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_y5m1q"]
|
||||
size = Vector2(128, 128)
|
||||
|
||||
[node name="CurrencyGenerator" type="Node2D" unique_id=967969064]
|
||||
script = ExtResource("1_4n4ca")
|
||||
currency = ExtResource("2_5tmvy")
|
||||
data = ExtResource("3_wx13b")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=807584513]
|
||||
texture = ExtResource("4_ruf1h")
|
||||
|
||||
[node name="Area2D" type="Area2D" parent="." unique_id=707349247]
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1572620025]
|
||||
shape = SubResource("RectangleShape2D_y5m1q")
|
||||
|
||||
[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"]
|
||||
150
core/generator/currency_generator_data.gd
Normal file
150
core/generator/currency_generator_data.gd
Normal file
@@ -0,0 +1,150 @@
|
||||
class_name CurrencyGeneratorData
|
||||
extends Resource
|
||||
|
||||
@export var id: StringName = &""
|
||||
@export var name: String = ""
|
||||
@export var starts_unlocked: bool = true
|
||||
@export var starts_available: bool = true
|
||||
|
||||
## 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
|
||||
|
||||
## Manual click reward for this generator.
|
||||
@export var click_mantissa: float = 1.0
|
||||
@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)
|
||||
|
||||
## 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 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
|
||||
1
core/generator/currency_generator_data.gd.uid
Normal file
1
core/generator/currency_generator_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b00tqsuhxdy0d
|
||||
95
core/generator/generator_buff_data.gd
Normal file
95
core/generator/generator_buff_data.gd
Normal file
@@ -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))
|
||||
1
core/generator/generator_buff_data.gd.uid
Normal file
1
core/generator/generator_buff_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dnhocfsuvmeyb
|
||||
Reference in New Issue
Block a user