This commit is contained in:
2026-05-03 19:00:16 +02:00
parent 905b863f53
commit 18a4c04ee1
9 changed files with 11 additions and 450 deletions

View File

@@ -1,122 +0,0 @@
# Buff System Migration Guide
## Overview
This guide walks you through migrating from per-generator buffs to the new global buff system.
## Prerequisites
- Backup your existing save file: `user://idle_save.json`
- Note: This is a **breaking change** - old saves (v1) will be rejected
## Migration Steps
### Step 1: Update Buff Resources
Open each buff in `res://sandbox/buffs/` and set the `target_ids` field:
**For buffs that should apply to all generators:**
- Set `target_ids = ["*"]`
**For buffs that target specific generators:**
- Set `target_ids = ["generator_id_1", "generator_id_2"]`
- Use the same IDs as defined in your generator data files
**Example:**
```gdscript
# farm_flux.tres
target_ids = ["farm", "forestry"] # Multi-target buff
# global_boost.tres
target_ids = ["*"] # Wildcard - all current and future generators
```
### Step 2: Update Generator Resources
Open each generator in `res://sandbox/generators/` and:
1. **Remove** the `buffs` array field (or clear it)
2. **(Optional)** Add `expected_buff_ids` for documentation purposes
This is safe because generators now query `GameState.get_buffs_for_generator()` at runtime.
### Step 3: Delete Old Save Files
Delete `user://idle_save.json` to start fresh with the new v2 format.
**Note:** There is no automatic migration from v1 to v2. Players will need to start new games.
### Step 4: Verify Auto-Discovery
The `BuffDatabase` autoload will automatically load all `.tres` files from `res://sandbox/buffs/` at runtime.
Verify in the output log:
```
BuffDatabase initialized with N buffs
```
### Step 5: Test Key Scenarios
Run through these test cases:
1. **Single-target buff**: Buy a buff that targets only one generator
- Verify only that generator's production increases
2. **Multi-target buff**: Buy a buff targeting multiple generators
- Verify all targeted generators' production increases
3. **Wildcard buff**: Buy a buff with `target_ids = ["*"]`
- Verify ALL generators (current and future) get the bonus
4. **Goal unlock**: Meet a buff unlock goal
- Verify the buff automatically unlocks and activates
5. **Save/Load**: Save the game, close, and reopen
- Verify buff levels and unlock states persist
6. **Prestige**: Perform a prestige
- Verify all buff levels reset to 0
- Verify buff unlock states reset to false
## Rollback (If Needed)
If issues arise:
1. Revert all code changes using git
2. Restore old save file from backup
3. Restore buff/generator `.tres` files to original state
4. Remove `BuffDatabase` from `project.godot` autoloads
## Checking Your Work
After migration, verify:
- [ ] All buffs load automatically at startup
- [ ] Buff UI displays correctly for each generator
- [ ] Buying a buff level increases the GLOBAL level (not per-generator)
- [ ] Multi-target buffs affect all specified generators
- [ ] Wildcard buffs affect all generators
- [ ] Save files include buff state (version 2)
- [ ] Prestige resets all buff levels
## Common Issues
### Issue: Buff not appearing in UI
**Solution**: Check that the buff's `target_ids` includes the generator's ID or uses `["*"]`
### Issue: Wildcard buffs not applying to new generators
**Solution**: Ensure new generators call `GameState.register_buff()` for each buff they should inherit
### Issue: Old save won't load
**Solution**: This is expected - v1 saves are incompatible. Delete `user://idle_save.json` and start fresh.
### Issue: Buff multipliers not stacking
**Solution**: Verify `get_effective_multiplier()` is being called and that buff levels > 0
## Success Criteria
Migration is complete when:
- ✅ All generators produce correct amounts with active buffs
- ✅ Buff levels are shared globally across all targets
- ✅ Wildcard buffs automatically apply to new generators
- ✅ Save/load preserves buff state correctly
- ✅ Prestige resets all buff levels to 0
- ✅ New buffs can be added by dropping `.tres` files in `res://sandbox/buffs/`

View File

@@ -1,325 +0,0 @@
# Per-Level GameState Architecture Refactoring Plan
## Overview
Convert the current autoload-based global GameState system to a per-level instance-based architecture where each level scene has its own `LevelGameState` node with references to catalogues (resources) and its own save file.
## Current Architecture (To Be Replaced)
```
project.godot autoloads:
├─ BuffDatabase (autoload)
├─ CurrencyDatabase (autoload)
├─ GameState (autoload)
└─ PrestigeManager (autoload)
tiny_sword.tscn:
└─ Uses GameState autoload directly from any node
```
**Problems:**
- Level scenes implicitly depend on global autoloads
- Cannot have multiple levels with independent state
- Catalogues auto-discover from filesystem (tightly coupled)
- Save file path is hardcoded globally
## Target Architecture
```
tiny_sword.tscn (root node)
├─ LevelGameState (Node)
│ ├─ @export var currency_catalogue: CurrencyCatalogue (Resource)
│ ├─ @export var buff_catalogue: BuffCatalogue (Resource)
│ ├─ @export var goal_catalogue: GoalCatalogue (Resource)
│ ├─ @export var save_file_path: String = "user://tiny_sword_save.json"
│ └─ PrestigeManager (Node child)
│ └─ @export var game_state: LevelGameState
├─ World (Node2D)
│ ├─ GoldMine (CurrencyGenerator)
│ │ └─ @onready var game_state = find_parent("LevelGameState")
│ └─ ...other generators
└─ UI (Control)
├─ CurrencyTile (each: @export var game_state: LevelGameState)
└─ ...
```
## Catalogue Resources
All catalogues are `Resource` subclasses with explicit arrays populated in the editor:
### CurrencyCatalogue
```gdscript
class_name CurrencyCatalogue
extends Resource
@export var currencies: Array[Currency] = []
func get_currency_by_id(id: StringName) -> Currency
func get_all_ids() -> Array[StringName]
```
**Location:** `res://docs/gyms/tiny_sword/currencies/tiny_sword_catalogue.tres`
### BuffCatalogue
```gdscript
class_name BuffCatalogue
extends Resource
@export var buffs: Array[GeneratorBuffData] = []
func get_buff_by_id(id: StringName) -> GeneratorBuffData
func get_all_ids() -> Array[StringName]
func get_buffs_for_generator(gen_id: StringName) -> Array[GeneratorBuffData]
```
**Location:** `res://docs/gyms/tiny_sword/buffs/tiny_sword_buffs.tres`
### GoalCatalogue
```gdscript
class_name GoalCatalogue
extends Resource
@export var goals: Array[GoalData] = []
func get_goal_by_id(id: StringName) -> GoalData
func get_all_ids() -> Array[StringName]
```
**Location:** `res://docs/gyms/tiny_sword/goals/tiny_sword_goals.tres`
---
## Implementation Steps
### Step 1: Create Catalogue Classes
**Files to create:**
1. `core/currency_catalogue.gd`
2. `core/buff_catalogue.gd`
3. `core/goal_catalogue.gd`
**Purpose:** Resource classes that hold arrays of game data and provide lookup methods.
---
### Step 2: Create LevelGameState
**File to create:** `core/level_game_state.gd`
**Key changes from current `game_state.gd`:**
| Current | New |
|---------|-----|
| `extends Node` (autoload) | `extends Node` (instance) |
| `CurrencyDatabase.get_known_currency_ids()` | `currency_catalogue.get_all_ids()` |
| `BuffDatabase` calls | `buff_catalogue` calls |
| Static `file_name` | `@export var save_file_path: String` |
| Filesystem goal discovery | `goal_catalogue.goals` array |
| Buff auto-registration from BuffDatabase | Buff registration from `buff_catalogue` |
**New properties:**
```gdscript
@export var currency_catalogue: CurrencyCatalogue
@export var buff_catalogue: BuffCatalogue
@export var goal_catalogue: GoalCatalogue
@export var save_file_path: String = "user://level_save.json"
```
**Methods to port:**
- All currency methods (`add_currency`, `spend_currency`, `get_currency_amount_by_id`, etc.)
- All generator methods (`register_generator`, `get_generator_state`, etc.)
- All buff methods (`register_buff`, `get_buff_level`, `get_effective_multiplier`, etc.)
- All goal methods (`register_goal`, `is_goal_completed`, `evaluate_all_goals`, etc.)
- Save/load (`save_game()`, `load_game()`) with configurable path
- Prestige methods (`reset_for_prestige`, `emit_currency_changed_for_all`)
---
### Step 3: Convert PrestigeManager
**File to modify:** `core/prestige/prestige_manager.gd`
**Changes:**
- Remove autoload registration from `project.godot`
- Add `@export var game_state: LevelGameState`
- Change `/root/PrestigeManager` lookup to use direct reference
- Make it a child node of `LevelGameState` in scenes
**Key fix in `_get_prestige_multiplier()`:**
```gdscript
# Old: get_node_or_null("/root/PrestigeManager")
# New: Direct access through injected reference
```
---
### Step 4: Update CurrencyGenerator
**File to modify:** `core/generator/currency_generator.gd`
**Changes:**
- Replace all `GameState.` calls with `game_state.` calls
- Add `@onready var game_state: LevelGameState = find_parent("LevelGameState")`
- Update signal connections: `game_state.currency_changed.connect(...)`
- Update `_get_prestige_multiplier()` to use injected reference
---
### Step 5: Update CurrencyTile
**File to modify:** `currency_label.gd`
**Changes:**
```gdscript
@export var game_state: LevelGameState # ← NEW
func _ready() -> void:
if game_state == null:
push_error("CurrencyTile '%s' missing game_state reference" % name)
return
_currency_id = game_state.get_currency_id(currency)
# ... rest of initialization
```
---
### Step 6: Update project.godot
**Remove autoloads:**
```ini
# REMOVE these lines:
# BuffDatabase="*res://core/buff_database.gd"
# CurrencyDatabase="*res://core/currency_database.gd"
# GameState="*uid://d2j7tvlgxr2jp"
# PrestigeManager="*res://core/prestige/prestige_manager.gd"
```
---
### Step 7: Update tiny_sword.tscn
**Add LevelGameState node:**
```ini
[node name="LevelGameState" type="Node" parent="."]
currency_catalogue = ExtResource("currency_catalogue_tres")
buff_catalogue = ExtResource("buff_catalogue_tres")
goal_catalogue = ExtResource("goal_catalogue_tres")
save_file_path = "user://tiny_sword_save.json"
```
**Add PrestigeManager as child:**
```ini
[node name="PrestigeManager" type="Node" parent="LevelGameState"]
game_state = NodePath("..") # Reference to LevelGameState
```
**Wire up children:**
- Each `CurrencyTile`: `game_state = LevelGameState`
- Each `CurrencyGenerator`: auto-discovers via `find_parent()`
---
### Step 8: Create Catalogue Resources
**Create `.tres` files via Godot editor:**
1. `res://docs/gyms/tiny_sword/currencies/tiny_sword_catalogue.tres`
- `currencies = [gold, worker, food, wood]`
2. `res://docs/gyms/tiny_sword/buffs/tiny_sword_buffs.tres`
- `buffs = [all relevant buffs for this level]`
3. `res://docs/gyms/tiny_sword/goals/tiny_sword_goals.tres`
- `goals = [all relevant goals for this level]`
---
### Step 9: Clean Up
**Files to delete:**
- `core/currency_database.gd` (replaced by `CurrencyCatalogue`)
- `core/buff_database.gd` (replaced by `BuffCatalogue`)
- `core/game_state.gd` (replaced by `level_game_state.gd`)
---
## File Summary
### Files to Create (4)
| File | Purpose |
|------|---------|
| `core/currency_catalogue.gd` | Resource with `Array[Currency]` + lookup methods |
| `core/buff_catalogue.gd` | Resource with `Array[GeneratorBuffData]` + lookup methods |
| `core/goal_catalogue.gd` | Resource with `Array[GoalData]` + lookup methods |
| `core/level_game_state.gd` | Complete GameState implementation without autoload dependencies |
### Files to Modify (4)
| File | Changes |
|------|---------|
| `core/prestige/prestige_manager.gd` | Remove autoload, add `@export var game_state` |
| `core/generator/currency_generator.gd` | Add `find_parent()` lookup, replace `GameState.` |
| `currency_label.gd` | Add `@export var game_state: LevelGameState` |
| `project.godot` | Remove 4 autoloads |
### Files to Delete (3)
| File | Reason |
|------|--------|
| `core/currency_database.gd` | Replaced by `CurrencyCatalogue` |
| `core/buff_database.gd` | Replaced by `BuffCatalogue` |
| `core/game_state.gd` | Replaced by `level_game_state.gd` |
### Scene Updates
| File | Changes |
|------|---------|
| `tiny_sword.tscn` | Add `LevelGameState` node, add `PrestigeManager` child, wire catalogues |
| Create `.tres` resources | `tiny_sword_catalogue.tres`, `tiny_sword_buffs.tres`, `tiny_sword_goals.tres` |
---
## Benefits
1. **Decoupled levels**: Each level can exist independently without global dependencies
2. **Multiple levels**: Can support multiple levels with independent state (future feature)
3. **Explicit dependencies**: All dependencies are visible in the inspector
4. **Resource-based catalogues**: Content is defined as editor-authorable resources
5. **Per-level saves**: Each level has its own save file path
6. **Better testing**: Easier to test individual levels in isolation
---
## Risks and Considerations
1. **Signal connections**: UI components must be manually wired to `LevelGameState` instance
2. **PrestigeManager access**: Must ensure proper reference injection
3. **Catalogue population**: Catalogue resources must be created and populated in editor
4. **Migration**: Existing save files may need migration logic
---
## Implementation Order
1. Create catalogue classes (3 files)
2. Create level_game_state.gd (the core refactor)
3. Update prestige_manager.gd (remove autoload dependency)
4. Update currency_generator.gd (add game_state injection)
5. Update currency_label.gd (add game_state export)
6. Update project.godot (remove autoloads)
7. Create catalogue resources (.tres files via editor)
8. Update tiny_sword.tscn (wire everything together)
9. Delete old files (cleanup)
---
## Status
- [ ] Step 1: Create catalogue classes
- [ ] Step 2: Create level_game_state.gd
- [ ] Step 3: Update prestige_manager.gd
- [ ] Step 4: Update currency_generator.gd
- [ ] Step 5: Update currency_label.gd
- [ ] Step 6: Update project.godot
- [ ] Step 7: Create catalogue resources (manual editor work)
- [ ] Step 8: Update tiny_sword.tscn (manual editor work)
- [ ] Step 9: Delete old files

View File

@@ -14,12 +14,16 @@ extends PanelContainer
var _is_waiting_for_confirm: bool = false var _is_waiting_for_confirm: bool = false
func _ready() -> void: func _ready() -> void:
var manager: Node = _get_prestige_manager() game_state.ready.connect(_on_game_state_ready)
func _on_game_state_ready() -> void:
var manager: PrestigeManager = _get_prestige_manager()
manager.prestige_state_changed.connect(_on_prestige_state_changed) manager.prestige_state_changed.connect(_on_prestige_state_changed)
manager.prestige_performed.connect(_on_prestige_performed) manager.prestige_performed.connect(_on_prestige_performed)
game_state.currency_changed.connect(_on_currency_changed) game_state.currency_changed.connect(_on_currency_changed)
_refresh_ui() _refresh_ui()
func _on_reset_button_pressed() -> void: func _on_reset_button_pressed() -> void:
var manager: PrestigeManager = _get_prestige_manager() var manager: PrestigeManager = _get_prestige_manager()

View File

@@ -12,7 +12,9 @@ func _process(delta: float) -> void:
pass pass
func _on_area_2d_mouse_entered() -> void: func _on_area_2d_mouse_entered() -> void:
_prestige_panel.visible = true ## TODO: remove this comment to enable prestige panel
#_prestige_panel.visible = true
pass
func _on_area_2d_mouse_exited() -> void: func _on_area_2d_mouse_exited() -> void:
_prestige_panel.visible = false _prestige_panel.visible = false

View File

@@ -23,6 +23,7 @@ position = Vector2(1, 22)
shape = SubResource("RectangleShape2D_tgvch") shape = SubResource("RectangleShape2D_tgvch")
[node name="PrestigePanel" parent="." unique_id=245519778 instance=ExtResource("3_l7gct")] [node name="PrestigePanel" parent="." unique_id=245519778 instance=ExtResource("3_l7gct")]
visible = false
offset_left = 155.0 offset_left = 155.0
offset_top = -80.0 offset_top = -80.0
offset_right = 575.0 offset_right = 575.0

View File

@@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="GoalCatalogue" format=3 uid="uid://cgt1mjir1v4br"] [gd_resource type="Resource" script_class="GoalCatalogue" format=3 uid="uid://cgt1mjir1v4br"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="1_6p6ue"] [ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="1_6p6ue"]
[ext_resource type="Script" uid="uid://bfbp4mo8ys5p8" path="res://core/goal_catalogue.gd" id="2_84pbn"] [ext_resource type="Script" uid="uid://bfbp4mo8ys5p8" path="res://core/goals/goal_catalogue.gd" id="2_84pbn"]
[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="2_xvtf2"] [ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="2_xvtf2"]
[ext_resource type="Resource" uid="uid://cts0407h130d6" path="res://docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres" id="3_wb626"] [ext_resource type="Resource" uid="uid://cts0407h130d6" path="res://docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres" id="3_wb626"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_3oaoj"] [ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_3oaoj"]

View File

@@ -43,6 +43,7 @@ anchors_preset = 0
layout_mode = 1 layout_mode = 1
[node name="GoalsDebugUI" parent="LevelGameState/CanvasLayer/UI" unique_id=1494700185 instance=ExtResource("10_qifrv")] [node name="GoalsDebugUI" parent="LevelGameState/CanvasLayer/UI" unique_id=1494700185 instance=ExtResource("10_qifrv")]
visible = false
layout_mode = 1 layout_mode = 1
offset_left = 1433.0 offset_left = 1433.0
offset_top = 5.0 offset_top = 5.0