systems rework: buffs, prestige graph, research, modular architecture

Decouples core systems from centralized globals in favor of catalogue-based architecture and data-driven content.

   Key changes:
   - Prestige: node-based buff tech tree, graph panel, progress bar
   - Research: multi-worker system, per-generator research, xp generation
   - Ascension: meta-currency layer via multi-currency prestige resets
   - Buffs: refactored as generator-independent via catalogues
   - UI: currency panel, edge-scrolling camera + zoom, current-goal panel
   - Alchemy Tower: crafting building with recipe/cost system
   - Testing: 7 test suites (ascension, prestige, research, goals)
   - Content: migrated from hardcoded idles/ to gym format (tiny_sword)
This commit was merged in pull request #1.
This commit is contained in:
2026-05-07 20:36:21 +00:00
parent 2743fd314a
commit 81a4058b04
242 changed files with 11226 additions and 2242 deletions

488
core/research/README.md Normal file
View File

@@ -0,0 +1,488 @@
# 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_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 |
| `../generator/research_buff_calculator.gd` | Static utility for buff multiplier calculations |
## Architecture
```
LevelGameState (Node2D)
├── 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
└── (buff calculation delegated to ResearchBuffCalculator static methods)
```
## Data Flow
```
Generator Production (currency_per_cycle)
LevelGameState.add_research_xp()
1. base_xp = currency_produced * xp_per_currency_produced
2. buff_multiplier = ResearchBuffCalculator.apply_buffs(research, buffs)
3. actual_xp = base_xp * buff_multiplier
4. research_xp += actual_xp
5. Check level threshold → auto-level
6. 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`.
## ResearchBuffCalculator
Static utility class for calculating research XP buff multipliers.
### Location
`res://core/generator/research_buff_calculator.gd`
### Key Methods
```gdscript
static func apply_buffs(research: ResearchData, game_state: LevelGameState) -> float:
"""Calculate total buff multiplier for research XP gain. Returns multiplicative factor."""
var multiplier: float = 1.0
if research.associated_buff_id.is_empty():
return multiplier
var buff: GeneratorBuffData = game_state.get_buff(research.associated_buff_id)
if buff != null and game_state.is_buff_active(buff.id):
var buff_level: int = game_state.get_buff_level(buff.id)
multiplier *= ResearchBuffCalculator._calculate_buff_multiplier(buff, buff_level)
return multiplier
static func _calculate_buff_multiplier(buff: GeneratorBuffData, level: int) -> float:
"""Calculate buff effect multiplier at given level."""
return buff.get_effect_multiplier(level)
```
### Usage
Called from `LevelGameState.add_research_xp()`:
```gdscript
func add_research_xp(research_id: StringName, xp: BigNumber) -> void:
var research: ResearchData = research_catalogue.get_research_by_id(research_id)
if research == null:
return
# Apply buff multipliers
var buff_multiplier: float = ResearchBuffCalculator.apply_buffs(research, self)
var multiplier_bn: BigNumber = BigNumber.from_float(buff_multiplier)
var actual_xp: BigNumber = xp.multiply(multiplier_bn)
# Store XP and check for level-up
var old_level: int = get_research_level(research_id)
research_xp[research_id] = research_xp.get(research_id, BigNumber.new(0.0, 0)).add(actual_xp)
var new_level: int = 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)
research_xp_changed.emit(research_id, research_xp[research_id])
```
## LevelGameState Extensions
### State Variables
```gdscript
var research_xp: Dictionary = {} # {research_id: BigNumber}
var research_levels: Dictionary = {} # {research_id: level_int}
# research_tracker removed - buff calculation moved to static utility
```
### 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) with buff multipliers applied, auto-level if threshold reached"""
var research: ResearchData = research_catalogue.get_research_by_id(research_id)
if research == null:
return
var old_level: int = get_research_level(research_id)
var current_xp: BigNumber = get_research_xp(research_id)
# Apply buff multipliers via static utility
var buff_multiplier: float = ResearchBuffCalculator.apply_buffs(research, self)
var multiplier_bn: BigNumber = BigNumber.from_float(buff_multiplier)
var actual_xp: BigNumber = xp.multiply(multiplier_bn)
research_xp[research_id] = current_xp.add(actual_xp)
var new_level: int = 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)
research_xp_changed.emit(research_id, research_xp[research_id])
```
### 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) - buff multipliers applied in LevelGameState
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)
game_state.add_research_xp(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 |
| Buff calculation | Static utility (`ResearchBuffCalculator`) - no dedicated tracker node |
| 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. **CurrencyGenerator calls**`game_state.add_research_xp()` with base XP
3. **LevelGameState applies buffs**`ResearchBuffCalculator.apply_buffs()` calculates multiplier
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/generator/research_buff_calculator.gd` - Static buff calculation utility
- `core/research/research_catalogue.gd` - Research configuration resource

616
core/research/TODO.md Normal file
View File

@@ -0,0 +1,616 @@
# Research Feature Implementation Plan
## Overview
A production-based research system where:
- **Generator production → Research XP → Auto-level → Production multiplier**
- **Research buffs** (purchased with currency) increase XP gain multiplicatively
- **One research track per generator** with a single associated buff
- **Progress bar shows percentage** to next level
- **Buff display shows name and effect**
- **Research tracks start unlocked**
- **Resets on prestige** (levels and XP lost)
---
## System Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ LevelGameState │
│ - research_xp: Dictionary │
│ - research_levels: Dictionary │
│ - research_catalogue: ResearchCatalogue │
│ - add_research_xp() applies buffs via ResearchBuffCalculator │
└─────────────────────────────────────────────────────────────────┘
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CurrencyGen │ │ ResearchData │ │ ResearchPanel │
│ (Gold Mine) │──│ (Resource) │──│ (Always visible)│
│ - produces gold │ │ - XP config │ │ - Shows all │
│ - emits signal │ │ - Multiplier │ │ research │
└─────────────────┘ └─────────────────┘ │ - Progress bars │
└─────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ResearchBuffCalculator (static utility) │
│ - apply_buffs() calculates XP multiplier from active buffs │
└───────────────────────────────────────────────────────────────┘
```
---
## Data Flow
```
Generator Production (currency_per_cycle)
┌──────────────────────┐
│ CurrencyGen │
│ emits production_tick │
└──────────────────────┘
┌─────────────────────────────────────────┐
│ LevelGameState.add_research_xp() │
│ 1. base_xp = currency_produced * rate │
│ 2. buff_mult = ResearchBuffCalculator.apply_buffs()
│ 3. actual_xp = base_xp * buff_mult │
│ 4. research_xp += actual_xp │
│ 5. Check level threshold → auto-level │
│ 6. Emit research_level_up signal │
└─────────────────────────────────────────┘
┌──────────────────────┐
│ ResearchState │
│ {xp: 150.5, level: 3}│
└──────────────────────┘
┌──────────────────────┐
│ CurrencyGen │
│ get_research_multiplier() → 1.3 (30% bonus)
└──────────────────────┘
┌──────────────────────┐
│ Production *= 1.3 │
└──────────────────────┘
```
---
## Component Specifications
### 1. ResearchData (core/generator/research_data.gd)
**Purpose:** Resource defining research configuration for a generator
**Location:** `res://core/generator/research_data.gd`
**Key Properties:**
```gdscript
@export var id: StringName # Unique research identifier
@export var generator_id: StringName # Which generator this research 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 produced
@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_total_xp_for_level(level: int) -> float:
"""Total cumulative XP needed to reach this level from level 0"""
if level <= 0:
return 0.0
var total: float = 0.0
for l in range(1, level + 1):
total += get_xp_required_for_level(l)
return total
func get_level_for_xp(xp: float) -> int:
"""Calculate level from total accumulated XP"""
var level: int = 0
var cumulative_xp: float = 0.0
while true:
var next_xp: float = cumulative_xp + get_xp_required_for_level(level + 1)
if xp < next_xp:
break
level += 1
cumulative_xp = next_xp
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"""
if xp <= 0.0:
return 0.0
var current_level: int = get_level_for_xp(xp)
if current_level < 0:
return 0.0
var xp_at_current_level: float = get_total_xp_for_level(current_level)
var xp_needed_for_next: float = get_xp_required_for_level(current_level + 1)
var xp_in_current_level: float = xp - xp_at_current_level
return clampf(xp_in_current_level / xp_needed_for_next, 0.0, 1.0)
```
---
### 2. ResearchCatalogue (core/research/research_catalogue.gd)
**Purpose:** Resource holding all research configurations
**Location:** `res://core/research/research_catalogue.gd`
**Structure:**
```gdscript
class_name ResearchCatalogue
extends Resource
@export var research_entries: Array[ResearchData] = []
func get_research_by_id(research_id: StringName) -> ResearchData:
for entry in research_entries:
if entry.id == research_id:
return entry
return null
func get_research_by_generator_id(generator_id: StringName) -> ResearchData:
for entry in research_entries:
if entry.generator_id == generator_id:
return entry
return null
func get_all_research() -> Array[ResearchData]:
return research_entries.duplicate()
```
**Usage:** Referenced directly by LevelGameState via `@export var research_catalogue: ResearchCatalogue`
---
### 3. ResearchBuffCalculator (core/generator/research_buff_calculator.gd)
**Purpose:** Static utility class for calculating research XP buff multipliers
**Location:** `res://core/generator/research_buff_calculator.gd`
**Key Methods:**
```gdscript
static func apply_buffs(research: ResearchData, game_state: LevelGameState) -> float:
"""Calculate total buff multiplier for research XP gain. Returns multiplicative factor."""
var multiplier: float = 1.0
if research.associated_buff_id.is_empty():
return multiplier
var buff: GeneratorBuffData = game_state.get_buff(research.associated_buff_id)
if buff != null and game_state.is_buff_active(buff.id):
var buff_level: int = game_state.get_buff_level(buff.id)
multiplier *= _calculate_buff_multiplier(buff, buff_level)
return multiplier
static func _calculate_buff_multiplier(buff: GeneratorBuffData, level: int) -> float:
"""Calculate buff effect multiplier at given level."""
return buff.get_effect_multiplier(level)
```
**Usage:** Called from `LevelGameState.add_research_xp()` to apply buffs before storing XP
---
### 4. LevelGameState Extensions (core/level_game_state.gd)
**New State Variables:**
```gdscript
var research_xp: Dictionary = {} # {research_id: BigNumber}
var research_levels: Dictionary = {} # {research_id: level_int}
# research_tracker removed - buff calculation moved to static utility
```
**New 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)
```
**New Properties:**
```gdscript
@export var research_catalogue: ResearchCatalogue
```
**New Methods:**
```gdscript
func register_research(research_id: StringName) -> void:
if not research_xp.has(research_id):
research_xp[research_id] = BigNumber.new(0.0, 0)
if not research_levels.has(research_id):
research_levels[research_id] = 0
func get_research_xp(research_id: StringName) -> BigNumber:
return research_xp.get(research_id, BigNumber.new(0.0, 0))
func get_research_level(research_id: StringName) -> int:
return research_levels.get(research_id, 0)
func get_research_multiplier(research_id: StringName) -> float:
var level: int = get_research_level(research_id)
var research: ResearchData = _get_research_data(research_id)
if research == null:
return 1.0
return research.get_multiplier_for_level(level)
func add_research_xp(research_id: StringName, xp: BigNumber) -> void:
"""Add XP with buff multipliers applied, auto-level if threshold reached"""
var research: ResearchData = research_catalogue.get_research_by_id(research_id)
if research == null:
return
var old_level: int = get_research_level(research_id)
var current_xp: BigNumber = get_research_xp(research_id)
# Apply buff multipliers via static utility
var buff_multiplier: float = ResearchBuffCalculator.apply_buffs(research, self)
var multiplier_bn: BigNumber = BigNumber.from_float(buff_multiplier)
var actual_xp: BigNumber = xp.multiply(multiplier_bn)
research_xp[research_id] = current_xp.add(actual_xp)
var new_level: int = 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)
research_xp_changed.emit(research_id, research_xp[research_id])
func _get_research_data(research_id: StringName) -> ResearchData:
if research_catalogue == null:
return null
return research_catalogue.get_research_by_id(research_id)
```
**Save/Load Integration:**
```gdscript
const RESEARCH_XP_KEY = "research_xp"
const RESEARCH_LEVELS_KEY = "research_levels"
# In save_game():
var save_data = {
# ... existing fields ...
RESEARCH_XP_KEY: research_xp.duplicate(),
RESEARCH_LEVELS_KEY: research_levels.duplicate(),
}
# In load_game():
if parsed_data.has(RESEARCH_XP_KEY):
research_xp = parsed_data.get(RESEARCH_XP_KEY, {})
if parsed_data.has(RESEARCH_LEVELS_KEY):
research_levels = parsed_data.get(RESEARCH_LEVELS_KEY, {})
# In reset_for_prestige():
research_xp.clear()
research_levels.clear()
```
**Initialization in _ready():**
```gdscript
# Initialize research state from catalogue
if research_catalogue:
for research in research_catalogue.get_all_research():
register_research(research.id)
```
---
### 5. CurrencyGeneratorData Extensions (core/generator/currency_generator_data.gd)
**New Property:**
```gdscript
@export var research_data: ResearchData # Link to research configuration
```
---
### 6. CurrencyGenerator Extensions (core/generator/currency_generator.gd)
**In `_grant_cycle_income()`:**
```gdscript
func _grant_cycle_income(cycle_count: int) -> void:
if data == null or cycle_count <= 0:
return
var effective_run_multiplier: float = get_effective_auto_run_multiplier()
var per_cycle: float = data.production_per_cycle(owned, effective_run_multiplier, purchased_count)
if per_cycle <= 0.0:
return
var produced: BigNumber = BigNumber.from_float(per_cycle).multiply(BigNumber.from_float(float(cycle_count)))
_add_currency(produced)
production_tick.emit(produced, cycle_count, owned)
# Award research XP - buff multipliers applied in LevelGameState
if data.research_data != null:
var xp: float = data.research_data.xp_per_currency_produced * produced.to_float()
game_state.add_research_xp(data.research_data.id, BigNumber.from_float(xp))
```
**New Method:**
```gdscript
func get_research_multiplier() -> float:
if data == null or data.research_data == null:
return 1.0
if game_state == null:
return 1.0
return game_state.get_research_multiplier(data.research_data.id)
```
**Modify `get_effective_auto_run_multiplier()`:**
```gdscript
func get_effective_auto_run_multiplier() -> float:
return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_multiplier()
```
---
### 7. GeneratorBuffData Extensions (core/generator/generator_buff_data.gd)
**New Enum Value:**
```gdscript
enum BuffKind {
AUTO_PRODUCTION_MULTIPLIER,
MANUAL_CLICK_MULTIPLIER,
RESOURCE_PURCHASE,
RESEARCH_XP_MULTIPLIER # NEW: 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 (uses get_effect_multiplier)
```
**Note:** Buff names are configured in the `text` field of each `GeneratorBuffData` instance
---
### 8. ResearchRow (core/research/research_row.tscn + .gd)
**Location:**
- Script: `res://core/research/research_row.gd`
- Scene: `res://core/research/research_row.tscn`
**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")
```
**Script Methods:**
```gdscript
func setup(research: ResearchData) -> void:
"""Initialize row with research data"""
_research = research
_name_label.text = research.name
_icon.texture = research.icon
func update_display(level: int, progress: float, multiplier: float, xp_current: float, xp_needed: float) -> void:
"""Update UI elements"""
_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 buff != null and game_state.is_buff_active(buff.id):
var level: int = game_state.get_buff_level(buff.id)
var effect: float = buff.get_effect_multiplier(level) - 1.0
_buff_label.text = "%s: +%d%% XP" % [buff.text, int(effect * 100)]
```
---
### 9. ResearchPanel (core/research/research_panel.tscn + .gd)
**Location:**
- Script: `res://core/research/research_panel.gd`
- Scene: `res://core/research/research_panel.tscn`
**Layout:**
```
ResearchPanel (ScrollContainer)
├── VBoxContainer
│ ├── TitleLabel ("Research")
│ └── ResearchRows (VBoxContainer)
│ └── ResearchRow (for each research track)
```
**Script Methods:**
```gdscript
func _ready() -> void:
_game_state = find_parent("LevelGameState")
if _game_state == null:
return
_game_state.research_xp_changed.connect(_on_research_xp_changed)
_game_state.research_level_up.connect(_on_research_level_up)
_build_research_rows()
_refresh_all()
func _build_research_rows() -> void:
for child in _research_rows_container.get_children():
child.queue_free()
_research_rows.clear()
if _game_state.research_catalogue:
for research in _game_state.research_catalogue.get_all_research():
var row = RESEARCH_ROW_SCENE.instantiate()
_research_rows_container.add_child(row)
row.setup(research)
_research_rows[research.id] = row
func _refresh_all() -> void:
for row in _research_rows.values():
_refresh_row(row)
func _refresh_row(row) -> void:
var research_id: StringName = row.get_research_id()
var xp: float = _game_state.get_research_xp(research_id)
var level: int = _game_state.get_research_level(research_id)
var research: ResearchData = _game_state.research_catalogue.get_research_by_id(research_id)
if research != null:
var progress: float = research.get_xp_progress(xp)
var multiplier: float = research.get_multiplier_for_level(level)
var xp_current: float = xp - research.get_total_xp_for_level(level)
var xp_needed: float = research.get_xp_required_for_level(level + 1)
row.update_display(level, progress, multiplier, xp_current, xp_needed)
func _on_research_xp_changed(research_id: StringName, _new_xp: float) -> void:
if _research_rows.has(research_id):
_refresh_row(_research_rows[research_id])
func _on_research_level_up(research_id: StringName, _old_level: int, _new_level: int) -> void:
if _research_rows.has(research_id):
_refresh_row(_research_rows[research_id])
```
---
## Implementation Sequence
| Step | Task | Files | Priority | Status |
|------|------|-------|----------|--------|
| 1 | Create `ResearchData` resource class | `core/generator/research_data.gd` | High | ⬜ |
| 2 | Create `ResearchCatalogue` resource class | `core/research/research_catalogue.gd` | High | ⬜ |
| 3 | Create `ResearchBuffCalculator` static utility | `core/generator/research_buff_calculator.gd` | High | ✅ Done |
| 4 | Add research state to `LevelGameState` | `core/level_game_state.gd` | High | ✅ Done |
| 5 | Add `RESEARCH_XP_MULTIPLIER` buff kind | `core/generator/generator_buff_data.gd` | High | ⬜ |
| 6 | Add `research_data` export to `CurrencyGeneratorData` | `core/generator/currency_generator_data.gd` | High | ⬜ |
| 7 | Connect production to research XP in `CurrencyGenerator` | `core/generator/currency_generator.gd` | High | ✅ Done |
| 8 | Create `ResearchRow` scene & script | `core/research/research_row.tscn`, `.gd` | Medium | ⬜ |
| 9 | Create `ResearchPanel` scene & script | `core/research/research_panel.tscn`, `.gd` | Medium | ⬜ |
| 10 | Add save/load support for research state | `core/level_game_state.gd` | Medium | ⬜ |
| 11 | Add prestige reset for research | `core/level_game_state.gd` | Medium | ⬜ |
| 12 | Create sample `ResearchData` resource | `doc/gyms/tiny_sword/resources/` | Low | ⬜ |
| 13 | Test integration | Manual testing | Low | ⬜ |
---
## Design Decisions (Confirmed)
| Decision | Value |
|----------|-------|
| ResearchCatalogue reference | Direct `@export var research_catalogue: ResearchCatalogue` in LevelGameState |
| ResearchCatalogue location | `core/research/research_catalogue.gd` (resource, referenced directly) |
| Buff calculation | Static utility (`ResearchBuffCalculator`) - no dedicated tracker node |
| 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 |
| ResearchData location | `res://doc/gyms/tiny_sword/resources/` |
| ResearchPanel visibility | Always visible (like GeneratorPanel) |
---
## 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
```
---
## Notes
- **ResearchData instances** should be created as `.tres` resources in `doc/gyms/tiny_sword/resources/`
- **Associated buffs** should be created as `GeneratorBuffData` with `BuffKind.RESEARCH_XP_MULTIPLIER`
- **Buff names** are configured in the `text` field of each `GeneratorBuffData`
- **ResearchPanel** should be added as a child node in the main game scene (always visible)
- **Save format version** should be incremented to 5 when adding research support (BigNumber XP)
- **ResearchXPTracker removed** - buff calculation moved to static `ResearchBuffCalculator` utility
---
## Testing Checklist
- [ ] Generator produces currency → research XP increases
- [ ] Buff multipliers correctly applied to XP gain
- [ ] Auto-level triggers at correct XP thresholds
- [ ] Production multiplier updates after level-up
- [ ] ResearchPanel displays correct progress and level
- [ ] Save/load preserves research state
- [ ] Prestige resets research levels and XP
- [ ] Multiple research tracks work independently
- [ ] Buff purchase correctly affects XP gain
---
## Dependencies
- `CurrencyGenerator` must emit `production_tick` signal
- `GeneratorBuffData` must support `RESEARCH_XP_MULTIPLIER` kind
- `LevelGameState` must support buff tracking and signals
- Save format version must be updated

View File

@@ -0,0 +1,19 @@
class_name ResearchCatalogue
extends Resource
@export var research_entries: Array[ResearchData] = []
func get_research_by_id(research_id: StringName) -> ResearchData:
for entry in research_entries:
if entry.id == research_id:
return entry
return null
func get_research_by_generator_id(generator_id: StringName) -> ResearchData:
for entry in research_entries:
if entry.generator_id == generator_id:
return entry
return null
func get_all_research() -> Array[ResearchData]:
return research_entries.duplicate()

View File

@@ -0,0 +1 @@
uid://d2v6t6w2todfy

View File

@@ -0,0 +1,70 @@
class_name ResearchData
extends Resource
@export var id: StringName = &""
@export var generator_id: StringName = &""
@export var name: String = ""
@export_multiline var description: String = ""
@export var icon: Texture2D
## XP Configuration
@export var xp_per_currency_produced: float = 0.01
@export var base_xp_required: float = 100.0
@export var xp_growth_multiplier: float = 1.5
## Multiplier Configuration
@export var base_multiplier: float = 1.0
@export var multiplier_per_level: float = 0.1
## Associated Buff (single buff per research track)
@export var associated_buff_id: StringName = &""
## Worker Scaling Configuration
@export var worker_scaling_factor: float = 0.01
@export var min_workers_for_xp: int = 1
func get_xp_required_for_level(level: int) -> float:
return base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0)))
func get_xp_required_for_level_big(level: int) -> BigNumber:
var float_val: float = base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0)))
return BigNumber.from_float(float_val)
func get_total_xp_for_level(level: int) -> 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:
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:
return base_multiplier + (multiplier_per_level * float(maxi(level, 0)))
func get_xp_progress(xp: BigNumber) -> float:
if xp.mantissa == 0.0:
return 0.0
var current_level: int = get_level_for_xp(xp)
if current_level < 0:
return 0.0
var xp_at_current_level: BigNumber = get_total_xp_for_level(current_level)
var xp_needed_for_next: BigNumber = get_xp_required_for_level_big(current_level + 1)
var xp_in_current_level: BigNumber = xp.subtract(xp_at_current_level)
if xp_needed_for_next.mantissa == 0.0:
return 1.0
var ratio: float = (xp_in_current_level.mantissa / xp_needed_for_next.mantissa) * pow(10.0, float(xp_in_current_level.exponent - xp_needed_for_next.exponent))
return clampf(ratio, 0.0, 1.0)

View File

@@ -0,0 +1 @@
uid://m7baywrfnpn0

View File

@@ -0,0 +1,155 @@
class_name ResearchPanel
extends PanelContainer
const RESEARCH_ROW_SCENE: PackedScene = preload("res://core/research/research_row.tscn")
@onready var _title_label: Label = $ScrollContainer/VBoxContainer/TitleLabel
@onready var _research_rows_container: VBoxContainer = $ScrollContainer/VBoxContainer/ResearchRows
@onready var _buy_worker_button: Button = $ScrollContainer/VBoxContainer/HBoxContainer/BuyWorkerButton
var _game_state: LevelGameState
var _research_rows: Dictionary = {}
var _active_research_id: StringName = &""
func _ready() -> void:
_game_state = find_parent("LevelGameState")
if _game_state == null:
push_error("ResearchPanel: Could not find LevelGameState parent")
return
_game_state.research_xp_changed.connect(_on_research_xp_changed)
_game_state.research_level_up.connect(_on_research_level_up)
_game_state.currency_changed.connect(_on_currency_changed)
_game_state.research_workers_changed.connect(_on_research_workers_changed)
_build_research_rows()
_refresh_all()
_refresh_worker_button()
func _build_research_rows() -> void:
for child in _research_rows_container.get_children():
child.queue_free()
_research_rows.clear()
_active_research_id = &""
if _game_state.research_catalogue:
for research in _game_state.research_catalogue.get_all_research():
var row: ResearchRow = RESEARCH_ROW_SCENE.instantiate()
_research_rows_container.add_child(row)
row.setup(research)
row.active_changed.connect(_on_research_active_changed.bind(research.id))
_research_rows[research.id] = row
_load_active_research_state()
func _refresh_all() -> void:
for row in _research_rows.values():
_refresh_row(row)
func _refresh_row(row: ResearchRow) -> 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 research: ResearchData = _game_state.research_catalogue.get_research_by_id(research_id)
if research != null:
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)
func _on_research_xp_changed(research_id: StringName, _new_xp: BigNumber) -> void:
if _research_rows.has(research_id):
_refresh_row(_research_rows[research_id])
func _on_research_level_up(research_id: StringName, _old_level: int, _new_level: int) -> void:
if _research_rows.has(research_id):
_refresh_row(_research_rows[research_id])
func _on_research_active_changed(is_active: bool, research_id: StringName) -> void:
if is_active:
if _active_research_id != &"" and _active_research_id != research_id:
var existing_row: ResearchRow = _research_rows.get(_active_research_id)
if existing_row != null:
existing_row.set_active(false, false)
_active_research_id = research_id
_save_active_research_state()
else:
if _active_research_id == research_id:
_active_research_id = &""
_save_active_research_state()
func _load_active_research_state() -> void:
if _game_state == null:
return
var save_data: Dictionary = _game_state.get_external_save_data("research_active")
if save_data.has("active_research_id"):
var loaded_id: String = String(save_data.get("active_research_id", ""))
if not loaded_id.is_empty():
_active_research_id = StringName(loaded_id)
var row: ResearchRow = _research_rows.get(_active_research_id)
if row != null:
row.set_active(true, false)
func _save_active_research_state() -> void:
if _game_state == null:
return
var save_data: Dictionary = {}
if _active_research_id != &"":
save_data["active_research_id"] = String(_active_research_id)
_game_state.set_external_save_data("research_active", save_data)
func get_active_research_id() -> StringName:
return _active_research_id
func is_research_active(research_id: StringName) -> bool:
return _active_research_id == research_id
func activate_research(research_id: StringName) -> void:
if _research_rows.has(research_id):
var row: ResearchRow = _research_rows[research_id]
row.set_active(true)
func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void:
if currency_id == &"worker":
_refresh_worker_button()
func _on_research_workers_changed(_new_count: int) -> void:
_refresh_worker_button()
func _can_buy_worker() -> bool:
if _game_state == null:
return false
var worker_currency: BigNumber = _game_state.get_currency_amount_by_id(&"worker")
return worker_currency.mantissa >= 1.0
func _buy_worker() -> void:
if _game_state == null:
return
_game_state.assign_worker_to_research()
func _on_buy_worker_pressed() -> void:
_buy_worker()
_refresh_worker_button()
func _refresh_worker_button() -> void:
if _buy_worker_button == null:
return
if _game_state == null:
_buy_worker_button.disabled = true
return
var can_buy: bool = _can_buy_worker()
_buy_worker_button.disabled = not can_buy
var worker_currency: BigNumber = _game_state.get_currency_amount_by_id(&"worker")
var research_workers: int = _game_state.get_research_workers()
_buy_worker_button.text = "Assign Worker to Research (%d available, %d assigned)" % [int(worker_currency.mantissa), research_workers]

View File

@@ -0,0 +1 @@
uid://fdl7ftxw8w1e

View File

@@ -0,0 +1,34 @@
[gd_scene format=3 uid="uid://6d101h70mcx"]
[ext_resource type="Script" uid="uid://fdl7ftxw8w1e" path="res://core/research/research_panel.gd" id="1_script"]
[ext_resource type="PackedScene" uid="uid://ckr805xqy6h4w" path="res://core/research/worker_summary_label.tscn" id="3_worker"]
[node name="ResearchPanel" type="PanelContainer" unique_id=600021293]
custom_minimum_size = Vector2(400, 100)
offset_right = 400.0
offset_bottom = 100.0
script = ExtResource("1_script")
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=973132184]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1679714014]
layout_mode = 2
[node name="TitleLabel" type="Label" parent="ScrollContainer/VBoxContainer" unique_id=990297493]
layout_mode = 2
text = "Research"
[node name="HBoxContainer" type="HBoxContainer" parent="ScrollContainer/VBoxContainer" unique_id=1419318708]
layout_mode = 2
[node name="WorkerSummaryLabel" parent="ScrollContainer/VBoxContainer/HBoxContainer" unique_id=13607753 instance=ExtResource("3_worker")]
[node name="BuyWorkerButton" type="Button" parent="ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1083899286]
layout_mode = 2
text = "Invest Worker (0 available)"
[node name="ResearchRows" type="VBoxContainer" parent="ScrollContainer/VBoxContainer" unique_id=2066050169]
layout_mode = 2
[connection signal="pressed" from="ScrollContainer/VBoxContainer/HBoxContainer/BuyWorkerButton" to="." method="_buy_worker"]

View File

@@ -0,0 +1,98 @@
class_name ResearchRow
extends HBoxContainer
signal active_changed(is_active: bool)
@onready var _name_label: Label = $VBoxContainer/NameLabel
@onready var _level_label: Label = $VBoxContainer/HBoxContainer/LevelLabel
@onready var _progress_bar: ProgressBar = $VBoxContainer/HBoxContainer/ProgressBar
@onready var _multiplier_label: Label = $VBoxContainer/HBoxContainer/MultiplierLabel
@onready var _buff_label: Label = $VBoxContainer/HBoxContainer/BuffLabel
@onready var _check_button: CheckButton = $CheckButton
var _research: ResearchData
var _game_state: LevelGameState
var _is_active: bool = false
func _ready() -> void:
_game_state = find_parent("LevelGameState")
if _game_state == null:
push_error("ResearchRow: Could not find LevelGameState parent")
return
_game_state.research_xp_changed.connect(_on_research_xp_changed)
_game_state.research_level_up.connect(_on_research_level_up)
_check_button.toggled.connect(_on_check_button_toggled)
func setup(research: ResearchData) -> void:
_research = research
if _name_label:
_name_label.text = research.name
func get_research_id() -> StringName:
if _research == null:
return &""
return _research.id
func update_display(level: int, progress: float, multiplier: float, xp_current: BigNumber, xp_needed: BigNumber) -> void:
if _level_label:
_level_label.text = "Level %d" % level
if _progress_bar:
_progress_bar.value = progress * 100
if _multiplier_label:
_multiplier_label.text = "+%d%%" % int((multiplier - 1.0) * 100)
if _buff_label and _research != null:
if not _research.associated_buff_id.is_empty():
var buff: GeneratorBuffData = _game_state.get_buff(_research.associated_buff_id)
if buff != null and _game_state.is_buff_active(buff.id):
var buff_level: int = _game_state.get_buff_level(buff.id)
var effect: float = buff.get_effect_multiplier(buff_level) - 1.0
_buff_label.text = "%s: +%d%% XP" % [buff.text, int(effect * 100)]
else:
_buff_label.text = ""
else:
_buff_label.text = ""
func _on_research_xp_changed(research_id: StringName, _new_xp: BigNumber) -> void:
if _research != null and research_id == _research.id:
_update_from_state()
func _on_research_level_up(research_id: StringName, _old_level: int, _new_level: int) -> void:
if _research != null and research_id == _research.id:
_update_from_state()
func _update_from_state() -> void:
if _research == null or _game_state == null:
return
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)
update_display(level, progress, multiplier, xp_current, xp_needed)
func _on_check_button_toggled(toggled_on: bool) -> void:
if toggled_on != _is_active:
_is_active = toggled_on
active_changed.emit(_is_active)
func set_active(is_active: bool, emit_signal: bool = true) -> void:
_is_active = is_active
_check_button.button_pressed = is_active
if emit_signal:
active_changed.emit(is_active)
func is_research_active() -> bool:
return _is_active
func get_research_id_public() -> StringName:
if _research == null:
return &""
return _research.id

View File

@@ -0,0 +1 @@
uid://gk3mr3k5yvp7

View File

@@ -0,0 +1,42 @@
[gd_scene format=3 uid="uid://bpo8pl5tipav"]
[ext_resource type="Script" uid="uid://gk3mr3k5yvp7" path="res://core/research/research_row.gd" id="1_script"]
[node name="ResearchRow" type="HBoxContainer" unique_id=810714290]
mouse_filter = 2
script = ExtResource("1_script")
[node name="Icon" type="TextureRect" parent="." unique_id=1376308659]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=468422769]
layout_mode = 2
[node name="NameLabel" type="Label" parent="VBoxContainer" unique_id=1742937834]
layout_mode = 2
text = "Research Name"
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" unique_id=819270678]
layout_mode = 2
alignment = 2
[node name="LevelLabel" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=1108328348]
layout_mode = 2
text = "Level 0"
[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/HBoxContainer" unique_id=1855503166]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
[node name="MultiplierLabel" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=135310321]
layout_mode = 2
text = "+0%"
[node name="BuffLabel" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=2124734446]
layout_mode = 2
[node name="CheckButton" type="CheckButton" parent="." unique_id=3456789012]
layout_mode = 2
text = "Active"

View File

@@ -0,0 +1,27 @@
class_name WorkerSummaryLabel
extends Label
@onready var _game_state: LevelGameState = find_parent("LevelGameState")
signal research_workers_changed(new_count: int)
func _ready() -> void:
if _game_state == null:
push_error("WorkerSummaryLabel: Could not find LevelGameState parent")
return
_game_state.currency_changed.connect(_on_currency_changed)
_game_state.research_workers_changed.connect(_on_research_workers_changed)
_update_worker_display()
func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void:
if currency_id == "worker":
_update_worker_display()
func _on_research_workers_changed(new_count: int) -> void:
_update_worker_display()
func _update_worker_display() -> void:
var worker_currency: int = int(_game_state.get_currency_amount_by_id("worker").mantissa)
var research_workers: int = _game_state.get_research_workers()
text = "Workers: %d available, %d assigned" % [worker_currency, research_workers]

View File

@@ -0,0 +1 @@
uid://brvoy607b3nft

View File

@@ -0,0 +1,8 @@
[gd_scene format=3 uid="uid://ckr8z5xqy6h4w"]
[ext_resource type="Script" path="res://core/research/worker_summary_label.gd" id="1_script"]
[node name="WorkerSummaryLabel" type="Label"]
layout_mode = 2
script = ExtResource("1_script")
text = "Workers: 0"