Add docs
This commit is contained in:
117
core/README.md
Normal file
117
core/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Core Module Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The `core/` directory contains the fundamental systems for the idle game. It provides:
|
||||
|
||||
- **Numeric Foundation**: `BigNumber` for handling huge idle game values
|
||||
- **Game State Management**: `GameState` autoload for all persistent state
|
||||
- **Currency System**: Database and tracking for multiple currency types
|
||||
- **Generator System**: Automated resource production with scaling costs
|
||||
- **Buff System**: Upgrades that modify generator behavior
|
||||
- **Goal System**: Achievement tracking and unlock triggers
|
||||
- **Prestige System**: Rebirth mechanics with permanent multipliers
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
core/
|
||||
├── big_number.gd # Core numeric API
|
||||
├── game_state.gd # Main state management autoload
|
||||
├── buff_database.gd # Buff registration system
|
||||
├── currency_database.gd # Currency registration system
|
||||
├── currency/ # Currency data structures
|
||||
├── generator/ # Generator logic and data
|
||||
├── goals/ # Goal/achievement system
|
||||
└── prestige/ # Prestige/rebirth system
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Game Start**: `GameState._ready()` initializes currencies, loads save
|
||||
2. **Player Action**: Generator bought → `GameState` updated → signals emitted
|
||||
3. **UI Reaction**: Listeners respond to signals → update display
|
||||
4. **Goal Check**: Currency changed → goals evaluated → buffs unlocked
|
||||
5. **Save**: `GameState.save_game()` → JSON to `user://idle_save.json`
|
||||
|
||||
## Save Format
|
||||
|
||||
**Version**: 3
|
||||
|
||||
```json
|
||||
{
|
||||
"save_format_version": 3,
|
||||
"currencies": {
|
||||
"<currency_id>": {
|
||||
"current": {"m": float, "e": int},
|
||||
"total": {"m": float, "e": int},
|
||||
"all_time": {"m": float, "e": int}
|
||||
}
|
||||
},
|
||||
"generator_states": {
|
||||
"<generator_id>": {
|
||||
"owned": int,
|
||||
"purchased_count": int,
|
||||
"unlocked": bool,
|
||||
"available": bool
|
||||
}
|
||||
},
|
||||
"buff_levels": {
|
||||
"<buff_id>": int
|
||||
},
|
||||
"buff_unlocked": {
|
||||
"<buff_id>": true
|
||||
},
|
||||
"buff_active": {
|
||||
"<buff_id>": true
|
||||
},
|
||||
"goals": {
|
||||
"completed": ["<goal_id>", ...]
|
||||
},
|
||||
"last_save_time": unix_timestamp
|
||||
}
|
||||
```
|
||||
|
||||
## Key Signals
|
||||
|
||||
| Signal | Emitted When |
|
||||
|--------|--------------|
|
||||
| `currency_changed(currency_id, new_amount)` | Any currency balance changes |
|
||||
| `generator_state_changed(generator_id, state)` | Generator owned/unlocked changes |
|
||||
| `generator_buff_level_changed(gen_id, buff_id, level)` | Buff level increased |
|
||||
| `buff_unlocked_changed(buff_id, unlocked)` | Buff unlock state changed |
|
||||
| `goal_completed(goal_id)` | Goal criteria met |
|
||||
|
||||
## BigNumber Serialization
|
||||
|
||||
All large numbers use scientific notation internally:
|
||||
```gdscript
|
||||
# Value = mantissa * 10^exponent
|
||||
var num = BigNumber.new(1.5, 24) # 1.5e24
|
||||
var dict = num.serialize() # {"m": 1.5, "e": 24}
|
||||
var restored = BigNumber.deserialize(dict)
|
||||
```
|
||||
|
||||
## Extending the Core
|
||||
|
||||
### Adding a New Currency
|
||||
1. Create `res://sandbox/currencies/<name>.tres` extending `Currency`
|
||||
2. Set `id`, `display_name`, and `icon`
|
||||
3. Auto-discovered by `CurrencyDatabase`
|
||||
|
||||
### Adding a New Generator
|
||||
1. Create `res://sandbox/generators/<name>.tres` extending `CurrencyGeneratorData`
|
||||
2. Configure costs, production, and unlock goals
|
||||
3. Instance `CurrencyGenerator` scene with this data
|
||||
|
||||
### Adding a New Buff
|
||||
1. Create `res://sandbox/buffs/<name>.tres` extending `GeneratorBuffData`
|
||||
2. Set `target_ids`, `kind`, `effect_increment`, and `cost`
|
||||
3. Auto-discovered by `BuffDatabase`
|
||||
|
||||
## See Also
|
||||
|
||||
- `currency/` - Currency data structures
|
||||
- `generator/` - Generator production system
|
||||
- `goals/` - Achievement system
|
||||
- `prestige/` - Rebirth mechanics
|
||||
84
core/currency/README.md
Normal file
84
core/currency/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Currency Module Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The `currency/` subfolder contains the data structure for defining in-game currencies.
|
||||
|
||||
## Files
|
||||
|
||||
### `currency.gd`
|
||||
|
||||
Defines the `Currency` class - a Resource for currency metadata.
|
||||
|
||||
## Currency Class
|
||||
|
||||
```gdscript
|
||||
class_name Currency
|
||||
extends Resource
|
||||
|
||||
@export var id: StringName = &"" # Unique identifier (e.g., &"gold")
|
||||
@export var display_name: String = "" # UI-friendly name
|
||||
@export var icon: Texture2D # Display icon
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Defining a Currency
|
||||
|
||||
Create a `.tres` resource file with:
|
||||
- **id**: Lowercase unique key (used internally)
|
||||
- **display_name**: What players see ("Gold", "Gems")
|
||||
- **icon**: Texture for UI display
|
||||
|
||||
Example `res://sandbox/currencies/gold.tres`:
|
||||
```
|
||||
[gd_resource type="Resource" script_class="Currency"]
|
||||
[resource]
|
||||
id = &"gold"
|
||||
display_name = "Gold"
|
||||
icon = ExtResource("uid://...")
|
||||
```
|
||||
|
||||
### Accessing Currencies
|
||||
|
||||
```gdscript
|
||||
# Get all known currencies
|
||||
var currencies: Array[Currency] = CurrencyDatabase.get_known_currencies()
|
||||
|
||||
# Get currency by ID
|
||||
var gold: Currency = CurrencyDatabase.get_currency_resource(&"gold")
|
||||
|
||||
# Check if currency exists
|
||||
var is_valid: bool = CurrencyDatabase.is_known_currency_id(&"gems")
|
||||
|
||||
# Get display name
|
||||
var name: String = CurrencyDatabase.get_currency_name(&"gold")
|
||||
```
|
||||
|
||||
## Integration with GameState
|
||||
|
||||
Currencies are tracked in three ways:
|
||||
1. **current**: Current balance (resets on prestige)
|
||||
2. **total**: Total acquired this run (resets on prestige)
|
||||
3. **all_time**: Lifetime total (never resets)
|
||||
|
||||
```gdscript
|
||||
# Add currency
|
||||
GameState.add_currency(gold_resource, BigNumber.new(100.0, 0))
|
||||
|
||||
# Get balance
|
||||
var balance: BigNumber = GameState.get_currency_amount_by_id(&"gold")
|
||||
|
||||
# Spend currency (returns false if insufficient)
|
||||
var success: bool = GameState.spend_currency_by_id(&"gold", BigNumber.new(50.0, 0))
|
||||
```
|
||||
|
||||
## Auto-Discovery
|
||||
|
||||
`CurrencyDatabase` automatically scans `res://sandbox/currencies/` for `.tres` and `.res` files on startup.
|
||||
|
||||
## See Also
|
||||
|
||||
- `core/game_state.gd` - Currency state management
|
||||
- `core/big_number.gd` - Large number handling
|
||||
- `generator/currency_generator_data.gd` - Generator purchase currency
|
||||
@@ -721,7 +721,10 @@ func _get_generator_state(generator_id: StringName) -> Dictionary:
|
||||
var existing_raw: Variant = generator_states.get(key, _default_generator_state(false, false, 0))
|
||||
if existing_raw is Dictionary:
|
||||
var state: Dictionary = existing_raw
|
||||
var sanitized: Dictionary = _sanitize_generator_state(state, false, false, 0)
|
||||
var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false)
|
||||
var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false)
|
||||
var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0)
|
||||
var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned)
|
||||
generator_states[key] = sanitized
|
||||
return sanitized
|
||||
|
||||
@@ -734,7 +737,10 @@ func _set_generator_state(generator_id: StringName, state: Dictionary) -> void:
|
||||
if key.is_empty():
|
||||
return
|
||||
|
||||
var sanitized: Dictionary = _sanitize_generator_state(state, false, false, 0)
|
||||
var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false)
|
||||
var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false)
|
||||
var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0)
|
||||
var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned)
|
||||
generator_states[key] = sanitized
|
||||
generator_state_changed.emit(StringName(key), sanitized.duplicate(true))
|
||||
|
||||
@@ -1098,9 +1104,30 @@ func _try_complete_goal(goal_id: StringName) -> void:
|
||||
if _goal_completed[goal_id]:
|
||||
return
|
||||
if goal.is_met():
|
||||
if _is_goal_manual_unlock(goal_id):
|
||||
return
|
||||
_goal_completed[goal_id] = true
|
||||
goal_completed.emit(goal_id)
|
||||
|
||||
func _is_goal_manual_unlock(goal_id: StringName) -> bool:
|
||||
var goal: GoalData = _goal_definitions.get(goal_id, null)
|
||||
if goal == null or not goal.has_id():
|
||||
return false
|
||||
|
||||
var scene_root: Node = get_tree().current_scene
|
||||
if scene_root == null:
|
||||
return false
|
||||
|
||||
var gen_nodes: Array[Node] = scene_root.find_children("*", "CurrencyGenerator", true, false)
|
||||
for node in gen_nodes:
|
||||
if node.has_method("get_generator_id") and node.has_method("data"):
|
||||
var data: Variant = node.get("data")
|
||||
if data != null and data.has_method("has_unlock_goal") and data.has_method("unlocks_automatically_from_goal"):
|
||||
if data.has_unlock_goal() and data.get_unlock_goal_id() == goal_id:
|
||||
return not data.unlocks_automatically_from_goal()
|
||||
|
||||
return false
|
||||
|
||||
func _serialize_goals() -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
var completed_goals: Array[StringName] = []
|
||||
|
||||
204
core/generator/README.md
Normal file
204
core/generator/README.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
# 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
|
||||
|
||||
## Prestige Integration
|
||||
|
||||
Generators automatically apply prestige multipliers:
|
||||
```gdscript
|
||||
effective_multiplier = run_multiplier * buff_multiplier * prestige_multiplier
|
||||
```
|
||||
|
||||
On prestige:
|
||||
1. `reset_runtime_state_for_prestige()` called
|
||||
2. `cycle_progress_seconds` reset
|
||||
3. Buff levels reset to 0
|
||||
4. Unlocks re-evaluated from goals
|
||||
|
||||
## See Also
|
||||
|
||||
- `core/game_state.gd` - Generator state storage
|
||||
- `goals/goal_data.gd` - Unlock conditions
|
||||
- `prestige/prestige_manager.gd` - Multiplier application
|
||||
@@ -96,12 +96,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)
|
||||
|
||||
# Register with default state first, then evaluate unlock goal
|
||||
_ensure_registered()
|
||||
if data != null and data.unlocks_automatically_from_goal():
|
||||
_evaluate_generator_unlock_goal(false)
|
||||
|
||||
## Updates click cooldown/click grants and runs automatic production cycles.
|
||||
func _process(delta: float) -> void:
|
||||
@@ -319,7 +322,8 @@ 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)
|
||||
if data != null and data.unlocks_automatically_from_goal():
|
||||
_evaluate_generator_unlock_goal(false)
|
||||
#_evaluate_buff_unlock_goals()
|
||||
visible = is_available_to_player()
|
||||
|
||||
@@ -579,7 +583,8 @@ func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
|
||||
|
||||
func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
|
||||
GameState.evaluate_all_goals()
|
||||
_evaluate_generator_unlock_goal(false)
|
||||
if data != null and data.unlocks_automatically_from_goal():
|
||||
_evaluate_generator_unlock_goal(false)
|
||||
|
||||
func try_unlock_from_goal() -> bool:
|
||||
return _evaluate_generator_unlock_goal(true)
|
||||
@@ -595,12 +600,17 @@ func _evaluate_generator_unlock_goal(allow_manual_unlock: bool) -> bool:
|
||||
return false
|
||||
|
||||
var goal_id: StringName = data.get_unlock_goal_id()
|
||||
if goal_id.is_empty() or not GameState.is_goal_completed(goal_id):
|
||||
if goal_id.is_empty():
|
||||
return false
|
||||
|
||||
var goal_completed: bool = GameState.is_goal_completed(goal_id)
|
||||
if not goal_completed:
|
||||
return false
|
||||
|
||||
GameState.set_generator_unlocked(_generator_id, true)
|
||||
GameState.set_generator_available(_generator_id, true)
|
||||
goal_achieved.emit(_generator_id, goal_id)
|
||||
_on_generated_state_changed(_generator_id, GameState._get_generator_state(_generator_id))
|
||||
return true
|
||||
|
||||
## Default unlocked state loaded from generator data.
|
||||
@@ -634,3 +644,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()
|
||||
|
||||
@@ -147,7 +147,14 @@ func _refresh_buff_rows(can_interact: bool) -> void:
|
||||
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
|
||||
|
||||
|
||||
167
core/goals/README.md
Normal file
167
core/goals/README.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Goals Module Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The `goals/` subfolder contains the achievement system. Goals track player progress and can unlock generators or buffs when conditions are met.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `goal_data.gd` | Goal definition resource |
|
||||
| `goal_requirement_data.gd` | Individual requirement criteria |
|
||||
|
||||
## GoalData
|
||||
|
||||
Resource class defining an achievement.
|
||||
|
||||
### Properties
|
||||
|
||||
```gdscript
|
||||
class_name GoalData
|
||||
extends Resource
|
||||
|
||||
@export var id: StringName = &"" # Unique identifier
|
||||
@export var requirements: Array[GoalRequirementData] # Conditions to complete
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```gdscript
|
||||
func is_valid() -> bool # Has id and valid requirements
|
||||
func is_met() -> bool # All requirements satisfied
|
||||
func get_progress() -> float # 0.0 to 1.0 based on closest requirement
|
||||
func get_first_requirement_summary() -> String # "100 Gold" format
|
||||
```
|
||||
|
||||
## GoalRequirementData
|
||||
|
||||
Single condition within a goal.
|
||||
|
||||
### Properties
|
||||
|
||||
```gdscript
|
||||
class_name GoalRequirementData
|
||||
extends Resource
|
||||
|
||||
@export var currency: Currency # Currency to track
|
||||
@export var amount_mantissa: float = 0.0 # Required amount (scientific notation)
|
||||
@export var amount_exponent: int = 0
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```gdscript
|
||||
func get_amount() -> BigNumber # Required amount
|
||||
func is_met() -> bool # Current >= required
|
||||
func get_progress() -> float # Log-scaled 0.0-1.0
|
||||
func get_summary_text() -> String # "100 Gold" format
|
||||
```
|
||||
|
||||
### Progress Calculation
|
||||
|
||||
Uses **logarithmic scaling** for better visual feedback on huge numbers:
|
||||
|
||||
```gdscript
|
||||
# Convert to log space
|
||||
current_log = log(current_mantissa) / log(10) + current_exponent
|
||||
required_log = log(required_mantissa) / log(10) + required_exponent
|
||||
|
||||
# Progress ratio
|
||||
progress = clampf(current_log / required_log, 0.0, 1.0)
|
||||
```
|
||||
|
||||
This means reaching 50% progress on a 1e100 goal requires ~1e50 currency.
|
||||
|
||||
## Goal Evaluation
|
||||
|
||||
### Automatic Checking
|
||||
|
||||
`GameState` evaluates goals whenever currency changes:
|
||||
|
||||
```gdscript
|
||||
func _on_currency_changed(currency_id, new_amount):
|
||||
GameState.evaluate_all_goals()
|
||||
```
|
||||
|
||||
### Completion Flow
|
||||
|
||||
1. Currency updated → signal emitted
|
||||
2. `GameState.evaluate_all_goals()` called
|
||||
3. Each uncompleted goal checked via `is_met()`
|
||||
4. If met → `goal_completed` signal emitted
|
||||
5. Buffs with unlock goals checked → unlocked if goal complete
|
||||
|
||||
### Manual Unlock vs Automatic
|
||||
|
||||
Generators can be configured to:
|
||||
- **Automatic**: Unlock immediately when goal completes
|
||||
- **Manual**: Goal completion enables a button player must click
|
||||
|
||||
## Usage Example
|
||||
|
||||
### Defining a Goal
|
||||
|
||||
Create `res://sandbox/goals/first_million.tres`:
|
||||
|
||||
```
|
||||
[gd_resource type="Resource" script_class="GoalData"]
|
||||
[resource]
|
||||
id = &"first_million"
|
||||
requirements = [
|
||||
preload("res://sandbox/goals/first_million_req.tres")
|
||||
]
|
||||
```
|
||||
|
||||
### Defining a Requirement
|
||||
|
||||
Create `res://sandbox/goals/first_million_req.tres`:
|
||||
|
||||
```
|
||||
[gd_resource type="Resource" script_class="GoalRequirementData"]
|
||||
[resource]
|
||||
currency = preload("res://sandbox/currencies/gold.tres")
|
||||
amount_mantissa = 1.0
|
||||
amount_exponent = 6 # 1,000,000
|
||||
```
|
||||
|
||||
### Connecting to Generator
|
||||
|
||||
In `CurrencyGeneratorData`:
|
||||
```gdscript
|
||||
@export var unlock_goal: GoalData # Assign goal resource
|
||||
@export var unlock_goal_behavior: UnlockGoalBehavior = AUTOMATIC
|
||||
```
|
||||
|
||||
## Signals
|
||||
|
||||
```gdscript
|
||||
signal goal_completed(goal_id: StringName)
|
||||
signal goal_progress_changed(goal_id: StringName, progress: float)
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### With GameState
|
||||
- Goals auto-discovered from `res://sandbox/goals/`
|
||||
- Completion state saved to JSON
|
||||
- Buff unlock goals checked automatically
|
||||
|
||||
### With Buffs
|
||||
```gdscript
|
||||
# In GeneratorBuffData
|
||||
@export var unlock_goal: GoalData
|
||||
|
||||
# When goal completes, buff auto-unlocks:
|
||||
GameState._try_unlock_buff_from_goal(buff)
|
||||
```
|
||||
|
||||
### With Prestige
|
||||
- Goal completion state persists through prestige
|
||||
- Reset via `GameState.reset_for_prestige()`
|
||||
|
||||
## See Also
|
||||
|
||||
- `core/game_state.gd` - Goal state management
|
||||
- `generator/currency_generator_data.gd` - Generator unlock goals
|
||||
- `generator/generator_buff_data.gd` - Buff unlock goals
|
||||
244
core/prestige/README.md
Normal file
244
core/prestige/README.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# Prestige Module Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The `prestige/` subfolder implements the rebirth/reset system. Players can reset progress in exchange for a permanent multiplier that accelerates future runs.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `prestige_config.gd` | Configuration resource for prestige rules |
|
||||
| `prestige_manager.gd` | Core logic and state management |
|
||||
| `prestige_panel.gd` | UI panel for prestige interaction |
|
||||
| `TECH_SPEC.md` | Detailed technical specification |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
PrestigeManager (autoload node)
|
||||
├── Tracks prestige currency
|
||||
├── Calculates pending gain
|
||||
├── Applies multipliers to generators
|
||||
└── PrestigePanel (UI)
|
||||
├── Shows current/pending prestige
|
||||
└── Trigger reset button
|
||||
```
|
||||
|
||||
## PrestigeConfig
|
||||
|
||||
Resource defining prestige rules.
|
||||
|
||||
### Enums
|
||||
|
||||
```gdscript
|
||||
enum BasisType {
|
||||
LIFETIME_TOTAL, # All-time currency acquired
|
||||
RUN_TOTAL, # Currency this run
|
||||
RUN_MAX # Peak currency this run
|
||||
}
|
||||
|
||||
enum FormulaType {
|
||||
POWER, # gain = scale * (ratio^exponent)
|
||||
TRIANGULAR_INVERSE # gain based on triangular number inverse
|
||||
}
|
||||
|
||||
enum RoundingMode {
|
||||
FLOOR, ROUND, CEIL
|
||||
}
|
||||
|
||||
enum MultiplierMode {
|
||||
ADDITIVE, # +X per prestige
|
||||
MULTIPLICATIVE_POWER # x^(multiplier_per_prestige)
|
||||
}
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
```gdscript
|
||||
@export var id: StringName = &"primary"
|
||||
@export var display_name: String = "Ascension"
|
||||
@export var prestige_currency_id: StringName = &"prestige"
|
||||
@export var source_currency_id: StringName = &"magic" # What to measure
|
||||
@export var basis: BasisType = LIFETIME_TOTAL
|
||||
@export var formula: FormulaType = POWER
|
||||
|
||||
# Threshold: minimum source currency needed
|
||||
@export var threshold_mantissa: float = 1.0
|
||||
@export var threshold_exponent: int = 4 # 1e4 = 10,000
|
||||
|
||||
# Formula parameters
|
||||
@export var scale: float = 1.0
|
||||
@export var exponent: float = 0.5 # Square root
|
||||
|
||||
# Multiplier growth
|
||||
@export var multiplier_per_prestige: float = 0.05 # +5% per prestige
|
||||
@export var multiplier_mode: MultiplierMode = ADDITIVE
|
||||
```
|
||||
|
||||
## PrestigeManager
|
||||
|
||||
Autoload node managing prestige state.
|
||||
|
||||
### Signals
|
||||
|
||||
```gdscript
|
||||
signal prestige_state_changed(total_prestige, pending_gain)
|
||||
signal prestige_performed(gain, total_prestige)
|
||||
```
|
||||
|
||||
### State Variables
|
||||
|
||||
```gdscript
|
||||
var total_prestige_earned: BigNumber # Lifetime prestige
|
||||
var current_prestige_unspent: BigNumber # Available to spend
|
||||
var prestige_resets_count: int # How many resets
|
||||
var run_start_total_source_currency: BigNumber # Track run start
|
||||
var run_peak_source_currency: BigNumber # Peak this run
|
||||
```
|
||||
|
||||
### Key Methods
|
||||
|
||||
```gdscript
|
||||
# Query state
|
||||
func get_total_prestige() -> BigNumber
|
||||
func calculate_pending_gain() -> BigNumber
|
||||
func can_prestige() -> bool
|
||||
func get_total_multiplier() -> float
|
||||
|
||||
# Perform prestige
|
||||
func perform_prestige() -> bool
|
||||
```
|
||||
|
||||
### Gain Calculation
|
||||
|
||||
**Power Formula** (default):
|
||||
```gdscript
|
||||
ratio = basis_value / threshold
|
||||
raw_gain = scale * (ratio^exponent) + flat_bonus
|
||||
|
||||
# Cumulative basis subtracts already earned
|
||||
if basis == LIFETIME_TOTAL:
|
||||
gain = raw_gain - total_prestige_earned
|
||||
```
|
||||
|
||||
**Triangular Inverse Formula**:
|
||||
```gdscript
|
||||
# Based on inverse triangular number: n = (sqrt(1 + 8k) - 1) / 2
|
||||
k = basis_value / threshold
|
||||
target_total = (sqrt(1 + 8k) - 1) * 0.5
|
||||
|
||||
if basis == LIFETIME_TOTAL:
|
||||
gain = target_total - total_prestige_earned
|
||||
```
|
||||
|
||||
### Multiplier Application
|
||||
|
||||
**Additive Mode**:
|
||||
```gdscript
|
||||
multiplier = base_multiplier + (total_prestige * multiplier_per_prestige)
|
||||
# e.g., 1.0 + (5 * 0.05) = 1.25 (25% bonus)
|
||||
```
|
||||
|
||||
**Multiplicative Power Mode**:
|
||||
```gdscript
|
||||
growth_base = 1.0 + multiplier_per_prestige
|
||||
multiplier = base_multiplier * (growth_base ^ total_prestige)
|
||||
# e.g., 1.0 * (1.05 ^ 5) = 1.276 (27.6% bonus)
|
||||
```
|
||||
|
||||
### Prestige Reset Flow
|
||||
|
||||
1. Player clicks "Prestige" button
|
||||
2. `perform_prestige()` called
|
||||
3. Gain calculated and added to total
|
||||
4. `GameState.reset_for_prestige()` called:
|
||||
- Current currencies reset to 0
|
||||
- Generator states cleared
|
||||
- Buff levels reset to 0
|
||||
- Goal completion cleared
|
||||
- Lifetime totals preserved
|
||||
5. Generators reinitialized
|
||||
6. Save triggered
|
||||
|
||||
## PrestigePanel
|
||||
|
||||
UI component for prestige interaction.
|
||||
|
||||
### Displays
|
||||
|
||||
```gdscript
|
||||
_total_value.text = "123.45" # Total prestige earned
|
||||
_pending_value.text = "56.78" # Gain available now
|
||||
_multiplier_value.text = "x2.34" # Current multiplier
|
||||
_basis_label.text = "Lifetime Total (1.50e6 Magic)"
|
||||
```
|
||||
|
||||
### Buttons
|
||||
|
||||
- **Reset**: Triggers `perform_prestige()` after confirmation
|
||||
- **Save**: Manually saves game state
|
||||
|
||||
## Integration with Generators
|
||||
|
||||
Generators apply prestige multipliers automatically:
|
||||
|
||||
```gdscript
|
||||
func get_effective_auto_run_multiplier() -> float:
|
||||
return run_multiplier * buff_multiplier * _get_prestige_multiplier()
|
||||
|
||||
func _get_prestige_multiplier() -> float:
|
||||
var manager = get_node_or_null("/root/PrestigeManager")
|
||||
if manager:
|
||||
return manager.get_total_multiplier()
|
||||
return 1.0
|
||||
```
|
||||
|
||||
## Save Integration
|
||||
|
||||
Prestige state stored in `GameState` external save:
|
||||
|
||||
```json
|
||||
{
|
||||
"prestige": {
|
||||
"total_prestige_earned": {"m": 10.0, "e": 0},
|
||||
"current_prestige_unspent": {"m": 5.0, "e": 0},
|
||||
"prestige_resets_count": 3,
|
||||
"run_start_total_source_currency": {"m": 1000.0, "e": 0},
|
||||
"run_peak_source_currency": {"m": 5000.0, "e": 0},
|
||||
"last_reset_time": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
For a typical idle game:
|
||||
|
||||
```gdscript
|
||||
# Primary prestige currency
|
||||
id = &"primary"
|
||||
display_name = "Ascension"
|
||||
source_currency_id = &"magic"
|
||||
|
||||
# Based on lifetime total of Magic currency
|
||||
basis = LIFETIME_TOTAL
|
||||
|
||||
# Need 1e6 Magic to get first prestige
|
||||
threshold_mantissa = 1.0
|
||||
threshold_exponent = 6
|
||||
|
||||
# Square root scaling (diminishing returns)
|
||||
formula = POWER
|
||||
exponent = 0.5
|
||||
|
||||
# +10% multiplier per prestige point
|
||||
multiplier_mode = ADDITIVE
|
||||
multiplier_per_prestige = 0.10
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `core/game_state.gd` - State persistence
|
||||
- `generator/currency_generator.gd` - Multiplier application
|
||||
- `core/big_number.gd` - Large number math
|
||||
Reference in New Issue
Block a user