buff #1
@@ -1,5 +1,6 @@
|
||||
# AGENTS.md
|
||||
Godot 4.6 idle-game prototype; code is GDScript + `.tscn` scenes + JSON data at repo root.
|
||||
Godot 4.6 idle-game; code is GDScript + `.tscn` scenes.
|
||||
|
||||
## Build, Lint, And Test Commands
|
||||
Use `GODOT_BIN` for your local editor binary (e.g. `godot`, `godot4`, or full path).
|
||||
Run project: `"$GODOT_BIN" --path .`
|
||||
@@ -7,12 +8,14 @@ Headless smoke/lint parse: `"$GODOT_BIN" --headless --path . --quit`
|
||||
Script syntax check (single script): `"$GODOT_BIN" --headless --check-only --script res://big_number.gd`
|
||||
Tests: no test framework is currently committed (`tests/` and `addons/gut` are absent).
|
||||
Single-test command: N/A right now; if GUT is added, use `"$GODOT_BIN" --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/<file>.gd`.
|
||||
|
||||
## Architecture And Structure
|
||||
`project.godot` defines autoload singletons: `GameState` (runtime currency state + save/load) and `CurrencyGeneratorDatabase` (loads `generator_data.json`).
|
||||
`big_number.gd` (`class_name BigNumber`) is the internal numeric API for huge-value math, comparisons, formatting, and serialization.
|
||||
`big_number_museum.tscn` + `big_number_museum.gd` is the current main playable scene/prototype UI.
|
||||
`currency_generator.gd`, `currency_label.gd`, and `big_number_progress_bar.gd` are UI/gameplay adapters that react to `GameState` signals.
|
||||
Persistence is local JSON only: `user://idle_save.json`; there is no external DB/server/backend.
|
||||
|
||||
## Code Style And Conventions
|
||||
Prefer typed GDScript (`var x: Type`, `func f() -> void`) and keep shared models/components as `class_name` scripts.
|
||||
Naming: `snake_case` for vars/functions/signals, `PascalCase` for classes, `UPPER_SNAKE_CASE` for constants.
|
||||
@@ -20,3 +23,4 @@ Follow existing Godot style: tabs/consistent indentation, early returns, and sho
|
||||
When reading files/JSON, always guard `FileAccess.open(...)` and validate parsed types before indexing.
|
||||
Use signals (already in `GameState`) for UI updates instead of polling or cross-node direct mutation.
|
||||
No Cursor/Claude/Windsurf/Cline/Goose/Copilot rule files were found in this repository, so this file is the canonical agent guidance.
|
||||
Avoid trivial one-line wrapper/helper functions that only forward or repack data. Inline the logic at the real call site unless the wrapper adds meaningful abstraction or is reused enough to justify it.
|
||||
|
||||
@@ -37,7 +37,6 @@ A modular idle/incremental game prototype featuring a decoupled buff system, mul
|
||||
| `LevelGameState` | `core/level_game_state.gd` | Central state management, save/load |
|
||||
| `CurrencyCatalogue` | `core/currency_catalogue.gd` | Currency catalog resource |
|
||||
| `PrestigeManager` | `core/prestige/prestige_manager.gd` | Prestige system |
|
||||
| `ResearchXPTracker` | `core/research/research_xp_tracker.gd` | Research XP accumulation |
|
||||
|
||||
### Data Directory Structure
|
||||
|
||||
|
||||
@@ -39,9 +39,6 @@ const HUGE_COST_EXPONENT: int = 1000000
|
||||
## Reference to generator UI
|
||||
@export var info_generator_container: GeneratorPanel
|
||||
|
||||
## Reference to research tracker
|
||||
var research_tracker: Node = null
|
||||
|
||||
## True while the pointer is inside the generator Area2D.
|
||||
var _mouse_entered: bool = false
|
||||
## Time left until next click/hover grant is allowed.
|
||||
@@ -108,8 +105,7 @@ func _ready() -> void:
|
||||
game_state.currency_changed.connect(_on_currency_changed)
|
||||
game_state.generator_state_changed.connect(_on_generated_state_changed)
|
||||
game_state.goal_completed.connect(_on_goal_completed)
|
||||
# Don't cache research_tracker here - it may not be initialized yet
|
||||
# Access it lazily when needed instead
|
||||
# Research XP is awarded via game_state.add_research_xp() - buff calculation handled statically
|
||||
|
||||
_ensure_registered()
|
||||
|
||||
@@ -155,10 +151,6 @@ func _grant_cycle_income(cycle_count: int) -> void:
|
||||
var amount_float: float = produced.mantissa * pow(10.0, float(produced.exponent))
|
||||
var research_xp_amount: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float)
|
||||
if game_state != null and research_xp_amount.mantissa > 0.0:
|
||||
var tracker: ResearchXPTracker = game_state.research_tracker as ResearchXPTracker
|
||||
if tracker:
|
||||
tracker.add_xp_with_buffs(data.research_data.id, research_xp_amount)
|
||||
else:
|
||||
game_state.add_research_xp(data.research_data.id, research_xp_amount)
|
||||
|
||||
## Convenience helper for the next single-unit purchase cost.
|
||||
@@ -444,10 +436,6 @@ func _try_grant_click_currency() -> void:
|
||||
var amount_float: float = click_value.mantissa * pow(10.0, float(click_value.exponent))
|
||||
var research_xp_amount: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float)
|
||||
if game_state != null and research_xp_amount.mantissa > 0.0:
|
||||
var tracker: ResearchXPTracker = game_state.research_tracker as ResearchXPTracker
|
||||
if tracker:
|
||||
tracker.add_xp_with_buffs(data.research_data.id, research_xp_amount)
|
||||
else:
|
||||
game_state.add_research_xp(data.research_data.id, research_xp_amount)
|
||||
|
||||
|
||||
|
||||
22
core/generator/research_buff_calculator.gd
Normal file
22
core/generator/research_buff_calculator.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
## Static utility class for research XP buff calculations
|
||||
class_name ResearchBuffCalculator
|
||||
|
||||
static func apply_buffs(game_state: LevelGameState, research_id: StringName, base_xp: BigNumber) -> BigNumber:
|
||||
if game_state == null:
|
||||
return base_xp
|
||||
|
||||
var multiplier: float = _calculate_buff_multiplier(game_state, research_id)
|
||||
var multiplier_bn: BigNumber = BigNumber.from_float(multiplier)
|
||||
return base_xp.multiply(multiplier_bn)
|
||||
|
||||
static func _calculate_buff_multiplier(game_state: LevelGameState, research_id: StringName) -> float:
|
||||
var research: ResearchData = game_state.research_catalogue.get_research_by_id(research_id)
|
||||
if research == null or research.associated_buff_id.is_empty():
|
||||
return 1.0
|
||||
|
||||
var buff: GeneratorBuffData = game_state.get_buff(research.associated_buff_id)
|
||||
if buff == null or not game_state.is_buff_active(buff.id):
|
||||
return 1.0
|
||||
|
||||
var level: int = game_state.get_buff_level(buff.id)
|
||||
return buff.get_effect_multiplier(level)
|
||||
1
core/generator/research_buff_calculator.gd.uid
Normal file
1
core/generator/research_buff_calculator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bhrcgp2dna0mb
|
||||
@@ -46,7 +46,6 @@ var _goal_completed: Dictionary = {}
|
||||
|
||||
var research_xp: Dictionary = {}
|
||||
var research_levels: Dictionary = {}
|
||||
var research_tracker: Node = null
|
||||
|
||||
var last_save_time: int = 0
|
||||
|
||||
@@ -80,6 +79,8 @@ var prestige_manager: PrestigeManager
|
||||
|
||||
func _ready() -> void:
|
||||
prestige_manager = find_child("PrestigeManager")
|
||||
assert(prestige_manager != null)
|
||||
|
||||
_initialize_currency_maps()
|
||||
_initialize_catalogues()
|
||||
_load_catalogue_goals()
|
||||
@@ -482,9 +483,11 @@ func get_research_multiplier(research_id: StringName) -> float:
|
||||
return research.get_multiplier_for_level(level)
|
||||
|
||||
func add_research_xp(research_id: StringName, xp: BigNumber) -> void:
|
||||
var actual_xp: BigNumber = ResearchBuffCalculator.apply_buffs(self, research_id, xp)
|
||||
|
||||
var old_level: int = get_research_level(research_id)
|
||||
var current_xp: BigNumber = get_research_xp(research_id)
|
||||
research_xp[research_id] = current_xp.add(xp)
|
||||
research_xp[research_id] = current_xp.add(actual_xp)
|
||||
|
||||
var research: ResearchData = _get_research_data(research_id)
|
||||
if research != null:
|
||||
@@ -496,20 +499,6 @@ func add_research_xp(research_id: StringName, xp: BigNumber) -> void:
|
||||
|
||||
research_xp_changed.emit(research_id, research_xp[research_id])
|
||||
|
||||
func get_research_xp_multiplier(research_id: StringName) -> float:
|
||||
var multiplier: float = 1.0
|
||||
var research: ResearchData = _get_research_data(research_id)
|
||||
if research == null:
|
||||
return 1.0
|
||||
|
||||
if not research.associated_buff_id.is_empty():
|
||||
var buff: GeneratorBuffData = get_buff(research.associated_buff_id)
|
||||
if buff != null and is_buff_active(buff.id):
|
||||
var level: int = get_buff_level(buff.id)
|
||||
multiplier *= buff.get_effect_multiplier(level)
|
||||
|
||||
return multiplier
|
||||
|
||||
func _get_research_data(research_id: StringName) -> ResearchData:
|
||||
if research_catalogue == null:
|
||||
return null
|
||||
|
||||
@@ -10,30 +10,26 @@ specific generators and can be enhanced with purchasable buffs that increase XP
|
||||
|
||||
| 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 |
|
||||
| `../generator/research_buff_calculator.gd` | Static utility for buff multiplier calculations |
|
||||
|
||||
## 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
|
||||
├── 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
|
||||
@@ -42,12 +38,13 @@ LevelGameState (Node2D)
|
||||
Generator Production (currency_per_cycle)
|
||||
│
|
||||
▼
|
||||
ResearchXPTracker
|
||||
LevelGameState.add_research_xp()
|
||||
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
|
||||
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}
|
||||
@@ -143,38 +140,61 @@ func get_all_research() -> Array[ResearchData]
|
||||
|
||||
Referenced directly by `LevelGameState` via `@export var research_catalogue: ResearchCatalogue`.
|
||||
|
||||
## ResearchXPTracker
|
||||
## ResearchBuffCalculator
|
||||
|
||||
Child node of `LevelGameState` that accumulates XP from production and applies buff multipliers.
|
||||
Static utility class for calculating research XP buff multipliers.
|
||||
|
||||
### Signals
|
||||
### Location
|
||||
|
||||
```gdscript
|
||||
signal research_xp_changed(research_id: StringName, new_xp: float)
|
||||
signal research_level_up(research_id: StringName, old_level: int, new_level: int)
|
||||
```
|
||||
`res://core/generator/research_buff_calculator.gd`
|
||||
|
||||
### 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
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
```
|
||||
|
||||
### Connection
|
||||
### Usage
|
||||
|
||||
Connected to `LevelGameState` via `generator_produced` signal.
|
||||
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
|
||||
|
||||
@@ -182,7 +202,7 @@ Connected to `LevelGameState` via `generator_produced` signal.
|
||||
```gdscript
|
||||
var research_xp: Dictionary = {} # {research_id: BigNumber}
|
||||
var research_levels: Dictionary = {} # {research_id: level_int}
|
||||
var research_tracker: ResearchXPTracker
|
||||
# research_tracker removed - buff calculation moved to static utility
|
||||
```
|
||||
|
||||
### Signals
|
||||
@@ -208,21 +228,27 @@ func get_research_multiplier(research_id: StringName) -> float:
|
||||
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"""
|
||||
"""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)
|
||||
research_xp[research_id] = current_xp.add(xp)
|
||||
new_level = research.get_level_for_xp(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)
|
||||
|
||||
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
|
||||
research_xp_changed.emit(research_id, research_xp[research_id])
|
||||
```
|
||||
|
||||
### Save/Load Integration
|
||||
@@ -272,11 +298,11 @@ func _grant_cycle_income(cycle_count: int) -> void:
|
||||
_add_currency(produced)
|
||||
production_tick.emit(produced, cycle_count, owned)
|
||||
|
||||
# Award research XP (BigNumber)
|
||||
# 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)
|
||||
research_tracker.add_xp_with_buffs(data.research_data.id, xp)
|
||||
game_state.add_research_xp(data.research_data.id, xp)
|
||||
```
|
||||
|
||||
### Research Multiplier
|
||||
@@ -425,7 +451,7 @@ actual_xp = base_xp * xp_multiplier
|
||||
| Decision | Value |
|
||||
|----------|-------|
|
||||
| ResearchCatalogue reference | Direct `@export var research_catalogue: ResearchCatalogue` in LevelGameState |
|
||||
| ResearchXPTracker location | Child of `LevelGameState` (receives signals via connection) |
|
||||
| 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 |
|
||||
@@ -438,8 +464,8 @@ actual_xp = base_xp * xp_multiplier
|
||||
## 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
|
||||
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
|
||||
@@ -458,4 +484,5 @@ On prestige reset:
|
||||
- `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
|
||||
|
||||
@@ -18,16 +18,10 @@ A production-based research system where:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LevelGameState │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 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 │
|
||||
│ - add_research_xp() applies buffs via ResearchBuffCalculator │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
@@ -39,6 +33,12 @@ A production-based research system where:
|
||||
│ - emits signal │ │ - Multiplier │ │ research │
|
||||
└─────────────────┘ └─────────────────┘ │ - Progress bars │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ResearchBuffCalculator (static utility) │
|
||||
│ - apply_buffs() calculates XP multiplier from active buffs │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
@@ -50,15 +50,22 @@ 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
|
||||
│ 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}│
|
||||
@@ -188,53 +195,33 @@ func get_all_research() -> Array[ResearchData]:
|
||||
|
||||
---
|
||||
|
||||
### 3. ResearchXPTracker (core/research/research_xp_tracker.gd)
|
||||
### 3. ResearchBuffCalculator (core/generator/research_buff_calculator.gd)
|
||||
|
||||
**Purpose:** Child of LevelGameState; accumulates XP from production and applies buff multipliers
|
||||
**Purpose:** Static utility class for calculating research XP buff multipliers
|
||||
|
||||
**Location:** `res://core/research/research_xp_tracker.gd`
|
||||
|
||||
**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)
|
||||
```
|
||||
**Location:** `res://core/generator/research_buff_calculator.gd`
|
||||
|
||||
**Key Methods:**
|
||||
```gdscript
|
||||
func add_xp_with_buffs(research_id: StringName, base_xp: float) -> float:
|
||||
"""Apply buff multipliers and award XP. Returns actual XP after buffs."""
|
||||
if game_state == null:
|
||||
return 0.0
|
||||
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 actual_xp: float = base_xp
|
||||
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)
|
||||
|
||||
# Apply research-specific buff multiplier
|
||||
var buff_multiplier: float = game_state.get_research_xp_multiplier(research_id)
|
||||
actual_xp *= buff_multiplier
|
||||
return multiplier
|
||||
|
||||
# Award XP (handles auto-leveling internally)
|
||||
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)
|
||||
if research == null:
|
||||
return
|
||||
|
||||
var amount_float: float = amount.to_float()
|
||||
if amount_float <= 0.0:
|
||||
return
|
||||
|
||||
var base_xp: float = research.xp_per_currency_produced * amount_float
|
||||
if base_xp <= 0.0:
|
||||
return
|
||||
|
||||
add_xp_with_buffs(research.id, base_xp)
|
||||
static func _calculate_buff_multiplier(buff: GeneratorBuffData, level: int) -> float:
|
||||
"""Calculate buff effect multiplier at given level."""
|
||||
return buff.get_effect_multiplier(level)
|
||||
```
|
||||
|
||||
**Connection:** Connected to `LevelGameState` via `generator_produced` signal
|
||||
**Usage:** Called from `LevelGameState.add_research_xp()` to apply buffs before storing XP
|
||||
|
||||
---
|
||||
|
||||
@@ -242,14 +229,14 @@ func _on_generator_produced(generator_id: StringName, amount: BigNumber) -> void
|
||||
|
||||
**New State Variables:**
|
||||
```gdscript
|
||||
var research_xp: Dictionary = {} # {research_id: xp_float}
|
||||
var research_xp: Dictionary = {} # {research_id: BigNumber}
|
||||
var research_levels: Dictionary = {} # {research_id: level_int}
|
||||
var research_tracker: ResearchXPTracker
|
||||
# research_tracker removed - buff calculation moved to static utility
|
||||
```
|
||||
|
||||
**New Signals:**
|
||||
```gdscript
|
||||
signal research_xp_changed(research_id: StringName, new_xp: float)
|
||||
signal research_xp_changed(research_id: StringName, new_xp: BigNumber)
|
||||
signal research_level_up(research_id: StringName, old_level: int, new_level: int)
|
||||
```
|
||||
|
||||
@@ -262,12 +249,12 @@ signal research_level_up(research_id: StringName, old_level: int, new_level: int
|
||||
```gdscript
|
||||
func register_research(research_id: StringName) -> void:
|
||||
if not research_xp.has(research_id):
|
||||
research_xp[research_id] = 0.0
|
||||
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) -> float:
|
||||
return research_xp.get(research_id, 0.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)
|
||||
@@ -279,11 +266,22 @@ func get_research_multiplier(research_id: StringName) -> float:
|
||||
return 1.0
|
||||
return research.get_multiplier_for_level(level)
|
||||
|
||||
func add_research_xp(research_id: StringName, xp: float) -> void:
|
||||
var old_level: int = get_research_level(research_id)
|
||||
research_xp[research_id] = research_xp.get(research_id, 0.0) + xp
|
||||
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 new_level: int = _get_research_data(research_id).get_level_for_xp(research_xp[research_id])
|
||||
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
|
||||
@@ -291,22 +289,6 @@ func add_research_xp(research_id: StringName, xp: float) -> void:
|
||||
|
||||
research_xp_changed.emit(research_id, research_xp[research_id])
|
||||
|
||||
func get_research_xp_multiplier(research_id: StringName) -> float:
|
||||
"""Returns multiplicative buff for research XP gain"""
|
||||
var multiplier: float = 1.0
|
||||
var research: ResearchData = _get_research_data(research_id)
|
||||
if research == null:
|
||||
return 1.0
|
||||
|
||||
# Find associated buff and apply its multiplier
|
||||
if not research.associated_buff_id.is_empty():
|
||||
var buff: GeneratorBuffData = get_buff(research.associated_buff_id)
|
||||
if buff != null and is_buff_active(buff.id):
|
||||
var level: int = get_buff_level(buff.id)
|
||||
multiplier *= buff.get_effect_multiplier(level)
|
||||
|
||||
return multiplier
|
||||
|
||||
func _get_research_data(research_id: StringName) -> ResearchData:
|
||||
if research_catalogue == null:
|
||||
return null
|
||||
@@ -338,11 +320,6 @@ research_levels.clear()
|
||||
|
||||
**Initialization in _ready():**
|
||||
```gdscript
|
||||
# After initializing other state, create research tracker
|
||||
research_tracker = ResearchXPTracker.new()
|
||||
add_child(research_tracker)
|
||||
research_tracker.game_state = self
|
||||
|
||||
# Initialize research state from catalogue
|
||||
if research_catalogue:
|
||||
for research in research_catalogue.get_all_research():
|
||||
@@ -377,10 +354,10 @@ func _grant_cycle_income(cycle_count: int) -> void:
|
||||
_add_currency(produced)
|
||||
production_tick.emit(produced, cycle_count, owned)
|
||||
|
||||
# NEW: Award research XP
|
||||
# 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()
|
||||
research_tracker.add_xp_with_buffs(data.research_data.id, xp)
|
||||
game_state.add_research_xp(data.research_data.id, BigNumber.from_float(xp))
|
||||
```
|
||||
|
||||
**New Method:**
|
||||
@@ -399,13 +376,6 @@ func get_effective_auto_run_multiplier() -> float:
|
||||
return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_multiplier()
|
||||
```
|
||||
|
||||
**In `_ready()`:**
|
||||
```gdscript
|
||||
# Get reference to research tracker
|
||||
if game_state:
|
||||
research_tracker = game_state.research_tracker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. GeneratorBuffData Extensions (core/generator/generator_buff_data.gd)
|
||||
@@ -549,11 +519,11 @@ func _on_research_level_up(research_id: StringName, _old_level: int, _new_level:
|
||||
|------|------|-------|----------|--------|
|
||||
| 1 | Create `ResearchData` resource class | `core/generator/research_data.gd` | High | ⬜ |
|
||||
| 2 | Create `ResearchCatalogue` resource class | `core/research/research_catalogue.gd` | High | ⬜ |
|
||||
| 3 | Create `ResearchXPTracker` node class | `core/research/research_xp_tracker.gd` | High | ⬜ |
|
||||
| 4 | Add research state to `LevelGameState` | `core/level_game_state.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 | ⬜ |
|
||||
| 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 | ⬜ |
|
||||
@@ -569,7 +539,7 @@ func _on_research_level_up(research_id: StringName, _old_level: int, _new_level:
|
||||
|----------|-------|
|
||||
| ResearchCatalogue reference | Direct `@export var research_catalogue: ResearchCatalogue` in LevelGameState |
|
||||
| ResearchCatalogue location | `core/research/research_catalogue.gd` (resource, referenced directly) |
|
||||
| ResearchXPTracker location | Child of `LevelGameState` (receives signals via connection) |
|
||||
| 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 |
|
||||
@@ -619,7 +589,8 @@ actual_xp = base_xp * xp_multiplier
|
||||
- **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 4 when adding research support
|
||||
- **Save format version** should be incremented to 5 when adding research support (BigNumber XP)
|
||||
- **ResearchXPTracker removed** - buff calculation moved to static `ResearchBuffCalculator` utility
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
class_name ResearchXPTracker
|
||||
extends Node
|
||||
|
||||
@export var game_state: LevelGameState
|
||||
|
||||
signal research_xp_changed(research_id: StringName, new_xp: BigNumber)
|
||||
signal research_level_up(research_id: StringName, old_level: int, new_level: int)
|
||||
|
||||
func add_xp_with_buffs(research_id: StringName, base_xp: BigNumber) -> BigNumber:
|
||||
if game_state == null:
|
||||
return BigNumber.new(0.0, 0)
|
||||
|
||||
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: Variant) -> void:
|
||||
var research: ResearchData = _get_research_for_generator(generator_id)
|
||||
if research == null:
|
||||
return
|
||||
|
||||
var research_catalogue: ResearchCatalogue = game_state.research_catalogue
|
||||
if research_catalogue == null:
|
||||
return
|
||||
|
||||
research = research_catalogue.get_research_by_generator_id(generator_id)
|
||||
if research == null:
|
||||
return
|
||||
|
||||
if not _is_research_active(research.id):
|
||||
return
|
||||
|
||||
var amount_big: BigNumber = _amount
|
||||
var amount_float: float = amount_big.mantissa * pow(10.0, float(amount_big.exponent))
|
||||
if amount_float <= 0.0:
|
||||
return
|
||||
|
||||
var base_xp: BigNumber = BigNumber.from_float(research.xp_per_currency_produced * amount_float)
|
||||
if base_xp.mantissa <= 0.0:
|
||||
return
|
||||
|
||||
add_xp_with_buffs(research.id, base_xp)
|
||||
|
||||
func _get_research_for_generator(generator_id: StringName) -> ResearchData:
|
||||
if game_state == null or game_state.research_catalogue == null:
|
||||
return null
|
||||
return game_state.research_catalogue.get_research_by_generator_id(generator_id)
|
||||
|
||||
func _is_research_active(research_id: StringName) -> bool:
|
||||
if game_state == null:
|
||||
return false
|
||||
|
||||
var research_panel: Node = game_state.find_child("ResearchPanel", true, false)
|
||||
if research_panel == null:
|
||||
return true
|
||||
|
||||
if research_panel.has_method("is_research_active"):
|
||||
return research_panel.is_research_active(research_id)
|
||||
|
||||
return true
|
||||
|
||||
func activate_research(research_id: StringName) -> void:
|
||||
if game_state == null:
|
||||
return
|
||||
|
||||
var research_panel: Node = game_state.find_child("ResearchPanel", true, false)
|
||||
if research_panel != null and research_panel.has_method("activate_research"):
|
||||
research_panel.activate_research(research_id)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bv3a4utlnn2fg
|
||||
@@ -10,7 +10,6 @@
|
||||
[ext_resource type="Script" uid="uid://srkiu4qe8s2m" path="res://core/prestige/prestige_manager.gd" id="5_x77df"]
|
||||
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="6_xnhlc"]
|
||||
[ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="7_0cs5o"]
|
||||
[ext_resource type="Script" uid="uid://bv3a4utlnn2fg" path="res://core/research/research_xp_tracker.gd" id="8_no27p"]
|
||||
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="9_1363k"]
|
||||
[ext_resource type="PackedScene" uid="uid://djedqovgngrx5" path="res://docs/gyms/tiny_sword/buildings/farm/farm.tscn" id="10_1lv5i"]
|
||||
[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="10_i1cck"]
|
||||
@@ -37,10 +36,6 @@ script = ExtResource("5_x77df")
|
||||
config = ExtResource("6_xnhlc")
|
||||
game_state = NodePath("..")
|
||||
|
||||
[node name="ResearchXpTracker" type="Node" parent="LevelGameState" unique_id=229654635 node_paths=PackedStringArray("game_state")]
|
||||
script = ExtResource("8_no27p")
|
||||
game_state = NodePath("..")
|
||||
|
||||
[node name="World" type="Node2D" parent="LevelGameState" unique_id=2118297724]
|
||||
|
||||
[node name="ForestProps" type="Node2D" parent="LevelGameState/World" unique_id=649424542]
|
||||
|
||||
Reference in New Issue
Block a user