# Research Module Documentation ## Overview The `research/` subfolder implements a production-based research system where generator output earns research XP, automatically leveling up to provide production multipliers. Research tracks are tied to specific generators and can be enhanced with purchasable buffs that increase XP gain. ## Files | File | Purpose | |------|---------| | `research_xp_tracker.gd` | Accumulates XP from production and handles auto-leveling (Node) | | `research_catalogue.gd` | Resource holding all research configurations | | `research_panel.gd` | UI panel showing all research tracks | | `research_row.gd` | UI row component for individual research display | ## Architecture ``` LevelGameState (Node2D) ├── ResearchXPTracker (child node) │ ├── Receives production_tick signals │ ├── Applies buff multipliers │ ├── Awards XP to research tracks │ └── Auto-levels when thresholds reached ├── research_xp: Dictionary ├── research_levels: Dictionary └── research_catalogue: ResearchCatalogue └── ResearchPanel (UI, always visible) ├── ResearchRow (for each research track) │ ├── Name & icon │ ├── Level display │ ├── Progress bar (percentage) │ ├── Multiplier indicator │ └── Active buff display ``` ## Data Flow ``` Generator Production (currency_per_cycle) │ ▼ ResearchXPTracker 1. base_xp = currency_produced * xp_per_currency_produced 2. actual_xp = base_xp * buff_multiplier 3. research_xp += actual_xp 4. Check level threshold → auto-level 5. Emit research_level_up signal │ ▼ ResearchState {xp: 150.5, level: 3} │ ▼ CurrencyGenerator.get_research_multiplier() → 1.3 (30% bonus) │ ▼ Production *= 1.3 ``` ## ResearchData Resource class defining research configuration for a generator (defined in `core/generator/research_data.gd`). ### Key Properties ```gdscript @export var id: StringName # Unique research identifier @export var generator_id: StringName # Which generator this affects @export var name: String # Display name @export_multiline var description: String = "" # Description text @export var icon: Texture2D # Icon for UI # XP Configuration @export var xp_per_currency_produced: float = 0.01 # XP per 1 currency unit @export var base_xp_required: float = 100.0 # XP needed for level 1 @export var xp_growth_multiplier: float = 1.5 # XP requirement growth per level # Multiplier Configuration @export var base_multiplier: float = 1.0 # Production multiplier at level 0 @export var multiplier_per_level: float = 0.1 # +10% production per level # Associated Buff (single buff per research track) @export var associated_buff_id: StringName # Buff that increases XP gain ``` ### Key Methods ```gdscript func get_xp_required_for_level(level: int) -> float: """XP needed to reach this level from previous""" return base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0))) func get_xp_required_for_level_big(level: int) -> BigNumber: """XP needed to reach this level from previous (BigNumber)""" return BigNumber.from_float(base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0)))) func get_total_xp_for_level(level: int) -> BigNumber: """Total cumulative XP needed to reach this level from level 0 (BigNumber)""" if level <= 0: return BigNumber.new(0.0, 0) var total: BigNumber = BigNumber.new(0.0, 0) for l in range(1, level + 1): total = total.add(get_xp_required_for_level_big(l)) return total func get_level_for_xp(xp: BigNumber) -> int: """Calculate level from total accumulated BigNumber XP (loop-based)""" if xp.mantissa == 0.0: return 0 var level: int = 0 while true: var next_xp: BigNumber = get_total_xp_for_level(level + 1) if xp.compare_to(next_xp) < 0: break level += 1 return level func get_multiplier_for_level(level: int) -> float: """Production multiplier at given level""" return base_multiplier + (multiplier_per_level * float(maxi(level, 0))) func get_xp_progress(xp: float) -> float: """Returns 0.0-1.0 progress to next level""" ``` ## ResearchCatalogue Resource class holding all research configurations. ### Structure ```gdscript class_name ResearchCatalogue extends Resource @export var research_entries: Array[ResearchData] = [] func get_research_by_id(research_id: StringName) -> ResearchData func get_research_by_generator_id(generator_id: StringName) -> ResearchData func get_all_research() -> Array[ResearchData] ``` ### Usage Referenced directly by `LevelGameState` via `@export var research_catalogue: ResearchCatalogue`. ## ResearchXPTracker Child node of `LevelGameState` that accumulates XP from production and applies buff multipliers. ### Signals ```gdscript signal research_xp_changed(research_id: StringName, new_xp: float) signal research_level_up(research_id: StringName, old_level: int, new_level: int) ``` ### Key Methods ```gdscript func add_xp_with_buffs(research_id: StringName, base_xp: BigNumber) -> BigNumber: """Apply buff multipliers and award XP. Returns actual XP after buffs (BigNumber).""" var buff_multiplier: float = game_state.get_research_xp_multiplier(research_id) var multiplier_bn: BigNumber = BigNumber.from_float(buff_multiplier) var actual_xp: BigNumber = base_xp.multiply(multiplier_bn) game_state.add_research_xp(research_id, actual_xp) return actual_xp func _on_generator_produced(generator_id: StringName, amount: BigNumber) -> void: var research: ResearchData = _get_research_for_generator(generator_id) var amount_float: float = amount.mantissa * pow(10.0, float(amount.exponent)) var base_xp: BigNumber = BigNumber.from_float(research.xp_per_currency_produced * amount_float) add_xp_with_buffs(research.id, base_xp) ``` ### Connection Connected to `LevelGameState` via `generator_produced` signal. ## LevelGameState Extensions ### State Variables ```gdscript var research_xp: Dictionary = {} # {research_id: BigNumber} var research_levels: Dictionary = {} # {research_id: level_int} var research_tracker: ResearchXPTracker ``` ### Signals ```gdscript signal research_xp_changed(research_id: StringName, new_xp: BigNumber) signal research_level_up(research_id: StringName, old_level: int, new_level: int) ``` ### Key Methods ```gdscript func register_research(research_id: StringName) -> void: """Initialize research state from catalogue (BigNumber XP)""" if not research_xp.has(research_id): research_xp[research_id] = BigNumber.new(0.0, 0) func get_research_xp(research_id: StringName) -> BigNumber func get_research_level(research_id: StringName) -> int func get_research_multiplier(research_id: StringName) -> float: """Returns production multiplier from research level""" var level: int = get_research_level(research_id) return research.get_multiplier_for_level(level) func add_research_xp(research_id: StringName, xp: BigNumber) -> void: """Add XP (BigNumber) and auto-level if threshold reached""" var current_xp: BigNumber = get_research_xp(research_id) research_xp[research_id] = current_xp.add(xp) new_level = research.get_level_for_xp(research_xp[research_id]) if new_level > old_level: research_levels[research_id] = new_level research_level_up.emit(research_id, old_level, new_level) func get_research_xp_multiplier(research_id: StringName) -> float: """Returns multiplicative buff for research XP gain""" var multiplier: float = 1.0 var buff: GeneratorBuffData = get_buff(research.associated_buff_id) if is_buff_active(buff.id): multiplier *= buff.get_effect_multiplier(get_buff_level(buff.id)) return multiplier ``` ### Save/Load Integration ```gdscript const RESEARCH_XP_KEY = "research_xp" const RESEARCH_LEVELS_KEY = "research_levels" const CURRENT_SAVE_FORMAT_VERSION = 5 # Bumped from 4 to support BigNumber # In save_game(): var save_data = { # ... existing fields ... RESEARCH_XP_KEY: _serialize_research_xp(), # Serialize BigNumber as {m, e} RESEARCH_LEVELS_KEY: research_levels.duplicate(), } func _serialize_research_xp() -> Dictionary: var result: Dictionary = {} for research_id in research_xp.keys(): var xp: BigNumber = research_xp[research_id] result[research_id] = xp.serialize() return result # In load_game(): research_xp = _deserialize_research_xp(parsed_data.get(RESEARCH_XP_KEY, {})) research_levels = parsed_data.get(RESEARCH_LEVELS_KEY, {}) func _deserialize_research_xp(raw: Variant) -> Dictionary: var result: Dictionary = {} if raw is Dictionary: for research_id in raw.keys(): var serialized: Variant = raw[research_id] if serialized is Dictionary: result[research_id] = BigNumber.deserialize(serialized) return result # In reset_for_prestige(): research_xp.clear() research_levels.clear() ``` ## CurrencyGenerator Extensions ### Production Tick Integration ```gdscript func _grant_cycle_income(cycle_count: int) -> void: var produced: BigNumber = ... _add_currency(produced) production_tick.emit(produced, cycle_count, owned) # Award research XP (BigNumber) if data.research_data != null: var amount_float: float = produced.mantissa * pow(10.0, float(produced.exponent)) var xp: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float) research_tracker.add_xp_with_buffs(data.research_data.id, xp) ``` ### Research Multiplier ```gdscript func get_research_multiplier() -> float: if data.research_data == null: return 1.0 return game_state.get_research_multiplier(data.research_data.id) func get_effective_auto_run_multiplier() -> float: return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_multiplier() ``` ## GeneratorBuffData Extensions ### New Buff Kind ```gdscript enum BuffKind { AUTO_PRODUCTION_MULTIPLIER, MANUAL_CLICK_MULTIPLIER, RESOURCE_PURCHASE, RESEARCH_XP_MULTIPLIER # Increases research XP gain } ``` ### New Properties (for RESEARCH_XP_MULTIPLIER buffs) ```gdscript @export var research_target_id: StringName # Which research track this buff affects @export var xp_multiplier_per_level: float = 0.1 # +10% XP per level ``` ## ResearchPanel UI component showing all research tracks (always visible, like GeneratorPanel). ### Layout ``` ResearchPanel (ScrollContainer) └── VBoxContainer ├── TitleLabel ("Research") └── ResearchRows (VBoxContainer) └── ResearchRow (for each research track) ``` ### Key Methods ```gdscript func _build_research_rows() -> void: for research in research_catalogue.get_all_research(): var row = RESEARCH_ROW_SCENE.instantiate() row.setup(research) _research_rows[research.id] = row func _refresh_row(row) -> void: var research_id: StringName = row.get_research_id() var xp: BigNumber = game_state.get_research_xp(research_id) var level: int = game_state.get_research_level(research_id) var progress: float = research.get_xp_progress(xp) var multiplier: float = research.get_multiplier_for_level(level) var xp_current: BigNumber = xp.subtract(research.get_total_xp_for_level(level)) var xp_needed: BigNumber = research.get_xp_required_for_level_big(level + 1) row.update_display(level, progress, multiplier, xp_current, xp_needed) ``` ### Signals Connected - `research_xp_changed` - Refresh progress bars - `research_level_up` - Refresh level and multiplier displays ## ResearchRow UI row component for individual research display. ### UI Layout ``` ResearchRow (HBoxContainer) ├── Icon (TextureRect) ├── VBoxContainer │ ├── NameLabel ("Gold Mine Research") │ └── LevelLabel ("Level 3") ├── ProgressBar (with percentage label "75%") ├── MultiplierLabel ("+30%") └── BuffLabel ("Mining Expert: +20% XP") ``` ### Key Methods ```gdscript func setup(research: ResearchData) -> void: _name_label.text = research.name _icon.texture = research.icon func update_display(level: int, progress: float, multiplier: float, xp_current: BigNumber, xp_needed: BigNumber) -> void: _level_label.text = "Level %d" % level _progress_bar.value = progress * 100 _progress_label.text = "%d%%" % int(progress * 100) _multiplier_label.text = "+%d%%" % int((multiplier - 1.0) * 100) # Update buff label if buff is active if _research.associated_buff_id != &"": var buff: GeneratorBuffData = game_state.get_buff(_research.associated_buff_id) if is_buff_active(buff.id): _buff_label.text = "%s: +%d%% XP" % [buff.text, int(effect * 100)] ``` ## Formulas ### XP Calculation ``` base_xp = currency_produced * xp_per_currency_produced actual_xp = base_xp * buff_multiplier ``` ### Level Progression ``` xp_required_for_level(n) = base_xp_required * (xp_growth_multiplier ^ (n - 1)) total_xp_for_level(n) = sum(xp_required_for_level(i) for i in 1..n) level = get_level_for_xp(total_xp) ``` ### Production Multiplier ``` multiplier = base_multiplier + (multiplier_per_level * level) effective_production = base_production * multiplier ``` ### Buff XP Multiplier ``` xp_multiplier = 1.0 for each active buff targeting this research: xp_multiplier *= buff.get_effect_multiplier(level) actual_xp = base_xp * xp_multiplier ``` ## Design Decisions | Decision | Value | |----------|-------| | ResearchCatalogue reference | Direct `@export var research_catalogue: ResearchCatalogue` in LevelGameState | | ResearchXPTracker location | Child of `LevelGameState` (receives signals via connection) | | Progress bar display | Percentage format (e.g., "75%") | | Buff display | Shows buff name and effect (e.g., "Mining Expert: +20% XP") | | Research unlock | Starts unlocked | | XP formula | Production-based: `XP = currency_produced * xp_per_currency_produced` | | Level-up | Auto-level when XP threshold reached | | Buffs | Single buff per research track, multiplicative XP bonus | | Prestige | Research levels and XP reset to 0 | | ResearchPanel visibility | Always visible (like GeneratorPanel) | ## System Behavior 1. **Generator produces currency** → `production_tick` signal emitted 2. **ResearchXPTracker receives signal** → calculates base XP from production amount 3. **Buff multipliers applied** → checks for active RESEARCH_XP_MULTIPLIER buffs 4. **XP awarded** → stored in `research_xp` dictionary 5. **Level check** → if XP exceeds threshold, auto-level and emit `research_level_up` 6. **Multiplier applied** → `get_research_multiplier()` returns updated production bonus 7. **UI refresh** → `ResearchPanel` updates progress bars and level displays ## Prestige Integration On prestige reset: 1. `research_xp.clear()` - All XP lost 2. `research_levels.clear()` - All levels reset to 0 3. Buff purchases persist (separate state) 4. Research tracks remain unlocked ## See Also - `core/level_game_state.gd` - Research state storage and management - `core/generator/currency_generator_data.gd` - Links to ResearchData - `core/generator/generator_buff_data.gd` - RESEARCH_XP_MULTIPLIER buff type - `core/research/research_catalogue.gd` - Research configuration resource