460 lines
15 KiB
Markdown
460 lines
15 KiB
Markdown
# 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
|