From c55b59fcadf63cdcffec864a550bb1d0fb9e8d8a Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Sat, 21 Mar 2026 09:14:38 +0100 Subject: [PATCH] Add README --- README.md | 263 ++++++++++++++++++ .../generator_unlock_goal_data.gd | 8 +- .../generator_unlock_system.gd | 28 +- 3 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..25ccb0a --- /dev/null +++ b/README.md @@ -0,0 +1,263 @@ +# Idles - Systems Architecture Report + +## Scope Covered + +This report covers all tracked files in the repository (62 total at time of analysis), including: + +1. Project configuration. +2. Core runtime scripts. +3. Gameplay/economy systems. +4. Goal/unlock systems. +5. UI adapter scripts and scenes. +6. Data resources (`.tres`). +7. Technical docs and metadata/import files. + +The focus below is on behavior-bearing files and how they interact. + +## Architecture Summary + +1. Global startup and singleton wiring is defined in `project.godot`. +2. Two autoload services power the runtime: `CurrencyDatabase` and `GameState`. +3. `BigNumber` is the numeric backbone used by currency amounts, costs, goals, production, and persistence. +4. Generator behavior is runtime-driven by `CurrencyGenerator`, parameterized by `CurrencyGeneratorData` and `GeneratorBuffData` resources. +5. Goal-based progression uses reusable goal primitives (`GoalData`, `GoalRequirementData`) plus unlock bindings (`GeneratorUnlockGoalData`) and runtime evaluator (`generator_unlock_system.gd`). +6. UI scripts are thin adapters listening to `GameState` signals. + +## Core Systems + +### 1) Numeric System (`BigNumber`) + +File: `core/big_number.gd` + +`BigNumber` stores values as mantissa + exponent (scientific notation style), normalizes them, and provides: + +1. Arithmetic operations (`add`, `subtract`, `multiply`, `divide`). +2. In-place fast accumulation (`add_in_place`) for frame-loop performance. +3. Safe comparisons (`compare_to`, `is_greater_than`, etc.). +4. Ratio conversion for progress UI (`get_ratio`). +5. Display formatting (`to_string_sci`, `to_string_suffix`). +6. JSON-friendly serialization/deserialization. + +This class is used everywhere currency values move. + +### 2) Currency Catalog System + +Files: `core/currency/currency.gd`, `core/currency_database.gd`, `idles/currencies/*.tres` + +`CurrencyDatabase` scans `res://idles/currencies`, loads typed `Currency` resources, normalizes IDs, and resolves names/icons. + +Current data assets: + +1. `idles/currencies/magic.tres` (`id = magic`). +2. `idles/currencies/knowledge.tres` (`id = knowledge`). + +This creates a data-driven currency catalog that `GameState`, goals, and UI can query consistently. + +### 3) Global State + Persistence (`GameState`) + +File: `core/game_state.gd` + +`GameState` is the central state authority and signal bus. + +It owns: + +1. Current currency map by currency ID. +2. Total acquired currency map by currency ID. +3. Generator states (`owned`, `purchased_count`, `unlocked`, `available`). +4. Generator buff levels. +5. Generator buff unlocked flags. +6. `last_save_time`. + +Important signals: + +1. `currency_changed`. +2. `generator_state_changed`. +3. `generator_buff_level_changed`. +4. `generator_buff_unlocked_changed`. + +Persistence: + +1. Save path: `user://idle_save.json`. +2. Save payload includes currencies, generator states, buff levels, buff unlocked map, and timestamp. +3. Load path includes defensive sanitization and backward-compatible defaulting. + +### 4) Generator Economy Runtime + +Files: `core/generator/currency_generator.gd`, `core/generator/currency_generator_data.gd`, `core/generator/currency_generator.tscn` + +`CurrencyGenerator` implements active gameplay behavior: + +1. Auto production cycles (`_process`, cycle accumulation). +2. Click/hover grants with cooldown. +3. Buy-one and buy-max generator purchases. +4. Buff purchase logic and buff effect application. +5. State registration and lookup through `GameState`. + +`CurrencyGeneratorData` provides formulas and tuning knobs: + +1. Exponential cost growth. +2. Bulk-buy geometric cost. +3. Max-affordable estimate. +4. Milestone multipliers. +5. Purchased-count multiplier. +6. Production/cycle and production/second helpers. +7. ROI-like helpers (`payback_seconds`, `income_to_cost_ratio`). + +### 5) Buff System + +Files: `core/generator/generator_buff_data.gd`, `idles/buffs/*.tres` + +Buff kinds: + +1. Auto production multiplier. +2. Manual click multiplier. +3. Resource purchase (instant grant on buff buy). + +Each buff resource defines: + +1. Unlock rules (`unlocked` and optional `unlock_goal`). +2. Effect scaling (`base_effect`, `effect_increment`). +3. Cost scaling (`base_cost`, `cost_multiplier`). +4. Optional target currency and resource grant scaling. + +### 6) Goals and Unlock Progression + +Files: + +1. `core/goals/goal_requirement_data.gd` +2. `core/goals/goal_data.gd` +3. `core/generator-unlock-goals/generator_unlock_goal_data.gd` +4. `core/generator-unlock-goals/generator_unlock_system.gd` +5. `idles/goals/*.tres` + +Key behavior: + +1. Goal requirements validate currency + target amount. +2. Requirement completion checks **total acquired currency**, not current wallet. +3. Unlock system evaluates goals on currency change and generator state changes. +4. If a goal is met and target generator is resolvable, it sets both `unlocked = true` and `available = true`. + +### 7) UI Adapter Layer + +Files: + +1. `generator_container.gd` + `generator_container.tscn`. +2. `generator_buff_tile.gd` + `generator_buff_tile.tscn`. +3. `currency_label.gd` + `currency_tile.tscn`. +4. `big_number_progress_bar.gd` + `big_number_progress_bar.tscn`. +5. `core/generator-unlock-goals/goals_debug_ui.gd` + `.tscn`. + +These scripts primarily: + +1. Subscribe to `GameState` and generator signals. +2. Convert runtime values to UI strings/states. +3. Trigger user actions (buy generator, buy buff, debug unlock). + +The architecture is mostly event-driven and avoids polling for state refresh. + +## Scene Composition + +### Active Gameplay Prototype + +File: `docs/museums/generator_museum.tscn` + +It composes: + +1. `MagicGenerator` instance. +2. `KnowledgeGenerator` instance (initially hidden and starts locked via data). +3. Currency tiles for `magic` and `knowledge`. +4. Goals debug UI panel. +5. Runtime `GeneratorUnlockSystem` node with configured unlock goals. + +### Legacy/Secondary Museum + +Files: `docs/museums/big_number_museum.tscn`, `docs/museums/big_number_museum.gd` + +This appears to be an older/placeholder scene and not the current gameplay focus. + +## Data-Driven Content Snapshot + +### Generators + +1. `primary_generator.tres`: `Magic Orb`, active from start, includes 3 buffs. +2. `secondary_generator.tres`: `Library`, starts locked/unavailable. + +### Buffs (Magic) + +1. `magic_auto_flux.tres`: auto-production buff, unlocked by default. +2. `magic_click_focus.tres`: manual-click buff. +3. `magic_supply_cache.tres`: resource-purchase buff. + +### Goals + +1. `magic_total_30.tres`: reach total magic 30. +2. `magic_total_1300.tres`: reach total magic 1300. +3. `generator_unlock_knowledge.tres`: maps `magic_total_30` to generator `Knowledge`. +4. `generator_unlock_wood.tres`: maps `magic_total_1300` to generator `Wood`. + +## End-to-End Runtime Flow + +1. `project.godot` autoloads `CurrencyDatabase` and `GameState`. +2. `CurrencyDatabase` builds currency catalog from `idles/currencies`. +3. `GameState` initializes per-currency maps and loads save data. +4. Scene nodes initialize and connect to `GameState` and local gameplay signals. +5. Generators produce currency automatically and via click/hover paths. +6. Currency updates emit `currency_changed` and drive UI refresh + unlock checks. +7. Goal unlock evaluator may transition generator state to unlocked/available. +8. Generator and buff purchases route all balance/state mutations through `GameState`. + +## Signal Guide (Emitters and Listeners) + +### Custom Gameplay Signals + +| Signal | Declared In | Emitted By | Emitted When | Listeners | +| --- | --- | --- | --- | --- | +| `currency_changed(currency_id, new_amount)` | `core/game_state.gd` | `GameState.add_currency_by_id`, `GameState.spend_currency_by_id` | Any currency balance increases/decreases | `CurrencyGenerator._on_currency_changed`, `GeneratorPanel._on_currency_changed`, `CurrencyTile._on_currency_changed`, `BigNumberProgressBar._on_currency_changed`, `GeneratorUnlockSystem._on_currency_changed`, `GoalsDebugUI._on_currency_changed` | +| `generator_state_changed(generator_id, state)` | `core/game_state.gd` | `GameState.register_generator`, `GameState._set_generator_state` | Generator state created or changed (`owned`, `purchased_count`, `unlocked`, `available`) | `CurrencyGenerator._on_generated_state_changed`, `GeneratorPanel._on_generator_state_changed`, `GeneratorUnlockSystem._on_generator_state_changed`, `GoalsDebugUI._on_generator_state_changed` | +| `generator_buff_level_changed(generator_id, buff_id, new_level)` | `core/game_state.gd` | `GameState.register_generator_buff`, `GameState.set_generator_buff_level` | Buff level created/sanitized/updated | `GeneratorPanel._on_generator_buff_level_changed` | +| `generator_buff_unlocked_changed(generator_id, buff_id, unlocked)` | `core/game_state.gd` | `GameState.register_generator_buff_unlocked`, `GameState.set_generator_buff_unlocked` | Buff unlock state created/sanitized/updated | `GeneratorPanel._on_generator_buff_unlocked_changed` | +| `purchase_completed(amount, total_owned, total_purchased, total_cost)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy` | Generator purchase succeeds | `GeneratorPanel._on_generator_updated` | +| `purchase_failed(requested_amount, required_cost, available_currency)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy` | Generator purchase fails for insufficient funds | `GeneratorPanel._on_generator_updated` | +| `production_tick(amount, cycle_count, total_owned)` | `core/generator/currency_generator.gd` | `CurrencyGenerator._grant_cycle_income` | One or more automatic production cycles complete | `GeneratorPanel._on_generator_updated` | +| `buff_purchased(buff_id, new_level, cost, cost_currency_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy_buff` | Buff purchase succeeds | `GeneratorPanel._on_generator_updated` | +| `buff_purchase_failed(buff_id, required_cost, cost_currency_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator.buy_buff` | Buff purchase fails for insufficient funds | `GeneratorPanel._on_generator_updated` | +| `goal_achieved(goal_id, target_generator_id)` | `core/generator-unlock-goals/generator_unlock_system.gd` | `GeneratorUnlockSystem._evaluate_goal` | Unlock goal transitions target generator to unlocked+available | None currently (available for gameplay/UI hooks) | +| `buy_pressed(buff_id)` | `generator_buff_tile.gd` | `GeneratorBuffTile._on_buy_button_pressed` | User presses buff buy button in a buff row | `GeneratorPanel._on_buy_buff_pressed` | + +### Built-In Godot Signals Wired In This Project + +| Source Signal | Connected In | Listener Method | Purpose | +| --- | --- | --- | --- | +| `Area2D.mouse_entered` | `core/generator/currency_generator.tscn` | `CurrencyGenerator._on_area_2d_mouse_entered` | Enter generator hover zone; mark hover true and show generator panel | +| `Area2D.mouse_exited` | `core/generator/currency_generator.tscn` | `CurrencyGenerator._on_area_2d_mouse_exited` | Exit generator hover zone; mark hover false and hide generator panel | +| `BuyOneButton.pressed` | `generator_container.tscn` | `GeneratorPanel._on_buy_pressed` | Buy one generator | +| `BuyMaxButton.pressed` | `generator_container.tscn` | `GeneratorPanel._on_buy_max_pressed` | Buy max affordable generators | +| `GeneratorContainer.mouse_entered` | `generator_container.tscn` | `GeneratorPanel._on_mouse_entered` | Intended hover hook on panel container (method currently missing) | +| `GeneratorContainer.mouse_exited` | `generator_container.tscn` | `GeneratorPanel._on_mouse_exited` | Intended hover hook on panel container (method currently missing) | +| `BuyButton.pressed` | `generator_buff_tile.tscn` | `GeneratorBuffTile._on_buy_button_pressed` | Emit tile-level `buy_pressed` custom signal | +| `DebugIncomeButton.pressed` | `currency_tile.tscn` | `CurrencyTile._on_debug_income_button_pressed` | Add debug income to the tile currency | +| `Button.pressed` (runtime-created unlock button) | `core/generator-unlock-goals/goals_debug_ui.gd` | `GoalsDebugUI._on_unlock_pressed` (bound with goal id) | Manual debug unlock when a goal is ready | + +### Common Signal Chains + +1. Currency gain/spend chain: generator or debug action mutates `GameState` currency, `GameState` emits `currency_changed`, UI widgets and unlock systems recompute and redraw. +2. Generator purchase chain: panel button triggers `CurrencyGenerator.buy`, generator updates `GameState` state, generator emits purchase signal, panel refreshes stats and buttons. +3. Buff purchase chain: buff tile emits `buy_pressed`, panel calls `CurrencyGenerator.buy_buff`, generator updates buff level/unlock and optionally grants currency, `GameState` emits buff/currency/state signals, panel and other listeners refresh. +4. Goal unlock chain: `currency_changed` triggers unlock evaluator, evaluator sets generator `unlocked/available`, emits `goal_achieved(goal_id, target_generator_id)`, `GameState` emits `generator_state_changed`, generator and UI become interactable/visible. + +## Key Findings and Risks + +1. `project.godot` has no explicit `run/main_scene` entry, so startup scene is not pinned in tracked config. +2. Save/load is only partially wired: `load_game()` runs at startup, but `save_game()` is not invoked anywhere else in the repository. +3. `currency_label.gd` and `big_number_progress_bar.gd` reference `GameState.GOLD_CURRENCY_ID`, but that constant does not exist in `core/game_state.gd`. +4. `generator_container.tscn` connects `mouse_entered`/`mouse_exited` to methods that do not exist in `generator_container.gd`. +5. `generator_unlock_wood.tres` targets generator ID `Wood`, but no generator with that ID appears in current runtime scene/content, so this goal is currently non-functional. +6. `KnowledgeGenerator` visibility logic may be inconsistent because `currency_generator.gd` sets `visible = true` whenever its generator state changes. +7. Two buffs are configured locked with no unlock-goal path, so they remain permanently inaccessible under current logic. +8. `core/generator-unlock-goals/TECH_SPEC.md` contains some outdated assumptions relative to current `.tres`-based implementation. + +## Additional Notes + +1. `.uid` files are identity metadata used by Godot and contain no runtime logic. +2. `icon.svg` and `icon.svg.import` are standard icon/import metadata. +3. Root config files (`.editorconfig`, `.gitattributes`, `.gitignore`) are minimal and conventional. diff --git a/core/generator-unlock-goals/generator_unlock_goal_data.gd b/core/generator-unlock-goals/generator_unlock_goal_data.gd index c809d80..1fbcfcc 100644 --- a/core/generator-unlock-goals/generator_unlock_goal_data.gd +++ b/core/generator-unlock-goals/generator_unlock_goal_data.gd @@ -1,15 +1,11 @@ class_name GeneratorUnlockGoalData extends Resource -const GOAL_DATA_SCRIPT: Script = preload("res://core/goals/goal_data.gd") - @export var target_generator_id: StringName = &"" -@export var goal: Resource +@export var goal: GoalData -func get_goal_data() -> Resource: +func get_goal_data() -> GoalData: if goal == null: return null - if goal.get_script() != GOAL_DATA_SCRIPT: - return null return goal diff --git a/core/generator-unlock-goals/generator_unlock_system.gd b/core/generator-unlock-goals/generator_unlock_system.gd index 61e99dd..6530f92 100644 --- a/core/generator-unlock-goals/generator_unlock_system.gd +++ b/core/generator-unlock-goals/generator_unlock_system.gd @@ -1,5 +1,7 @@ extends Node +signal goal_achieved(goal_id: StringName, target_generator_id: StringName) + class GeneratorUnlockGoal extends RefCounted: var target_generator_id: StringName var goal: Resource @@ -16,6 +18,8 @@ const GENERATOR_UNLOCK_GOAL_DATA_SCRIPT: Script = preload("res://core/generator- var _runtime_goals: Array[GeneratorUnlockGoal] = [] var _known_generator_ids: Dictionary = {} var _warned_unknown_target_ids: Dictionary = {} +var _is_evaluating_goals: bool = false +var _pending_goal_evaluation: bool = false func _ready() -> void: _collect_known_generator_ids() @@ -37,10 +41,20 @@ func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) - func _evaluate_all_goals() -> void: if _runtime_goals.is_empty(): return + if _is_evaluating_goals: + _pending_goal_evaluation = true + return + + _is_evaluating_goals = true for runtime_goal in _runtime_goals: _evaluate_goal(runtime_goal) + _is_evaluating_goals = false + if _pending_goal_evaluation: + _pending_goal_evaluation = false + _evaluate_all_goals.call_deferred() + func _evaluate_goal(runtime_goal: GeneratorUnlockGoal) -> bool: if runtime_goal == null: return false @@ -55,11 +69,23 @@ func _evaluate_goal(runtime_goal: GeneratorUnlockGoal) -> bool: if not bool(runtime_goal.goal.call("is_met")): return false + var goal_id: StringName = _extract_goal_id(runtime_goal.goal) GameState.set_generator_unlocked(runtime_goal.target_generator_id, true) GameState.set_generator_available(runtime_goal.target_generator_id, true) - print("[UnlockSystem] Goal completed: %s -> %s" % [String(runtime_goal.goal.get("id")), String(runtime_goal.target_generator_id)]) + goal_achieved.emit(goal_id, runtime_goal.target_generator_id) + print("[UnlockSystem] Goal completed: %s -> %s" % [String(goal_id), String(runtime_goal.target_generator_id)]) return true +func _extract_goal_id(goal: Resource) -> StringName: + if goal == null: + return &"" + + var goal_id_text: String = String(goal.get("id")).strip_edges() + if goal_id_text.is_empty(): + return &"" + + return StringName(goal_id_text) + func _is_goal_completed(runtime_goal: GeneratorUnlockGoal) -> bool: return ( GameState.is_generator_unlocked(runtime_goal.target_generator_id)