diff --git a/core/game_state.gd b/core/game_state.gd index 27100a0..406309e 100644 --- a/core/game_state.gd +++ b/core/game_state.gd @@ -1104,29 +1104,21 @@ func _try_complete_goal(goal_id: StringName) -> void: if _goal_completed[goal_id]: return if goal.is_met(): - if _is_goal_manual_unlock(goal_id): + if goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL: return _goal_completed[goal_id] = true goal_completed.emit(goal_id) -func _is_goal_manual_unlock(goal_id: StringName) -> bool: +func _complete_goal_manually(goal_id: StringName) -> void: var goal: GoalData = _goal_definitions.get(goal_id, null) - if goal == null or not goal.has_id(): - return false - - var scene_root: Node = get_tree().current_scene - if scene_root == null: - return false - - var gen_nodes: Array[Node] = scene_root.find_children("*", "CurrencyGenerator", true, false) - for node in gen_nodes: - if node.has_method("get_generator_id") and node.has_method("data"): - var data: Variant = node.get("data") - if data != null and data.has_method("has_unlock_goal") and data.has_method("unlocks_automatically_from_goal"): - if data.has_unlock_goal() and data.get_unlock_goal_id() == goal_id: - return not data.unlocks_automatically_from_goal() - - return false + if goal == null: + return + if _goal_completed[goal_id]: + return + if not goal.is_met(): + return + _goal_completed[goal_id] = true + goal_completed.emit(goal_id) func _serialize_goals() -> Dictionary: var result: Dictionary = {} diff --git a/core/generator/README.md b/core/generator/README.md index cec85f6..5e544c8 100644 --- a/core/generator/README.md +++ b/core/generator/README.md @@ -36,7 +36,7 @@ Resource class for generator balancing data. @export var name: String = "" # Display name @export var starts_unlocked: bool = true # Visible at game start @export var purchase_currency: Currency # What to spend -@export var unlock_goal: GoalData # Goal to unlock this +@export var unlock_goal: GoalData # Goal to unlock this (behavior defined in GoalData) # Cost formula: cost = base * coefficient^owned @export var initial_cost: float = 10.0 # Base cost @@ -90,7 +90,7 @@ Runtime node instance attached to generator scenes. signal purchase_completed(amount, total_owned, total_purchased, total_cost) signal production_tick(amount, cycle_count, total_owned) signal buff_purchased(buff_id, new_level, cost, cost_currency_id) -signal goal_achieved(generator_id, goal_id) +signal goal_achieved(generator_id, goal_id) # Emitted when generator unlocks from goal ``` ### Key Methods @@ -102,6 +102,10 @@ func buy_max() -> int func get_cost_for_amount(amount: int) -> BigNumber func can_buy(amount: int) -> bool +# Goal-based unlock (automatic when goal completes) +# Generator listens to GameState.goal_completed signal +``` + # Production func grant_currency(amount: BigNumber) func get_effective_auto_run_multiplier() -> float @@ -184,6 +188,22 @@ UI component showing generator information. - `production_tick` - Update production display - `generator_buff_level_changed` - Refresh buff rows +## Goal-Based Unlock + +Generators with `unlock_goal` automatically listen to `GameState.goal_completed` signal: +- When goal completes, generator unlocks automatically +- Goal's `unlock_behavior` (AUTOMATIC/MANUAL) controls when goal completes +- Manual goals require player to click "Unlock" button first + +```gdscript +# In CurrencyGenerator._ready(): +GameState.goal_completed.connect(_on_goal_completed) + +func _on_goal_completed(goal_id: StringName) -> void: + if data.has_unlock_goal() and data.get_unlock_goal_id() == goal_id: + unlock_generator() +``` + ## Prestige Integration Generators automatically apply prestige multipliers: diff --git a/core/generator/currency_generator.gd b/core/generator/currency_generator.gd index b5f2073..79907a0 100644 --- a/core/generator/currency_generator.gd +++ b/core/generator/currency_generator.gd @@ -100,11 +100,9 @@ func _ready() -> void: GameState.currency_changed.connect(_on_currency_changed) GameState.generator_state_changed.connect(_on_generated_state_changed) + GameState.goal_completed.connect(_on_goal_completed) - # Register with default state first, then evaluate unlock goal _ensure_registered() - if data != null and data.unlocks_automatically_from_goal(): - _evaluate_generator_unlock_goal(false) ## Updates click cooldown/click grants and runs automatic production cycles. func _process(delta: float) -> void: @@ -322,9 +320,6 @@ func reset_runtime_state_for_prestige() -> void: cycle_progress_seconds = 0.0 _remaining_click_cooldown_seconds = 0.0 _ensure_registered() - if data != null and data.unlocks_automatically_from_goal(): - _evaluate_generator_unlock_goal(false) - #_evaluate_buff_unlock_goals() visible = is_available_to_player() func get_manual_click_multiplier() -> float: @@ -583,35 +578,19 @@ func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void: func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void: GameState.evaluate_all_goals() - if data != null and data.unlocks_automatically_from_goal(): - _evaluate_generator_unlock_goal(false) -func try_unlock_from_goal() -> bool: - return _evaluate_generator_unlock_goal(true) - -func _evaluate_generator_unlock_goal(allow_manual_unlock: bool) -> bool: - if data == null: - return false +func _on_goal_completed(goal_id: StringName) -> void: + if data == null or not data.has_unlock_goal(): + return + if data.get_unlock_goal_id() != goal_id: + return if is_available_to_player(): - return false - if not data.has_unlock_goal(): - return false - if not data.unlocks_automatically_from_goal() and not allow_manual_unlock: - return false - - var goal_id: StringName = data.get_unlock_goal_id() - if goal_id.is_empty(): - return false - - var goal_completed: bool = GameState.is_goal_completed(goal_id) - if not goal_completed: - return false + return GameState.set_generator_unlocked(_generator_id, true) GameState.set_generator_available(_generator_id, true) goal_achieved.emit(_generator_id, goal_id) _on_generated_state_changed(_generator_id, GameState._get_generator_state(_generator_id)) - return true ## Default unlocked state loaded from generator data. func _default_unlocked_state() -> bool: diff --git a/core/generator/currency_generator_data.gd b/core/generator/currency_generator_data.gd index d3717e0..338ea63 100644 --- a/core/generator/currency_generator_data.gd +++ b/core/generator/currency_generator_data.gd @@ -1,11 +1,6 @@ class_name CurrencyGeneratorData extends Resource -enum UnlockGoalBehavior { - AUTOMATIC, - MANUAL_BUTTON, -} - @export var id: StringName = &"" @export var name: String = "" @export var starts_unlocked: bool = true @@ -13,7 +8,6 @@ enum UnlockGoalBehavior { @export var initial_owned: int = 0 @export var purchase_currency: Currency @export var unlock_goal: GoalData -@export var unlock_goal_behavior: UnlockGoalBehavior = UnlockGoalBehavior.AUTOMATIC ## Base cost and exponential growth (part I): cost_next = b * r^owned @export var initial_cost: float = 10.0 @@ -43,12 +37,6 @@ func has_unlock_goal() -> bool: return bool(unlock_goal.is_valid()) -func is_unlock_goal_met() -> bool: - if not has_unlock_goal(): - return false - - return bool(unlock_goal.is_met()) - func get_unlock_goal_id() -> StringName: if not has_unlock_goal(): return &"" @@ -59,9 +47,6 @@ func get_unlock_goal_id() -> StringName: return StringName(goal_id_text) -func unlocks_automatically_from_goal() -> bool: - return unlock_goal_behavior == UnlockGoalBehavior.AUTOMATIC - ## Returns cost of next generator func cost_next(owned: int) -> float: return cost_for_amount(owned, 1) diff --git a/core/generator/generator_buff_data.gd b/core/generator/generator_buff_data.gd index 4c00adf..c92d936 100644 --- a/core/generator/generator_buff_data.gd +++ b/core/generator/generator_buff_data.gd @@ -84,16 +84,6 @@ func get_unlock_goal_currency() -> Currency: return requirement_resource.currency -func is_unlock_goal_met() -> bool: - if unlock_goal == null: - return false - - var goal_id: StringName = unlock_goal.id - if goal_id.is_empty(): - return false - - return GameState.is_goal_completed(goal_id) - func can_purchase_next_level(current_level: int) -> bool: if max_level < 0: return true diff --git a/core/goals/README.md b/core/goals/README.md index 22b1e3a..a4ba440 100644 --- a/core/goals/README.md +++ b/core/goals/README.md @@ -23,6 +23,16 @@ extends Resource @export var id: StringName = &"" # Unique identifier @export var requirements: Array[GoalRequirementData] # Conditions to complete +@export var unlock_behavior: UnlockBehavior = MANUAL # AUTOMATIC or MANUAL +``` + +### Enum + +```gdscript +enum UnlockBehavior { + AUTOMATIC, # Goal completes immediately when met + MANUAL, # Goal requires player to click unlock button +} ``` ### Methods @@ -94,9 +104,11 @@ func _on_currency_changed(currency_id, new_amount): ### Manual Unlock vs Automatic -Generators can be configured to: -- **Automatic**: Unlock immediately when goal completes -- **Manual**: Goal completion enables a button player must click +Goals control their own unlock behavior via `unlock_behavior`: +- **AUTOMATIC**: Goal completes immediately when requirements are met +- **MANUAL**: Goal stays incomplete until player clicks unlock button + +Generators with `unlock_goal` automatically unlock when the goal completes. ## Usage Example @@ -130,9 +142,10 @@ amount_exponent = 6 # 1,000,000 In `CurrencyGeneratorData`: ```gdscript @export var unlock_goal: GoalData # Assign goal resource -@export var unlock_goal_behavior: UnlockGoalBehavior = AUTOMATIC ``` +The goal's `unlock_behavior` determines if the generator unlocks automatically or requires manual button click. + ## Signals ```gdscript diff --git a/core/goals/goal_data.gd b/core/goals/goal_data.gd index 31ae374..559b84c 100644 --- a/core/goals/goal_data.gd +++ b/core/goals/goal_data.gd @@ -1,8 +1,14 @@ class_name GoalData extends Resource +enum UnlockBehavior { + AUTOMATIC, + MANUAL, +} + @export var id: StringName = &"" @export var requirements: Array[GoalRequirementData] = [] +@export var unlock_behavior: UnlockBehavior = UnlockBehavior.MANUAL func has_id() -> bool: return not String(id).strip_edges().is_empty() diff --git a/goals_debug_ui.gd b/goals_debug_ui.gd index c628481..13bfecf 100644 --- a/goals_debug_ui.gd +++ b/goals_debug_ui.gd @@ -154,18 +154,14 @@ func _refresh_goal_row(row: GoalRow) -> void: var already_unlocked: bool = _is_goal_completed(goal) var can_resolve_target: bool = _can_resolve_target(goal.target_generator_id) var met: bool = _is_goal_met(goal) - var generator: CurrencyGenerator = _get_generator_for_goal(goal) - var is_manual_goal: bool = _is_manual_goal_generator(generator) - var can_unlock: bool = can_resolve_target and met and not already_unlocked and is_manual_goal + var can_click: bool = can_resolve_target and met and not already_unlocked row.requirements_label.text = _get_requirements_text(goal) - row.button.disabled = not can_unlock + row.button.disabled = not can_click if already_unlocked: row.button.text = "Done" - elif is_manual_goal: - row.button.text = "Unlock" else: - row.button.text = "Auto" + row.button.text = "Unlock" if already_unlocked: row.status_label.text = "Unlocked" @@ -210,9 +206,11 @@ func _on_unlock_pressed(goal_id: StringName) -> void: var generator: CurrencyGenerator = _get_generator_for_goal(goal) if generator == null: return - - if not generator.try_unlock_from_goal(): - return + + GameState.set_generator_unlocked(goal.target_generator_id, true) + GameState.set_generator_available(goal.target_generator_id, true) + if goal.goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL: + GameState._complete_goal_manually(goal.goal.id) print("[GoalsDebugUI] Unlocked goal '%s' -> %s" % [String(goal.goal.id), String(goal.target_generator_id)]) _refresh_ui() @@ -287,16 +285,6 @@ func _find_generator_for_goal(goal: GoalData) -> StringName: return StringName(generator_id) return &"" -func _is_manual_goal_generator(generator: CurrencyGenerator) -> bool: - if generator == null: - return false - if generator.data == null: - return false - if not generator.data.has_unlock_goal(): - return false - - return not generator.data.unlocks_automatically_from_goal() - func _get_requirements_text(goal: GoalDefinition) -> String: if goal == null: return ""