Add goals
This commit is contained in:
@@ -123,22 +123,35 @@ func divide(other: BigNumber) -> BigNumber:
|
|||||||
|
|
||||||
## Returns 1 if this > other, -1 if this < other, 0 if equal
|
## Returns 1 if this > other, -1 if this < other, 0 if equal
|
||||||
func compare_to(other: BigNumber) -> int:
|
func compare_to(other: BigNumber) -> int:
|
||||||
if mantissa == 0.0 and other.mantissa == 0.0: return 0
|
if mantissa == 0.0 and other.mantissa == 0.0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Handle zero explicitly before sign/exponent checks.
|
||||||
|
if mantissa == 0.0:
|
||||||
|
return -1 if other.mantissa > 0.0 else 1
|
||||||
|
if other.mantissa == 0.0:
|
||||||
|
return 1 if mantissa > 0.0 else -1
|
||||||
|
|
||||||
# Handle signs
|
# Handle signs
|
||||||
if mantissa > 0 and other.mantissa <= 0: return 1
|
if mantissa > 0 and other.mantissa < 0:
|
||||||
if mantissa < 0 and other.mantissa >= 0: return -1
|
return 1
|
||||||
|
if mantissa < 0 and other.mantissa > 0:
|
||||||
|
return -1
|
||||||
|
|
||||||
# Both are same sign. Compare exponents first.
|
# Both are same sign. Compare exponents first.
|
||||||
var sign_mult: int = 1 if mantissa > 0 else -1
|
var sign_mult: int = 1 if mantissa > 0 else -1
|
||||||
|
|
||||||
if exponent > other.exponent: return sign_mult
|
if exponent > other.exponent:
|
||||||
if exponent < other.exponent: return -sign_mult
|
return sign_mult
|
||||||
|
if exponent < other.exponent:
|
||||||
|
return -sign_mult
|
||||||
|
|
||||||
# Exponents are equal, compare mantissas
|
# Exponents are equal, compare mantissas
|
||||||
if mantissa > other.mantissa: return 1
|
if mantissa > other.mantissa:
|
||||||
if mantissa < other.mantissa: return -1
|
return 1
|
||||||
|
if mantissa < other.mantissa:
|
||||||
|
return -1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
func is_greater_than(other: BigNumber) -> bool:
|
func is_greater_than(other: BigNumber) -> bool:
|
||||||
|
|||||||
121
game_state.gd
121
game_state.gd
@@ -12,8 +12,33 @@ enum CurrencyType { gold, gems }
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# STATE VARIABLES
|
# STATE VARIABLES
|
||||||
# ==========================================
|
# ==========================================
|
||||||
var gold: BigNumber = BigNumber.from_float(0.0)
|
var _current_currency: Dictionary = {}
|
||||||
var gems: BigNumber = BigNumber.from_float(0.0)
|
var _total_currency_acquired: Dictionary = {}
|
||||||
|
|
||||||
|
var gold: BigNumber:
|
||||||
|
get:
|
||||||
|
return _get_current_currency_ref(CurrencyType.gold)
|
||||||
|
set(value):
|
||||||
|
_set_current_currency(CurrencyType.gold, value)
|
||||||
|
|
||||||
|
var gems: BigNumber:
|
||||||
|
get:
|
||||||
|
return _get_current_currency_ref(CurrencyType.gems)
|
||||||
|
set(value):
|
||||||
|
_set_current_currency(CurrencyType.gems, value)
|
||||||
|
|
||||||
|
var total_gold_acquired: BigNumber:
|
||||||
|
get:
|
||||||
|
return _get_total_currency_ref(CurrencyType.gold)
|
||||||
|
set(value):
|
||||||
|
_set_total_currency(CurrencyType.gold, value)
|
||||||
|
|
||||||
|
var total_gems_acquired: BigNumber:
|
||||||
|
get:
|
||||||
|
return _get_total_currency_ref(CurrencyType.gems)
|
||||||
|
set(value):
|
||||||
|
_set_total_currency(CurrencyType.gems, value)
|
||||||
|
|
||||||
var generator_states: Dictionary = {}
|
var generator_states: Dictionary = {}
|
||||||
|
|
||||||
var last_save_time: int = 0 # Unix timestamp
|
var last_save_time: int = 0 # Unix timestamp
|
||||||
@@ -26,35 +51,47 @@ const GENERATOR_UNLOCKED_KEY: String = "unlocked"
|
|||||||
const GENERATOR_AVAILABLE_KEY: String = "available"
|
const GENERATOR_AVAILABLE_KEY: String = "available"
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_initialize_currency_maps()
|
||||||
load_game()
|
load_game()
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# STATE MODIFIERS
|
# STATE MODIFIERS
|
||||||
# ==========================================
|
# ==========================================
|
||||||
func add_gold(amount: BigNumber) -> void:
|
func add_gold(amount: BigNumber) -> void:
|
||||||
gold.add_in_place(amount)
|
add_currency(CurrencyType.gold, amount)
|
||||||
currency_changed.emit(CurrencyType.gold, gold)
|
|
||||||
|
|
||||||
func spend_gold(cost: BigNumber) -> bool:
|
func spend_gold(cost: BigNumber) -> bool:
|
||||||
if gold.is_greater_than(cost) or gold.is_equal_to(cost):
|
return spend_currency(CurrencyType.gold, cost)
|
||||||
var negative_cost = BigNumber.new(-cost.mantissa, cost.exponent)
|
|
||||||
gold.add_in_place(negative_cost)
|
|
||||||
currency_changed.emit(CurrencyType.gold, gold)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
func add_gems(amount: BigNumber) -> void:
|
func add_gems(amount: BigNumber) -> void:
|
||||||
gems.add_in_place(amount)
|
add_currency(CurrencyType.gems, amount)
|
||||||
currency_changed.emit(CurrencyType.gems, gems)
|
|
||||||
|
|
||||||
func spend_gems(cost: BigNumber) -> bool:
|
func spend_gems(cost: BigNumber) -> bool:
|
||||||
if gems.is_greater_than(cost) or gems.is_equal_to(cost):
|
return spend_currency(CurrencyType.gems, cost)
|
||||||
var negative_cost = BigNumber.new(-cost.mantissa, cost.exponent)
|
|
||||||
gems.add_in_place(negative_cost)
|
func add_currency(currency: CurrencyType, amount: BigNumber) -> void:
|
||||||
currency_changed.emit(CurrencyType.gems, gems)
|
if amount.mantissa > 0.0:
|
||||||
|
_get_total_currency_ref(currency).add_in_place(amount)
|
||||||
|
|
||||||
|
var current: BigNumber = _get_current_currency_ref(currency)
|
||||||
|
current.add_in_place(amount)
|
||||||
|
currency_changed.emit(currency, current)
|
||||||
|
|
||||||
|
func spend_currency(currency: CurrencyType, cost: BigNumber) -> bool:
|
||||||
|
var current: BigNumber = _get_current_currency_ref(currency)
|
||||||
|
if current.is_greater_than(cost) or current.is_equal_to(cost):
|
||||||
|
var negative_cost: BigNumber = BigNumber.new(-cost.mantissa, cost.exponent)
|
||||||
|
current.add_in_place(negative_cost)
|
||||||
|
currency_changed.emit(currency, current)
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
func get_currency_amount(currency: CurrencyType) -> BigNumber:
|
||||||
|
return _get_current_currency_ref(currency)
|
||||||
|
|
||||||
|
func get_total_currency_acquired(currency: CurrencyType) -> BigNumber:
|
||||||
|
return _get_total_currency_ref(currency)
|
||||||
|
|
||||||
func register_generator(generator_id: StringName, unlocked: bool = false, available: bool = false) -> void:
|
func register_generator(generator_id: StringName, unlocked: bool = false, available: bool = false) -> void:
|
||||||
var key: String = String(generator_id)
|
var key: String = String(generator_id)
|
||||||
if key.is_empty():
|
if key.is_empty():
|
||||||
@@ -132,6 +169,8 @@ func save_game() -> void:
|
|||||||
var save_data = {
|
var save_data = {
|
||||||
"gold": gold.serialize(),
|
"gold": gold.serialize(),
|
||||||
"gems": gems.serialize(),
|
"gems": gems.serialize(),
|
||||||
|
"total_gold_acquired": total_gold_acquired.serialize(),
|
||||||
|
"total_gems_acquired": total_gems_acquired.serialize(),
|
||||||
"generator_states": _serialize_generator_states(),
|
"generator_states": _serialize_generator_states(),
|
||||||
"last_save_time": Time.get_unix_time_from_system()
|
"last_save_time": Time.get_unix_time_from_system()
|
||||||
}
|
}
|
||||||
@@ -150,9 +189,59 @@ func load_game() -> void:
|
|||||||
if parsed is Dictionary:
|
if parsed is Dictionary:
|
||||||
gold = BigNumber.deserialize(parsed.get("gold", {}))
|
gold = BigNumber.deserialize(parsed.get("gold", {}))
|
||||||
gems = BigNumber.deserialize(parsed.get("gems", {}))
|
gems = BigNumber.deserialize(parsed.get("gems", {}))
|
||||||
|
total_gold_acquired = _deserialize_total_currency(parsed.get("total_gold_acquired", null), gold)
|
||||||
|
total_gems_acquired = _deserialize_total_currency(parsed.get("total_gems_acquired", null), gems)
|
||||||
generator_states = _deserialize_generator_states(parsed.get("generator_states", {}))
|
generator_states = _deserialize_generator_states(parsed.get("generator_states", {}))
|
||||||
last_save_time = parsed.get("last_save_time", Time.get_unix_time_from_system())
|
last_save_time = parsed.get("last_save_time", Time.get_unix_time_from_system())
|
||||||
|
|
||||||
|
func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) -> BigNumber:
|
||||||
|
var minimum_total: BigNumber = _copy_big_number(current_amount)
|
||||||
|
if minimum_total.mantissa < 0.0:
|
||||||
|
minimum_total = BigNumber.from_float(0.0)
|
||||||
|
|
||||||
|
if not (raw_total is Dictionary):
|
||||||
|
return minimum_total
|
||||||
|
|
||||||
|
var parsed_total: BigNumber = BigNumber.deserialize(raw_total)
|
||||||
|
if parsed_total.mantissa < 0.0:
|
||||||
|
return minimum_total
|
||||||
|
if parsed_total.is_less_than(minimum_total):
|
||||||
|
return minimum_total
|
||||||
|
return parsed_total
|
||||||
|
|
||||||
|
func _copy_big_number(value: BigNumber) -> BigNumber:
|
||||||
|
return BigNumber.new(value.mantissa, value.exponent)
|
||||||
|
|
||||||
|
func _initialize_currency_maps() -> void:
|
||||||
|
_set_current_currency(CurrencyType.gold, BigNumber.from_float(0.0))
|
||||||
|
_set_current_currency(CurrencyType.gems, BigNumber.from_float(0.0))
|
||||||
|
_set_total_currency(CurrencyType.gold, BigNumber.from_float(0.0))
|
||||||
|
_set_total_currency(CurrencyType.gems, BigNumber.from_float(0.0))
|
||||||
|
|
||||||
|
func _get_current_currency_ref(currency: CurrencyType) -> BigNumber:
|
||||||
|
var value: Variant = _current_currency.get(currency)
|
||||||
|
if value is BigNumber:
|
||||||
|
return value
|
||||||
|
|
||||||
|
var fallback: BigNumber = BigNumber.from_float(0.0)
|
||||||
|
_current_currency[currency] = fallback
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
func _get_total_currency_ref(currency: CurrencyType) -> BigNumber:
|
||||||
|
var value: Variant = _total_currency_acquired.get(currency)
|
||||||
|
if value is BigNumber:
|
||||||
|
return value
|
||||||
|
|
||||||
|
var fallback: BigNumber = BigNumber.from_float(0.0)
|
||||||
|
_total_currency_acquired[currency] = fallback
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
func _set_current_currency(currency: CurrencyType, value: BigNumber) -> void:
|
||||||
|
_current_currency[currency] = _copy_big_number(value)
|
||||||
|
|
||||||
|
func _set_total_currency(currency: CurrencyType, value: BigNumber) -> void:
|
||||||
|
_total_currency_acquired[currency] = _copy_big_number(value)
|
||||||
|
|
||||||
func _default_generator_state(unlocked: bool, available: bool) -> Dictionary:
|
func _default_generator_state(unlocked: bool, available: bool) -> Dictionary:
|
||||||
return {
|
return {
|
||||||
GENERATOR_OWNED_KEY: 0,
|
GENERATOR_OWNED_KEY: 0,
|
||||||
|
|||||||
197
generator-unlock-goals/TECH_SPEC.md
Normal file
197
generator-unlock-goals/TECH_SPEC.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Goal-Based Generator Unlocks - Technical Specification
|
||||||
|
|
||||||
|
## Document Status
|
||||||
|
- Version: 1.0
|
||||||
|
- Date: 2026-03-14
|
||||||
|
- Scope: Unlock generators when goal thresholds are reached using one or more currencies.
|
||||||
|
- Context sources: Amp threads `T-019ce93a-d410-70ad-aaa0-4697dafb0148` and `T-019c846f-2556-75bc-a1aa-fb26cd9ff52a`.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
The project already supports generator state (`unlocked` + `available`) in `GameState`, but unlock
|
||||||
|
transitions are currently configured only by static defaults (`starts_unlocked`, `starts_available`).
|
||||||
|
|
||||||
|
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
|
||||||
|
1. Generator state is persisted in `GameState.generator_states` with keys `owned`, `purchased_count`,
|
||||||
|
`unlocked`, `available`.
|
||||||
|
2. `CurrencyGenerator` already exposes `is_unlocked`, `is_available`, and `is_available_to_player()`
|
||||||
|
and uses them to gate click, auto-production, and purchases.
|
||||||
|
3. `GeneratorContainer` listens to `GameState.generator_state_changed` and updates button enabled/disabled state.
|
||||||
|
4. Save/load sanitization already handles generator state schema evolution safely.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
1. A goal targets exactly one generator (`target_generator_id`).
|
||||||
|
2. A goal contains one or more currency requirements (`requirements[]`).
|
||||||
|
3. Goal completion rule is logical AND across requirements (all thresholds must be met).
|
||||||
|
4. Supported currencies must map to existing `GameState.CurrencyType` values (`gold`, `gems`).
|
||||||
|
5. When completed, a goal sets both:
|
||||||
|
- `GameState.set_generator_unlocked(target, true)`
|
||||||
|
- `GameState.set_generator_available(target, true)`
|
||||||
|
6. Goal completion must be idempotent (re-evaluating does not re-apply side effects).
|
||||||
|
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
|
||||||
|
|
||||||
|
## New Config File
|
||||||
|
|
||||||
|
Add a root JSON file:
|
||||||
|
- `res://generator_unlock_goals.json`
|
||||||
|
|
||||||
|
Schema (v1):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"goals": [
|
||||||
|
{
|
||||||
|
"id": "unlock_gems_generator",
|
||||||
|
"target_generator_id": "Gems",
|
||||||
|
"requirements": [
|
||||||
|
{
|
||||||
|
"currency": "gold",
|
||||||
|
"amount": { "m": 1.0, "e": 3 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
1. `amount` uses existing `BigNumber` serialization (`m`, `e`) to avoid float precision/overflow issues.
|
||||||
|
2. `id` must be unique and stable.
|
||||||
|
3. `requirements` must contain at least one entry.
|
||||||
|
|
||||||
|
## Runtime Structures (GDScript)
|
||||||
|
|
||||||
|
Recommended internal structs/classes:
|
||||||
|
|
||||||
|
1. `UnlockGoalRequirement`
|
||||||
|
- `currency: GameState.CurrencyType`
|
||||||
|
- `amount: BigNumber`
|
||||||
|
2. `UnlockGoal`
|
||||||
|
- `id: StringName`
|
||||||
|
- `target_generator_id: StringName`
|
||||||
|
- `requirements: Array[UnlockGoalRequirement]`
|
||||||
|
|
||||||
|
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 `generator_unlock_goals.json`.
|
||||||
|
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
|
||||||
|
|
||||||
|
1. Unlock result persists naturally via existing generator state persistence.
|
||||||
|
2. On load, if a goal was completed previously, no duplicate effect occurs because state is already unlocked.
|
||||||
|
3. If config thresholds are reduced in future versions, startup evaluation can unlock additional generators from existing balances.
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
1. v1 can safely evaluate all goals on every currency change (small goal count).
|
||||||
|
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 `generator_unlock_goals.json` 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.
|
||||||
25
generator-unlock-goals/generator_unlock_goals.json
Normal file
25
generator-unlock-goals/generator_unlock_goals.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"goals": [
|
||||||
|
{
|
||||||
|
"id": "first_goal",
|
||||||
|
"target_generator_id": "Gems",
|
||||||
|
"requirements": [
|
||||||
|
{
|
||||||
|
"currency": "gold",
|
||||||
|
"amount": { "m": 30, "e": 0 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "second_goal",
|
||||||
|
"target_generator_id": "Gems",
|
||||||
|
"requirements": [
|
||||||
|
{
|
||||||
|
"currency": "gold",
|
||||||
|
"amount": { "m": 1.3, "e": 3 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
211
generator-unlock-goals/generator_unlock_system.gd
Normal file
211
generator-unlock-goals/generator_unlock_system.gd
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
class UnlockGoalRequirement extends RefCounted:
|
||||||
|
var currency: GameState.CurrencyType
|
||||||
|
var amount: BigNumber
|
||||||
|
|
||||||
|
func _init(req_currency: GameState.CurrencyType, req_amount: BigNumber) -> void:
|
||||||
|
currency = req_currency
|
||||||
|
amount = req_amount
|
||||||
|
|
||||||
|
class UnlockGoal extends RefCounted:
|
||||||
|
var id: StringName
|
||||||
|
var target_generator_id: StringName
|
||||||
|
var requirements: Array[UnlockGoalRequirement]
|
||||||
|
|
||||||
|
func _init(goal_id: StringName, target_id: StringName, goal_requirements: Array[UnlockGoalRequirement]) -> void:
|
||||||
|
id = goal_id
|
||||||
|
target_generator_id = target_id
|
||||||
|
requirements = goal_requirements
|
||||||
|
|
||||||
|
const GOALS_FILE_PATH: String = "res://generator-unlock-goals/generator_unlock_goals.json"
|
||||||
|
const SCHEMA_VERSION: int = 1
|
||||||
|
|
||||||
|
var _goals: Array[UnlockGoal] = []
|
||||||
|
var _known_generator_ids: Dictionary = {}
|
||||||
|
var _warned_unknown_target_ids: Dictionary = {}
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_collect_known_generator_ids()
|
||||||
|
_load_goals()
|
||||||
|
GameState.currency_changed.connect(_on_currency_changed)
|
||||||
|
_evaluate_all_goals()
|
||||||
|
|
||||||
|
func _on_currency_changed(_changed_currency: GameState.CurrencyType, _new_amount: BigNumber) -> void:
|
||||||
|
_evaluate_all_goals()
|
||||||
|
|
||||||
|
func _evaluate_all_goals() -> void:
|
||||||
|
if _goals.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
for goal in _goals:
|
||||||
|
_evaluate_goal(goal)
|
||||||
|
|
||||||
|
func _evaluate_goal(goal: UnlockGoal) -> bool:
|
||||||
|
if _is_goal_completed(goal):
|
||||||
|
return false
|
||||||
|
if not _can_resolve_target(goal.target_generator_id):
|
||||||
|
return false
|
||||||
|
|
||||||
|
for requirement in goal.requirements:
|
||||||
|
var total_amount: BigNumber = _get_total_currency_amount(requirement.currency)
|
||||||
|
if total_amount.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)
|
||||||
|
print("[UnlockSystem] Goal completed: %s -> %s" % [String(goal.id), String(goal.target_generator_id)])
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _is_goal_completed(goal: UnlockGoal) -> bool:
|
||||||
|
return (
|
||||||
|
GameState.is_generator_unlocked(goal.target_generator_id)
|
||||||
|
and GameState.is_generator_available(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:
|
||||||
|
_goals.clear()
|
||||||
|
|
||||||
|
if not FileAccess.file_exists(GOALS_FILE_PATH):
|
||||||
|
push_warning("Unlock goals file not found: %s" % GOALS_FILE_PATH)
|
||||||
|
return
|
||||||
|
|
||||||
|
var file: FileAccess = FileAccess.open(GOALS_FILE_PATH, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
push_warning("Unable to open unlock goals file: %s" % GOALS_FILE_PATH)
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||||
|
if not (parsed is Dictionary):
|
||||||
|
push_warning("Unlock goals root must be a Dictionary: %s" % GOALS_FILE_PATH)
|
||||||
|
return
|
||||||
|
|
||||||
|
var root: Dictionary = parsed
|
||||||
|
var version: int = int(root.get("version", SCHEMA_VERSION))
|
||||||
|
if version != SCHEMA_VERSION:
|
||||||
|
push_warning("Unlock goals schema version %d is different from expected %d" % [version, SCHEMA_VERSION])
|
||||||
|
|
||||||
|
var raw_goals: Variant = root.get("goals", [])
|
||||||
|
if not (raw_goals is Array):
|
||||||
|
push_warning("Unlock goals file has invalid 'goals' field (expected Array)")
|
||||||
|
return
|
||||||
|
|
||||||
|
var seen_goal_ids: Dictionary = {}
|
||||||
|
for raw_goal in raw_goals:
|
||||||
|
var goal: UnlockGoal = _parse_goal(raw_goal)
|
||||||
|
if goal == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var goal_key: String = String(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
|
||||||
|
_goals.append(goal)
|
||||||
|
|
||||||
|
if _goals.is_empty():
|
||||||
|
push_warning("Unlock goals loaded, but no valid entries were found.")
|
||||||
|
|
||||||
|
func _parse_goal(raw_goal: Variant) -> UnlockGoal:
|
||||||
|
if not (raw_goal is Dictionary):
|
||||||
|
push_warning("Skipping unlock goal: entry is not a Dictionary")
|
||||||
|
return null
|
||||||
|
|
||||||
|
var goal_dict: Dictionary = raw_goal
|
||||||
|
var goal_id: String = String(goal_dict.get("id", "")).strip_edges()
|
||||||
|
var target_id: String = String(goal_dict.get("target_generator_id", "")).strip_edges()
|
||||||
|
|
||||||
|
if goal_id.is_empty():
|
||||||
|
push_warning("Skipping unlock goal: missing 'id'")
|
||||||
|
return null
|
||||||
|
if target_id.is_empty():
|
||||||
|
push_warning("Skipping unlock goal '%s': missing 'target_generator_id'" % goal_id)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var raw_requirements: Variant = goal_dict.get("requirements", [])
|
||||||
|
if not (raw_requirements is Array):
|
||||||
|
push_warning("Skipping unlock goal '%s': 'requirements' must be an Array" % goal_id)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var parsed_requirements: Array[UnlockGoalRequirement] = []
|
||||||
|
for raw_requirement in raw_requirements:
|
||||||
|
var requirement: UnlockGoalRequirement = _parse_requirement(raw_requirement, goal_id)
|
||||||
|
if requirement != null:
|
||||||
|
parsed_requirements.append(requirement)
|
||||||
|
|
||||||
|
if parsed_requirements.is_empty():
|
||||||
|
push_warning("Skipping unlock goal '%s': no valid requirements" % goal_id)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return UnlockGoal.new(StringName(goal_id), StringName(target_id), parsed_requirements)
|
||||||
|
|
||||||
|
func _parse_requirement(raw_requirement: Variant, goal_id: String) -> UnlockGoalRequirement:
|
||||||
|
if not (raw_requirement is Dictionary):
|
||||||
|
push_warning("Skipping requirement in goal '%s': entry is not a Dictionary" % goal_id)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var requirement_dict: Dictionary = raw_requirement
|
||||||
|
var currency_value: int = _parse_currency(requirement_dict.get("currency", ""))
|
||||||
|
if currency_value < 0:
|
||||||
|
push_warning("Skipping requirement in goal '%s': unknown currency '%s'" % [goal_id, String(requirement_dict.get("currency", ""))])
|
||||||
|
return null
|
||||||
|
|
||||||
|
var raw_amount: Variant = requirement_dict.get("amount", {})
|
||||||
|
if not (raw_amount is Dictionary):
|
||||||
|
push_warning("Skipping requirement in goal '%s': 'amount' must be a Dictionary" % goal_id)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var amount: BigNumber = BigNumber.deserialize(raw_amount)
|
||||||
|
if amount.mantissa < 0.0:
|
||||||
|
push_warning("Skipping requirement in goal '%s': amount must be non-negative" % goal_id)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return UnlockGoalRequirement.new(currency_value, amount)
|
||||||
|
|
||||||
|
func _parse_currency(raw_currency: Variant) -> int:
|
||||||
|
if raw_currency is int:
|
||||||
|
var currency_int: int = raw_currency
|
||||||
|
if currency_int == GameState.CurrencyType.gold or currency_int == GameState.CurrencyType.gems:
|
||||||
|
return currency_int
|
||||||
|
return -1
|
||||||
|
|
||||||
|
var currency_text: String = String(raw_currency).to_lower().strip_edges()
|
||||||
|
match currency_text:
|
||||||
|
"gold":
|
||||||
|
return GameState.CurrencyType.gold
|
||||||
|
"gems", "gem":
|
||||||
|
return GameState.CurrencyType.gems
|
||||||
|
_:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
func _get_total_currency_amount(currency: GameState.CurrencyType) -> BigNumber:
|
||||||
|
return GameState.get_total_currency_acquired(currency)
|
||||||
|
|
||||||
|
func _collect_known_generator_ids() -> void:
|
||||||
|
_known_generator_ids.clear()
|
||||||
|
|
||||||
|
var generator_nodes: Array[Node] = 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
|
||||||
1
generator-unlock-goals/generator_unlock_system.gd.uid
Normal file
1
generator-unlock-goals/generator_unlock_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://1sykgtg24a7g
|
||||||
273
generator-unlock-goals/goals_debug_ui.gd
Normal file
273
generator-unlock-goals/goals_debug_ui.gd
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
class GoalRequirement extends RefCounted:
|
||||||
|
var currency: GameState.CurrencyType
|
||||||
|
var amount: BigNumber
|
||||||
|
|
||||||
|
func _init(req_currency: GameState.CurrencyType, req_amount: BigNumber) -> void:
|
||||||
|
currency = req_currency
|
||||||
|
amount = req_amount
|
||||||
|
|
||||||
|
class GoalDefinition extends RefCounted:
|
||||||
|
var id: StringName
|
||||||
|
var target_generator_id: StringName
|
||||||
|
var requirements: Array[GoalRequirement]
|
||||||
|
|
||||||
|
func _init(goal_id: StringName, target_id: StringName, goal_requirements: Array[GoalRequirement]) -> void:
|
||||||
|
id = goal_id
|
||||||
|
target_generator_id = target_id
|
||||||
|
requirements = goal_requirements
|
||||||
|
|
||||||
|
class GoalRow extends RefCounted:
|
||||||
|
var goal: GoalDefinition
|
||||||
|
var status_label: Label
|
||||||
|
var requirements_label: Label
|
||||||
|
var button: Button
|
||||||
|
|
||||||
|
func _init(goal_definition: GoalDefinition, status: Label, requirements: Label, unlock_button: Button) -> void:
|
||||||
|
goal = goal_definition
|
||||||
|
status_label = status
|
||||||
|
requirements_label = requirements
|
||||||
|
button = unlock_button
|
||||||
|
|
||||||
|
const GOALS_FILE_PATH: String = "res://generator-unlock-goals/generator_unlock_goals.json"
|
||||||
|
|
||||||
|
@onready var _summary_label: Label = $MarginContainer/VBoxContainer/SummaryLabel
|
||||||
|
@onready var _goals_list: VBoxContainer = $MarginContainer/VBoxContainer/ScrollContainer/GoalsList
|
||||||
|
|
||||||
|
var _goals: Array[GoalDefinition] = []
|
||||||
|
var _rows_by_goal_id: Dictionary = {}
|
||||||
|
var _completed_goal_ids: Dictionary = {}
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_load_goals()
|
||||||
|
_build_goal_rows()
|
||||||
|
GameState.currency_changed.connect(_on_currency_changed)
|
||||||
|
GameState.generator_state_changed.connect(_on_generator_state_changed)
|
||||||
|
_refresh_ui()
|
||||||
|
_refresh_ui.call_deferred()
|
||||||
|
|
||||||
|
func _on_currency_changed(_currency_type: GameState.CurrencyType, _new_amount: BigNumber) -> void:
|
||||||
|
_refresh_ui()
|
||||||
|
|
||||||
|
func _on_generator_state_changed(_generator_id: StringName, _state: Dictionary) -> void:
|
||||||
|
_refresh_ui()
|
||||||
|
|
||||||
|
func _load_goals() -> void:
|
||||||
|
_goals.clear()
|
||||||
|
|
||||||
|
if not FileAccess.file_exists(GOALS_FILE_PATH):
|
||||||
|
push_warning("Goals debug UI: goals file not found at %s" % GOALS_FILE_PATH)
|
||||||
|
return
|
||||||
|
|
||||||
|
var file: FileAccess = FileAccess.open(GOALS_FILE_PATH, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
push_warning("Goals debug UI: could not open goals file at %s" % GOALS_FILE_PATH)
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||||
|
if not (parsed is Dictionary):
|
||||||
|
push_warning("Goals debug UI: goals root must be a Dictionary")
|
||||||
|
return
|
||||||
|
|
||||||
|
var goals_raw: Variant = parsed.get("goals", [])
|
||||||
|
if not (goals_raw is Array):
|
||||||
|
push_warning("Goals debug UI: goals must be an Array")
|
||||||
|
return
|
||||||
|
|
||||||
|
var seen_goal_ids: Dictionary = {}
|
||||||
|
for raw_goal in goals_raw:
|
||||||
|
var goal: GoalDefinition = _parse_goal(raw_goal)
|
||||||
|
if goal == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var goal_key: String = String(goal.id)
|
||||||
|
if seen_goal_ids.has(goal_key):
|
||||||
|
push_warning("Goals debug UI: duplicate goal id '%s' skipped" % goal_key)
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen_goal_ids[goal_key] = true
|
||||||
|
_goals.append(goal)
|
||||||
|
|
||||||
|
func _parse_goal(raw_goal: Variant) -> GoalDefinition:
|
||||||
|
if not (raw_goal is Dictionary):
|
||||||
|
return null
|
||||||
|
|
||||||
|
var goal_dict: Dictionary = raw_goal
|
||||||
|
var goal_id: String = String(goal_dict.get("id", "")).strip_edges()
|
||||||
|
var target_id: String = String(goal_dict.get("target_generator_id", "")).strip_edges()
|
||||||
|
if goal_id.is_empty() or target_id.is_empty():
|
||||||
|
return null
|
||||||
|
|
||||||
|
var raw_requirements: Variant = goal_dict.get("requirements", [])
|
||||||
|
if not (raw_requirements is Array):
|
||||||
|
return null
|
||||||
|
|
||||||
|
var requirements: Array[GoalRequirement] = []
|
||||||
|
for raw_requirement in raw_requirements:
|
||||||
|
var requirement: GoalRequirement = _parse_requirement(raw_requirement)
|
||||||
|
if requirement != null:
|
||||||
|
requirements.append(requirement)
|
||||||
|
|
||||||
|
if requirements.is_empty():
|
||||||
|
return null
|
||||||
|
|
||||||
|
return GoalDefinition.new(StringName(goal_id), StringName(target_id), requirements)
|
||||||
|
|
||||||
|
func _parse_requirement(raw_requirement: Variant) -> GoalRequirement:
|
||||||
|
if not (raw_requirement is Dictionary):
|
||||||
|
return null
|
||||||
|
|
||||||
|
var requirement_dict: Dictionary = raw_requirement
|
||||||
|
var currency_value: int = _parse_currency(requirement_dict.get("currency", ""))
|
||||||
|
if currency_value < 0:
|
||||||
|
return null
|
||||||
|
|
||||||
|
var raw_amount: Variant = requirement_dict.get("amount", {})
|
||||||
|
if not (raw_amount is Dictionary):
|
||||||
|
return null
|
||||||
|
|
||||||
|
var amount: BigNumber = BigNumber.deserialize(raw_amount)
|
||||||
|
if amount.mantissa < 0.0:
|
||||||
|
return null
|
||||||
|
|
||||||
|
return GoalRequirement.new(currency_value, amount)
|
||||||
|
|
||||||
|
func _parse_currency(raw_currency: Variant) -> int:
|
||||||
|
if raw_currency is int:
|
||||||
|
var currency_int: int = raw_currency
|
||||||
|
if currency_int == GameState.CurrencyType.gold or currency_int == GameState.CurrencyType.gems:
|
||||||
|
return currency_int
|
||||||
|
return -1
|
||||||
|
|
||||||
|
var currency_text: String = String(raw_currency).to_lower().strip_edges()
|
||||||
|
match currency_text:
|
||||||
|
"gold":
|
||||||
|
return GameState.CurrencyType.gold
|
||||||
|
"gems", "gem":
|
||||||
|
return GameState.CurrencyType.gems
|
||||||
|
_:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
func _build_goal_rows() -> void:
|
||||||
|
_rows_by_goal_id.clear()
|
||||||
|
for child in _goals_list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
for goal in _goals:
|
||||||
|
var row_container: HBoxContainer = HBoxContainer.new()
|
||||||
|
row_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row_container.add_theme_constant_override("separation", 12)
|
||||||
|
|
||||||
|
var title_label: Label = Label.new()
|
||||||
|
title_label.custom_minimum_size = Vector2(220.0, 0.0)
|
||||||
|
title_label.text = "%s -> %s" % [String(goal.id), String(goal.target_generator_id)]
|
||||||
|
|
||||||
|
var status_label: Label = Label.new()
|
||||||
|
status_label.custom_minimum_size = Vector2(95.0, 0.0)
|
||||||
|
|
||||||
|
var requirements_label: Label = Label.new()
|
||||||
|
requirements_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
var unlock_button: Button = Button.new()
|
||||||
|
unlock_button.custom_minimum_size = Vector2(90.0, 0.0)
|
||||||
|
unlock_button.text = "Unlock"
|
||||||
|
unlock_button.disabled = true
|
||||||
|
unlock_button.pressed.connect(_on_unlock_pressed.bind(goal.id))
|
||||||
|
|
||||||
|
row_container.add_child(title_label)
|
||||||
|
row_container.add_child(status_label)
|
||||||
|
row_container.add_child(requirements_label)
|
||||||
|
row_container.add_child(unlock_button)
|
||||||
|
_goals_list.add_child(row_container)
|
||||||
|
|
||||||
|
_rows_by_goal_id[String(goal.id)] = GoalRow.new(goal, status_label, requirements_label, unlock_button)
|
||||||
|
|
||||||
|
func _refresh_ui() -> void:
|
||||||
|
if _goals.is_empty():
|
||||||
|
_summary_label.text = "No valid goals loaded"
|
||||||
|
return
|
||||||
|
|
||||||
|
var ready_to_unlock: int = 0
|
||||||
|
var unlocked: int = 0
|
||||||
|
for goal in _goals:
|
||||||
|
var row: GoalRow = _rows_by_goal_id.get(String(goal.id))
|
||||||
|
if row == null:
|
||||||
|
continue
|
||||||
|
_refresh_goal_row(row)
|
||||||
|
|
||||||
|
if _is_goal_completed(goal.id):
|
||||||
|
unlocked += 1
|
||||||
|
elif _is_goal_met(goal):
|
||||||
|
ready_to_unlock += 1
|
||||||
|
|
||||||
|
_summary_label.text = "Goals: %d | Ready: %d | Unlocked: %d" % [_goals.size(), ready_to_unlock, unlocked]
|
||||||
|
|
||||||
|
func _refresh_goal_row(row: GoalRow) -> void:
|
||||||
|
var goal: GoalDefinition = row.goal
|
||||||
|
var already_unlocked: bool = _is_goal_completed(goal.id)
|
||||||
|
var met: bool = _is_goal_met(goal)
|
||||||
|
var can_unlock: bool = met and not already_unlocked
|
||||||
|
|
||||||
|
row.requirements_label.text = _get_requirements_text(goal)
|
||||||
|
row.button.disabled = not can_unlock
|
||||||
|
row.button.text = "Done" if already_unlocked else "Unlock"
|
||||||
|
|
||||||
|
if already_unlocked:
|
||||||
|
row.status_label.text = "Unlocked"
|
||||||
|
return
|
||||||
|
|
||||||
|
if met:
|
||||||
|
row.status_label.text = "Ready"
|
||||||
|
return
|
||||||
|
|
||||||
|
row.status_label.text = "Locked"
|
||||||
|
|
||||||
|
func _is_goal_met(goal: GoalDefinition) -> bool:
|
||||||
|
for requirement in goal.requirements:
|
||||||
|
var total_amount: BigNumber = GameState.get_total_currency_acquired(requirement.currency)
|
||||||
|
if total_amount.is_less_than(requirement.amount):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _is_goal_completed(goal_id: StringName) -> bool:
|
||||||
|
return _completed_goal_ids.get(String(goal_id), false)
|
||||||
|
|
||||||
|
func _on_unlock_pressed(goal_id: StringName) -> void:
|
||||||
|
var row: GoalRow = _rows_by_goal_id.get(String(goal_id))
|
||||||
|
if row == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var goal: GoalDefinition = row.goal
|
||||||
|
if _is_goal_completed(goal.id):
|
||||||
|
return
|
||||||
|
if not _is_goal_met(goal):
|
||||||
|
return
|
||||||
|
|
||||||
|
_completed_goal_ids[String(goal.id)] = true
|
||||||
|
GameState.set_generator_unlocked(goal.target_generator_id, true)
|
||||||
|
GameState.set_generator_available(goal.target_generator_id, true)
|
||||||
|
print("[GoalsDebugUI] Unlocked goal '%s' -> %s" % [String(goal.id), String(goal.target_generator_id)])
|
||||||
|
_refresh_ui()
|
||||||
|
|
||||||
|
func _get_requirements_text(goal: GoalDefinition) -> String:
|
||||||
|
var parts: Array[String] = []
|
||||||
|
for requirement in goal.requirements:
|
||||||
|
var total_amount: BigNumber = GameState.get_total_currency_acquired(requirement.currency)
|
||||||
|
parts.append(
|
||||||
|
"%s %s / %s" % [
|
||||||
|
_currency_label(requirement.currency),
|
||||||
|
total_amount.to_string_suffix(2),
|
||||||
|
requirement.amount.to_string_suffix(2)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return " | ".join(parts)
|
||||||
|
|
||||||
|
func _currency_label(currency: GameState.CurrencyType) -> String:
|
||||||
|
match currency:
|
||||||
|
GameState.CurrencyType.gold:
|
||||||
|
return "Gold"
|
||||||
|
GameState.CurrencyType.gems:
|
||||||
|
return "Gems"
|
||||||
|
_:
|
||||||
|
return "Unknown"
|
||||||
1
generator-unlock-goals/goals_debug_ui.gd.uid
Normal file
1
generator-unlock-goals/goals_debug_ui.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bmrbaulftvvwm
|
||||||
45
generator-unlock-goals/goals_debug_ui.tscn
Normal file
45
generator-unlock-goals/goals_debug_ui.tscn
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://generator-unlock-goals/goals_debug_ui.gd" id="1_s7s8v"]
|
||||||
|
|
||||||
|
[node name="GoalsDebugUI" type="PanelContainer"]
|
||||||
|
offset_right = 640.0
|
||||||
|
offset_bottom = 260.0
|
||||||
|
script = ExtResource("1_s7s8v")
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/margin_left = 8
|
||||||
|
theme_override_constants/margin_top = 8
|
||||||
|
theme_override_constants/margin_right = 8
|
||||||
|
theme_override_constants/margin_bottom = 8
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Goals Debug"
|
||||||
|
|
||||||
|
[node name="SummaryLabel" type="Label" parent="MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Goals: 0 | Ready: 0 | Unlocked: 0"
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 170)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="GoalsList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 6
|
||||||
@@ -8,5 +8,4 @@ id = &"Gold"
|
|||||||
name = "Gold Mine"
|
name = "Gold Mine"
|
||||||
initial_time = 0.6
|
initial_time = 0.6
|
||||||
initial_productivity = 1.67
|
initial_productivity = 1.67
|
||||||
grants_click_while_hovering = true
|
|
||||||
metadata/_custom_type_script = "uid://b00tqsuhxdy0d"
|
metadata/_custom_type_script = "uid://b00tqsuhxdy0d"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
script = ExtResource("1_s3g28")
|
script = ExtResource("1_s3g28")
|
||||||
id = &"Gems"
|
id = &"Gems"
|
||||||
name = "Gems Mine"
|
name = "Gems Mine"
|
||||||
|
starts_unlocked = false
|
||||||
|
starts_available = false
|
||||||
initial_cost = 60.0
|
initial_cost = 60.0
|
||||||
initial_time = 3.0
|
initial_time = 3.0
|
||||||
initial_revenue = 60.0
|
initial_revenue = 60.0
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="3_mhw8n"]
|
[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://ckos7f22pnmyh" path="res://generator_container.tscn" id="3_pw2a0"]
|
||||||
[ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="3_x5lt1"]
|
[ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="3_x5lt1"]
|
||||||
|
[ext_resource type="PackedScene" path="res://generator-unlock-goals/goals_debug_ui.tscn" id="4_63gjq"]
|
||||||
|
|
||||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_y5m1q"]
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_y5m1q"]
|
||||||
size = Vector2(128, 128)
|
size = Vector2(128, 128)
|
||||||
@@ -16,6 +17,7 @@ size = Vector2(128, 128)
|
|||||||
position = Vector2(566, 455)
|
position = Vector2(566, 455)
|
||||||
script = ExtResource("1_pl17p")
|
script = ExtResource("1_pl17p")
|
||||||
data = ExtResource("2_x5lt1")
|
data = ExtResource("2_x5lt1")
|
||||||
|
grants_click_while_hovering = true
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="GoldGenerator" unique_id=381586216]
|
[node name="Sprite2D" type="Sprite2D" parent="GoldGenerator" unique_id=381586216]
|
||||||
texture = ExtResource("3_mhw8n")
|
texture = ExtResource("3_mhw8n")
|
||||||
@@ -29,6 +31,8 @@ shape = SubResource("RectangleShape2D_y5m1q")
|
|||||||
script = ExtResource("1_pl17p")
|
script = ExtResource("1_pl17p")
|
||||||
currency = 1
|
currency = 1
|
||||||
data = ExtResource("3_dc82g")
|
data = ExtResource("3_dc82g")
|
||||||
|
starts_unlocked = false
|
||||||
|
starts_available = false
|
||||||
|
|
||||||
[node name="UI" type="Control" parent="." unique_id=452530906]
|
[node name="UI" type="Control" parent="." unique_id=452530906]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
@@ -66,5 +70,12 @@ offset_right = 247.0
|
|||||||
offset_bottom = 380.0
|
offset_bottom = 380.0
|
||||||
_generator = NodePath("../../GemsGenerator")
|
_generator = NodePath("../../GemsGenerator")
|
||||||
|
|
||||||
|
[node name="GoalsDebugUI" parent="UI" unique_id=4710697 instance=ExtResource("4_63gjq")]
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 577.0
|
||||||
|
offset_top = 3.0
|
||||||
|
offset_right = 1150.0
|
||||||
|
offset_bottom = 262.0
|
||||||
|
|
||||||
[connection signal="mouse_entered" from="GoldGenerator/Area2D" to="GoldGenerator" method="_on_area_2d_mouse_entered"]
|
[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"]
|
[connection signal="mouse_exited" from="GoldGenerator/Area2D" to="GoldGenerator" method="_on_area_2d_mouse_exited"]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Idles"
|
config/name="Idles"
|
||||||
|
config/version="0.0.1"
|
||||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user