434 lines
13 KiB
Markdown
434 lines
13 KiB
Markdown
# 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*
|