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:
225
core/generator/README.md
Normal file
225
core/generator/README.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Generator Module Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The `generator/` subfolder contains the core idle game mechanics: automated resource production with scaling costs, upgrades (buffs), and manual interaction.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `currency_generator.gd` | Runtime generator logic (Node2D) |
|
||||
| `currency_generator_data.gd` | Balancing data resource |
|
||||
| `generator_buff_data.gd` | Buff/upgrade definitions |
|
||||
| `generator_container.gd` | UI panel for generator stats |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CurrencyGenerator (Node2D)
|
||||
├── Handles production cycles
|
||||
├── Manages purchases
|
||||
├── Processes clicks/hover
|
||||
└── GeneratorPanel (UI)
|
||||
├── Shows stats/costs
|
||||
└── Buff upgrade buttons
|
||||
```
|
||||
|
||||
## CurrencyGeneratorData
|
||||
|
||||
Resource class for generator balancing data.
|
||||
|
||||
### Key Properties
|
||||
|
||||
```gdscript
|
||||
@export var id: StringName = &"" # Unique identifier
|
||||
@export var name: String = "" # Display name
|
||||
@export var starts_unlocked: bool = true # Visible at game start
|
||||
@export var purchase_currency: Currency # What to spend
|
||||
@export var unlock_goal: GoalData # Goal to unlock this (behavior defined in GoalData)
|
||||
|
||||
# Cost formula: cost = base * coefficient^owned
|
||||
@export var initial_cost: float = 10.0 # Base cost
|
||||
@export var coefficient: float = 1.15 # Growth factor
|
||||
|
||||
# Production
|
||||
@export var initial_time: float = 1.0 # Seconds per cycle
|
||||
@export var initial_productivity: float = 1.0 # Resources per cycle
|
||||
|
||||
# Milestones
|
||||
@export var milestone_step: int = 25 # Every N units
|
||||
@export var milestone_multiplier: float = 2.0 # x multiplier
|
||||
|
||||
# Manual click
|
||||
@export var click_mantissa: float = 1.0
|
||||
@export var click_exponent: int = 0
|
||||
@export var click_cooldown_seconds: float = 0.2
|
||||
```
|
||||
|
||||
### Cost Formula
|
||||
|
||||
**Bulk Buy Cost** (geometric series):
|
||||
```
|
||||
total_cost = base * r^owned * ((r^n - 1) / (r - 1))
|
||||
```
|
||||
Where:
|
||||
- `base` = initial_cost
|
||||
- `r` = coefficient
|
||||
- `owned` = current count
|
||||
- `n` = amount to buy
|
||||
|
||||
**Max Buy** (inverse formula):
|
||||
```
|
||||
max = floor(log((currency * (r-1) / (base * r^owned)) + 1) / log(r))
|
||||
```
|
||||
|
||||
### Production Formula
|
||||
|
||||
```gdscript
|
||||
production_per_cycle = initial_productivity * owned * run_multiplier * milestone_mult * purchased_mult
|
||||
production_per_second = production_per_cycle / initial_time
|
||||
```
|
||||
|
||||
## CurrencyGenerator
|
||||
|
||||
Runtime node instance attached to generator scenes.
|
||||
|
||||
### Signals
|
||||
|
||||
```gdscript
|
||||
signal purchase_completed(amount, total_owned, total_purchased, total_cost)
|
||||
signal production_tick(amount, cycle_count, total_owned)
|
||||
signal buff_purchased(buff_id, new_level, cost, cost_currency_id)
|
||||
signal goal_achieved(generator_id, goal_id) # Emitted when generator unlocks from goal
|
||||
```
|
||||
|
||||
### Key Methods
|
||||
|
||||
```gdscript
|
||||
# Purchase
|
||||
func buy(amount: int = 1) -> bool
|
||||
func buy_max() -> int
|
||||
func get_cost_for_amount(amount: int) -> BigNumber
|
||||
func can_buy(amount: int) -> bool
|
||||
|
||||
# Goal-based unlock (automatic when goal completes)
|
||||
# Generator listens to GameState.goal_completed signal
|
||||
```
|
||||
|
||||
# Production
|
||||
func grant_currency(amount: BigNumber)
|
||||
func get_effective_auto_run_multiplier() -> float
|
||||
|
||||
# Buffs
|
||||
func buy_buff(buff_id: StringName) -> bool
|
||||
func get_buff_level(buff_id: StringName) -> int
|
||||
func is_buff_unlocked(buff_id: StringName) -> bool
|
||||
```
|
||||
|
||||
### Production Cycle
|
||||
|
||||
1. Accumulate `cycle_progress_seconds` in `_process()`
|
||||
2. When `>= initial_time`, grant production
|
||||
3. Emit `production_tick` signal
|
||||
4. Multiply by:
|
||||
- Owned count
|
||||
- Run multiplier (export)
|
||||
- Buff multipliers (auto_production)
|
||||
- Milestone bonuses
|
||||
- Purchase boost
|
||||
|
||||
## GeneratorBuffData
|
||||
|
||||
Defines upgradeable modifiers for generators.
|
||||
|
||||
### Buff Kinds
|
||||
|
||||
```gdscript
|
||||
enum BuffKind {
|
||||
AUTO_PRODUCTION_MULTIPLIER, # x output per cycle
|
||||
MANUAL_CLICK_MULTIPLIER, # x click reward
|
||||
RESOURCE_PURCHASE # Instant currency grant
|
||||
}
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
```gdscript
|
||||
@export var id: StringName = &""
|
||||
@export var target_ids: Array[StringName] = [] # Generators affected
|
||||
@export var kind: BuffKind
|
||||
@export var max_level: int = -1 # -1 = unlimited
|
||||
@export var base_effect: float = 1.0 # Starting multiplier
|
||||
@export var effect_increment: float = 0.1 # +per level
|
||||
|
||||
@export var base_cost_mantissa: float = 10.0
|
||||
@export var base_cost_exponent: int = 0
|
||||
@export var cost_multiplier: float = 1.5 # Cost growth
|
||||
|
||||
# Effect: base_effect + (effect_increment * level)
|
||||
func get_effect_multiplier(level: int) -> float
|
||||
```
|
||||
|
||||
### Cost Formula
|
||||
|
||||
```gdscript
|
||||
cost_for_level = base_cost * cost_multiplier^level
|
||||
```
|
||||
|
||||
### Resource Purchase Buff
|
||||
|
||||
Special buff type that grants instant currency:
|
||||
```gdscript
|
||||
resource_purchase_amount = base_amount * (resource_purchase_increment_multiplier^level)
|
||||
```
|
||||
|
||||
## GeneratorPanel
|
||||
|
||||
UI component showing generator information.
|
||||
|
||||
### Displays
|
||||
- Generator name and owned count
|
||||
- Next purchase cost
|
||||
- Production per second
|
||||
- Buff upgrade buttons with costs
|
||||
|
||||
### Signals Connected
|
||||
- `purchase_completed` - Refresh UI
|
||||
- `production_tick` - Update production display
|
||||
- `generator_buff_level_changed` - Refresh buff rows
|
||||
|
||||
## Goal-Based Unlock
|
||||
|
||||
Generators with `unlock_goal` automatically listen to `LevelGameState.goal_completed` signal:
|
||||
- When goal completes, generator unlocks automatically
|
||||
- Goal's `unlock_behavior` (AUTOMATIC/MANUAL) controls when goal completes
|
||||
- Manual goals require player to click "Unlock" button first
|
||||
|
||||
```gdscript
|
||||
# In CurrencyGenerator._ready():
|
||||
level_game_state.goal_completed.connect(_on_goal_completed)
|
||||
|
||||
func _on_goal_completed(goal_id: StringName) -> void:
|
||||
if data.has_unlock_goal() and data.get_unlock_goal_id() == goal_id:
|
||||
unlock_generator()
|
||||
```
|
||||
|
||||
## Prestige Integration
|
||||
|
||||
Generators automatically apply prestige multipliers:
|
||||
```gdscript
|
||||
effective_multiplier = run_multiplier * buff_multiplier * prestige_multiplier
|
||||
```
|
||||
|
||||
On prestige via `LevelGameState.reset_for_prestige()`:
|
||||
1. Current currencies reset to 0
|
||||
2. `cycle_progress_seconds` reset
|
||||
3. Buff levels reset to 0
|
||||
4. Goal completion state cleared and re-initialized
|
||||
5. Research XP and levels reset to 0
|
||||
|
||||
## See Also
|
||||
|
||||
- `core/level_game_state.gd` - Generator state storage
|
||||
- `core/goals/goal_data.gd` - Unlock conditions
|
||||
- `core/prestige/prestige_manager.gd` - Multiplier application
|
||||
@@ -24,7 +24,7 @@ signal goal_achieved(generator_id: StringName, goal_id: StringName)
|
||||
const HUGE_COST_EXPONENT: int = 1000000
|
||||
|
||||
## Currency target updated by this generator.
|
||||
@export var currency: Resource
|
||||
@export var currency: Currency
|
||||
## Data resource containing balancing values and defaults (required).
|
||||
@export var data: CurrencyGeneratorData
|
||||
## Enables per-cycle automatic production when true.
|
||||
@@ -37,49 +37,52 @@ const HUGE_COST_EXPONENT: int = 1000000
|
||||
@export var run_multiplier: float = 1.0
|
||||
|
||||
## Reference to generator UI
|
||||
@onready var info_generator_container: GeneratorPanel = $GeneratorPanel
|
||||
@export var info_generator_container: GeneratorPanel
|
||||
|
||||
## True while the pointer is inside the generator Area2D.
|
||||
var _mouse_entered: bool = false
|
||||
## Time left until next click/hover grant is allowed.
|
||||
var _remaining_click_cooldown_seconds: float = 0.0
|
||||
|
||||
## Reference to LevelGameState instance.
|
||||
@onready var game_state: LevelGameState = find_parent("LevelGameState")
|
||||
|
||||
## Number of currently owned generators.
|
||||
## Backed by GameState via this generator's resolved id.
|
||||
## Backed by LevelGameState via this generator's resolved id.
|
||||
var owned: int:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.get_generator_owned(_generator_id)
|
||||
return game_state.get_generator_owned(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_owned(_generator_id, value)
|
||||
game_state.set_generator_owned(_generator_id, value)
|
||||
|
||||
## Lifetime purchased generators (does not decrease if ownership drops).
|
||||
var purchased_count: int:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.get_generator_purchased_count(_generator_id)
|
||||
return game_state.get_generator_purchased_count(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_purchased_count(_generator_id, value)
|
||||
game_state.set_generator_purchased_count(_generator_id, value)
|
||||
|
||||
## Whether this generator is unlocked for the player.
|
||||
var is_unlocked: bool:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.is_generator_unlocked(_generator_id)
|
||||
return game_state.is_generator_unlocked(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_unlocked(_generator_id, value)
|
||||
game_state.set_generator_unlocked(_generator_id, value)
|
||||
|
||||
## Whether this unlocked generator is currently visible/usable to the player.
|
||||
var is_available: bool:
|
||||
get:
|
||||
_ensure_registered()
|
||||
return GameState.is_generator_available(_generator_id)
|
||||
return game_state.is_generator_available(_generator_id)
|
||||
set(value):
|
||||
_ensure_registered()
|
||||
GameState.set_generator_available(_generator_id, value)
|
||||
game_state.set_generator_available(_generator_id, value)
|
||||
|
||||
## Accumulated elapsed time toward the next automatic production cycle.
|
||||
var cycle_progress_seconds: float = 0.0
|
||||
@@ -96,12 +99,15 @@ func _ready() -> void:
|
||||
assert(data != null, "Data cannot be null")
|
||||
|
||||
_generator_id = _resolve_generator_id()
|
||||
#_ensure_registered()
|
||||
cycle_progress_seconds = 0.0
|
||||
|
||||
GameState.currency_changed.connect(_on_currency_changed)
|
||||
GameState.generator_state_changed.connect(_on_generated_state_changed)
|
||||
_evaluate_generator_unlock_goal(false)
|
||||
if game_state:
|
||||
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)
|
||||
# Research XP is awarded via game_state.add_research_xp() - buff calculation handled statically
|
||||
|
||||
_ensure_registered()
|
||||
|
||||
## Updates click cooldown/click grants and runs automatic production cycles.
|
||||
func _process(delta: float) -> void:
|
||||
@@ -137,6 +143,23 @@ func _grant_cycle_income(cycle_count: int) -> void:
|
||||
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)
|
||||
|
||||
if data != null and data.research_data != null:
|
||||
if not _is_research_active(data.research_data.id):
|
||||
return
|
||||
|
||||
var research_workers: int = game_state.get_research_workers()
|
||||
if research_workers < data.research_data.min_workers_for_xp:
|
||||
return
|
||||
|
||||
var amount_float: float = produced.mantissa * pow(10.0, float(produced.exponent))
|
||||
var base_xp: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float)
|
||||
|
||||
var worker_multiplier: float = 1.0 + (float(research_workers) * data.research_data.worker_scaling_factor)
|
||||
var scaled_xp: BigNumber = base_xp.multiply(BigNumber.from_float(worker_multiplier))
|
||||
|
||||
if game_state != null and scaled_xp.mantissa > 0.0:
|
||||
game_state.add_research_xp(data.research_data.id, scaled_xp)
|
||||
|
||||
## Convenience helper for the next single-unit purchase cost.
|
||||
func get_next_cost() -> BigNumber:
|
||||
@@ -195,7 +218,7 @@ func buy_max() -> int:
|
||||
if purchase_currency_id == &"":
|
||||
return 0
|
||||
|
||||
var available_currency: float = _big_number_to_float(_get_currency_amount_by_id(purchase_currency_id))
|
||||
var available_currency: float = _get_currency_amount_by_id(purchase_currency_id).to_float()
|
||||
var estimated_max: int = data.max_affordable(owned, available_currency)
|
||||
if estimated_max <= 0:
|
||||
return 0
|
||||
@@ -219,18 +242,27 @@ func buy_max() -> int:
|
||||
func get_buffs() -> Array[GeneratorBuffData]:
|
||||
if data == null:
|
||||
return []
|
||||
return data.buffs
|
||||
if game_state == null:
|
||||
return []
|
||||
return game_state.get_buffs_for_generator(_generator_id)
|
||||
|
||||
func get_buff_level(buff_id: StringName) -> int:
|
||||
_ensure_registered()
|
||||
return GameState.get_generator_buff_level(_generator_id, buff_id)
|
||||
if game_state == null:
|
||||
return 0
|
||||
return game_state.get_buff_level(buff_id)
|
||||
|
||||
func is_buff_unlocked(buff_id: StringName) -> bool:
|
||||
_ensure_registered()
|
||||
return GameState.is_generator_buff_unlocked(_generator_id, buff_id)
|
||||
if game_state == null:
|
||||
return false
|
||||
return game_state.is_buff_unlocked(buff_id)
|
||||
|
||||
func get_buff_cost(buff_id: StringName) -> BigNumber:
|
||||
var buff: GeneratorBuffData = _find_buff(buff_id)
|
||||
if game_state == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
|
||||
if buff == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
if not is_buff_unlocked(buff.id):
|
||||
@@ -240,7 +272,10 @@ func get_buff_cost(buff_id: StringName) -> BigNumber:
|
||||
return buff.get_cost_for_level(level)
|
||||
|
||||
func get_buff_cost_currency_id(buff_id: StringName) -> StringName:
|
||||
var buff: GeneratorBuffData = _find_buff(buff_id)
|
||||
if game_state == null:
|
||||
return &""
|
||||
|
||||
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
|
||||
if buff == null:
|
||||
return &""
|
||||
return _resolve_buff_cost_currency_id(buff)
|
||||
@@ -249,7 +284,10 @@ func can_buy_buff(buff_id: StringName) -> bool:
|
||||
if not is_available_to_player():
|
||||
return false
|
||||
|
||||
var buff: GeneratorBuffData = _find_buff(buff_id)
|
||||
if game_state == null:
|
||||
return false
|
||||
|
||||
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
|
||||
if buff == null:
|
||||
return false
|
||||
|
||||
@@ -265,21 +303,24 @@ func can_buy_buff(buff_id: StringName) -> bool:
|
||||
return false
|
||||
|
||||
var cost: BigNumber = buff.get_cost_for_level(current_level)
|
||||
var current_amount: BigNumber = GameState.get_currency_amount_by_id(cost_currency_id)
|
||||
var current_amount: BigNumber = game_state.get_currency_amount_by_id(cost_currency_id)
|
||||
return current_amount.is_greater_than(cost) or current_amount.is_equal_to(cost)
|
||||
|
||||
func buy_buff(buff_id: StringName) -> bool:
|
||||
if not is_available_to_player():
|
||||
return false
|
||||
|
||||
var buff: GeneratorBuffData = _find_buff(buff_id)
|
||||
if game_state == null:
|
||||
return false
|
||||
|
||||
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
|
||||
if buff == null:
|
||||
return false
|
||||
|
||||
if not is_buff_unlocked(buff.id):
|
||||
if not game_state.is_buff_unlocked(buff.id):
|
||||
return false
|
||||
|
||||
var current_level: int = get_buff_level(buff.id)
|
||||
var current_level: int = game_state.get_buff_level(buff.id)
|
||||
if not buff.can_purchase_next_level(current_level):
|
||||
return false
|
||||
|
||||
@@ -293,25 +334,58 @@ func buy_buff(buff_id: StringName) -> bool:
|
||||
return false
|
||||
|
||||
var next_level: int = current_level + 1
|
||||
GameState.set_generator_buff_level(_generator_id, buff.id, next_level)
|
||||
game_state.set_buff_level(buff.id, next_level)
|
||||
_apply_resource_purchase_buff(buff, next_level)
|
||||
buff_purchased.emit(buff.id, next_level, cost, cost_currency_id)
|
||||
return true
|
||||
|
||||
func is_buff_maxed(buff_id: StringName) -> bool:
|
||||
var buff: GeneratorBuffData = _find_buff(buff_id)
|
||||
if game_state == null:
|
||||
return true
|
||||
|
||||
var buff: GeneratorBuffData = game_state.get_buff(buff_id)
|
||||
if buff == null:
|
||||
return true
|
||||
if not is_buff_unlocked(buff.id):
|
||||
if not game_state.is_buff_unlocked(buff.id):
|
||||
return false
|
||||
|
||||
return not buff.can_purchase_next_level(get_buff_level(buff.id))
|
||||
return not buff.can_purchase_next_level(game_state.get_buff_level(buff.id))
|
||||
|
||||
func get_automatic_production_multiplier() -> float:
|
||||
return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER)
|
||||
|
||||
func get_effective_auto_run_multiplier() -> float:
|
||||
return run_multiplier * get_automatic_production_multiplier() * _get_prestige_multiplier()
|
||||
return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_buff_production_multiplier()
|
||||
|
||||
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)
|
||||
|
||||
func _is_research_active(research_id: StringName) -> bool:
|
||||
if game_state == null:
|
||||
return true
|
||||
|
||||
var research_panel: Node = null
|
||||
var parent: Node = get_parent()
|
||||
while parent != null and parent != game_state.get_tree().root:
|
||||
if parent.has_method("is_research_active"):
|
||||
research_panel = parent
|
||||
break
|
||||
parent = parent.get_parent()
|
||||
|
||||
if research_panel == null:
|
||||
research_panel = 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 reset_runtime_state_for_prestige() -> void:
|
||||
_is_registered = false
|
||||
@@ -319,8 +393,6 @@ func reset_runtime_state_for_prestige() -> void:
|
||||
cycle_progress_seconds = 0.0
|
||||
_remaining_click_cooldown_seconds = 0.0
|
||||
_ensure_registered()
|
||||
_evaluate_generator_unlock_goal(false)
|
||||
_evaluate_buff_unlock_goals()
|
||||
visible = is_available_to_player()
|
||||
|
||||
func get_manual_click_multiplier() -> float:
|
||||
@@ -359,9 +431,30 @@ func _try_grant_click_currency() -> void:
|
||||
if _remaining_click_cooldown_seconds > 0.0:
|
||||
return
|
||||
|
||||
_add_currency(_get_click_value())
|
||||
var click_value: BigNumber = _get_click_value()
|
||||
_add_currency(click_value)
|
||||
|
||||
_remaining_click_cooldown_seconds = _get_click_cooldown_seconds()
|
||||
|
||||
# Grant research XP for manual clicks
|
||||
if data.research_data != null and click_value.mantissa > 0.0:
|
||||
if not _is_research_active(data.research_data.id):
|
||||
return
|
||||
|
||||
var research_workers: int = game_state.get_research_workers()
|
||||
if research_workers < data.research_data.min_workers_for_xp:
|
||||
return
|
||||
|
||||
var amount_float: float = click_value.mantissa * pow(10.0, float(click_value.exponent))
|
||||
var base_xp: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float)
|
||||
|
||||
var worker_multiplier: float = 1.0 + (float(research_workers) * data.research_data.worker_scaling_factor)
|
||||
var scaled_xp: BigNumber = base_xp.multiply(BigNumber.from_float(worker_multiplier))
|
||||
|
||||
if game_state != null and scaled_xp.mantissa > 0.0:
|
||||
game_state.add_research_xp(data.research_data.id, scaled_xp)
|
||||
|
||||
|
||||
## Resolves click reward from data.
|
||||
func _get_click_value() -> BigNumber:
|
||||
if data == null:
|
||||
@@ -384,20 +477,10 @@ func _get_click_cooldown_seconds() -> float:
|
||||
func _grants_click_while_hovering() -> bool:
|
||||
return grants_click_while_hovering
|
||||
|
||||
func _get_prestige_multiplier() -> float:
|
||||
var manager: PrestigeManager = get_node_or_null("/root/PrestigeManager")
|
||||
if manager == null:
|
||||
func _get_prestige_buff_production_multiplier() -> float:
|
||||
if game_state == null:
|
||||
return 1.0
|
||||
if not manager.get_total_multiplier():
|
||||
return 1.0
|
||||
|
||||
var raw_multiplier: Variant = manager.get_total_multiplier()
|
||||
if raw_multiplier is float:
|
||||
return maxf(raw_multiplier, 0.0)
|
||||
if raw_multiplier is int:
|
||||
return maxf(float(raw_multiplier), 0.0)
|
||||
|
||||
return 1.0
|
||||
return game_state.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, _generator_id)
|
||||
|
||||
## Returns this generator's resolved state id.
|
||||
func get_generator_id() -> StringName:
|
||||
@@ -406,14 +489,16 @@ func get_generator_id() -> StringName:
|
||||
|
||||
## Returns the configured currency id.
|
||||
func get_currency_id() -> StringName:
|
||||
return GameState.get_currency_id(currency)
|
||||
if game_state == null:
|
||||
return &""
|
||||
return game_state.get_currency_id(currency)
|
||||
|
||||
func get_purchase_currency_id() -> StringName:
|
||||
var purchase_currency: Currency = _resolve_purchase_currency()
|
||||
if purchase_currency == null:
|
||||
return &""
|
||||
|
||||
return GameState.get_currency_id(purchase_currency)
|
||||
return game_state.get_currency_id(purchase_currency)
|
||||
|
||||
## True when the generator is both unlocked and available.
|
||||
func is_available_to_player() -> bool:
|
||||
@@ -425,13 +510,17 @@ func _add_currency(amount: BigNumber) -> void:
|
||||
push_error("CurrencyGenerator '%s' cannot add currency: currency resource is null." % String(name))
|
||||
return
|
||||
|
||||
GameState.add_currency(currency, amount)
|
||||
if game_state == null:
|
||||
return
|
||||
game_state.add_currency(currency, amount)
|
||||
|
||||
func _add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void:
|
||||
if currency_id == &"":
|
||||
return
|
||||
|
||||
GameState.add_currency_by_id(currency_id, amount)
|
||||
if game_state == null:
|
||||
return
|
||||
game_state.add_currency_by_id(currency_id, amount)
|
||||
|
||||
## Attempts to spend target currency; returns false when insufficient.
|
||||
func _spend_currency(cost: BigNumber) -> bool:
|
||||
@@ -439,25 +528,33 @@ func _spend_currency(cost: BigNumber) -> bool:
|
||||
push_error("CurrencyGenerator '%s' cannot spend currency: currency resource is null." % String(name))
|
||||
return false
|
||||
|
||||
return GameState.spend_currency(currency, cost)
|
||||
if game_state == null:
|
||||
return false
|
||||
return game_state.spend_currency(currency, cost)
|
||||
|
||||
func _spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool:
|
||||
if currency_id == &"":
|
||||
return false
|
||||
|
||||
return GameState.spend_currency_by_id(currency_id, cost)
|
||||
if game_state == null:
|
||||
return false
|
||||
return game_state.spend_currency_by_id(currency_id, cost)
|
||||
|
||||
## Returns the current amount for the configured currency type.
|
||||
func _get_currency_amount() -> BigNumber:
|
||||
if currency == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
return GameState.get_currency_amount(currency)
|
||||
if game_state == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
return game_state.get_currency_amount(currency)
|
||||
|
||||
func _get_currency_amount_by_id(currency_id: StringName) -> BigNumber:
|
||||
if currency_id == &"":
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
return GameState.get_currency_amount_by_id(currency_id)
|
||||
if game_state == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
return game_state.get_currency_amount_by_id(currency_id)
|
||||
|
||||
## Safely converts finite float values into BigNumber costs.
|
||||
func _float_to_big_number(value: float) -> BigNumber:
|
||||
@@ -468,29 +565,19 @@ func _float_to_big_number(value: float) -> BigNumber:
|
||||
return BigNumber.from_float(value)
|
||||
|
||||
## Converts BigNumber to float for approximate estimate math.
|
||||
func _big_number_to_float(value: BigNumber) -> float:
|
||||
if value.mantissa <= 0.0:
|
||||
return 0.0
|
||||
if value.exponent > 308:
|
||||
return INF
|
||||
if value.exponent < -308:
|
||||
return 0.0
|
||||
return value.mantissa * pow(10.0, float(value.exponent))
|
||||
|
||||
func _find_buff(buff_id: StringName) -> GeneratorBuffData:
|
||||
if data == null:
|
||||
return null
|
||||
return data.find_buff(buff_id)
|
||||
|
||||
func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName:
|
||||
if buff == null:
|
||||
return &""
|
||||
|
||||
var cost_currency: Resource = buff.cost_currency if buff.cost_currency != null else currency
|
||||
var cost_currency: Currency = buff.cost_currency if buff.cost_currency != null else currency
|
||||
if cost_currency == null:
|
||||
return &""
|
||||
|
||||
return GameState.get_currency_id(cost_currency)
|
||||
if game_state == null:
|
||||
return &""
|
||||
return game_state.get_currency_id(cost_currency)
|
||||
|
||||
func _resolve_purchase_currency() -> Currency:
|
||||
if data == null:
|
||||
@@ -502,25 +589,18 @@ func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName:
|
||||
if buff == null:
|
||||
return &""
|
||||
|
||||
var target_currency: Resource = buff.resource_target_currency if buff.resource_target_currency != null else currency
|
||||
var target_currency: Currency = buff.resource_target_currency if buff.resource_target_currency != null else currency
|
||||
if target_currency == null:
|
||||
return &""
|
||||
|
||||
return GameState.get_currency_id(target_currency)
|
||||
if game_state == null:
|
||||
return &""
|
||||
return game_state.get_currency_id(target_currency)
|
||||
|
||||
func _get_multiplier_for_buff_kind(kind: GeneratorBuffData.BuffKind) -> float:
|
||||
var multiplier: float = 1.0
|
||||
for buff in get_buffs():
|
||||
if buff == null:
|
||||
continue
|
||||
if buff.kind != kind:
|
||||
continue
|
||||
if not is_buff_unlocked(buff.id):
|
||||
continue
|
||||
|
||||
multiplier *= buff.get_effect_multiplier(get_buff_level(buff.id))
|
||||
|
||||
return maxf(multiplier, 0.0)
|
||||
if game_state == null:
|
||||
return 1.0
|
||||
return game_state.get_effective_multiplier(_generator_id, kind)
|
||||
|
||||
func _apply_resource_purchase_buff(buff: GeneratorBuffData, level: int) -> void:
|
||||
if buff == null:
|
||||
@@ -550,7 +630,7 @@ func _resolve_generator_id() -> StringName:
|
||||
|
||||
return &"generator"
|
||||
|
||||
## Ensures this generator has a state entry in GameState.
|
||||
## Ensures this generator has a state entry in LevelGameState.
|
||||
func _ensure_registered() -> void:
|
||||
if _is_registered or _is_registering:
|
||||
return
|
||||
@@ -560,8 +640,11 @@ func _ensure_registered() -> void:
|
||||
if _generator_id == &"":
|
||||
return
|
||||
|
||||
if game_state == null:
|
||||
return
|
||||
|
||||
_is_registering = true
|
||||
GameState.register_generator(
|
||||
game_state.register_generator(
|
||||
_generator_id,
|
||||
_default_unlocked_state(),
|
||||
_default_available_state(),
|
||||
@@ -572,6 +655,9 @@ func _ensure_registered() -> void:
|
||||
_is_registering = false
|
||||
|
||||
func _register_buffs() -> void:
|
||||
if game_state == null:
|
||||
return
|
||||
|
||||
for buff in get_buffs():
|
||||
if buff == null:
|
||||
continue
|
||||
@@ -581,58 +667,34 @@ func _register_buffs() -> void:
|
||||
continue
|
||||
|
||||
var buff_id: StringName = StringName(buff_id_text)
|
||||
GameState.register_generator_buff(_generator_id, buff_id, 0)
|
||||
var persisted_level: int = GameState.get_generator_buff_level(_generator_id, buff_id)
|
||||
var starts_unlocked: bool = buff.unlocked or persisted_level > 0
|
||||
GameState.register_generator_buff_unlocked(_generator_id, buff_id, starts_unlocked)
|
||||
game_state.register_buff(buff)
|
||||
|
||||
if not game_state.is_buff_unlocked(buff_id):
|
||||
var persisted_level: int = game_state.get_buff_level(buff_id)
|
||||
var starts_unlocked: bool = buff.unlocked or persisted_level > 0
|
||||
game_state.set_buff_unlocked(buff_id, starts_unlocked)
|
||||
|
||||
_evaluate_buff_unlock_goals()
|
||||
|
||||
func _evaluate_buff_unlock_goals() -> void:
|
||||
for buff in get_buffs():
|
||||
_try_unlock_buff_from_goal(buff)
|
||||
|
||||
func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
|
||||
if buff == null:
|
||||
return
|
||||
|
||||
var buff_id_text: String = String(buff.id).strip_edges()
|
||||
if buff_id_text.is_empty():
|
||||
return
|
||||
|
||||
var buff_id: StringName = StringName(buff_id_text)
|
||||
if GameState.is_generator_buff_unlocked(_generator_id, buff_id):
|
||||
return
|
||||
if not buff.has_unlock_goal():
|
||||
return
|
||||
if not buff.is_unlock_goal_met():
|
||||
return
|
||||
|
||||
GameState.set_generator_buff_unlocked(_generator_id, buff_id, true)
|
||||
game_state.try_unlock_buff_from_goal(buff)
|
||||
|
||||
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
|
||||
_evaluate_generator_unlock_goal(false)
|
||||
_evaluate_buff_unlock_goals()
|
||||
if game_state == null:
|
||||
return
|
||||
game_state.evaluate_all_goals()
|
||||
|
||||
func try_unlock_from_goal() -> bool:
|
||||
return _evaluate_generator_unlock_goal(true)
|
||||
|
||||
func _evaluate_generator_unlock_goal(allow_manual_unlock: bool) -> bool:
|
||||
if data == null:
|
||||
return false
|
||||
func _on_goal_completed(goal_id: StringName) -> void:
|
||||
if data == null or not data.has_unlock_goal():
|
||||
return
|
||||
if data.get_unlock_goal_id() != goal_id:
|
||||
return
|
||||
if is_available_to_player():
|
||||
return false
|
||||
if not data.has_unlock_goal():
|
||||
return false
|
||||
if not data.unlocks_automatically_from_goal() and not allow_manual_unlock:
|
||||
return false
|
||||
if not data.is_unlock_goal_met():
|
||||
return false
|
||||
return
|
||||
|
||||
GameState.set_generator_unlocked(_generator_id, true)
|
||||
GameState.set_generator_available(_generator_id, true)
|
||||
goal_achieved.emit(_generator_id, data.get_unlock_goal_id())
|
||||
return true
|
||||
if game_state == null:
|
||||
return
|
||||
game_state.set_generator_unlocked(_generator_id, true)
|
||||
game_state.set_generator_available(_generator_id, true)
|
||||
goal_achieved.emit(_generator_id, goal_id)
|
||||
_on_generated_state_changed(_generator_id, game_state._get_generator_state(_generator_id))
|
||||
|
||||
## Default unlocked state loaded from generator data.
|
||||
func _default_unlocked_state() -> bool:
|
||||
@@ -665,3 +727,5 @@ func _on_area_2d_mouse_exited() -> void:
|
||||
func _on_generated_state_changed(generator_id: StringName, _state: Dictionary) -> void:
|
||||
if generator_id == _generator_id:
|
||||
visible = is_available_to_player()
|
||||
if info_generator_container != null:
|
||||
info_generator_container._refresh_generator_ui()
|
||||
|
||||
@@ -1,34 +1,6 @@
|
||||
[gd_scene format=3 uid="uid://jeoiinukrrsp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dtbxopw6ulhl8" path="res://core/generator/currency_generator.gd" id="1_4n4ca"]
|
||||
[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_5tmvy"]
|
||||
[ext_resource type="Resource" uid="uid://co0mcc2kvcpo5" path="res://idles/generators/orb.tres" id="3_wx13b"]
|
||||
[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="4_ruf1h"]
|
||||
[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://generator_container.tscn" id="5_bb14m"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_y5m1q"]
|
||||
size = Vector2(128, 128)
|
||||
|
||||
[node name="CurrencyGenerator" type="Node2D" unique_id=967969064]
|
||||
script = ExtResource("1_4n4ca")
|
||||
currency = ExtResource("2_5tmvy")
|
||||
data = ExtResource("3_wx13b")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=807584513]
|
||||
texture = ExtResource("4_ruf1h")
|
||||
|
||||
[node name="Area2D" type="Area2D" parent="." unique_id=707349247]
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1572620025]
|
||||
shape = SubResource("RectangleShape2D_y5m1q")
|
||||
|
||||
[node name="GeneratorPanel" parent="." unique_id=1129190041 node_paths=PackedStringArray("_generator") instance=ExtResource("5_bb14m")]
|
||||
visible = false
|
||||
offset_left = 33.0
|
||||
offset_top = -65.0
|
||||
offset_right = 474.0
|
||||
offset_bottom = 200.5
|
||||
_generator = NodePath("..")
|
||||
|
||||
[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"]
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
class_name CurrencyGeneratorData
|
||||
extends Resource
|
||||
|
||||
enum UnlockGoalBehavior {
|
||||
AUTOMATIC,
|
||||
MANUAL_BUTTON,
|
||||
}
|
||||
|
||||
@export var id: StringName = &""
|
||||
@export var name: String = ""
|
||||
@export var starts_unlocked: bool = true
|
||||
@@ -13,7 +8,6 @@ enum UnlockGoalBehavior {
|
||||
@export var initial_owned: int = 0
|
||||
@export var purchase_currency: Currency
|
||||
@export var unlock_goal: GoalData
|
||||
@export var unlock_goal_behavior: UnlockGoalBehavior = UnlockGoalBehavior.AUTOMATIC
|
||||
|
||||
## Base cost and exponential growth (part I): cost_next = b * r^owned
|
||||
@export var initial_cost: float = 10.0
|
||||
@@ -37,8 +31,7 @@ enum UnlockGoalBehavior {
|
||||
@export var click_exponent: int = 0
|
||||
@export var click_cooldown_seconds: float = 0.2
|
||||
|
||||
## Data-driven buffs purchasable for this generator.
|
||||
@export var buffs: Array[GeneratorBuffData] = []
|
||||
@export var research_data: ResearchData
|
||||
|
||||
func has_unlock_goal() -> bool:
|
||||
if unlock_goal == null:
|
||||
@@ -46,12 +39,6 @@ func has_unlock_goal() -> bool:
|
||||
|
||||
return bool(unlock_goal.is_valid())
|
||||
|
||||
func is_unlock_goal_met() -> bool:
|
||||
if not has_unlock_goal():
|
||||
return false
|
||||
|
||||
return bool(unlock_goal.is_met())
|
||||
|
||||
func get_unlock_goal_id() -> StringName:
|
||||
if not has_unlock_goal():
|
||||
return &""
|
||||
@@ -62,22 +49,6 @@ func get_unlock_goal_id() -> StringName:
|
||||
|
||||
return StringName(goal_id_text)
|
||||
|
||||
func unlocks_automatically_from_goal() -> bool:
|
||||
return unlock_goal_behavior == UnlockGoalBehavior.AUTOMATIC
|
||||
|
||||
func find_buff(buff_id: StringName) -> GeneratorBuffData:
|
||||
var expected_id: String = String(buff_id).strip_edges()
|
||||
if expected_id.is_empty():
|
||||
return null
|
||||
|
||||
for buff in buffs:
|
||||
if buff == null:
|
||||
continue
|
||||
if String(buff.id) == expected_id:
|
||||
return buff
|
||||
|
||||
return null
|
||||
|
||||
## Returns cost of next generator
|
||||
func cost_next(owned: int) -> float:
|
||||
return cost_for_amount(owned, 1)
|
||||
|
||||
@@ -5,11 +5,13 @@ enum BuffKind {
|
||||
AUTO_PRODUCTION_MULTIPLIER,
|
||||
MANUAL_CLICK_MULTIPLIER,
|
||||
RESOURCE_PURCHASE,
|
||||
RESEARCH_XP_MULTIPLIER,
|
||||
}
|
||||
|
||||
const HUGE_EXPONENT: int = 1000000
|
||||
|
||||
@export var id: StringName = &""
|
||||
@export var target_ids: Array[StringName] = []
|
||||
@export var kind: BuffKind = BuffKind.AUTO_PRODUCTION_MULTIPLIER
|
||||
@export_multiline var text: String = ""
|
||||
@export var icon: Texture2D
|
||||
@@ -23,6 +25,10 @@ const HUGE_EXPONENT: int = 1000000
|
||||
@export var base_effect: float = 1.0
|
||||
@export var effect_increment: float = 0.1
|
||||
|
||||
@export_group("Research XP Multiplier")
|
||||
@export var research_target_id: StringName = &""
|
||||
@export var xp_multiplier_per_level: float = 0.1
|
||||
|
||||
@export_group("Cost")
|
||||
@export var cost_currency: Currency
|
||||
@export var base_cost_mantissa: float = 10.0
|
||||
@@ -35,6 +41,18 @@ const HUGE_EXPONENT: int = 1000000
|
||||
@export var resource_purchase_base_exponent: int = 0
|
||||
@export var resource_purchase_increment_multiplier: float = 1.2
|
||||
|
||||
func targets_generator(generator_id: StringName) -> bool:
|
||||
if target_ids.is_empty():
|
||||
return false
|
||||
|
||||
if target_ids.has("*"):
|
||||
return true
|
||||
|
||||
return target_ids.has(generator_id)
|
||||
|
||||
func get_target_generators() -> Array[StringName]:
|
||||
return target_ids.duplicate()
|
||||
|
||||
func has_unlock_goal() -> bool:
|
||||
if unlock_goal == null:
|
||||
return false
|
||||
@@ -45,10 +63,10 @@ func get_unlock_goal_amount() -> BigNumber:
|
||||
if unlock_goal == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements()
|
||||
if valid_requirements.is_empty():
|
||||
var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements()
|
||||
if requirements.is_empty():
|
||||
return BigNumber.from_float(0.0)
|
||||
var requirement_resource: GoalRequirementData = valid_requirements[0]
|
||||
var requirement_resource: GoalRequirementData = requirements[0]
|
||||
if requirement_resource == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
@@ -62,21 +80,15 @@ func get_unlock_goal_currency() -> Currency:
|
||||
if unlock_goal == null:
|
||||
return null
|
||||
|
||||
var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements()
|
||||
if valid_requirements.is_empty():
|
||||
var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements()
|
||||
if requirements.is_empty():
|
||||
return null
|
||||
var requirement_resource: GoalRequirementData = valid_requirements[0]
|
||||
var requirement_resource: GoalRequirementData = requirements[0]
|
||||
if requirement_resource == null:
|
||||
return null
|
||||
|
||||
return requirement_resource.currency
|
||||
|
||||
func is_unlock_goal_met() -> bool:
|
||||
if unlock_goal == null:
|
||||
return false
|
||||
|
||||
return bool(unlock_goal.is_met())
|
||||
|
||||
func can_purchase_next_level(current_level: int) -> bool:
|
||||
if max_level < 0:
|
||||
return true
|
||||
@@ -126,7 +138,9 @@ func get_effect_description(level: int) -> String:
|
||||
BuffKind.AUTO_PRODUCTION_MULTIPLIER, BuffKind.MANUAL_CLICK_MULTIPLIER:
|
||||
return "x%.2f" % get_effect_multiplier(safe_level)
|
||||
BuffKind.RESOURCE_PURCHASE:
|
||||
return "+%s" % get_resource_purchase_amount_for_level(safe_level).to_string_suffix(2)
|
||||
return "+%s" % get_resource_purchase_amount_for_level(level).to_string_suffix(2)
|
||||
BuffKind.RESEARCH_XP_MULTIPLIER:
|
||||
return "+%.0f%% XP" % ((get_effect_multiplier(safe_level) - 1.0) * 100)
|
||||
_:
|
||||
return "-"
|
||||
|
||||
|
||||
167
core/generator/generator_container.gd
Normal file
167
core/generator/generator_container.gd
Normal file
@@ -0,0 +1,167 @@
|
||||
class_name GeneratorPanel
|
||||
extends Container
|
||||
|
||||
const GENERATOR_BUFF_TILE_SCENE: PackedScene = preload("res://generator_buff_tile.tscn")
|
||||
|
||||
class BuffRow extends RefCounted:
|
||||
var buff: GeneratorBuffData
|
||||
var tile: GeneratorBuffTile
|
||||
|
||||
func _init(row_buff: GeneratorBuffData, row_tile: GeneratorBuffTile) -> void:
|
||||
buff = row_buff
|
||||
tile = row_tile
|
||||
|
||||
@export var _generator: CurrencyGenerator
|
||||
|
||||
@onready var _buy_button: Button = $PanelContainer/VBoxContainer/HBoxContainerGenerators/BuyOneButton
|
||||
@onready var _buy_max_button: Button = $PanelContainer/VBoxContainer/HBoxContainerGenerators/BuyMaxButton
|
||||
@onready var _name_label: Label = $PanelContainer/VBoxContainer/Label
|
||||
@onready var _owned_value: Label = $PanelContainer/VBoxContainer/GeneratorStatsGrid/OwnedValue
|
||||
@onready var _next_cost_value: Label = $PanelContainer/VBoxContainer/GeneratorStatsGrid/NextCostValue
|
||||
@onready var _production_value: Label = $PanelContainer/VBoxContainer/GeneratorStatsGrid/ProductionValue
|
||||
@onready var _buffs_section: VBoxContainer = $PanelContainer/VBoxContainer/BuffsSection
|
||||
@onready var _buffs_list: VBoxContainer = $PanelContainer/VBoxContainer/BuffsSection/BuffsList
|
||||
|
||||
var _buff_rows: Array[BuffRow] = []
|
||||
var _generator_id: StringName = &""
|
||||
|
||||
func _ready() -> void:
|
||||
if _generator == null:
|
||||
push_warning("GeneratorContainer '%s' has no generator assigned." % String(name))
|
||||
return
|
||||
|
||||
_generator_id = _generator.get_generator_id()
|
||||
|
||||
_generator.purchase_completed.connect(_on_generator_updated)
|
||||
_generator.production_tick.connect(_on_generator_updated)
|
||||
_generator.purchase_failed.connect(_on_generator_updated)
|
||||
_generator.buff_purchased.connect(_on_generator_updated)
|
||||
_generator.buff_purchase_failed.connect(_on_generator_updated)
|
||||
|
||||
if _generator.game_state:
|
||||
_generator.game_state.currency_changed.connect(_on_currency_changed)
|
||||
_generator.game_state.generator_state_changed.connect(_on_generator_state_changed)
|
||||
|
||||
_name_label.text = _generator.data.name if _generator.data != null else "Generator"
|
||||
_build_buff_rows()
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _on_buy_pressed() -> void:
|
||||
_generator.buy(1)
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _on_buy_max_pressed() -> void:
|
||||
_generator.buy_max()
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _on_debug_income_pressed() -> void:
|
||||
_generator.grant_currency(BigNumber.from_float(100.0))
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _on_currency_changed(_changed_currency_id: StringName, _new_value: BigNumber) -> void:
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _on_generator_updated(_a = null, _b = null, _c = null, _d = null) -> void:
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) -> void:
|
||||
if generator_id == _generator_id:
|
||||
visible = true
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _on_buy_buff_pressed(buff_id: StringName) -> void:
|
||||
_generator.buy_buff(buff_id)
|
||||
_refresh_generator_ui()
|
||||
|
||||
func _build_buff_rows() -> void:
|
||||
_buff_rows.clear()
|
||||
for child in _buffs_list.get_children():
|
||||
child.queue_free()
|
||||
|
||||
for buff in _generator.get_buffs():
|
||||
if buff == null:
|
||||
continue
|
||||
|
||||
var buff_id_text: String = String(buff.id).strip_edges()
|
||||
if buff_id_text.is_empty():
|
||||
continue
|
||||
|
||||
var tile: GeneratorBuffTile = GENERATOR_BUFF_TILE_SCENE.instantiate() as GeneratorBuffTile
|
||||
if tile == null:
|
||||
push_warning("Failed to instantiate GeneratorBuffTile for buff '%s'." % buff_id_text)
|
||||
continue
|
||||
|
||||
_buffs_list.add_child(tile)
|
||||
tile.set_buff(buff)
|
||||
tile.buy_pressed.connect(_on_buy_buff_pressed)
|
||||
_buff_rows.append(BuffRow.new(buff, tile))
|
||||
|
||||
func _refresh_buff_rows(can_interact: bool) -> void:
|
||||
_buffs_section.visible = not _buff_rows.is_empty()
|
||||
if _buff_rows.is_empty():
|
||||
return
|
||||
|
||||
for row in _buff_rows:
|
||||
if row == null or row.buff == null or row.tile == null:
|
||||
continue
|
||||
|
||||
var buff: GeneratorBuffData = row.buff
|
||||
var buff_unlocked: bool = _generator.is_buff_unlocked(buff.id)
|
||||
if not buff_unlocked:
|
||||
var locked_hint: String = "Locked"
|
||||
var unlock_goal_currency: Currency = buff.get_unlock_goal_currency()
|
||||
if buff.has_unlock_goal() and unlock_goal_currency != null and _generator.game_state:
|
||||
var goal_currency_id: StringName = _generator.game_state.get_currency_id(unlock_goal_currency)
|
||||
var currency_name: String = _generator.game_state.get_currency_name(goal_currency_id)
|
||||
var required_text: String = buff.get_unlock_goal_amount().to_string_suffix(2)
|
||||
locked_hint = "Unlock at %s %s" % [required_text, currency_name]
|
||||
|
||||
row.tile.set_runtime(0, "Locked", locked_hint, false, false)
|
||||
continue
|
||||
|
||||
var level: int = _generator.get_buff_level(buff.id)
|
||||
var effect_text: String = buff.get_effect_description(level)
|
||||
|
||||
if _generator.is_buff_maxed(buff.id):
|
||||
row.tile.set_runtime(level, effect_text, "", false, true)
|
||||
continue
|
||||
|
||||
var cost: BigNumber = _generator.get_buff_cost(buff.id)
|
||||
var cost_currency_id: StringName = _generator.get_buff_cost_currency_id(buff.id)
|
||||
var currency_label: String = _generator.game_state.get_currency_name(cost_currency_id) if _generator.game_state else "Unknown"
|
||||
var cost_text: String = "%s %s" % [cost.to_string_suffix(2), currency_label]
|
||||
var can_buy: bool = can_interact and _generator.can_buy_buff(buff.id)
|
||||
row.tile.set_runtime(level, effect_text, cost_text, can_buy, false)
|
||||
|
||||
func _refresh_generator_ui() -> void:
|
||||
if _buy_button == null or _buy_max_button == null:
|
||||
return
|
||||
|
||||
if _generator == null:
|
||||
return
|
||||
|
||||
var can_interact: bool = _generator.is_available_to_player()
|
||||
|
||||
_buy_button.disabled = not can_interact
|
||||
_buy_max_button.disabled = not can_interact
|
||||
|
||||
_owned_value.text = str(_generator.owned)
|
||||
if can_interact:
|
||||
var purchase_currency_id: StringName = _generator.get_purchase_currency_id()
|
||||
var purchase_currency_name: String = _generator.game_state.get_currency_name(purchase_currency_id) if _generator.game_state else "Unknown"
|
||||
if purchase_currency_name.is_empty():
|
||||
purchase_currency_name = "Unconfigured"
|
||||
_next_cost_value.text = "%s %s" % [_generator.get_next_cost().to_string_suffix(2), purchase_currency_name]
|
||||
else:
|
||||
_next_cost_value.text = "-"
|
||||
|
||||
var production_per_second: float = 0.0
|
||||
if _generator.data != null and can_interact:
|
||||
production_per_second = _generator.data.production_per_second(
|
||||
_generator.owned,
|
||||
_generator.get_effective_auto_run_multiplier(),
|
||||
_generator.purchased_count
|
||||
)
|
||||
|
||||
_production_value.text = BigNumber.from_float(production_per_second).to_string_suffix(2)
|
||||
_refresh_buff_rows(can_interact)
|
||||
1
core/generator/generator_container.gd.uid
Normal file
1
core/generator/generator_container.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://es14nqk6vrrk
|
||||
85
core/generator/generator_container.tscn
Normal file
85
core/generator/generator_container.tscn
Normal file
@@ -0,0 +1,85 @@
|
||||
[gd_scene format=3 uid="uid://ckos7f22pnmyh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://es14nqk6vrrk" path="res://core/generator/generator_container.gd" id="1_8wouw"]
|
||||
|
||||
[node name="GeneratorContainer" type="PanelContainer" unique_id=1451609580]
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -132.0
|
||||
offset_top = -82.5
|
||||
offset_right = 132.0
|
||||
offset_bottom = 87.5
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_8wouw")
|
||||
|
||||
[node name="PanelContainer" type="MarginContainer" parent="." unique_id=370221865]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer" unique_id=1139263929]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="PanelContainer/VBoxContainer" unique_id=1867673612]
|
||||
layout_mode = 2
|
||||
text = "Generator"
|
||||
|
||||
[node name="HBoxContainerGenerators" type="HBoxContainer" parent="PanelContainer/VBoxContainer" unique_id=2033456807]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="BuyOneButton" type="Button" parent="PanelContainer/VBoxContainer/HBoxContainerGenerators" unique_id=1171095139]
|
||||
layout_mode = 2
|
||||
text = "Buy x1"
|
||||
|
||||
[node name="BuyMaxButton" type="Button" parent="PanelContainer/VBoxContainer/HBoxContainerGenerators" unique_id=871628534]
|
||||
layout_mode = 2
|
||||
text = "Buy Max"
|
||||
|
||||
[node name="GeneratorStatsGrid" type="GridContainer" parent="PanelContainer/VBoxContainer" unique_id=745078076]
|
||||
layout_mode = 2
|
||||
columns = 2
|
||||
|
||||
[node name="OwnedTitle" type="Label" parent="PanelContainer/VBoxContainer/GeneratorStatsGrid" unique_id=891417119]
|
||||
layout_mode = 2
|
||||
text = "Owned"
|
||||
|
||||
[node name="OwnedValue" type="Label" parent="PanelContainer/VBoxContainer/GeneratorStatsGrid" unique_id=549086894]
|
||||
layout_mode = 2
|
||||
text = "0"
|
||||
|
||||
[node name="NextCostTitle" type="Label" parent="PanelContainer/VBoxContainer/GeneratorStatsGrid" unique_id=303459589]
|
||||
layout_mode = 2
|
||||
text = "Next Cost"
|
||||
|
||||
[node name="NextCostValue" type="Label" parent="PanelContainer/VBoxContainer/GeneratorStatsGrid" unique_id=50480604]
|
||||
layout_mode = 2
|
||||
text = "0"
|
||||
|
||||
[node name="ProductionTitle" type="Label" parent="PanelContainer/VBoxContainer/GeneratorStatsGrid" unique_id=2139535774]
|
||||
layout_mode = 2
|
||||
text = "Production / sec"
|
||||
|
||||
[node name="ProductionValue" type="Label" parent="PanelContainer/VBoxContainer/GeneratorStatsGrid" unique_id=938669936]
|
||||
layout_mode = 2
|
||||
text = "0"
|
||||
|
||||
[node name="BuffsSection" type="VBoxContainer" parent="PanelContainer/VBoxContainer" unique_id=640102105]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="BuffsTitle" type="Label" parent="PanelContainer/VBoxContainer/BuffsSection" unique_id=1169271241]
|
||||
layout_mode = 2
|
||||
text = "Buffs"
|
||||
|
||||
[node name="BuffsList" type="VBoxContainer" parent="PanelContainer/VBoxContainer/BuffsSection" unique_id=1221547661]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/HBoxContainerGenerators/BuyOneButton" to="." method="_on_buy_pressed"]
|
||||
[connection signal="pressed" from="PanelContainer/VBoxContainer/HBoxContainerGenerators/BuyMaxButton" to="." method="_on_buy_max_pressed"]
|
||||
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
|
||||
Reference in New Issue
Block a user