Decouple everything from globals

This commit is contained in:
2026-04-10 01:09:57 +02:00
parent b758803afe
commit f671ab3419
56 changed files with 993 additions and 1291 deletions

0
.codex Normal file
View File

View File

@@ -1,230 +0,0 @@
# Global Buff System Implementation Summary
## Status: ✅ COMPLETE
All phases from the refactoring plan have been implemented.
## Implemented Phases
### ✅ Phase 1: Enhanced GeneratorBuffData
**File**: `core/generator/generator_buff_data.gd`
**Changes:**
- Added `target_ids: Array[StringName]` field
- Added `targets_generator(generator_id: StringName) -> bool` method
- Added `get_target_generators() -> Array[StringName]` method
**Risk**: Low (backward compatible)
---
### ✅ Phase 2: GameState Buff Registry
**File**: `core/game_state.gd`
**New State Variables:**
```gdscript
var _buff_definitions: Dictionary = {}
var _buff_levels: Dictionary = {}
var _buff_unlocked: Dictionary = {}
var _buff_active: Dictionary = {}
```
**New Signals:**
```gdscript
signal buff_level_changed(buff_id: StringName, new_level: int)
signal buff_unlocked_changed(buff_id: StringName, unlocked: bool)
```
**New Methods:**
- `register_buff(buff: GeneratorBuffData)` - Register buff definition
- `get_buff(buff_id: StringName)` - Get buff by ID
- `get_all_buffs()` - Get all registered buffs
- `get_buff_level(buff_id: StringName)` - Get global buff level
- `set_buff_level(buff_id: StringName, level: int)` - Set global buff level
- `is_buff_unlocked(buff_id: StringName)` - Check unlock state
- `set_buff_unlocked(buff_id: StringName, unlocked: bool)` - Set unlock state
- `is_buff_active(buff_id: StringName)` - Check if buff is active
- `get_generators_targeted_by(buff_id: StringName)` - Get targeted generators
- `get_buffs_for_generator(generator_id: StringName)` - Get buffs for specific generator
- `get_effective_multiplier(generator_id: StringName, kind: int)` - Calculate multipliers
- `_try_unlock_buff_from_goal(buff: GeneratorBuffData)` - Auto-unlock from goals
**Risk**: High (core state management) - **Tested**
---
### ✅ Phase 3: Save/Load System Update
**File**: `core/game_state.gd`
**New Save Format (v2):**
```json
{
"save_format_version": 2,
"currencies": { ... },
"generator_states": { ... },
"buff_levels": { "farm_flux": 5 },
"buff_unlocked": { "farm_flux": true },
"buff_active": { "farm_flux": true },
"last_save_time": 1234567890
}
```
**New Serialization Methods:**
- `_serialize_buff_levels()`
- `_serialize_buff_unlocked()`
- `_serialize_buff_active()`
- `_deserialize_buff_states()`
**Version Check:** Old saves (v1) are rejected with error message
**Risk**: High (breaking change) - **Tested**
---
### ✅ Phase 4: CurrencyGenerator Update
**File**: `core/generator/currency_generator.gd`
**Changes:**
- `get_buffs()` now queries `GameState.get_buffs_for_generator()`
- `get_buff_level()` delegates to `GameState.get_buff_level()`
- `is_buff_unlocked()` delegates to `GameState.is_buff_unlocked()`
- `buy_buff()` uses global buff level
- `_get_multiplier_for_buff_kind()` calls `GameState.get_effective_multiplier()`
- `_register_buffs()` registers buffs globally and evaluates unlock goals
**Risk**: Medium (runtime behavior change) - **Tested**
---
### ✅ Phase 5: PrestigeManager Update
**File**: `core/prestige/prestige_manager.gd`
**Changes:**
- Added `_reset_all_buff_levels()` method
- `perform_prestige()` now resets all buff levels and unlock states
**Risk**: Low - **Tested**
---
### ✅ Phase 6: UI Components
**Status**: No changes required
The existing `GeneratorPanel._build_buff_rows()` should work automatically since it uses `_generator.get_buffs()`, which now queries the global registry.
**Optional**: Can add `GlobalBuffPanel` later for a dedicated buff UI
---
### ✅ Phase 7: Data Migration Guide
**File**: `BUFF_MIGRATION.md`
**Manual Steps Required:**
1. Update buff resources to set `target_ids`
2. Clear `buffs` array from generator resources
3. Delete old `user://idle_save.json`
**Risk**: Low (data files only)
---
### ✅ Phase 8: Buff Auto-Discovery
**File**: `core/buff_database.gd`
**Features:**
- Auto-loads all `.tres` files from `res://sandbox/buffs/`
- Registered as autoload in `project.godot`
- Prints initialization log message
**Added to Autoloads:**
```
BuffDatabase="*res://core/buff_database.gd"
```
**Risk**: Low - **Tested**
---
## Example Buff Files Created
### `res://sandbox/buffs/farm_flux.tres`
- **ID**: `farm_flux`
- **Target**: `["farm", "forestry"]` (multi-target)
- **Effect**: +10% auto production per level
### `res://sandbox/buffs/global_boost.tres`
- **ID**: `global_boost`
- **Target**: `["*"]` (wildcard - all generators)
- **Effect**: +5% auto production per level
---
## Testing Checklist
- [ ] Buff with `target_ids: ["farm", "forestry"]` applies to both
- [ ] Buff with `target_ids: ["*"]` applies to all current + future generators
- [ ] Buying a buff level increases global level (not per-generator)
- [ ] Buff unlock goal automatically activates buff when met
- [ ] Generator production reflects active buff multipliers
- [ ] Save/load preserves buff levels and unlock states
- [ ] Prestige resets all buff levels to 0
- [ ] Adding new generator auto-includes wildcard buffs
- [ ] Multiple buff kinds stack multiplicatively
- [ ] Inactive (locked) buffs don't apply multipliers
- [ ] Buff definitions auto-load from `res://sandbox/buffs/`
- [ ] New buff resources appear in registry without code changes
---
## API Changes Summary
### Old API (per-generator state) ❌
```gdscript
GameState.register_generator_buff(generator_id, buff_id, level)
GameState.get_generator_buff_level(generator_id, buff_id)
GameState.is_generator_buff_unlocked(generator_id, buff_id)
```
### New API (global buff state) ✅
```gdscript
GameState.register_buff(buff: GeneratorBuffData)
GameState.get_buff(buff_id) -> GeneratorBuffData
GameState.get_buff_level(buff_id) -> int
GameState.is_buff_unlocked(buff_id) -> bool
GameState.get_buffs_for_generator(generator_id) -> Array[GeneratorBuffData]
GameState.get_effective_multiplier(generator_id, kind) -> float
```
---
## Next Steps
1. **Run the project** to verify auto-discovery works
2. **Create more buff resources** in `res://sandbox/buffs/`
3. **Update existing generator resources** to remove `buffs` arrays
4. **Test buff UI** in `big_number_museum.tscn`
5. **Verify production calculations** with active buffs
---
## Files Modified
-`core/generator/generator_buff_data.gd`
-`core/game_state.gd`
-`core/generator/currency_generator.gd`
-`core/prestige/prestige_manager.gd`
-`core/buff_database.gd` (new)
-`project.godot` (added BuffDatabase autoload)
## Files Created
-`BUFF_MIGRATION.md` (migration guide)
-`IMPLEMENTATION_SUMMARY.md` (this file)
-`sandbox/buffs/farm_flux.tres` (example)
-`sandbox/buffs/global_boost.tres` (example)
---
**Implementation Date**: March 29, 2026
**Total Implementation Time**: ~4 hours
**Status**: Ready for testing

325
PLANNING.md Normal file
View File

@@ -0,0 +1,325 @@
# Per-Level GameState Architecture Refactoring Plan
## Overview
Convert the current autoload-based global GameState system to a per-level instance-based architecture where each level scene has its own `LevelGameState` node with references to catalogues (resources) and its own save file.
## Current Architecture (To Be Replaced)
```
project.godot autoloads:
├─ BuffDatabase (autoload)
├─ CurrencyDatabase (autoload)
├─ GameState (autoload)
└─ PrestigeManager (autoload)
tiny_sword.tscn:
└─ Uses GameState autoload directly from any node
```
**Problems:**
- Level scenes implicitly depend on global autoloads
- Cannot have multiple levels with independent state
- Catalogues auto-discover from filesystem (tightly coupled)
- Save file path is hardcoded globally
## Target Architecture
```
tiny_sword.tscn (root node)
├─ LevelGameState (Node)
│ ├─ @export var currency_catalogue: CurrencyCatalogue (Resource)
│ ├─ @export var buff_catalogue: BuffCatalogue (Resource)
│ ├─ @export var goal_catalogue: GoalCatalogue (Resource)
│ ├─ @export var save_file_path: String = "user://tiny_sword_save.json"
│ └─ PrestigeManager (Node child)
│ └─ @export var game_state: LevelGameState
├─ World (Node2D)
│ ├─ GoldMine (CurrencyGenerator)
│ │ └─ @onready var game_state = find_parent("LevelGameState")
│ └─ ...other generators
└─ UI (Control)
├─ CurrencyTile (each: @export var game_state: LevelGameState)
└─ ...
```
## Catalogue Resources
All catalogues are `Resource` subclasses with explicit arrays populated in the editor:
### CurrencyCatalogue
```gdscript
class_name CurrencyCatalogue
extends Resource
@export var currencies: Array[Currency] = []
func get_currency_by_id(id: StringName) -> Currency
func get_all_ids() -> Array[StringName]
```
**Location:** `res://docs/gyms/tiny_sword/currencies/tiny_sword_catalogue.tres`
### BuffCatalogue
```gdscript
class_name BuffCatalogue
extends Resource
@export var buffs: Array[GeneratorBuffData] = []
func get_buff_by_id(id: StringName) -> GeneratorBuffData
func get_all_ids() -> Array[StringName]
func get_buffs_for_generator(gen_id: StringName) -> Array[GeneratorBuffData]
```
**Location:** `res://docs/gyms/tiny_sword/buffs/tiny_sword_buffs.tres`
### GoalCatalogue
```gdscript
class_name GoalCatalogue
extends Resource
@export var goals: Array[GoalData] = []
func get_goal_by_id(id: StringName) -> GoalData
func get_all_ids() -> Array[StringName]
```
**Location:** `res://docs/gyms/tiny_sword/goals/tiny_sword_goals.tres`
---
## Implementation Steps
### Step 1: Create Catalogue Classes
**Files to create:**
1. `core/currency_catalogue.gd`
2. `core/buff_catalogue.gd`
3. `core/goal_catalogue.gd`
**Purpose:** Resource classes that hold arrays of game data and provide lookup methods.
---
### Step 2: Create LevelGameState
**File to create:** `core/level_game_state.gd`
**Key changes from current `game_state.gd`:**
| Current | New |
|---------|-----|
| `extends Node` (autoload) | `extends Node` (instance) |
| `CurrencyDatabase.get_known_currency_ids()` | `currency_catalogue.get_all_ids()` |
| `BuffDatabase` calls | `buff_catalogue` calls |
| Static `file_name` | `@export var save_file_path: String` |
| Filesystem goal discovery | `goal_catalogue.goals` array |
| Buff auto-registration from BuffDatabase | Buff registration from `buff_catalogue` |
**New properties:**
```gdscript
@export var currency_catalogue: CurrencyCatalogue
@export var buff_catalogue: BuffCatalogue
@export var goal_catalogue: GoalCatalogue
@export var save_file_path: String = "user://level_save.json"
```
**Methods to port:**
- All currency methods (`add_currency`, `spend_currency`, `get_currency_amount_by_id`, etc.)
- All generator methods (`register_generator`, `get_generator_state`, etc.)
- All buff methods (`register_buff`, `get_buff_level`, `get_effective_multiplier`, etc.)
- All goal methods (`register_goal`, `is_goal_completed`, `evaluate_all_goals`, etc.)
- Save/load (`save_game()`, `load_game()`) with configurable path
- Prestige methods (`reset_for_prestige`, `emit_currency_changed_for_all`)
---
### Step 3: Convert PrestigeManager
**File to modify:** `core/prestige/prestige_manager.gd`
**Changes:**
- Remove autoload registration from `project.godot`
- Add `@export var game_state: LevelGameState`
- Change `/root/PrestigeManager` lookup to use direct reference
- Make it a child node of `LevelGameState` in scenes
**Key fix in `_get_prestige_multiplier()`:**
```gdscript
# Old: get_node_or_null("/root/PrestigeManager")
# New: Direct access through injected reference
```
---
### Step 4: Update CurrencyGenerator
**File to modify:** `core/generator/currency_generator.gd`
**Changes:**
- Replace all `GameState.` calls with `game_state.` calls
- Add `@onready var game_state: LevelGameState = find_parent("LevelGameState")`
- Update signal connections: `game_state.currency_changed.connect(...)`
- Update `_get_prestige_multiplier()` to use injected reference
---
### Step 5: Update CurrencyTile
**File to modify:** `currency_label.gd`
**Changes:**
```gdscript
@export var game_state: LevelGameState # ← NEW
func _ready() -> void:
if game_state == null:
push_error("CurrencyTile '%s' missing game_state reference" % name)
return
_currency_id = game_state.get_currency_id(currency)
# ... rest of initialization
```
---
### Step 6: Update project.godot
**Remove autoloads:**
```ini
# REMOVE these lines:
# BuffDatabase="*res://core/buff_database.gd"
# CurrencyDatabase="*res://core/currency_database.gd"
# GameState="*uid://d2j7tvlgxr2jp"
# PrestigeManager="*res://core/prestige/prestige_manager.gd"
```
---
### Step 7: Update tiny_sword.tscn
**Add LevelGameState node:**
```ini
[node name="LevelGameState" type="Node" parent="."]
currency_catalogue = ExtResource("currency_catalogue_tres")
buff_catalogue = ExtResource("buff_catalogue_tres")
goal_catalogue = ExtResource("goal_catalogue_tres")
save_file_path = "user://tiny_sword_save.json"
```
**Add PrestigeManager as child:**
```ini
[node name="PrestigeManager" type="Node" parent="LevelGameState"]
game_state = NodePath("..") # Reference to LevelGameState
```
**Wire up children:**
- Each `CurrencyTile`: `game_state = LevelGameState`
- Each `CurrencyGenerator`: auto-discovers via `find_parent()`
---
### Step 8: Create Catalogue Resources
**Create `.tres` files via Godot editor:**
1. `res://docs/gyms/tiny_sword/currencies/tiny_sword_catalogue.tres`
- `currencies = [gold, worker, food, wood]`
2. `res://docs/gyms/tiny_sword/buffs/tiny_sword_buffs.tres`
- `buffs = [all relevant buffs for this level]`
3. `res://docs/gyms/tiny_sword/goals/tiny_sword_goals.tres`
- `goals = [all relevant goals for this level]`
---
### Step 9: Clean Up
**Files to delete:**
- `core/currency_database.gd` (replaced by `CurrencyCatalogue`)
- `core/buff_database.gd` (replaced by `BuffCatalogue`)
- `core/game_state.gd` (replaced by `level_game_state.gd`)
---
## File Summary
### Files to Create (4)
| File | Purpose |
|------|---------|
| `core/currency_catalogue.gd` | Resource with `Array[Currency]` + lookup methods |
| `core/buff_catalogue.gd` | Resource with `Array[GeneratorBuffData]` + lookup methods |
| `core/goal_catalogue.gd` | Resource with `Array[GoalData]` + lookup methods |
| `core/level_game_state.gd` | Complete GameState implementation without autoload dependencies |
### Files to Modify (4)
| File | Changes |
|------|---------|
| `core/prestige/prestige_manager.gd` | Remove autoload, add `@export var game_state` |
| `core/generator/currency_generator.gd` | Add `find_parent()` lookup, replace `GameState.` |
| `currency_label.gd` | Add `@export var game_state: LevelGameState` |
| `project.godot` | Remove 4 autoloads |
### Files to Delete (3)
| File | Reason |
|------|--------|
| `core/currency_database.gd` | Replaced by `CurrencyCatalogue` |
| `core/buff_database.gd` | Replaced by `BuffCatalogue` |
| `core/game_state.gd` | Replaced by `level_game_state.gd` |
### Scene Updates
| File | Changes |
|------|---------|
| `tiny_sword.tscn` | Add `LevelGameState` node, add `PrestigeManager` child, wire catalogues |
| Create `.tres` resources | `tiny_sword_catalogue.tres`, `tiny_sword_buffs.tres`, `tiny_sword_goals.tres` |
---
## Benefits
1. **Decoupled levels**: Each level can exist independently without global dependencies
2. **Multiple levels**: Can support multiple levels with independent state (future feature)
3. **Explicit dependencies**: All dependencies are visible in the inspector
4. **Resource-based catalogues**: Content is defined as editor-authorable resources
5. **Per-level saves**: Each level has its own save file path
6. **Better testing**: Easier to test individual levels in isolation
---
## Risks and Considerations
1. **Signal connections**: UI components must be manually wired to `LevelGameState` instance
2. **PrestigeManager access**: Must ensure proper reference injection
3. **Catalogue population**: Catalogue resources must be created and populated in editor
4. **Migration**: Existing save files may need migration logic
---
## Implementation Order
1. Create catalogue classes (3 files)
2. Create level_game_state.gd (the core refactor)
3. Update prestige_manager.gd (remove autoload dependency)
4. Update currency_generator.gd (add game_state injection)
5. Update currency_label.gd (add game_state export)
6. Update project.godot (remove autoloads)
7. Create catalogue resources (.tres files via editor)
8. Update tiny_sword.tscn (wire everything together)
9. Delete old files (cleanup)
---
## Status
- [ ] Step 1: Create catalogue classes
- [ ] Step 2: Create level_game_state.gd
- [ ] Step 3: Update prestige_manager.gd
- [ ] Step 4: Update currency_generator.gd
- [ ] Step 5: Update currency_label.gd
- [ ] Step 6: Update project.godot
- [ ] Step 7: Create catalogue resources (manual editor work)
- [ ] Step 8: Update tiny_sword.tscn (manual editor work)
- [ ] Step 9: Delete old files

View File

@@ -1,433 +0,0 @@
# Refactoring Plan: Decoupled Buff System
## Overview
Decouple buffs from generators to enable:
- Global buff levels shared across all targets
- Multi-target buffs (including wildcards for all current + future generators)
- Automatic buff activation via goal achievement
- Clean separation between buff registry and generator state
- Flexible UI patterns (global buff shop, per-generator panels, debug controls)
## Current State (Problem)
- Buffs stored in `CurrencyGeneratorData.buffs` array (tight coupling)
- Buff levels tracked per-generator in `GameState.generator_buff_levels[generator_id][buff_id]`
- Buff resources have no `target_ids` - don't know which generators they affect
- Cannot create global buffs or multi-target buffs
- Buff logic scattered across `CurrencyGenerator` and `GameState`
## Target State (Solution)
- Buff registry in `GameState._buff_definitions` (global, auto-discovered from `res://sandbox/buffs/`)
- Buff levels stored globally: `GameState._buff_levels[buff_id]`
- Buff resources have `target_ids: Array[StringName]` (supports `["*"]` for wildcards)
- Generators query `GameState.get_buffs_for_generator(generator_id)` for applicable buffs
- Multipliers calculated in `GameState.get_effective_multiplier(generator_id, kind)`
---
## Implementation Phases
### Phase 1: Enhance GeneratorBuffData (Safe, Non-Breaking)
**File**: `core/generator/generator_buff_data.gd`
**Add:**
```gdscript
@export var target_ids: Array[StringName] = []
func targets_generator(generator_id: StringName) -> bool:
if target_ids.is_empty():
return false
if "*" in target_ids:
return true
return generator_id in target_ids
func get_target_generators() -> Array[StringName]:
return target_ids.duplicate()
```
**Timeline**: 1-2 hours
**Risk**: Low (additive change, backward compatible)
**Testing**: Verify existing buffs work with empty target_ids
---
### Phase 2: Extend GameState with Buff Registry
**File**: `core/game_state.gd`
**Add state variables:**
```gdscript
var _buff_definitions: Dictionary = {} # buff_id → GeneratorBuffData
var _buff_levels: Dictionary = {} # buff_id → int
var _buff_unlocked: Dictionary = {} # buff_id → bool
var _buff_active: Dictionary = {} # buff_id → bool (auto-set when unlocked)
signal buff_unlocked_changed(buff_id: StringName, unlocked: bool)
signal buff_level_changed(buff_id: StringName, new_level: int)
```
**Add registry methods:**
```gdscript
func register_buff(buff: GeneratorBuffData) -> void
func get_buff(buff_id: StringName) -> GeneratorBuffData
func get_all_buffs() -> Array[GeneratorBuffData]
func get_buff_level(buff_id: StringName) -> int
func set_buff_level(buff_id: StringName, level: int) -> void
func is_buff_unlocked(buff_id: StringName) -> bool
func set_buff_unlocked(buff_id: StringName, unlocked: bool) -> void
func is_buff_active(buff_id: StringName) -> bool
func get_generators_targeted_by(buff_id: StringName) -> Array[StringName]
func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]
func get_effective_multiplier(generator_id: StringName, kind: int) -> float
```
**Add goal evaluation helper:**
```gdscript
func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void
```
**Timeline**: 4-6 hours
**Risk**: High (core state management)
**Testing**: Unit tests for each method, verify goal-based unlocks
---
### Phase 3: Update Save/Load System
**File**: `core/game_state.gd`
**New save format (v2):**
```json
{
"save_format_version": 2,
"currencies": { ... },
"generator_states": { ... },
"buff_definitions": { ... },
"buff_levels": { ... },
"buff_unlocked": { ... },
"buff_active": { ... },
"last_save_time": 1234567890
}
```
**Add serialization helpers:**
```gdscript
func _serialize_buff_definitions() -> Dictionary
func _serialize_buff_data(buff: GeneratorBuffData) -> Dictionary
func _deserialize_buff_definitions(raw: Variant) -> void
```
**Version check in load_game():**
```gdscript
if version < 2:
push_error("Save file version %d too old. Please start fresh." % version)
return
```
**Timeline**: 3-4 hours
**Risk**: High (data loss if bugs exist)
**Testing**: Save/load cycles, verify buff state persists, test version rejection
---
### Phase 4: Update CurrencyGenerator
**File**: `core/generator/currency_generator.gd`
**Changes:**
```gdscript
# Replace:
func get_buffs() -> Array[GeneratorBuffData] # Now queries GameState
func _get_multiplier_for_buff_kind(kind: int) -> float: # Delegates to GameState
return GameState.get_effective_multiplier(_generator_id, kind)
# Update _register_buffs() to only evaluate unlock goals
func _register_buffs() -> void:
for buff in get_buffs():
if buff == null:
continue
var buff_id: StringName = buff.id
if not GameState.get_buff(buff_id):
GameState.register_buff(buff)
_try_unlock_buff_from_goal(buff)
```
**Timeline**: 2-3 hours
**Risk**: Medium (runtime behavior change)
**Testing**: Verify production multipliers, buff purchases, goal unlocks
---
### Phase 5: Update PrestigeManager
**File**: `core/prestige/prestige_manager.gd`
**Add buff reset logic:**
```gdscript
func perform_prestige() -> bool:
# ... existing logic ...
GameState.reset_for_prestige(true, false)
_reset_all_buff_levels()
# ... rest of logic ...
func _reset_all_buff_levels() -> void:
for buff_id in GameState._buff_levels.keys():
GameState.set_buff_level(buff_id, 0)
GameState.set_buff_unlocked(buff_id, false)
```
**Timeline**: 1-2 hours
**Risk**: Low
**Testing**: Verify buff levels reset after prestige
---
### Phase 6: Update UI Components
**File**: `core/generator/generator_container.gd`
**Minimal changes needed** - `GeneratorPanel._build_buff_rows()` should work automatically since it uses `_generator.get_buffs()`.
**Optional: Add Global Buff Panel**
Create `res://ui/global_buff_panel.gd` for testing/future UI:
```gdscript
extends PanelContainer
func _build_buff_list() -> void:
for buff in GameState.get_all_buffs():
if GameState.is_buff_hidden(buff.id):
continue
# Create UI row showing buff state
```
**Timeline**: 1-2 hours (optional)
**Risk**: Low
**Testing**: Verify buff UI updates on level/unlock changes
---
### Phase 7: Data Migration (One-Time Manual)
**Manual steps:**
1. **Update `res://sandbox/buffs/*.tres`:**
- Open each buff in Godot editor
- Add `target_ids` array (e.g., `["farm", "forestry"]` or `["*"]`)
2. **Update `res://sandbox/generators/*.tres`:**
- Remove `buffs` array field
- (Optional) Add `expected_buff_ids` for documentation
3. **Delete old saves:**
- `user://idle_save.json` - starts fresh with v2 format
**Timeline**: 1 hour
**Risk**: Low (data files only)
**Testing**: Load game, verify all buffs work correctly
---
### Phase 8: Buff Auto-Discovery System (Bonus Feature)
**New file**: `core/buff_database.gd`
```gdscript
extends Node
const BUFF_DIRECTORY_PATH: String = "res://sandbox/buffs"
var _buff_by_id: Dictionary = {}
var _initialized: bool = false
func _ready() -> void:
_initialize_buff_catalog()
func _initialize_buff_catalog() -> void:
_buff_by_id.clear()
var dir: DirAccess = DirAccess.open(BUFF_DIRECTORY_PATH)
if dir == null:
push_warning("Unable to open buff directory: %s" % BUFF_DIRECTORY_PATH)
return
dir.list_dir_begin()
var entry_name: String = dir.get_next()
while not entry_name.is_empty():
if not dir.current_is_dir() and entry_name.ends_with(".tres"):
var path: String = "%s/%s" % [BUFF_DIRECTORY_PATH, entry_name]
var buff: GeneratorBuffData = load(path) as GeneratorBuffData
if buff != null:
GameState.register_buff(buff)
entry_name = dir.get_next()
dir.list_dir_end()
func get_all_buffs() -> Array[GeneratorBuffData]:
return _buff_by_id.values()
func get_buff(buff_id: StringName) -> GeneratorBuffData:
return _buff_by_id.get(buff_id)
```
**Add `BuffDatabase` to project.godot autoloads**
**Timeline**: 2-3 hours
**Risk**: Low
**Testing**: Verify all buffs auto-load at runtime
---
## Timeline Summary
| Phase | Duration | Risk |
|-------|----------|------|
| Phase 1: GeneratorBuffData | 1-2h | Low |
| Phase 2: GameState Extension | 4-6h | High |
| Phase 3: Save/Load Update | 3-4h | High |
| Phase 4: CurrencyGenerator | 2-3h | Medium |
| Phase 5: PrestigeManager | 1-2h | Low |
| Phase 6: UI Updates | 1-2h | Low |
| Phase 7: Data Migration | 1h | Low |
| Phase 8: Buff Auto-Discovery | 2-3h | Low |
| **Total** | **15-23 hours** | |
---
## Testing Checklist
- [ ] Buff with `target_ids: ["farm", "forestry"]` applies to both
- [ ] Buff with `target_ids: ["*"]` applies to all current + future generators
- [ ] Buying a buff level increases global level (not per-generator)
- [ ] Buff unlock goal automatically activates buff when met
- [ ] Generator production reflects active buff multipliers
- [ ] Save/load preserves buff levels and unlock states
- [ ] Prestige resets all buff levels to 0
- [ ] Adding new generator auto-includes wildcard buffs
- [ ] Multiple buff kinds stack multiplicatively
- [ ] Inactive (locked) buffs don't apply multipliers
- [ ] Buff definitions auto-load from `res://sandbox/buffs/`
- [ ] New buff resources appear in registry without code changes
---
## Rollback Plan
If issues arise:
1. **Revert code changes** - Use git to restore previous state
2. **Delete user://idle_save.json** - Start fresh with v1 format
3. **Restore buff/generator .tres files** - Remove target_ids, restore buffs arrays
4. **Remove GameState buff methods** - Restore original API
**Note**: No save migration code - breaking change requires fresh starts
---
## Success Criteria
- [ ] All generators produce correct amounts with active buffs
- [ ] Buff UI can display global buff list or per-generator view
- [ ] Save files load correctly and preserve buff state
- [ ] Prestige resets buff levels as expected
- [ ] New buffs can be added by dropping .tres files in sandbox/buffs/
- [ ] Wildcard buffs auto-apply to new generators
- [ ] No performance degradation in _process loop
---
## Future Enhancements (Post-Implementation)
1. **Buff synergies**: "If you have X buff, Y buff gets +10%"
2. **Buff tiers**: Common/rare/legendary classification
3. **Conditional buffs**: "Only active if generator_ownership > 10"
4. **Buff stacking rules**: Additive vs multiplicative vs highest-only
5. **Prestige-only buffs**: Permanent buffs unlocked via prestige currency
6. **Buff UI shop**: Global buff purchase interface
7. **Buff preview**: Show "what-if" calculations before purchase
---
## Technical Notes
### Architecture Decisions
| Decision | Implementation |
|----------|----------------|
| **Buff Levels** | Global (shared across all targets) |
| **Buff Registry** | Auto-discovered from `res://sandbox/buffs/` |
| **Wildcards** | `["*"]` matches all current AND future generators |
| **Default State** | Inactive/unlocked = false, auto-activates on goal met |
| **Persistence** | Buff definitions serialized to save (by id) |
| **Prestige** | All buff levels reset to 0 |
| **Activation** | Automatic via goals, no manual toggle |
### Key API Changes
**Old API (per-generator state):**
```gdscript
GameState.register_generator_buff(generator_id, buff_id, level)
GameState.get_generator_buff_level(generator_id, buff_id)
GameState.is_generator_buff_unlocked(generator_id, buff_id)
```
**New API (global buff state):**
```gdscript
GameState.register_buff(buff: GeneratorBuffData)
GameState.get_buff(buff_id) -> GeneratorBuffData
GameState.get_buff_level(buff_id) -> int
GameState.is_buff_unlocked(buff_id) -> bool
GameState.get_buffs_for_generator(generator_id) -> Array[GeneratorBuffData]
GameState.get_effective_multiplier(generator_id, kind) -> float
```
### Save Format Migration
**Version 1 (old - incompatible):**
```json
{
"currencies": { ... },
"generator_states": { ... },
"generator_buff_levels": { "farm": { "farm_flux": 5 } },
"generator_buff_unlocked": { "farm": { "farm_flux": true } }
}
```
**Version 2 (new):**
```json
{
"save_format_version": 2,
"currencies": { ... },
"generator_states": { ... },
"buff_definitions": { "farm_flux": { ... } },
"buff_levels": { "farm_flux": 5 },
"buff_unlocked": { "farm_flux": true },
"buff_active": { "farm_flux": true }
}
```
---
## Dependencies
- Godot 4.6+ (current project version)
- Existing `BigNumber` system for currency math
- Existing `GameState` autoload
- Existing `GoalData` and `GoalRequirementData` for unlock conditions
---
## Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Save format incompatibility | Clear version check, error on old saves |
| Performance in _process loop | Cache multiplier calculations, only recalc on buff changes |
| Buff definition loading failures | Graceful degradation, warn but continue |
| Wildcard expansion at runtime | Explicit registration in generator _ready() |
| Multiple buffs of same kind | Multiplicative stacking (document clearly) |
---
## Approval Status
- [ ] Architecture approved
- [ ] Implementation phases reviewed
- [ ] Timeline accepted
- [ ] Risks understood
- [ ] Ready to begin Phase 1
---
*Last updated: Initial planning document*
*Status: Pending implementation approval*

View File

@@ -1,6 +1,7 @@
extends HBoxContainer
@export var currency: Resource
@export var game_state: LevelGameState
@onready var _label_start = $LabelStart
@onready var _label_end = $LabelEnd
@@ -22,15 +23,16 @@ func set_limits(start: BigNumber, end: BigNumber) -> void:
_progress_bar.max_value = _end.get_ratio(_start)
func _ready() -> void:
if currency == null:
currency = CurrencyDatabase.get_currency_resource(GameState.GOLD_CURRENCY_ID)
if currency == null:
push_warning("BigNumberProgressBar '%s' has no currency configured." % String(name))
return
_currency_id = GameState.get_currency_id(currency)
if game_state == null:
push_warning("BigNumberProgressBar '%s' has no game_state configured." % String(name))
return
_currency_id = game_state.get_currency_id(currency)
GameState.currency_changed.connect(_on_currency_changed)
_current = GameState.get_currency_amount_by_id(_currency_id)
game_state.currency_changed.connect(_on_currency_changed)
_current = game_state.get_currency_amount_by_id(_currency_id)
set_limits(_current, _current.add(BigNumber.new(1, 1)))

24
core/buff_catalogue.gd Normal file
View File

@@ -0,0 +1,24 @@
class_name BuffCatalogue
extends Resource
@export var buffs: Array[GeneratorBuffData] = []
func get_buff_by_id(id: StringName) -> GeneratorBuffData:
for buff in buffs:
if buff.id == id:
return buff
return null
func get_all_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for buff in buffs:
if buff.id:
ids.append(buff.id)
return ids
func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]:
var result: Array[GeneratorBuffData] = []
for buff in buffs:
if buff.targets_generator(generator_id):
result.append(buff)
return result

View File

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

View File

@@ -1,47 +0,0 @@
extends Node
## Auto-discovers and registers all buff resources from res://docs/gyms/buffs/
const BUFF_DIRECTORY_PATH: String = "res://docs/gyms/buffs"
var _buff_by_id: Dictionary = {}
var _initialized: bool = false
func _ready() -> void:
_initialize_buff_catalog()
func _initialize_buff_catalog() -> void:
if _initialized:
return
_buff_by_id.clear()
var dir: DirAccess = DirAccess.open(BUFF_DIRECTORY_PATH)
if dir == null:
push_warning("Unable to open buff directory: %s" % BUFF_DIRECTORY_PATH)
_initialized = true
return
dir.list_dir_begin()
var entry_name: String = dir.get_next()
while not entry_name.is_empty():
if not dir.current_is_dir() and entry_name.ends_with(".tres"):
var path: String = "%s/%s" % [BUFF_DIRECTORY_PATH, entry_name]
var buff: GeneratorBuffData = load(path) as GeneratorBuffData
if buff != null:
_buff_by_id[buff.id] = buff
GameState.register_buff(buff)
entry_name = dir.get_next()
dir.list_dir_end()
_initialized = true
print("BuffDatabase initialized with %d buffs" % _buff_by_id.size())
func get_all_buffs() -> Array[GeneratorBuffData]:
var result: Array[GeneratorBuffData] = []
for buff in _buff_by_id.values():
if buff is GeneratorBuffData:
result.append(buff)
return result
func get_buff(buff_id: StringName) -> GeneratorBuffData:
return _buff_by_id.get(buff_id, null)

View File

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

View File

@@ -0,0 +1,17 @@
class_name CurrencyCatalogue
extends Resource
@export var currencies: Array[Currency] = []
func get_currency_by_id(id: StringName) -> Currency:
for currency in currencies:
if currency.id == id:
return currency
return null
func get_all_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for currency in currencies:
if currency.id != &"":
ids.append(currency.id)
return ids

View File

@@ -0,0 +1 @@
uid://621tus0uvbrd

View File

@@ -1,164 +0,0 @@
extends Node
#const GOLD_CURRENCY_ID: StringName = &"gold"
#const GEMS_CURRENCY_ID: StringName = &"gems"
#const LEGACY_CURRENCY_INDEX_TO_ID: Dictionary = {
#0: GOLD_CURRENCY_ID,
#1: GEMS_CURRENCY_ID,
#}
const CURRENCY_DIRECTORY_PATH: String = "res://docs/gyms/currencies"
const CURRENCY_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
var _currency_by_id: Dictionary = {}
var _catalog_initialized: bool = false
func _ready() -> void:
_initialize_currency_catalog()
func get_known_currencies() -> Array[Currency]:
_initialize_currency_catalog_if_needed()
var result: Array[Currency] = []
for raw_currency in _currency_by_id.values():
var currency: Currency = raw_currency as Currency
if currency != null:
result.append(currency)
return result
func get_known_currency_ids() -> Array[StringName]:
_initialize_currency_catalog_if_needed()
var ids: Array[StringName] = []
for raw_currency_id in _currency_by_id.keys():
var currency_id: StringName = _normalize_currency_id(StringName(String(raw_currency_id)))
if currency_id == &"":
continue
ids.append(currency_id)
return ids
func get_currency_id(currency: Currency) -> StringName:
if currency == null:
return &""
var raw_id: Variant = currency.id
if raw_id is StringName:
return _normalize_currency_id(raw_id)
return _normalize_currency_id(StringName(String(raw_id)))
func parse_currency_id(raw_currency: Variant) -> StringName:
#if raw_currency is int:
#return currency_id_from_legacy_index(raw_currency)
var currency_text: String = String(raw_currency).to_lower().strip_edges()
if currency_text == "gem":
currency_text = "gems"
return _normalize_currency_id(StringName(currency_text))
#func currency_id_from_legacy_index(currency_index: int) -> StringName:
#return LEGACY_CURRENCY_INDEX_TO_ID.get(currency_index, &"")
func is_known_currency_id(currency_id: StringName) -> bool:
_initialize_currency_catalog_if_needed()
var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
if normalized_currency_id == &"":
return false
return _currency_by_id.has(normalized_currency_id)
func get_currency_resource(currency_id: StringName) -> Currency:
_initialize_currency_catalog_if_needed()
var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
if normalized_currency_id == &"":
return null
var raw_currency: Variant = _currency_by_id.get(normalized_currency_id)
if raw_currency is Currency:
return raw_currency
return null
func get_currency_name(currency_id: StringName) -> String:
var currency: Currency = get_currency_resource(currency_id)
if currency != null:
var display_name: String = String(currency.display_name).strip_edges()
if not display_name.is_empty():
return display_name
return _humanize_currency_id(currency_id)
func get_currency_icon(currency_id: StringName) -> Texture2D:
var currency: Resource = get_currency_resource(currency_id)
if currency == null:
return null
var raw_icon: Variant = currency.icon
if raw_icon is Texture2D:
return raw_icon
return null
func _initialize_currency_catalog_if_needed() -> void:
if _catalog_initialized:
return
_initialize_currency_catalog()
func _initialize_currency_catalog() -> void:
_currency_by_id.clear()
_catalog_initialized = true
var currency_resource_paths: PackedStringArray = _discover_currency_resource_paths()
for resource_path in currency_resource_paths:
var loaded_resource: Resource = load(resource_path)
if loaded_resource == null:
push_warning("Failed to load currency resource at '%s'." % resource_path)
continue
if not (loaded_resource is Currency):
continue
var currency_id: StringName = get_currency_id(loaded_resource)
if currency_id == &"":
push_warning("Skipping currency with missing id: %s" % resource_path)
continue
if _currency_by_id.has(currency_id):
push_warning("Duplicate currency id '%s' found at %s. Keeping first occurrence." % [String(currency_id), resource_path])
continue
_currency_by_id[currency_id] = loaded_resource
if _currency_by_id.is_empty():
push_warning("No currency resources discovered under %s. Ensure currency .tres files exist and are included in export presets." % CURRENCY_DIRECTORY_PATH)
func _discover_currency_resource_paths() -> PackedStringArray:
var result: PackedStringArray = []
var directory: DirAccess = DirAccess.open(CURRENCY_DIRECTORY_PATH)
if directory == null:
push_warning("Unable to open currency directory: %s" % CURRENCY_DIRECTORY_PATH)
return result
directory.list_dir_begin()
var entry_name: String = directory.get_next()
while not entry_name.is_empty():
if directory.current_is_dir() or entry_name.begins_with("."):
entry_name = directory.get_next()
continue
var extension: String = entry_name.get_extension().to_lower()
if CURRENCY_RESOURCE_EXTENSIONS.has(extension):
result.append("%s/%s" % [CURRENCY_DIRECTORY_PATH, entry_name])
entry_name = directory.get_next()
directory.list_dir_end()
result.sort()
return result
func _normalize_currency_id(currency_id: StringName) -> StringName:
var normalized: String = String(currency_id).to_lower().strip_edges()
if normalized.is_empty():
return &""
return StringName(normalized)
func _humanize_currency_id(currency_id: StringName) -> String:
var normalized: String = String(_normalize_currency_id(currency_id))
if normalized.is_empty():
return "Unknown"
return normalized.replace("_", " ").capitalize()

View File

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

View File

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

View File

@@ -44,42 +44,45 @@ var _mouse_entered: bool = false
## Time left until next click/hover grant is allowed.
var _remaining_click_cooldown_seconds: float = 0.0
## Reference to LevelGameState instance.
@onready var game_state: LevelGameState = find_parent("LevelGameState")
## Number of currently owned generators.
## Backed by GameState via this generator's resolved id.
## Backed by LevelGameState via this generator's resolved id.
var owned: int:
get:
_ensure_registered()
return GameState.get_generator_owned(_generator_id)
return game_state.get_generator_owned(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_owned(_generator_id, value)
game_state.set_generator_owned(_generator_id, value)
## Lifetime purchased generators (does not decrease if ownership drops).
var purchased_count: int:
get:
_ensure_registered()
return GameState.get_generator_purchased_count(_generator_id)
return game_state.get_generator_purchased_count(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_purchased_count(_generator_id, value)
game_state.set_generator_purchased_count(_generator_id, value)
## Whether this generator is unlocked for the player.
var is_unlocked: bool:
get:
_ensure_registered()
return GameState.is_generator_unlocked(_generator_id)
return game_state.is_generator_unlocked(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_unlocked(_generator_id, value)
game_state.set_generator_unlocked(_generator_id, value)
## Whether this unlocked generator is currently visible/usable to the player.
var is_available: bool:
get:
_ensure_registered()
return GameState.is_generator_available(_generator_id)
return game_state.is_generator_available(_generator_id)
set(value):
_ensure_registered()
GameState.set_generator_available(_generator_id, value)
game_state.set_generator_available(_generator_id, value)
## Accumulated elapsed time toward the next automatic production cycle.
var cycle_progress_seconds: float = 0.0
@@ -98,9 +101,10 @@ func _ready() -> void:
_generator_id = _resolve_generator_id()
cycle_progress_seconds = 0.0
GameState.currency_changed.connect(_on_currency_changed)
GameState.generator_state_changed.connect(_on_generated_state_changed)
GameState.goal_completed.connect(_on_goal_completed)
if game_state:
game_state.currency_changed.connect(_on_currency_changed)
game_state.generator_state_changed.connect(_on_generated_state_changed)
game_state.goal_completed.connect(_on_goal_completed)
_ensure_registered()
@@ -220,18 +224,27 @@ func buy_max() -> int:
func get_buffs() -> Array[GeneratorBuffData]:
if data == null:
return []
return GameState.get_buffs_for_generator(_generator_id)
if game_state == null:
return []
return game_state.get_buffs_for_generator(_generator_id)
func get_buff_level(buff_id: StringName) -> int:
_ensure_registered()
return GameState.get_buff_level(buff_id)
if game_state == null:
return 0
return game_state.get_buff_level(buff_id)
func is_buff_unlocked(buff_id: StringName) -> bool:
_ensure_registered()
return GameState.is_buff_unlocked(buff_id)
if game_state == null:
return false
return game_state.is_buff_unlocked(buff_id)
func get_buff_cost(buff_id: StringName) -> BigNumber:
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return BigNumber.from_float(0.0)
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return BigNumber.from_float(0.0)
if not is_buff_unlocked(buff.id):
@@ -241,7 +254,10 @@ func get_buff_cost(buff_id: StringName) -> BigNumber:
return buff.get_cost_for_level(level)
func get_buff_cost_currency_id(buff_id: StringName) -> StringName:
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return &""
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return &""
return _resolve_buff_cost_currency_id(buff)
@@ -250,7 +266,10 @@ func can_buy_buff(buff_id: StringName) -> bool:
if not is_available_to_player():
return false
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return false
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return false
@@ -266,21 +285,24 @@ func can_buy_buff(buff_id: StringName) -> bool:
return false
var cost: BigNumber = buff.get_cost_for_level(current_level)
var current_amount: BigNumber = GameState.get_currency_amount_by_id(cost_currency_id)
var current_amount: BigNumber = game_state.get_currency_amount_by_id(cost_currency_id)
return current_amount.is_greater_than(cost) or current_amount.is_equal_to(cost)
func buy_buff(buff_id: StringName) -> bool:
if not is_available_to_player():
return false
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return false
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return false
if not GameState.is_buff_unlocked(buff.id):
if not game_state.is_buff_unlocked(buff.id):
return false
var current_level: int = GameState.get_buff_level(buff.id)
var current_level: int = game_state.get_buff_level(buff.id)
if not buff.can_purchase_next_level(current_level):
return false
@@ -294,19 +316,22 @@ func buy_buff(buff_id: StringName) -> bool:
return false
var next_level: int = current_level + 1
GameState.set_buff_level(buff.id, next_level)
game_state.set_buff_level(buff.id, next_level)
_apply_resource_purchase_buff(buff, next_level)
buff_purchased.emit(buff.id, next_level, cost, cost_currency_id)
return true
func is_buff_maxed(buff_id: StringName) -> bool:
var buff: GeneratorBuffData = GameState.get_buff(buff_id)
if game_state == null:
return true
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
if buff == null:
return true
if not GameState.is_buff_unlocked(buff.id):
if not game_state.is_buff_unlocked(buff.id):
return false
return not buff.can_purchase_next_level(GameState.get_buff_level(buff.id))
return not buff.can_purchase_next_level(game_state.get_buff_level(buff.id))
func get_automatic_production_multiplier() -> float:
return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER)
@@ -384,13 +409,18 @@ func _grants_click_while_hovering() -> bool:
return grants_click_while_hovering
func _get_prestige_multiplier() -> float:
var manager: PrestigeManager = get_node_or_null("/root/PrestigeManager")
if manager == null:
return 1.0
if not manager.get_total_multiplier():
if game_state == null:
return 1.0
var raw_multiplier: Variant = manager.get_total_multiplier()
var parent: Node = get_parent()
if parent == null:
return 1.0
var prestige_manager: PrestigeManager = parent.find_child("PrestigeManager", true, false)
if prestige_manager == null:
return 1.0
var raw_multiplier: Variant = prestige_manager.get_total_multiplier()
if raw_multiplier is float:
return maxf(raw_multiplier, 0.0)
if raw_multiplier is int:
@@ -405,14 +435,16 @@ func get_generator_id() -> StringName:
## Returns the configured currency id.
func get_currency_id() -> StringName:
return GameState.get_currency_id(currency)
if game_state == null:
return &""
return game_state.get_currency_id(currency)
func get_purchase_currency_id() -> StringName:
var purchase_currency: Currency = _resolve_purchase_currency()
if purchase_currency == null:
return &""
return GameState.get_currency_id(purchase_currency)
return game_state.get_currency_id(purchase_currency)
## True when the generator is both unlocked and available.
func is_available_to_player() -> bool:
@@ -424,13 +456,17 @@ func _add_currency(amount: BigNumber) -> void:
push_error("CurrencyGenerator '%s' cannot add currency: currency resource is null." % String(name))
return
GameState.add_currency(currency, amount)
if game_state == null:
return
game_state.add_currency(currency, amount)
func _add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void:
if currency_id == &"":
return
GameState.add_currency_by_id(currency_id, amount)
if game_state == null:
return
game_state.add_currency_by_id(currency_id, amount)
## Attempts to spend target currency; returns false when insufficient.
func _spend_currency(cost: BigNumber) -> bool:
@@ -438,25 +474,33 @@ func _spend_currency(cost: BigNumber) -> bool:
push_error("CurrencyGenerator '%s' cannot spend currency: currency resource is null." % String(name))
return false
return GameState.spend_currency(currency, cost)
if game_state == null:
return false
return game_state.spend_currency(currency, cost)
func _spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool:
if currency_id == &"":
return false
return GameState.spend_currency_by_id(currency_id, cost)
if game_state == null:
return false
return game_state.spend_currency_by_id(currency_id, cost)
## Returns the current amount for the configured currency type.
func _get_currency_amount() -> BigNumber:
if currency == null:
return BigNumber.from_float(0.0)
return GameState.get_currency_amount(currency)
if game_state == null:
return BigNumber.from_float(0.0)
return game_state.get_currency_amount(currency)
func _get_currency_amount_by_id(currency_id: StringName) -> BigNumber:
if currency_id == &"":
return BigNumber.from_float(0.0)
return GameState.get_currency_amount_by_id(currency_id)
if game_state == null:
return BigNumber.from_float(0.0)
return game_state.get_currency_amount_by_id(currency_id)
## Safely converts finite float values into BigNumber costs.
func _float_to_big_number(value: float) -> BigNumber:
@@ -484,7 +528,9 @@ func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName:
if cost_currency == null:
return &""
return GameState.get_currency_id(cost_currency)
if game_state == null:
return &""
return game_state.get_currency_id(cost_currency)
func _resolve_purchase_currency() -> Currency:
if data == null:
@@ -500,10 +546,14 @@ func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName:
if target_currency == null:
return &""
return GameState.get_currency_id(target_currency)
if game_state == null:
return &""
return game_state.get_currency_id(target_currency)
func _get_multiplier_for_buff_kind(kind: GeneratorBuffData.BuffKind) -> float:
return GameState.get_effective_multiplier(_generator_id, kind)
if game_state == null:
return 1.0
return game_state.get_effective_multiplier(_generator_id, kind)
func _apply_resource_purchase_buff(buff: GeneratorBuffData, level: int) -> void:
if buff == null:
@@ -533,7 +583,7 @@ func _resolve_generator_id() -> StringName:
return &"generator"
## Ensures this generator has a state entry in GameState.
## Ensures this generator has a state entry in LevelGameState.
func _ensure_registered() -> void:
if _is_registered or _is_registering:
return
@@ -543,8 +593,11 @@ func _ensure_registered() -> void:
if _generator_id == &"":
return
if game_state == null:
return
_is_registering = true
GameState.register_generator(
game_state.register_generator(
_generator_id,
_default_unlocked_state(),
_default_available_state(),
@@ -555,6 +608,9 @@ func _ensure_registered() -> void:
_is_registering = false
func _register_buffs() -> void:
if game_state == null:
return
for buff in get_buffs():
if buff == null:
continue
@@ -564,20 +620,24 @@ func _register_buffs() -> void:
continue
var buff_id: StringName = StringName(buff_id_text)
GameState.register_buff(buff)
game_state.register_buff(buff)
if not GameState.is_buff_unlocked(buff_id):
var persisted_level: int = GameState.get_buff_level(buff_id)
if not game_state.is_buff_unlocked(buff_id):
var persisted_level: int = game_state.get_buff_level(buff_id)
var starts_unlocked: bool = buff.unlocked or persisted_level > 0
GameState.set_buff_unlocked(buff_id, starts_unlocked)
game_state.set_buff_unlocked(buff_id, starts_unlocked)
_try_unlock_buff_from_goal(buff)
func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
GameState._try_unlock_buff_from_goal(buff)
if game_state == null:
return
game_state._try_unlock_buff_from_goal(buff)
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
GameState.evaluate_all_goals()
if game_state == null:
return
game_state.evaluate_all_goals()
func _on_goal_completed(goal_id: StringName) -> void:
if data == null or not data.has_unlock_goal():
@@ -587,10 +647,12 @@ func _on_goal_completed(goal_id: StringName) -> void:
if is_available_to_player():
return
GameState.set_generator_unlocked(_generator_id, true)
GameState.set_generator_available(_generator_id, true)
if game_state == null:
return
game_state.set_generator_unlocked(_generator_id, true)
game_state.set_generator_available(_generator_id, true)
goal_achieved.emit(_generator_id, goal_id)
_on_generated_state_changed(_generator_id, GameState._get_generator_state(_generator_id))
_on_generated_state_changed(_generator_id, game_state._get_generator_state(_generator_id))
## Default unlocked state loaded from generator data.
func _default_unlocked_state() -> bool:

View File

@@ -58,10 +58,10 @@ func get_unlock_goal_amount() -> BigNumber:
if unlock_goal == null:
return BigNumber.from_float(0.0)
var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements()
if valid_requirements.is_empty():
var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements()
if requirements.is_empty():
return BigNumber.from_float(0.0)
var requirement_resource: GoalRequirementData = valid_requirements[0]
var requirement_resource: GoalRequirementData = requirements[0]
if requirement_resource == null:
return BigNumber.from_float(0.0)
@@ -75,10 +75,10 @@ func get_unlock_goal_currency() -> Currency:
if unlock_goal == null:
return null
var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements()
if valid_requirements.is_empty():
var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements()
if requirements.is_empty():
return null
var requirement_resource: GoalRequirementData = valid_requirements[0]
var requirement_resource: GoalRequirementData = requirements[0]
if requirement_resource == null:
return null

View File

@@ -37,11 +37,12 @@ func _ready() -> void:
_generator.purchase_failed.connect(_on_generator_updated)
_generator.buff_purchased.connect(_on_generator_updated)
_generator.buff_purchase_failed.connect(_on_generator_updated)
GameState.currency_changed.connect(_on_currency_changed)
GameState.generator_state_changed.connect(_on_generator_state_changed)
GameState.generator_buff_level_changed.connect(_on_generator_buff_level_changed)
GameState.generator_buff_unlocked_changed.connect(_on_generator_buff_unlocked_changed)
if _generator.game_state:
_generator.game_state.currency_changed.connect(_on_currency_changed)
_generator.game_state.generator_state_changed.connect(_on_generator_state_changed)
_generator.game_state.generator_buff_level_changed.connect(_on_generator_buff_level_changed)
_generator.game_state.generator_buff_unlocked_changed.connect(_on_generator_buff_unlocked_changed)
_name_label.text = _generator.data.name if _generator.data != null else "Generator"
_build_buff_rows()
@@ -123,9 +124,9 @@ func _refresh_buff_rows(can_interact: bool) -> void:
if not buff_unlocked:
var locked_hint: String = "Locked"
var unlock_goal_currency: Resource = buff.get_unlock_goal_currency()
if buff.has_unlock_goal() and unlock_goal_currency != null:
var goal_currency_id: StringName = GameState.get_currency_id(unlock_goal_currency)
var currency_name: String = GameState.get_currency_name(goal_currency_id)
if buff.has_unlock_goal() and unlock_goal_currency != null and _generator.game_state:
var goal_currency_id: StringName = _generator.game_state.get_currency_id(unlock_goal_currency)
var currency_name: String = _generator.game_state.get_currency_name(goal_currency_id)
var required_text: String = buff.get_unlock_goal_amount().to_string_suffix(2)
locked_hint = "Unlock at %s %s" % [required_text, currency_name]
@@ -141,7 +142,7 @@ func _refresh_buff_rows(can_interact: bool) -> void:
var cost: BigNumber = _generator.get_buff_cost(buff.id)
var cost_currency_id: StringName = _generator.get_buff_cost_currency_id(buff.id)
var currency_label: String = GameState.get_currency_name(cost_currency_id)
var currency_label: String = _generator.game_state.get_currency_name(cost_currency_id) if _generator.game_state else "Unknown"
var cost_text: String = "%s %s" % [cost.to_string_suffix(2), currency_label]
var can_buy: bool = can_interact and _generator.can_buy_buff(buff.id)
row.tile.set_runtime(level, effect_text, cost_text, can_buy, false)
@@ -161,7 +162,7 @@ func _refresh_generator_ui() -> void:
_owned_value.text = str(_generator.owned)
if can_interact:
var purchase_currency_id: StringName = _generator.get_purchase_currency_id()
var purchase_currency_name: String = GameState.get_currency_name(purchase_currency_id)
var purchase_currency_name: String = _generator.game_state.get_currency_name(purchase_currency_id) if _generator.game_state else "Unknown"
if purchase_currency_name.is_empty():
purchase_currency_name = "Unconfigured"
_next_cost_value.text = "%s %s" % [_generator.get_next_cost().to_string_suffix(2), purchase_currency_name]

17
core/goal_catalogue.gd Normal file
View File

@@ -0,0 +1,17 @@
class_name GoalCatalogue
extends Resource
@export var goals: Array[GoalData] = []
func get_goal_by_id(id: StringName) -> GoalData:
for goal in goals:
if goal.id == id:
return goal
return null
func get_all_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for goal in goals:
if goal.has_id():
ids.append(goal.id)
return ids

View File

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

View File

@@ -38,12 +38,13 @@ enum UnlockBehavior {
### Methods
```gdscript
func is_valid() -> bool # Has id and valid requirements
func is_met() -> bool # All requirements satisfied
func get_progress() -> float # 0.0 to 1.0 based on closest requirement
func get_first_requirement_summary() -> String # "100 Gold" format
func has_id() -> bool # Has non-empty id
func is_valid() -> bool # Has valid id and requirements
func get_requirements() -> Array[GoalRequirementData] # Access requirements
```
**Note:** State-dependent methods (`is_met()`, `get_progress()`, `get_summary_text()`) have been moved to `LevelGameState` for proper decoupling.
## GoalRequirementData
Single condition within a goal.
@@ -62,12 +63,13 @@ extends Resource
### Methods
```gdscript
func get_currency() -> Currency # Currency to track
func get_amount() -> BigNumber # Required amount
func is_met() -> bool # Current >= required
func get_progress() -> float # Log-scaled 0.0-1.0
func get_summary_text() -> String # "100 Gold" format
func has_valid_amount() -> bool # Amount is non-negative
```
**Note:** State-dependent methods (`is_met()`, `get_progress()`, `get_summary_text()`) have been moved to `LevelGameState` for proper decoupling.
### Progress Calculation
Uses **logarithmic scaling** for better visual feedback on huge numbers:
@@ -87,18 +89,18 @@ This means reaching 50% progress on a 1e100 goal requires ~1e50 currency.
### Automatic Checking
`GameState` evaluates goals whenever currency changes:
`LevelGameState` evaluates goals whenever currency changes:
```gdscript
func _on_currency_changed(currency_id, new_amount):
GameState.evaluate_all_goals()
game_state.evaluate_all_goals()
```
### Completion Flow
1. Currency updated → signal emitted
2. `GameState.evaluate_all_goals()` called
3. Each uncompleted goal checked via `is_met()`
2. `LevelGameState.evaluate_all_goals()` called
3. Each uncompleted goal checked via `is_goal_met(goal)`
4. If met → `goal_completed` signal emitted
5. Buffs with unlock goals checked → unlocked if goal complete
@@ -110,6 +112,16 @@ Goals control their own unlock behavior via `unlock_behavior`:
Generators with `unlock_goal` automatically unlock when the goal completes.
### Goal Methods in LevelGameState
```gdscript
func is_goal_requirement_met(requirement: GoalRequirementData) -> bool
func get_goal_requirement_progress(requirement: GoalRequirementData) -> float
func get_goal_requirement_summary(requirement: GoalRequirementData) -> String
func is_goal_met(goal: GoalData) -> bool
func get_goal_progress(goal: GoalData) -> float
```
## Usage Example
### Defining a Goal

View File

@@ -13,53 +13,22 @@ enum UnlockBehavior {
func has_id() -> bool:
return not String(id).strip_edges().is_empty()
func get_valid_requirements() -> Array[GoalRequirementData]:
var result: Array[GoalRequirementData] = []
for requirement_resource in requirements:
if requirement_resource == null:
continue
if not bool(requirement_resource.is_valid()):
continue
result.append(requirement_resource)
return result
func get_requirements() -> Array[GoalRequirementData]:
return requirements.duplicate()
func is_valid() -> bool:
if not has_id():
return false
return not get_valid_requirements().is_empty()
func is_met() -> bool:
var valid_requirements: Array[GoalRequirementData] = get_valid_requirements()
if valid_requirements.is_empty():
if requirements.is_empty():
return false
for requirement_resource in valid_requirements:
if not bool(requirement_resource.is_met()):
return false
return true
func get_first_requirement_summary() -> String:
var valid_requirements: Array[GoalRequirementData] = get_valid_requirements()
if valid_requirements.is_empty():
return ""
return String(valid_requirements[0].get_summary_text())
func get_progress() -> float:
var valid_requirements: Array[GoalRequirementData] = get_valid_requirements()
if valid_requirements.is_empty():
return 0.0
var min_progress: float = 1.0
for requirement in valid_requirements:
if requirement == null or not requirement.is_valid():
for req in requirements:
if req == null:
continue
if req.get_currency() == null:
continue
if not req.has_valid_amount():
continue
var requirement_progress: float = requirement.get_progress()
if requirement_progress < min_progress:
min_progress = requirement_progress
return min_progress
return true

View File

@@ -5,63 +5,11 @@ extends Resource
@export var amount_mantissa: float = 0.0
@export var amount_exponent: int = 0
func get_currency_id() -> StringName:
if currency == null:
return &""
return GameState.get_currency_id(currency)
func get_currency() -> Currency:
return currency
func get_amount() -> BigNumber:
return BigNumber.new(amount_mantissa, amount_exponent)
func has_valid_currency() -> bool:
var currency_id: StringName = get_currency_id()
if currency_id == &"":
return false
return GameState.is_known_currency_id(currency_id)
func has_valid_amount() -> bool:
return get_amount().mantissa >= 0.0
func is_valid() -> bool:
return has_valid_currency() and has_valid_amount()
func is_met() -> bool:
if not is_valid():
return false
var total_amount: BigNumber = GameState.get_total_currency_acquired_by_id(get_currency_id())
return not total_amount.is_less_than(get_amount())
func get_summary_text() -> String:
if not is_valid():
return ""
var currency_name: String = GameState.get_currency_name(get_currency_id())
return "%s %s" % [get_amount().to_string_suffix(2), currency_name]
func get_progress() -> float:
if not is_valid():
return 0.0
var current_amount: BigNumber = GameState.get_total_currency_acquired_by_id(get_currency_id())
var required_amount: BigNumber = get_amount()
if required_amount.mantissa <= 0.0:
return 1.0
if current_amount.is_greater_than(required_amount) or current_amount.is_equal_to(required_amount):
return 1.0
if current_amount.mantissa <= 0.0:
return 0.0
var current_log: float = log(maxf(current_amount.mantissa, 0.0001)) / log(10) + float(current_amount.exponent)
var required_log: float = log(maxf(required_amount.mantissa, 0.0001)) / log(10) + float(required_amount.exponent)
if required_log <= 0.0:
return 1.0
var progress: float = current_log / required_log
return clampf(progress, 0.0, 1.0)

View File

@@ -1,5 +1,5 @@
class_name LevelGameState
extends Node
# This script is added as an Autoload named 'GameState'
# ==========================================
# SIGNALS
@@ -13,6 +13,14 @@ signal buff_unlocked_changed(buff_id: StringName, unlocked: bool)
signal goal_completed(goal_id: StringName)
signal goal_progress_changed(goal_id: StringName, progress: float)
# ==========================================
# EXPORTED REFERENCES
# ==========================================
@export var currency_catalogue: CurrencyCatalogue
@export var buff_catalogue: BuffCatalogue
@export var goal_catalogue: GoalCatalogue
@export var save_file_path: String = "user://level_save.json"
# ==========================================
# STATE VARIABLES
# ==========================================
@@ -33,12 +41,11 @@ var _buff_active: Dictionary = {}
var _goal_definitions: Dictionary = {}
var _goal_completed: Dictionary = {}
var last_save_time: int = 0 # Unix timestamp
var last_save_time: int = 0
# ==========================================
# Save / Load
# ==========================================
static var file_name: String = "user://idle_save.json"
const SAVE_FORMAT_VERSION_KEY: String = "save_format_version"
const CURRENT_SAVE_FORMAT_VERSION: int = 3
const GENERATOR_OWNED_KEY: String = "owned"
@@ -62,51 +69,56 @@ const LAST_SAVE_TIME_KEY: String = "last_save_time"
func _ready() -> void:
_initialize_currency_maps()
_initialize_goals()
_initialize_catalogues()
_load_catalogue_goals()
load_game()
_try_unlock_all_buffs_from_goals()
# ==========================================
# STATE MODIFIERS
# ==========================================
#func add_gold(amount: BigNumber) -> void:
#add_currency_by_id(GOLD_CURRENCY_ID, amount)
#
#func spend_gold(cost: BigNumber) -> bool:
#return spend_currency_by_id(GOLD_CURRENCY_ID, cost)
#
#func add_gems(amount: BigNumber) -> void:
#add_currency_by_id(GEMS_CURRENCY_ID, amount)
#
#func spend_gems(cost: BigNumber) -> bool:
#return spend_currency_by_id(GEMS_CURRENCY_ID, cost)
func get_known_currencies() -> Array[Currency]:
return CurrencyDatabase.get_known_currencies()
func get_currency_id(currency: Resource) -> StringName:
return CurrencyDatabase.get_currency_id(currency)
return _normalize_currency_id(currency.id if currency else &"")
func parse_currency_id(raw_currency: Variant) -> StringName:
return CurrencyDatabase.parse_currency_id(raw_currency)
var currency_text: String = String(raw_currency).to_lower().strip_edges()
if currency_text == "gem":
currency_text = "gems"
return _normalize_currency_id(StringName(currency_text))
func is_known_currency_id(currency_id: StringName) -> bool:
return CurrencyDatabase.is_known_currency_id(currency_id)
if not currency_catalogue:
return false
var ids: Array[StringName] = currency_catalogue.get_all_ids()
return ids.has(_normalize_currency_id(currency_id))
func get_currency_resource(currency_id: StringName) -> Resource:
return CurrencyDatabase.get_currency_resource(currency_id)
func get_currency_resource(currency_id: StringName) -> Currency:
if not currency_catalogue:
return null
return currency_catalogue.get_currency_by_id(_normalize_currency_id(currency_id))
func get_currency_name(currency_id: StringName) -> String:
return CurrencyDatabase.get_currency_name(currency_id)
var currency: Currency = get_currency_resource(currency_id)
if currency != null:
var display_name: String = String(currency.display_name).strip_edges()
if not display_name.is_empty():
return display_name
return _humanize_currency_id(currency_id)
func get_currency_icon(currency_id: StringName) -> Texture2D:
return CurrencyDatabase.get_currency_icon(currency_id)
var currency: Resource = get_currency_resource(currency_id)
if currency == null:
return null
var raw_icon: Variant = currency.icon
if raw_icon is Texture2D:
return raw_icon
return null
func add_currency(currency: Resource, amount: BigNumber) -> void:
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
push_warning("Attempted to add currency with an invalid Currency resource.")
return
add_currency_by_id(currency_id, amount)
func spend_currency(currency: Resource, cost: BigNumber) -> bool:
@@ -114,7 +126,6 @@ func spend_currency(currency: Resource, cost: BigNumber) -> bool:
if currency_id == &"":
push_warning("Attempted to spend currency with an invalid Currency resource.")
return false
return spend_currency_by_id(currency_id, cost)
func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void:
@@ -387,6 +398,9 @@ func get_generators_targeted_by(buff_id: StringName) -> Array[StringName]:
return buff.get_target_generators()
func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]:
if buff_catalogue:
return buff_catalogue.get_buffs_for_generator(generator_id)
var result: Array[GeneratorBuffData] = []
for buff in _buff_definitions.values():
if buff is GeneratorBuffData and buff.targets_generator(generator_id):
@@ -475,6 +489,10 @@ func emit_currency_changed_for_all() -> void:
# PERSISTENCE (Save/Load)
# ==========================================
func save_game() -> void:
if save_file_path.is_empty():
push_warning("LevelGameState: save_file_path is empty; skipping save.")
return
var save_data = {
SAVE_FORMAT_VERSION_KEY: CURRENT_SAVE_FORMAT_VERSION,
CURRENCIES_KEY: _serialize_currency_balances(),
@@ -497,15 +515,17 @@ func save_game() -> void:
save_data[key] = payload
var file: FileAccess = FileAccess.open(file_name, FileAccess.WRITE)
var file: FileAccess = FileAccess.open(save_file_path, FileAccess.WRITE)
if file:
file.store_string(JSON.stringify(save_data))
else:
push_error("LevelGameState: Failed to open save file: %s" % save_file_path)
func load_game() -> void:
if not FileAccess.file_exists(file_name):
if not FileAccess.file_exists(save_file_path):
return
var file: FileAccess = FileAccess.open(file_name, FileAccess.READ)
var file: FileAccess = FileAccess.open(save_file_path, FileAccess.READ)
if file:
var parsed: Variant = JSON.parse_string(file.get_as_text())
if parsed is Dictionary:
@@ -624,21 +644,147 @@ func _collect_currency_ids_for_save() -> Array[StringName]:
continue
ids.append(normalized_currency_id)
for known_currency_id in CurrencyDatabase.get_known_currency_ids():
var normalized_currency_id: StringName = _normalize_currency_id(known_currency_id)
if normalized_currency_id == &"" or ids.has(normalized_currency_id):
continue
ids.append(normalized_currency_id)
if currency_catalogue:
for known_currency_id in currency_catalogue.get_all_ids():
var normalized_currency_id: StringName = _normalize_currency_id(known_currency_id)
if normalized_currency_id == &"" or ids.has(normalized_currency_id):
continue
ids.append(normalized_currency_id)
return ids
func _initialize_currency_maps() -> void:
for currency_id in CurrencyDatabase.get_known_currency_ids():
if currency_id == &"":
if currency_catalogue:
for currency_id in currency_catalogue.get_all_ids():
if currency_id == &"":
continue
_set_current_currency(currency_id, BigNumber.from_float(0.0))
_set_total_currency(currency_id, BigNumber.from_float(0.0))
_set_all_time_currency(currency_id, BigNumber.from_float(0.0))
func _initialize_catalogues() -> void:
if currency_catalogue:
for currency in currency_catalogue.currencies:
if currency == null:
continue
if buff_catalogue:
for buff in buff_catalogue.buffs:
if buff == null:
continue
register_buff(buff)
if goal_catalogue:
for goal in goal_catalogue.goals:
if goal == null:
continue
register_goal(goal)
func _load_catalogue_goals() -> void:
if goal_catalogue:
for goal in goal_catalogue.goals:
if goal == null:
continue
if not goal.has_id():
continue
if _goal_definitions.has(goal.id):
continue
_goal_definitions[goal.id] = goal
_goal_completed[goal.id] = false
func is_goal_requirement_met(requirement: GoalRequirementData) -> bool:
if requirement == null:
return false
var currency: Currency = requirement.get_currency()
if currency == null:
return false
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
return false
var total_amount: BigNumber = get_total_currency_acquired_by_id(currency_id)
var required_amount: BigNumber = requirement.get_amount()
return not total_amount.is_less_than(required_amount)
func get_goal_requirement_progress(requirement: GoalRequirementData) -> float:
if requirement == null:
return 0.0
var currency: Currency = requirement.get_currency()
if currency == null:
return 0.0
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
return 0.0
var current_amount: BigNumber = get_total_currency_acquired_by_id(currency_id)
var required_amount: BigNumber = requirement.get_amount()
if required_amount.mantissa <= 0.0:
return 1.0
if current_amount.is_greater_than(required_amount) or current_amount.is_equal_to(required_amount):
return 1.0
if current_amount.mantissa <= 0.0:
return 0.0
var current_log: float = log(maxf(current_amount.mantissa, 0.0001)) / log(10) + float(current_amount.exponent)
var required_log: float = log(maxf(required_amount.mantissa, 0.0001)) / log(10) + float(required_amount.exponent)
if required_log <= 0.0:
return 1.0
var progress: float = current_log / required_log
return clampf(progress, 0.0, 1.0)
func get_goal_requirement_summary(requirement: GoalRequirementData) -> String:
if requirement == null:
return ""
var currency: Currency = requirement.get_currency()
if currency == null:
return ""
var currency_id: StringName = get_currency_id(currency)
if currency_id == &"":
return ""
var currency_name: String = get_currency_name(currency_id)
return "%s %s" % [requirement.get_amount().to_string_suffix(2), currency_name]
func is_goal_met(goal: GoalData) -> bool:
if goal == null:
return false
for req in goal.get_requirements():
if not is_goal_requirement_met(req):
return false
return true
func get_goal_progress(goal: GoalData) -> float:
if goal == null:
return 0.0
var requirements: Array[GoalRequirementData] = goal.get_requirements()
if requirements.is_empty():
return 0.0
var min_progress: float = 1.0
for req in requirements:
if req == null:
continue
_set_current_currency(currency_id, BigNumber.from_float(0.0))
_set_total_currency(currency_id, BigNumber.from_float(0.0))
_set_all_time_currency(currency_id, BigNumber.from_float(0.0))
var req_progress: float = get_goal_requirement_progress(req)
if req_progress < min_progress:
min_progress = req_progress
return min_progress
func _normalize_currency_id(currency_id: StringName) -> StringName:
var normalized: String = String(currency_id).to_lower().strip_edges()
@@ -794,7 +940,6 @@ func _sanitize_generator_buff_unlocked(raw_unlocked: Dictionary) -> Dictionary:
if buff_key.is_empty():
continue
# Save format only stores true values, but keep parser tolerant.
var is_unlocked: bool = _to_bool(raw_unlocked.get(buff_key_variant, false), false)
if not is_unlocked:
continue
@@ -1010,57 +1155,6 @@ func _deserialize_buff_states(parsed_data: Dictionary) -> void:
# ==========================================
# GOALS
# ==========================================
func _initialize_goals() -> void:
_goal_definitions.clear()
_goal_completed.clear()
var goal_paths: PackedStringArray = _discover_goal_paths()
for goal_path in goal_paths:
var goal_resource: Resource = load(goal_path)
if goal_resource == null:
push_warning("Failed to load goal resource at '%s'." % goal_path)
continue
if not (goal_resource is GoalData):
continue
var goal: GoalData = goal_resource
if not goal.has_id():
push_warning("Skipping goal with missing id: %s" % goal_path)
continue
if _goal_definitions.has(goal.id):
push_warning("Duplicate goal id '%s' found at %s. Keeping first occurrence." % [String(goal.id), goal_path])
continue
_goal_definitions[goal.id] = goal
_goal_completed[goal.id] = false
func _discover_goal_paths() -> PackedStringArray:
var result: PackedStringArray = []
const GOAL_DIRECTORY_PATH: String = "res://docs/gyms/goals"
const GOAL_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
var directory: DirAccess = DirAccess.open(GOAL_DIRECTORY_PATH)
if directory == null:
push_warning("Unable to open goals directory: %s" % GOAL_DIRECTORY_PATH)
return result
directory.list_dir_begin()
var entry_name: String = directory.get_next()
while not entry_name.is_empty():
if directory.current_is_dir() or entry_name.begins_with("."):
entry_name = directory.get_next()
continue
var extension: String = entry_name.get_extension().to_lower()
if GOAL_RESOURCE_EXTENSIONS.has(extension):
result.append("%s/%s" % [GOAL_DIRECTORY_PATH, entry_name])
entry_name = directory.get_next()
directory.list_dir_end()
result.sort()
return result
func register_goal(goal: GoalData) -> void:
if goal == null:
return
@@ -1086,11 +1180,11 @@ func get_all_goals() -> Array[GoalData]:
func is_goal_completed(goal_id: StringName) -> bool:
return _goal_completed.get(goal_id, false)
func get_goal_progress(goal_id: StringName) -> float:
func get_goal_progress_by_id(goal_id: StringName) -> float:
var goal: GoalData = get_goal(goal_id)
if goal == null:
return 0.0
return goal.get_progress()
return get_goal_progress(goal)
func evaluate_all_goals() -> void:
for goal_id in _goal_completed.keys():
@@ -1099,13 +1193,15 @@ func evaluate_all_goals() -> void:
_try_unlock_all_buffs_from_goals()
goal_progress_changed.emit(&"", 0.0)
func _try_complete_goal(goal_id: StringName) -> void:
var goal: GoalData = _goal_definitions.get(goal_id, null)
if goal == null:
return
if _goal_completed[goal_id]:
return
if goal.is_met():
if is_goal_met(goal):
if goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL:
return
_goal_completed[goal_id] = true
@@ -1117,7 +1213,7 @@ func _complete_goal_manually(goal_id: StringName) -> void:
return
if _goal_completed.get(goal_id, false):
return
if not goal.is_met():
if not is_goal_met(goal):
return
_goal_completed[goal_id] = true
goal_completed.emit(goal_id)
@@ -1161,11 +1257,7 @@ func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void:
if not state.get(GENERATOR_UNLOCKED_KEY, false):
continue
var generator_data: CurrencyGeneratorData = _load_generator_data_from_id(gen_id)
if generator_data == null or not generator_data.has_unlock_goal():
continue
var goal_id: StringName = generator_data.get_unlock_goal_id()
var goal_id: StringName = _get_unlock_goal_id_for_generator(gen_id)
if not goal_id.is_empty():
_goal_completed[goal_id] = true
@@ -1173,32 +1265,27 @@ func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void:
if not _goal_completed.has(goal_id):
_goal_completed[goal_id] = false
func _load_generator_data_from_id(generator_id: String) -> CurrencyGeneratorData:
const GENERATORS_DIRECTORY_PATH: String = "res://docs/gyms/generators"
const GENERATOR_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
func _get_unlock_goal_id_for_generator(generator_id: String) -> StringName:
if goal_catalogue == null:
return &""
var directory: DirAccess = DirAccess.open(GENERATORS_DIRECTORY_PATH)
if directory == null:
return null
directory.list_dir_begin()
var entry_name: String = directory.get_next()
while not entry_name.is_empty():
if directory.current_is_dir() or entry_name.begins_with("."):
entry_name = directory.get_next()
for goal in goal_catalogue.goals:
if goal == null or not goal.has_id():
continue
var extension: String = entry_name.get_extension().to_lower()
if GENERATOR_RESOURCE_EXTENSIONS.has(extension):
var generator_path: String = "%s/%s" % [GENERATORS_DIRECTORY_PATH, entry_name]
var generator_resource: Resource = load(generator_path)
if generator_resource is CurrencyGeneratorData:
var gen_data: CurrencyGeneratorData = generator_resource
if String(gen_data.id) == generator_id:
directory.list_dir_end()
return gen_data
var goal_id: StringName = goal.id
var requirements: Array[GoalRequirementData] = goal.get_requirements()
for req in requirements:
if req == null or req.get_currency() == null:
continue
entry_name = directory.get_next()
directory.list_dir_end()
if String(req.get_currency().id) == generator_id:
return goal_id
return null
return &""
func _humanize_currency_id(currency_id: StringName) -> String:
var normalized: String = String(_normalize_currency_id(currency_id))
if normalized.is_empty():
return "Unknown"
return normalized.replace("_", " ").capitalize()

View File

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

View File

@@ -1,3 +1,4 @@
class_name PrestigeManager
extends Node
signal prestige_state_changed(total_prestige: BigNumber, pending_gain: BigNumber)
@@ -23,6 +24,7 @@ const MULTIPLIER_MODE_ADDITIVE: int = 0
const MULTIPLIER_MODE_MULTIPLICATIVE_POWER: int = 1
@export var config: Resource
@export var game_state: LevelGameState
var total_prestige_earned: BigNumber = BigNumber.from_float(0.0)
var current_prestige_unspent: BigNumber = BigNumber.from_float(0.0)
@@ -33,13 +35,17 @@ var last_reset_time: int = 0
func _ready() -> void:
if config == null:
config = load("res://docs/gyms/prestige/primary_prestige.tres") as Resource
config = load("res://docs/gyms/tiny_sword/prestige/primary_prestige.tres") as Resource
if not _is_config_valid():
push_warning("PrestigeManager has invalid or missing config; prestige is disabled.")
return
var loaded_state: Dictionary = GameState.get_external_save_data(SAVE_KEY)
if game_state == null:
push_warning("PrestigeManager: game_state reference is not set.")
return
var loaded_state: Dictionary = game_state.get_external_save_data(SAVE_KEY)
if loaded_state.is_empty():
_initialize_run_tracking_from_current_state()
else:
@@ -47,7 +53,7 @@ func _ready() -> void:
_validate_loaded_run_tracking()
_save_state_to_game_state()
GameState.currency_changed.connect(_on_currency_changed)
game_state.currency_changed.connect(_on_currency_changed)
prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain())
prestige_threshold_changed.emit(get_next_prestige_threshold())
@@ -134,13 +140,16 @@ func perform_prestige() -> bool:
prestige_resets_count += 1
last_reset_time = Time.get_unix_time_from_system()
GameState.reset_for_prestige(true, false)
if game_state:
game_state.reset_for_prestige(true, false)
_reset_all_buff_levels()
_reinitialize_generators_after_prestige_reset()
GameState.emit_currency_changed_for_all()
if game_state:
game_state.emit_currency_changed_for_all()
_initialize_run_tracking_from_current_state()
_save_state_to_game_state()
GameState.save_game()
if game_state:
game_state.save_game()
var total: BigNumber = get_total_prestige()
var next_threshold: BigNumber = get_next_prestige_threshold()
@@ -261,9 +270,12 @@ func _get_basis_value() -> BigNumber:
if source_currency_id == &"":
return BigNumber.from_float(0.0)
if game_state == null:
return BigNumber.from_float(0.0)
match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
BASIS_RUN_TOTAL:
var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id)
var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id)
var run_total: BigNumber = lifetime_now.subtract(run_start_total_source_currency)
if run_total.mantissa < 0.0:
return BigNumber.from_float(0.0)
@@ -271,7 +283,7 @@ func _get_basis_value() -> BigNumber:
BASIS_RUN_MAX:
return _copy_big_number(run_peak_source_currency)
_:
return _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))
return _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id))
func _initialize_run_tracking_from_current_state() -> void:
if config == null:
@@ -283,8 +295,13 @@ func _initialize_run_tracking_from_current_state() -> void:
run_peak_source_currency = BigNumber.from_float(0.0)
return
run_start_total_source_currency = _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))
run_peak_source_currency = _copy_big_number(GameState.get_currency_amount_by_id(source_currency_id))
if game_state == null:
run_start_total_source_currency = BigNumber.from_float(0.0)
run_peak_source_currency = BigNumber.from_float(0.0)
return
run_start_total_source_currency = _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id))
run_peak_source_currency = _copy_big_number(game_state.get_currency_amount_by_id(source_currency_id))
_save_state_to_game_state()
func _validate_loaded_run_tracking() -> void:
@@ -297,11 +314,14 @@ func _validate_loaded_run_tracking() -> void:
run_peak_source_currency = BigNumber.from_float(0.0)
return
var current_currency: BigNumber = GameState.get_currency_amount_by_id(source_currency_id)
if game_state == null:
return
var current_currency: BigNumber = game_state.get_currency_amount_by_id(source_currency_id)
if current_currency.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(current_currency)
var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id)
var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id)
if run_start_total_source_currency.is_greater_than(lifetime_now):
run_start_total_source_currency = _copy_big_number(lifetime_now)
@@ -327,12 +347,16 @@ func _deserialize_state(raw_state: Dictionary) -> void:
current_prestige_unspent = _copy_big_number(total_prestige_earned)
func _save_state_to_game_state() -> void:
GameState.set_external_save_data(SAVE_KEY, _serialize_state())
if game_state:
game_state.set_external_save_data(SAVE_KEY, _serialize_state())
func _reset_all_buff_levels() -> void:
for buff_id in GameState._buff_levels.keys():
GameState.set_buff_level(buff_id, 0)
GameState.set_buff_unlocked(buff_id, false)
if game_state == null:
return
for buff_id in game_state._buff_levels.keys():
game_state.set_buff_level(buff_id, 0)
game_state.set_buff_unlocked(buff_id, false)
func _reinitialize_generators_after_prestige_reset() -> void:
var scene_root: Node = get_tree().current_scene

View File

@@ -1,6 +1,8 @@
class_name PrestigePanel
extends PanelContainer
@export var game_state: LevelGameState
@onready var _total_value: Label = $MarginContainer/VBoxContainer/StatsGrid/TotalValue
@onready var _pending_value: Label = $MarginContainer/VBoxContainer/StatsGrid/PendingValue
@onready var _multiplier_value: Label = $MarginContainer/VBoxContainer/StatsGrid/MultiplierValue
@@ -18,8 +20,8 @@ func _ready() -> void:
manager.prestige_state_changed.connect(_on_prestige_state_changed)
if manager != null and manager.has_signal("prestige_performed"):
manager.prestige_performed.connect(_on_prestige_performed)
if not GameState.currency_changed.is_connected(_on_currency_changed):
GameState.currency_changed.connect(_on_currency_changed)
if game_state and not game_state.currency_changed.is_connected(_on_currency_changed):
game_state.currency_changed.connect(_on_currency_changed)
_refresh_ui()
@@ -33,7 +35,7 @@ func _on_reset_button_pressed() -> void:
var pending: BigNumber = manager.call("calculate_pending_gain")
var source_currency_id: StringName = manager.call("get_source_currency_id")
var source_currency_name: String = GameState.get_currency_name(source_currency_id)
var source_currency_name: String = game_state.get_currency_name(source_currency_id) if game_state else ""
var confirm_message: String = "Reset this run for +%s prestige?\n\nThis resets currencies, owned generators, and buff levels. Lifetime totals and prestige are kept." % pending.to_string_suffix(2)
if not source_currency_name.is_empty():
confirm_message = "%s\n\nBased on %s progression." % [confirm_message, source_currency_name]
@@ -43,7 +45,8 @@ func _on_reset_button_pressed() -> void:
_confirm_dialog.popup_centered_ratio(0.4)
func _on_save_button_pressed() -> void:
GameState.save_game()
if game_state:
game_state.save_game()
func _on_prestige_state_changed(_total: BigNumber, _pending: BigNumber) -> void:
_refresh_ui()

View File

@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://dmsmmgtbrul1t" path="res://core/prestige/prestige_panel.gd" id="1_panel"]
[ext_resource type="PackedScene" path="res://core/prestige/prestige_progress_bar.tscn" id="2_7b6yg"]
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/prestige/primary_prestige.tres" id="3_r8h5p"]
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="3_r8h5p"]
[node name="PrestigePanel" type="PanelContainer" unique_id=789062217]
offset_right = 420.0

View File

@@ -2,28 +2,31 @@ class_name CurrencyTile
extends HBoxContainer
@export var currency: Resource
@export var game_state: LevelGameState
@onready var _icon: TextureRect = $CurrencyIcon
@onready var _label: Label = $Label
@onready var _label: Label = $CurrencyValueLabel
@onready var _currency_label: Label = $CurrencyValueLabel
@onready var _debug_button: Button = $DebugIncomeButton
var _currency_id: StringName = &""
func _ready() -> void:
#if currency == null:
#currency = CurrencyDatabase.get_currency_resource(GameState.GOLD_CURRENCY_ID)
if currency == null:
push_warning("CurrencyTile '%s' has no currency configured." % String(name))
return
_currency_id = GameState.get_currency_id(currency)
_label.text = "%s:" % GameState.get_currency_name(_currency_id)
_icon.texture = GameState.get_currency_icon(_currency_id)
_debug_button.text = "+100 %s" % GameState.get_currency_name(_currency_id)
if game_state == null:
push_error("CurrencyTile '%s' missing game_state reference" % String(name))
return
GameState.currency_changed.connect(_on_currency_changed)
_on_currency_changed(_currency_id, GameState.get_currency_amount_by_id(_currency_id))
_currency_id = game_state.get_currency_id(currency)
_label.text = "%s:" % game_state.get_currency_name(_currency_id)
_icon.texture = game_state.get_currency_icon(_currency_id)
_debug_button.text = "+100 %s" % game_state.get_currency_name(_currency_id)
game_state.currency_changed.connect(_on_currency_changed)
_on_currency_changed(_currency_id, game_state.get_currency_amount_by_id(_currency_id))
func _on_currency_changed(changed_currency_id: StringName, amount: BigNumber) -> void:
if changed_currency_id != _currency_id:
@@ -33,4 +36,6 @@ func _on_currency_changed(changed_currency_id: StringName, amount: BigNumber) ->
func _on_debug_income_button_pressed() -> void:
if currency == null:
return
GameState.add_currency(currency, BigNumber.from_float(100))
if game_state == null:
return
game_state.add_currency(currency, BigNumber.from_float(100))

View File

@@ -1,9 +1,9 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://cg7os1rfknw05"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/currencies/food.tres" id="1_qy8mo"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_pac24"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="1_qy8mo"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_pac24"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_t5xb7"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/goals/gold_total_13k.tres" id="4_r8eth"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_r8eth"]
[resource]
script = ExtResource("3_t5xb7")

View File

@@ -1,9 +1,9 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://kamgujbqqhg3"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/currencies/food.tres" id="1_pdm0x"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_rnlmu"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="1_pdm0x"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_rnlmu"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_y6252"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/goals/gold_total_13k.tres" id="4_y6252"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_y6252"]
[resource]
script = ExtResource("3_y6252")

View File

@@ -1,9 +1,9 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://c3op2mqy02sow"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/currencies/food.tres" id="1_j3saa"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_hvfo8"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="1_j3saa"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_hvfo8"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_bwcdh"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/goals/gold_total_30.tres" id="4_f52il"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="4_f52il"]
[resource]
script = ExtResource("3_bwcdh")

View File

@@ -1,8 +1,8 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://dlq2ke6i2pfob"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="1_8h747"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="1_8h747"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="2_aejsu"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/goals/gold_total_30.tres" id="3_aejsu"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="3_aejsu"]
[resource]
script = ExtResource("2_aejsu")

View File

@@ -1,9 +1,9 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://bjc6qmvr7pe12"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="1_we7j4"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/currencies/worker.tres" id="2_s45cj"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="1_we7j4"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="2_s45cj"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_tgtcn"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/goals/gold_total_30.tres" id="4_kqmci"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="4_kqmci"]
[resource]
script = ExtResource("3_tgtcn")

View File

@@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="BuffCatalogue" format=3 uid="uid://bgmj3lep0wmtf"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_up80l"]
[ext_resource type="Resource" uid="uid://dlq2ke6i2pfob" path="res://docs/gyms/tiny_sword/buffs/gold_click_buff.tres" id="2_dtvfl"]
[ext_resource type="Script" uid="uid://ctc5yjlnvi0ok" path="res://core/buff_catalogue.gd" id="2_iuxc1"]
[ext_resource type="Resource" uid="uid://c3op2mqy02sow" path="res://docs/gyms/tiny_sword/buffs/gold_auto_flux_buff.tres" id="3_0bb41"]
[ext_resource type="Resource" uid="uid://bjc6qmvr7pe12" path="res://docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres" id="4_hnq3m"]
[ext_resource type="Resource" uid="uid://cg7os1rfknw05" path="res://docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres" id="5_mii30"]
[ext_resource type="Resource" uid="uid://kamgujbqqhg3" path="res://docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres" id="6_k5yi4"]
[resource]
script = ExtResource("2_iuxc1")
buffs = Array[ExtResource("1_up80l")]([ExtResource("2_dtvfl"), ExtResource("3_0bb41"), ExtResource("4_hnq3m"), ExtResource("5_mii30"), ExtResource("6_k5yi4")])
metadata/_custom_type_script = "uid://ctc5yjlnvi0ok"

View File

@@ -4,7 +4,7 @@
[ext_resource type="Texture2D" uid="uid://cry4arn00kgrt" path="res://docs/gyms/tiny_sword/buildings/castle/Castle.png" id="1_tgvch"]
[ext_resource type="PackedScene" uid="uid://dlidx2x0otpjg" path="res://core/prestige/prestige_panel.tscn" id="3_l7gct"]
[ext_resource type="Script" uid="uid://cpfmctd4xsfho" path="res://docs/gyms/tiny_sword/buildings/castle/test_buff.gd" id="4_l7gct"]
[ext_resource type="Resource" uid="uid://bjc6qmvr7pe12" path="res://docs/gyms/buffs/spawn_worker.tres" id="5_s3a5k"]
[ext_resource type="Resource" uid="uid://bjc6qmvr7pe12" path="res://docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres" id="5_s3a5k"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_tgvch"]
size = Vector2(312, 198)

View File

@@ -1,6 +1,7 @@
extends PanelContainer
@export var buff_data: GeneratorBuffData
@export var game_state: LevelGameState
@onready var _label: Label = $HBoxContainer/Label
@onready var _button: Button = $HBoxContainer/Button
@@ -19,9 +20,10 @@ func _ready() -> void:
_button.disabled = true
_button.pressed.connect(_on_button_pressed)
GameState.buff_level_changed.connect(_on_buff_level_changed)
GameState.buff_unlocked_changed.connect(_on_buff_unlocked_changed)
GameState.currency_changed.connect(_on_currency_changed)
if game_state:
game_state.buff_level_changed.connect(_on_buff_level_changed)
game_state.buff_unlocked_changed.connect(_on_buff_unlocked_changed)
game_state.currency_changed.connect(_on_currency_changed)
_update_ui()
@@ -35,9 +37,9 @@ func _on_button_pressed() -> void:
return
var cost: BigNumber = buff_data.get_cost_for_level(_current_level)
if GameState.spend_currency_by_id(cost_currency_id, cost):
if game_state and game_state.spend_currency_by_id(cost_currency_id, cost):
var next_level: int = _current_level + 1
GameState.set_buff_level(buff_data.id, next_level)
game_state.set_buff_level(buff_data.id, next_level)
_apply_buff_effect(buff_data, next_level)
_current_level = next_level
_update_ui()
@@ -54,15 +56,17 @@ func _apply_buff_effect(buff: GeneratorBuffData, level: int) -> void:
return
var purchased_amount: BigNumber = buff.get_resource_purchase_amount_for_level(level)
if purchased_amount.mantissa > 0.0:
GameState.add_currency_by_id(target_currency_id, purchased_amount)
if purchased_amount.mantissa > 0.0 and game_state:
game_state.add_currency_by_id(target_currency_id, purchased_amount)
func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName:
if buff.resource_target_currency == null:
return &""
return CurrencyDatabase.get_currency_id(buff.resource_target_currency)
if game_state == null:
return &""
return game_state.get_currency_id(buff.resource_target_currency)
func _on_buff_level_changed(buff_id: StringName, new_level: int) -> void:
@@ -82,7 +86,7 @@ func _update_ui() -> void:
if buff_data == null:
return
if not GameState.is_buff_unlocked(buff_data.id):
if game_state and not game_state.is_buff_unlocked(buff_data.id):
_button.disabled = true
_button.text = "Locked"
return
@@ -93,7 +97,7 @@ func _update_ui() -> void:
var cost_currency_id: StringName = _resolve_buff_cost_currency_id(buff_data)
if cost_currency_id != &"":
var cost: BigNumber = buff_data.get_cost_for_level(_current_level)
var current_amount: BigNumber = GameState.get_currency_amount_by_id(cost_currency_id)
var current_amount: BigNumber = game_state.get_currency_amount_by_id(cost_currency_id) if game_state else BigNumber.from_float(0.0)
can_buy = can_buy and (current_amount.is_greater_than(cost) or current_amount.is_equal_to(cost))
_button.disabled = not can_buy
@@ -109,4 +113,6 @@ func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName:
if buff.cost_currency == null:
return &""
return CurrencyDatabase.get_currency_id(buff.cost_currency)
if game_state == null:
return &""
return game_state.get_currency_id(buff.cost_currency)

View File

@@ -1,8 +1,8 @@
[gd_scene format=3 uid="uid://djedqovgngrx5"]
[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_rvsna"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/currencies/food.tres" id="2_djlc5"]
[ext_resource type="Resource" uid="uid://d08h51y0pnsnf" path="res://docs/gyms/generators/farm.tres" id="3_82rmq"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="2_djlc5"]
[ext_resource type="Resource" uid="uid://d08h51y0pnsnf" path="res://docs/gyms/tiny_sword/buildings/farm/farm_generator.tres" id="3_82rmq"]
[ext_resource type="Texture2D" uid="uid://dfdh8r03sj7qv" path="res://docs/gyms/tiny_sword/buildings/farm/House1.png" id="4_djlc5"]
[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://core/generator/generator_container.tscn" id="5_82rmq"]

View File

@@ -1,10 +1,10 @@
[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://d08h51y0pnsnf"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_cl7uf"]
[ext_resource type="Resource" uid="uid://cg7os1rfknw05" path="res://docs/gyms/buffs/farm_auto_flux.tres" id="2_bbypn"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/currencies/worker.tres" id="3_bbypn"]
[ext_resource type="Resource" uid="uid://cg7os1rfknw05" path="res://docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres" id="2_bbypn"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="3_bbypn"]
[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="4_0kvfm"]
[ext_resource type="Resource" uid="uid://cts0407h130d6" path="res://docs/gyms/goals/gold_total_1300.tres" id="5_qiy1b"]
[ext_resource type="Resource" uid="uid://cts0407h130d6" path="res://docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres" id="5_qiy1b"]
[resource]
script = ExtResource("4_0kvfm")

View File

@@ -1,8 +1,8 @@
[gd_scene format=3 uid="uid://cf01wvy3f17i"]
[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_7j1xc"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/currencies/wood.tres" id="2_23x2t"]
[ext_resource type="Resource" uid="uid://v6hjoa2vky5k" path="res://docs/gyms/generators/forestry.tres" id="3_pby1g"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="2_23x2t"]
[ext_resource type="Resource" uid="uid://v6hjoa2vky5k" path="res://docs/gyms/tiny_sword/buildings/forestry/forestry_generator.tres" id="3_pby1g"]
[ext_resource type="Texture2D" uid="uid://doxauaa3vn3lm" path="res://docs/gyms/tiny_sword/buildings/forestry/House3.png" id="4_5yp33"]
[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://core/generator/generator_container.tscn" id="5_rh3m6"]

View File

@@ -1,9 +1,9 @@
[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://v6hjoa2vky5k"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_mgk3v"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/currencies/worker.tres" id="2_fs80u"]
[ext_resource type="Resource" uid="uid://kamgujbqqhg3" path="res://docs/gyms/buffs/forestry_auto_flux.tres" id="2_mgk3v"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/goals/gold_total_13k.tres" id="4_mgk3v"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="2_fs80u"]
[ext_resource type="Resource" uid="uid://kamgujbqqhg3" path="res://docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres" id="2_mgk3v"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_mgk3v"]
[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="4_rij12"]
[resource]

View File

@@ -1,10 +1,10 @@
[gd_scene format=3 uid="uid://brgkkw2n3o1ox"]
[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_dgq4l"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_d2x03"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_d2x03"]
[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://core/generator/generator_container.tscn" id="2_lnbxc"]
[ext_resource type="Texture2D" uid="uid://bqf6yhtsnmjad" path="res://docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6.png" id="2_n0fgx"]
[ext_resource type="Resource" uid="uid://dliilvvjvksk1" path="res://docs/gyms/generators/gold_mine.tres" id="2_sbi6i"]
[ext_resource type="Resource" uid="uid://dliilvvjvksk1" path="res://docs/gyms/tiny_sword/buildings/gold_mine/gold_mine_generator.tres" id="2_sbi6i"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_sbi6i"]
size = Vector2(82, 84)

View File

@@ -1,9 +1,9 @@
[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://dliilvvjvksk1"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_1ni3y"]
[ext_resource type="Resource" uid="uid://dlq2ke6i2pfob" path="res://docs/gyms/buffs/gold_click_buff.tres" id="2_cnabh"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/currencies/worker.tres" id="2_j1lw5"]
[ext_resource type="Resource" uid="uid://c3op2mqy02sow" path="res://docs/gyms/buffs/gold_auto_flux.tres" id="3_baaj7"]
[ext_resource type="Resource" uid="uid://dlq2ke6i2pfob" path="res://docs/gyms/tiny_sword/buffs/gold_click_buff.tres" id="2_cnabh"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="2_j1lw5"]
[ext_resource type="Resource" uid="uid://c3op2mqy02sow" path="res://docs/gyms/tiny_sword/buffs/gold_auto_flux_buff.tres" id="3_baaj7"]
[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="3_cnabh"]
[resource]

View File

@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="CurrencyCatalogue" format=3 uid="uid://iqphilgfp2i7"]
[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_501l6"]
[ext_resource type="Script" uid="uid://621tus0uvbrd" path="res://core/currency/currency_catalogue.gd" id="2_hmju3"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_ucsji"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="3_7hx6u"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="4_c77gh"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="5_gyp05"]
[resource]
script = ExtResource("2_hmju3")
currencies = Array[ExtResource("1_501l6")]([ExtResource("2_ucsji"), ExtResource("3_7hx6u"), ExtResource("4_c77gh"), ExtResource("5_gyp05")])
metadata/_custom_type_script = "uid://621tus0uvbrd"

View File

@@ -1,8 +1,8 @@
[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://b6gb6vk0kgxnw"]
[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_68ddp"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_2b0j8"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/currencies/wood.tres" id="3_7tb0t"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_2b0j8"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="3_7tb0t"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="4_20y5g"]
[sub_resource type="Resource" id="Resource_tvl3d"]

View File

@@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://cts0407h130d6"]
[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_pahmu"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_oek0g"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_oek0g"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_3fgtm"]
[sub_resource type="Resource" id="Resource_tvl3d"]

View File

@@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://bo463s6jt0ep7"]
[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_hj5d1"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_w5qg8"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_w5qg8"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_daoxn"]
[sub_resource type="Resource" id="Resource_tvl3d"]

View File

@@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://ik7t0r3su633"]
[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_udubw"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="2_lde8k"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_lde8k"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_ilpcf"]
[sub_resource type="Resource" id="Resource_v4v16"]

View File

@@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="GoalCatalogue" format=3 uid="uid://cgt1mjir1v4br"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="1_6p6ue"]
[ext_resource type="Script" uid="uid://bfbp4mo8ys5p8" path="res://core/goal_catalogue.gd" id="2_84pbn"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="2_xvtf2"]
[ext_resource type="Resource" uid="uid://cts0407h130d6" path="res://docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres" id="3_wb626"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_3oaoj"]
[ext_resource type="Resource" uid="uid://b6gb6vk0kgxnw" path="res://docs/gyms/tiny_sword/goals/gold_350k_wood_20k_goal.tres" id="5_or88d"]
[resource]
script = ExtResource("2_84pbn")
goals = Array[ExtResource("1_6p6ue")]([ExtResource("2_xvtf2"), ExtResource("3_wb626"), ExtResource("4_3oaoj"), ExtResource("5_or88d")])

View File

@@ -1,19 +1,37 @@
[gd_scene format=3 uid="uid://dadeowsvaywpp"]
[ext_resource type="Script" uid="uid://cv2132o4hi7q3" path="res://core/level_game_state.gd" id="1_tma5j"]
[ext_resource type="PackedScene" uid="uid://brgkkw2n3o1ox" path="res://docs/gyms/tiny_sword/buildings/gold_mine/gold_mine.tscn" id="2_2j2oc"]
[ext_resource type="Resource" uid="uid://iqphilgfp2i7" path="res://docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres" id="2_dwrof"]
[ext_resource type="PackedScene" uid="uid://cl05bdri38mxf" path="res://docs/gyms/tiny_sword/buildings/castle/castle.tscn" id="3_5ru58"]
[ext_resource type="Resource" uid="uid://bgmj3lep0wmtf" path="res://docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres" id="3_wl838"]
[ext_resource type="Resource" uid="uid://cgt1mjir1v4br" path="res://docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres" id="4_x77df"]
[ext_resource type="Script" uid="uid://srkiu4qe8s2m" path="res://core/prestige/prestige_manager.gd" id="5_x77df"]
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="6_xnhlc"]
[ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="7_0cs5o"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/currencies/gold.tres" id="9_1363k"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="9_1363k"]
[ext_resource type="PackedScene" uid="uid://djedqovgngrx5" path="res://docs/gyms/tiny_sword/buildings/farm/farm.tscn" id="10_1lv5i"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/currencies/worker.tres" id="10_i1cck"]
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="10_i1cck"]
[ext_resource type="PackedScene" uid="uid://dirvi76rkoowf" path="res://goals_debug_ui.tscn" id="10_qifrv"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/currencies/food.tres" id="11_hskcg"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="11_hskcg"]
[ext_resource type="PackedScene" uid="uid://cf01wvy3f17i" path="res://docs/gyms/tiny_sword/buildings/forestry/forestry.tscn" id="11_pyqyw"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/currencies/wood.tres" id="12_l6a68"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="12_l6a68"]
[ext_resource type="PackedScene" uid="uid://rejxvjwybkll" path="res://sandbox/tiny_swords/Terrain/Resources/Wood/Trees/tree_1.tscn" id="14_0cs5o"]
[node name="TinySwords" type="Node" unique_id=498237642]
[node name="LevelGameState" type="Node" parent="." unique_id=1257082478]
script = ExtResource("1_tma5j")
currency_catalogue = ExtResource("2_dwrof")
buff_catalogue = ExtResource("3_wl838")
goal_catalogue = ExtResource("4_x77df")
save_file_path = "user://tiny_sword_save.json"
[node name="PrestigeManager" type="Node" parent="LevelGameState" unique_id=1019028144 node_paths=PackedStringArray("game_state")]
script = ExtResource("5_x77df")
config = ExtResource("6_xnhlc")
game_state = NodePath("..")
[node name="World" type="Node2D" parent="." unique_id=2118297724]
[node name="ForestProps" type="Node2D" parent="World" unique_id=649424542]
@@ -66,32 +84,36 @@ anchors_preset = 0
offset_right = 40.0
offset_bottom = 40.0
[node name="GoldCurrencyTile" parent="UI" unique_id=1440583137 instance=ExtResource("7_0cs5o")]
[node name="GoldCurrencyTile" parent="UI" unique_id=1440583137 node_paths=PackedStringArray("game_state") instance=ExtResource("7_0cs5o")]
layout_mode = 0
offset_right = 160.0
offset_bottom = 31.0
currency = ExtResource("9_1363k")
game_state = NodePath("../../LevelGameState")
[node name="WorkerCurrencyTile" parent="UI" unique_id=1164876942 instance=ExtResource("7_0cs5o")]
[node name="WorkerCurrencyTile" parent="UI" unique_id=1164876942 node_paths=PackedStringArray("game_state") instance=ExtResource("7_0cs5o")]
layout_mode = 0
offset_top = 32.0
offset_right = 160.0
offset_bottom = 63.0
currency = ExtResource("10_i1cck")
game_state = NodePath("../../LevelGameState")
[node name="FoodCurrencyTile" parent="UI" unique_id=1814936562 instance=ExtResource("7_0cs5o")]
[node name="FoodCurrencyTile" parent="UI" unique_id=1814936562 node_paths=PackedStringArray("game_state") instance=ExtResource("7_0cs5o")]
layout_mode = 0
offset_top = 63.0
offset_right = 160.0
offset_bottom = 94.0
currency = ExtResource("11_hskcg")
game_state = NodePath("../../LevelGameState")
[node name="WoodCurrencyTile" parent="UI" unique_id=338003163 instance=ExtResource("7_0cs5o")]
[node name="WoodCurrencyTile" parent="UI" unique_id=338003163 node_paths=PackedStringArray("game_state") instance=ExtResource("7_0cs5o")]
layout_mode = 0
offset_top = 94.0
offset_right = 160.0
offset_bottom = 125.0
currency = ExtResource("12_l6a68")
game_state = NodePath("../../LevelGameState")
[node name="GoalsDebugUI" parent="UI" unique_id=1494700185 instance=ExtResource("10_qifrv")]
layout_mode = 1

View File

@@ -25,6 +25,7 @@ class GoalRow extends RefCounted:
@onready var _summary_label: Label = $MarginContainer/VBoxContainer/SummaryLabel
@onready var _goals_list: VBoxContainer = $MarginContainer/VBoxContainer/ScrollContainer/GoalsList
@onready var game_state: LevelGameState = find_parent("LevelGameState")
var _loaded_goals: Array[GoalDefinition] = []
var _rows_by_goal_id: Dictionary = {}
@@ -36,9 +37,10 @@ func _ready() -> void:
_collect_known_generator_ids()
_load_goals()
_build_goal_rows()
GameState.currency_changed.connect(_on_currency_changed)
GameState.generator_state_changed.connect(_on_generator_state_changed)
GameState.goal_completed.connect(_on_goal_completed)
if game_state:
game_state.currency_changed.connect(_on_currency_changed)
game_state.generator_state_changed.connect(_on_generator_state_changed)
game_state.goal_completed.connect(_on_goal_completed)
_refresh_ui()
_refresh_ui.call_deferred()
@@ -74,7 +76,9 @@ func _load_goals() -> void:
_generators_by_id[String(generator_id)] = generator
var seen_goal_ids: Dictionary = {}
for goal in GameState.get_all_goals():
var all_goals_raw: Array = game_state.get_all_goals() if game_state else []
for goal_raw in all_goals_raw:
var goal: GoalData = goal_raw as GoalData
if goal == null or not goal.has_id():
continue
@@ -182,13 +186,17 @@ func _is_goal_met(goal: GoalDefinition) -> bool:
return false
if goal.goal == null:
return false
if game_state == null:
return false
return bool(goal.goal.is_met())
return bool(game_state.is_goal_met(goal.goal))
func _is_goal_completed(goal: GoalDefinition) -> bool:
if goal.goal == null or goal.goal.id.is_empty():
return false
return GameState.is_goal_completed(goal.goal.id)
if game_state == null:
return false
return game_state.is_goal_completed(goal.goal.id)
func _on_unlock_pressed(goal_id: StringName) -> void:
var row: GoalRow = _rows_by_goal_id.get(String(goal_id))
@@ -203,14 +211,17 @@ func _on_unlock_pressed(goal_id: StringName) -> void:
if not _is_goal_met(goal):
return
if game_state == null:
return
var generator: CurrencyGenerator = _get_generator_for_goal(goal)
if generator == null:
return
GameState.set_generator_unlocked(goal.target_generator_id, true)
GameState.set_generator_available(goal.target_generator_id, true)
game_state.set_generator_unlocked(goal.target_generator_id, true)
game_state.set_generator_available(goal.target_generator_id, true)
if goal.goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL:
GameState._complete_goal_manually(goal.goal.id)
game_state._complete_goal_manually(goal.goal.id)
print("[GoalsDebugUI] Unlocked goal '%s' -> %s" % [String(goal.goal.id), String(goal.target_generator_id)])
_refresh_ui()
@@ -220,7 +231,7 @@ func _can_resolve_target(target_generator_id: StringName) -> bool:
if target_key.is_empty():
return false
if _known_generator_ids.has(target_key) or GameState.generator_states.has(target_key):
if _known_generator_ids.has(target_key) or (game_state and game_state.generator_states.has(target_key)):
return true
if not _warned_unknown_target_ids.has(target_key):
@@ -292,16 +303,22 @@ func _get_requirements_text(goal: GoalDefinition) -> String:
return ""
if goal.goal.get_script() != GOAL_DATA_SCRIPT:
return ""
if game_state == null:
return ""
var parts: Array[String] = []
for requirement_resource in goal.goal.get_valid_requirements():
for requirement_resource in goal.goal.get_requirements():
if requirement_resource == null:
continue
if requirement_resource.get_script() != GOAL_REQUIREMENT_SCRIPT:
continue
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 currency: Currency = requirement_resource.get_currency()
if currency == null:
continue
var requirement_currency_id: StringName = game_state.get_currency_id(currency)
var total_amount: BigNumber = game_state.get_total_currency_acquired_by_id(requirement_currency_id)
var required_amount: BigNumber = requirement_resource.get_amount()
parts.append(
"%s %s / %s" % [
@@ -316,4 +333,6 @@ func _get_requirements_text(goal: GoalDefinition) -> String:
return " | ".join(parts)
func _currency_label(currency_id: StringName) -> String:
return GameState.get_currency_name(currency_id)
if game_state == null:
return "Unknown"
return game_state.get_currency_name(currency_id)

View File

@@ -16,13 +16,6 @@ run/main_scene="uid://dadeowsvaywpp"
config/features=PackedStringArray("4.6", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
BuffDatabase="*res://core/buff_database.gd"
CurrencyDatabase="*res://core/currency_database.gd"
GameState="*uid://d2j7tvlgxr2jp"
PrestigeManager="*res://core/prestige/prestige_manager.gd"
[display]
window/size/viewport_width=1920