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:
2026-05-07 20:36:21 +00:00
parent 2743fd314a
commit 81a4058b04
242 changed files with 11226 additions and 2242 deletions

467
core/prestige/README.md Normal file
View File

@@ -0,0 +1,467 @@
# 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

View File

@@ -0,0 +1,40 @@
## Flat list of all ascension buff nodes. The graph structure emerges from each node's parent_ids field.
class_name PrestigeBuffCatalogue
extends Resource
@export var nodes: Array[PrestigeBuffNode] = []
## Finds a node by its unique ID. Returns null if not found.
func get_node_by_id(id: StringName) -> PrestigeBuffNode:
for node in nodes:
if node.id == id:
return node
return null
## Returns all non-empty node IDs in the catalogue.
func get_all_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for node in nodes:
if node.id:
ids.append(node.id)
return ids
## Returns all root nodes (nodes with no prerequisites).
func get_root_nodes() -> Array[PrestigeBuffNode]:
var roots: Array[PrestigeBuffNode] = []
for node in nodes:
if node.parent_ids.is_empty():
roots.append(node)
return roots
## Returns all nodes that list the given parent_id as a prerequisite.
func get_children_of(parent_id: StringName) -> Array[PrestigeBuffNode]:
var children: Array[PrestigeBuffNode] = []
for node in nodes:
if node.parent_ids.has(parent_id):
children.append(node)
return children

View File

@@ -0,0 +1 @@
uid://bjcvnyh2bc6l5

View File

@@ -0,0 +1,43 @@
## Draws bezier curves between connected PrestigeBuffGraphTile nodes in the graph panel.
## Store pairs of tile references before calling queue_redraw().
class_name PrestigeBuffConnectionOverlay
extends Control
## Pairs of [parent_tile, child_tile] to connect.
var _connections: Array = []
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
## Replace all connections and redraw.
func set_connections(connections: Array) -> void:
_connections = connections
queue_redraw()
func _draw() -> void:
if _connections.is_empty():
return
for pair in _connections:
var parent_tile: Control = pair[0] as Control
var child_tile: Control = pair[1] as Control
if parent_tile == null or child_tile == null:
continue
var from_pos: Vector2 = _tile_bottom_center(parent_tile)
var to_pos: Vector2 = _tile_top_center(child_tile)
draw_line(from_pos, to_pos, Color(0.6, 0.6, 0.7, 0.6), 2.0)
func _tile_bottom_center(tile: Control) -> Vector2:
var rect: Rect2 = tile.get_global_rect()
var global_pos: Vector2 = Vector2(rect.position.x + rect.size.x * 0.5, rect.position.y + rect.size.y)
return get_global_transform().affine_inverse() * global_pos
func _tile_top_center(tile: Control) -> Vector2:
var rect: Rect2 = tile.get_global_rect()
var global_pos: Vector2 = Vector2(rect.position.x + rect.size.x * 0.5, rect.position.y)
return get_global_transform().affine_inverse() * global_pos

View File

@@ -0,0 +1 @@
uid://uv5lxj6hmpk

View File

@@ -0,0 +1,85 @@
## Separate-screen panel displaying the prestige buff graph.
## Tiles are placed manually in the scene editor — each PrestigeBuffGraphTile has its PrestigeBuffNode
## resource assigned directly. The panel manages the prestige currency balance display and
## delegates connection-line drawing to a PrestigeBuffConnectionOverlay child.
class_name PrestigeBuffGraphPanel
extends PanelContainer
@onready var _balance_label: Label = $MarginContainer/VBoxContainer/BalanceLabel
@onready var _tiles_area: Control = $MarginContainer/VBoxContainer/TilesArea
@onready var _close_button: Button = $MarginContainer/VBoxContainer/Header/CloseButton
@onready var _connection_overlay: PrestigeBuffConnectionOverlay = $MarginContainer/VBoxContainer/TilesArea/ConnectionOverlay
var _game_state: LevelGameState
func _ready() -> void:
_game_state = find_parent("LevelGameState")
if _game_state == null:
push_error("PrestigeBuffGraphPanel: Could not find LevelGameState parent")
return
_game_state.currency_changed.connect(_on_currency_changed)
_close_button.pressed.connect(_on_close_pressed)
visibility_changed.connect(_on_visibility_changed)
_refresh_balance()
## Rebuilds the connection overlay by scanning all PrestigeBuffGraphTile children and matching parent_ids.
func _refresh_connections() -> void:
if _connection_overlay == null:
return
var tiles_by_id: Dictionary = {}
var all_tiles: Array[PrestigeBuffGraphTile] = []
_collect_tiles(_tiles_area, all_tiles)
for tile in all_tiles:
var node_id: StringName = tile.get_node_id()
if node_id != &"":
tiles_by_id[node_id] = tile
var connections: Array = []
for tile in all_tiles:
if tile.prestige_buff == null:
continue
for parent_id in tile.prestige_buff.parent_ids:
var parent_tile: PrestigeBuffGraphTile = tiles_by_id.get(parent_id, null)
if parent_tile:
connections.append([parent_tile, tile])
_connection_overlay.set_connections(connections)
## Collects all PrestigeBuffGraphTile nodes rooted at `from`, recursing into every child.
func _collect_tiles(from: Node, out_tiles: Array[PrestigeBuffGraphTile]) -> void:
var found: Array[Node] = from.find_children("*", "PrestigeBuffGraphTile", true, false)
for node in found:
var tile := node as PrestigeBuffGraphTile
if tile:
out_tiles.append(tile)
func _refresh_balance() -> void:
if _game_state == null:
_balance_label.text = "Prestige Currency: 0"
return
var currency_id: StringName = _game_state.get_prestige_currency_id()
var balance: BigNumber = _game_state.get_currency_amount_by_id(currency_id)
_balance_label.text = "Prestige Currency: %s" % balance.to_string_suffix(2)
func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void:
if currency_id == _game_state.get_prestige_currency_id():
_refresh_balance()
func _on_close_pressed() -> void:
hide()
func _on_visibility_changed() -> void:
if visible:
_refresh_balance()
_refresh_connections()
func _on_prestige_buff_toggle_pressed() -> void:
show()

View File

@@ -0,0 +1 @@
uid://n01pkakxllnj

View File

@@ -0,0 +1,122 @@
## Individual tile in the prestige buff graph UI.
## Visually represents a single PrestigeBuffNode with locked/available/unlocked states.
## The PrestigeBuffNode resource is assigned directly via the editor export.
class_name PrestigeBuffGraphTile
extends PanelContainer
## The prestige buff node this tile represents. Set in the scene editor.
@export var prestige_buff: PrestigeBuffNode
@onready var _button: Button = $Button
@onready var _icon: TextureRect = $Button/HBoxContainer/Icon
@onready var _name_label: Label = $Button/HBoxContainer/VBoxContainer/NameLabel
@onready var _effect_label: Label = $Button/HBoxContainer/VBoxContainer/EffectLabel
@onready var _cost_label: Label = $Button/HBoxContainer/VBoxContainer/CostLabel
@onready var _state_label: Label = $Button/HBoxContainer/StateLabel
var _game_state: LevelGameState
func _ready() -> void:
_game_state = find_parent("LevelGameState")
_button.pressed.connect(_on_button_pressed)
if _game_state:
_game_state.prestige_buff_unlocked.connect(_on_prestige_buff_unlocked)
_game_state.currency_changed.connect(_on_currency_changed)
if prestige_buff == null:
push_warning("PrestigeBuffGraphTile: prestige_buff is not set")
return
_refresh_visuals()
_refresh_state()
func get_node_id() -> StringName:
if prestige_buff:
return prestige_buff.id
return &""
func _refresh_visuals() -> void:
if prestige_buff == null:
return
_name_label.text = prestige_buff.display_name
_cost_label.text = "Cost: %s" % prestige_buff.get_cost().to_string_suffix(2)
if prestige_buff.icon:
_icon.texture = prestige_buff.icon
var effect_text: String = _format_effect_description()
_effect_label.text = effect_text
_effect_label.visible = not effect_text.is_empty()
func _refresh_state() -> void:
if prestige_buff == null or _game_state == null:
return
var unlocked: bool = _game_state.is_prestige_buff_unlocked(prestige_buff.id)
var available: bool = _game_state.can_purchase_prestige_buff(prestige_buff.id)
if unlocked:
_state_label.text = "✓ Unlocked"
_button.disabled = true
modulate = Color.GREEN
elif available:
_state_label.text = "Available"
_button.disabled = false
modulate = Color.WHITE
else:
_state_label.text = "Locked"
_button.disabled = true
modulate = Color(0.5, 0.5, 0.5, 1.0)
func _format_effect_description() -> String:
if prestige_buff == null:
return ""
match prestige_buff.effect_type:
PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER:
if prestige_buff.target_id != &"":
return "+%.0f%% %s production" % [((prestige_buff.effect_value - 1.0) * 100.0), prestige_buff.target_id]
return "+%.0f%% all production" % ((prestige_buff.effect_value - 1.0) * 100.0)
PrestigeBuffNode.EffectType.BUILDING_CREATION_BONUS:
return "+%.0f%% gain on build" % ((prestige_buff.effect_value - 1.0) * 100.0)
PrestigeBuffNode.EffectType.WORKER_THRESHOLD_MULTIPLIER:
return "+%.0f%% with 10+ workers" % ((prestige_buff.effect_value - 1.0) * 100.0)
PrestigeBuffNode.EffectType.CARRYOVER_BUILDINGS:
return "Keep %d buildings on prestige" % int(prestige_buff.effect_value)
PrestigeBuffNode.EffectType.UNLOCK_AUTO_BUY_WORKER:
return "Unlock auto-buy worker"
PrestigeBuffNode.EffectType.HOVER_SPEED_BOOST:
return "Hover to speed up workers"
PrestigeBuffNode.EffectType.UNLOCK_BUILDING:
return "Unlock %s building" % prestige_buff.target_id
PrestigeBuffNode.EffectType.WORKER_PRODUCTIVITY:
return "+%.0f%% worker productivity" % ((prestige_buff.effect_value - 1.0) * 100.0)
PrestigeBuffNode.EffectType.RESEARCH_XP_MULTIPLIER:
return "+%.0f%% research XP" % ((prestige_buff.effect_value - 1.0) * 100.0)
_:
return ""
func _on_button_pressed() -> void:
if _game_state and prestige_buff:
_game_state.purchase_prestige_buff(prestige_buff.id)
func _on_prestige_buff_unlocked(buff_id: StringName) -> void:
if prestige_buff and buff_id == prestige_buff.id:
_refresh_state()
func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void:
if _game_state == null or prestige_buff == null:
return
var asc_id: StringName = _game_state.get_prestige_currency_id()
if currency_id == asc_id:
_refresh_state()

View File

@@ -0,0 +1 @@
uid://w8ogrp1s6qsf

View File

@@ -0,0 +1,40 @@
[gd_scene format=3 uid="uid://bqk8rwia7lpbg"]
[ext_resource type="Script" uid="uid://w8ogrp1s6qsf" path="res://core/prestige/prestige_buff_graph_tile.gd" id="1_script"]
[node name="AscensionBuffGraphTile" type="PanelContainer" unique_id=582396224]
custom_minimum_size = Vector2(220, 100)
script = ExtResource("1_script")
[node name="Button" type="Button" parent="." unique_id=34096996]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Button" unique_id=760838153]
layout_mode = 0
[node name="Icon" type="TextureRect" parent="Button/HBoxContainer" unique_id=2086768939]
custom_minimum_size = Vector2(48, 48)
layout_mode = 2
size_flags_horizontal = 0
[node name="VBoxContainer" type="VBoxContainer" parent="Button/HBoxContainer" unique_id=1701436193]
layout_mode = 2
size_flags_horizontal = 3
[node name="NameLabel" type="Label" parent="Button/HBoxContainer/VBoxContainer" unique_id=334883451]
layout_mode = 2
text = "Buff Name"
[node name="EffectLabel" type="Label" parent="Button/HBoxContainer/VBoxContainer" unique_id=1416390723]
layout_mode = 2
text = "+10% effect"
[node name="CostLabel" type="Label" parent="Button/HBoxContainer/VBoxContainer" unique_id=983468527]
layout_mode = 2
text = "Cost: 0"
[node name="StateLabel" type="Label" parent="Button/HBoxContainer" unique_id=629071282]
layout_mode = 2
size_flags_horizontal = 0
text = "Locked"
horizontal_alignment = 2

View File

@@ -0,0 +1,69 @@
## A single node in the prestige buff graph.
## Each node represents a permanent upgrade that persists across prestige resets and is purchased with prestige currency.
class_name PrestigeBuffNode
extends Resource
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%
}
## Unique node identifier.
@export var id: StringName = &""
## UI display label.
@export var display_name: String = ""
## Tooltip / description text.
@export_multiline var description: String = ""
## Icon shown in the graph UI.
@export var icon: Texture2D
## Prerequisite node IDs that must be unlocked before this node becomes available.
@export var parent_ids: Array[StringName] = []
## What kind of effect this buff provides.
@export var effect_type: EffectType = EffectType.CURRENCY_PRODUCTION_MULTIPLIER
## Magnitude of the effect (interpreted per effect type).
@export var effect_value: float = 1.0
## Target generator, building, or currency ID for type-specific effects. Empty means global.
@export var target_id: StringName = &""
## Prestige currency cost mantissa (combined with cost_exponent for BigNumber).
@export var cost_mantissa: float = 1.0
## Prestige currency cost exponent (combined with cost_mantissa for BigNumber).
@export var cost_exponent: int = 0
## Visual tier / row in the graph UI.
@export var tier: int = 0
## Visual column / horizontal position in the graph UI.
@export var x_position: float = 0.0
## Builds and returns the fixed prestige currency cost as a BigNumber.
func get_cost() -> BigNumber:
return BigNumber.new(cost_mantissa, cost_exponent)
## Returns true if this node has any prerequisite parent nodes.
func has_parents() -> bool:
return not parent_ids.is_empty()
## Checks whether all prerequisites are unlocked in the provided dictionary.
## [param unlocked] maps node_id (StringName) → bool (true if unlocked).
func all_parents_unlocked(unlocked: Dictionary) -> bool:
for parent_id in parent_ids:
if not bool(unlocked.get(parent_id, false)):
return false
return true
## Returns true if this node has valid required data (non-empty id, positive cost).
func is_valid() -> bool:
if id == &"":
return false
if cost_mantissa <= 0.0:
return false
return true

View File

@@ -0,0 +1 @@
uid://mjyig8xlgki0

View File

@@ -5,6 +5,7 @@ enum BasisType {
LIFETIME_TOTAL,
RUN_TOTAL,
RUN_MAX,
ALL_CURRENCIES,
}
enum FormulaType {
@@ -41,6 +42,7 @@ enum MultiplierMode {
@export var base_multiplier: float = 1.0
@export var multiplier_per_prestige: float = 0.05
@export var multiplier_exponent: float = 1.0
@export var include_currency_ids: Array[StringName] = []
func get_threshold() -> BigNumber:
return BigNumber.new(threshold_mantissa, threshold_exponent)
@@ -48,7 +50,7 @@ func get_threshold() -> BigNumber:
func is_valid() -> bool:
if String(id).strip_edges().is_empty():
return false
if String(source_currency_id).strip_edges().is_empty():
if String(source_currency_id).strip_edges().is_empty() and basis != BasisType.ALL_CURRENCIES:
return false
if threshold_mantissa <= 0.0:
return false

View File

@@ -1,7 +1,9 @@
class_name PrestigeManager
extends Node
signal prestige_state_changed(total_prestige: BigNumber, pending_gain: BigNumber)
signal prestige_performed(gain: BigNumber, total_prestige: BigNumber)
signal prestige_threshold_changed(next_threshold: BigNumber)
const SAVE_KEY: String = "prestige_state"
const TOTAL_PRESTIGE_KEY: String = "total_prestige_earned"
@@ -14,6 +16,7 @@ const LAST_RESET_TIME_KEY: String = "last_reset_time"
const BASIS_LIFETIME_TOTAL: int = 0
const BASIS_RUN_TOTAL: int = 1
const BASIS_RUN_MAX: int = 2
const BASIS_ALL_CURRENCIES: int = 3
const FORMULA_POWER: int = 0
const FORMULA_TRIANGULAR_INVERSE: int = 1
@@ -21,7 +24,8 @@ const FORMULA_TRIANGULAR_INVERSE: int = 1
const MULTIPLIER_MODE_ADDITIVE: int = 0
const MULTIPLIER_MODE_MULTIPLICATIVE_POWER: int = 1
@export var config: Resource
@export var config: PrestigeConfig
@export var game_state: LevelGameState
var total_prestige_earned: BigNumber = BigNumber.from_float(0.0)
var current_prestige_unspent: BigNumber = BigNumber.from_float(0.0)
@@ -32,13 +36,17 @@ var last_reset_time: int = 0
func _ready() -> void:
if config == null:
config = load("res://idles/prestige/primary_prestige.tres") as Resource
config = load("res://docs/gyms/tiny_sword/prestige/primary_prestige.tres") as PrestigeConfig
if not _is_config_valid():
push_warning("PrestigeManager has invalid or missing config; prestige is disabled.")
return
var loaded_state: Dictionary = GameState.get_external_save_data(SAVE_KEY)
if game_state == null:
push_warning("PrestigeManager: game_state reference is not set.")
return
var loaded_state: Dictionary = game_state.get_external_save_data(SAVE_KEY)
if loaded_state.is_empty():
_initialize_run_tracking_from_current_state()
else:
@@ -46,8 +54,12 @@ func _ready() -> void:
_validate_loaded_run_tracking()
_save_state_to_game_state()
GameState.currency_changed.connect(_on_currency_changed)
game_state.currency_changed.connect(_on_currency_changed)
prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain())
prestige_threshold_changed.emit(get_next_prestige_threshold())
func get_config() -> PrestigeConfig:
return config
func get_source_currency_id() -> StringName:
if not _is_config_valid():
@@ -55,6 +67,38 @@ func get_source_currency_id() -> StringName:
return _normalize_currency_id(_get_config_string_name("source_currency_id", &""))
func get_prestige_currency_id() -> StringName:
if not _is_config_valid():
return &""
return _normalize_currency_id(_get_config_string_name("prestige_currency_id", &""))
func get_tracked_currency_ids() -> Array[StringName]:
if not _is_config_valid():
return []
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
if basis_type == BASIS_ALL_CURRENCIES:
var include_ids: Array[StringName] = _get_config_array("include_currency_ids", [])
if not include_ids.is_empty():
return include_ids
if game_state and game_state.currency_catalogue:
return game_state.currency_catalogue.get_all_ids()
return []
return [get_source_currency_id()]
func _get_config_array(property_name: String, fallback: Array = []) -> Array:
if config == null:
return fallback
var value: Variant = config.get(property_name)
if value is Array:
return value
return fallback
func get_total_prestige() -> BigNumber:
return _copy_big_number(total_prestige_earned)
@@ -66,7 +110,7 @@ func get_total_multiplier() -> float:
return 1.0
var base: float = maxf(_get_config_float("base_multiplier", 1.0), 0.0)
var prestige_value: float = _big_number_to_float(total_prestige_earned)
var prestige_value: float = total_prestige_earned.to_float()
if prestige_value <= 0.0:
return base
@@ -125,52 +169,119 @@ func perform_prestige() -> bool:
if gain.mantissa > 0.0:
total_prestige_earned.add_in_place(gain)
current_prestige_unspent.add_in_place(gain)
var prestige_currency_id: StringName = get_prestige_currency_id()
if prestige_currency_id != &"" and game_state:
game_state.add_currency_by_id(prestige_currency_id, gain)
prestige_resets_count += 1
last_reset_time = Time.get_unix_time_from_system()
GameState.reset_for_prestige(true, false)
if game_state:
var preserve_currencies: Array[StringName] = []
var prestige_currency_id: StringName = get_prestige_currency_id()
if prestige_currency_id != &"":
preserve_currencies.append(prestige_currency_id)
game_state.reset_for_prestige(true, false, preserve_currencies)
_reset_all_buff_levels()
_reinitialize_generators_after_prestige_reset()
GameState.emit_currency_changed_for_all()
if game_state:
game_state.emit_currency_changed_for_all()
_initialize_run_tracking_from_current_state()
_save_state_to_game_state()
GameState.save_game()
if game_state:
game_state.save_game()
var total: BigNumber = get_total_prestige()
var next_threshold: BigNumber = get_next_prestige_threshold()
prestige_performed.emit(gain, total)
prestige_state_changed.emit(total, calculate_pending_gain())
prestige_threshold_changed.emit(next_threshold)
return true
func get_next_prestige_threshold() -> BigNumber:
if not _is_config_valid():
return BigNumber.from_float(0.0)
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
match basis_type:
BASIS_RUN_TOTAL, BASIS_ALL_CURRENCIES:
return _get_config_threshold()
_:
var current_total: float = total_prestige_earned.to_float()
var next_target: float = _calculate_target_for_prestige(current_total + 1.0)
return BigNumber.from_float(next_target)
func get_basis_value_for_display() -> BigNumber:
return _get_basis_value()
func get_basis_label() -> String:
if not _is_config_valid():
return "-"
match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
match basis_type:
BASIS_RUN_TOTAL:
return "Run Total"
BASIS_RUN_MAX:
return "Run Max"
BASIS_ALL_CURRENCIES:
return "Total Currency"
_:
return "Lifetime Total"
func _on_currency_changed(changed_currency_id: StringName, new_amount: BigNumber) -> void:
if config == null:
return
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
return
if changed_currency_id != source_currency_id:
return
if new_amount.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(new_amount)
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
if basis_type == BASIS_ALL_CURRENCIES:
if new_amount.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(new_amount)
else:
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
return
if changed_currency_id != source_currency_id:
return
if new_amount.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(new_amount)
_save_state_to_game_state()
var next_threshold: BigNumber = get_next_prestige_threshold()
prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain())
prestige_threshold_changed.emit(next_threshold)
func _calculate_target_for_prestige(prestige_level: float) -> float:
if config == null:
return 0.0
var threshold_value: float = _get_config_threshold().to_float()
if threshold_value <= 0.0:
return 0.0
match _get_config_int("formula", FORMULA_POWER):
FORMULA_TRIANGULAR_INVERSE:
var target_total: float = prestige_level
var k: float = target_total * (target_total + 1.0) * 0.5
return k * threshold_value
_:
var scale: float = maxf(_get_config_float("scale", 1.0), 0.0001)
var exponent: float = maxf(_get_config_float("exponent", 1.0), 0.0001)
var flat_bonus: float = _get_config_float("flat_bonus", 0.0)
if scale <= 0.0:
scale = 0.0001
if exponent <= 0.0:
exponent = 0.0001
var ratio: float = pow((prestige_level - flat_bonus) / scale, 1.0 / exponent)
if ratio <= 0.0:
return threshold_value
return ratio * threshold_value
func _calculate_raw_gain_float(basis_value: BigNumber) -> float:
if config == null:
@@ -178,11 +289,11 @@ func _calculate_raw_gain_float(basis_value: BigNumber) -> float:
match _get_config_int("formula", FORMULA_POWER):
FORMULA_TRIANGULAR_INVERSE:
var threshold_value: float = _big_number_to_float(_get_config_threshold())
var threshold_value: float = _get_config_threshold().to_float()
if threshold_value <= 0.0:
return 0.0
var basis_float: float = _big_number_to_float(basis_value)
var basis_float: float = basis_value.to_float()
if basis_float <= 0.0:
return 0.0
@@ -192,7 +303,7 @@ func _calculate_raw_gain_float(basis_value: BigNumber) -> float:
var target_total: float = (sqrt(1.0 + 8.0 * k) - 1.0) * 0.5
if _is_cumulative_basis():
return target_total - _big_number_to_float(total_prestige_earned)
return target_total - total_prestige_earned.to_float()
return target_total
_:
var ratio: float = _big_number_ratio_to_float(basis_value, _get_config_threshold())
@@ -201,20 +312,26 @@ func _calculate_raw_gain_float(basis_value: BigNumber) -> float:
var value: float = maxf(_get_config_float("scale", 0.0), 0.0) * pow(ratio, maxf(_get_config_float("exponent", 0.0001), 0.0001)) + _get_config_float("flat_bonus", 0.0)
if _is_cumulative_basis():
return value - _big_number_to_float(total_prestige_earned)
return value - total_prestige_earned.to_float()
return value
func _get_basis_value() -> BigNumber:
if config == null:
return BigNumber.from_float(0.0)
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
if game_state == null:
return BigNumber.from_float(0.0)
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
match basis_type:
BASIS_ALL_CURRENCIES:
return _get_total_all_currencies()
BASIS_RUN_TOTAL:
var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id)
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
return BigNumber.from_float(0.0)
var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id)
var run_total: BigNumber = lifetime_now.subtract(run_start_total_source_currency)
if run_total.mantissa < 0.0:
return BigNumber.from_float(0.0)
@@ -222,11 +339,37 @@ func _get_basis_value() -> BigNumber:
BASIS_RUN_MAX:
return _copy_big_number(run_peak_source_currency)
_:
return _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
return BigNumber.from_float(0.0)
return _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id))
func _get_total_all_currencies() -> BigNumber:
var total: BigNumber = BigNumber.from_float(0.0)
var currency_ids: Array[StringName] = get_tracked_currency_ids()
for currency_id in currency_ids:
var currency_total: BigNumber = game_state.get_total_currency_acquired_by_id(currency_id)
total.add_in_place(currency_total)
return total
func _initialize_run_tracking_from_current_state() -> void:
if config == null:
return
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
if basis_type == BASIS_ALL_CURRENCIES:
run_start_total_source_currency = _get_total_all_currencies()
run_peak_source_currency = BigNumber.from_float(0.0)
var currency_ids: Array[StringName] = get_tracked_currency_ids()
for currency_id in currency_ids:
var current: BigNumber = game_state.get_currency_amount_by_id(currency_id)
if current.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(current)
_save_state_to_game_state()
return
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
@@ -234,13 +377,31 @@ func _initialize_run_tracking_from_current_state() -> void:
run_peak_source_currency = BigNumber.from_float(0.0)
return
run_start_total_source_currency = _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))
run_peak_source_currency = _copy_big_number(GameState.get_currency_amount_by_id(source_currency_id))
if game_state == null:
run_start_total_source_currency = BigNumber.from_float(0.0)
run_peak_source_currency = BigNumber.from_float(0.0)
return
run_start_total_source_currency = _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id))
run_peak_source_currency = _copy_big_number(game_state.get_currency_amount_by_id(source_currency_id))
_save_state_to_game_state()
func _validate_loaded_run_tracking() -> void:
if config == null:
return
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
if basis_type == BASIS_ALL_CURRENCIES:
var currency_ids: Array[StringName] = get_tracked_currency_ids()
var peak: BigNumber = BigNumber.from_float(0.0)
for currency_id in currency_ids:
var current: BigNumber = game_state.get_currency_amount_by_id(currency_id)
if current.is_greater_than(peak):
peak = _copy_big_number(current)
if peak.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(peak)
return
var source_currency_id: StringName = get_source_currency_id()
if source_currency_id == &"":
@@ -248,11 +409,14 @@ func _validate_loaded_run_tracking() -> void:
run_peak_source_currency = BigNumber.from_float(0.0)
return
var current_currency: BigNumber = GameState.get_currency_amount_by_id(source_currency_id)
if game_state == null:
return
var current_currency: BigNumber = game_state.get_currency_amount_by_id(source_currency_id)
if current_currency.is_greater_than(run_peak_source_currency):
run_peak_source_currency = _copy_big_number(current_currency)
var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id)
var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id)
if run_start_total_source_currency.is_greater_than(lifetime_now):
run_start_total_source_currency = _copy_big_number(lifetime_now)
@@ -278,7 +442,18 @@ func _deserialize_state(raw_state: Dictionary) -> void:
current_prestige_unspent = _copy_big_number(total_prestige_earned)
func _save_state_to_game_state() -> void:
GameState.set_external_save_data(SAVE_KEY, _serialize_state())
if game_state:
game_state.set_external_save_data(SAVE_KEY, _serialize_state())
func _reset_all_buff_levels() -> void:
if game_state == null:
return
for buff in game_state.get_all_buffs():
if buff == null:
continue
game_state.set_buff_level(buff.id, 0)
game_state.set_buff_unlocked(buff.id, false)
func _reinitialize_generators_after_prestige_reset() -> void:
var scene_root: Node = get_tree().current_scene
@@ -316,14 +491,7 @@ func _big_number_ratio_to_float(a: BigNumber, b: BigNumber) -> float:
return (a.mantissa / b.mantissa) * pow(10.0, float(exponent_delta))
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 _normalize_currency_id(currency_id: StringName) -> StringName:
var normalized: String = String(currency_id).to_lower().strip_edges()
@@ -341,33 +509,18 @@ func _to_non_negative_int(value: Variant, fallback: int = 0) -> int:
func _is_config_valid() -> bool:
if config == null:
return false
if not config.has_method("is_valid"):
return false
return bool(config.call("is_valid"))
return config.is_valid()
func _is_cumulative_basis() -> bool:
if config != null and config.has_method("is_cumulative_basis"):
return bool(config.call("is_cumulative_basis"))
return _get_config_int("basis", BASIS_LIFETIME_TOTAL) != BASIS_RUN_TOTAL
var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL)
return basis_type != BASIS_RUN_TOTAL and basis_type != BASIS_ALL_CURRENCIES
func _round_gain(value: float) -> float:
if config != null and config.has_method("round_value"):
var rounded: Variant = config.call("round_value", value)
if rounded is float:
return rounded
if rounded is int:
return float(rounded)
return floorf(value)
return config.round_value(value)
func _get_config_threshold() -> BigNumber:
if config != null and config.has_method("get_threshold"):
var threshold: Variant = config.call("get_threshold")
if threshold is BigNumber:
return threshold
if config != null:
return config.get_threshold()
return BigNumber.from_float(1.0)
func _get_config_string_name(property_name: String, fallback: StringName = &"") -> StringName:

View File

@@ -5,44 +5,44 @@ extends PanelContainer
@onready var _pending_value: Label = $MarginContainer/VBoxContainer/StatsGrid/PendingValue
@onready var _multiplier_value: Label = $MarginContainer/VBoxContainer/StatsGrid/MultiplierValue
@onready var _basis_label: Label = $MarginContainer/VBoxContainer/StatsGrid/BasisValue
@onready var _progress_bar: Node = $MarginContainer/VBoxContainer/PrestigeProgressBar
@onready var _reset_button: Button = $MarginContainer/VBoxContainer/Buttons/ResetButton
@onready var _save_button: Button = $MarginContainer/VBoxContainer/Buttons/SaveButton
@onready var _confirm_dialog: ConfirmationDialog = $ConfirmDialog
@onready var game_state: LevelGameState = find_parent("LevelGameState")
var _is_waiting_for_confirm: bool = false
func _ready() -> void:
var manager: Node = _get_prestige_manager()
if manager != null and manager.has_signal("prestige_state_changed"):
manager.prestige_state_changed.connect(_on_prestige_state_changed)
if manager != null and manager.has_signal("prestige_performed"):
manager.prestige_performed.connect(_on_prestige_performed)
if not GameState.currency_changed.is_connected(_on_currency_changed):
GameState.currency_changed.connect(_on_currency_changed)
game_state.ready.connect(_on_game_state_ready)
func _on_game_state_ready() -> void:
var manager: PrestigeManager = _get_prestige_manager()
manager.prestige_state_changed.connect(_on_prestige_state_changed)
manager.prestige_performed.connect(_on_prestige_performed)
game_state.currency_changed.connect(_on_currency_changed)
_refresh_ui()
func _on_reset_button_pressed() -> void:
var manager: Node = _get_prestige_manager()
if manager == null or not manager.has_method("can_prestige"):
var manager: PrestigeManager = _get_prestige_manager()
if not bool(manager.can_prestige()):
return
if not bool(manager.call("can_prestige")):
return
var pending: BigNumber = manager.call("calculate_pending_gain")
var source_currency_id: StringName = manager.call("get_source_currency_id")
var source_currency_name: String = GameState.get_currency_name(source_currency_id)
var pending: BigNumber = manager.calculate_pending_gain()
var basis_label: String = String(manager.get_basis_label())
var basis_value: BigNumber = manager.get_basis_value_for_display()
var confirm_message: String = "Reset this run for +%s prestige?\n\nThis resets currencies, owned generators, and buff levels. Lifetime totals and prestige are kept." % pending.to_string_suffix(2)
if not source_currency_name.is_empty():
confirm_message = "%s\n\nBased on %s progression." % [confirm_message, source_currency_name]
confirm_message = "%s\n\nBased on %s: %s" % [confirm_message, basis_label, basis_value.to_string_suffix(2)]
_confirm_dialog.dialog_text = confirm_message
_is_waiting_for_confirm = true
_confirm_dialog.popup_centered_ratio(0.4)
func _on_save_button_pressed() -> void:
GameState.save_game()
if game_state:
game_state.save_game()
func _on_prestige_state_changed(_total: BigNumber, _pending: BigNumber) -> void:
_refresh_ui()
@@ -58,33 +58,33 @@ func _on_confirm_dialog_confirmed() -> void:
return
_is_waiting_for_confirm = false
var manager: Node = _get_prestige_manager()
var manager: PrestigeManager = _get_prestige_manager()
if manager == null:
return
manager.call("perform_prestige")
manager.perform_prestige()
func _on_confirm_dialog_canceled() -> void:
_is_waiting_for_confirm = false
func _refresh_ui() -> void:
var manager: Node = _get_prestige_manager()
var manager: PrestigeManager = _get_prestige_manager()
if manager == null:
visible = false
return
visible = true
var total: BigNumber = manager.call("get_total_prestige")
var pending: BigNumber = manager.call("calculate_pending_gain")
var multiplier: float = float(manager.call("get_total_multiplier"))
var basis_text: String = String(manager.call("get_basis_label"))
var basis_value: BigNumber = manager.call("get_basis_value_for_display")
var total: BigNumber = manager.get_total_prestige()
var pending: BigNumber = manager.calculate_pending_gain()
var multiplier: float = float(manager.get_total_multiplier())
var basis_text: String = String(manager.get_basis_label())
var basis_value: BigNumber = manager.get_basis_value_for_display()
_total_value.text = total.to_string_suffix(2)
_pending_value.text = pending.to_string_suffix(2)
_multiplier_value.text = "x%.2f" % multiplier
_basis_label.text = "%s (%s)" % [basis_text, basis_value.to_string_suffix(2)]
_reset_button.disabled = not bool(manager.call("can_prestige"))
_reset_button.disabled = not bool(manager.can_prestige())
func _get_prestige_manager() -> Node:
return get_node_or_null("/root/PrestigeManager")
func _get_prestige_manager() -> PrestigeManager:
return game_state.prestige_manager

View File

@@ -1,76 +1,83 @@
[gd_scene load_steps=2 format=3]
[gd_scene format=3 uid="uid://dlidx2x0otpjg"]
[ext_resource type="Script" path="res://core/prestige/prestige_panel.gd" id="1_7fdwq"]
[ext_resource type="Script" uid="uid://dmsmmgtbrul1t" path="res://core/prestige/prestige_panel.gd" id="1_panel"]
[ext_resource type="PackedScene" path="res://core/prestige/prestige_progress_bar.tscn" id="2_7b6yg"]
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="3_r8h5p"]
[node name="PrestigePanel" type="PanelContainer"]
[node name="PrestigePanel" type="PanelContainer" unique_id=789062217]
offset_right = 420.0
offset_bottom = 206.0
script = ExtResource("1_7fdwq")
script = ExtResource("1_panel")
[node name="MarginContainer" type="MarginContainer" parent="."]
[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1133221945]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1014101412]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"]
[node name="Title" type="Label" parent="MarginContainer/VBoxContainer" unique_id=738033951]
layout_mode = 2
text = "Prestige"
[node name="StatsGrid" type="GridContainer" parent="MarginContainer/VBoxContainer"]
[node name="StatsGrid" type="GridContainer" parent="MarginContainer/VBoxContainer" unique_id=1121463897]
layout_mode = 2
columns = 2
[node name="TotalTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="TotalTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1462148351]
layout_mode = 2
text = "Total"
[node name="TotalValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="TotalValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1607797034]
layout_mode = 2
text = "0"
[node name="PendingTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="PendingTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1999999111]
layout_mode = 2
text = "Pending"
[node name="PendingValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="PendingValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1731674897]
layout_mode = 2
text = "0"
[node name="MultiplierTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="MultiplierTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1338227256]
layout_mode = 2
text = "Multiplier"
[node name="MultiplierValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="MultiplierValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1407185183]
layout_mode = 2
text = "x1.00"
[node name="BasisTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="BasisTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1092652344]
layout_mode = 2
text = "Basis"
[node name="BasisValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"]
[node name="BasisValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1421212102]
layout_mode = 2
text = "-"
[node name="Buttons" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
[node name="PrestigeProgressBar" parent="MarginContainer/VBoxContainer" unique_id=619882824 instance=ExtResource("2_7b6yg")]
layout_mode = 2
config = ExtResource("3_r8h5p")
[node name="Buttons" type="HBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=634533362]
layout_mode = 2
theme_override_constants/separation = 8
[node name="ResetButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons"]
[node name="ResetButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons" unique_id=379577642]
layout_mode = 2
text = "Prestige Reset"
[node name="SaveButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons"]
[node name="SaveButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons" unique_id=1151017798]
layout_mode = 2
text = "Save"
[node name="ConfirmDialog" type="ConfirmationDialog" parent="."]
[node name="ConfirmDialog" type="ConfirmationDialog" parent="." unique_id=1893210048]
oversampling_override = 1.0
title = "Prestige Reset"
ok_button_text = "Prestige"
dialog_text = "Reset this run?"

View File

@@ -0,0 +1,72 @@
extends HBoxContainer
@export var config: PrestigeConfig
@onready var _label_start = $LabelStart
@onready var _label_end = $LabelEnd
@onready var _progress_bar = $ProgressBar
var _basis_value: BigNumber
var _next_threshold: BigNumber
var _connected: bool = false
var _game_state: LevelGameState
var _manager: PrestigeManager
func _ready() -> void:
_game_state = find_parent("LevelGameState")
_game_state.ready.connect(_on_game_state_ready)
func _on_game_state_ready() -> void:
_manager = _game_state.prestige_manager
if _manager == null:
push_warning("PrestigeProgressBar '%s' cannot find PrestigeManager; progress bar disabled." % String(name))
visible = false
return
if not _manager.has_signal("prestige_state_changed"):
push_warning("PrestigeManager does not have 'prestige_state_changed' signal; progress bar will not update.")
visible = false
return
if config == null:
config = _manager.get_config()
if config == null:
push_warning("PrestigeProgressBar '%s' has no config; progress bar disabled." % String(name))
visible = false
return
if _manager.has_signal("prestige_threshold_changed"):
_manager.prestige_threshold_changed.connect(_on_prestige_threshold_changed)
_manager.prestige_state_changed.connect(_on_prestige_state_changed)
_connected = true
_update_progress()
func _on_prestige_state_changed(_total: BigNumber, _pending: BigNumber) -> void:
_update_progress()
func _on_prestige_threshold_changed(_threshold: BigNumber) -> void:
_update_progress()
func _update_progress() -> void:
if not _connected:
return
if _manager == null or config == null:
return
_basis_value = _manager.get_basis_value_for_display()
_next_threshold = _manager.get_next_prestige_threshold()
var basis_float: float = _basis_value.to_float()
var threshold_float: float = _next_threshold.to_float()
if threshold_float > 0.0 and basis_float >= 0.0:
var ratio: float = minf(basis_float / threshold_float, 1.0)
_progress_bar.value = ratio * 100.0
else:
_progress_bar.value = 0.0
_label_start.text = _basis_value.to_string_suffix(2)
_label_end.text = _next_threshold.to_string_suffix(2)

View File

@@ -0,0 +1 @@
uid://dsq8yix7kkyaj

View File

@@ -0,0 +1,22 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://core/prestige/prestige_progress_bar.gd" id="1_pbg"]
[node name="PrestigeProgressBar" type="HBoxContainer"]
offset_right = 298.0
offset_bottom = 49.0
alignment = 1
script = ExtResource("1_pbg")
[node name="LabelStart" type="Label" parent="."]
layout_mode = 2
text = "0"
[node name="ProgressBar" type="ProgressBar" parent="."]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
show_percentage = true
[node name="LabelEnd" type="Label" parent="."]
layout_mode = 2
text = "0"