Files
idle/core/research/README.md

15 KiB

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

@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

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

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

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

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

var research_xp: Dictionary = {}        # {research_id: BigNumber}
var research_levels: Dictionary = {}    # {research_id: level_int}
var research_tracker: ResearchXPTracker

Signals

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

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

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

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

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

enum BuffKind {
	AUTO_PRODUCTION_MULTIPLIER,
	MANUAL_CLICK_MULTIPLIER,
	RESOURCE_PURCHASE,
	RESEARCH_XP_MULTIPLIER  # Increases research XP gain
}

New Properties (for RESEARCH_XP_MULTIPLIER buffs)

@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

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

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 currencyproduction_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 appliedget_research_multiplier() returns updated production bonus
  7. UI refreshResearchPanel 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