diff --git a/generator/currency_generator.gd b/generator/currency_generator.gd index b3685e5..bc4a111 100644 --- a/generator/currency_generator.gd +++ b/generator/currency_generator.gd @@ -1,24 +1,53 @@ class_name CurrencyGenerator extends Node +## 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) +## Sentinel exponent used when cost math overflows float range. const HUGE_COST_EXPONENT: int = 1000000 +## Currency target updated by this generator. @export var currency: GameState.CurrencyType = GameState.CurrencyType.gold +## Data resource containing balancing values and defaults. @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 +## Extra multiplier applied to automatic production output. @export var run_multiplier: float = 1.0 +## Fallback unlocked state when `data` is not assigned. @export var starts_unlocked: bool = true +## Fallback availability state when `data` is not assigned. @export var starts_available: bool = true -## Manual click fallback when no data is assigned. +## Manual click reward fallback when no data is assigned. @export var click_mantissa: float = 1.0 +## Exponent for fallback click reward when no data is assigned. @export var click_exponent: int = 0 +## Cooldown between click/hover grants when no data is assigned. +@export var click_cooldown_seconds: float = 0.2 +## If true, hovering grants click reward every cooldown without pressing. +@export var grants_click_while_hovering: bool = false +## 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() @@ -27,6 +56,7 @@ var owned: int: _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() @@ -35,6 +65,7 @@ var purchased_count: int: _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() @@ -43,6 +74,7 @@ var is_unlocked: bool: _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() @@ -51,16 +83,25 @@ var is_available: bool: _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: _generator_id = _resolve_generator_id() _ensure_registered() cycle_progress_seconds = 0.0 +## 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(): @@ -77,6 +118,7 @@ func _process(delta: float) -> void: 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 @@ -89,9 +131,11 @@ func _grant_cycle_income(cycle_count: int) -> void: _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) @@ -100,6 +144,7 @@ func get_cost_for_amount(amount: int = 1) -> BigNumber: 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 @@ -108,6 +153,7 @@ func can_buy(amount: int = 1) -> bool: 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 @@ -125,6 +171,7 @@ func buy(amount: int = 1) -> bool: 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 @@ -152,6 +199,7 @@ func buy_max() -> int: return 0 return best if buy(best) else 0 +## Handles external "pressed" interaction: buy if configured, otherwise click-grant. func _on_pressed() -> void: if not is_available_to_player(): return @@ -160,18 +208,59 @@ func _on_pressed() -> void: buy(1) return - _add_currency(BigNumber.new(click_mantissa, click_exponent)) + _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 _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, with exported fallback when data is missing. +func _get_click_value() -> BigNumber: + if data != null: + return BigNumber.new(data.click_mantissa, data.click_exponent) + return BigNumber.new(click_mantissa, click_exponent) + +## Resolves click cooldown from data, with exported fallback when data is missing. +func _get_click_cooldown_seconds() -> float: + if data != null: + return maxf(data.click_cooldown_seconds, 0.0) + return maxf(click_cooldown_seconds, 0.0) + +## Resolves hover-only click-grant mode from data, with exported fallback. +func _grants_click_while_hovering() -> bool: + if data != null: + return data.grants_click_while_hovering + return grants_click_while_hovering + +## Returns this generator's resolved state id. func get_generator_id() -> StringName: _ensure_registered() return _generator_id +## 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: match currency: GameState.CurrencyType.gold: @@ -181,6 +270,7 @@ func _add_currency(amount: BigNumber) -> void: _: push_error("Unknown currency type in CurrencyGenerator._add_currency") +## Attempts to spend target currency; returns false when insufficient. func _spend_currency(cost: BigNumber) -> bool: match currency: GameState.CurrencyType.gold: @@ -191,6 +281,7 @@ func _spend_currency(cost: BigNumber) -> bool: push_error("Unknown currency type in CurrencyGenerator._spend_currency") return false +## Returns the current amount for the configured currency type. func _get_currency_amount() -> BigNumber: match currency: GameState.CurrencyType.gold: @@ -200,6 +291,7 @@ func _get_currency_amount() -> BigNumber: _: return BigNumber.from_float(0.0) +## 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) @@ -207,6 +299,7 @@ func _float_to_big_number(value: float) -> BigNumber: 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 @@ -216,6 +309,7 @@ func _big_number_to_float(value: BigNumber) -> float: return 0.0 return value.mantissa * pow(10.0, float(value.exponent)) +## 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) @@ -227,6 +321,7 @@ func _resolve_generator_id() -> StringName: return &"generator" +## Ensures this generator has a state entry in GameState. func _ensure_registered() -> void: if _is_registered: return @@ -239,12 +334,22 @@ func _ensure_registered() -> void: GameState.register_generator(_generator_id, _default_unlocked_state(), _default_available_state()) _is_registered = true +## Default unlocked state used at registration time. func _default_unlocked_state() -> bool: if data != null: return data.starts_unlocked return starts_unlocked +## Default available state used at registration time. func _default_available_state() -> bool: if data != null: return data.starts_available return 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 diff --git a/generator/currency_generator_data.gd b/generator/currency_generator_data.gd index f60fdc3..a4cb425 100644 --- a/generator/currency_generator_data.gd +++ b/generator/currency_generator_data.gd @@ -23,6 +23,12 @@ extends Resource ## 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 +@export var grants_click_while_hovering: bool = false + ## Returns cost of next generator func cost_next(owned: int) -> float: return cost_for_amount(owned, 1) diff --git a/generator/primary_generator.tres b/generator/primary_generator.tres index e96e49b..95a8eeb 100644 --- a/generator/primary_generator.tres +++ b/generator/primary_generator.tres @@ -8,4 +8,5 @@ id = &"Gold" name = "Gold Mine" initial_time = 0.6 initial_productivity = 1.67 +grants_click_while_hovering = true metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/generator_museum.tscn b/generator_museum.tscn index 9c75700..79c4695 100644 --- a/generator_museum.tscn +++ b/generator_museum.tscn @@ -3,15 +3,28 @@ [ext_resource type="Script" uid="uid://dtbxopw6ulhl8" path="res://generator/currency_generator.gd" id="1_pl17p"] [ext_resource type="Resource" uid="uid://co0mcc2kvcpo5" path="res://generator/primary_generator.tres" id="2_x5lt1"] [ext_resource type="Resource" uid="uid://04pmc034qupd" path="res://generator/secondary_generator.tres" id="3_dc82g"] +[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="3_mhw8n"] [ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://generator_container.tscn" id="3_pw2a0"] [ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="3_x5lt1"] +[sub_resource type="RectangleShape2D" id="RectangleShape2D_y5m1q"] +size = Vector2(128, 128) + [node name="GeneratorMuseum" type="Node2D" unique_id=1219373683] [node name="GoldGenerator" type="Node2D" parent="." unique_id=964187690] +position = Vector2(566, 455) script = ExtResource("1_pl17p") data = ExtResource("2_x5lt1") +[node name="Sprite2D" type="Sprite2D" parent="GoldGenerator" unique_id=381586216] +texture = ExtResource("3_mhw8n") + +[node name="Area2D" type="Area2D" parent="GoldGenerator" unique_id=2084873356] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="GoldGenerator/Area2D" unique_id=726858258] +shape = SubResource("RectangleShape2D_y5m1q") + [node name="GemsGenerator" type="Node2D" parent="." unique_id=931294956] script = ExtResource("1_pl17p") currency = 1 @@ -52,3 +65,6 @@ offset_top = 215.0 offset_right = 247.0 offset_bottom = 380.0 _generator = NodePath("../../GemsGenerator") + +[connection signal="mouse_entered" from="GoldGenerator/Area2D" to="GoldGenerator" method="_on_area_2d_mouse_entered"] +[connection signal="mouse_exited" from="GoldGenerator/Area2D" to="GoldGenerator" method="_on_area_2d_mouse_exited"] diff --git a/project.godot b/project.godot index 2c6d2aa..08d72b0 100644 --- a/project.godot +++ b/project.godot @@ -18,6 +18,14 @@ config/icon="res://icon.svg" GameState="*uid://d2j7tvlgxr2jp" +[input] + +click={ +"deadzone": 1.0, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +] +} + [physics] 3d/physics_engine="Jolt Physics"