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

@@ -1,186 +1,58 @@
# Goal-Based Generator Unlocks - Technical Specification
# Generator-Embedded Unlock Goals - 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`.
- Version: 2.0
- Date: 2026-03-21
- Scope: Unlock generators from `CurrencyGeneratorData.unlock_goal` when goal thresholds are reached.
## 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.
Generator unlock progression should be authored where generator behavior lives: `CurrencyGeneratorData`.
The previous scene-level target mapping layer increased setup complexity and created mismatch risk between goal targets and scene content.
## 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.
1. Generator state persists in `GameState.generator_states` (`owned`, `purchased_count`, `unlocked`, `available`).
2. `CurrencyGenerator` already gates interactions using `is_available_to_player()`.
3. Goal primitives remain reusable and data-driven:
- `GoalRequirementData`
- `GoalData`
4. Generator data now owns unlock definition via `unlock_goal: GoalData`.
## 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`).
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.
## Runtime Design
1. `CurrencyGeneratorData` exposes:
- `has_unlock_goal()`
- `is_unlock_goal_met()`
- `get_unlock_goal_id()`
2. `CurrencyGenerator` executes `_evaluate_generator_unlock_goal()`.
3. No scene-level unlock controller is required.
4. Goals debug UI discovers goals from scene generators instead of external target-mapping resources.
## Data Model
## New Config Resources
Goals are now authored as typed resources instead of JSON:
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.
## Data Authoring Rules
1. Configure locked generators with `starts_unlocked = false` and `starts_available = false`.
2. Set `unlock_goal` directly on that generator `.tres` resource.
3. Keep `GoalData.id` unique and stable.
4. Goal data can still be shared across systems (generator unlocks, buff unlocks).
## 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.
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 `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.
## Validation
1. Run headless parse: `"$GODOT_BIN" --headless --path . --quit`
2. Manual smoke test in `generator_museum`:
- verify locked generator starts unavailable
- grant required currency
- verify generator unlocks automatically
- verify state persists after restart