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

13 KiB

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:

@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:

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:

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:

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):

{
  "save_format_version": 2,
  "currencies": { ... },
  "generator_states": { ... },
  "buff_definitions": { ... },
  "buff_levels": { ... },
  "buff_unlocked": { ... },
  "buff_active": { ... },
  "last_save_time": 1234567890
}

Add serialization helpers:

func _serialize_buff_definitions() -> Dictionary
func _serialize_buff_data(buff: GeneratorBuffData) -> Dictionary
func _deserialize_buff_definitions(raw: Variant) -> void

Version check in load_game():

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:

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

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:

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

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):

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):

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):

{
  "currencies": { ... },
  "generator_states": { ... },
  "generator_buff_levels": { "farm": { "farm_flux": 5 } },
  "generator_buff_unlocked": { "farm": { "farm_flux": true } }
}

Version 2 (new):

{
  "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