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