systems rework: buffs, prestige graph, research, modular architecture
Decouples core systems from centralized globals in favor of catalogue-based architecture and data-driven content. Key changes: - Prestige: node-based buff tech tree, graph panel, progress bar - Research: multi-worker system, per-generator research, xp generation - Ascension: meta-currency layer via multi-currency prestige resets - Buffs: refactored as generator-independent via catalogues - UI: currency panel, edge-scrolling camera + zoom, current-goal panel - Alchemy Tower: crafting building with recipe/cost system - Testing: 7 test suites (ascension, prestige, research, goals) - Content: migrated from hardcoded idles/ to gym format (tiny_sword)
This commit was merged in pull request #1.
This commit is contained in:
283
TODO.md
Normal file
283
TODO.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user