Decouple everything from globals

This commit is contained in:
2026-04-10 01:09:57 +02:00
parent b758803afe
commit f671ab3419
56 changed files with 993 additions and 1291 deletions

325
PLANNING.md Normal file
View File

@@ -0,0 +1,325 @@
# 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