Add prestige graph
This commit is contained in:
224
TODO.md
224
TODO.md
@@ -1,20 +1,20 @@
|
||||
# TODO
|
||||
|
||||
## Ascension Buff Tree — Needs Review Before Implementation
|
||||
## Ascension Buff Graph — Needs Review Before Implementation
|
||||
|
||||
### Goal
|
||||
|
||||
Add a permanent buff tree system:
|
||||
Add a permanent buff graph system:
|
||||
- **Separate** from existing `GeneratorBuffData` (which resets on prestige).
|
||||
- **Permanent** — prestige does NOT reset ascension buff state.
|
||||
- **Unlocked** by spending ascension currency (earned during prestige).
|
||||
- **Tree/DAG-structured** — nodes have prerequisites (parent_ids) that must be met before a node can be purchased.
|
||||
- **Multi-level** — nodes can be one-shot unlocks (`max_level=1`) or repeatable upgrades (`max_level>1` or `-1` for infinite).
|
||||
- **Graph/DAG-structured** — nodes have prerequisites (parent_ids) that must be met before a node can be purchased.
|
||||
- **One-shot unlocks** — each node is either locked or unlocked. No repeatable levels.
|
||||
|
||||
### Data Structure
|
||||
|
||||
#### `AscensionBuffNode` (Resource)
|
||||
Single node in the tree.
|
||||
Single node in the graph.
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
@@ -22,17 +22,14 @@ Single node in the tree.
|
||||
| `display_name` | `String` | UI label |
|
||||
| `description` | `String` (multiline) | Tooltip |
|
||||
| `icon` | `Texture2D` | Visual |
|
||||
| `parent_ids` | `Array[StringName]` | Prerequisites (DAG, not strict tree — multiple parents allowed) |
|
||||
| `parent_ids` | `Array[StringName]` | Prerequisites (DAG — multiple parents allowed) |
|
||||
| `effect_type` | `enum EffectType` | What this buff does |
|
||||
| `effect_value` | `float` | Base effect magnitude |
|
||||
| `effect_per_level` | `float` | Extra effect per level beyond first (default 0.0) |
|
||||
| `effect_value` | `float` | Effect magnitude |
|
||||
| `target_id` | `StringName` | Target generator/currency for type-specific effects (empty = global) |
|
||||
| `cost_mantissa` | `float` | Ascension currency cost base |
|
||||
| `cost_mantissa` | `float` | Ascension currency cost |
|
||||
| `cost_exponent` | `int` | |
|
||||
| `cost_multiplier` | `float` | Cost scaling per level |
|
||||
| `max_level` | `int` | How many times it can be upgraded (1 = one-time, -1 = infinite) |
|
||||
| `tier` | `int` | Row in tree UI (visual grouping) |
|
||||
| `x_position` | `float` | Column in tree UI |
|
||||
| `tier` | `int` | Row in graph UI (visual grouping) |
|
||||
| `x_position` | `float` | Column in graph UI |
|
||||
|
||||
**Effect types** (extensible enum):
|
||||
|
||||
@@ -47,7 +44,7 @@ RESEARCH_XP_MULTIPLIER → faster research leveling
|
||||
```
|
||||
|
||||
#### `AscensionBuffCatalogue` (Resource)
|
||||
Flat list of nodes. The tree emerges from `parent_ids`. Follows existing catalogue pattern (`BuffCatalogue`, `GoalCatalogue`, etc.).
|
||||
Flat list of nodes. The graph emerges from `parent_ids`. Follows existing catalogue pattern (`BuffCatalogue`, `GoalCatalogue`, etc.).
|
||||
|
||||
| Field | Type |
|
||||
|-------|------|
|
||||
@@ -57,7 +54,7 @@ Flat list of nodes. The tree emerges from `parent_ids`. Follows existing catalog
|
||||
|
||||
```gdscript
|
||||
@export var ascension_buff_catalogue: AscensionBuffCatalogue
|
||||
var _ascension_buff_levels: Dictionary = {} # {StringName: int} — node_id → unlock count
|
||||
var _ascension_buff_unlocked: Dictionary = {} # {StringName: bool} — node_id → unlocked
|
||||
```
|
||||
|
||||
- Never cleared by `reset_for_prestige()` (unlike `_buff_levels` for generator buffs, which IS cleared).
|
||||
@@ -66,45 +63,46 @@ var _ascension_buff_levels: Dictionary = {} # {StringName: int} — node_id →
|
||||
### Save Format
|
||||
|
||||
- Bump `CURRENT_SAVE_FORMAT_VERSION` from 7 → 8.
|
||||
- New key: `"ascension_buff_levels"` → `{ "node_id": level_int, ... }`.
|
||||
- New key: `"ascension_buff_unlocked"` → array of unlocked node_id strings: `["strong_start", "efficiency"]`.
|
||||
|
||||
### API on LevelGameState
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `purchase_ascension_buff(buff_id) → bool` | Spend ascension currency, increment level, emit signal, save |
|
||||
| `can_purchase_ascension_buff(buff_id) → bool` | All parent_ids met? Under max_level? Enough currency? |
|
||||
| `get_ascension_buff_cost(buff_id) → BigNumber` | Cost for the NEXT level (scaled by `cost_multiplier`) |
|
||||
| `get_ascension_buff_level(buff_id) → int` | Current unlock count |
|
||||
| `get_ascension_buff_multiplier(effect_type, target_id) → float` | Walk tree, compute combined effect |
|
||||
| `get_available_ascension_buffs() → Array[AscensionBuffNode]` | Nodes with prerequisites met |
|
||||
| `purchase_ascension_buff(buff_id) → bool` | Spend ascension currency, unlock node, emit signal, save |
|
||||
| `can_purchase_ascension_buff(buff_id) → bool` | All parent_ids unlocked? Not already unlocked? Enough currency? |
|
||||
| `is_ascension_buff_unlocked(buff_id) → bool` | Check if node is unlocked |
|
||||
| `get_ascension_buff_cost(buff_id) → BigNumber` | Fixed cost for the node |
|
||||
| `get_ascension_buff_multiplier(effect_type, target_id) → float` | Walk graph, compute combined effect from unlocked nodes |
|
||||
| `get_available_ascension_buffs() → Array[AscensionBuffNode]` | Nodes whose prerequisites are all met |
|
||||
|
||||
### Signals
|
||||
|
||||
```gdscript
|
||||
signal ascension_buff_changed(buff_id: StringName, new_level: int)
|
||||
signal ascension_currency_changed(new_unspent: BigNumber)
|
||||
signal ascension_buff_unlocked(buff_id: StringName)
|
||||
```
|
||||
|
||||
Ascension currency balance changes are covered by the existing `currency_changed` signal — no separate signal needed.
|
||||
|
||||
### Prestige Integration
|
||||
|
||||
No changes needed to `PrestigeManager._reset_all_buff_levels()` — it only touches `_buff_levels` (generator buffs), not `_ascension_buff_levels`. The new dictionary naturally survives the reset.
|
||||
No changes needed to `PrestigeManager` — `_reset_all_buff_levels()` only touches `_buff_levels` (generator buffs), not `_ascension_buff_unlocked`. Ascension currency earning already emits `currency_changed` via `add_currency_by_id()`. The new dictionary naturally survives the reset.
|
||||
|
||||
### Purchase Flow
|
||||
|
||||
```
|
||||
Player clicks upgrade node
|
||||
Player clicks unlock node
|
||||
→ can_purchase_ascension_buff(node_id)
|
||||
→ All parent_ids at required level? (max_level for prerequisite, or just ≥1)
|
||||
→ Current level < max_level?
|
||||
→ All parent_ids unlocked?
|
||||
→ Not already unlocked?
|
||||
→ Enough ascension currency?
|
||||
→ YES: spend_currency_by_id(ascension_currency, cost)
|
||||
→ _ascension_buff_levels[node_id] += 1
|
||||
→ emit ascension_buff_changed
|
||||
→ _ascension_buff_unlocked[node_id] = true
|
||||
→ emit ascension_buff_unlocked
|
||||
→ save_game()
|
||||
```
|
||||
|
||||
### Example Tree Content (Illustrative)
|
||||
### Example Graph Content (Illustrative)
|
||||
|
||||
```
|
||||
Tier 0 (roots):
|
||||
@@ -127,15 +125,159 @@ Tier 2:
|
||||
|------|--------|---------|
|
||||
| `core/ascension/ascension_buff_node.gd` | Create | Node resource |
|
||||
| `core/ascension/ascension_buff_catalogue.gd` | Create | Catalogue resource |
|
||||
| `core/ascension/ascension_buff_tree_panel.gd` | Create | UI panel (or integrate into prestige panel) |
|
||||
| `core/level_game_state.gd` | Modify | Add catalogue ref, `_ascension_buff_levels`, purchase/query methods, save/load, signals |
|
||||
| `core/prestige/prestige_manager.gd` | Modify (light) | Emit `ascension_currency_changed` on spend/earn |
|
||||
| `docs/gyms/tiny_sword/ascension/*.tres` | Create | `.tres` resources for actual tree content |
|
||||
| `core/ascension/ascension_buff_graph_panel.gd` | Create | Separate-screen UI panel |
|
||||
| `core/level_game_state.gd` | Modify | Add catalogue ref, `_ascension_buff_unlocked`, purchase/query methods, save/load, signal |
|
||||
| `docs/gyms/tiny_sword/ascension/*.tres` | Create | `.tres` resources for actual graph content |
|
||||
|
||||
### Open Questions for Review
|
||||
---
|
||||
|
||||
1. **Prerequisite level requirement**: Should a parent need to be at `max_level`, or is level 1 sufficient? (Leaning: `max_level` is required, otherwise the tree loses structural meaning.)
|
||||
2. **Effect stacking**: For `GLOBAL_PRODUCTION_MULTIPLIER`, should separate nodes multiply together or add? (Leaning: multiply — consistent with the existing generator buff multiplier pattern.)
|
||||
3. **UI placement**: Should the tree panel be a tab in the prestige panel, a separate screen, or inlined somewhere?
|
||||
4. **Ascension currency unspent tracking**: Currently `PrestigeManager.current_prestige_unspent` tracks unspent prestige currency. Should the buff tree consume from this directly, or should ascension currency be a proper `Currency` in the catalogue that the tree spends via `LevelGameState.spend_currency_by_id()`? (Leaning: use `spend_currency_by_id()` for consistency — ascension currency is already added to the currency catalogue.)
|
||||
5. **Content**: What actual buffs and tree shape should the first version ship with?
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: `core/ascension/ascension_buff_node.gd`
|
||||
|
||||
Create new `Resource` class.
|
||||
|
||||
- `class_name AscensionBuffNode extends Resource`
|
||||
- `enum EffectType` with all seven types from the data structure table
|
||||
- `@export` fields: `id`, `display_name`, `description` (multiline), `icon`, `parent_ids`, `effect_type`, `effect_value`, `target_id`, `cost_mantissa`, `cost_exponent`, `tier`, `x_position`
|
||||
- Helper methods:
|
||||
- `get_cost() → BigNumber` — returns `BigNumber.new(cost_mantissa, cost_exponent)`
|
||||
- `has_parents() → bool` — returns `not parent_ids.is_empty()`
|
||||
- `all_parents_unlocked(unlocked: Dictionary) → bool` — checks every id in `parent_ids` is in the dict as `true`
|
||||
- `is_valid() → bool` — id not empty, cost_mantissa > 0
|
||||
|
||||
### Step 2: `core/ascension/ascension_buff_catalogue.gd`
|
||||
|
||||
Create new `Resource` class. Follows `BuffCatalogue` pattern exactly.
|
||||
|
||||
- `class_name AscensionBuffCatalogue extends Resource`
|
||||
- `@export var nodes: Array[AscensionBuffNode] = []`
|
||||
- `get_node_by_id(id: StringName) → AscensionBuffNode` — linear search
|
||||
- `get_all_ids() → Array[StringName]` — collect non-empty ids
|
||||
- `get_root_nodes() → Array[AscensionBuffNode]` — nodes with empty `parent_ids`
|
||||
- `get_children_of(parent_id: StringName) → Array[AscensionBuffNode]` — nodes that list this id in `parent_ids`
|
||||
|
||||
### Step 3: Modify `core/level_game_state.gd`
|
||||
|
||||
**3a. Add export, state, signal, constants**
|
||||
|
||||
- Add `@export var ascension_buff_catalogue: AscensionBuffCatalogue` in the export section
|
||||
- Add `var _ascension_buff_unlocked: Dictionary = {}` in state variables
|
||||
- Add signal: `signal ascension_buff_unlocked(buff_id: StringName)` in signals section
|
||||
- Add constant: `const ASCENSION_BUFF_UNLOCKED_KEY: String = "ascension_buff_unlocked"` in save/load constants
|
||||
- Bump `CURRENT_SAVE_FORMAT_VERSION` from 7 to 8
|
||||
|
||||
**3b. Add initialization in `_ready()`**
|
||||
|
||||
After `_initialize_catalogues()`, call a new `_initialize_ascension_buffs()` method that:
|
||||
- Iterates `ascension_buff_catalogue.nodes` (if catalogue is set)
|
||||
- For each node id not already in `_ascension_buff_unlocked`, sets it to `false`
|
||||
|
||||
**3c. Add save serialization**
|
||||
|
||||
In `save_game()`, add:
|
||||
```gdscript
|
||||
ASCENSION_BUFF_UNLOCKED_KEY: _serialize_ascension_buff_unlocked(),
|
||||
```
|
||||
|
||||
New private method `_serialize_ascension_buff_unlocked() → Array`:
|
||||
- Returns an array of node id strings where `_ascension_buff_unlocked[id] == true`
|
||||
|
||||
**3d. Add load deserialization**
|
||||
|
||||
In `load_game()`, under the save format version check, add:
|
||||
```gdscript
|
||||
if parsed_data.has(ASCENSION_BUFF_UNLOCKED_KEY):
|
||||
_deserialize_ascension_buff_unlocked(parsed_data.get(ASCENSION_BUFF_UNLOCKED_KEY))
|
||||
```
|
||||
|
||||
New private method `_deserialize_ascension_buff_unlocked(raw: Variant) → void`:
|
||||
- If raw is Array, for each string element set `_ascension_buff_unlocked[StringName(s)] = true`
|
||||
- Then call `_initialize_ascension_buffs()` to fill in any missing node ids as `false`
|
||||
|
||||
**3e. Add API methods**
|
||||
|
||||
In a new `#region ascension_buff` section:
|
||||
|
||||
- `purchase_ascension_buff(buff_id: StringName) → bool`
|
||||
- Guard: if not `can_purchase_ascension_buff(buff_id)`, return false
|
||||
- Get node from catalogue, get cost via `BigNumber` from node's mantissa/exponent
|
||||
- Call `spend_currency_by_id(ascension_currency_id, cost)` — use `get_ascension_currency_id()` helper
|
||||
- Set `_ascension_buff_unlocked[buff_id] = true`
|
||||
- Emit `ascension_buff_unlocked.emit(buff_id)`
|
||||
- `save_game()`
|
||||
- Return true
|
||||
|
||||
- `can_purchase_ascension_buff(buff_id: StringName) → bool`
|
||||
- Node must exist in catalogue
|
||||
- Not already unlocked (`_ascension_buff_unlocked.get(buff_id, false) == false`)
|
||||
- All parent_ids unlocked (use node's helper or iterate)
|
||||
- Enough ascension currency (check via `get_currency_amount_by_id(ascension_currency_id)`)
|
||||
|
||||
- `is_ascension_buff_unlocked(buff_id: StringName) → bool`
|
||||
- `return _ascension_buff_unlocked.get(buff_id, false)`
|
||||
|
||||
- `get_ascension_buff_cost(buff_id: StringName) → BigNumber`
|
||||
- Get node, return `BigNumber.new(node.cost_mantissa, node.cost_exponent)`
|
||||
|
||||
- `get_ascension_buff_multiplier(effect_type: int, target_id: StringName = &"") → float`
|
||||
- Iterate all unlocked nodes matching the effect type
|
||||
- Multiply their `effect_value` together
|
||||
- If `target_id` is non-empty, filter nodes whose `target_id` matches or is empty (global) — TBD based on type semantics
|
||||
- Return the product (starting from 1.0)
|
||||
|
||||
- `get_available_ascension_buffs() → Array[AscensionBuffNode]`
|
||||
- Iterate all catalogue nodes not yet unlocked where all parent_ids are unlocked
|
||||
|
||||
- `get_ascension_currency_id() → StringName` (private helper or public)
|
||||
- Look up the ascension currency ID from PrestigeManager's config, or use a well-known ID like `&"ascension"`. Prefer using the catalogue to determine it. For now, return `&"ascension"` and validate it exists in the currency catalogue.
|
||||
|
||||
**3f. Ensure prestige does NOT reset ascension buffs**
|
||||
|
||||
Verify `reset_for_prestige()` does not touch `_ascension_buff_unlocked`. It currently clears `_buff_levels` (generator buffs), `generator_states`, `research_xp`, `research_levels`, goals, etc. No change needed — `_ascension_buff_unlocked` is a separate dictionary that is not listed in the reset logic.
|
||||
|
||||
### Step 4: Create `core/ascension/ascension_buff_graph_panel.gd`
|
||||
|
||||
Create a new `PanelContainer`-based UI. This is a separate screen — it can be toggled visible/hidden (e.g., via a button in the main UI).
|
||||
|
||||
- `class_name AscensionBuffGraphPanel extends PanelContainer`
|
||||
- `@onready` references to:
|
||||
- Title label
|
||||
- `ScrollContainer`/`VBoxContainer` for tier rows
|
||||
- Ascension currency balance label
|
||||
- `_game_state: LevelGameState` — found via `find_parent("LevelGameState")`
|
||||
- In `_ready()`:
|
||||
- Connect `game_state.ascension_buff_unlocked` → refresh
|
||||
- Connect `game_state.currency_changed` → refresh ascension currency label
|
||||
- Call `_build_graph()`
|
||||
- `_build_graph()`:
|
||||
- Clear existing rows
|
||||
- Group all catalogue nodes by `tier`, sort tiers ascending
|
||||
- For each tier, create an `HBoxContainer` row
|
||||
- For each node in the tier (sorted by `x_position`), instantiate a tile scene
|
||||
- **Tile scene** (`ascension_buff_graph_tile.tscn` + `.gd`):
|
||||
- A small `Button`/`Panel` showing:
|
||||
- Node icon
|
||||
- Node display_name
|
||||
- Effect description (e.g., "+10% global production")
|
||||
- Cost in ascension currency
|
||||
- Visual state: locked (greyed out), available (highlighted, purchasable), unlocked (green check)
|
||||
- On press: call `_game_state.purchase_ascension_buff(node_id)`
|
||||
- Connect to `ascension_buff_unlocked` and `currency_changed` to re-evaluate state
|
||||
- Ascension currency label at the top showing current balance
|
||||
|
||||
### Step 5: Create content `.tres` files (deferred)
|
||||
|
||||
Content is not defined yet per resolved question 4. When ready:
|
||||
- Create `docs/gyms/tiny_sword/ascension/` directory
|
||||
- Create `ascension_buff_catalogue.tres` resource with nodes
|
||||
- Wire it into the `LevelGameState` node in `tiny_sword.tscn` via editor
|
||||
|
||||
---
|
||||
|
||||
### Resolved Questions
|
||||
|
||||
1. **Effect stacking**: Multiply — consistent with the existing generator buff multiplier pattern.
|
||||
2. **UI placement**: A separate screen.
|
||||
3. **Ascension currency spending**: Spend through `LevelGameState.spend_currency_by_id()` — ascension currency is already in the currency catalogue.
|
||||
4. **Content**: None at the moment — define later.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extends HBoxContainer
|
||||
|
||||
@export var currency: Resource
|
||||
@export var currency: Currency
|
||||
@export var game_state: LevelGameState
|
||||
|
||||
@onready var _label_start = $LabelStart
|
||||
|
||||
@@ -195,6 +195,16 @@ func get_ratio(target: BigNumber) -> float:
|
||||
static func from_float(val: float) -> BigNumber:
|
||||
return BigNumber.new(val, 0)
|
||||
|
||||
## Converts this BigNumber to a standard float. Returns INF or 0.0 for extreme values.
|
||||
func to_float() -> float:
|
||||
if mantissa <= 0.0:
|
||||
return 0.0
|
||||
if exponent > 308:
|
||||
return INF
|
||||
if exponent < -308:
|
||||
return 0.0
|
||||
return mantissa * pow(10.0, float(exponent))
|
||||
|
||||
## Outputs a UI-friendly string (e.g., "1.50e12")
|
||||
func to_string_sci(decimals: int = 2) -> String:
|
||||
if exponent < 3:
|
||||
|
||||
@@ -24,7 +24,7 @@ signal goal_achieved(generator_id: StringName, goal_id: StringName)
|
||||
const HUGE_COST_EXPONENT: int = 1000000
|
||||
|
||||
## Currency target updated by this generator.
|
||||
@export var currency: Resource
|
||||
@export var currency: Currency
|
||||
## Data resource containing balancing values and defaults (required).
|
||||
@export var data: CurrencyGeneratorData
|
||||
## Enables per-cycle automatic production when true.
|
||||
@@ -218,7 +218,7 @@ func buy_max() -> int:
|
||||
if purchase_currency_id == &"":
|
||||
return 0
|
||||
|
||||
var available_currency: float = _big_number_to_float(_get_currency_amount_by_id(purchase_currency_id))
|
||||
var available_currency: float = _get_currency_amount_by_id(purchase_currency_id).to_float()
|
||||
var estimated_max: int = data.max_affordable(owned, available_currency)
|
||||
if estimated_max <= 0:
|
||||
return 0
|
||||
@@ -355,7 +355,7 @@ func get_automatic_production_multiplier() -> float:
|
||||
return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER)
|
||||
|
||||
func get_effective_auto_run_multiplier() -> float:
|
||||
return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_multiplier()
|
||||
return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_buff_production_multiplier()
|
||||
|
||||
func get_research_multiplier() -> float:
|
||||
if data == null or data.research_data == null:
|
||||
@@ -477,25 +477,10 @@ func _get_click_cooldown_seconds() -> float:
|
||||
func _grants_click_while_hovering() -> bool:
|
||||
return grants_click_while_hovering
|
||||
|
||||
func _get_prestige_multiplier() -> float:
|
||||
func _get_prestige_buff_production_multiplier() -> float:
|
||||
if game_state == null:
|
||||
return 1.0
|
||||
|
||||
var parent: Node = get_parent()
|
||||
if parent == null:
|
||||
return 1.0
|
||||
|
||||
var prestige_manager: PrestigeManager = parent.find_child("PrestigeManager", true, false)
|
||||
if prestige_manager == null:
|
||||
return 1.0
|
||||
|
||||
var raw_multiplier: Variant = prestige_manager.get_total_multiplier()
|
||||
if raw_multiplier is float:
|
||||
return maxf(raw_multiplier, 0.0)
|
||||
if raw_multiplier is int:
|
||||
return maxf(float(raw_multiplier), 0.0)
|
||||
|
||||
return 1.0
|
||||
return game_state.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, _generator_id)
|
||||
|
||||
## Returns this generator's resolved state id.
|
||||
func get_generator_id() -> StringName:
|
||||
@@ -580,20 +565,13 @@ func _float_to_big_number(value: float) -> BigNumber:
|
||||
return BigNumber.from_float(value)
|
||||
|
||||
## Converts BigNumber to float for approximate estimate math.
|
||||
func _big_number_to_float(value: BigNumber) -> float:
|
||||
if value.mantissa <= 0.0:
|
||||
return 0.0
|
||||
if value.exponent > 308:
|
||||
return INF
|
||||
if value.exponent < -308:
|
||||
return 0.0
|
||||
return value.mantissa * pow(10.0, float(value.exponent))
|
||||
|
||||
|
||||
func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName:
|
||||
if buff == null:
|
||||
return &""
|
||||
|
||||
var cost_currency: Resource = buff.cost_currency if buff.cost_currency != null else currency
|
||||
var cost_currency: Currency = buff.cost_currency if buff.cost_currency != null else currency
|
||||
if cost_currency == null:
|
||||
return &""
|
||||
|
||||
@@ -611,7 +589,7 @@ func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName:
|
||||
if buff == null:
|
||||
return &""
|
||||
|
||||
var target_currency: Resource = buff.resource_target_currency if buff.resource_target_currency != null else currency
|
||||
var target_currency: Currency = buff.resource_target_currency if buff.resource_target_currency != null else currency
|
||||
if target_currency == null:
|
||||
return &""
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ func _refresh_buff_rows(can_interact: bool) -> void:
|
||||
var buff_unlocked: bool = _generator.is_buff_unlocked(buff.id)
|
||||
if not buff_unlocked:
|
||||
var locked_hint: String = "Locked"
|
||||
var unlock_goal_currency: Resource = buff.get_unlock_goal_currency()
|
||||
var unlock_goal_currency: Currency = buff.get_unlock_goal_currency()
|
||||
if buff.has_unlock_goal() and unlock_goal_currency != null and _generator.game_state:
|
||||
var goal_currency_id: StringName = _generator.game_state.get_currency_id(unlock_goal_currency)
|
||||
var currency_name: String = _generator.game_state.get_currency_name(goal_currency_id)
|
||||
|
||||
@@ -27,6 +27,8 @@ signal research_level_up(research_id: StringName, old_level: int, new_level: int
|
||||
signal worker_count_changed(currency_id: StringName, new_count: int)
|
||||
## Emitted when research workers change.
|
||||
signal research_workers_changed(new_count: int)
|
||||
## Emitted when an prestige buff node is unlocked.
|
||||
signal prestige_buff_unlocked(buff_id: StringName)
|
||||
|
||||
# ==============================================================================
|
||||
# EXPORTED REFERENCES
|
||||
@@ -39,6 +41,8 @@ signal research_workers_changed(new_count: int)
|
||||
@export var goal_catalogue: GoalCatalogue
|
||||
## Catalog of all defined research tracks in the game.
|
||||
@export var research_catalogue: ResearchCatalogue
|
||||
## Catalog of all prestige buff nodes in the graph.
|
||||
@export var prestige_buff_catalogue: PrestigeBuffCatalogue
|
||||
## File path for save/load persistence. Defaults to user directory.
|
||||
@export var save_file_path: String = "user://level_save.json"
|
||||
|
||||
@@ -78,6 +82,8 @@ var research_xp: Dictionary = {}
|
||||
var research_levels: Dictionary = {}
|
||||
## Workers assigned to research (total across all tracks).
|
||||
var research_workers: int = 0
|
||||
## Ascension buff unlock state: maps node_id (StringName) → bool (true if unlocked). Persists across prestige.
|
||||
var _prestige_buff_unlocked: Dictionary = {}
|
||||
|
||||
## Unix timestamp of last save.
|
||||
var last_save_time: int = 0
|
||||
@@ -88,7 +94,7 @@ var last_save_time: int = 0
|
||||
## JSON key for save format version.
|
||||
const SAVE_FORMAT_VERSION_KEY: String = "save_format_version"
|
||||
## Current save format version (7 includes research workers tracking).
|
||||
const CURRENT_SAVE_FORMAT_VERSION: int = 7
|
||||
const CURRENT_SAVE_FORMAT_VERSION: int = 8
|
||||
## JSON key for generator owned count.
|
||||
const GENERATOR_OWNED_KEY: String = "owned"
|
||||
## JSON key for generator purchased count.
|
||||
@@ -128,6 +134,8 @@ const RESEARCH_XP_KEY: String = "research_xp"
|
||||
const RESEARCH_LEVELS_KEY: String = "research_levels"
|
||||
## JSON key for research workers section.
|
||||
const RESEARCH_WORKERS_KEY: String = "research_workers"
|
||||
## JSON key for prestige buff unlocked array.
|
||||
const PRESTIGE_BUFF_UNLOCKED_KEY: String = "prestige_buff_unlocked"
|
||||
|
||||
## Reference to the PrestigeManager autoload node (set in _ready).
|
||||
var prestige_manager: PrestigeManager
|
||||
@@ -139,6 +147,7 @@ func _ready() -> void:
|
||||
|
||||
_initialize_currency_maps()
|
||||
_initialize_catalogues()
|
||||
_initialize_prestige_buffs()
|
||||
_load_catalogue_goals()
|
||||
load_game()
|
||||
_try_unlock_all_buffs_from_goals()
|
||||
@@ -184,7 +193,7 @@ func get_currency_name(currency_id: StringName) -> String:
|
||||
|
||||
## Returns the icon texture for a currency ID, or null if not found.
|
||||
func get_currency_icon(currency_id: StringName) -> Texture2D:
|
||||
var currency: Resource = get_currency_resource(currency_id)
|
||||
var currency: Currency = get_currency_resource(currency_id)
|
||||
if currency == null:
|
||||
return null
|
||||
var raw_icon: Variant = currency.icon
|
||||
@@ -627,6 +636,7 @@ func save_game() -> void:
|
||||
RESEARCH_XP_KEY: _serialize_research_xp(),
|
||||
RESEARCH_LEVELS_KEY: research_levels.duplicate(),
|
||||
RESEARCH_WORKERS_KEY: research_workers,
|
||||
PRESTIGE_BUFF_UNLOCKED_KEY: _serialize_prestige_buff_unlocked(),
|
||||
LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
@@ -682,11 +692,13 @@ func load_game() -> void:
|
||||
research_workers = int(parsed_data.get(RESEARCH_WORKERS_KEY, 0))
|
||||
|
||||
last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system())
|
||||
if parsed_data.has(PRESTIGE_BUFF_UNLOCKED_KEY):
|
||||
_deserialize_prestige_buff_unlocked(parsed_data.get(PRESTIGE_BUFF_UNLOCKED_KEY))
|
||||
for save_key_variant in parsed_data.keys():
|
||||
var save_key: String = String(save_key_variant)
|
||||
if save_key.is_empty():
|
||||
continue
|
||||
if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, GOALS_KEY, RESEARCH_XP_KEY, RESEARCH_LEVELS_KEY, RESEARCH_WORKERS_KEY, LAST_SAVE_TIME_KEY]:
|
||||
if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, GOALS_KEY, RESEARCH_XP_KEY, RESEARCH_LEVELS_KEY, RESEARCH_WORKERS_KEY, PRESTIGE_BUFF_UNLOCKED_KEY, LAST_SAVE_TIME_KEY]:
|
||||
continue
|
||||
|
||||
var section_payload: Variant = parsed_data.get(save_key_variant)
|
||||
@@ -819,6 +831,16 @@ func _initialize_catalogues() -> void:
|
||||
continue
|
||||
register_goal(goal)
|
||||
|
||||
## Internal helper: Initializes prestige buff unlock state from the catalogue, filling missing node IDs as false.
|
||||
func _initialize_prestige_buffs() -> void:
|
||||
if prestige_buff_catalogue == null:
|
||||
return
|
||||
for node in prestige_buff_catalogue.nodes:
|
||||
if node == null or node.id == &"":
|
||||
continue
|
||||
if not _prestige_buff_unlocked.has(node.id):
|
||||
_prestige_buff_unlocked[node.id] = false
|
||||
|
||||
## Internal helper: Loads goal definitions from the catalogue.
|
||||
func _load_catalogue_goals() -> void:
|
||||
if goal_catalogue:
|
||||
@@ -1297,6 +1319,99 @@ func _try_unlock_all_buffs_from_goals() -> void:
|
||||
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PRESTIGE BUFF API
|
||||
# ==============================================================================
|
||||
|
||||
## Returns the ascension currency ID used for purchasing buff nodes.
|
||||
func get_prestige_currency_id() -> StringName:
|
||||
return &"ascension"
|
||||
|
||||
## Attempts to purchase and unlock an prestige buff node. Returns true on success.
|
||||
func purchase_prestige_buff(buff_id: StringName) -> bool:
|
||||
if not can_purchase_prestige_buff(buff_id):
|
||||
return false
|
||||
|
||||
var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id)
|
||||
if node == null:
|
||||
return false
|
||||
|
||||
var cost: BigNumber = node.get_cost()
|
||||
var currency_id: StringName = get_prestige_currency_id()
|
||||
if not spend_currency_by_id(currency_id, cost):
|
||||
return false
|
||||
|
||||
_prestige_buff_unlocked[buff_id] = true
|
||||
prestige_buff_unlocked.emit(buff_id)
|
||||
save_game()
|
||||
return true
|
||||
|
||||
## Checks whether an prestige buff node can be purchased (all parents unlocked, not already unlocked, enough currency).
|
||||
func can_purchase_prestige_buff(buff_id: StringName) -> bool:
|
||||
var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id)
|
||||
if node == null:
|
||||
return false
|
||||
|
||||
if is_prestige_buff_unlocked(buff_id):
|
||||
return false
|
||||
|
||||
if not node.all_parents_unlocked(_prestige_buff_unlocked):
|
||||
return false
|
||||
|
||||
var cost: BigNumber = node.get_cost()
|
||||
var currency_id: StringName = get_prestige_currency_id()
|
||||
var balance: BigNumber = get_currency_amount_by_id(currency_id)
|
||||
return balance.is_greater_than(cost) or balance.is_equal_to(cost)
|
||||
|
||||
## Returns true if the given prestige buff node is unlocked.
|
||||
func is_prestige_buff_unlocked(buff_id: StringName) -> bool:
|
||||
return bool(_prestige_buff_unlocked.get(buff_id, false))
|
||||
|
||||
## Returns the fixed BigNumber cost for an prestige buff node.
|
||||
func get_prestige_buff_cost(buff_id: StringName) -> BigNumber:
|
||||
var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id)
|
||||
if node == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
return node.get_cost()
|
||||
|
||||
## Computes the combined multiplier from all unlocked prestige buff nodes matching the given effect type.
|
||||
## [param target_id] filters to nodes targeting this ID (or global nodes when empty).
|
||||
func get_prestige_buff_multiplier(effect_type: int, target_id: StringName = &"") -> float:
|
||||
var multiplier: float = 1.0
|
||||
for node_id in _prestige_buff_unlocked.keys():
|
||||
if not bool(_prestige_buff_unlocked.get(node_id, false)):
|
||||
continue
|
||||
var node: PrestigeBuffNode = _get_prestige_buff_node(node_id)
|
||||
if node == null:
|
||||
continue
|
||||
if node.effect_type != effect_type:
|
||||
continue
|
||||
if target_id != &"" and node.target_id != &"" and node.target_id != target_id:
|
||||
continue
|
||||
multiplier *= node.effect_value
|
||||
return multiplier
|
||||
|
||||
## Returns all prestige buff nodes whose prerequisites are met but are not yet unlocked.
|
||||
func get_available_prestige_buffs() -> Array[PrestigeBuffNode]:
|
||||
if prestige_buff_catalogue == null:
|
||||
return []
|
||||
|
||||
var available: Array[PrestigeBuffNode] = []
|
||||
for node in prestige_buff_catalogue.nodes:
|
||||
if node == null or node.id == &"":
|
||||
continue
|
||||
if is_prestige_buff_unlocked(node.id):
|
||||
continue
|
||||
if node.all_parents_unlocked(_prestige_buff_unlocked):
|
||||
available.append(node)
|
||||
return available
|
||||
|
||||
## Returns the PrestigeBuffNode resource for a given ID, or null if not found.
|
||||
func _get_prestige_buff_node(buff_id: StringName) -> PrestigeBuffNode:
|
||||
if prestige_buff_catalogue == null:
|
||||
return null
|
||||
return prestige_buff_catalogue.get_node_by_id(buff_id)
|
||||
|
||||
# ==============================================================================
|
||||
# PERSISTENCE HELPERS
|
||||
# ==============================================================================
|
||||
@@ -1394,3 +1509,26 @@ func _deserialize_research_xp(raw: Variant) -> Dictionary:
|
||||
if serialized is Dictionary:
|
||||
result[research_id] = BigNumber.deserialize(serialized)
|
||||
return result
|
||||
|
||||
## Serializes prestige buff unlock state as an array of unlocked node IDs.
|
||||
func _serialize_prestige_buff_unlocked() -> Array:
|
||||
var unlocked: Array = []
|
||||
for node_id in _prestige_buff_unlocked.keys():
|
||||
if _prestige_buff_unlocked.get(node_id, false):
|
||||
unlocked.append(node_id)
|
||||
return unlocked
|
||||
|
||||
## Deserializes prestige buff unlock state from an array of unlocked node ID strings, then fills missing catalogue entries.
|
||||
func _deserialize_prestige_buff_unlocked(raw: Variant) -> void:
|
||||
_prestige_buff_unlocked.clear()
|
||||
if raw is Array:
|
||||
for element in raw:
|
||||
var node_id: StringName = StringName(String(element))
|
||||
if node_id != &"":
|
||||
_prestige_buff_unlocked[node_id] = true
|
||||
_initialize_prestige_buffs()
|
||||
|
||||
# Notify tiles that were waiting for state to load
|
||||
for node_id in _prestige_buff_unlocked.keys():
|
||||
if _prestige_buff_unlocked.get(node_id, false):
|
||||
prestige_buff_unlocked.emit(node_id)
|
||||
|
||||
@@ -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
|
||||
|
||||
40
core/prestige/prestige_buff_catalogue.gd
Normal file
40
core/prestige/prestige_buff_catalogue.gd
Normal 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
|
||||
1
core/prestige/prestige_buff_catalogue.gd.uid
Normal file
1
core/prestige/prestige_buff_catalogue.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bjcvnyh2bc6l5
|
||||
44
core/prestige/prestige_buff_connection_overlay.gd
Normal file
44
core/prestige/prestige_buff_connection_overlay.gd
Normal file
@@ -0,0 +1,44 @@
|
||||
## 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
|
||||
1
core/prestige/prestige_buff_connection_overlay.gd.uid
Normal file
1
core/prestige/prestige_buff_connection_overlay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://uv5lxj6hmpk
|
||||
85
core/prestige/prestige_buff_graph_panel.gd
Normal file
85
core/prestige/prestige_buff_graph_panel.gd
Normal 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)
|
||||
|
||||
|
||||
## Recursively collects all PrestigeBuffGraphTile nodes from a container.
|
||||
func _collect_tiles(from: Node, out_tiles: Array[PrestigeBuffGraphTile]) -> void:
|
||||
for child in from.get_children():
|
||||
if child is PrestigeBuffGraphTile:
|
||||
out_tiles.append(child)
|
||||
elif child is Container:
|
||||
_collect_tiles(child, out_tiles)
|
||||
|
||||
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()
|
||||
1
core/prestige/prestige_buff_graph_panel.gd.uid
Normal file
1
core/prestige/prestige_buff_graph_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://n01pkakxllnj
|
||||
122
core/prestige/prestige_buff_graph_tile.gd
Normal file
122
core/prestige/prestige_buff_graph_tile.gd
Normal 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()
|
||||
1
core/prestige/prestige_buff_graph_tile.gd.uid
Normal file
1
core/prestige/prestige_buff_graph_tile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://w8ogrp1s6qsf
|
||||
40
core/prestige/prestige_buff_graph_tile.tscn
Normal file
40
core/prestige/prestige_buff_graph_tile.tscn
Normal 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
|
||||
69
core/prestige/prestige_buff_node.gd
Normal file
69
core/prestige/prestige_buff_node.gd
Normal 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
|
||||
1
core/prestige/prestige_buff_node.gd.uid
Normal file
1
core/prestige/prestige_buff_node.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://mjyig8xlgki0
|
||||
@@ -24,7 +24,7 @@ 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)
|
||||
@@ -36,7 +36,7 @@ var last_reset_time: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
if config == null:
|
||||
config = load("res://docs/gyms/tiny_sword/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.")
|
||||
@@ -58,7 +58,7 @@ func _ready() -> void:
|
||||
prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain())
|
||||
prestige_threshold_changed.emit(get_next_prestige_threshold())
|
||||
|
||||
func get_config() -> Resource:
|
||||
func get_config() -> PrestigeConfig:
|
||||
return config
|
||||
|
||||
func get_source_currency_id() -> StringName:
|
||||
@@ -110,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
|
||||
|
||||
@@ -209,7 +209,7 @@ func get_next_prestige_threshold() -> BigNumber:
|
||||
BASIS_RUN_TOTAL, BASIS_ALL_CURRENCIES:
|
||||
return _get_config_threshold()
|
||||
_:
|
||||
var current_total: float = _big_number_to_float(total_prestige_earned)
|
||||
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)
|
||||
|
||||
@@ -260,7 +260,7 @@ func _calculate_target_for_prestige(prestige_level: float) -> float:
|
||||
if config == null:
|
||||
return 0.0
|
||||
|
||||
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
|
||||
|
||||
@@ -289,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
|
||||
|
||||
@@ -303,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())
|
||||
@@ -312,7 +312,7 @@ 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:
|
||||
@@ -449,9 +449,11 @@ func _reset_all_buff_levels() -> void:
|
||||
if game_state == null:
|
||||
return
|
||||
|
||||
for buff_id in game_state._buff_levels.keys():
|
||||
game_state.set_buff_level(buff_id, 0)
|
||||
game_state.set_buff_unlocked(buff_id, false)
|
||||
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
|
||||
@@ -489,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()
|
||||
@@ -514,35 +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"))
|
||||
|
||||
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:
|
||||
|
||||
@@ -30,7 +30,6 @@ func _on_game_state_ready() -> void:
|
||||
return
|
||||
|
||||
if config == null:
|
||||
if _manager.has_method("get_config"):
|
||||
config = _manager.get_config()
|
||||
|
||||
if config == null:
|
||||
@@ -57,14 +56,11 @@ func _update_progress() -> void:
|
||||
if _manager == null or config == null:
|
||||
return
|
||||
|
||||
_basis_value = _manager.call("get_basis_value_for_display")
|
||||
if _manager.has_method("get_next_prestige_threshold"):
|
||||
_next_threshold = _manager.call("get_next_prestige_threshold")
|
||||
else:
|
||||
_next_threshold = _get_threshold_from_config()
|
||||
_basis_value = _manager.get_basis_value_for_display()
|
||||
_next_threshold = _manager.get_next_prestige_threshold()
|
||||
|
||||
var basis_float: float = _big_number_to_float(_basis_value)
|
||||
var threshold_float: float = _big_number_to_float(_next_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)
|
||||
@@ -74,25 +70,3 @@ func _update_progress() -> void:
|
||||
|
||||
_label_start.text = _basis_value.to_string_suffix(2)
|
||||
_label_end.text = _next_threshold.to_string_suffix(2)
|
||||
|
||||
func _get_threshold_from_config() -> BigNumber:
|
||||
if config == null:
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
if not config.has_method("get_threshold"):
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
var threshold = config.call("get_threshold")
|
||||
if threshold is BigNumber:
|
||||
return threshold
|
||||
|
||||
return BigNumber.from_float(0.0)
|
||||
|
||||
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))
|
||||
|
||||
@@ -23,15 +23,16 @@ position = Vector2(1, 22)
|
||||
shape = SubResource("RectangleShape2D_tgvch")
|
||||
|
||||
[node name="PrestigePanel" parent="." unique_id=245519778 instance=ExtResource("3_l7gct")]
|
||||
visible = false
|
||||
offset_left = 155.0
|
||||
offset_top = -80.0
|
||||
offset_right = 575.0
|
||||
offset_bottom = 126.0
|
||||
offset_left = 161.0
|
||||
offset_top = -232.0
|
||||
offset_right = 581.0
|
||||
offset_bottom = -3.0
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=549346899]
|
||||
offset_right = 415.0
|
||||
offset_bottom = 196.0
|
||||
offset_left = 160.0
|
||||
offset_top = 5.0
|
||||
offset_right = 575.0
|
||||
offset_bottom = 201.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
|
||||
10
docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres
Normal file
10
docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres
Normal file
@@ -0,0 +1,10 @@
|
||||
[gd_resource type="Resource" script_class="PrestigeBuffCatalogue" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://core/prestige/prestige_buff_node.gd" id="1_node"]
|
||||
[ext_resource type="Script" path="res://core/prestige/prestige_buff_catalogue.gd" id="2_cat"]
|
||||
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres" id="3_gold"]
|
||||
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres" id="4_farm"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_cat")
|
||||
nodes = Array[ExtResource("1_node")]([ExtResource("3_gold"), ExtResource("4_farm")])
|
||||
12
docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres
Normal file
12
docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres
Normal file
@@ -0,0 +1,12 @@
|
||||
[gd_resource type="Resource" script_class="PrestigeBuffNode" format=3 uid="uid://cvogxo0msis2w"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://mjyig8xlgki0" path="res://core/prestige/prestige_buff_node.gd" id="1_script"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_script")
|
||||
id = &"farm_boost"
|
||||
display_name = "Fertile Grounds"
|
||||
description = "Permanently increases farm food production."
|
||||
effect_value = 1.5
|
||||
target_id = &"farm"
|
||||
x_position = 1.0
|
||||
11
docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres
Normal file
11
docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres
Normal file
@@ -0,0 +1,11 @@
|
||||
[gd_resource type="Resource" script_class="PrestigeBuffNode" format=3 uid="uid://vl3ot4t876pc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://mjyig8xlgki0" path="res://core/prestige/prestige_buff_node.gd" id="1_script"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_script")
|
||||
id = &"gold_boost"
|
||||
display_name = "Golden Touch"
|
||||
description = "Permanently increases gold mine production."
|
||||
effect_value = 1.5
|
||||
target_id = &"goldmine"
|
||||
76
docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn
Normal file
76
docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn
Normal file
@@ -0,0 +1,76 @@
|
||||
[gd_scene format=3 uid="uid://dprbgokqitaav"]
|
||||
|
||||
[ext_resource type="Script" path="res://core/prestige/prestige_buff_graph_panel.gd" id="1_panel"]
|
||||
[ext_resource type="Script" path="res://core/prestige/prestige_buff_connection_overlay.gd" id="2_overlay"]
|
||||
[ext_resource type="PackedScene" path="res://core/prestige/prestige_buff_graph_tile.tscn" id="3_tile"]
|
||||
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres" id="4_gold"]
|
||||
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres" id="5_farm"]
|
||||
|
||||
[node name="PrestigeBuffGraphPanel" type="PanelContainer" unique_id=947835716]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(600, 400)
|
||||
script = ExtResource("1_panel")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="." unique_id=829451203]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 12
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 12
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=637192845]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=372819465]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer/Header" unique_id=518293746]
|
||||
layout_mode = 2
|
||||
text = "Prestige Buffs"
|
||||
theme_override_font_sizes/font_size = 20
|
||||
|
||||
[node name="CloseButton" type="Button" parent="MarginContainer/VBoxContainer/Header" unique_id=184756392]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 10
|
||||
text = "✕"
|
||||
theme_override_font_sizes/font_size = 16
|
||||
|
||||
[node name="BalanceLabel" type="Label" parent="MarginContainer/VBoxContainer" unique_id=293847561]
|
||||
layout_mode = 2
|
||||
text = "Prestige Currency: 0"
|
||||
theme_override_font_sizes/font_size = 14
|
||||
|
||||
[node name="TilesArea" type="Control" parent="MarginContainer/VBoxContainer" unique_id=847291034]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
mouse_filter = 1
|
||||
|
||||
[node name="ConnectionOverlay" type="Control" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=563829104]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 1
|
||||
grow_vertical = 1
|
||||
mouse_filter = 2
|
||||
script = ExtResource("2_overlay")
|
||||
|
||||
[node name="TiersContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=129384756]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 1
|
||||
grow_vertical = 1
|
||||
|
||||
[node name="Tier0" type="HBoxContainer" parent="MarginContainer/VBoxContainer/TilesArea/TiersContainer" unique_id=384756192]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="GoldBoostTile" parent="MarginContainer/VBoxContainer/TilesArea/TiersContainer/Tier0" unique_id=756192834 instance=ExtResource("3_tile")]
|
||||
layout_mode = 2
|
||||
prestige_buff = ExtResource("4_gold")
|
||||
|
||||
[node name="FarmBoostTile" parent="MarginContainer/VBoxContainer/TilesArea/TiersContainer/Tier0" unique_id=619283475 instance=ExtResource("3_tile")]
|
||||
layout_mode = 2
|
||||
prestige_buff = ExtResource("5_farm")
|
||||
11
docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres
Normal file
11
docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres
Normal file
@@ -0,0 +1,11 @@
|
||||
[gd_resource type="Resource" script_class="PrestigeBuffNode" format=3 uid="uid://bif136o485oqh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://mjyig8xlgki0" path="res://core/prestige/prestige_buff_node.gd" id="1_42hk5"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_42hk5")
|
||||
id = &"wood_boost"
|
||||
display_name = "Wood Touch"
|
||||
description = "Permanently increases forestry production."
|
||||
effect_value = 1.5
|
||||
target_id = &"forestry"
|
||||
@@ -8,6 +8,7 @@
|
||||
[ext_resource type="Resource" uid="uid://cgt1mjir1v4br" path="res://docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres" id="4_x77df"]
|
||||
[ext_resource type="Resource" uid="uid://umi37hotcq8m" path="res://docs/gyms/tiny_sword/research/ts_research_catalogue.tres" id="5_v0pty"]
|
||||
[ext_resource type="Script" uid="uid://srkiu4qe8s2m" path="res://core/prestige/prestige_manager.gd" id="5_x77df"]
|
||||
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres" id="6_asc"]
|
||||
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="6_xnhlc"]
|
||||
[ext_resource type="PackedScene" path="res://docs/gyms/tiny_sword/currency_panel.tscn" id="9_cpnl"]
|
||||
[ext_resource type="PackedScene" uid="uid://djedqovgngrx5" path="res://docs/gyms/tiny_sword/buildings/farm/farm.tscn" id="10_1lv5i"]
|
||||
@@ -17,6 +18,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://rejxvjwybkll" path="res://sandbox/tiny_swords/Terrain/Resources/Wood/Trees/tree_1.tscn" id="14_0cs5o"]
|
||||
[ext_resource type="PackedScene" uid="uid://bp5ng4vu4ot4a" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.tscn" id="14_hum8s"]
|
||||
[ext_resource type="Script" uid="uid://b6systw05frh0" path="res://core/edge_scroll_camera.gd" id="17_x77df"]
|
||||
[ext_resource type="PackedScene" uid="uid://dprbgokqitaav" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn" id="18_pnl"]
|
||||
|
||||
[node name="TinySwords" type="Node" unique_id=498237642]
|
||||
|
||||
@@ -26,6 +28,7 @@ currency_catalogue = ExtResource("2_dwrof")
|
||||
buff_catalogue = ExtResource("3_wl838")
|
||||
goal_catalogue = ExtResource("4_x77df")
|
||||
research_catalogue = ExtResource("5_v0pty")
|
||||
prestige_buff_catalogue = ExtResource("6_asc")
|
||||
save_file_path = "user://tiny_sword_save.json"
|
||||
|
||||
[node name="PrestigeManager" type="Node" parent="LevelGameState" unique_id=1019028144 node_paths=PackedStringArray("game_state")]
|
||||
@@ -41,6 +44,10 @@ anchors_preset = 0
|
||||
|
||||
[node name="CurrencyPanel" parent="LevelGameState/CanvasLayer/UI" unique_id=2000000001 instance=ExtResource("9_cpnl")]
|
||||
layout_mode = 1
|
||||
offset_left = 0.0
|
||||
offset_top = 429.0
|
||||
offset_right = 250.0
|
||||
offset_bottom = 769.0
|
||||
|
||||
[node name="GoalsDebugUI" parent="LevelGameState/CanvasLayer/UI" unique_id=1494700185 instance=ExtResource("10_qifrv")]
|
||||
visible = false
|
||||
@@ -50,6 +57,22 @@ offset_top = 5.0
|
||||
offset_right = 1913.0
|
||||
offset_bottom = 263.0
|
||||
|
||||
[node name="PrestigeBuffToggle" type="Button" parent="LevelGameState/CanvasLayer/UI" unique_id=293847102]
|
||||
layout_mode = 1
|
||||
offset_left = 1730.0
|
||||
offset_top = 64.0
|
||||
offset_right = 1857.0
|
||||
offset_bottom = 100.0
|
||||
toggle_mode = true
|
||||
text = "Prestige Buffs"
|
||||
|
||||
[node name="PrestigeBuffGraphPanel" parent="LevelGameState/CanvasLayer/UI" unique_id=847291035 instance=ExtResource("18_pnl")]
|
||||
layout_mode = 1
|
||||
offset_left = 400.0
|
||||
offset_top = 200.0
|
||||
offset_right = 1000.0
|
||||
offset_bottom = 700.0
|
||||
|
||||
[node name="World" type="Node2D" parent="LevelGameState" unique_id=2118297724]
|
||||
|
||||
[node name="ForestProps" type="Node2D" parent="LevelGameState/World" unique_id=649424542]
|
||||
@@ -97,3 +120,5 @@ script = ExtResource("17_x77df")
|
||||
clamp_enabled = true
|
||||
clamp_min = Vector2(-1920, -1080)
|
||||
clamp_max = Vector2(1920, 1080)
|
||||
|
||||
[connection signal="pressed" from="LevelGameState/CanvasLayer/UI/PrestigeBuffToggle" to="LevelGameState/CanvasLayer/UI/PrestigeBuffGraphPanel" method="_on_prestige_buff_toggle_pressed"]
|
||||
|
||||
318
tests/test_ascension.gd
Normal file
318
tests/test_ascension.gd
Normal file
@@ -0,0 +1,318 @@
|
||||
extends Node
|
||||
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
var _game_root: Node = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await run()
|
||||
|
||||
|
||||
func run() -> void:
|
||||
print("\n=== TEST: Ascension Buff System ===\n")
|
||||
TestUtils.set_test_name("ascension")
|
||||
|
||||
_test_node_resource()
|
||||
_test_node_validation()
|
||||
_test_node_parent_checking()
|
||||
_test_catalogue_queries()
|
||||
_test_purchase_flow()
|
||||
_test_prestige_does_not_reset_prestige_buffs()
|
||||
_test_multiplier_computation()
|
||||
_test_save_load_roundtrip()
|
||||
|
||||
_print_summary()
|
||||
|
||||
|
||||
#region Node Resource Tests
|
||||
func _test_node_resource() -> void:
|
||||
print("--- Node Resource ---")
|
||||
|
||||
var node: PrestigeBuffNode = PrestigeBuffNode.new()
|
||||
node.id = &"test_node"
|
||||
node.display_name = "Test Node"
|
||||
node.effect_type = PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER
|
||||
node.effect_value = 2.0
|
||||
node.cost_mantissa = 5.0
|
||||
node.cost_exponent = 2 # cost = 5e2 = 500
|
||||
|
||||
var cost: BigNumber = node.get_cost()
|
||||
TestUtils.assert_equals(5.0, cost.mantissa, "cost mantissa matches")
|
||||
TestUtils.assert_equals(2, cost.exponent, "cost exponent matches")
|
||||
TestUtils.assert_false(node.has_parents(), "node with no parents returns false")
|
||||
|
||||
|
||||
func _test_node_validation() -> void:
|
||||
print("--- Node Validation ---")
|
||||
|
||||
var valid: PrestigeBuffNode = PrestigeBuffNode.new()
|
||||
valid.id = &"valid_id"
|
||||
valid.cost_mantissa = 1.0
|
||||
TestUtils.assert_true(valid.is_valid(), "node with id and positive cost is valid")
|
||||
|
||||
var no_id: PrestigeBuffNode = PrestigeBuffNode.new()
|
||||
no_id.cost_mantissa = 1.0
|
||||
TestUtils.assert_false(no_id.is_valid(), "node with empty id is invalid")
|
||||
|
||||
var zero_cost: PrestigeBuffNode = PrestigeBuffNode.new()
|
||||
zero_cost.id = &"has_id"
|
||||
zero_cost.cost_mantissa = 0.0
|
||||
TestUtils.assert_false(zero_cost.is_valid(), "node with zero cost is invalid")
|
||||
|
||||
|
||||
func _test_node_parent_checking() -> void:
|
||||
print("--- Parent Checking ---")
|
||||
|
||||
var unlocked: Dictionary = {}
|
||||
unlocked[&"parent_a"] = true
|
||||
unlocked[&"parent_b"] = false
|
||||
|
||||
var node: PrestigeBuffNode = PrestigeBuffNode.new()
|
||||
node.id = &"child"
|
||||
node.parent_ids = [&"parent_a"]
|
||||
|
||||
TestUtils.assert_true(node.all_parents_unlocked(unlocked), "all parents unlocked when all are true")
|
||||
|
||||
node.parent_ids = [&"parent_a", &"parent_b"]
|
||||
TestUtils.assert_false(node.all_parents_unlocked(unlocked), "not all parents unlocked when one is false")
|
||||
|
||||
node.parent_ids = [&"nonexistent"]
|
||||
TestUtils.assert_false(node.all_parents_unlocked(unlocked), "missing parent treated as not unlocked")
|
||||
#endregion
|
||||
|
||||
|
||||
#region Catalogue Tests
|
||||
func _test_catalogue_queries() -> void:
|
||||
print("--- Catalogue Queries ---")
|
||||
|
||||
var root: PrestigeBuffNode = _make_node(&"root", [], 0, 0.0)
|
||||
var child_a: PrestigeBuffNode = _make_node(&"child_a", [&"root"], 1, 0.0)
|
||||
var child_b: PrestigeBuffNode = _make_node(&"child_b", [&"root"], 1, 1.0)
|
||||
var grandchild: PrestigeBuffNode = _make_node(&"grandchild", [&"child_a", &"child_b"], 2, 0.0)
|
||||
|
||||
var cat: PrestigeBuffCatalogue = PrestigeBuffCatalogue.new()
|
||||
cat.nodes = [root, child_a, child_b, grandchild]
|
||||
|
||||
TestUtils.assert_equals(root, cat.get_node_by_id(&"root"), "get_node_by_id finds root")
|
||||
TestUtils.assert_equals(null, cat.get_node_by_id(&"nonexistent"), "get_node_by_id returns null for unknown id")
|
||||
|
||||
var ids: Array[StringName] = cat.get_all_ids()
|
||||
TestUtils.assert_equals(4, ids.size(), "get_all_ids returns 4 ids")
|
||||
|
||||
var roots: Array[PrestigeBuffNode] = cat.get_root_nodes()
|
||||
TestUtils.assert_equals(1, roots.size(), "one root node")
|
||||
TestUtils.assert_equals(&"root", roots[0].id, "root node id is 'root'")
|
||||
|
||||
var children: Array[PrestigeBuffNode] = cat.get_children_of(&"root")
|
||||
TestUtils.assert_equals(2, children.size(), "root has 2 children")
|
||||
|
||||
var grandkids: Array[PrestigeBuffNode] = cat.get_children_of(&"child_a")
|
||||
TestUtils.assert_equals(1, grandkids.size(), "child_a has 1 child")
|
||||
#endregion
|
||||
|
||||
|
||||
#region Purchase Flow Tests
|
||||
func _test_purchase_flow() -> void:
|
||||
print("--- Purchase Flow ---")
|
||||
|
||||
var scene: PackedScene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
|
||||
_game_root = scene.instantiate()
|
||||
add_child(_game_root)
|
||||
await _wait()
|
||||
|
||||
var gs: LevelGameState = _game_root.find_child("LevelGameState")
|
||||
if gs == null:
|
||||
print("[ERROR] LevelGameState not found")
|
||||
return
|
||||
|
||||
# Give ascension currency for purchasing
|
||||
gs.add_currency_by_id(&"ascension", BigNumber.from_float(100.0))
|
||||
await _wait()
|
||||
|
||||
# Verify catalogue has our two nodes
|
||||
var cat: PrestigeBuffCatalogue = gs.prestige_buff_catalogue
|
||||
TestUtils.assert_not_null(cat, "prestige_buff_catalogue is set")
|
||||
if cat == null:
|
||||
return
|
||||
|
||||
TestUtils.assert_true(cat.nodes.size() >= 2, "catalogue has at least 2 nodes")
|
||||
|
||||
# Verify nodes initialized in state
|
||||
TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost starts locked")
|
||||
TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost starts locked")
|
||||
|
||||
# Check can_purchase (both are root nodes with no parents, should be purchaseable)
|
||||
TestUtils.assert_true(gs.can_purchase_prestige_buff(&"gold_boost"), "gold_boost is purchasable")
|
||||
TestUtils.assert_true(gs.can_purchase_prestige_buff(&"farm_boost"), "farm_boost is purchasable")
|
||||
|
||||
# Check cost
|
||||
var cost: BigNumber = gs.get_prestige_buff_cost(&"gold_boost")
|
||||
TestUtils.assert_greater_than(cost.mantissa, 0.0, "gold_boost cost > 0")
|
||||
|
||||
# Purchase gold_boost
|
||||
var purchased: bool = gs.purchase_prestige_buff(&"gold_boost")
|
||||
TestUtils.assert_true(purchased, "purchase_prestige_buff returns true")
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost is now unlocked")
|
||||
TestUtils.assert_false(gs.can_purchase_prestige_buff(&"gold_boost"), "cannot repurchase already unlocked node")
|
||||
|
||||
# farm_boost should still be available (both are roots, independent)
|
||||
TestUtils.assert_true(gs.can_purchase_prestige_buff(&"farm_boost"), "farm_boost is still purchasable")
|
||||
|
||||
# Verify available buffs
|
||||
var available: Array[PrestigeBuffNode] = gs.get_available_prestige_buffs()
|
||||
var found_farm: bool = false
|
||||
var found_gold: bool = false
|
||||
for node in available:
|
||||
if node.id == &"farm_boost":
|
||||
found_farm = true
|
||||
if node.id == &"gold_boost":
|
||||
found_gold = true
|
||||
TestUtils.assert_true(found_farm, "farm_boost appears in available buffs")
|
||||
TestUtils.assert_false(found_gold, "gold_boost does NOT appear in available buffs (already unlocked)")
|
||||
|
||||
# Test cannot purchase with insufficient currency
|
||||
gs._prestige_buff_unlocked.clear()
|
||||
gs._initialize_prestige_buffs()
|
||||
# Drain ascension currency
|
||||
var bal: BigNumber = gs.get_currency_amount_by_id(&"ascension")
|
||||
gs.spend_currency_by_id(&"ascension", bal)
|
||||
TestUtils.assert_false(gs.can_purchase_prestige_buff(&"gold_boost"), "cannot purchase with no currency")
|
||||
|
||||
# Restore currency, re-unlock gold_boost for later tests
|
||||
gs.add_currency_by_id(&"ascension", BigNumber.from_float(100.0))
|
||||
gs.purchase_prestige_buff(&"gold_boost")
|
||||
await _wait()
|
||||
#endregion
|
||||
|
||||
|
||||
func _test_prestige_does_not_reset_prestige_buffs() -> void:
|
||||
print("--- Prestige Does Not Reset Prestige ---")
|
||||
|
||||
if _game_root == null:
|
||||
return
|
||||
|
||||
var gs: LevelGameState = _game_root.find_child("LevelGameState")
|
||||
if gs == null:
|
||||
return
|
||||
|
||||
# gold_boost should be unlocked from previous test
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost unlocked before prestige")
|
||||
|
||||
# Give enough currency to prestige
|
||||
gs.add_currency_by_id(&"gold", BigNumber.new(1.0, 10)) # 1e10 gold
|
||||
await _wait()
|
||||
|
||||
var pm: PrestigeManager = _game_root.find_child("PrestigeManager") as PrestigeManager
|
||||
if pm == null:
|
||||
print("[WARN] PrestigeManager not found; skipping prestige test")
|
||||
return
|
||||
|
||||
# Perform prestige if possible
|
||||
if pm.can_prestige():
|
||||
pm.perform_prestige()
|
||||
await _wait()
|
||||
|
||||
# After prestige, gold_boost should STILL be unlocked
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost still unlocked after prestige")
|
||||
else:
|
||||
# Manual simulation: call reset_for_prestige directly and verify ascension survives
|
||||
gs.reset_for_prestige(true, false, [&"ascension"])
|
||||
await _wait()
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost still unlocked after reset_for_prestige")
|
||||
#endregion
|
||||
|
||||
|
||||
func _test_multiplier_computation() -> void:
|
||||
print("--- Multiplier Computation ---")
|
||||
|
||||
if _game_root == null:
|
||||
return
|
||||
|
||||
var gs: LevelGameState = _game_root.find_child("LevelGameState")
|
||||
if gs == null:
|
||||
return
|
||||
|
||||
# gold_boost is already unlocked (from purchase test). Effect type 1 = GENERATOR_PRODUCTION_MULTIPLIER
|
||||
var mult: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, &"goldmine")
|
||||
TestUtils.assert_greater_than(mult, 1.0, "multiplier for goldmine is > 1.0 with gold_boost unlocked")
|
||||
|
||||
# farm_boost is NOT unlocked, multiplier for farm should be 1.0
|
||||
var farm_mult: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, &"farm")
|
||||
TestUtils.assert_equals(1.0, farm_mult, "multiplier for farm is 1.0 (farm_boost not unlocked)")
|
||||
|
||||
# Unlock farm_boost and check again
|
||||
gs.add_currency_by_id(&"ascension", BigNumber.from_float(100.0))
|
||||
gs.purchase_prestige_buff(&"farm_boost")
|
||||
await _wait()
|
||||
|
||||
var farm_mult2: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, &"farm")
|
||||
TestUtils.assert_greater_than(farm_mult2, 1.0, "multiplier for farm is > 1.0 after unlocking farm_boost")
|
||||
|
||||
# Check that unrelated effect type returns 1.0
|
||||
var unrelated: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.RESEARCH_XP_MULTIPLIER, &"")
|
||||
TestUtils.assert_equals(1.0, unrelated, "unrelated effect type returns 1.0")
|
||||
#endregion
|
||||
|
||||
|
||||
func _test_save_load_roundtrip() -> void:
|
||||
print("--- Save/Load Roundtrip ---")
|
||||
|
||||
if _game_root == null:
|
||||
return
|
||||
|
||||
var gs: LevelGameState = _game_root.find_child("LevelGameState")
|
||||
if gs == null:
|
||||
return
|
||||
|
||||
# Both gold_boost and farm_boost should be unlocked
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost unlocked before save")
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost unlocked before save")
|
||||
|
||||
# Save
|
||||
gs.save_game()
|
||||
await _wait()
|
||||
|
||||
# Clear state manually to simulate fresh load
|
||||
gs._prestige_buff_unlocked.clear()
|
||||
TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost false after clear")
|
||||
TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost false after clear")
|
||||
|
||||
# Load
|
||||
gs.load_game()
|
||||
await _wait()
|
||||
|
||||
# Both should be restored
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost restored after load")
|
||||
TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost restored after load")
|
||||
#endregion
|
||||
|
||||
|
||||
#region Helpers
|
||||
func _make_node(id: StringName, parent_ids: Array[StringName], tier: int, x_pos: float) -> PrestigeBuffNode:
|
||||
var n: PrestigeBuffNode = PrestigeBuffNode.new()
|
||||
n.id = id
|
||||
n.display_name = String(id)
|
||||
n.parent_ids = parent_ids
|
||||
n.tier = tier
|
||||
n.x_position = x_pos
|
||||
n.cost_mantissa = 1.0
|
||||
return n
|
||||
|
||||
|
||||
func _wait() -> void:
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
|
||||
|
||||
func _print_summary() -> void:
|
||||
TestUtils.print_result()
|
||||
passed = TestUtils.get_passed()
|
||||
failed = TestUtils.get_failed()
|
||||
|
||||
|
||||
func get_passed() -> int:
|
||||
return passed
|
||||
|
||||
|
||||
func get_failed() -> int:
|
||||
return failed
|
||||
1
tests/test_ascension.gd.uid
Normal file
1
tests/test_ascension.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cwmf4h7be356h
|
||||
@@ -6,10 +6,8 @@ var _game_root: Node = null
|
||||
|
||||
func _ready():
|
||||
await run()
|
||||
if failed == 0:
|
||||
# get_tree().quit(0) // Let test_runner handle it
|
||||
else:
|
||||
# get_tree().quit(1) // Let test_runner handle it
|
||||
# Let test_runner handle exit
|
||||
pass
|
||||
|
||||
func run():
|
||||
print("\n=== TEST: Prestige Mechanics ===\n")
|
||||
|
||||
Reference in New Issue
Block a user