Decouple everything from globals
This commit is contained in:
@@ -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
325
PLANNING.md
Normal 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
|
||||
433
REFACTORING.md
433
REFACTORING.md
@@ -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*
|
||||
@@ -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
24
core/buff_catalogue.gd
Normal 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
|
||||
1
core/buff_catalogue.gd.uid
Normal file
1
core/buff_catalogue.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctc5yjlnvi0ok
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://cjm87u4xh0718
|
||||
17
core/currency/currency_catalogue.gd
Normal file
17
core/currency/currency_catalogue.gd
Normal 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
|
||||
1
core/currency/currency_catalogue.gd.uid
Normal file
1
core/currency/currency_catalogue.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://621tus0uvbrd
|
||||
@@ -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()
|
||||
@@ -1 +0,0 @@
|
||||
uid://dmv83fnq26xao
|
||||
@@ -1 +0,0 @@
|
||||
uid://d2j7tvlgxr2jp
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
17
core/goal_catalogue.gd
Normal 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
|
||||
1
core/goal_catalogue.gd.uid
Normal file
1
core/goal_catalogue.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bfbp4mo8ys5p8
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,7 +644,8 @@ func _collect_currency_ids_for_save() -> Array[StringName]:
|
||||
continue
|
||||
ids.append(normalized_currency_id)
|
||||
|
||||
for known_currency_id in CurrencyDatabase.get_known_currency_ids():
|
||||
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
|
||||
@@ -633,13 +654,138 @@ func _collect_currency_ids_for_save() -> Array[StringName]:
|
||||
return ids
|
||||
|
||||
func _initialize_currency_maps() -> void:
|
||||
for currency_id in CurrencyDatabase.get_known_currency_ids():
|
||||
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
|
||||
|
||||
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()
|
||||
if normalized.is_empty():
|
||||
@@ -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()
|
||||
1
core/level_game_state.gd.uid
Normal file
1
core/level_game_state.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cv2132o4hi7q3
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
14
docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres
Normal file
14
docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres
Normal 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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
13
docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres
Normal file
13
docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres
Normal 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"
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
12
docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres
Normal file
12
docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres
Normal 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")])
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user