Add prestige graph

This commit is contained in:
2026-05-06 23:37:52 +02:00
parent af62e379ee
commit 1ccb498947
32 changed files with 1537 additions and 279 deletions

View File

@@ -2,29 +2,51 @@
## Overview
The `prestige/` subfolder implements the rebirth/reset system. Players can reset progress in exchange for a permanent multiplier that accelerates future runs.
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 |
| `prestige_manager.gd` | Core logic and state management |
| `prestige_panel.gd` | UI panel for prestige interaction |
| `TECH_SPEC.md` | Detailed technical specification |
| `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
```
PrestigeManager (autoload node)
├── Tracks prestige currency
├── Calculates pending gain
├── Applies multipliers to generators
└── PrestigePanel (UI)
├── Shows current/pending prestige
└── Trigger reset button
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.
@@ -82,13 +104,14 @@ enum MultiplierMode {
## PrestigeManager
Autoload node managing prestige state.
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
@@ -106,9 +129,11 @@ var run_peak_source_currency: BigNumber # Peak this run
```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
@@ -171,20 +196,14 @@ multiplier = base_multiplier * (growth_base ^ total_prestige)
1. Player clicks "Prestige" button
2. `perform_prestige()` called
3. Gain calculated and added to total:
- For ALL_CURRENCIES: sum of all tracked currencies is used
- For single-currency: source currency is used
4. `GameState.reset_for_prestige()` called:
- Current currencies reset to 0
- Generator states cleared
- Buff levels reset to 0
- Goal completion cleared and re-initialized
- Lifetime totals preserved
5. Run tracking reinitialized:
- For ALL_CURRENCIES: sum of all currencies at reset time
- For single-currency: source currency at reset time
6. Generators reinitialized
7. Save triggered
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
@@ -193,40 +212,39 @@ 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 = "Total Currency (1.50e6)" # For ALL_CURRENCIES basis
# Or: "Lifetime Total (1.50e6 Magic)" # For LIFETIME_TOTAL basis
_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
- **Reset**: Triggers `perform_prestige()` after confirmation dialog
- **Save**: Manually saves game state
## Integration with Generators
## Generator Integration
Generators apply prestige multipliers automatically:
Generators apply prestige multipliers via `PrestigeManager.get_total_multiplier()`:
```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()
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 `GameState` external save:
Prestige state stored in `LevelGameState` external save:
```json
{
"prestige": {
"prestige_state": {
"total_prestige_earned": {"m": 10.0, "e": 0},
"current_prestige_unspent": {"m": 5.0, "e": 0},
"prestige_resets_count": 3,
@@ -242,56 +260,208 @@ Prestige state stored in `GameState` external save:
For a typical idle game with single currency:
```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)
threshold_exponent = 6 # 1e6
formula = POWER
exponent = 0.5
# +10% multiplier per prestige point
exponent = 0.5 # Square root
multiplier_mode = ADDITIVE
multiplier_per_prestige = 0.10
```
For multi-currency tracking (ALL_CURRENCIES):
For multi-currency tracking (`ALL_CURRENCIES`):
```gdscript
id = &"ascension"
display_name = "Ascension"
prestige_currency_id = &"ascension"
# Track sum of ALL currencies (gold + wood + food + etc.)
basis = ALL_CURRENCIES
# Empty include_currency_ids means all currencies from catalog
# Set specific IDs to track only those: include_currency_ids = [&"gold", &"wood"]
include_currency_ids = []
# Need 1e4 total currency sum to get first prestige
include_currency_ids = [] # Empty = all currencies from catalogue
threshold_mantissa = 1.0
threshold_exponent = 4
# Square root scaling
threshold_exponent = 4 # 1e4
formula = POWER
exponent = 0.5
# +5% multiplier per prestige point
multiplier_mode = ADDITIVE
multiplier_per_prestige = 0.05
```
## See Also
---
- `core/level_game_state.gd` - State persistence
- `core/generator/currency_generator.gd` - Multiplier application
- `core/big_number.gd` - Large number math
# 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