buff #1
@@ -1104,29 +1104,21 @@ func _try_complete_goal(goal_id: StringName) -> void:
|
|||||||
if _goal_completed[goal_id]:
|
if _goal_completed[goal_id]:
|
||||||
return
|
return
|
||||||
if goal.is_met():
|
if goal.is_met():
|
||||||
if _is_goal_manual_unlock(goal_id):
|
if goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL:
|
||||||
return
|
return
|
||||||
_goal_completed[goal_id] = true
|
_goal_completed[goal_id] = true
|
||||||
goal_completed.emit(goal_id)
|
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)
|
var goal: GoalData = _goal_definitions.get(goal_id, null)
|
||||||
if goal == null or not goal.has_id():
|
if goal == null:
|
||||||
return false
|
return
|
||||||
|
if _goal_completed[goal_id]:
|
||||||
var scene_root: Node = get_tree().current_scene
|
return
|
||||||
if scene_root == null:
|
if not goal.is_met():
|
||||||
return false
|
return
|
||||||
|
_goal_completed[goal_id] = true
|
||||||
var gen_nodes: Array[Node] = scene_root.find_children("*", "CurrencyGenerator", true, false)
|
goal_completed.emit(goal_id)
|
||||||
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
|
|
||||||
|
|
||||||
func _serialize_goals() -> Dictionary:
|
func _serialize_goals() -> Dictionary:
|
||||||
var result: Dictionary = {}
|
var result: Dictionary = {}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Resource class for generator balancing data.
|
|||||||
@export var name: String = "" # Display name
|
@export var name: String = "" # Display name
|
||||||
@export var starts_unlocked: bool = true # Visible at game start
|
@export var starts_unlocked: bool = true # Visible at game start
|
||||||
@export var purchase_currency: Currency # What to spend
|
@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
|
# Cost formula: cost = base * coefficient^owned
|
||||||
@export var initial_cost: float = 10.0 # Base cost
|
@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 purchase_completed(amount, total_owned, total_purchased, total_cost)
|
||||||
signal production_tick(amount, cycle_count, total_owned)
|
signal production_tick(amount, cycle_count, total_owned)
|
||||||
signal buff_purchased(buff_id, new_level, cost, cost_currency_id)
|
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
|
### Key Methods
|
||||||
@@ -102,6 +102,10 @@ func buy_max() -> int
|
|||||||
func get_cost_for_amount(amount: int) -> BigNumber
|
func get_cost_for_amount(amount: int) -> BigNumber
|
||||||
func can_buy(amount: int) -> bool
|
func can_buy(amount: int) -> bool
|
||||||
|
|
||||||
|
# Goal-based unlock (automatic when goal completes)
|
||||||
|
# Generator listens to GameState.goal_completed signal
|
||||||
|
```
|
||||||
|
|
||||||
# Production
|
# Production
|
||||||
func grant_currency(amount: BigNumber)
|
func grant_currency(amount: BigNumber)
|
||||||
func get_effective_auto_run_multiplier() -> float
|
func get_effective_auto_run_multiplier() -> float
|
||||||
@@ -184,6 +188,22 @@ UI component showing generator information.
|
|||||||
- `production_tick` - Update production display
|
- `production_tick` - Update production display
|
||||||
- `generator_buff_level_changed` - Refresh buff rows
|
- `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
|
## Prestige Integration
|
||||||
|
|
||||||
Generators automatically apply prestige multipliers:
|
Generators automatically apply prestige multipliers:
|
||||||
|
|||||||
@@ -100,11 +100,9 @@ func _ready() -> void:
|
|||||||
|
|
||||||
GameState.currency_changed.connect(_on_currency_changed)
|
GameState.currency_changed.connect(_on_currency_changed)
|
||||||
GameState.generator_state_changed.connect(_on_generated_state_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()
|
_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.
|
## Updates click cooldown/click grants and runs automatic production cycles.
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
@@ -322,9 +320,6 @@ func reset_runtime_state_for_prestige() -> void:
|
|||||||
cycle_progress_seconds = 0.0
|
cycle_progress_seconds = 0.0
|
||||||
_remaining_click_cooldown_seconds = 0.0
|
_remaining_click_cooldown_seconds = 0.0
|
||||||
_ensure_registered()
|
_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()
|
visible = is_available_to_player()
|
||||||
|
|
||||||
func get_manual_click_multiplier() -> float:
|
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:
|
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
|
||||||
GameState.evaluate_all_goals()
|
GameState.evaluate_all_goals()
|
||||||
if data != null and data.unlocks_automatically_from_goal():
|
|
||||||
_evaluate_generator_unlock_goal(false)
|
|
||||||
|
|
||||||
func try_unlock_from_goal() -> bool:
|
func _on_goal_completed(goal_id: StringName) -> void:
|
||||||
return _evaluate_generator_unlock_goal(true)
|
if data == null or not data.has_unlock_goal():
|
||||||
|
return
|
||||||
func _evaluate_generator_unlock_goal(allow_manual_unlock: bool) -> bool:
|
if data.get_unlock_goal_id() != goal_id:
|
||||||
if data == null:
|
return
|
||||||
return false
|
|
||||||
if is_available_to_player():
|
if is_available_to_player():
|
||||||
return false
|
return
|
||||||
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
|
|
||||||
|
|
||||||
GameState.set_generator_unlocked(_generator_id, true)
|
GameState.set_generator_unlocked(_generator_id, true)
|
||||||
GameState.set_generator_available(_generator_id, true)
|
GameState.set_generator_available(_generator_id, true)
|
||||||
goal_achieved.emit(_generator_id, goal_id)
|
goal_achieved.emit(_generator_id, goal_id)
|
||||||
_on_generated_state_changed(_generator_id, GameState._get_generator_state(_generator_id))
|
_on_generated_state_changed(_generator_id, GameState._get_generator_state(_generator_id))
|
||||||
return true
|
|
||||||
|
|
||||||
## Default unlocked state loaded from generator data.
|
## Default unlocked state loaded from generator data.
|
||||||
func _default_unlocked_state() -> bool:
|
func _default_unlocked_state() -> bool:
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
class_name CurrencyGeneratorData
|
class_name CurrencyGeneratorData
|
||||||
extends Resource
|
extends Resource
|
||||||
|
|
||||||
enum UnlockGoalBehavior {
|
|
||||||
AUTOMATIC,
|
|
||||||
MANUAL_BUTTON,
|
|
||||||
}
|
|
||||||
|
|
||||||
@export var id: StringName = &""
|
@export var id: StringName = &""
|
||||||
@export var name: String = ""
|
@export var name: String = ""
|
||||||
@export var starts_unlocked: bool = true
|
@export var starts_unlocked: bool = true
|
||||||
@@ -13,7 +8,6 @@ enum UnlockGoalBehavior {
|
|||||||
@export var initial_owned: int = 0
|
@export var initial_owned: int = 0
|
||||||
@export var purchase_currency: Currency
|
@export var purchase_currency: Currency
|
||||||
@export var unlock_goal: GoalData
|
@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
|
## Base cost and exponential growth (part I): cost_next = b * r^owned
|
||||||
@export var initial_cost: float = 10.0
|
@export var initial_cost: float = 10.0
|
||||||
@@ -43,12 +37,6 @@ func has_unlock_goal() -> bool:
|
|||||||
|
|
||||||
return bool(unlock_goal.is_valid())
|
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:
|
func get_unlock_goal_id() -> StringName:
|
||||||
if not has_unlock_goal():
|
if not has_unlock_goal():
|
||||||
return &""
|
return &""
|
||||||
@@ -59,9 +47,6 @@ func get_unlock_goal_id() -> StringName:
|
|||||||
|
|
||||||
return StringName(goal_id_text)
|
return StringName(goal_id_text)
|
||||||
|
|
||||||
func unlocks_automatically_from_goal() -> bool:
|
|
||||||
return unlock_goal_behavior == UnlockGoalBehavior.AUTOMATIC
|
|
||||||
|
|
||||||
## Returns cost of next generator
|
## Returns cost of next generator
|
||||||
func cost_next(owned: int) -> float:
|
func cost_next(owned: int) -> float:
|
||||||
return cost_for_amount(owned, 1)
|
return cost_for_amount(owned, 1)
|
||||||
|
|||||||
@@ -84,16 +84,6 @@ func get_unlock_goal_currency() -> Currency:
|
|||||||
|
|
||||||
return requirement_resource.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:
|
func can_purchase_next_level(current_level: int) -> bool:
|
||||||
if max_level < 0:
|
if max_level < 0:
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ extends Resource
|
|||||||
|
|
||||||
@export var id: StringName = &"" # Unique identifier
|
@export var id: StringName = &"" # Unique identifier
|
||||||
@export var requirements: Array[GoalRequirementData] # Conditions to complete
|
@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
|
### Methods
|
||||||
@@ -94,9 +104,11 @@ func _on_currency_changed(currency_id, new_amount):
|
|||||||
|
|
||||||
### Manual Unlock vs Automatic
|
### Manual Unlock vs Automatic
|
||||||
|
|
||||||
Generators can be configured to:
|
Goals control their own unlock behavior via `unlock_behavior`:
|
||||||
- **Automatic**: Unlock immediately when goal completes
|
- **AUTOMATIC**: Goal completes immediately when requirements are met
|
||||||
- **Manual**: Goal completion enables a button player must click
|
- **MANUAL**: Goal stays incomplete until player clicks unlock button
|
||||||
|
|
||||||
|
Generators with `unlock_goal` automatically unlock when the goal completes.
|
||||||
|
|
||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
@@ -130,9 +142,10 @@ amount_exponent = 6 # 1,000,000
|
|||||||
In `CurrencyGeneratorData`:
|
In `CurrencyGeneratorData`:
|
||||||
```gdscript
|
```gdscript
|
||||||
@export var unlock_goal: GoalData # Assign goal resource
|
@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
|
## Signals
|
||||||
|
|
||||||
```gdscript
|
```gdscript
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
class_name GoalData
|
class_name GoalData
|
||||||
extends Resource
|
extends Resource
|
||||||
|
|
||||||
|
enum UnlockBehavior {
|
||||||
|
AUTOMATIC,
|
||||||
|
MANUAL,
|
||||||
|
}
|
||||||
|
|
||||||
@export var id: StringName = &""
|
@export var id: StringName = &""
|
||||||
@export var requirements: Array[GoalRequirementData] = []
|
@export var requirements: Array[GoalRequirementData] = []
|
||||||
|
@export var unlock_behavior: UnlockBehavior = UnlockBehavior.MANUAL
|
||||||
|
|
||||||
func has_id() -> bool:
|
func has_id() -> bool:
|
||||||
return not String(id).strip_edges().is_empty()
|
return not String(id).strip_edges().is_empty()
|
||||||
|
|||||||
@@ -154,18 +154,14 @@ func _refresh_goal_row(row: GoalRow) -> void:
|
|||||||
var already_unlocked: bool = _is_goal_completed(goal)
|
var already_unlocked: bool = _is_goal_completed(goal)
|
||||||
var can_resolve_target: bool = _can_resolve_target(goal.target_generator_id)
|
var can_resolve_target: bool = _can_resolve_target(goal.target_generator_id)
|
||||||
var met: bool = _is_goal_met(goal)
|
var met: bool = _is_goal_met(goal)
|
||||||
var generator: CurrencyGenerator = _get_generator_for_goal(goal)
|
var can_click: bool = can_resolve_target and met and not already_unlocked
|
||||||
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
|
|
||||||
|
|
||||||
row.requirements_label.text = _get_requirements_text(goal)
|
row.requirements_label.text = _get_requirements_text(goal)
|
||||||
row.button.disabled = not can_unlock
|
row.button.disabled = not can_click
|
||||||
if already_unlocked:
|
if already_unlocked:
|
||||||
row.button.text = "Done"
|
row.button.text = "Done"
|
||||||
elif is_manual_goal:
|
|
||||||
row.button.text = "Unlock"
|
|
||||||
else:
|
else:
|
||||||
row.button.text = "Auto"
|
row.button.text = "Unlock"
|
||||||
|
|
||||||
if already_unlocked:
|
if already_unlocked:
|
||||||
row.status_label.text = "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)
|
var generator: CurrencyGenerator = _get_generator_for_goal(goal)
|
||||||
if generator == null:
|
if generator == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not generator.try_unlock_from_goal():
|
GameState.set_generator_unlocked(goal.target_generator_id, true)
|
||||||
return
|
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)])
|
print("[GoalsDebugUI] Unlocked goal '%s' -> %s" % [String(goal.goal.id), String(goal.target_generator_id)])
|
||||||
_refresh_ui()
|
_refresh_ui()
|
||||||
@@ -287,16 +285,6 @@ func _find_generator_for_goal(goal: GoalData) -> StringName:
|
|||||||
return StringName(generator_id)
|
return StringName(generator_id)
|
||||||
return &""
|
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:
|
func _get_requirements_text(goal: GoalDefinition) -> String:
|
||||||
if goal == null:
|
if goal == null:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user