# 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*