Files
idle/REFACTORING.md
2026-03-29 15:24:13 +02:00

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*