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