Files
idle/TODO.md
2026-05-06 23:37:52 +02:00

12 KiB

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

@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

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:

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:

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.