Use GoalData for everything

This commit is contained in:
2026-03-21 12:11:38 +01:00
parent 52c23b194e
commit 471a7b10f7
15 changed files with 205 additions and 417 deletions

View File

@@ -20,7 +20,7 @@ The focus below is on behavior-bearing files and how they interact.
2. Two autoload services power the runtime: `CurrencyDatabase` and `GameState`. 2. Two autoload services power the runtime: `CurrencyDatabase` and `GameState`.
3. `BigNumber` is the numeric backbone used by currency amounts, costs, goals, production, and persistence. 3. `BigNumber` is the numeric backbone used by currency amounts, costs, goals, production, and persistence.
4. Generator behavior is runtime-driven by `CurrencyGenerator`, parameterized by `CurrencyGeneratorData` and `GeneratorBuffData` resources. 4. Generator behavior is runtime-driven by `CurrencyGenerator`, parameterized by `CurrencyGeneratorData` and `GeneratorBuffData` resources.
5. Goal-based progression uses reusable goal primitives (`GoalData`, `GoalRequirementData`) plus unlock bindings (`GeneratorUnlockGoalData`) and runtime evaluator (`generator_unlock_system.gd`). 5. Goal-based progression uses reusable goal primitives (`GoalData`, `GoalRequirementData`) directly on generator data (`CurrencyGeneratorData.unlock_goal`).
6. UI scripts are thin adapters listening to `GameState` signals. 6. UI scripts are thin adapters listening to `GameState` signals.
## Core Systems ## Core Systems
@@ -102,6 +102,7 @@ Files: `core/generator/currency_generator.gd`, `core/generator/currency_generato
5. Purchased-count multiplier. 5. Purchased-count multiplier.
6. Production/cycle and production/second helpers. 6. Production/cycle and production/second helpers.
7. ROI-like helpers (`payback_seconds`, `income_to_cost_ratio`). 7. ROI-like helpers (`payback_seconds`, `income_to_cost_ratio`).
8. Optional generator unlock goal (`unlock_goal`).
### 5) Buff System ### 5) Buff System
@@ -126,16 +127,16 @@ Files:
1. `core/goals/goal_requirement_data.gd` 1. `core/goals/goal_requirement_data.gd`
2. `core/goals/goal_data.gd` 2. `core/goals/goal_data.gd`
3. `core/generator-unlock-goals/generator_unlock_goal_data.gd` 3. `core/generator/currency_generator_data.gd`
4. `core/generator-unlock-goals/generator_unlock_system.gd` 4. `core/generator/currency_generator.gd`
5. `idles/goals/*.tres` 5. `idles/goals/*.tres`
Key behavior: Key behavior:
1. Goal requirements validate currency + target amount. 1. Goal requirements validate currency + target amount.
2. Requirement completion checks **total acquired currency**, not current wallet. 2. Requirement completion checks **total acquired currency**, not current wallet.
3. Unlock system evaluates goals on currency change and generator state changes. 3. Each `CurrencyGenerator` evaluates its own `data.unlock_goal` on startup and on `currency_changed`.
4. If a goal is met and target generator is resolvable, it sets both `unlocked = true` and `available = true`. 4. If the goal is met, it sets both `unlocked = true` and `available = true`, then emits `goal_achieved(generator_id, goal_id)`.
### 7) UI Adapter Layer ### 7) UI Adapter Layer
@@ -167,7 +168,6 @@ It composes:
2. `KnowledgeGenerator` instance (initially hidden and starts locked via data). 2. `KnowledgeGenerator` instance (initially hidden and starts locked via data).
3. Currency tiles for `magic` and `knowledge`. 3. Currency tiles for `magic` and `knowledge`.
4. Goals debug UI panel. 4. Goals debug UI panel.
5. Runtime `GeneratorUnlockSystem` node with configured unlock goals.
### Legacy/Secondary Museum ### Legacy/Secondary Museum
@@ -180,7 +180,7 @@ This appears to be an older/placeholder scene and not the current gameplay focus
### Generators ### Generators
1. `primary_generator.tres`: `Magic Orb`, active from start, includes 3 buffs. 1. `primary_generator.tres`: `Magic Orb`, active from start, includes 3 buffs.
2. `secondary_generator.tres`: `Library`, starts locked/unavailable. 2. `secondary_generator.tres`: `Library`, starts locked/unavailable and unlocks via `magic_total_30` goal.
### Buffs (Magic) ### Buffs (Magic)
@@ -192,8 +192,6 @@ This appears to be an older/placeholder scene and not the current gameplay focus
1. `magic_total_30.tres`: reach total magic 30. 1. `magic_total_30.tres`: reach total magic 30.
2. `magic_total_1300.tres`: reach total magic 1300. 2. `magic_total_1300.tres`: reach total magic 1300.
3. `generator_unlock_knowledge.tres`: maps `magic_total_30` to generator `Knowledge`.
4. `generator_unlock_wood.tres`: maps `magic_total_1300` to generator `Wood`.
## End-to-End Runtime Flow ## End-to-End Runtime Flow
@@ -212,8 +210,8 @@ This appears to be an older/placeholder scene and not the current gameplay focus
| Signal | Declared In | Emitted By | Emitted When | Listeners | | Signal | Declared In | Emitted By | Emitted When | Listeners |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `currency_changed(currency_id, new_amount)` | `core/game_state.gd` | `GameState.add_currency_by_id`, `GameState.spend_currency_by_id` | Any currency balance increases/decreases | `CurrencyGenerator._on_currency_changed`, `GeneratorPanel._on_currency_changed`, `CurrencyTile._on_currency_changed`, `BigNumberProgressBar._on_currency_changed`, `GeneratorUnlockSystem._on_currency_changed`, `GoalsDebugUI._on_currency_changed` | | `currency_changed(currency_id, new_amount)` | `core/game_state.gd` | `GameState.add_currency_by_id`, `GameState.spend_currency_by_id` | Any currency balance increases/decreases | `CurrencyGenerator._on_currency_changed`, `GeneratorPanel._on_currency_changed`, `CurrencyTile._on_currency_changed`, `BigNumberProgressBar._on_currency_changed`, `GoalsDebugUI._on_currency_changed` |
| `generator_state_changed(generator_id, state)` | `core/game_state.gd` | `GameState.register_generator`, `GameState._set_generator_state` | Generator state created or changed (`owned`, `purchased_count`, `unlocked`, `available`) | `CurrencyGenerator._on_generated_state_changed`, `GeneratorPanel._on_generator_state_changed`, `GeneratorUnlockSystem._on_generator_state_changed`, `GoalsDebugUI._on_generator_state_changed` | | `generator_state_changed(generator_id, state)` | `core/game_state.gd` | `GameState.register_generator`, `GameState._set_generator_state` | Generator state created or changed (`owned`, `purchased_count`, `unlocked`, `available`) | `CurrencyGenerator._on_generated_state_changed`, `GeneratorPanel._on_generator_state_changed`, `GoalsDebugUI._on_generator_state_changed` |
| `generator_buff_level_changed(generator_id, buff_id, new_level)` | `core/game_state.gd` | `GameState.register_generator_buff`, `GameState.set_generator_buff_level` | Buff level created/sanitized/updated | `GeneratorPanel._on_generator_buff_level_changed` | | `generator_buff_level_changed(generator_id, buff_id, new_level)` | `core/game_state.gd` | `GameState.register_generator_buff`, `GameState.set_generator_buff_level` | Buff level created/sanitized/updated | `GeneratorPanel._on_generator_buff_level_changed` |
| `generator_buff_unlocked_changed(generator_id, buff_id, unlocked)` | `core/game_state.gd` | `GameState.register_generator_buff_unlocked`, `GameState.set_generator_buff_unlocked` | Buff unlock state created/sanitized/updated | `GeneratorPanel._on_generator_buff_unlocked_changed` | | `generator_buff_unlocked_changed(generator_id, buff_id, unlocked)` | `core/game_state.gd` | `GameState.register_generator_buff_unlocked`, `GameState.set_generator_buff_unlocked` | Buff unlock state created/sanitized/updated | `GeneratorPanel._on_generator_buff_unlocked_changed` |
| `purchase_completed(amount, total_owned, total_purchased, total_cost)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy` | Generator purchase succeeds | `GeneratorPanel._on_generator_updated` | | `purchase_completed(amount, total_owned, total_purchased, total_cost)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy` | Generator purchase succeeds | `GeneratorPanel._on_generator_updated` |
@@ -221,7 +219,7 @@ This appears to be an older/placeholder scene and not the current gameplay focus
| `production_tick(amount, cycle_count, total_owned)` | `core/generator/currency_generator.gd` | `CurrencyGenerator._grant_cycle_income` | One or more automatic production cycles complete | `GeneratorPanel._on_generator_updated` | | `production_tick(amount, cycle_count, total_owned)` | `core/generator/currency_generator.gd` | `CurrencyGenerator._grant_cycle_income` | One or more automatic production cycles complete | `GeneratorPanel._on_generator_updated` |
| `buff_purchased(buff_id, new_level, cost, cost_currency_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy_buff` | Buff purchase succeeds | `GeneratorPanel._on_generator_updated` | | `buff_purchased(buff_id, new_level, cost, cost_currency_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy_buff` | Buff purchase succeeds | `GeneratorPanel._on_generator_updated` |
| `buff_purchase_failed(buff_id, required_cost, cost_currency_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy_buff` | Buff purchase fails for insufficient funds | `GeneratorPanel._on_generator_updated` | | `buff_purchase_failed(buff_id, required_cost, cost_currency_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy_buff` | Buff purchase fails for insufficient funds | `GeneratorPanel._on_generator_updated` |
| `goal_achieved(goal_id, target_generator_id)` | `core/generator-unlock-goals/generator_unlock_system.gd` | `GeneratorUnlockSystem._evaluate_goal` | Unlock goal transitions target generator to unlocked+available | None currently (available for gameplay/UI hooks) | | `goal_achieved(generator_id, goal_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator._evaluate_generator_unlock_goal` | Generator unlock goal transitions that generator to unlocked+available | None currently (available for gameplay/UI hooks) |
| `buy_pressed(buff_id)` | `generator_buff_tile.gd` | `GeneratorBuffTile._on_buy_button_pressed` | User presses buff buy button in a buff row | `GeneratorPanel._on_buy_buff_pressed` | | `buy_pressed(buff_id)` | `generator_buff_tile.gd` | `GeneratorBuffTile._on_buy_button_pressed` | User presses buff buy button in a buff row | `GeneratorPanel._on_buy_buff_pressed` |
### Built-In Godot Signals Wired In This Project ### Built-In Godot Signals Wired In This Project
@@ -243,7 +241,7 @@ This appears to be an older/placeholder scene and not the current gameplay focus
1. Currency gain/spend chain: generator or debug action mutates `GameState` currency, `GameState` emits `currency_changed`, UI widgets and unlock systems recompute and redraw. 1. Currency gain/spend chain: generator or debug action mutates `GameState` currency, `GameState` emits `currency_changed`, UI widgets and unlock systems recompute and redraw.
2. Generator purchase chain: panel button triggers `CurrencyGenerator.buy`, generator updates `GameState` state, generator emits purchase signal, panel refreshes stats and buttons. 2. Generator purchase chain: panel button triggers `CurrencyGenerator.buy`, generator updates `GameState` state, generator emits purchase signal, panel refreshes stats and buttons.
3. Buff purchase chain: buff tile emits `buy_pressed`, panel calls `CurrencyGenerator.buy_buff`, generator updates buff level/unlock and optionally grants currency, `GameState` emits buff/currency/state signals, panel and other listeners refresh. 3. Buff purchase chain: buff tile emits `buy_pressed`, panel calls `CurrencyGenerator.buy_buff`, generator updates buff level/unlock and optionally grants currency, `GameState` emits buff/currency/state signals, panel and other listeners refresh.
4. Goal unlock chain: `currency_changed` triggers unlock evaluator, evaluator sets generator `unlocked/available`, emits `goal_achieved(goal_id, target_generator_id)`, `GameState` emits `generator_state_changed`, generator and UI become interactable/visible. 4. Goal unlock chain: `currency_changed` triggers generator-local unlock evaluator, evaluator sets generator `unlocked/available`, emits `goal_achieved(generator_id, goal_id)`, `GameState` emits `generator_state_changed`, generator and UI become interactable/visible.
## Key Findings and Risks ## Key Findings and Risks
@@ -251,7 +249,7 @@ This appears to be an older/placeholder scene and not the current gameplay focus
2. Save/load is only partially wired: `load_game()` runs at startup, but `save_game()` is not invoked anywhere else in the repository. 2. Save/load is only partially wired: `load_game()` runs at startup, but `save_game()` is not invoked anywhere else in the repository.
3. `currency_label.gd` and `big_number_progress_bar.gd` reference `GameState.GOLD_CURRENCY_ID`, but that constant does not exist in `core/game_state.gd`. 3. `currency_label.gd` and `big_number_progress_bar.gd` reference `GameState.GOLD_CURRENCY_ID`, but that constant does not exist in `core/game_state.gd`.
4. `generator_container.tscn` connects `mouse_entered`/`mouse_exited` to methods that do not exist in `generator_container.gd`. 4. `generator_container.tscn` connects `mouse_entered`/`mouse_exited` to methods that do not exist in `generator_container.gd`.
5. `generator_unlock_wood.tres` targets generator ID `Wood`, but no generator with that ID appears in current runtime scene/content, so this goal is currently non-functional. 5. Generator unlock goals now assume the target generator exists in-scene and owns its own `unlock_goal`.
6. `KnowledgeGenerator` visibility logic may be inconsistent because `currency_generator.gd` sets `visible = true` whenever its generator state changes. 6. `KnowledgeGenerator` visibility logic may be inconsistent because `currency_generator.gd` sets `visible = true` whenever its generator state changes.
7. Two buffs are configured locked with no unlock-goal path, so they remain permanently inaccessible under current logic. 7. Two buffs are configured locked with no unlock-goal path, so they remain permanently inaccessible under current logic.
8. `core/generator-unlock-goals/TECH_SPEC.md` contains some outdated assumptions relative to current `.tres`-based implementation. 8. `core/generator-unlock-goals/TECH_SPEC.md` contains some outdated assumptions relative to current `.tres`-based implementation.

View File

@@ -1,186 +1,58 @@
# Goal-Based Generator Unlocks - Technical Specification # Generator-Embedded Unlock Goals - Technical Specification
## Document Status ## Document Status
- Version: 1.0 - Version: 2.0
- Date: 2026-03-14 - Date: 2026-03-21
- Scope: Unlock generators when goal thresholds are reached using one or more currencies. - Scope: Unlock generators from `CurrencyGeneratorData.unlock_goal` when goal thresholds are reached.
- Context sources: Amp threads `T-019ce93a-d410-70ad-aaa0-4697dafb0148` and `T-019c846f-2556-75bc-a1aa-fb26cd9ff52a`.
## Problem Statement ## Problem Statement
The project already supports generator state (`unlocked` + `available`) in `GameState`, but unlock Generator unlock progression should be authored where generator behavior lives: `CurrencyGeneratorData`.
transitions are currently configured only by static defaults (`starts_unlocked`, `starts_available`). The previous scene-level target mapping layer increased setup complexity and created mismatch risk between goal targets and scene content.
We need progression goals so that a locked generator becomes usable only after the player reaches
specific currency quantities. Goals must support one or more currencies per objective.
## Current Baseline ## Current Baseline
1. Generator state is persisted in `GameState.generator_states` with keys `owned`, `purchased_count`, 1. Generator state persists in `GameState.generator_states` (`owned`, `purchased_count`, `unlocked`, `available`).
`unlocked`, `available`. 2. `CurrencyGenerator` already gates interactions using `is_available_to_player()`.
2. `CurrencyGenerator` already exposes `is_unlocked`, `is_available`, and `is_available_to_player()` 3. Goal primitives remain reusable and data-driven:
and uses them to gate click, auto-production, and purchases. - `GoalRequirementData`
3. `GeneratorContainer` listens to `GameState.generator_state_changed` and updates button enabled/disabled state. - `GoalData`
4. Save/load sanitization already handles generator state schema evolution safely. 4. Generator data now owns unlock definition via `unlock_goal: GoalData`.
This means the unlock feature can be added without changing core purchase/production mechanics.
## Goals And Non-Goals
## Goals
1. Define unlock goals that depend on one or multiple currency thresholds.
2. Evaluate goals automatically when relevant currency values change.
3. Unlock and make target generator available exactly once when a goal is met.
4. Keep behavior deterministic after save/load.
## Non-Goals
1. Adding quests, timed objectives, or repeatable missions.
2. Adding reward types other than generator unlock/availability.
3. Rebalancing generator economy values in this feature.
## Functional Requirements ## Functional Requirements
1. A generator may define one optional unlock goal (`CurrencyGeneratorData.unlock_goal`).
2. Goal completion uses logical AND across requirements.
3. Requirement checks use total acquired currency (`GameState.get_total_currency_acquired_by_id`).
4. Unlock evaluation runs:
- once on generator `_ready()`
- on every `GameState.currency_changed`
5. When a goal is met, runtime sets:
- `GameState.set_generator_unlocked(generator_id, true)`
- `GameState.set_generator_available(generator_id, true)`
6. Unlock transitions are idempotent.
7. Unlock completion emits `CurrencyGenerator.goal_achieved(generator_id, goal_id)` for hooks/UI.
1. A goal targets exactly one generator (`target_generator_id`). ## Runtime Design
2. A goal contains one or more currency requirements (`requirements[]`). 1. `CurrencyGeneratorData` exposes:
3. Goal completion rule is logical AND across requirements (all thresholds must be met). - `has_unlock_goal()`
4. Supported currencies must map to existing `GameState.CurrencyType` values (`gold`, `gems`). - `is_unlock_goal_met()`
5. When completed, a goal sets both: - `get_unlock_goal_id()`
- `GameState.set_generator_unlocked(target, true)` 2. `CurrencyGenerator` executes `_evaluate_generator_unlock_goal()`.
- `GameState.set_generator_available(target, true)` 3. No scene-level unlock controller is required.
6. Goal completion must be idempotent (re-evaluating does not re-apply side effects). 4. Goals debug UI discovers goals from scene generators instead of external target-mapping resources.
7. Goal checks must run:
- at runtime on `currency_changed`
- once at startup after loading save data
8. Invalid goal entries (unknown currency, missing target id, malformed amount) must be ignored with a warning,
not a crash.
## Data Model ## Data Authoring Rules
1. Configure locked generators with `starts_unlocked = false` and `starts_available = false`.
## New Config Resources 2. Set `unlock_goal` directly on that generator `.tres` resource.
3. Keep `GoalData.id` unique and stable.
Goals are now authored as typed resources instead of JSON: 4. Goal data can still be shared across systems (generator unlocks, buff unlocks).
1. `res://core/goals/goal_requirement_data.gd` (`GoalRequirementData`)
2. `res://core/goals/goal_data.gd` (`GoalData`)
3. `res://core/generator-unlock-goals/generator_unlock_goal_data.gd` (`GeneratorUnlockGoalData`)
Example assets:
1. `res://idles/goals/magic_total_30.tres` (goal definition)
2. `res://idles/goals/generator_unlock_knowledge.tres` (goal target binding)
Notes:
1. `GoalData.id` must be unique and stable.
2. `GoalData.requirements` must contain at least one valid `GoalRequirementData`.
3. `GoalRequirementData` stores amount as mantissa/exponent to preserve `BigNumber` semantics.
## Runtime Structures (GDScript)
Runtime data comes from resource classes:
1. `GoalRequirementData`
- `currency: Resource`
- `amount_mantissa: float`
- `amount_exponent: int`
2. `GoalData`
- `id: StringName`
- `requirements: Array[Resource]` (validated as `GoalRequirementData`)
3. `GeneratorUnlockGoalData`
- `target_generator_id: StringName`
- `goal: Resource` (validated as `GoalData`)
No persistent `completed_goals` storage is required for v1 because completion is derived from generator unlock state.
## System Design
## New Runtime Component
Add a scene-level controller script (recommended name: `generator_unlock_system.gd`) responsible for:
1. Loading + validating exported `GeneratorUnlockGoalData` resources.
2. Subscribing to currency change signals.
3. Evaluating pending goals.
4. Triggering generator unlock transitions.
Placement options:
1. Attach to the main gameplay scene root (preferred for prototype).
2. Promote to autoload later if multiple gameplay scenes require shared unlock logic.
## Evaluation Flow
1. On `_ready()`:
- load goals
- connect to `GameState.currency_changed`
- run `evaluate_all_goals()` once
2. On currency change:
- run `evaluate_all_goals()` (or an optimized subset)
3. For each goal:
- skip if target generator already unlocked and available
- verify all requirement thresholds
- if satisfied, unlock target generator and emit debug/info log
## Pseudocode
```gdscript
func _evaluate_goal(goal: UnlockGoal) -> bool:
if GameState.is_generator_unlocked(goal.target_generator_id) and GameState.is_generator_available(goal.target_generator_id):
return false
for requirement in goal.requirements:
var current: BigNumber = _get_currency_amount(requirement.currency)
if current.is_less_than(requirement.amount):
return false
GameState.set_generator_unlocked(goal.target_generator_id, true)
GameState.set_generator_available(goal.target_generator_id, true)
return true
```
## UI/UX Behavior
1. Existing `GeneratorContainer` behavior already prevents interaction while locked.
2. Optional v1.1 enhancement: show a compact "Unlock requirement" line for locked generators.
3. On unlock, UI updates automatically through existing `generator_state_changed` signal path.
## Error Handling And Validation
1. File open failure: log warning and disable unlock processing for that session.
2. JSON parse failure or wrong root type: log warning and disable unlock processing.
3. Invalid goal entries: skip only invalid entries, continue loading valid ones.
4. Duplicate goal IDs: keep first entry, ignore duplicates with warning.
5. Unknown `target_generator_id`: allow config load, but warn when evaluation cannot find/resolve state.
## Save/Load Behavior ## Save/Load Behavior
1. Unlock persistence remains unchanged because `GameState` stores `unlocked/available`.
2. Re-evaluation after load is safe due to idempotent state writes.
1. Unlock result persists naturally via existing generator state persistence. ## Validation
2. On load, if a goal was completed previously, no duplicate effect occurs because state is already unlocked. 1. Run headless parse: `"$GODOT_BIN" --headless --path . --quit`
3. If config thresholds are reduced in future versions, startup evaluation can unlock additional generators from existing balances. 2. Manual smoke test in `generator_museum`:
- verify locked generator starts unavailable
## Performance Considerations - grant required currency
- verify generator unlocks automatically
1. v1 can safely evaluate all goals on every currency change (small goal count). - verify state persists after restart
2. If goals scale up, add currency-to-goal index to evaluate only affected goals.
3. BigNumber comparisons are lightweight for this usage profile.
## Implementation Plan
1. Add `GoalData` and `GeneratorUnlockGoalData` resources with initial goals.
2. Implement `generator_unlock_system.gd` loader/validator/evaluator.
3. Attach unlock system to gameplay scene (`generator_museum.tscn` or current active scene).
4. Configure at least one generator as initially locked (`starts_unlocked = false`, `starts_available = false`) in its `.tres` data.
5. Add runtime logs for unlock events and invalid config entries.
6. Run headless project parse smoke check.
## Acceptance Criteria
1. A locked generator becomes usable immediately after all configured currency thresholds are reached.
2. Goals with multiple requirements unlock only when every requirement is satisfied.
3. Unlock state persists after save/load and app restart.
4. No crashes occur when goals file is missing or malformed.
5. Existing generator purchase/production behavior is unchanged for already unlocked generators.
## Manual Test Cases
1. Start with a generator configured as locked; verify buy buttons are disabled.
2. Grant required currency via debug controls; verify automatic unlock and enabled buttons.
3. Save/restart; verify generator remains unlocked.
4. Use a multi-currency goal; verify partial progress does not unlock.
5. Corrupt one goal entry in JSON; verify warning is logged and other valid goals still work.

View File

@@ -1,11 +0,0 @@
class_name GeneratorUnlockGoalData
extends Resource
@export var target_generator_id: StringName = &""
@export var goal: GoalData
func get_goal_data() -> GoalData:
if goal == null:
return null
return goal

View File

@@ -1 +0,0 @@
uid://dt4df3gwr6n5m

View File

@@ -1,151 +0,0 @@
extends Node
signal goal_achieved(goal_id: StringName, target_generator_id: StringName)
class GeneratorUnlockGoal extends RefCounted:
var target_generator_id: StringName
var goal: GoalData
func _init(target_id: StringName, goal_data: GoalData) -> void:
target_generator_id = target_id
goal = goal_data
@export var goals: Array[GeneratorUnlockGoalData] = []
var _runtime_goals: Array[GeneratorUnlockGoal] = []
var _known_generator_ids: Dictionary = {}
var _warned_unknown_target_ids: Dictionary = {}
var _is_evaluating_goals: bool = false
var _pending_goal_evaluation: bool = false
func _ready() -> void:
_collect_known_generator_ids()
_load_goals()
GameState.currency_changed.connect(_on_currency_changed)
GameState.generator_state_changed.connect(_on_generator_state_changed)
_evaluate_all_goals()
_evaluate_all_goals.call_deferred()
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
_evaluate_all_goals()
func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) -> void:
var generator_key: String = String(generator_id)
if not generator_key.is_empty():
_known_generator_ids[generator_key] = true
_evaluate_all_goals()
func _evaluate_all_goals() -> void:
if _runtime_goals.is_empty():
return
if _is_evaluating_goals:
_pending_goal_evaluation = true
return
_is_evaluating_goals = true
for runtime_goal in _runtime_goals:
_evaluate_goal(runtime_goal)
_is_evaluating_goals = false
if _pending_goal_evaluation:
_pending_goal_evaluation = false
_evaluate_all_goals.call_deferred()
func _evaluate_goal(runtime_goal: GeneratorUnlockGoal) -> bool:
if runtime_goal == null:
return false
if runtime_goal.goal == null:
return false
if _is_goal_completed(runtime_goal):
return false
if not _can_resolve_target(runtime_goal.target_generator_id):
return false
if not bool(runtime_goal.goal.is_met()):
return false
var goal_id: StringName = _extract_goal_id(runtime_goal.goal)
GameState.set_generator_unlocked(runtime_goal.target_generator_id, true)
GameState.set_generator_available(runtime_goal.target_generator_id, true)
goal_achieved.emit(goal_id, runtime_goal.target_generator_id)
print("[UnlockSystem] Goal completed: %s -> %s" % [String(goal_id), String(runtime_goal.target_generator_id)])
return true
func _extract_goal_id(goal: GoalData) -> StringName:
if goal == null:
return &""
var goal_id_text: String = String(goal.id).strip_edges()
if goal_id_text.is_empty():
return &""
return StringName(goal_id_text)
func _is_goal_completed(runtime_goal: GeneratorUnlockGoal) -> bool:
return (
GameState.is_generator_unlocked(runtime_goal.target_generator_id)
and GameState.is_generator_available(runtime_goal.target_generator_id)
)
func _can_resolve_target(target_generator_id: StringName) -> bool:
var target_key: String = String(target_generator_id)
if target_key.is_empty():
return false
if _known_generator_ids.has(target_key) or GameState.generator_states.has(target_key):
return true
if not _warned_unknown_target_ids.has(target_key):
_warned_unknown_target_ids[target_key] = true
push_warning("Unlock goal target generator not found in scene/state: '%s'" % target_key)
return false
func _load_goals() -> void:
_runtime_goals.clear()
var seen_goal_ids: Dictionary = {}
for generator_unlock_goal_data in goals:
if generator_unlock_goal_data == null:
continue
var goal_data: GeneratorUnlockGoalData = generator_unlock_goal_data
if goal_data == null:
continue
var target_generator_id: String = String(goal_data.target_generator_id).strip_edges()
if target_generator_id.is_empty():
push_warning("Skipping unlock goal resource: missing target_generator_id")
continue
var resolved_goal: GoalData = generator_unlock_goal_data.get_goal_data()
if resolved_goal == null:
push_warning("Skipping unlock goal for target '%s': goal is null" % target_generator_id)
continue
if not bool(resolved_goal.is_valid()):
push_warning("Skipping unlock goal for target '%s': goal is invalid" % target_generator_id)
continue
var goal_key: String = String(resolved_goal.id)
if seen_goal_ids.has(goal_key):
push_warning("Duplicate unlock goal id skipped: '%s'" % goal_key)
continue
seen_goal_ids[goal_key] = true
_runtime_goals.append(GeneratorUnlockGoal.new(StringName(target_generator_id), resolved_goal))
if _runtime_goals.is_empty():
push_warning("Unlock goals loaded, but no valid entries were found.")
func _collect_known_generator_ids() -> void:
_known_generator_ids.clear()
var scene_root: Node = get_tree().current_scene
var search_root: Node = scene_root if scene_root != null else self
var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false)
for generator_node in generator_nodes:
var generator: CurrencyGenerator = generator_node as CurrencyGenerator
if generator == null:
continue
var generator_key: String = String(generator.get_generator_id())
if generator_key.is_empty():
continue
_known_generator_ids[generator_key] = true

View File

@@ -1 +0,0 @@
uid://1sykgtg24a7g

View File

@@ -17,6 +17,8 @@ signal production_tick(amount: BigNumber, cycle_count: int, total_owned: int)
signal buff_purchased(buff_id: StringName, new_level: int, cost: BigNumber, cost_currency_id: StringName) signal buff_purchased(buff_id: StringName, new_level: int, cost: BigNumber, cost_currency_id: StringName)
## Emitted when a buff purchase cannot be paid. ## Emitted when a buff purchase cannot be paid.
signal buff_purchase_failed(buff_id: StringName, required_cost: BigNumber, cost_currency_id: StringName) signal buff_purchase_failed(buff_id: StringName, required_cost: BigNumber, cost_currency_id: StringName)
## Emitted when this generator unlock goal transitions to completed.
signal goal_achieved(generator_id: StringName, goal_id: StringName)
## Sentinel exponent used when cost math overflows float range. ## Sentinel exponent used when cost math overflows float range.
const HUGE_COST_EXPONENT: int = 1000000 const HUGE_COST_EXPONENT: int = 1000000
@@ -97,6 +99,7 @@ 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)
_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:
@@ -542,8 +545,29 @@ func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
GameState.set_generator_buff_unlocked(_generator_id, buff_id, true) GameState.set_generator_buff_unlocked(_generator_id, buff_id, true)
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void: func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
_evaluate_generator_unlock_goal(false)
_evaluate_buff_unlock_goals() _evaluate_buff_unlock_goals()
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
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
if not data.is_unlock_goal_met():
return false
GameState.set_generator_unlocked(_generator_id, true)
GameState.set_generator_available(_generator_id, true)
goal_achieved.emit(_generator_id, data.get_unlock_goal_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:
if data == null: if data == null:

View File

@@ -1,10 +1,17 @@
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
@export var starts_available: bool = true @export var starts_available: bool = true
@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
@@ -31,6 +38,31 @@ extends Resource
## Data-driven buffs purchasable for this generator. ## Data-driven buffs purchasable for this generator.
@export var buffs: Array[GeneratorBuffData] = [] @export var buffs: Array[GeneratorBuffData] = []
func has_unlock_goal() -> bool:
if unlock_goal == null:
return false
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 &""
var goal_id_text: String = String(unlock_goal.id).strip_edges()
if goal_id_text.is_empty():
return &""
return StringName(goal_id_text)
func unlocks_automatically_from_goal() -> bool:
return unlock_goal_behavior == UnlockGoalBehavior.AUTOMATIC
func find_buff(buff_id: StringName) -> GeneratorBuffData: func find_buff(buff_id: StringName) -> GeneratorBuffData:
var expected_id: String = String(buff_id).strip_edges() var expected_id: String = String(buff_id).strip_edges()
if expected_id.is_empty(): if expected_id.is_empty():

View File

@@ -5,11 +5,7 @@
[ext_resource type="Resource" uid="uid://04pmc034qupd" path="res://idles/generators/secondary_generator.tres" id="3_4ly0e"] [ext_resource type="Resource" uid="uid://04pmc034qupd" path="res://idles/generators/secondary_generator.tres" id="3_4ly0e"]
[ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="4_6ri4a"] [ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="4_6ri4a"]
[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="5_dl3gy"] [ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="5_dl3gy"]
[ext_resource type="PackedScene" uid="uid://dirvi76rkoowf" path="res://core/generator-unlock-goals/goals_debug_ui.tscn" id="7_7i63j"] [ext_resource type="PackedScene" uid="uid://dirvi76rkoowf" path="res://goals_debug_ui.tscn" id="7_7i63j"]
[ext_resource type="Script" uid="uid://dt4df3gwr6n5m" path="res://core/generator-unlock-goals/generator_unlock_goal_data.gd" id="8_6ri4a"]
[ext_resource type="Script" uid="uid://1sykgtg24a7g" path="res://core/generator-unlock-goals/generator_unlock_system.gd" id="8_bhig6"]
[ext_resource type="Resource" uid="uid://mnuihnwb70ba" path="res://idles/goals/generator_unlock_knowledge.tres" id="9_l765j"]
[ext_resource type="Resource" uid="uid://8wrqdjewfdq5" path="res://idles/goals/generator_unlock_wood.tres" id="10_x0wp6"]
[node name="GeneratorMuseum" type="Node2D" unique_id=1219373683] [node name="GeneratorMuseum" type="Node2D" unique_id=1219373683]
@@ -47,7 +43,3 @@ offset_left = 1253.0
offset_top = 817.0 offset_top = 817.0
offset_right = 1913.0 offset_right = 1913.0
offset_bottom = 1076.0 offset_bottom = 1076.0
[node name="GeneratorUnlockSystem" type="Node" parent="." unique_id=1909967402]
script = ExtResource("8_bhig6")
goals = Array[ExtResource("8_6ri4a")]([ExtResource("9_l765j"), ExtResource("10_x0wp6")])

View File

@@ -2,15 +2,14 @@ extends PanelContainer
class GoalDefinition extends RefCounted: class GoalDefinition extends RefCounted:
var target_generator_id: StringName var target_generator_id: StringName
var goal: Resource var goal: GoalData
func _init(target_id: StringName, goal_data: Resource) -> void: func _init(target_id: StringName, goal_data: GoalData) -> void:
target_generator_id = target_id target_generator_id = target_id
goal = goal_data goal = goal_data
const GOAL_DATA_SCRIPT: Script = preload("res://core/goals/goal_data.gd") const GOAL_DATA_SCRIPT: Script = preload("res://core/goals/goal_data.gd")
const GOAL_REQUIREMENT_SCRIPT: Script = preload("res://core/goals/goal_requirement_data.gd") const GOAL_REQUIREMENT_SCRIPT: Script = preload("res://core/goals/goal_requirement_data.gd")
const GENERATOR_UNLOCK_GOAL_DATA_SCRIPT: Script = preload("res://core/generator-unlock-goals/generator_unlock_goal_data.gd")
class GoalRow extends RefCounted: class GoalRow extends RefCounted:
var goal: GoalDefinition var goal: GoalDefinition
@@ -24,8 +23,6 @@ class GoalRow extends RefCounted:
requirements_label = requirements requirements_label = requirements
button = unlock_button button = unlock_button
@export var goals: Array[GeneratorUnlockGoalData] = []
@onready var _summary_label: Label = $MarginContainer/VBoxContainer/SummaryLabel @onready var _summary_label: Label = $MarginContainer/VBoxContainer/SummaryLabel
@onready var _goals_list: VBoxContainer = $MarginContainer/VBoxContainer/ScrollContainer/GoalsList @onready var _goals_list: VBoxContainer = $MarginContainer/VBoxContainer/ScrollContainer/GoalsList
@@ -33,6 +30,7 @@ var _loaded_goals: Array[GoalDefinition] = []
var _rows_by_goal_id: Dictionary = {} var _rows_by_goal_id: Dictionary = {}
var _known_generator_ids: Dictionary = {} var _known_generator_ids: Dictionary = {}
var _warned_unknown_target_ids: Dictionary = {} var _warned_unknown_target_ids: Dictionary = {}
var _generators_by_id: Dictionary = {}
func _ready() -> void: func _ready() -> void:
_collect_known_generator_ids() _collect_known_generator_ids()
@@ -50,36 +48,44 @@ func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) -
var generator_key: String = String(generator_id) var generator_key: String = String(generator_id)
if not generator_key.is_empty(): if not generator_key.is_empty():
_known_generator_ids[generator_key] = true _known_generator_ids[generator_key] = true
_refresh_generator_lookup(generator_id)
_refresh_ui() _refresh_ui()
func _load_goals() -> void: func _load_goals() -> void:
_loaded_goals.clear() _loaded_goals.clear()
_generators_by_id.clear()
var scene_root: Node = get_tree().current_scene
var search_root: Node = scene_root if scene_root != null else self
var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false)
var seen_goal_ids: Dictionary = {} var seen_goal_ids: Dictionary = {}
for goal_resource in goals: for generator_node in generator_nodes:
if goal_resource == null: var generator: CurrencyGenerator = generator_node as CurrencyGenerator
if generator == null:
continue continue
if goal_resource.get_script() != GENERATOR_UNLOCK_GOAL_DATA_SCRIPT:
var generator_id: StringName = generator.get_generator_id()
if generator_id == &"":
continue continue
var goal_data: Resource = goal_resource _generators_by_id[String(generator_id)] = generator
if goal_data == null:
if generator.data == null:
continue continue
var target_generator_id: String = String(goal_data.get("target_generator_id")).strip_edges() if not generator.data.has_unlock_goal():
if target_generator_id.is_empty():
continue continue
var resolved_goal: Resource = goal_data.call("get_goal_data")
var resolved_goal: GoalData = generator.data.unlock_goal
if resolved_goal == null: if resolved_goal == null:
continue continue
if not bool(resolved_goal.call("is_valid")): if not bool(resolved_goal.is_valid()):
continue continue
var goal_key: String = String(resolved_goal.get("id")) var goal_key: String = String(resolved_goal.get("id"))
if seen_goal_ids.has(goal_key): if seen_goal_ids.has(goal_key):
push_warning("Goals debug UI: duplicate goal id '%s' skipped" % goal_key) push_warning("Goals debug UI: duplicate goal id '%s' skipped" % goal_key)
continue continue
seen_goal_ids[goal_key] = true seen_goal_ids[goal_key] = true
_loaded_goals.append(GoalDefinition.new(StringName(target_generator_id), resolved_goal)) _loaded_goals.append(GoalDefinition.new(generator_id, resolved_goal))
func _build_goal_rows() -> void: func _build_goal_rows() -> void:
_rows_by_goal_id.clear() _rows_by_goal_id.clear()
@@ -89,8 +95,6 @@ func _build_goal_rows() -> void:
for goal in _loaded_goals: for goal in _loaded_goals:
if goal.goal == null: if goal.goal == null:
continue continue
if goal.goal.get_script() != GOAL_DATA_SCRIPT:
continue
var row_container: HBoxContainer = HBoxContainer.new() var row_container: HBoxContainer = HBoxContainer.new()
row_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL row_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -98,7 +102,7 @@ func _build_goal_rows() -> void:
var title_label: Label = Label.new() var title_label: Label = Label.new()
title_label.custom_minimum_size = Vector2(220.0, 0.0) title_label.custom_minimum_size = Vector2(220.0, 0.0)
title_label.text = "%s -> %s" % [String(goal.goal.get("id")), String(goal.target_generator_id)] title_label.text = "%s -> %s" % [String(goal.goal.id), String(goal.target_generator_id)]
var status_label: Label = Label.new() var status_label: Label = Label.new()
status_label.custom_minimum_size = Vector2(95.0, 0.0) status_label.custom_minimum_size = Vector2(95.0, 0.0)
@@ -110,7 +114,7 @@ func _build_goal_rows() -> void:
unlock_button.custom_minimum_size = Vector2(90.0, 0.0) unlock_button.custom_minimum_size = Vector2(90.0, 0.0)
unlock_button.text = "Unlock" unlock_button.text = "Unlock"
unlock_button.disabled = true unlock_button.disabled = true
unlock_button.pressed.connect(_on_unlock_pressed.bind(goal.goal.get("id"))) unlock_button.pressed.connect(_on_unlock_pressed.bind(goal.goal.id))
row_container.add_child(title_label) row_container.add_child(title_label)
row_container.add_child(status_label) row_container.add_child(status_label)
@@ -118,7 +122,7 @@ func _build_goal_rows() -> void:
row_container.add_child(unlock_button) row_container.add_child(unlock_button)
_goals_list.add_child(row_container) _goals_list.add_child(row_container)
_rows_by_goal_id[String(goal.goal.get("id"))] = GoalRow.new(goal, status_label, requirements_label, unlock_button) _rows_by_goal_id[String(goal.goal.id)] = GoalRow.new(goal, status_label, requirements_label, unlock_button)
func _refresh_ui() -> void: func _refresh_ui() -> void:
if _loaded_goals.is_empty(): if _loaded_goals.is_empty():
@@ -130,10 +134,8 @@ func _refresh_ui() -> void:
for goal in _loaded_goals: for goal in _loaded_goals:
if goal.goal == null: if goal.goal == null:
continue continue
if goal.goal.get_script() != GOAL_DATA_SCRIPT:
continue
var row: GoalRow = _rows_by_goal_id.get(String(goal.goal.get("id"))) var row: GoalRow = _rows_by_goal_id.get(String(goal.goal.id))
if row == null: if row == null:
continue continue
_refresh_goal_row(row) _refresh_goal_row(row)
@@ -150,11 +152,18 @@ 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 can_unlock: bool = can_resolve_target and met and not already_unlocked 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
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_unlock
row.button.text = "Done" if already_unlocked else "Unlock" if already_unlocked:
row.button.text = "Done"
elif is_manual_goal:
row.button.text = "Unlock"
else:
row.button.text = "Auto"
if already_unlocked: if already_unlocked:
row.status_label.text = "Unlocked" row.status_label.text = "Unlocked"
@@ -175,10 +184,8 @@ func _is_goal_met(goal: GoalDefinition) -> bool:
return false return false
if goal.goal == null: if goal.goal == null:
return false return false
if goal.goal.get_script() != GOAL_DATA_SCRIPT:
return false
return bool(goal.goal.call("is_met")) return bool(goal.goal.is_met())
func _is_goal_completed(goal: GoalDefinition) -> bool: func _is_goal_completed(goal: GoalDefinition) -> bool:
return ( return (
@@ -199,9 +206,14 @@ func _on_unlock_pressed(goal_id: StringName) -> void:
if not _is_goal_met(goal): if not _is_goal_met(goal):
return return
GameState.set_generator_unlocked(goal.target_generator_id, true) var generator: CurrencyGenerator = _get_generator_for_goal(goal)
GameState.set_generator_available(goal.target_generator_id, true) if generator == null:
print("[GoalsDebugUI] Unlocked goal '%s' -> %s" % [String(goal.goal.get("id")), String(goal.target_generator_id)]) return
if not generator.try_unlock_from_goal():
return
print("[GoalsDebugUI] Unlocked goal '%s' -> %s" % [String(goal.goal.id), String(goal.target_generator_id)])
_refresh_ui() _refresh_ui()
func _can_resolve_target(target_generator_id: StringName) -> bool: func _can_resolve_target(target_generator_id: StringName) -> bool:
@@ -219,6 +231,7 @@ func _can_resolve_target(target_generator_id: StringName) -> bool:
func _collect_known_generator_ids() -> void: func _collect_known_generator_ids() -> void:
_known_generator_ids.clear() _known_generator_ids.clear()
_generators_by_id.clear()
var scene_root: Node = get_tree().current_scene var scene_root: Node = get_tree().current_scene
var search_root: Node = scene_root if scene_root != null else self var search_root: Node = scene_root if scene_root != null else self
@@ -233,6 +246,45 @@ func _collect_known_generator_ids() -> void:
continue continue
_known_generator_ids[generator_key] = true _known_generator_ids[generator_key] = true
_generators_by_id[generator_key] = generator
func _refresh_generator_lookup(generator_id: StringName) -> void:
var generator_key: String = String(generator_id)
if generator_key.is_empty():
return
var scene_root: Node = get_tree().current_scene
var search_root: Node = scene_root if scene_root != null else self
var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false)
for generator_node in generator_nodes:
var generator: CurrencyGenerator = generator_node as CurrencyGenerator
if generator == null:
continue
if generator.get_generator_id() != generator_id:
continue
_generators_by_id[generator_key] = generator
return
func _get_generator_for_goal(goal: GoalDefinition) -> CurrencyGenerator:
if goal == null:
return null
var key: String = String(goal.target_generator_id)
if key.is_empty():
return null
return _generators_by_id.get(key) as CurrencyGenerator
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:
@@ -243,15 +295,15 @@ func _get_requirements_text(goal: GoalDefinition) -> String:
return "" return ""
var parts: Array[String] = [] var parts: Array[String] = []
for requirement_resource in goal.goal.call("get_valid_requirements"): for requirement_resource in goal.goal.get_valid_requirements():
if requirement_resource == null: if requirement_resource == null:
continue continue
if requirement_resource.get_script() != GOAL_REQUIREMENT_SCRIPT: if requirement_resource.get_script() != GOAL_REQUIREMENT_SCRIPT:
continue continue
var requirement_currency_id: StringName = requirement_resource.call("get_currency_id") var requirement_currency_id: StringName = requirement_resource.get_currency_id()
var total_amount: BigNumber = GameState.get_total_currency_acquired_by_id(requirement_currency_id) var total_amount: BigNumber = GameState.get_total_currency_acquired_by_id(requirement_currency_id)
var required_amount: BigNumber = requirement_resource.call("get_amount") var required_amount: BigNumber = requirement_resource.get_amount()
parts.append( parts.append(
"%s %s / %s" % [ "%s %s / %s" % [
_currency_label(requirement_currency_id), _currency_label(requirement_currency_id),

View File

@@ -1,14 +1,11 @@
[gd_scene format=3 uid="uid://dirvi76rkoowf"] [gd_scene format=3 uid="uid://dirvi76rkoowf"]
[ext_resource type="Script" uid="uid://bmrbaulftvvwm" path="res://core/generator-unlock-goals/goals_debug_ui.gd" id="1_s7s8v"] [ext_resource type="Script" uid="uid://bmrbaulftvvwm" path="res://goals_debug_ui.gd" id="1_s7s8v"]
[ext_resource type="Resource" uid="uid://mnuihnwb70ba" path="res://idles/goals/generator_unlock_knowledge.tres" id="2_jyi2u"]
[ext_resource type="Resource" uid="uid://8wrqdjewfdq5" path="res://idles/goals/generator_unlock_wood.tres" id="3_0nfd8"]
[node name="GoalsDebugUI" type="PanelContainer" unique_id=1494700185] [node name="GoalsDebugUI" type="PanelContainer" unique_id=1494700185]
offset_right = 640.0 offset_right = 640.0
offset_bottom = 260.0 offset_bottom = 260.0
script = ExtResource("1_s7s8v") script = ExtResource("1_s7s8v")
goals = [ExtResource("2_jyi2u"), ExtResource("3_0nfd8")]
[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1582529872] [node name="MarginContainer" type="MarginContainer" parent="." unique_id=1582529872]
layout_mode = 2 layout_mode = 2

View File

@@ -2,6 +2,7 @@
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_d86db"] [ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_d86db"]
[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="1_s3g28"] [ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="1_s3g28"]
[ext_resource type="Resource" uid="uid://qyxct5gbrxwa" path="res://idles/goals/magic_total_30.tres" id="2_r2b8m"]
[resource] [resource]
script = ExtResource("1_s3g28") script = ExtResource("1_s3g28")
@@ -9,6 +10,8 @@ id = &"Knowledge"
name = "Library" name = "Library"
starts_unlocked = false starts_unlocked = false
starts_available = false starts_available = false
unlock_goal = ExtResource("2_r2b8m")
unlock_goal_behavior = 1
initial_cost = 60.0 initial_cost = 60.0
initial_time = 3.0 initial_time = 3.0
initial_revenue = 60.0 initial_revenue = 60.0

View File

@@ -1,9 +0,0 @@
[gd_resource type="Resource" script_class="GeneratorUnlockGoalData" format=3 uid="uid://mnuihnwb70ba"]
[ext_resource type="Script" uid="uid://dt4df3gwr6n5m" path="res://core/generator-unlock-goals/generator_unlock_goal_data.gd" id="1_hv2u1"]
[ext_resource type="Resource" uid="uid://qyxct5gbrxwa" path="res://idles/goals/magic_total_30.tres" id="2_7r0qe"]
[resource]
script = ExtResource("1_hv2u1")
target_generator_id = &"Knowledge"
goal = ExtResource("2_7r0qe")

View File

@@ -1,9 +0,0 @@
[gd_resource type="Resource" script_class="GeneratorUnlockGoalData" format=3 uid="uid://8wrqdjewfdq5"]
[ext_resource type="Script" uid="uid://dt4df3gwr6n5m" path="res://core/generator-unlock-goals/generator_unlock_goal_data.gd" id="1_phux3"]
[ext_resource type="Resource" uid="uid://bmlhoeasl7xor" path="res://idles/goals/magic_total_1300.tres" id="2_w06cq"]
[resource]
script = ExtResource("1_phux3")
target_generator_id = &"Wood"
goal = ExtResource("2_w06cq")