# TODO ## Ascension Buff Graph — Needs Review Before Implementation ### Goal 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). - **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 graph. | Field | Type | Notes | |-------|------|-------| | `id` | `StringName` | Unique node ID | | `display_name` | `String` | UI label | | `description` | `String` (multiline) | Tooltip | | `icon` | `Texture2D` | Visual | | `parent_ids` | `Array[StringName]` | Prerequisites (DAG — multiple parents allowed) | | `effect_type` | `enum EffectType` | What this buff does | | `effect_value` | `float` | Effect magnitude | | `target_id` | `StringName` | Target generator/currency for type-specific effects (empty = global) | | `cost_mantissa` | `float` | Ascension currency cost | | `cost_exponent` | `int` | | | `tier` | `int` | Row in graph UI (visual grouping) | | `x_position` | `float` | Column in graph UI | **Effect types** (extensible enum): ``` GLOBAL_PRODUCTION_MULTIPLIER → multiply all generator output GENERATOR_PRODUCTION_MULTIPLIER → multiply one generator's output CURRENCY_PER_RUN → start each prestige with this currency COST_REDUCTION → reduce all generator purchase costs PRESTIGE_GAIN_MULTIPLIER → earn more ascension currency per prestige GENERATOR_COUNT_MULTIPLIER → scaling based on owned generator count RESEARCH_XP_MULTIPLIER → faster research leveling ``` #### `AscensionBuffCatalogue` (Resource) Flat list of nodes. The graph emerges from `parent_ids`. Follows existing catalogue pattern (`BuffCatalogue`, `GoalCatalogue`, etc.). | Field | Type | |-------|------| | `nodes` | `Array[AscensionBuffNode]` | #### State in `LevelGameState` ```gdscript @export var ascension_buff_catalogue: AscensionBuffCatalogue 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). - Normalized/sanitized on load (like generator states). ### Save Format - Bump `CURRENT_SAVE_FORMAT_VERSION` from 7 → 8. - 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, 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_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()` 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 unlock node → can_purchase_ascension_buff(node_id) → All parent_ids unlocked? → Not already unlocked? → Enough ascension currency? → YES: spend_currency_by_id(ascension_currency, cost) → _ascension_buff_unlocked[node_id] = true → emit ascension_buff_unlocked → save_game() ``` ### Example Graph Content (Illustrative) ``` Tier 0 (roots): strong_start → +10 gold/run cost: 1 parents: [] efficiency → +10% global prod cost: 1 parents: [] thrifty → -5% gen costs cost: 2 parents: [] Tier 1: better_start → +100 gold/run cost: 3 parents: [strong_start] enhanced_efficiency → +25% global prod cost: 3 parents: [efficiency] double_dip → +50% gold from forestry cost: 5 parents: [strong_start, efficiency] Tier 2: power_surge → +100% global prod cost: 10 parents: [better_start, enhanced_efficiency] ``` ### Files to Create/Modify | File | Action | Purpose | |------|--------|---------| | `core/ascension/ascension_buff_node.gd` | Create | Node resource | | `core/ascension/ascension_buff_catalogue.gd` | Create | Catalogue resource | | `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 | --- ## 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.