diff --git a/AGENTS.md b/AGENTS.md index 7db0bb7..e258761 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/.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. diff --git a/README.md b/README.md index de83f65..27b99c9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/core/generator/currency_generator.gd b/core/generator/currency_generator.gd index feb8c55..8d1370f 100644 --- a/core/generator/currency_generator.gd +++ b/core/generator/currency_generator.gd @@ -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,11 +151,7 @@ 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) + game_state.add_research_xp(data.research_data.id, research_xp_amount) ## Convenience helper for the next single-unit purchase cost. func get_next_cost() -> BigNumber: @@ -444,11 +436,7 @@ 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) + game_state.add_research_xp(data.research_data.id, research_xp_amount) ## Resolves click reward from data. diff --git a/core/generator/research_buff_calculator.gd b/core/generator/research_buff_calculator.gd new file mode 100644 index 0000000..7ddad96 --- /dev/null +++ b/core/generator/research_buff_calculator.gd @@ -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) diff --git a/core/generator/research_buff_calculator.gd.uid b/core/generator/research_buff_calculator.gd.uid new file mode 100644 index 0000000..61136dd --- /dev/null +++ b/core/generator/research_buff_calculator.gd.uid @@ -0,0 +1 @@ +uid://bhrcgp2dna0mb diff --git a/core/level_game_state.gd b/core/level_game_state.gd index dc2aba2..ab1612c 100644 --- a/core/level_game_state.gd +++ b/core/level_game_state.gd @@ -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 diff --git a/core/research/README.md b/core/research/README.md index 5dd6b25..44737cc 100644 --- a/core/research/README.md +++ b/core/research/README.md @@ -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 + + 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 -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) +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 diff --git a/core/research/TODO.md b/core/research/TODO.md index 70b1e4f..5b441cd 100644 --- a/core/research/TODO.md +++ b/core/research/TODO.md @@ -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 - - # Award XP (handles auto-leveling internally) - game_state.add_research_xp(research_id, actual_xp) - - return actual_xp + return multiplier -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 --- diff --git a/core/research/research_xp_tracker.gd b/core/research/research_xp_tracker.gd deleted file mode 100644 index d1eb22a..0000000 --- a/core/research/research_xp_tracker.gd +++ /dev/null @@ -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) diff --git a/core/research/research_xp_tracker.gd.uid b/core/research/research_xp_tracker.gd.uid deleted file mode 100644 index 21fd44c..0000000 --- a/core/research/research_xp_tracker.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bv3a4utlnn2fg diff --git a/docs/gyms/tiny_sword/tiny_sword.tscn b/docs/gyms/tiny_sword/tiny_sword.tscn index c534522..503f8c0 100644 --- a/docs/gyms/tiny_sword/tiny_sword.tscn +++ b/docs/gyms/tiny_sword/tiny_sword.tscn @@ -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]