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)
468 lines
16 KiB
Markdown
468 lines
16 KiB
Markdown
# Prestige Module Documentation
|
|
|
|
## Overview
|
|
|
|
The `prestige/` subfolder implements the rebirth/reset system and the prestige buff graph — permanent upgrades purchasable with prestige currency that persist across resets.
|
|
|
|
## Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `prestige_config.gd` | Configuration resource for prestige rules (`PrestigeConfig`) |
|
|
| `prestige_manager.gd` | Core prestige state, gain calculation, and reset orchestration (`PrestigeManager`) |
|
|
| `prestige_buff_node.gd` | Single node in the prestige buff graph (`PrestigeBuffNode`) |
|
|
| `prestige_buff_catalogue.gd` | Flat list of all prestige buff nodes (`PrestigeBuffCatalogue`) |
|
|
| `prestige_buff_graph_tile.gd` | Individual tile UI for a buff node (`PrestigeBuffGraphTile`) |
|
|
| `prestige_buff_graph_tile.tscn` | Tile scene (icon, name, effect, cost, state) |
|
|
| `prestige_buff_graph_panel.gd` | Full-screen panel displaying the buff graph (`PrestigeBuffGraphPanel`) |
|
|
| `prestige_panel.gd` | UI panel for prestige reset interaction (`PrestigePanel`) |
|
|
| `prestige_panel.tscn` | Prestige panel scene |
|
|
| `prestige_progress_bar.gd` | Progress bar toward next prestige threshold |
|
|
| `prestige_progress_bar.tscn` | Progress bar scene |
|
|
| `TECH_SPEC.md` | Detailed technical specification (historical design doc) |
|
|
|
|
## Architecture
|
|
|
|
```
|
|
LevelGameState (Node)
|
|
├── PrestigeManager (Node)
|
|
│ ├── Tracks prestige currency & reset count
|
|
│ ├── Calculates pending gain from config
|
|
│ └── Orchestrates reset via GameState
|
|
├── PrestigePanel (UI)
|
|
│ ├── Shows current/pending prestige & multiplier
|
|
│ └── Reset button with confirmation dialog
|
|
├── PrestigeProgressBar (UI)
|
|
│ └── Visual progress toward next threshold
|
|
├── PrestigeBuffGraphPanel (UI)
|
|
│ ├── Ascension currency balance
|
|
│ └── Tiered rows of PrestigeBuffGraphTile nodes
|
|
└── prestige_buff_catalogue (PrestigeBuffCatalogue)
|
|
└── Array of PrestigeBuffNode resources
|
|
```
|
|
|
|
PrestigeManager is **not** an autoload — it is a child node of `LevelGameState`. The game state finds it via `find_child("PrestigeManager")` in `_ready()`.
|
|
|
|
---
|
|
|
|
# Part 1 — Prestige Reset System
|
|
|
|
## PrestigeConfig
|
|
|
|
Resource defining prestige rules.
|
|
|
|
### Enums
|
|
|
|
```gdscript
|
|
enum BasisType {
|
|
LIFETIME_TOTAL, # All-time currency acquired (single currency)
|
|
RUN_TOTAL, # Currency this run (single currency)
|
|
RUN_MAX, # Peak currency this run (single currency)
|
|
ALL_CURRENCIES # Sum of all currencies (multi-currency tracking)
|
|
}
|
|
|
|
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 (ignored for ALL_CURRENCIES)
|
|
@export var basis: BasisType = LIFETIME_TOTAL
|
|
@export var formula: FormulaType = POWER
|
|
|
|
# Currency tracking
|
|
@export var include_currency_ids: Array[StringName] = [] # Empty = all currencies (for ALL_CURRENCIES basis)
|
|
|
|
# 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
|
|
|
|
Child node of `LevelGameState` managing prestige state.
|
|
|
|
### Signals
|
|
|
|
```gdscript
|
|
signal prestige_state_changed(total_prestige, pending_gain)
|
|
signal prestige_performed(gain, total_prestige)
|
|
signal prestige_threshold_changed(next_threshold)
|
|
```
|
|
|
|
### 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 get_current_prestige_unspent() -> BigNumber
|
|
func calculate_pending_gain() -> BigNumber
|
|
func can_prestige() -> bool
|
|
func get_total_multiplier() -> float
|
|
func get_next_prestige_threshold() -> BigNumber
|
|
|
|
# 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
|
|
```
|
|
|
|
**Basis Value Calculation**:
|
|
```gdscript
|
|
if basis == ALL_CURRENCIES:
|
|
# Sum of all tracked currencies
|
|
basis_value = sum(get_total_currency_acquired(currency_id) for currency_id in tracked_currencies)
|
|
elif basis == RUN_TOTAL:
|
|
# Currency earned since last reset
|
|
basis_value = lifetime_total - run_start_total
|
|
elif basis == RUN_MAX:
|
|
# Peak currency reached this run
|
|
basis_value = run_peak_currency
|
|
else: # LIFETIME_TOTAL
|
|
# All-time currency acquired
|
|
basis_value = lifetime_total
|
|
```
|
|
|
|
### 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. Prestige currency granted via `game_state.add_currency_by_id()`
|
|
5. `game_state.reset_for_prestige()` called (preserves prestige currency, resets generators, currencies, buff levels, goals)
|
|
6. `_reset_all_buff_levels()` — clears generator buff levels (NOT prestige buff unlocks)
|
|
7. Generator runtime state reinitialized
|
|
8. Run tracking reinitialized
|
|
9. `game_state.emit_currency_changed_for_all()` — triggers UI refresh
|
|
10. Save triggered
|
|
|
|
## PrestigePanel
|
|
|
|
UI component for prestige interaction.
|
|
|
|
### Displays
|
|
|
|
```gdscript
|
|
_total_value.text # Total prestige earned (e.g. "123.45")
|
|
_pending_value.text # Gain available now
|
|
_multiplier_value.text # Current multiplier (e.g. "x2.34")
|
|
_basis_label.text # Basis + value (e.g. "Total Currency (1.50e6)")
|
|
```
|
|
|
|
### Buttons
|
|
|
|
- **Reset**: Triggers `perform_prestige()` after confirmation dialog
|
|
- **Save**: Manually saves game state
|
|
|
|
## Generator Integration
|
|
|
|
Generators apply prestige multipliers via `PrestigeManager.get_total_multiplier()`:
|
|
|
|
```gdscript
|
|
func _get_prestige_multiplier() -> float:
|
|
var parent: Node = get_parent()
|
|
var prestige_manager: PrestigeManager = parent.find_child("PrestigeManager", true, false)
|
|
if prestige_manager:
|
|
return prestige_manager.get_total_multiplier()
|
|
return 1.0
|
|
```
|
|
|
|
This is called from `get_effective_auto_run_multiplier()` alongside research and buff multipliers. In the future, prestige buff multipliers from the graph system will replace or augment this.
|
|
|
|
## Save Integration
|
|
|
|
Prestige state stored in `LevelGameState` external save:
|
|
|
|
```json
|
|
{
|
|
"prestige_state": {
|
|
"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 with single currency:
|
|
|
|
```gdscript
|
|
id = &"primary"
|
|
display_name = "Ascension"
|
|
source_currency_id = &"magic"
|
|
basis = LIFETIME_TOTAL
|
|
threshold_mantissa = 1.0
|
|
threshold_exponent = 6 # 1e6
|
|
formula = POWER
|
|
exponent = 0.5 # Square root
|
|
multiplier_mode = ADDITIVE
|
|
multiplier_per_prestige = 0.10
|
|
```
|
|
|
|
For multi-currency tracking (`ALL_CURRENCIES`):
|
|
|
|
```gdscript
|
|
id = &"ascension"
|
|
display_name = "Ascension"
|
|
prestige_currency_id = &"ascension"
|
|
basis = ALL_CURRENCIES
|
|
include_currency_ids = [] # Empty = all currencies from catalogue
|
|
threshold_mantissa = 1.0
|
|
threshold_exponent = 4 # 1e4
|
|
formula = POWER
|
|
exponent = 0.5
|
|
multiplier_mode = ADDITIVE
|
|
multiplier_per_prestige = 0.05
|
|
```
|
|
|
|
---
|
|
|
|
# Part 2 — Prestige Buff Graph
|
|
|
|
## Overview
|
|
|
|
The prestige buff graph is a permanent upgrade system where players spend prestige currency to unlock nodes in a DAG-structured graph. Unlike generator buffs (which reset on prestige), prestige buff unlocks are **permanent** and survive resets.
|
|
|
|
## PrestigeBuffNode
|
|
|
|
Single node in the graph. Each node is a one-shot unlock — either locked or unlocked, no repeatable levels.
|
|
|
|
### EffectType Enum
|
|
|
|
```gdscript
|
|
enum EffectType {
|
|
CURRENCY_PRODUCTION_MULTIPLIER, # Multiply currency output by x%
|
|
BUILDING_CREATION_BONUS, # When a building is created, increase currency gain by x%
|
|
WORKER_THRESHOLD_MULTIPLIER, # When 10+ workers are assigned to a building, increase currency gain by x%
|
|
CARRYOVER_BUILDINGS, # After prestige, start with some buildings and workers already assigned
|
|
UNLOCK_AUTO_BUY_WORKER, # Unlock option to auto-buy workers
|
|
HOVER_SPEED_BOOST, # Hovering over a place speeds up workers instead of clicking
|
|
UNLOCK_BUILDING, # Unlock a new building (generator)
|
|
WORKER_PRODUCTIVITY, # Increase worker productivity by x%
|
|
RESEARCH_XP_MULTIPLIER, # Increase research XP gain by x%
|
|
}
|
|
```
|
|
|
|
### Exported Fields
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| `id` | `StringName` | Unique node identifier |
|
|
| `display_name` | `String` | UI label |
|
|
| `description` | `String` (multiline) | Tooltip |
|
|
| `icon` | `Texture2D` | Icon in graph UI |
|
|
| `parent_ids` | `Array[StringName]` | Prerequisites (DAG — multiple parents allowed) |
|
|
| `effect_type` | `EffectType` | What this buff does |
|
|
| `effect_value` | `float` | Effect magnitude (multiplier or count depending on type) |
|
|
| `target_id` | `StringName` | Target building/currency for type-specific effects (empty = global) |
|
|
| `cost_mantissa` | `float` | Prestige currency cost mantissa |
|
|
| `cost_exponent` | `int` | Prestige currency cost exponent |
|
|
| `tier` | `int` | Row in graph UI (visual grouping) |
|
|
| `x_position` | `float` | Column in graph UI |
|
|
|
|
### Key Methods
|
|
|
|
```gdscript
|
|
func get_cost() -> BigNumber # BigNumber from mantissa/exponent
|
|
func has_parents() -> bool # True if parent_ids is non-empty
|
|
func all_parents_unlocked(unlocked: Dictionary) -> bool # All prerequisites met?
|
|
func is_valid() -> bool # Non-empty id and positive cost
|
|
```
|
|
|
|
## PrestigeBuffCatalogue
|
|
|
|
Flat resource containing all buff nodes. The graph structure emerges from `parent_ids`.
|
|
|
|
| Field | Type |
|
|
|-------|------|
|
|
| `nodes` | `Array[PrestigeBuffNode]` |
|
|
|
|
### Key Methods
|
|
|
|
```gdscript
|
|
func get_node_by_id(id: StringName) -> PrestigeBuffNode
|
|
func get_all_ids() -> Array[StringName]
|
|
func get_root_nodes() -> Array[PrestigeBuffNode] # Nodes with empty parent_ids
|
|
func get_children_of(parent_id: StringName) -> Array[PrestigeBuffNode]
|
|
```
|
|
|
|
## State in LevelGameState
|
|
|
|
```gdscript
|
|
@export var prestige_buff_catalogue: PrestigeBuffCatalogue
|
|
var _prestige_buff_unlocked: Dictionary = {} # {StringName: bool} — node_id → unlocked
|
|
```
|
|
|
|
- Never cleared by `reset_for_prestige()` (unlike `_buff_levels` which IS cleared)
|
|
- Normalized on load: missing catalogue node IDs are filled as `false`
|
|
- On initialization, iterates the catalogue and sets any missing entries to `false`
|
|
|
|
## LevelGameState API
|
|
|
|
```gdscript
|
|
# Purchase and query
|
|
func purchase_prestige_buff(buff_id: StringName) -> bool
|
|
func can_purchase_prestige_buff(buff_id: StringName) -> bool
|
|
func is_prestige_buff_unlocked(buff_id: StringName) -> bool
|
|
func get_prestige_buff_cost(buff_id: StringName) -> BigNumber
|
|
func get_prestige_currency_id() -> StringName # Returns &"ascension"
|
|
|
|
# Multiplier computation — walks graph, returns combined product from unlocked nodes
|
|
func get_prestige_buff_multiplier(effect_type: int, target_id: StringName = &"") -> float
|
|
|
|
# Available nodes (prerequisites met, not yet unlocked)
|
|
func get_available_prestige_buffs() -> Array[PrestigeBuffNode]
|
|
```
|
|
|
|
### Signal
|
|
|
|
```gdscript
|
|
signal prestige_buff_unlocked(buff_id: StringName)
|
|
```
|
|
|
|
Prestige currency balance changes use the existing `currency_changed` signal — no separate signal needed.
|
|
|
|
## Purchase Flow
|
|
|
|
```
|
|
Player clicks unlock node
|
|
→ can_purchase_prestige_buff(node_id)
|
|
→ Node exists in catalogue?
|
|
→ Not already unlocked?
|
|
→ All parent_ids unlocked?
|
|
→ Enough prestige currency?
|
|
→ YES: spend_currency_by_id("ascension", cost)
|
|
→ _prestige_buff_unlocked[node_id] = true
|
|
→ emit prestige_buff_unlocked
|
|
→ save_game()
|
|
```
|
|
|
|
## Multiplier Computation
|
|
|
|
`get_prestige_buff_multiplier(effect_type, target_id)` iterates all unlocked nodes matching the given effect type and multiplies their `effect_value` together. Results start at `1.0`.
|
|
|
|
- If `target_id` is empty: all matching nodes are included (global effect)
|
|
- If `target_id` is set: only nodes with empty or matching `target_id` are included
|
|
|
|
## Multiplier Usage In Generators
|
|
|
|
Generators should call `get_prestige_buff_multiplier()` with their generator ID:
|
|
|
|
```gdscript
|
|
var asc_mult: float = game_state.get_prestige_buff_multiplier(
|
|
PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER,
|
|
get_generator_id()
|
|
)
|
|
```
|
|
|
|
This replaces the old `_get_prestige_multiplier()` call for prestige buff-driven production bonuses.
|
|
|
|
## Save Format
|
|
|
|
- Save format version bumped to **8** when prestige buffs were introduced.
|
|
- Key: `"prestige_buff_unlocked"` → array of unlocked node ID strings: `["gold_boost", "farm_boost"]`
|
|
- Load deserialization reads the array, sets matching entries to `true`, then calls `_initialize_prestige_buffs()` to fill in missing entries as `false`.
|
|
|
|
## UI — PrestigeBuffGraphPanel
|
|
|
|
A `PanelContainer`-based separate screen.
|
|
|
|
- **Layout**: Tiered rows (`HBoxContainer` per tier), sorted by `tier` then `x_position`
|
|
- **Tiles** (`PrestigeBuffGraphTile`): show icon, name, effect description, cost, state
|
|
- **States**:
|
|
- **Locked** (greyed out): prerequisites not met
|
|
- **Available** (highlighted, button enabled): all prerequisites met, can purchase
|
|
- **Unlocked** (green check, button disabled): already owned
|
|
- **Balance**: Shows current prestige currency balance at top, refreshes on `currency_changed`
|
|
- **Graph rebuild**: Rebuilds entire graph on `prestige_buff_unlocked` to reflect newly available children
|
|
|
|
## Content Files
|
|
|
|
Prestige buff content lives in `docs/gyms/tiny_sword/prestige/`:
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `prestige_buff_catalogue.tres` | Catalogue resource referencing all buff nodes |
|
|
| `prestige_buff_gold_boost.tres` | Example: +50% gold mine production |
|
|
| `prestige_buff_farm_boost.tres` | Example: +50% farm food production |
|
|
|
|
Each `.tres` is a `PrestigeBuffNode` resource with exported fields set in the inspector.
|
|
|
|
## Prestige Reset And Buffs
|
|
|
|
- **Generator buffs** (`_buff_levels`, `_buff_unlocked`, `_buff_active`): reset to 0/false on prestige
|
|
- **Prestige buff unlocks** (`_prestige_buff_unlocked`): **never reset** — permanent across all prestige resets
|