From 81a4058b043ef9506957e56fc0041030fa3c2da8 Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Thu, 7 May 2026 20:36:21 +0000 Subject: [PATCH] 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) --- .../godot-test-instrumentation/SKILL.md | 642 +++++++ .codex | 0 .opencode/plans/currency-panel.md | 97 ++ AGENTS.md | 7 +- README.md | 546 +++--- TODO.md | 283 +++ big_number_progress_bar.gd | 14 +- core/README.md | 266 +++ core/big_number.gd | 10 + core/buff_catalogue.gd | 24 + core/buff_catalogue.gd.uid | 1 + core/currency/README.md | 111 ++ core/currency/currency.gd | 1 + core/currency/currency_catalogue.gd | 17 + core/currency/currency_catalogue.gd.uid | 1 + core/currency/currency_panel.gd | 35 + core/currency/currency_panel.gd.uid | 1 + core/currency_database.gd | 164 -- core/currency_database.gd.uid | 1 - core/edge_scroll_camera.gd | 118 ++ core/edge_scroll_camera.gd.uid | 1 + core/game_state.gd | 810 --------- core/game_state.gd.uid | 1 - core/generator-unlock-goals/TECH_SPEC.md | 58 - core/generator/README.md | 225 +++ core/generator/currency_generator.gd | 326 ++-- core/generator/currency_generator.tscn | 28 - core/generator/currency_generator_data.gd | 31 +- core/generator/generator_buff_data.gd | 40 +- .../generator/generator_container.gd | 40 +- .../generator/generator_container.gd.uid | 0 .../generator/generator_container.tscn | 2 +- core/generator/research_buff_calculator.gd | 22 + .../generator/research_buff_calculator.gd.uid | 1 + core/goals/README.md | 192 +++ core/goals/current_goal_panel.gd | 153 ++ core/goals/current_goal_panel.gd.uid | 1 + core/goals/current_goal_panel.tscn | 26 + core/goals/goal_catalogue.gd | 17 + core/goals/goal_catalogue.gd.uid | 1 + core/goals/goal_data.gd | 47 +- core/goals/goal_requirement_data.gd | 31 +- core/level_game_state.gd | 1534 +++++++++++++++++ core/level_game_state.gd.uid | 1 + core/prestige/README.md | 467 +++++ core/prestige/prestige_buff_catalogue.gd | 40 + core/prestige/prestige_buff_catalogue.gd.uid | 1 + .../prestige_buff_connection_overlay.gd | 43 + .../prestige_buff_connection_overlay.gd.uid | 1 + core/prestige/prestige_buff_graph_panel.gd | 85 + .../prestige/prestige_buff_graph_panel.gd.uid | 1 + core/prestige/prestige_buff_graph_tile.gd | 122 ++ core/prestige/prestige_buff_graph_tile.gd.uid | 1 + core/prestige/prestige_buff_graph_tile.tscn | 40 + core/prestige/prestige_buff_node.gd | 69 + core/prestige/prestige_buff_node.gd.uid | 1 + core/prestige/prestige_config.gd | 4 +- core/prestige/prestige_manager.gd | 279 ++- core/prestige/prestige_panel.gd | 58 +- core/prestige/prestige_panel.tscn | 47 +- core/prestige/prestige_progress_bar.gd | 72 + core/prestige/prestige_progress_bar.gd.uid | 1 + core/prestige/prestige_progress_bar.tscn | 22 + core/research/README.md | 488 ++++++ core/research/TODO.md | 616 +++++++ core/research/research_catalogue.gd | 19 + core/research/research_catalogue.gd.uid | 1 + core/research/research_data.gd | 70 + core/research/research_data.gd.uid | 1 + core/research/research_panel.gd | 155 ++ core/research/research_panel.gd.uid | 1 + core/research/research_panel.tscn | 34 + core/research/research_row.gd | 98 ++ core/research/research_row.gd.uid | 1 + core/research/research_row.tscn | 42 + core/research/worker_summary_label.gd | 27 + core/research/worker_summary_label.gd.uid | 1 + core/research/worker_summary_label.tscn | 8 + currency_label.gd | 31 +- currency_tile.tscn | 4 +- docs/gyms/generator_gym.tscn | 78 - .../tiny_sword/buffs/farm_auto_flux_buff.tres | 19 + .../buffs/forestry_auto_flux_buff.tres | 20 + .../tiny_sword/buffs/gold_auto_flux_buff.tres | 19 + .../tiny_sword/buffs/gold_click_buff.tres | 20 + .../tiny_sword/buffs/spawn_worker_buff.tres | 21 + .../tiny_sword/buffs/ts_buff_catalogue.tres | 14 + .../buildings/alchemy_tower/README.md | 55 + .../buildings/alchemy_tower/Tower.png | Bin 0 -> 6496 bytes .../buildings/alchemy_tower/Tower.png.import | 40 + .../alchemy_tower/alchemy_craftable_panel.gd | 161 ++ .../alchemy_craftable_panel.gd.uid | 1 + .../alchemy_craftable_panel.tscn | 40 + .../alchemy_craftable_panel_tile.gd | 81 + .../alchemy_craftable_panel_tile.gd.uid | 1 + .../alchemy_craftable_panel_tile.tscn | 31 + .../alchemy_currencies_panel_old.gd.uid | 1 + .../buildings/alchemy_tower/alchemy_tower.gd | 202 +++ .../alchemy_tower/alchemy_tower.gd.uid | 1 + .../alchemy_tower/alchemy_tower.tscn | 37 + .../alchemy_tower/alchemy_tower_data.gd | 23 + .../alchemy_tower/alchemy_tower_data.gd.uid | 1 + .../alchemy_tower/alchemy_tower_data.tres | 6 + .../recipes/alchemy_craft_catalogue.gd | 18 + .../recipes/alchemy_craft_catalogue.gd.uid | 1 + .../recipes/alchemy_craft_catalogue.tres | 10 + .../recipes/alchemy_craft_recipe.gd | 10 + .../recipes/alchemy_craft_recipe.gd.uid | 1 + .../recipes/cognite_cost_entry.tres | 10 + .../alchemy_tower/recipes/cognite_recipe.tres | 11 + .../recipes/currency_cost_entry.gd | 6 + .../recipes/currency_cost_entry.gd.uid | 1 + .../recipes/mana_stone_cost_entry.tres | 10 + .../recipes/mana_stone_recipe.tres | 11 + .../philosoper_stone_cost_entry_0.tres | 9 + .../philosoper_stone_cost_entry_1.tres | 9 + .../recipes/philosoper_stone_recipe.tres | 12 + .../buildings/alchemy_tower/test_panel.gd.uid | 1 + .../tiny_sword/buildings/castle/Castle.png | Bin 0 -> 17094 bytes .../buildings/castle/Castle.png.import | 40 + .../tiny_sword/buildings/castle/castle.gd | 20 + .../tiny_sword/buildings/castle/castle.gd.uid | 1 + .../tiny_sword/buildings/castle/castle.tscn | 72 + .../tiny_sword/buildings/castle/test_buff.gd | 118 ++ .../buildings/castle/test_buff.gd.uid | 1 + .../gyms/tiny_sword/buildings/farm/House1.png | Bin 0 -> 5282 bytes .../buildings/farm/House1.png.import | 40 + docs/gyms/tiny_sword/buildings/farm/farm.tscn | 38 + .../buildings/farm/farm_generator.tres | 21 + .../tiny_sword/buildings/forestry/House3.png | Bin 0 -> 4418 bytes .../buildings/forestry/House3.png.import | 40 + .../tiny_sword/buildings/forestry/forestry.gd | 13 + .../buildings/forestry/forestry.gd.uid | 1 + .../buildings/forestry/forestry.tscn | 40 + .../forestry/forestry_generator.tres | 21 + .../buildings/gold_mine/Gold Stone 1.png | Bin 0 -> 802 bytes .../gold_mine/Gold Stone 1.png.import | 40 + .../gold_mine/Gold Stone 1_Highlight.png | Bin 0 -> 2150 bytes .../Gold Stone 1_Highlight.png.import | 40 + .../buildings/gold_mine/Gold Stone 2.png | Bin 0 -> 797 bytes .../gold_mine/Gold Stone 2.png.import | 40 + .../gold_mine/Gold Stone 2_Highlight.png | Bin 0 -> 2117 bytes .../Gold Stone 2_Highlight.png.import | 40 + .../buildings/gold_mine/Gold Stone 3.png | Bin 0 -> 1275 bytes .../gold_mine/Gold Stone 3.png.import | 40 + .../gold_mine/Gold Stone 3_Highlight.png | Bin 0 -> 3604 bytes .../Gold Stone 3_Highlight.png.import | 40 + .../buildings/gold_mine/Gold Stone 4.png | Bin 0 -> 1364 bytes .../gold_mine/Gold Stone 4.png.import | 40 + .../gold_mine/Gold Stone 4_Highlight.png | Bin 0 -> 3897 bytes .../Gold Stone 4_Highlight.png.import | 40 + .../buildings/gold_mine/Gold Stone 5.png | Bin 0 -> 2099 bytes .../gold_mine/Gold Stone 5.png.import | 40 + .../gold_mine/Gold Stone 5_Highlight.png | Bin 0 -> 6192 bytes .../Gold Stone 5_Highlight.png.import | 40 + .../buildings/gold_mine/Gold Stone 6.png | Bin 0 -> 2146 bytes .../gold_mine/Gold Stone 6.png.import | 40 + .../gold_mine/Gold Stone 6_Highlight.png | Bin 0 -> 5802 bytes .../Gold Stone 6_Highlight.png.import | 40 + .../buildings/gold_mine/Gold Stones.aseprite | Bin 0 -> 35957 bytes .../buildings/gold_mine/gold_mine.tscn | 37 + .../gold_mine/gold_mine_generator.tres | 19 + .../buildings/monastery/Monastery.png | Bin 0 -> 11279 bytes .../buildings/monastery/Monastery.png.import | 40 + .../buildings/monastery/monastery.gd | 14 + .../buildings/monastery/monastery.gd.uid | 1 + .../buildings/monastery/monastery.tscn | 30 + .../monastery/monastery_generator.tres | 7 + .../gyms/tiny_sword/currencies/ascension.tres | 11 + docs/gyms/tiny_sword/currencies/cognite.tres | 11 + docs/gyms/tiny_sword/currencies/food.tres | 10 + docs/gyms/tiny_sword/currencies/gold.tres | 11 + .../tiny_sword/currencies/magic_gold.tres | 11 + .../tiny_sword/currencies/mana_stone.tres | 11 + .../currencies/philosoper_stone.tres | 11 + .../currencies/ts_currency_catalogue.tres | 18 + docs/gyms/tiny_sword/currencies/wood.tres | 10 + docs/gyms/tiny_sword/currencies/worker.tres | 11 + docs/gyms/tiny_sword/currency_panel.tscn | 15 + .../goals/gold_350k_wood_20k_goal.tres | 24 + .../goals/gold_total_1300_goal.tres | 16 + .../tiny_sword/goals/gold_total_13k_goal.tres | 16 + .../tiny_sword/goals/gold_total_30_goal.tres | 16 + .../tiny_sword/goals/ts_goal_catalogue.tres | 12 + .../prestige/prestige_buff_catalogue.tres | 10 + .../prestige/prestige_buff_farm_boost.tres | 13 + .../prestige/prestige_buff_gold_boost.tres | 11 + .../prestige/prestige_buff_graph_panel.tscn | 69 + .../prestige/prestige_buff_wood_boost.tres | 12 + .../tiny_sword/prestige/primary_prestige.tres | 9 + .../tiny_sword/research/farm_research.tres | 13 + .../research/forestry_research.tres | 13 + .../research/gold_mine_research.tres | 13 + .../research/ts_research_catalogue.tres | 12 + docs/gyms/tiny_sword/tiny_sword.tscn | 125 ++ docs/museums/big_number_museum.tscn | 2 +- docs/museums/edge_scroll_camera.tscn | 42 + goals_debug_ui.gd | 219 +-- goals_debug_ui.tscn | 13 +- idles/buffs/bigger_forest.tres | 18 - idles/buffs/library_auto_flux.tres | 18 - idles/buffs/orb_auto_flux.tres | 18 - idles/buffs/orb_click_focus.tres | 18 - idles/buffs/orb_summon_spirit.tres | 20 - idles/currencies/knowledge.tres | 10 - idles/currencies/magic.tres | 10 - idles/currencies/spirit.tres | 9 - idles/currencies/wood.tres | 10 - idles/generators/forestry.tres | 22 - idles/generators/library.tres | 25 - idles/generators/orb.tres | 19 - idles/generators/spirit_factory.tres | 18 - idles/goals/magic_350k_wood_20k.tres | 24 - idles/goals/magic_total_1300.tres | 16 - idles/goals/magic_total_13k.tres | 16 - idles/goals/magic_total_30.tres | 15 - idles/prestige/primary_prestige.tres | 25 - project.godot | 7 +- scripts/check_syntax.sh | 37 + scripts/run_all.sh | 26 + scripts/run_test.sh | 39 + test_multi_currency_prestige.gd | 61 + test_multi_currency_prestige.gd.uid | 1 + tests/test_ascension.gd | 318 ++++ tests/test_ascension.gd.uid | 1 + tests/test_goals_prestige_reset.gd | 146 ++ tests/test_goals_prestige_reset.gd.uid | 1 + tests/test_goals_prestige_reset.tscn | 6 + tests/test_gold_mine_click.gd | 75 + tests/test_gold_mine_click.gd.uid | 1 + tests/test_prestige.gd | 92 + tests/test_prestige.gd.uid | 1 + tests/test_research_activation.gd | 159 ++ tests/test_research_activation.gd.uid | 1 + tests/test_research_prestige_reset.gd | 128 ++ tests/test_research_prestige_reset.gd.uid | 1 + tests/test_research_xp_acquisition.gd | 146 ++ tests/test_research_xp_acquisition.gd.uid | 1 + tests/test_runner.gd | 79 + tests/test_runner.gd.uid | 1 + tests/test_utils.gd | 74 + tests/test_utils.gd.uid | 1 + 242 files changed, 11226 insertions(+), 2242 deletions(-) create mode 100644 .agents/skills/godot-test-instrumentation/SKILL.md create mode 100644 .codex create mode 100644 .opencode/plans/currency-panel.md create mode 100644 TODO.md create mode 100644 core/README.md create mode 100644 core/buff_catalogue.gd create mode 100644 core/buff_catalogue.gd.uid create mode 100644 core/currency/README.md create mode 100644 core/currency/currency_catalogue.gd create mode 100644 core/currency/currency_catalogue.gd.uid create mode 100644 core/currency/currency_panel.gd create mode 100644 core/currency/currency_panel.gd.uid delete mode 100644 core/currency_database.gd delete mode 100644 core/currency_database.gd.uid create mode 100644 core/edge_scroll_camera.gd create mode 100644 core/edge_scroll_camera.gd.uid delete mode 100644 core/game_state.gd delete mode 100644 core/game_state.gd.uid delete mode 100644 core/generator-unlock-goals/TECH_SPEC.md create mode 100644 core/generator/README.md rename generator_container.gd => core/generator/generator_container.gd (83%) rename generator_container.gd.uid => core/generator/generator_container.gd.uid (100%) rename generator_container.tscn => core/generator/generator_container.tscn (98%) create mode 100644 core/generator/research_buff_calculator.gd create mode 100644 core/generator/research_buff_calculator.gd.uid create mode 100644 core/goals/README.md create mode 100644 core/goals/current_goal_panel.gd create mode 100644 core/goals/current_goal_panel.gd.uid create mode 100644 core/goals/current_goal_panel.tscn create mode 100644 core/goals/goal_catalogue.gd create mode 100644 core/goals/goal_catalogue.gd.uid create mode 100644 core/level_game_state.gd create mode 100644 core/level_game_state.gd.uid create mode 100644 core/prestige/README.md create mode 100644 core/prestige/prestige_buff_catalogue.gd create mode 100644 core/prestige/prestige_buff_catalogue.gd.uid create mode 100644 core/prestige/prestige_buff_connection_overlay.gd create mode 100644 core/prestige/prestige_buff_connection_overlay.gd.uid create mode 100644 core/prestige/prestige_buff_graph_panel.gd create mode 100644 core/prestige/prestige_buff_graph_panel.gd.uid create mode 100644 core/prestige/prestige_buff_graph_tile.gd create mode 100644 core/prestige/prestige_buff_graph_tile.gd.uid create mode 100644 core/prestige/prestige_buff_graph_tile.tscn create mode 100644 core/prestige/prestige_buff_node.gd create mode 100644 core/prestige/prestige_buff_node.gd.uid create mode 100644 core/prestige/prestige_progress_bar.gd create mode 100644 core/prestige/prestige_progress_bar.gd.uid create mode 100644 core/prestige/prestige_progress_bar.tscn create mode 100644 core/research/README.md create mode 100644 core/research/TODO.md create mode 100644 core/research/research_catalogue.gd create mode 100644 core/research/research_catalogue.gd.uid create mode 100644 core/research/research_data.gd create mode 100644 core/research/research_data.gd.uid create mode 100644 core/research/research_panel.gd create mode 100644 core/research/research_panel.gd.uid create mode 100644 core/research/research_panel.tscn create mode 100644 core/research/research_row.gd create mode 100644 core/research/research_row.gd.uid create mode 100644 core/research/research_row.tscn create mode 100644 core/research/worker_summary_label.gd create mode 100644 core/research/worker_summary_label.gd.uid create mode 100644 core/research/worker_summary_label.tscn delete mode 100644 docs/gyms/generator_gym.tscn create mode 100644 docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres create mode 100644 docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres create mode 100644 docs/gyms/tiny_sword/buffs/gold_auto_flux_buff.tres create mode 100644 docs/gyms/tiny_sword/buffs/gold_click_buff.tres create mode 100644 docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres create mode 100644 docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/README.md create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/Tower.png create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/Tower.png.import create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.gd create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.tscn create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.tscn create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_currencies_panel_old.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.tscn create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_cost_entry.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_recipe.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_cost_entry.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_recipe.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_0.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_1.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_recipe.tres create mode 100644 docs/gyms/tiny_sword/buildings/alchemy_tower/test_panel.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/castle/Castle.png create mode 100644 docs/gyms/tiny_sword/buildings/castle/Castle.png.import create mode 100644 docs/gyms/tiny_sword/buildings/castle/castle.gd create mode 100644 docs/gyms/tiny_sword/buildings/castle/castle.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/castle/castle.tscn create mode 100644 docs/gyms/tiny_sword/buildings/castle/test_buff.gd create mode 100644 docs/gyms/tiny_sword/buildings/castle/test_buff.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/farm/House1.png create mode 100644 docs/gyms/tiny_sword/buildings/farm/House1.png.import create mode 100644 docs/gyms/tiny_sword/buildings/farm/farm.tscn create mode 100644 docs/gyms/tiny_sword/buildings/farm/farm_generator.tres create mode 100644 docs/gyms/tiny_sword/buildings/forestry/House3.png create mode 100644 docs/gyms/tiny_sword/buildings/forestry/House3.png.import create mode 100644 docs/gyms/tiny_sword/buildings/forestry/forestry.gd create mode 100644 docs/gyms/tiny_sword/buildings/forestry/forestry.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/forestry/forestry.tscn create mode 100644 docs/gyms/tiny_sword/buildings/forestry/forestry_generator.tres create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1_Highlight.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1_Highlight.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 2.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 2.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 2_Highlight.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 2_Highlight.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3_Highlight.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3_Highlight.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4_Highlight.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4_Highlight.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5_Highlight.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5_Highlight.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png.import create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/Gold Stones.aseprite create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/gold_mine.tscn create mode 100644 docs/gyms/tiny_sword/buildings/gold_mine/gold_mine_generator.tres create mode 100644 docs/gyms/tiny_sword/buildings/monastery/Monastery.png create mode 100644 docs/gyms/tiny_sword/buildings/monastery/Monastery.png.import create mode 100644 docs/gyms/tiny_sword/buildings/monastery/monastery.gd create mode 100644 docs/gyms/tiny_sword/buildings/monastery/monastery.gd.uid create mode 100644 docs/gyms/tiny_sword/buildings/monastery/monastery.tscn create mode 100644 docs/gyms/tiny_sword/buildings/monastery/monastery_generator.tres create mode 100644 docs/gyms/tiny_sword/currencies/ascension.tres create mode 100644 docs/gyms/tiny_sword/currencies/cognite.tres create mode 100644 docs/gyms/tiny_sword/currencies/food.tres create mode 100644 docs/gyms/tiny_sword/currencies/gold.tres create mode 100644 docs/gyms/tiny_sword/currencies/magic_gold.tres create mode 100644 docs/gyms/tiny_sword/currencies/mana_stone.tres create mode 100644 docs/gyms/tiny_sword/currencies/philosoper_stone.tres create mode 100644 docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres create mode 100644 docs/gyms/tiny_sword/currencies/wood.tres create mode 100644 docs/gyms/tiny_sword/currencies/worker.tres create mode 100644 docs/gyms/tiny_sword/currency_panel.tscn create mode 100644 docs/gyms/tiny_sword/goals/gold_350k_wood_20k_goal.tres create mode 100644 docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres create mode 100644 docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres create mode 100644 docs/gyms/tiny_sword/goals/gold_total_30_goal.tres create mode 100644 docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres create mode 100644 docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres create mode 100644 docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres create mode 100644 docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres create mode 100644 docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn create mode 100644 docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres create mode 100644 docs/gyms/tiny_sword/prestige/primary_prestige.tres create mode 100644 docs/gyms/tiny_sword/research/farm_research.tres create mode 100644 docs/gyms/tiny_sword/research/forestry_research.tres create mode 100644 docs/gyms/tiny_sword/research/gold_mine_research.tres create mode 100644 docs/gyms/tiny_sword/research/ts_research_catalogue.tres create mode 100644 docs/gyms/tiny_sword/tiny_sword.tscn create mode 100644 docs/museums/edge_scroll_camera.tscn delete mode 100644 idles/buffs/bigger_forest.tres delete mode 100644 idles/buffs/library_auto_flux.tres delete mode 100644 idles/buffs/orb_auto_flux.tres delete mode 100644 idles/buffs/orb_click_focus.tres delete mode 100644 idles/buffs/orb_summon_spirit.tres delete mode 100644 idles/currencies/knowledge.tres delete mode 100644 idles/currencies/magic.tres delete mode 100644 idles/currencies/spirit.tres delete mode 100644 idles/currencies/wood.tres delete mode 100644 idles/generators/forestry.tres delete mode 100644 idles/generators/library.tres delete mode 100644 idles/generators/orb.tres delete mode 100644 idles/generators/spirit_factory.tres delete mode 100644 idles/goals/magic_350k_wood_20k.tres delete mode 100644 idles/goals/magic_total_1300.tres delete mode 100644 idles/goals/magic_total_13k.tres delete mode 100644 idles/goals/magic_total_30.tres delete mode 100644 idles/prestige/primary_prestige.tres create mode 100755 scripts/check_syntax.sh create mode 100755 scripts/run_all.sh create mode 100755 scripts/run_test.sh create mode 100644 test_multi_currency_prestige.gd create mode 100644 test_multi_currency_prestige.gd.uid create mode 100644 tests/test_ascension.gd create mode 100644 tests/test_ascension.gd.uid create mode 100644 tests/test_goals_prestige_reset.gd create mode 100644 tests/test_goals_prestige_reset.gd.uid create mode 100644 tests/test_goals_prestige_reset.tscn create mode 100644 tests/test_gold_mine_click.gd create mode 100644 tests/test_gold_mine_click.gd.uid create mode 100644 tests/test_prestige.gd create mode 100644 tests/test_prestige.gd.uid create mode 100644 tests/test_research_activation.gd create mode 100644 tests/test_research_activation.gd.uid create mode 100644 tests/test_research_prestige_reset.gd create mode 100644 tests/test_research_prestige_reset.gd.uid create mode 100644 tests/test_research_xp_acquisition.gd create mode 100644 tests/test_research_xp_acquisition.gd.uid create mode 100644 tests/test_runner.gd create mode 100644 tests/test_runner.gd.uid create mode 100644 tests/test_utils.gd create mode 100644 tests/test_utils.gd.uid diff --git a/.agents/skills/godot-test-instrumentation/SKILL.md b/.agents/skills/godot-test-instrumentation/SKILL.md new file mode 100644 index 0000000..f9e3a06 --- /dev/null +++ b/.agents/skills/godot-test-instrumentation/SKILL.md @@ -0,0 +1,642 @@ +--- +name: godot-test-instrumentation +description: Automates Godot testing for coding agents via headless execution, method simulation, and stdout validation. +--- + +# Godot Test Instrumentation + +Enables coding agents to automatically test game changes by running Godot in headless mode, simulating user actions via method calls, and validating results through structured stdout output. + +## Use This Skill When + +- A coding agent needs to verify gameplay changes work correctly +- Testing generator purchases, currency gains, prestige mechanics, or buff systems +- Running automated validation in CI/CD pipelines +- Debugging features without manual GUI interaction + +## Core Workflow + +``` +1. Agent writes/modifies game code + ↓ +2. Agent creates or updates test script in res://tests/ + ↓ +3. Agent runs: godot --headless --path . -s res://tests/test_x.gd + ↓ +4. Test loads gym scene, simulates actions, prints [TAG] results + ↓ +5. Agent parses stdout for [PASS]/[FAIL]/[RESULT] + ↓ +6. Exit code 0 = PASS, Exit code 1 = FAIL +``` + +--- + +## Directory Structure + +``` +res:// +├── tests/ +│ ├── test_runner.gd # Cross-platform test discovery & execution +│ ├── test_utils.gd # Shared assertion helpers +│ ├── test_gold_mine_click.gd # Example: generator click → currency gain +│ └── test_prestige.gd # Example: prestige mechanics validation +├── docs/gyms/ # Test scenes (e.g., tiny_sword.tscn) +└── core/ # Game logic (LevelGameState, generators, etc.) +``` + +--- + +## 1. Test Utils (res://tests/test_utils.gd) + +Shared assertion helpers with structured stdout output. + +```gdscript +class_name TestUtils +extends RefCounted + +static var _test_name: String = "" +static var _passed: int = 0 +static var _failed: int = 0 + +static func set_test_name(name: String) -> void: + _test_name = name + _passed = 0 + _failed = 0 + +static func assert_equals(expected: Variant, actual: Variant, message: String) -> void: + if expected == actual: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s (expected %s, got %s)" % [ + _test_name, message, str(expected), str(actual) + ]) + _failed += 1 + +static func assert_true(condition: bool, message: String) -> void: + if condition: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s" % [_test_name, message]) + _failed += 1 + +static func assert_greater_than(a: float, b: float, message: String) -> void: + if a > b: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s (expected %s > %s)" % [_test_name, message, str(a), str(b)]) + _failed += 1 + +static func print_result() -> void: + print("[SUMMARY] %s: %d passed, %d failed" % [_test_name, _passed, _failed]) + if _failed == 0: + print("[RESULT] PASS") + else: + print("[RESULT] FAIL") +``` + +--- + +## 2. Test Runner (res://tests/test_runner.gd) + +Cross-platform test discovery and execution. Runs all `test_*.gd` scripts in the tests directory. + +```gdscript +extends SceneTree + +var test_scripts: Array[String] = [] +var test_index: int = 0 +var total_passed: int = 0 +var total_failed: int = 0 + +func _init(): + print("\n=== GODOT TEST RUNNER ===\n") + _discover_tests() + + if test_scripts.is_empty(): + print("[ERROR] No test scripts found in res://tests/") + quit(1) + return + + print("Found %d test(s): %s\n" % [test_scripts.size(), str(test_scripts)]) + await _run_next_test() + + print("\n=== TEST SUMMARY ===") + print("Total passed: %d" % total_passed) + print("Total failed: %d" % total_failed) + + if total_failed == 0: + print("[OVERALL_RESULT] PASS") + quit(0) + else: + print("[OVERALL_RESULT] FAIL") + quit(1) + +func _discover_tests() -> void: + var dir = DirAccess.open("res://tests") + if dir: + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + if file_name.ends_with(".gd") and file_name.begins_with("test_") and file_name != "test_runner.gd" and file_name != "test_utils.gd": + test_scripts.append("res://tests/%s" % file_name) + file_name = dir.get_next() + dir.list_dir_end() + test_scripts.sort() + +func _run_next_test() -> void: + if test_index >= test_scripts.size(): + return + + var test_script = test_scripts[test_index] + print("--- Running test %d/%d: %s ---" % [test_index + 1, test_scripts.size(), test_script]) + + var test_instance = load(test_script).new() + await test_instance.run() + + total_passed += test_instance.passed + total_failed += test_instance.failed + + test_index += 1 + await _run_next_test() + +class TestScript: + var passed: int = 0 + var failed: int = 0 + + func run(): + pass +``` + +**Usage:** +```bash +# Run all tests via test runner +"$GODOT_BIN" --headless --path . -s res://tests/test_runner.gd +``` + +--- + +## 3. Test Script Template + +Each test extends `SceneTree` and implements a `run()` method. + +```gdscript +extends SceneTree + +var passed: int = 0 +var failed: int = 0 + +func _init(): + await run() + quit(0 if failed == 0 else 1) + +func run(): + # Setup + TestUtils.set_test_name("") + + # Load scene + var scene = load("") # e.g., "res://docs/gyms/tiny_sword/tiny_sword.tscn" + var root_node = scene.instantiate() + add_root_node(root_node) + + # Wait for scene initialization + await _wait() + + # Get references + var game_state: LevelGameState = root_node.find_child("LevelGameState") + var generator = root_node.find_child("") as CurrencyGenerator + + if game_state == null: + print("[ERROR] LevelGameState not found") + _print_summary() + return + + # Test logic + var initial_value = game_state.get_currency_amount_by_id(&"") + print("[TARGET] initial_: %s" % initial_value.to_string_suffix(2)) + + # Simulate action (method call, not mouse click) + generator._on_pressed() + + # Wait for signal propagation + await _wait() + + # Validate + var final_value = game_state.get_currency_amount_by_id(&"") + print("[TARGET] final_: %s" % final_value.to_string_suffix(2)) + + TestUtils.assert_greater_than( + final_value.mantissa, + initial_value.mantissa, + " increased after action" + ) + + _print_summary() + +func _wait() -> void: + await get_tree().create_timer(0.5).timeout + +func _print_summary(): + TestUtils.print_result() + passed = _get_passed_count() + failed = _get_failed_count() + +func _get_passed_count() -> int: + # Extract from TestUtils or track locally + return 0 + +func _get_failed_count() -> int: + return 0 +``` + +--- + +## 4. Example Test: Gold Mine Click + +Tests that clicking a generator grants currency. + +```gdscript +extends SceneTree + +var passed: int = 0 +var failed: int = 0 +var _local_passed: int = 0 +var _local_failed: int = 0 + +func _init(): + await run() + quit(failed == 0 ? 0 : 1) + +func run(): + print("\n=== TEST: Gold Mine Click ===\n") + TestUtils.set_test_name("gold_mine_click") + + # Load tiny_sword scene + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + var root_node = scene.instantiate() + add_root_node(root_node) + + await _wait() + + var game_state: LevelGameState = root_node.find_child("LevelGameState") + var gold_mine = root_node.find_child("GoldMine") as CurrencyGenerator + + if game_state == null or gold_mine == null: + print("[ERROR] Missing LevelGameState or GoldMine node") + _print_summary() + return + + # Record initial gold + var initial_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold") + print("[TARGET] initial_gold: %s" % initial_gold.to_string_suffix(2)) + + # Simulate clicking the generator + gold_mine._on_pressed() + + await _wait() + + # Validate gold increased + var final_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold") + print("[TARGET] final_gold: %s" % final_gold.to_string_suffix(2)) + + TestUtils.assert_greater_than( + final_gold.mantissa, + initial_gold.mantissa, + "Gold increased after generator click" + ) + + # Check specific value (adjust based on click_mantissa in data) + var expected_min: float = initial_gold.mantissa + 1.0 + TestUtils.assert_true( + final_gold.mantissa >= expected_min, + "Gold increased by at least 1.0" + ) + + _print_summary() + +func _wait() -> void: + await get_tree().create_timer(0.5).timeout + +func _print_summary(): + TestUtils.print_result() + # Note: For accurate counts, modify TestUtils to return counts or track locally + _local_failed = 0 # Update based on actual assertions +``` + +--- + +## 5. Example Test: Prestige Mechanics + +Tests prestige threshold and currency gain. + +```gdscript +extends SceneTree + +var passed: int = 0 +var failed: int = 0 + +func _init(): + await run() + quit(failed == 0 ? 0 : 1) + +func run(): + print("\n=== TEST: Prestige Mechanics ===\n") + TestUtils.set_test_name("prestige") + + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + var root_node = scene.instantiate() + add_root_node(root_node) + + await _wait() + + var game_state: LevelGameState = root_node.find_child("LevelGameState") + var prestige_manager = root_node.find_child("PrestigeManager") as PrestigeManager + + if game_state == null or prestige_manager == null: + print("[ERROR] Missing LevelGameState or PrestigeManager") + _print_summary() + return + + # Setup: Accumulate currencies to meet prestige threshold + game_state.add_currency_by_id(&"gold", BigNumber.from_float(100000.0)) + game_state.add_currency_by_id(&"gems", BigNumber.from_float(100.0)) + + await _wait() + + # Check if can prestige + var can_prestige: bool = prestige_manager.can_prestige() + print("[TARGET] can_prestige: %s" % str(can_prestige)) + + TestUtils.assert_true(can_prestige, "Should be able to prestige after accumulating currencies") + + if can_prestige: + # Perform prestige + prestige_manager.perform_prestige() + + await _wait() + + # Check ascension currency + var ascension: BigNumber = game_state.get_currency_amount_by_id(&"ascension") + print("[TARGET] ascension_currency: %s" % ascension.to_string_suffix(2)) + + TestUtils.assert_true( + ascension.mantissa > 0, + "Should receive ascension currency after prestige" + ) + + # Check prestige multiplier increased + var total_multiplier = prestige_manager.get_total_multiplier() + print("[TARGET] total_multiplier: %s" % str(total_multiplier)) + + TestUtils.assert_greater_than( + total_multiplier if total_multiplier is float else float(total_multiplier), + 1.0, + "Prestige multiplier should be > 1.0" + ) + + _print_summary() + +func _wait() -> void: + await get_tree().create_timer(0.5).timeout + +func _print_summary(): + TestUtils.print_result() +``` + +--- + +## 6. Shell Scripts + +### run_test.sh + +```bash +#!/bin/bash +# Usage: ./run_test.sh +# Example: ./run_test.sh res://tests/test_gold_mine_click.gd + +set -e + +TEST_SCRIPT="$1" +GODOT_BIN="${GODOT_BIN:-godot}" + +if [ -z "$TEST_SCRIPT" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Running test: $TEST_SCRIPT" +echo "" + +"$GODOT_BIN" --headless --path . -s "$TEST_SCRIPT" +EXIT_CODE=$? + +echo "" +if [ $EXIT_CODE -eq 0 ]; then + echo "✓ Test passed" +else + echo "✗ Test failed with exit code: $EXIT_CODE" +fi + +exit $EXIT_CODE +``` + +--- + +### run_all.sh + +```bash +#!/bin/bash +# Usage: ./run_all.sh + +set -e + +GODOT_BIN="${GODOT_BIN:-godot}" +TEST_DIR="res://tests" + +echo "=== Running All Tests ===" +echo "" + +# Run test runner (discovers all tests automatically) +"$GODOT_BIN" --headless --path . -s "$TEST_DIR/test_runner.gd" +EXIT_CODE=$? + +echo "" +if [ $EXIT_CODE -eq 0 ]; then + echo "✓ All tests passed" +else + echo "✗ Some tests failed" +fi + +exit $EXIT_CODE +``` + +--- + +### check_syntax.sh + +```bash +#!/bin/bash +# Usage: ./check_syntax.sh + +SCRIPT="$1" +GODOT_BIN="${GODOT_BIN:-godot}" + +if [ -z "$SCRIPT" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Checking syntax: $SCRIPT" +"$GODOT_BIN" --headless --path . --check-only --script "$SCRIPT" +EXIT_CODE=$? + +if [ $EXIT_CODE -eq 0 ]; then + echo "✓ Syntax valid" +else + echo "✗ Syntax errors found" +fi + +exit $EXIT_CODE +``` + +--- + +## 7. Godot CLI Command Reference + +| Command | Purpose | +|---------|---------| +| `"$GODOT_BIN" --headless --path . -s res://tests/test_x.gd` | Run single test in headless mode | +| `"$GODOT_BIN" --headless --path . -s res://tests/test_runner.gd` | Run all tests via test runner | +| `"$GODOT_BIN" --headless --path . --check-only --script res://test.gd` | Syntax validation only | +| `"$GODOT_BIN" --headless --verbose --path . -s res://tests/test_x.gd` | Verbose output for debugging | +| `"$GODOT_BIN" --headless --path . --quit-after 100 -s res://tests/test_x.gd` | Run for 100 frames then quit | +| `"$GODOT_BIN" --headless --path . -s res://tests/test_x.gd --log-file /tmp/test.log` | Log output to file | + +**Important:** Always use `--headless` for automated testing. This sets `--display-driver headless --audio-driver Dummy`. + +--- + +## 8. Output Parsing Protocol + +Agents should parse stdout for these tags: + +| Tag | Purpose | Example | +|-----|---------|---------| +| `[TARGET]` | Values to validate | `[TARGET] gold: 100.00` | +| `[PASS]` | Assertion passed | `[PASS] gold_mine_click: Gold increased]` | +| `[FAIL]` | Assertion failed | `[FAIL] gold_mine_click: Expected gold > 0]` | +| `[SUMMARY]` | Test completion stats | `[SUMMARY] gold_mine_click: 2 passed, 0 failed]` | +| `[RESULT]` | Overall test result | `[RESULT] PASS` or `[RESULT] FAIL]` | +| `[ERROR]` | Setup/runtime error | `[ERROR] Missing LevelGameState node]` | +| `[OVERALL_RESULT]` | Test runner summary | `[OVERALL_RESULT] PASS` or `[OVERALL_RESULT] FAIL]` | + +**Exit Codes:** +- `0` = All assertions passed +- `1` = One or more assertions failed or error occurred + +--- + +## 9. Best Practices + +### Simulating User Actions + +In headless mode, **do not** simulate mouse clicks with `InputEventMouseButton`. Instead, call methods directly: + +```gdscript +# ✅ DO: Call the method directly +generator._on_pressed() +game_state.add_currency_by_id(&"gold", BigNumber.from_float(100.0)) +prestige_manager.perform_prestige() + +# ❌ DON'T: Try to synthesize mouse events (won't work in headless) +var event = InputEventMouseButton.new() +Input.parse_input_event(event) # Ineffective without GUI +``` + +### Async Handling + +Use standard wait time for signal propagation: + +```gdscript +await get_tree().create_timer(0.5).timeout +``` + +Adjust based on test needs: +- `0.1s` - Simple state changes +- `0.5s` - Signal propagation (recommended default) +- `1.0s+` - Complex async operations + +### Scene Selection + +Use gym scenes from `res://docs/gyms/`: +- `res://docs/gyms/tiny_sword/tiny_sword.tscn` - Full game with generators, currencies, prestige +- Create minimal test scenes if testing isolated features + +### Test Naming + +- Use `test_.gd` naming convention +- Test name in `TestUtils.set_test_name()` should match filename (without `.gd`) + +### Assertions + +- One assertion per line for clear parsing +- Include descriptive messages +- Use `[TARGET]` to log values before/after actions + +--- + +## 10. Quick Start for Agents + +1. **Create test file:** + ```bash + touch res://tests/test_my_feature.gd + ``` + +2. **Copy template:** + ```gdscript + extends SceneTree + var passed: int = 0 + var failed: int = 0 + + func _init(): + await run() + quit(failed == 0 ? 0 : 1) + + func run(): + print("\n=== TEST: My Feature ===\n") + TestUtils.set_test_name("my_feature") + + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + var root = scene.instantiate() + add_root_node(root) + + await get_tree().create_timer(0.5).timeout + + # Your test logic here + + TestUtils.print_result() + ``` + +3. **Run test:** + ```bash + godot --headless --path . -s res://tests/test_my_feature.gd + ``` + +4. **Parse output:** + - Look for `[PASS]`, `[FAIL]`, `[RESULT]` tags + - Check exit code (0 = pass, 1 = fail) + +--- + +## References + +- [Godot CLI Tutorial](https://docs.godotengine.org/en/latest/tutorials/editor/command_line_tutorial.html) +- [Godot Headless Mode](https://docs.godotengine.org/en/latest/tutorials/editor/command_line_tutorial.html#run-options) +- [InputEvent Documentation](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html) + + + +/home/mikymod/work/jmp/idle/.agents/skills/godot-test-instrumentation/SKILL.md \ No newline at end of file diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.opencode/plans/currency-panel.md b/.opencode/plans/currency-panel.md new file mode 100644 index 0000000..495ab7e --- /dev/null +++ b/.opencode/plans/currency-panel.md @@ -0,0 +1,97 @@ +# Plan: Create CurrencyPanel Widget + +## Goal +Regroup the 9 individual CurrencyTile nodes in tiny_sword.tscn into a single dynamic `CurrencyPanel` widget that automatically retrieves and displays all currencies from the LevelGameState catalogue. + +## Changes + +### 1. Create `CurrencyPanel` script (`core/currency/currency_panel.gd`) + +**New file**: `res://core/currency/currency_panel.gd` + +**Class**: `class_name CurrencyPanel`, extends `PanelContainer` + +**Structure**: +```gdscript +class_name CurrencyPanel +extends PanelContainer + +const CURRENCY_TILE_SCENE: PackedScene = preload("res://currency_tile.tscn") + +@onready var _tiles_container: VBoxContainer = $CurrencyTiles + +var _game_state: LevelGameState + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + if _game_state == null: + push_error("CurrencyPanel: No LevelGameState found in parent hierarchy") + return + _build_currency_tiles() + _game_state.ready.connect(_on_game_state_ready) + +func _build_currency_tiles() -> void: + # Clear existing children + for child in _tiles_container.get_children(): + child.queue_free() + + if _game_state.currency_catalogue == null: + return + + for currency in _game_state.currency_catalogue.currencies: + if currency == null: + continue + var tile: CurrencyTile = CURRENCY_TILE_SCENE.instantiate() + # Set exports before add_child() so they're available in _ready() + tile.currency = currency + tile.game_state = _game_state + _tiles_container.add_child(tile) + +func _on_game_state_ready() -> void: + pass +``` + +**Why this works with existing CurrencyTile**: CurrencyTile uses `@export` for `currency` and `game_state`. When `instantiate()` is called, `_ready()` is NOT invoked yet - it fires when the node is added to the scene tree. By setting properties before `add_child()`, `_ready()` sees them as non-null. The DebugIncomeButton is preserved since CurrencyTile is unchanged. + +### 2. Update tiny_sword.tscn scene + +**Replace**: 9 individual CurrencyTile nodes under `LevelGameState/UI` with a single CurrencyPanel hierarchy. + +**Remove from scene**: +- All 9 individual CurrencyTile nodes: `GoldCurrencyTile`, `WorkerCurrencyTile`, `FoodCurrencyTile`, `WoodCurrencyTile`, `AscensionCurrencyTile`, `MagicGoldTile`, `ManaStoneTile`, `CogniteTile`, `PhilosoperStoneTile` +- ExtResource `7_0cs5o` (currency_tile.tscn) - no longer referenced directly +- All 9 individual currency resource ExtResources: `9_1363k` (gold), `10_i1cck` (worker), `11_hskcg` (food), `12_l6a68` (wood), `15_ascension`, `20_no27p` (magic_gold), `22_rbyxa` (mana_stone), `23_vrcrd` (cognite), `24_eaq6h` (philosoper_stone) + +**Add to scene**: +- ExtResource `8_cpnl` → `res://core/currency/currency_panel.gd` +- New node hierarchy: + ``` + LevelGameState/UI (Control, layout_mode=3) + CurrencyPanel (PanelContainer, layout_mode=1, pos 5,5 size 250x340, script=8_cpnl) + CurrencyTiles (VBoxContainer, separation=4) ← script-less, populated at runtime + GoalsDebugUI (repositioned: offset_left=265) + ``` + +**Keep**: GoalsDebugUI (repositioned slightly from offset_left=1275 to offset_left=265 to account for panel width) + +## File Structure After Changes + +``` +core/ + level_game_state.gd # unchanged (ready is built-in) + currency/ + currency_panel.gd # NEW: dynamic panel widget +docs/gyms/tiny_sword/ + tiny_sword.tscn # 9 tiles → 1 panel hierarchy (cleaner) +``` + +## No Changes Needed +- **`level_game_state.gd`**: The `ready` signal is built-in for Godot nodes - no addition needed. +- **`currency_label.gd` (CurrencyTile)**: Unchanged - DebugIncomeButton preserved. + +## Testing +- Run Godot headless to verify scene loads without errors +- Verify all 9 currencies appear in the CurrencyPanel +- Verify currency values update on changes (currency_changed signal) +- Verify DebugIncomeButton works on each tile +- Verify the panel doesn't overlap with GoalsDebugUI diff --git a/AGENTS.md b/AGENTS.md index 7db0bb7..f4a619f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,6 @@ # AGENTS.md -Godot 4.6 idle-game prototype; code is GDScript + `.tscn` scenes + JSON data at repo root. +Godot 4.6 idle-game; code is GDScript + `.tscn` scenes. + ## Build, Lint, And Test Commands Use `GODOT_BIN` for your local editor binary (e.g. `godot`, `godot4`, or full path). Run project: `"$GODOT_BIN" --path .` @@ -7,12 +8,14 @@ Headless smoke/lint parse: `"$GODOT_BIN" --headless --path . --quit` Script syntax check (single script): `"$GODOT_BIN" --headless --check-only --script res://big_number.gd` Tests: no test framework is currently committed (`tests/` and `addons/gut` are absent). Single-test command: N/A right now; if GUT is added, use `"$GODOT_BIN" --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/.gd`. + ## Architecture And Structure `project.godot` defines autoload singletons: `GameState` (runtime currency state + save/load) and `CurrencyGeneratorDatabase` (loads `generator_data.json`). `big_number.gd` (`class_name BigNumber`) is the internal numeric API for huge-value math, comparisons, formatting, and serialization. `big_number_museum.tscn` + `big_number_museum.gd` is the current main playable scene/prototype UI. `currency_generator.gd`, `currency_label.gd`, and `big_number_progress_bar.gd` are UI/gameplay adapters that react to `GameState` signals. Persistence is local JSON only: `user://idle_save.json`; there is no external DB/server/backend. + ## Code Style And Conventions Prefer typed GDScript (`var x: Type`, `func f() -> void`) and keep shared models/components as `class_name` scripts. Naming: `snake_case` for vars/functions/signals, `PascalCase` for classes, `UPPER_SNAKE_CASE` for constants. @@ -20,3 +23,5 @@ Follow existing Godot style: tabs/consistent indentation, early returns, and sho When reading files/JSON, always guard `FileAccess.open(...)` and validate parsed types before indexing. Use signals (already in `GameState`) for UI updates instead of polling or cross-node direct mutation. No Cursor/Claude/Windsurf/Cline/Goose/Copilot rule files were found in this repository, so this file is the canonical agent guidance. +Avoid trivial one-line wrapper/helper functions that only forward or repack data. Inline the logic at the real call site unless the wrapper adds meaningful abstraction or is reused enough to justify it. +Resource type safety: Use the specific resource. Avoid as much as possible explicit `Resource` usage. diff --git a/README.md b/README.md index 188cc30..6079e5a 100644 --- a/README.md +++ b/README.md @@ -1,261 +1,313 @@ -# Idles - Systems Architecture Report +# Idles — Godot 4.6 Idle Game + +A modular idle/incremental game prototype featuring a decoupled buff system, multi-target buffs with wildcard support, goal-based progression, prestige resets, and production-driven research. + +## Quick Start + +```bash +# Run the project +"$GODOT_BIN" --path . + +# Headless mode (lint/smoke test) +"$GODOT_BIN" --headless --path . --quit + +# Check single script syntax +"$GODOT_BIN" --headless --check-only --script res://core/big_number.gd +``` + +## Architecture Overview + +### Core Systems + +| System | Location | Purpose | +|--------|----------|---------| +| **BigNumber** | `core/big_number.gd` | Handles huge numbers (mantissa + exponent) for idle game math | +| **LevelGameState** | `core/level_game_state.gd` | Central state authority — currencies, generators, buffs, goals, research, persistence | +| **Currency** | `core/currency/`, `core/currency_catalogue.gd` | Catalog system for all in-game currencies | +| **Generators** | `core/generator/` | Production mechanics (auto cycles, clicks, purchases) | +| **Buffs** | `core/generator/generator_buff_data.gd` | Global, multi-target buffs applied to generators | +| **Goals** | `core/goals/` | Unlock conditions and achievement tracking | +| **Prestige** | `core/prestige/` | Reset-with-bonus mechanics | +| **Research** | `core/research/` | XP-based progression with production multipliers | + +### Key Nodes + +| Node | Script | Description | +|------|--------|-------------| +| `LevelGameState` | `core/level_game_state.gd` | Central state management, save/load, catalogs, all signals | +| `PrestigeManager` | `core/prestige/prestige_manager.gd` | Prestige calculation, reset orchestration | +| `CurrencyGenerator` | `core/generator/currency_generator.gd` | Per-generator production, purchasing, buff interaction | +| `GeneratorPanel` | `core/generator/generator_container.gd` | UI panel for a single generator | + +**Important:** There are **no autoload singletons**. `LevelGameState` and `PrestigeManager` are regular nodes instantiated in the scene (e.g., `docs/gyms/tiny_sword/tiny_sword.tscn`). All code accesses them through `@export` references or `get_node()`, never through an autoload name like `/root/GameState`. -## Scope Covered +### Data Directory Structure -This report covers all tracked files in the repository (62 total at time of analysis), including: +All gameplay resources (`.tres` files) live under: -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. +``` +docs/gyms/tiny_sword/ +├── currencies/ # Currency resources (gold, food, wood, worker, etc.) +├── buildings/ # Generator configuration data +│ ├── farm/ # farm_generator.tres +│ ├── forestry/ # forestry_generator.tres +│ ├── gold_mine/ # gold_mine_generator.tres +│ ├── monastery/ # monastery_generator.tres +│ └── alchemy_tower/ # alchemy_tower_data.tres + recipes/ +├── buffs/ # Buff definitions with target_ids +├── goals/ # Unlock goal definitions +├── prestige/ # Prestige configuration (primary_prestige.tres) +├── research/ # Research track data +└── test/ # (reserved for tests) +``` + +Game art assets live separately in: + +``` +sandbox/tiny_swords/ +├── Buildings/ # Building sprites +├── Terrain/ # Tilesets and decorations +├── UI Elements/ # UI assets +└── Units/ # Unit sprites +``` + +## Core Documentation + +Each `core/` subsystem has its own detailed README: + +| Module | Documentation | +|--------|--------------| +| Core overview | `core/README.md` | +| Currency system | `core/currency/README.md` | +| Generator system | `core/generator/README.md` | +| Goals system | `core/goals/README.md` | +| Prestige system | `core/prestige/README.md` | +| Research system | `core/research/README.md` | + +## Buff System + +Buffs are **globally registered** and **multi-target capable**: + +- **Global Levels**: Each buff has a single level shared across all generators it targets. +- **Multi-Target**: A single buff resource can affect multiple generators. Controlled by the `target_ids` array on `GeneratorBuffData`. +- **Wildcards**: A `target_ids` value of `["*"]` applies the buff to all current AND future generators. +- **Catalog-Based Loading**: Buffs are defined in a `BuffCatalogue` resource (`@export var buff_catalogue: BuffCatalogue` on `LevelGameState`). No runtime directory scanning is performed. +- **Goal-Based Activation**: Buffs with an `unlock_goal` automatically unlock when their goal is completed. +- **Multiplicative Stacking**: Multiple buffs of the same kind on a generator stack multiplicatively (`buff1 × buff2 × …`). +- **Buff Kinds**: `AUTO_PRODUCTION_MULTIPLIER`, `MANUAL_CLICK_MULTIPLIER`, `RESOURCE_PURCHASE`, `RESEARCH_XP_MULTIPLIER` + +### Lifecycle + +| Event | What happens | +|-------|-------------| +| **Default state** | Inactive (locked) until its unlock goal is met | +| **Goal met** | Buff auto-unlocks, becomes purchasable | +| **Purchased** | Level increments, effect applies to all target generators | +| **Prestige reset** | All buff levels reset to 0, all buffs re-locked | + +### API (on LevelGameState) + +```gdscript +LevelGameState.register_buff(buff: GeneratorBuffData) +LevelGameState.get_buff(buff_id: StringName) -> GeneratorBuffData +LevelGameState.get_buff_level(buff_id: StringName) -> int +LevelGameState.get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData] +LevelGameState.get_effective_multiplier(generator_id: StringName, kind: int) -> float +LevelGameState.set_buff_level(buff_id: StringName, level: int) +LevelGameState.is_buff_active(buff_id: StringName) -> bool +``` + +## Goals System + +Goals track player progress and unlock generators or buffs when requirements are met. + +- **Automatic or Manual**: Goals define their own `unlock_behavior` (`AUTOMATIC` or `MANUAL`). +- **Logarithmic Progress**: Progress bars use log-scale for visual feedback on huge numbers. +- **Prestige Reset**: All goals are reset to incomplete on prestige. Previously unlocked generators re-lock until their goal is met again. + +### Signals on LevelGameState + +```gdscript +goal_completed(goal_id: StringName) +goal_progress_changed(goal_id: StringName, progress: float) +``` + +## Prestige System + +Prestige resets run progress in exchange for a permanent production multiplier. + +- **Configuration**: `PrestigeConfig` resource at `docs/gyms/tiny_sword/prestige/primary_prestige.tres` +- **Basis types**: `LIFETIME_TOTAL` (single currency), `RUN_TOTAL`, `RUN_MAX`, `ALL_CURRENCIES` (sum of all tracked currencies) +- **Formula types**: `POWER` (`scale × ratio^exponent`) or `TRIANGULAR_INVERSE` +- **Multiplier modes**: `ADDITIVE` or `MULTIPLICATIVE_POWER` + +### What gets reset on prestige + +| State | Behavior | +|-------|----------| +| **Current currency balances** | Reset to 0 (except prestige currency if configured) | +| **Generator states** (owned, unlocked) | Cleared | +| **Buff levels** | All reset to 0, all re-locked | +| **Goals** | All reset to incomplete | +| **Research XP and levels** | All cleared to 0 | +| **Lifetime currency totals** | Preserved | +| **All-time currency acquired** | Preserved | +| **Prestige points earned** | Preserved (permanent) | + +## Research System + +XP-based progression tied to generator output. Research tracks level up automatically, granting permanent production multipliers to their associated generator. + +- **XP Formula**: `XP = currency_produced × xp_per_currency_produced × buff_multiplier` +- **Level Formula**: `xp_required(n) = base_xp_required × growth_multiplier^(n-1)` +- **Buffs**: The `RESEARCH_XP_MULTIPLIER` buff kind increases XP gain for a research track. +- **Prestige Reset**: All research XP and levels reset to 0. + +## Save Format + +**Version 5 (current):** + +```json +{ + "save_format_version": 5, + "currencies": { + "": { + "current": {"m": 1.0, "e": 0}, + "total": {"m": 1.0, "e": 0}, + "all_time": {"m": 1.0, "e": 0} + } + }, + "generator_states": { + "": { + "owned": 0, + "purchased_count": 0, + "unlocked": false, + "available": false + } + }, + "buff_levels": { "": 0 }, + "buff_unlocked": { "": false }, + "buff_active": { "": false }, + "goals": { "completed": [""] }, + "research_xp": { "": {"m": 0.0, "e": 0} }, + "research_levels": { "": 0 }, + "research_workers": 0, + "prestige_state": { … }, + "last_save_time": 1234567890 +} +``` + +Notes: +- All large numbers are serialized as `{"m": mantissa_float, "e": exponent_int}` (BigNumber format). +- `prestige_state` is an external section managed by `PrestigeManager`, not by `LevelGameState` directly. +- `research_workers` tracks workers assigned to research across all tracks. +- Save file is written to `user://level_save.json`. + +## Save/Load + +- **Save**: `LevelGameState.save_game()` serializes all state to JSON. Called on prestige reset (via `PrestigeManager`) and when manually triggered. +- **Load**: `LevelGameState.load_game()` deserializes from disk during `_ready()`. Handles version migration from v2+. +- **External sections**: Other systems (e.g. `PrestigeManager`) can attach their data via `set_external_save_data()` / `get_external_save_data()`. + +## Development + +### Adding New Content + +All gameplay resources go under `docs/gyms/tiny_sword/` (not `sandbox/` — that's for art assets only). + +**New Currency:** +1. Create `res://docs/gyms/tiny_sword/currencies/.tres` (extend `Currency`) +2. Set `id`, `display_name`, `icon` +3. Add to the `CurrencyCatalogue` resource (`ts_currency_catalogue.tres`) + +**New Generator:** +1. Create `res://docs/gyms/tiny_sword/buildings//_generator.tres` (extend `CurrencyGeneratorData`) +2. Configure production stats, costs, purchase currency, optional `unlock_goal` +3. Add a `CurrencyGenerator` node to the scene referencing this data + +**New Buff:** +1. Create `res://docs/gyms/tiny_sword/buffs/.tres` (extend `GeneratorBuffData`) +2. Set `target_ids` (e.g., `["farm", "forestry"]` or `["*"]` for all generators) +3. Configure `kind`, `effect_increment`, cost, and optional `unlock_goal` +4. Add to the `BuffCatalogue` resource (`ts_buff_catalogue.tres`) + +**New Research:** +1. Create `res://docs/gyms/tiny_sword/research/.tres` (extend `ResearchData`) +2. Link to a generator via `generator_id` +3. Configure `xp_per_currency_produced`, `base_xp_required`, `xp_growth_multiplier`, `multiplier_per_level` +4. Add to the `ResearchCatalogue` resource (`ts_research_catalogue.tres`) + +### BigNumber System + +- **Representation**: `mantissa: float` × `10^exponent: int` +- **Range**: Values up to ~10^308 before float precision loss +- **Operations**: `add()`, `subtract()`, `multiply()`, `divide()`, `compare_to()`, `add_in_place()` +- **Serialization**: `.serialize()` → `{"m": float, "e": int}`; `BigNumber.deserialize(dict)` → `BigNumber` +- **Display**: `.to_string_suffix(decimals: int)` → `"1.50K"`, `"3.21M"`, etc. + +### Production Formulas + +``` +Cost for N items: base × coefficient^owned × (coefficient^n − 1) / (coefficient − 1) +Max affordable: floor(log(currency × (coefficient − 1) / (base × coefficient^owned) + 1) / log(coefficient)) +Production/cycle: initial_productivity × owned × run_mult × milestone_mult × purchased_mult +Production/second: production/cycle / initial_time +``` + +## Project Structure + +``` +core/ # Engine code (all .gd scripts) +├── big_number.gd # BigNumber class +├── level_game_state.gd # Central state manager (LevelGameState node) +├── buff_catalogue.gd # Buff catalogue resource +├── currency_catalogue.gd # Currency catalogue resource +├── goal_catalogue.gd # Goal catalogue resource +├── currency/ # Currency resource definition +├── generator/ # Generator, buff data, UI panel, research buff calculator +├── goals/ # Goal/requirement resources +├── prestige/ # Prestige manager, config, panel +└── research/ # Research catalogue, panel, row UI + +docs/gyms/tiny_sword/ # Playable game content (.tres resources) +├── currencies/ # Currency definitions +├── buildings/ # Generator configs +├── buffs/ # Buff definitions +├── goals/ # Goal definitions +├── prestige/ # Prestige config +├── research/ # Research configs +└── test/ # (reserved for tests) + +sandbox/tiny_swords/ # Art assets (sprites, tilesets, UI textures) +└── Buildings/, Terrain/, UI Elements/, Units/ + +docs/museums/ # Prototype/demo scenes (big_number_museum) +``` + +## Known Issues / TODOs -The focus below is on behavior-bearing files and how they interact. +- [ ] Periodic auto-save not implemented (saves only on prestige and manual trigger) +- [ ] No automated test suite +- [ ] Some buffs may be configured with no unlock path -## Architecture Summary +## Technical Details -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`) directly on generator data (`CurrencyGeneratorData.unlock_goal`). -6. UI scripts are thin adapters listening to `GameState` signals. +### Buff Multiplier Stacking -## Core Systems +Multiple buffs of the same kind on the same generator stack **multiplicatively**: -### 1) Numeric System (`BigNumber`) +``` +total_multiplier = buff1_effect(level) × buff2_effect(level) × buff3_effect(level) × … +``` -File: `core/big_number.gd` +### Prestige Multiplier Application -`BigNumber` stores values as mantissa + exponent (scientific notation style), normalizes them, and provides: +Generators apply prestige multipliers automatically. The prestige multiplier is fetched from `PrestigeManager.get_total_multiplier()` and multiplied into the effective production rate alongside research and buff multipliers. -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. +## License -This class is used everywhere currency values move. +Internal prototype — no external license applied. -### 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`). -8. Optional generator unlock goal (`unlock_goal`). - -### 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/currency_generator_data.gd` -4. `core/generator/currency_generator.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. Each `CurrencyGenerator` evaluates its own `data.unlock_goal` on startup and on `currency_changed`. -4. If the goal is met, it sets both `unlocked = true` and `available = true`, then emits `goal_achieved(generator_id, goal_id)`. - -### 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. - -### 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 and unlocks via `magic_total_30` goal. - -### 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. - -## 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`, `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`, `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(generator_id, goal_id)` | `core/generator/currency_generator.gd` | `CurrencyGenerator._evaluate_generator_unlock_goal` | Generator unlock goal transitions that 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 generator-local unlock evaluator, evaluator sets generator `unlocked/available`, emits `goal_achieved(generator_id, goal_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 goals now assume the target generator exists in-scene and owns its own `unlock_goal`. -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. +*Godot version: 4.6* diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..3b1b8c2 --- /dev/null +++ b/TODO.md @@ -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. diff --git a/big_number_progress_bar.gd b/big_number_progress_bar.gd index 773a2e0..7480214 100644 --- a/big_number_progress_bar.gd +++ b/big_number_progress_bar.gd @@ -1,6 +1,7 @@ extends HBoxContainer -@export var currency: Resource +@export var currency: Currency +@export var game_state: LevelGameState @onready var _label_start = $LabelStart @onready var _label_end = $LabelEnd @@ -22,15 +23,16 @@ func set_limits(start: BigNumber, end: BigNumber) -> void: _progress_bar.max_value = _end.get_ratio(_start) func _ready() -> void: - if currency == null: - currency = CurrencyDatabase.get_currency_resource(GameState.GOLD_CURRENCY_ID) if currency == null: push_warning("BigNumberProgressBar '%s' has no currency configured." % String(name)) return - _currency_id = GameState.get_currency_id(currency) + if game_state == null: + push_warning("BigNumberProgressBar '%s' has no game_state configured." % String(name)) + return + _currency_id = game_state.get_currency_id(currency) - GameState.currency_changed.connect(_on_currency_changed) - _current = GameState.get_currency_amount_by_id(_currency_id) + game_state.currency_changed.connect(_on_currency_changed) + _current = game_state.get_currency_amount_by_id(_currency_id) set_limits(_current, _current.add(BigNumber.new(1, 1))) diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..71ce681 --- /dev/null +++ b/core/README.md @@ -0,0 +1,266 @@ +# Core Module Documentation + +## Overview + +The `core/` directory contains the fundamental systems for the idle game. It provides: + +- **Numeric Foundation**: `BigNumber` for handling huge idle game values +- **Game State Management**: `LevelGameState` for all persistent state +- **Currency System**: `CurrencyCatalogue` and tracking for multiple currency types +- **Generator System**: Automated resource production with scaling costs +- **Buff System**: Upgrades that modify generator behavior +- **Goal System**: Achievement tracking and unlock triggers +- **Prestige System**: Rebirth mechanics with permanent multipliers +- **Research System**: XP-based progression with production multipliers + +## Architecture + +``` +core/ +├── big_number.gd # Core numeric API +├── level_game_state.gd # Main state management (Node) +├── currency/ # Currency data structures + CurrencyCatalogue +├── currency_catalogue.gd # Currency catalogue resource +├── buff_catalogue.gd # Buff catalogue resource +├── goal_catalogue.gd # Goal catalogue resource +├── generator/ # Generator logic and data +├── goals/ # Goal/achievement system +├── prestige/ # Prestige/rebirth system +└── research/ # Research XP and progression +``` + +## Data Flow + +1. **Game Start**: `LevelGameState._ready()` initializes currencies, loads save +2. **Player Action**: Generator bought → `LevelGameState` updated → signals emitted +3. **UI Reaction**: Listeners respond to signals → update display +4. **Goal Check**: Currency changed → goals evaluated → buffs unlocked +5. **Save**: `LevelGameState.save_game()` → JSON to `user://level_save.json` + +## Production Multiplier Chain + +All multipliers compose multiplicatively from callee to caller. The entry point is +`_grant_cycle_income()` in `CurrencyGenerator`. + +### Entry: `CurrencyGenerator._grant_cycle_income()` (`currency_generator.gd:134`) + +``` +var effective = get_effective_auto_run_multiplier() +var per_cycle = data.production_per_cycle(owned, effective, purchased_count) +var produced = per_cycle × cycle_count +``` + +### Layer 1 — `CurrencyGeneratorData.production_per_cycle()` (`currency_generator_data.gd:117`) + +``` +production_per_cycle = initial_productivity × owned × run_multiplier × milestone_mult × purchased_mult +``` + +| Sub-multiplier | Method | Formula | +|---|---|---| +| **milestone_mult** | `milestone_multiplier_total(owned)` | `milestone_multiplier ^ floor(owned / milestone_step)` | +| **purchased_mult** | `purchased_multiplier(purchased)` | `1.0 + purchased × (purchased_boost_percent / 100)` | + +### Layer 2 — `CurrencyGenerator.get_effective_auto_run_multiplier()` (`currency_generator.gd:357`) + +This is the `run_multiplier` value passed into Layer 1 above: + +``` +effective_run_multiplier = run_multiplier × research_multiplier × auto_production_buffs × prestige_multiplier +``` + +#### 2a. `run_multiplier` + +Exported `float` on the `CurrencyGenerator` node. Default `1.0`. A scene-level tweak. + +#### 2b. `research_multiplier` → `LevelGameState.get_research_multiplier(id)` (`level_game_state.gd:488`) → `ResearchData.get_multiplier_for_level(level)` (`research/research_data.gd:52`) + +``` +research_multiplier = base_multiplier + (multiplier_per_level × research_level) +``` + +Level is auto-calculated from accumulated BigNumber XP. Both constants are exports on `ResearchData`. + +#### 2c. `auto_production_buffs` → `LevelGameState.get_effective_multiplier(generator_id, AUTO_PRODUCTION_MULTIPLIER)` (`level_game_state.gd:441`) + +```gdscript +# For every active buff of kind AUTO_PRODUCTION_MULTIPLIER targeting this generator: +mult = 1.0 +for buff in buffs: + if buff.active and buff.kind == AUTO_PRODUCTION_MULTIPLIER: + mult *= buff.get_effect_multiplier(level) +``` + +`GeneratorBuffData.get_effect_multiplier(level)` (`generator_buff_data.gd:97`): + +``` +buff_effect = base_effect + (effect_increment × level) +``` + +Multiple buffs of the same kind stack **multiplicatively**: `∏ (base_effect_i + increment_i × level_i)`. + +#### 2d. `prestige_multiplier` → `PrestigeManager.get_total_multiplier()` (`prestige_manager.gd:107`) + +Configured via `PrestigeConfig.multiplier_mode`: + +| Mode | Formula | +|---|---| +| `ADDITIVE` | `base + prestige_points × multiplier_per_prestige` | +| `MULTIPLICATIVE_POWER` | `base × (1 + multiplier_per_prestige) ^ (prestige_points ^ exponent)` | + +### Layer 3 — Production per second + +``` +production_per_second = production_per_cycle / maxf(initial_time, 0.0001) +``` + +(`currency_generator_data.gd:128`) + +### Full Formula (one production cycle) + +``` +produced = initial_productivity × owned + + # --- effective_run_multiplier --- + × run_multiplier + × (base_multiplier + multiplier_per_level × research_level) + × ∏(base_effect_i + effect_increment_i × buff_level_i) [auto-production buffs] + × prestige_multiplier [from PrestigeManager] + + # --- milestone & purchased --- + × milestone_multiplier ^ floor(owned / milestone_step) + × (1.0 + purchased_count × purchased_boost_percent / 100) +``` + +### Manual Click Formula (separate path) + +`_try_grant_click_currency()` (`currency_generator.gd:460`): + +``` +click_reward = (click_mantissa × 10^click_exponent) × ∏ buff_effect_i [MANUAL_CLICK_MULTIPLIER buffs] +``` + +Uses the same `get_effective_multiplier()` loop filtered to `BuffKind.MANUAL_CLICK_MULTIPLIER`. +Research, prestige, milestone, and purchased multipliers are **not** applied to clicks. + +### Research XP Awarded (production side-effect) + +Awarded inside `_grant_cycle_income()` when the generator has an associated `ResearchData`: + +``` +base_xp = produced_currency × xp_per_currency_produced +scaled_xp = base_xp × (1.0 + research_workers × worker_scaling_factor) +actual_xp = scaled_xp × ∏ buff_effect_i [RESEARCH_XP_MULTIPLIER buffs] +``` + +Buff multiplier for XP is applied later in `LevelGameState.add_research_xp()` +via `ResearchBuffCalculator.apply_buffs()` (`core/generator/research_buff_calculator.gd`). + +### Cascade Diagram + +``` + ┌─────────────────────────────────┐ + │ run_multiplier (node export) │ + │ research_multiplier (XP level) │─── effective_run_multiplier ──┐ + │ auto-production buffs (∏) │ │ + │ prestige_multiplier │ │ + └─────────────────────────────────┘ │ + ▼ +production_per_cycle = initial_productivity × owned × effective_run_multiplier × milestone × purchased + │ + ▼ +production_per_second = production_per_cycle / initial_time +``` + +## Save Format + +**Version**: 5 + +```json +{ + "save_format_version": 5, + "currencies": { + "": { + "current": {"m": float, "e": int}, + "total": {"m": float, "e": int}, + "all_time": {"m": float, "e": int} + } + }, + "generator_states": { + "": { + "owned": int, + "purchased_count": int, + "unlocked": bool, + "available": bool + } + }, + "buff_levels": { + "": int + }, + "buff_unlocked": { + "": true + }, + "buff_active": { + "": true + }, + "goals": { + "completed": ["", ...] + }, + "research_xp": { + "": {"m": float, "e": int} + }, + "research_levels": { + "": int + }, + "last_save_time": unix_timestamp +} +``` + +## Key Signals + +| Signal | Emitted When | +|--------|--------------| +| `currency_changed(currency_id, new_amount)` | Any currency balance changes | +| `generator_state_changed(generator_id, state)` | Generator owned/unlocked changes | +| `generator_buff_level_changed(gen_id, buff_id, level)` | Buff level increased | +| `buff_unlocked_changed(buff_id, unlocked)` | Buff unlock state changed | +| `goal_completed(goal_id)` | Goal criteria met | + +## BigNumber Serialization + +All large numbers use scientific notation internally: +```gdscript +# Value = mantissa * 10^exponent +var num = BigNumber.new(1.5, 24) # 1.5e24 +var dict = num.serialize() # {"m": 1.5, "e": 24} +var restored = BigNumber.deserialize(dict) +``` + +## Extending the Core + +### Adding a New Currency +1. Create `res://sandbox/currencies/.tres` extending `Currency` +2. Set `id`, `display_name`, and `icon` +3. Add to `CurrencyCatalogue` resource assigned to `LevelGameState` + +### Adding a New Generator +1. Create `res://sandbox/generators/.tres` extending `CurrencyGeneratorData` +2. Configure costs, production, and unlock goals +3. Instance `CurrencyGenerator` scene with this data + +### Adding a New Buff +1. Create `res://sandbox/buffs/.tres` extending `GeneratorBuffData` +2. Set `target_ids`, `kind`, `effect_increment`, and `cost` +3. Add to `BuffCatalogue` resource assigned to `LevelGameState` + +### Adding New Research +1. Create `res://sandbox/research/.tres` extending `ResearchData` +2. Link to a generator via `generator_id` +3. Add to `ResearchCatalogue` resource assigned to `LevelGameState` + +## See Also + +- `currency/` - Currency data structures +- `generator/` - Generator production system +- `goals/` - Achievement system +- `prestige/` - Rebirth mechanics diff --git a/core/big_number.gd b/core/big_number.gd index 92dda83..de29fc3 100644 --- a/core/big_number.gd +++ b/core/big_number.gd @@ -195,6 +195,16 @@ func get_ratio(target: BigNumber) -> float: static func from_float(val: float) -> BigNumber: return BigNumber.new(val, 0) +## Converts this BigNumber to a standard float. Returns INF or 0.0 for extreme values. +func to_float() -> float: + if mantissa <= 0.0: + return 0.0 + if exponent > 308: + return INF + if exponent < -308: + return 0.0 + return mantissa * pow(10.0, float(exponent)) + ## Outputs a UI-friendly string (e.g., "1.50e12") func to_string_sci(decimals: int = 2) -> String: if exponent < 3: diff --git a/core/buff_catalogue.gd b/core/buff_catalogue.gd new file mode 100644 index 0000000..63ecb2f --- /dev/null +++ b/core/buff_catalogue.gd @@ -0,0 +1,24 @@ +class_name BuffCatalogue +extends Resource + +@export var buffs: Array[GeneratorBuffData] = [] + +func get_buff_by_id(id: StringName) -> GeneratorBuffData: + for buff in buffs: + if buff.id == id: + return buff + return null + +func get_all_ids() -> Array[StringName]: + var ids: Array[StringName] = [] + for buff in buffs: + if buff.id: + ids.append(buff.id) + return ids + +func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]: + var result: Array[GeneratorBuffData] = [] + for buff in buffs: + if buff.targets_generator(generator_id): + result.append(buff) + return result diff --git a/core/buff_catalogue.gd.uid b/core/buff_catalogue.gd.uid new file mode 100644 index 0000000..7021592 --- /dev/null +++ b/core/buff_catalogue.gd.uid @@ -0,0 +1 @@ +uid://ctc5yjlnvi0ok diff --git a/core/currency/README.md b/core/currency/README.md new file mode 100644 index 0000000..58e58ab --- /dev/null +++ b/core/currency/README.md @@ -0,0 +1,111 @@ +# Currency Module Documentation + +## Overview + +The `currency/` subfolder contains the data structure for defining in-game currencies. + +## Files + +### `currency.gd` + +Defines the `Currency` class - a Resource for currency metadata. + +## Currency Class + +```gdscript +class_name Currency +extends Resource + +@export var id: StringName = &"" # Unique identifier (e.g., &"gold") +@export var display_name: String = "" # UI-friendly name +@export var icon: Texture2D # Display icon +``` + +## Usage + +### Defining a Currency + +Create a `.tres` resource file with: +- **id**: Lowercase unique key (used internally) +- **display_name**: What players see ("Gold", "Gems") +- **icon**: Texture for UI display + +Example `res://sandbox/currencies/gold.tres`: +``` +[gd_resource type="Resource" script_class="Currency"] +[resource] +id = &"gold" +display_name = "Gold" +icon = ExtResource("uid://...") +``` + +### Accessing Currencies + +Currencies are defined in a `CurrencyCatalogue` resource that holds an array of `Currency` resources. + +```gdscript +# Get all known currency IDs from the catalogue +var ids: Array[StringName] = currency_catalogue.get_all_ids() + +# Get currency resource by ID +var gold: Currency = currency_catalogue.get_currency_by_id(&"gold") +``` + +## Integration with LevelGameState + +Currencies are tracked in three ways: +1. **current**: Current balance (resets on prestige) +2. **total**: Total acquired this run (resets on prestige) +3. **all_time**: Lifetime total (never resets) + +```gdscript +# Add currency +level_game_state.add_currency(gold_resource, BigNumber.new(100.0, 0)) + +# Add by ID +level_game_state.add_currency_by_id(&"gold", BigNumber.new(100.0, 0)) + +# Get balance +var balance: BigNumber = level_game_state.get_currency_amount_by_id(&"gold") + +# Spend currency (returns false if insufficient) +var success: bool = level_game_state.spend_currency_by_id(&"gold", BigNumber.new(50.0, 0)) + +# Check if currency exists +var is_valid: bool = level_game_state.is_known_currency_id(&"gems") + +# Get currency resource +var gold: Currency = level_game_state.get_currency_resource(&"gold") + +# Get display name +var name: String = level_game_state.get_currency_name(&"gold") +``` + +## CurrencyCatalogue Class + +```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] +``` + +The `CurrencyCatalogue` is a resource that holds all defined currencies and is assigned to `LevelGameState` via the `currency_catalogue` export variable. + +## Signals + +`LevelGameState` emits `currency_changed(currency_id: StringName, new_amount: BigNumber)` whenever a currency balance changes. + +## See Also + +- `core/level_game_state.gd` - Currency state management +- `core/big_number.gd` - Large number handling +- `core/currency/currency_catalogue.gd` - Currency catalogue resource +- `core/generator/currency_generator_data.gd` - Generator purchase currency + + + +/home/mikymod/work/jmp/idle/core/currency/README.md diff --git a/core/currency/currency.gd b/core/currency/currency.gd index 31f81e8..bb8da2f 100644 --- a/core/currency/currency.gd +++ b/core/currency/currency.gd @@ -4,3 +4,4 @@ extends Resource @export var id: StringName = &"" @export var display_name: String = "" @export var icon: Texture2D +@export var always_visible: bool = false diff --git a/core/currency/currency_catalogue.gd b/core/currency/currency_catalogue.gd new file mode 100644 index 0000000..7a2d0c0 --- /dev/null +++ b/core/currency/currency_catalogue.gd @@ -0,0 +1,17 @@ +class_name CurrencyCatalogue +extends Resource + +@export var currencies: Array[Currency] = [] + +func get_currency_by_id(id: StringName) -> Currency: + for currency in currencies: + if currency.id == id: + return currency + return null + +func get_all_ids() -> Array[StringName]: + var ids: Array[StringName] = [] + for currency in currencies: + if currency.id != &"": + ids.append(currency.id) + return ids diff --git a/core/currency/currency_catalogue.gd.uid b/core/currency/currency_catalogue.gd.uid new file mode 100644 index 0000000..4eaf29c --- /dev/null +++ b/core/currency/currency_catalogue.gd.uid @@ -0,0 +1 @@ +uid://621tus0uvbrd diff --git a/core/currency/currency_panel.gd b/core/currency/currency_panel.gd new file mode 100644 index 0000000..c7cb9b0 --- /dev/null +++ b/core/currency/currency_panel.gd @@ -0,0 +1,35 @@ +## Dynamically displays all currencies from the LevelGameState catalogue as CurrencyTile widgets. +class_name CurrencyPanel +extends PanelContainer + +const CURRENCY_TILE_SCENE: PackedScene = preload("res://currency_tile.tscn") + +@onready var _tiles_container: VBoxContainer = $CurrencyTiles + +var _game_state: LevelGameState + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + if _game_state == null: + push_error("CurrencyPanel: No LevelGameState found in parent hierarchy") + return + _build_currency_tiles() + _game_state.ready.connect(_on_game_state_ready) + +func _build_currency_tiles() -> void: + for child in _tiles_container.get_children(): + child.queue_free() + + if _game_state.currency_catalogue == null: + return + + for currency in _game_state.currency_catalogue.currencies: + if currency == null: + continue + var tile: CurrencyTile = CURRENCY_TILE_SCENE.instantiate() + tile.currency = currency + tile.game_state = _game_state + _tiles_container.add_child(tile) + +func _on_game_state_ready() -> void: + pass diff --git a/core/currency/currency_panel.gd.uid b/core/currency/currency_panel.gd.uid new file mode 100644 index 0000000..f5e762d --- /dev/null +++ b/core/currency/currency_panel.gd.uid @@ -0,0 +1 @@ +uid://blfou5xiynxqf diff --git a/core/currency_database.gd b/core/currency_database.gd deleted file mode 100644 index cb0921a..0000000 --- a/core/currency_database.gd +++ /dev/null @@ -1,164 +0,0 @@ -extends Node - -#const GOLD_CURRENCY_ID: StringName = &"gold" -#const GEMS_CURRENCY_ID: StringName = &"gems" -#const LEGACY_CURRENCY_INDEX_TO_ID: Dictionary = { - #0: GOLD_CURRENCY_ID, - #1: GEMS_CURRENCY_ID, -#} - -const CURRENCY_DIRECTORY_PATH: String = "res://idles/currencies" -const CURRENCY_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"] - -var _currency_by_id: Dictionary = {} -var _catalog_initialized: bool = false - -func _ready() -> void: - _initialize_currency_catalog() - -func get_known_currencies() -> Array[Currency]: - _initialize_currency_catalog_if_needed() - - var result: Array[Currency] = [] - for raw_currency in _currency_by_id.values(): - var currency: Currency = raw_currency as Currency - if currency != null: - result.append(currency) - return result - -func get_known_currency_ids() -> Array[StringName]: - _initialize_currency_catalog_if_needed() - - var ids: Array[StringName] = [] - for raw_currency_id in _currency_by_id.keys(): - var currency_id: StringName = _normalize_currency_id(StringName(String(raw_currency_id))) - if currency_id == &"": - continue - ids.append(currency_id) - return ids - -func get_currency_id(currency: Currency) -> StringName: - if currency == null: - return &"" - - var raw_id: Variant = currency.id - if raw_id is StringName: - return _normalize_currency_id(raw_id) - return _normalize_currency_id(StringName(String(raw_id))) - -func parse_currency_id(raw_currency: Variant) -> StringName: - #if raw_currency is int: - #return currency_id_from_legacy_index(raw_currency) - - var currency_text: String = String(raw_currency).to_lower().strip_edges() - if currency_text == "gem": - currency_text = "gems" - return _normalize_currency_id(StringName(currency_text)) - -#func currency_id_from_legacy_index(currency_index: int) -> StringName: - #return LEGACY_CURRENCY_INDEX_TO_ID.get(currency_index, &"") - -func is_known_currency_id(currency_id: StringName) -> bool: - _initialize_currency_catalog_if_needed() - - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return false - return _currency_by_id.has(normalized_currency_id) - -func get_currency_resource(currency_id: StringName) -> Currency: - _initialize_currency_catalog_if_needed() - - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return null - - var raw_currency: Variant = _currency_by_id.get(normalized_currency_id) - if raw_currency is Currency: - return raw_currency - return null - -func get_currency_name(currency_id: StringName) -> String: - var currency: Currency = get_currency_resource(currency_id) - if currency != null: - var display_name: String = String(currency.display_name).strip_edges() - if not display_name.is_empty(): - return display_name - return _humanize_currency_id(currency_id) - -func get_currency_icon(currency_id: StringName) -> Texture2D: - var currency: Resource = get_currency_resource(currency_id) - if currency == null: - return null - - var raw_icon: Variant = currency.icon - if raw_icon is Texture2D: - return raw_icon - return null - -func _initialize_currency_catalog_if_needed() -> void: - if _catalog_initialized: - return - _initialize_currency_catalog() - -func _initialize_currency_catalog() -> void: - _currency_by_id.clear() - _catalog_initialized = true - - var currency_resource_paths: PackedStringArray = _discover_currency_resource_paths() - for resource_path in currency_resource_paths: - var loaded_resource: Resource = load(resource_path) - if loaded_resource == null: - push_warning("Failed to load currency resource at '%s'." % resource_path) - continue - if not (loaded_resource is Currency): - continue - - var currency_id: StringName = get_currency_id(loaded_resource) - if currency_id == &"": - push_warning("Skipping currency with missing id: %s" % resource_path) - continue - if _currency_by_id.has(currency_id): - push_warning("Duplicate currency id '%s' found at %s. Keeping first occurrence." % [String(currency_id), resource_path]) - continue - - _currency_by_id[currency_id] = loaded_resource - - if _currency_by_id.is_empty(): - push_warning("No currency resources discovered under %s. Ensure currency .tres files exist and are included in export presets." % CURRENCY_DIRECTORY_PATH) - -func _discover_currency_resource_paths() -> PackedStringArray: - var result: PackedStringArray = [] - var directory: DirAccess = DirAccess.open(CURRENCY_DIRECTORY_PATH) - if directory == null: - push_warning("Unable to open currency directory: %s" % CURRENCY_DIRECTORY_PATH) - return result - - directory.list_dir_begin() - var entry_name: String = directory.get_next() - while not entry_name.is_empty(): - if directory.current_is_dir() or entry_name.begins_with("."): - entry_name = directory.get_next() - continue - - var extension: String = entry_name.get_extension().to_lower() - if CURRENCY_RESOURCE_EXTENSIONS.has(extension): - result.append("%s/%s" % [CURRENCY_DIRECTORY_PATH, entry_name]) - - entry_name = directory.get_next() - directory.list_dir_end() - - result.sort() - return result - -func _normalize_currency_id(currency_id: StringName) -> StringName: - var normalized: String = String(currency_id).to_lower().strip_edges() - if normalized.is_empty(): - return &"" - return StringName(normalized) - -func _humanize_currency_id(currency_id: StringName) -> String: - var normalized: String = String(_normalize_currency_id(currency_id)) - if normalized.is_empty(): - return "Unknown" - return normalized.replace("_", " ").capitalize() diff --git a/core/currency_database.gd.uid b/core/currency_database.gd.uid deleted file mode 100644 index 27c1cd2..0000000 --- a/core/currency_database.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dmv83fnq26xao diff --git a/core/edge_scroll_camera.gd b/core/edge_scroll_camera.gd new file mode 100644 index 0000000..00ad1fb --- /dev/null +++ b/core/edge_scroll_camera.gd @@ -0,0 +1,118 @@ +## Scrolls the camera when the mouse cursor reaches the screen edge. +## Attach to a Camera2D node. Requires an OrthographicCamera2D or ViewportContainer +## to determine screen bounds at runtime. +@tool +extends Camera2D + +## Speed at which the camera scrolls (pixels per second). +@export var scroll_speed: float = 400.0 + +## Distance from the edge (in pixels) that triggers scrolling. +@export var edge_margin: float = 40.0 + +## Whether to clamp the camera position to a specific region. +@export var clamp_enabled: bool = false + +## Minimum bounds when clamping (in world space). +@export var clamp_min: Vector2 = Vector2.ZERO + +## Maximum bounds when clamping (in world space). +@export var clamp_max: Vector2 = Vector2.ZERO + +## Zoom sensitivity per mouse wheel tick. +@export var zoom_sensitivity: float = 0.1 + +## Minimum zoom level. +@export var zoom_min: float = 0.25 + +## Maximum zoom level. +@export var zoom_max: float = 3.0 + +var _screen_rect: Rect2 + + +func _ready() -> void: + _update_screen_rect() + var viewport: Viewport = get_viewport() + if viewport != null: + viewport.size_changed.connect(_update_screen_rect) + + +func _process(delta: float) -> void: + if Engine.is_editor_hint(): + queue_redraw() + return + + if is_zero_approx(scroll_speed): + return + + var mouse_pos: Vector2 = get_viewport().get_mouse_position() + var scroll_dir: Vector2 = _get_scroll_direction(mouse_pos) + + if scroll_dir != Vector2.ZERO: + position += scroll_dir * scroll_speed * delta + + if clamp_enabled: + _clamp_position() + + +func _get_scroll_direction(mouse_pos: Vector2) -> Vector2: + var direction: Vector2 = Vector2.ZERO + + if mouse_pos.x < _screen_rect.position.x + edge_margin: + direction.x -= 1.0 + elif mouse_pos.x > _screen_rect.position.x + _screen_rect.size.x - edge_margin: + direction.x += 1.0 + + if mouse_pos.y < _screen_rect.position.y + edge_margin: + direction.y -= 1.0 + elif mouse_pos.y > _screen_rect.position.y + _screen_rect.size.y - edge_margin: + direction.y += 1.0 + + return direction.normalized() + + +func _draw() -> void: + if not Engine.is_editor_hint(): + return + if not clamp_enabled: + return + + var rect_size: Vector2 = clamp_max - clamp_min + draw_rect(Rect2(clamp_min, rect_size), Color(1, 0.4, 0, 0.35)) + draw_rect(Rect2(clamp_min, rect_size), Color(1, 0.6, 0.2, 0.8), false, 2.0) + + +func _clamp_position() -> void: + position.x = clampf(position.x, clamp_min.x, clamp_max.x) + position.y = clampf(position.y, clamp_min.y, clamp_max.y) + + +func _unhandled_input(event: InputEvent) -> void: + if Engine.is_editor_hint(): + return + + if event is InputEventMouseButton: + var mouse_event: InputEventMouseButton = event + if mouse_event.button_index == MOUSE_BUTTON_WHEEL_UP: + _zoom(zoom_sensitivity, mouse_event.position) + elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + _zoom(-zoom_sensitivity, mouse_event.position) + + +func _zoom(change: float, mouse_pos: Vector2) -> void: + var new_zoom_value: float = clampf(zoom.x + change, zoom_min, zoom_max) + if is_equal_approx(new_zoom_value, zoom.x): + return + + var viewport_center: Vector2 = get_viewport_rect().size / 2.0 + var offset: Vector2 = mouse_pos - viewport_center + var world_point: Vector2 = position + offset / zoom + zoom = Vector2(new_zoom_value, new_zoom_value) + position = world_point - offset / zoom + + +func _update_screen_rect() -> void: + var viewport: Viewport = get_viewport() + if viewport != null: + _screen_rect = viewport.get_visible_rect() diff --git a/core/edge_scroll_camera.gd.uid b/core/edge_scroll_camera.gd.uid new file mode 100644 index 0000000..08087d9 --- /dev/null +++ b/core/edge_scroll_camera.gd.uid @@ -0,0 +1 @@ +uid://b6systw05frh0 diff --git a/core/game_state.gd b/core/game_state.gd deleted file mode 100644 index d4bf03d..0000000 --- a/core/game_state.gd +++ /dev/null @@ -1,810 +0,0 @@ -extends Node -# This script is added as an Autoload named 'GameState' - -# ========================================== -# SIGNALS -# ========================================== -signal currency_changed(currency_id: StringName, new_amount: BigNumber) -signal generator_state_changed(generator_id: StringName, state: Dictionary) -signal generator_buff_level_changed(generator_id: StringName, buff_id: StringName, new_level: int) -signal generator_buff_unlocked_changed(generator_id: StringName, buff_id: StringName, unlocked: bool) - -# ========================================== -# STATE VARIABLES -# ========================================== -var _current_currency: Dictionary = {} -var _total_currency_acquired: Dictionary = {} -var _all_time_currency_acquired: Dictionary = {} - -var generator_states: Dictionary = {} -var generator_buff_levels: Dictionary = {} -var generator_buff_unlocked: Dictionary = {} -var _external_save_sections: Dictionary = {} - -var last_save_time: int = 0 # Unix timestamp - -# ========================================== -# Save / Load -# ========================================== -static var file_name: String = "user://idle_save.json" -const GENERATOR_OWNED_KEY: String = "owned" -const GENERATOR_PURCHASED_COUNT_KEY: String = "purchased_count" -const GENERATOR_UNLOCKED_KEY: String = "unlocked" -const GENERATOR_AVAILABLE_KEY: String = "available" -const GENERATOR_STATES_KEY: String = "generator_states" -const GENERATOR_BUFF_LEVELS_KEY: String = "generator_buff_levels" -const GENERATOR_BUFF_UNLOCKED_KEY: String = "generator_buff_unlocked" -const CURRENCIES_KEY: String = "currencies" -const CURRENT_KEY: String = "current" -const TOTAL_KEY: String = "total" -const ALL_TIME_KEY: String = "all_time" -const LAST_SAVE_TIME_KEY: String = "last_save_time" - -func _ready() -> void: - _initialize_currency_maps() - load_game() - -# ========================================== -# STATE MODIFIERS -# ========================================== -#func add_gold(amount: BigNumber) -> void: - #add_currency_by_id(GOLD_CURRENCY_ID, amount) -# -#func spend_gold(cost: BigNumber) -> bool: - #return spend_currency_by_id(GOLD_CURRENCY_ID, cost) -# -#func add_gems(amount: BigNumber) -> void: - #add_currency_by_id(GEMS_CURRENCY_ID, amount) -# -#func spend_gems(cost: BigNumber) -> bool: - #return spend_currency_by_id(GEMS_CURRENCY_ID, cost) - -func get_known_currencies() -> Array[Currency]: - return CurrencyDatabase.get_known_currencies() - -func get_currency_id(currency: Resource) -> StringName: - return CurrencyDatabase.get_currency_id(currency) - -func parse_currency_id(raw_currency: Variant) -> StringName: - return CurrencyDatabase.parse_currency_id(raw_currency) - -func is_known_currency_id(currency_id: StringName) -> bool: - return CurrencyDatabase.is_known_currency_id(currency_id) - -func get_currency_resource(currency_id: StringName) -> Resource: - return CurrencyDatabase.get_currency_resource(currency_id) - -func get_currency_name(currency_id: StringName) -> String: - return CurrencyDatabase.get_currency_name(currency_id) - -func get_currency_icon(currency_id: StringName) -> Texture2D: - return CurrencyDatabase.get_currency_icon(currency_id) - -func add_currency(currency: Resource, amount: BigNumber) -> void: - var currency_id: StringName = get_currency_id(currency) - if currency_id == &"": - push_warning("Attempted to add currency with an invalid Currency resource.") - return - - add_currency_by_id(currency_id, amount) - -func spend_currency(currency: Resource, cost: BigNumber) -> bool: - var currency_id: StringName = get_currency_id(currency) - if currency_id == &"": - push_warning("Attempted to spend currency with an invalid Currency resource.") - return false - - return spend_currency_by_id(currency_id, cost) - -func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return - - if amount.mantissa > 0.0: - _get_total_currency_ref(normalized_currency_id).add_in_place(amount) - _get_all_time_currency_ref(normalized_currency_id).add_in_place(amount) - - var current: BigNumber = _get_current_currency_ref(normalized_currency_id) - current.add_in_place(amount) - currency_changed.emit(normalized_currency_id, current) - -func spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return false - - var current: BigNumber = _get_current_currency_ref(normalized_currency_id) - if current.is_greater_than(cost) or current.is_equal_to(cost): - var negative_cost: BigNumber = BigNumber.new(-cost.mantissa, cost.exponent) - current.add_in_place(negative_cost) - currency_changed.emit(normalized_currency_id, current) - return true - return false - -func get_currency_amount(currency: Resource) -> BigNumber: - return get_currency_amount_by_id(get_currency_id(currency)) - -func get_currency_amount_by_id(currency_id: StringName) -> BigNumber: - return _get_current_currency_ref(currency_id) - -func get_total_currency_acquired(currency: Resource) -> BigNumber: - return get_total_currency_acquired_by_id(get_currency_id(currency)) - -func get_total_currency_acquired_by_id(currency_id: StringName) -> BigNumber: - return _get_total_currency_ref(currency_id) - -func get_all_time_currency_acquired(currency: Resource) -> BigNumber: - return get_all_time_currency_acquired_by_id(get_currency_id(currency)) - -func get_all_time_currency_acquired_by_id(currency_id: StringName) -> BigNumber: - return _get_all_time_currency_ref(currency_id) - -func register_generator(generator_id: StringName, unlocked: bool = false, available: bool = false, owned: int = 0) -> void: - var key: String = String(generator_id) - if key.is_empty(): - return - var default_owned: int = _to_non_negative_int(owned, 0) - - if generator_states.has(key): - var existing_raw: Variant = generator_states.get(key) - if existing_raw is Dictionary: - generator_states[key] = _sanitize_generator_state(existing_raw, unlocked, available, default_owned) - return - - var new_state: Dictionary = _default_generator_state(unlocked, available, default_owned) - generator_states[key] = new_state - generator_state_changed.emit(StringName(key), new_state.duplicate(true)) - -func get_generator_state(generator_id: StringName) -> Dictionary: - return _get_generator_state(generator_id).duplicate(true) - -func get_generator_owned(generator_id: StringName) -> int: - var state: Dictionary = _get_generator_state(generator_id) - return int(state.get(GENERATOR_OWNED_KEY, 0)) - -func set_generator_owned(generator_id: StringName, value: int) -> void: - var state: Dictionary = _get_generator_state(generator_id) - var next_owned: int = maxi(value, 0) - if int(state.get(GENERATOR_OWNED_KEY, 0)) == next_owned: - return - - state[GENERATOR_OWNED_KEY] = next_owned - state[GENERATOR_PURCHASED_COUNT_KEY] = maxi(int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)), next_owned) - _set_generator_state(generator_id, state) - -func get_generator_purchased_count(generator_id: StringName) -> int: - var state: Dictionary = _get_generator_state(generator_id) - return int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)) - -func set_generator_purchased_count(generator_id: StringName, value: int) -> void: - var state: Dictionary = _get_generator_state(generator_id) - var min_purchased: int = int(state.get(GENERATOR_OWNED_KEY, 0)) - var next_purchased: int = maxi(value, min_purchased) - if int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)) == next_purchased: - return - - state[GENERATOR_PURCHASED_COUNT_KEY] = next_purchased - _set_generator_state(generator_id, state) - -func is_generator_unlocked(generator_id: StringName) -> bool: - var state: Dictionary = _get_generator_state(generator_id) - return bool(state.get(GENERATOR_UNLOCKED_KEY, false)) - -func set_generator_unlocked(generator_id: StringName, value: bool) -> void: - var state: Dictionary = _get_generator_state(generator_id) - if bool(state.get(GENERATOR_UNLOCKED_KEY, false)) == value: - return - - state[GENERATOR_UNLOCKED_KEY] = value - _set_generator_state(generator_id, state) - -func is_generator_available(generator_id: StringName) -> bool: - var state: Dictionary = _get_generator_state(generator_id) - return bool(state.get(GENERATOR_AVAILABLE_KEY, false)) - -func set_generator_available(generator_id: StringName, value: bool) -> void: - var state: Dictionary = _get_generator_state(generator_id) - if bool(state.get(GENERATOR_AVAILABLE_KEY, false)) == value: - return - - state[GENERATOR_AVAILABLE_KEY] = value - _set_generator_state(generator_id, state) - -func register_generator_buff(generator_id: StringName, buff_id: StringName, initial_level: int = 0) -> void: - var generator_key: String = String(generator_id) - var buff_key: String = String(buff_id) - if generator_key.is_empty() or buff_key.is_empty(): - return - - var levels: Dictionary = _get_generator_buff_levels_ref(generator_id) - if levels.has(buff_key): - var existing_level: int = _to_non_negative_int(levels.get(buff_key, initial_level), initial_level) - if int(levels.get(buff_key, initial_level)) != existing_level: - levels[buff_key] = existing_level - generator_buff_levels[generator_key] = levels - generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), existing_level) - return - - var sanitized_level: int = _to_non_negative_int(initial_level, 0) - levels[buff_key] = sanitized_level - generator_buff_levels[generator_key] = levels - generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), sanitized_level) - -func register_generator_buff_unlocked(generator_id: StringName, buff_id: StringName, initially_unlocked: bool = false) -> void: - var generator_key: String = String(generator_id) - var buff_key: String = String(buff_id) - if generator_key.is_empty() or buff_key.is_empty(): - return - - var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id) - if unlocked_map.has(buff_key): - var existing_unlocked: bool = _to_bool(unlocked_map.get(buff_key, initially_unlocked), initially_unlocked) - if bool(unlocked_map.get(buff_key, initially_unlocked)) != existing_unlocked: - unlocked_map[buff_key] = existing_unlocked - generator_buff_unlocked[generator_key] = unlocked_map - generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), existing_unlocked) - return - - var sanitized_unlocked: bool = _to_bool(initially_unlocked, false) - unlocked_map[buff_key] = sanitized_unlocked - generator_buff_unlocked[generator_key] = unlocked_map - generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), sanitized_unlocked) - -func get_generator_buff_level(generator_id: StringName, buff_id: StringName) -> int: - var buff_key: String = String(buff_id) - if buff_key.is_empty(): - return 0 - - var levels: Dictionary = _get_generator_buff_levels_ref(generator_id) - return _to_non_negative_int(levels.get(buff_key, 0), 0) - -func set_generator_buff_level(generator_id: StringName, buff_id: StringName, value: int) -> void: - var generator_key: String = String(generator_id) - var buff_key: String = String(buff_id) - if generator_key.is_empty() or buff_key.is_empty(): - return - - var levels: Dictionary = _get_generator_buff_levels_ref(generator_id) - var next_level: int = _to_non_negative_int(value, 0) - if int(levels.get(buff_key, -1)) == next_level: - return - - levels[buff_key] = next_level - generator_buff_levels[generator_key] = levels - generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), next_level) - -func is_generator_buff_unlocked(generator_id: StringName, buff_id: StringName) -> bool: - var buff_key: String = String(buff_id) - if buff_key.is_empty(): - return false - - var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id) - return _to_bool(unlocked_map.get(buff_key, false), false) - -func set_generator_buff_unlocked(generator_id: StringName, buff_id: StringName, value: bool) -> void: - var generator_key: String = String(generator_id) - var buff_key: String = String(buff_id) - if generator_key.is_empty() or buff_key.is_empty(): - return - - var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id) - var next_unlocked: bool = _to_bool(value, false) - if _to_bool(unlocked_map.get(buff_key, false), false) == next_unlocked: - return - - unlocked_map[buff_key] = next_unlocked - generator_buff_unlocked[generator_key] = unlocked_map - generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), next_unlocked) - -func get_generator_buff_unlocked(generator_id: StringName) -> Dictionary: - return _get_generator_buff_unlocked_ref(generator_id).duplicate(true) - -func get_generator_buff_levels(generator_id: StringName) -> Dictionary: - return _get_generator_buff_levels_ref(generator_id).duplicate(true) - -func set_external_save_data(section_key: StringName, payload: Dictionary) -> void: - var key: String = String(section_key).strip_edges() - if key.is_empty(): - return - - _external_save_sections[key] = payload.duplicate(true) - -func get_external_save_data(section_key: StringName) -> Dictionary: - var key: String = String(section_key).strip_edges() - if key.is_empty(): - return {} - - var raw_payload: Variant = _external_save_sections.get(key, {}) - if raw_payload is Dictionary: - return raw_payload.duplicate(true) - - return {} - -func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signals: bool = true) -> void: - var currency_ids: Array[StringName] = _collect_currency_ids_for_save() - for currency_id in currency_ids: - _set_current_currency(currency_id, BigNumber.from_float(0.0)) - if reset_total_currency: - _set_total_currency(currency_id, BigNumber.from_float(0.0)) - - generator_states.clear() - generator_buff_levels.clear() - generator_buff_unlocked.clear() - - if not emit_currency_signals: - return - - for currency_id in currency_ids: - currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) - -func emit_currency_changed_for_all() -> void: - for currency_id in _collect_currency_ids_for_save(): - currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) - -# ========================================== -# PERSISTENCE (Save/Load) -# ========================================== -func save_game() -> void: - var save_data = { - CURRENCIES_KEY: _serialize_currency_balances(), - GENERATOR_STATES_KEY: _serialize_generator_states(), - GENERATOR_BUFF_LEVELS_KEY: _serialize_generator_buff_levels(), - GENERATOR_BUFF_UNLOCKED_KEY: _serialize_generator_buff_unlocked(), - LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system() - } - - for section_key in _external_save_sections.keys(): - var key: String = String(section_key) - if key.is_empty(): - continue - - var payload: Variant = _external_save_sections.get(section_key) - if not (payload is Dictionary): - continue - - save_data[key] = payload - - var file: FileAccess = FileAccess.open(file_name, FileAccess.WRITE) - if file: - file.store_string(JSON.stringify(save_data)) - -func load_game() -> void: - if not FileAccess.file_exists(file_name): - return - - var file: FileAccess = FileAccess.open(file_name, FileAccess.READ) - if file: - var parsed: Variant = JSON.parse_string(file.get_as_text()) - if parsed is Dictionary: - var parsed_data: Dictionary = parsed - _external_save_sections.clear() - if parsed_data.has(CURRENCIES_KEY): - _load_currency_balances(parsed_data.get(CURRENCIES_KEY, {})) - - generator_states = _deserialize_generator_states(parsed_data.get(GENERATOR_STATES_KEY, {})) - generator_buff_levels = _deserialize_generator_buff_levels(parsed_data.get(GENERATOR_BUFF_LEVELS_KEY, {})) - generator_buff_unlocked = _deserialize_generator_buff_unlocked(parsed_data.get(GENERATOR_BUFF_UNLOCKED_KEY, {})) - last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system()) - for save_key_variant in parsed_data.keys(): - var save_key: String = String(save_key_variant) - if save_key.is_empty(): - continue - if save_key in [CURRENCIES_KEY, GENERATOR_STATES_KEY, GENERATOR_BUFF_LEVELS_KEY, GENERATOR_BUFF_UNLOCKED_KEY, LAST_SAVE_TIME_KEY]: - continue - - var section_payload: Variant = parsed_data.get(save_key_variant) - if section_payload is Dictionary: - _external_save_sections[save_key] = section_payload.duplicate(true) - -func _load_currency_balances(raw_currencies: Variant) -> void: - if not (raw_currencies is Dictionary): - return - - var currencies: Dictionary = raw_currencies - for raw_currency_id in currencies.keys(): - var currency_id: StringName = _normalize_currency_id(StringName(String(raw_currency_id))) - if currency_id == &"": - continue - - var raw_entry: Variant = currencies.get(raw_currency_id) - if not (raw_entry is Dictionary): - continue - - var entry: Dictionary = raw_entry - var current_amount: BigNumber = BigNumber.deserialize(entry.get(CURRENT_KEY, {})) - var total_amount: BigNumber = _deserialize_total_currency(entry.get(TOTAL_KEY, null), current_amount) - var all_time_amount: BigNumber = _deserialize_all_time_currency(entry.get(ALL_TIME_KEY, null), total_amount) - _set_current_currency(currency_id, current_amount) - _set_total_currency(currency_id, total_amount) - _set_all_time_currency(currency_id, all_time_amount) - -func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) -> BigNumber: - var minimum_total: BigNumber = _copy_big_number(current_amount) - if minimum_total.mantissa < 0.0: - minimum_total = BigNumber.from_float(0.0) - - if not (raw_total is Dictionary): - return minimum_total - - var parsed_total: BigNumber = BigNumber.deserialize(raw_total) - if parsed_total.mantissa < 0.0: - return minimum_total - if parsed_total.is_less_than(minimum_total): - return minimum_total - return parsed_total - -func _deserialize_all_time_currency(raw_all_time: Variant, total_amount: BigNumber) -> BigNumber: - var minimum_all_time: BigNumber = _copy_big_number(total_amount) - if minimum_all_time.mantissa < 0.0: - minimum_all_time = BigNumber.from_float(0.0) - - if not (raw_all_time is Dictionary): - return minimum_all_time - - var parsed_all_time: BigNumber = BigNumber.deserialize(raw_all_time) - if parsed_all_time.mantissa < 0.0: - return minimum_all_time - if parsed_all_time.is_less_than(minimum_all_time): - return minimum_all_time - return parsed_all_time - -func _copy_big_number(value: BigNumber) -> BigNumber: - return BigNumber.new(value.mantissa, value.exponent) - -func _serialize_currency_balances() -> Dictionary: - var result: Dictionary = {} - - for currency_id in _collect_currency_ids_for_save(): - var key: String = String(currency_id) - if key.is_empty(): - continue - - result[key] = { - CURRENT_KEY: _get_current_currency_ref(currency_id).serialize(), - TOTAL_KEY: _get_total_currency_ref(currency_id).serialize(), - ALL_TIME_KEY: _get_all_time_currency_ref(currency_id).serialize(), - } - - return result - -func _collect_currency_ids_for_save() -> Array[StringName]: - var ids: Array[StringName] = [] - - for raw_id in _current_currency.keys(): - var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id))) - if normalized_currency_id == &"" or ids.has(normalized_currency_id): - continue - ids.append(normalized_currency_id) - - for raw_id in _all_time_currency_acquired.keys(): - var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id))) - if normalized_currency_id == &"" or ids.has(normalized_currency_id): - continue - ids.append(normalized_currency_id) - - for known_currency_id in CurrencyDatabase.get_known_currency_ids(): - var normalized_currency_id: StringName = _normalize_currency_id(known_currency_id) - if normalized_currency_id == &"" or ids.has(normalized_currency_id): - continue - ids.append(normalized_currency_id) - - return ids - -func _initialize_currency_maps() -> void: - for currency_id in CurrencyDatabase.get_known_currency_ids(): - if currency_id == &"": - continue - _set_current_currency(currency_id, BigNumber.from_float(0.0)) - _set_total_currency(currency_id, BigNumber.from_float(0.0)) - _set_all_time_currency(currency_id, BigNumber.from_float(0.0)) - -func _normalize_currency_id(currency_id: StringName) -> StringName: - var normalized: String = String(currency_id).to_lower().strip_edges() - if normalized.is_empty(): - return &"" - return StringName(normalized) - -func _get_current_currency_ref(currency_id: StringName) -> BigNumber: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return BigNumber.from_float(0.0) - - var value: Variant = _current_currency.get(normalized_currency_id) - if value is BigNumber: - return value - - var fallback: BigNumber = BigNumber.from_float(0.0) - _current_currency[normalized_currency_id] = fallback - return fallback - -func _get_total_currency_ref(currency_id: StringName) -> BigNumber: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return BigNumber.from_float(0.0) - - var value: Variant = _total_currency_acquired.get(normalized_currency_id) - if value is BigNumber: - return value - - var fallback: BigNumber = BigNumber.from_float(0.0) - _total_currency_acquired[normalized_currency_id] = fallback - return fallback - -func _get_all_time_currency_ref(currency_id: StringName) -> BigNumber: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return BigNumber.from_float(0.0) - - var value: Variant = _all_time_currency_acquired.get(normalized_currency_id) - if value is BigNumber: - return value - - var fallback: BigNumber = BigNumber.from_float(0.0) - _all_time_currency_acquired[normalized_currency_id] = fallback - return fallback - -func _set_current_currency(currency_id: StringName, value: BigNumber) -> void: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return - _current_currency[normalized_currency_id] = _copy_big_number(value) - -func _set_total_currency(currency_id: StringName, value: BigNumber) -> void: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return - _total_currency_acquired[normalized_currency_id] = _copy_big_number(value) - -func _set_all_time_currency(currency_id: StringName, value: BigNumber) -> void: - var normalized_currency_id: StringName = _normalize_currency_id(currency_id) - if normalized_currency_id == &"": - return - _all_time_currency_acquired[normalized_currency_id] = _copy_big_number(value) - -func _default_generator_state(unlocked: bool, available: bool, owned: int = 0) -> Dictionary: - var owned_value: int = _to_non_negative_int(owned, 0) - return { - GENERATOR_OWNED_KEY: owned_value, - GENERATOR_PURCHASED_COUNT_KEY: owned_value, - GENERATOR_UNLOCKED_KEY: unlocked, - GENERATOR_AVAILABLE_KEY: available, - } - -func _get_generator_state(generator_id: StringName) -> Dictionary: - var key: String = String(generator_id) - if key.is_empty(): - return _default_generator_state(false, false, 0) - - if not generator_states.has(key): - register_generator(generator_id) - - var existing_raw: Variant = generator_states.get(key, _default_generator_state(false, false, 0)) - if existing_raw is Dictionary: - var state: Dictionary = existing_raw - var sanitized: Dictionary = _sanitize_generator_state(state, false, false, 0) - generator_states[key] = sanitized - return sanitized - - var fallback: Dictionary = _default_generator_state(false, false, 0) - generator_states[key] = fallback - return fallback - -func _set_generator_state(generator_id: StringName, state: Dictionary) -> void: - var key: String = String(generator_id) - if key.is_empty(): - return - - var sanitized: Dictionary = _sanitize_generator_state(state, false, false, 0) - generator_states[key] = sanitized - generator_state_changed.emit(StringName(key), sanitized.duplicate(true)) - -func _get_generator_buff_levels_ref(generator_id: StringName) -> Dictionary: - var key: String = String(generator_id) - if key.is_empty(): - return {} - - var existing_raw: Variant = generator_buff_levels.get(key, {}) - if existing_raw is Dictionary: - var sanitized: Dictionary = _sanitize_generator_buff_levels(existing_raw) - generator_buff_levels[key] = sanitized - return sanitized - - var fallback: Dictionary = {} - generator_buff_levels[key] = fallback - return fallback - -func _get_generator_buff_unlocked_ref(generator_id: StringName) -> Dictionary: - var key: String = String(generator_id) - if key.is_empty(): - return {} - - var existing_raw: Variant = generator_buff_unlocked.get(key, {}) - if existing_raw is Dictionary: - var sanitized: Dictionary = _sanitize_generator_buff_unlocked(existing_raw) - generator_buff_unlocked[key] = sanitized - return sanitized - - var fallback: Dictionary = {} - generator_buff_unlocked[key] = fallback - return fallback - -func _sanitize_generator_buff_levels(raw_levels: Dictionary) -> Dictionary: - var sanitized_levels: Dictionary = {} - for buff_key_variant in raw_levels.keys(): - var buff_key: String = String(buff_key_variant) - if buff_key.is_empty(): - continue - - sanitized_levels[buff_key] = _to_non_negative_int(raw_levels.get(buff_key_variant, 0), 0) - - return sanitized_levels - -func _sanitize_generator_buff_unlocked(raw_unlocked: Dictionary) -> Dictionary: - var sanitized_unlocked: Dictionary = {} - for buff_key_variant in raw_unlocked.keys(): - var buff_key: String = String(buff_key_variant) - if buff_key.is_empty(): - continue - - # Save format only stores true values, but keep parser tolerant. - var is_unlocked: bool = _to_bool(raw_unlocked.get(buff_key_variant, false), false) - if not is_unlocked: - continue - - sanitized_unlocked[buff_key] = true - - return sanitized_unlocked - -func _serialize_generator_states() -> Dictionary: - var result: Dictionary = {} - for key_variant in generator_states.keys(): - var key: String = String(key_variant) - if key.is_empty(): - continue - - var value: Variant = generator_states.get(key_variant) - if value is Dictionary: - result[key] = _serialize_generator_state_entry(value) - return result - -func _deserialize_generator_states(raw_value: Variant) -> Dictionary: - var result: Dictionary = {} - if not (raw_value is Dictionary): - return result - - for key_variant in raw_value.keys(): - var key: String = String(key_variant) - if key.is_empty(): - continue - - var entry: Variant = raw_value.get(key_variant) - if entry is Dictionary: - result[key] = _deserialize_generator_state_entry(entry) - return result - -func _serialize_generator_buff_levels() -> Dictionary: - var result: Dictionary = {} - for generator_key_variant in generator_buff_levels.keys(): - var generator_key: String = String(generator_key_variant) - if generator_key.is_empty(): - continue - - var raw_levels: Variant = generator_buff_levels.get(generator_key_variant) - if raw_levels is Dictionary: - result[generator_key] = _sanitize_generator_buff_levels(raw_levels) - - return result - -func _deserialize_generator_buff_levels(raw_value: Variant) -> Dictionary: - var result: Dictionary = {} - if not (raw_value is Dictionary): - return result - - for generator_key_variant in raw_value.keys(): - var generator_key: String = String(generator_key_variant) - if generator_key.is_empty(): - continue - - var raw_levels: Variant = raw_value.get(generator_key_variant) - if raw_levels is Dictionary: - result[generator_key] = _sanitize_generator_buff_levels(raw_levels) - - return result - -func _serialize_generator_buff_unlocked() -> Dictionary: - var result: Dictionary = {} - for generator_key_variant in generator_buff_unlocked.keys(): - var generator_key: String = String(generator_key_variant) - if generator_key.is_empty(): - continue - - var raw_unlocked: Variant = generator_buff_unlocked.get(generator_key_variant) - if not (raw_unlocked is Dictionary): - continue - - var sanitized_unlocked: Dictionary = _sanitize_generator_buff_unlocked(raw_unlocked) - if sanitized_unlocked.is_empty(): - continue - - result[generator_key] = sanitized_unlocked - - return result - -func _deserialize_generator_buff_unlocked(raw_value: Variant) -> Dictionary: - var result: Dictionary = {} - if not (raw_value is Dictionary): - return result - - for generator_key_variant in raw_value.keys(): - var generator_key: String = String(generator_key_variant) - if generator_key.is_empty(): - continue - - var raw_unlocked: Variant = raw_value.get(generator_key_variant) - if not (raw_unlocked is Dictionary): - continue - - var sanitized_unlocked: Dictionary = _sanitize_generator_buff_unlocked(raw_unlocked) - if sanitized_unlocked.is_empty(): - continue - - result[generator_key] = sanitized_unlocked - - return result - -func _serialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: - var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0) - var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) - - var serialized_state: Dictionary = { - GENERATOR_OWNED_KEY: owned_value, - GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), - } - - if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool: - serialized_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY) - if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool: - serialized_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY) - - return serialized_state - -func _deserialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: - var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0) - var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) - - var parsed_state: Dictionary = { - GENERATOR_OWNED_KEY: owned_value, - GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), - } - - if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool: - parsed_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY) - if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool: - parsed_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY) - - return parsed_state - -func _sanitize_generator_state(raw_state: Dictionary, default_unlocked: bool, default_available: bool, default_owned: int = 0) -> Dictionary: - var normalized_default_owned: int = _to_non_negative_int(default_owned, 0) - var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, normalized_default_owned), normalized_default_owned) - var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) - - return { - GENERATOR_OWNED_KEY: owned_value, - GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), - GENERATOR_UNLOCKED_KEY: _to_bool(raw_state.get(GENERATOR_UNLOCKED_KEY, default_unlocked), default_unlocked), - GENERATOR_AVAILABLE_KEY: _to_bool(raw_state.get(GENERATOR_AVAILABLE_KEY, default_available), default_available), - } - -func _to_non_negative_int(value: Variant, fallback: int = 0) -> int: - if value is int: - return maxi(value, 0) - if value is float: - return maxi(int(value), 0) - return maxi(fallback, 0) - -func _to_bool(value: Variant, fallback: bool) -> bool: - if value is bool: - return value - return fallback diff --git a/core/game_state.gd.uid b/core/game_state.gd.uid deleted file mode 100644 index fdee5b0..0000000 --- a/core/game_state.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d2j7tvlgxr2jp diff --git a/core/generator-unlock-goals/TECH_SPEC.md b/core/generator-unlock-goals/TECH_SPEC.md deleted file mode 100644 index ee8d4d8..0000000 --- a/core/generator-unlock-goals/TECH_SPEC.md +++ /dev/null @@ -1,58 +0,0 @@ -# Generator-Embedded Unlock Goals - Technical Specification - -## Document Status -- Version: 2.0 -- Date: 2026-03-21 -- Scope: Unlock generators from `CurrencyGeneratorData.unlock_goal` when goal thresholds are reached. - -## Problem Statement -Generator unlock progression should be authored where generator behavior lives: `CurrencyGeneratorData`. -The previous scene-level target mapping layer increased setup complexity and created mismatch risk between goal targets and scene content. - -## Current Baseline -1. Generator state persists in `GameState.generator_states` (`owned`, `purchased_count`, `unlocked`, `available`). -2. `CurrencyGenerator` already gates interactions using `is_available_to_player()`. -3. Goal primitives remain reusable and data-driven: - - `GoalRequirementData` - - `GoalData` -4. Generator data now owns unlock definition via `unlock_goal: GoalData`. - -## Functional Requirements -1. A generator may define one optional unlock goal (`CurrencyGeneratorData.unlock_goal`). -2. Goal completion uses logical AND across requirements. -3. Requirement checks use total acquired currency (`GameState.get_total_currency_acquired_by_id`). -4. Unlock evaluation runs: - - once on generator `_ready()` - - on every `GameState.currency_changed` -5. When a goal is met, runtime sets: - - `GameState.set_generator_unlocked(generator_id, true)` - - `GameState.set_generator_available(generator_id, true)` -6. Unlock transitions are idempotent. -7. Unlock completion emits `CurrencyGenerator.goal_achieved(generator_id, goal_id)` for hooks/UI. - -## Runtime Design -1. `CurrencyGeneratorData` exposes: - - `has_unlock_goal()` - - `is_unlock_goal_met()` - - `get_unlock_goal_id()` -2. `CurrencyGenerator` executes `_evaluate_generator_unlock_goal()`. -3. No scene-level unlock controller is required. -4. Goals debug UI discovers goals from scene generators instead of external target-mapping resources. - -## Data Authoring Rules -1. Configure locked generators with `starts_unlocked = false` and `starts_available = false`. -2. Set `unlock_goal` directly on that generator `.tres` resource. -3. Keep `GoalData.id` unique and stable. -4. Goal data can still be shared across systems (generator unlocks, buff unlocks). - -## Save/Load Behavior -1. Unlock persistence remains unchanged because `GameState` stores `unlocked/available`. -2. Re-evaluation after load is safe due to idempotent state writes. - -## Validation -1. Run headless parse: `"$GODOT_BIN" --headless --path . --quit` -2. Manual smoke test in `generator_museum`: - - verify locked generator starts unavailable - - grant required currency - - verify generator unlocks automatically - - verify state persists after restart diff --git a/core/generator/README.md b/core/generator/README.md new file mode 100644 index 0000000..10b3409 --- /dev/null +++ b/core/generator/README.md @@ -0,0 +1,225 @@ +# Generator Module Documentation + +## Overview + +The `generator/` subfolder contains the core idle game mechanics: automated resource production with scaling costs, upgrades (buffs), and manual interaction. + +## Files + +| File | Purpose | +|------|---------| +| `currency_generator.gd` | Runtime generator logic (Node2D) | +| `currency_generator_data.gd` | Balancing data resource | +| `generator_buff_data.gd` | Buff/upgrade definitions | +| `generator_container.gd` | UI panel for generator stats | + +## Architecture + +``` +CurrencyGenerator (Node2D) +├── Handles production cycles +├── Manages purchases +├── Processes clicks/hover +└── GeneratorPanel (UI) + ├── Shows stats/costs + └── Buff upgrade buttons +``` + +## CurrencyGeneratorData + +Resource class for generator balancing data. + +### Key Properties + +```gdscript +@export var id: StringName = &"" # Unique identifier +@export var name: String = "" # Display name +@export var starts_unlocked: bool = true # Visible at game start +@export var purchase_currency: Currency # What to spend +@export var unlock_goal: GoalData # Goal to unlock this (behavior defined in GoalData) + +# Cost formula: cost = base * coefficient^owned +@export var initial_cost: float = 10.0 # Base cost +@export var coefficient: float = 1.15 # Growth factor + +# Production +@export var initial_time: float = 1.0 # Seconds per cycle +@export var initial_productivity: float = 1.0 # Resources per cycle + +# Milestones +@export var milestone_step: int = 25 # Every N units +@export var milestone_multiplier: float = 2.0 # x multiplier + +# Manual click +@export var click_mantissa: float = 1.0 +@export var click_exponent: int = 0 +@export var click_cooldown_seconds: float = 0.2 +``` + +### Cost Formula + +**Bulk Buy Cost** (geometric series): +``` +total_cost = base * r^owned * ((r^n - 1) / (r - 1)) +``` +Where: +- `base` = initial_cost +- `r` = coefficient +- `owned` = current count +- `n` = amount to buy + +**Max Buy** (inverse formula): +``` +max = floor(log((currency * (r-1) / (base * r^owned)) + 1) / log(r)) +``` + +### Production Formula + +```gdscript +production_per_cycle = initial_productivity * owned * run_multiplier * milestone_mult * purchased_mult +production_per_second = production_per_cycle / initial_time +``` + +## CurrencyGenerator + +Runtime node instance attached to generator scenes. + +### Signals + +```gdscript +signal purchase_completed(amount, total_owned, total_purchased, total_cost) +signal production_tick(amount, cycle_count, total_owned) +signal buff_purchased(buff_id, new_level, cost, cost_currency_id) +signal goal_achieved(generator_id, goal_id) # Emitted when generator unlocks from goal +``` + +### Key Methods + +```gdscript +# Purchase +func buy(amount: int = 1) -> bool +func buy_max() -> int +func get_cost_for_amount(amount: int) -> BigNumber +func can_buy(amount: int) -> bool + +# Goal-based unlock (automatic when goal completes) +# Generator listens to GameState.goal_completed signal +``` + +# Production +func grant_currency(amount: BigNumber) +func get_effective_auto_run_multiplier() -> float + +# Buffs +func buy_buff(buff_id: StringName) -> bool +func get_buff_level(buff_id: StringName) -> int +func is_buff_unlocked(buff_id: StringName) -> bool +``` + +### Production Cycle + +1. Accumulate `cycle_progress_seconds` in `_process()` +2. When `>= initial_time`, grant production +3. Emit `production_tick` signal +4. Multiply by: + - Owned count + - Run multiplier (export) + - Buff multipliers (auto_production) + - Milestone bonuses + - Purchase boost + +## GeneratorBuffData + +Defines upgradeable modifiers for generators. + +### Buff Kinds + +```gdscript +enum BuffKind { + AUTO_PRODUCTION_MULTIPLIER, # x output per cycle + MANUAL_CLICK_MULTIPLIER, # x click reward + RESOURCE_PURCHASE # Instant currency grant +} +``` + +### Key Properties + +```gdscript +@export var id: StringName = &"" +@export var target_ids: Array[StringName] = [] # Generators affected +@export var kind: BuffKind +@export var max_level: int = -1 # -1 = unlimited +@export var base_effect: float = 1.0 # Starting multiplier +@export var effect_increment: float = 0.1 # +per level + +@export var base_cost_mantissa: float = 10.0 +@export var base_cost_exponent: int = 0 +@export var cost_multiplier: float = 1.5 # Cost growth + +# Effect: base_effect + (effect_increment * level) +func get_effect_multiplier(level: int) -> float +``` + +### Cost Formula + +```gdscript +cost_for_level = base_cost * cost_multiplier^level +``` + +### Resource Purchase Buff + +Special buff type that grants instant currency: +```gdscript +resource_purchase_amount = base_amount * (resource_purchase_increment_multiplier^level) +``` + +## GeneratorPanel + +UI component showing generator information. + +### Displays +- Generator name and owned count +- Next purchase cost +- Production per second +- Buff upgrade buttons with costs + +### Signals Connected +- `purchase_completed` - Refresh UI +- `production_tick` - Update production display +- `generator_buff_level_changed` - Refresh buff rows + +## Goal-Based Unlock + +Generators with `unlock_goal` automatically listen to `LevelGameState.goal_completed` signal: +- When goal completes, generator unlocks automatically +- Goal's `unlock_behavior` (AUTOMATIC/MANUAL) controls when goal completes +- Manual goals require player to click "Unlock" button first + +```gdscript +# In CurrencyGenerator._ready(): +level_game_state.goal_completed.connect(_on_goal_completed) + +func _on_goal_completed(goal_id: StringName) -> void: + if data.has_unlock_goal() and data.get_unlock_goal_id() == goal_id: + unlock_generator() +``` + +## Prestige Integration + +Generators automatically apply prestige multipliers: +```gdscript +effective_multiplier = run_multiplier * buff_multiplier * prestige_multiplier +``` + +On prestige via `LevelGameState.reset_for_prestige()`: +1. Current currencies reset to 0 +2. `cycle_progress_seconds` reset +3. Buff levels reset to 0 +4. Goal completion state cleared and re-initialized +5. Research XP and levels reset to 0 + +## See Also + +- `core/level_game_state.gd` - Generator state storage +- `core/goals/goal_data.gd` - Unlock conditions +- `core/prestige/prestige_manager.gd` - Multiplier application diff --git a/core/generator/currency_generator.gd b/core/generator/currency_generator.gd index 83472c8..d65432b 100644 --- a/core/generator/currency_generator.gd +++ b/core/generator/currency_generator.gd @@ -24,7 +24,7 @@ signal goal_achieved(generator_id: StringName, goal_id: StringName) const HUGE_COST_EXPONENT: int = 1000000 ## Currency target updated by this generator. -@export var currency: Resource +@export var currency: Currency ## Data resource containing balancing values and defaults (required). @export var data: CurrencyGeneratorData ## Enables per-cycle automatic production when true. @@ -37,49 +37,52 @@ const HUGE_COST_EXPONENT: int = 1000000 @export var run_multiplier: float = 1.0 ## Reference to generator UI -@onready var info_generator_container: GeneratorPanel = $GeneratorPanel +@export var info_generator_container: GeneratorPanel ## True while the pointer is inside the generator Area2D. var _mouse_entered: bool = false ## Time left until next click/hover grant is allowed. var _remaining_click_cooldown_seconds: float = 0.0 +## Reference to LevelGameState instance. +@onready var game_state: LevelGameState = find_parent("LevelGameState") + ## Number of currently owned generators. -## Backed by GameState via this generator's resolved id. +## Backed by LevelGameState via this generator's resolved id. var owned: int: get: _ensure_registered() - return GameState.get_generator_owned(_generator_id) + return game_state.get_generator_owned(_generator_id) set(value): _ensure_registered() - GameState.set_generator_owned(_generator_id, value) + game_state.set_generator_owned(_generator_id, value) ## Lifetime purchased generators (does not decrease if ownership drops). var purchased_count: int: get: _ensure_registered() - return GameState.get_generator_purchased_count(_generator_id) + return game_state.get_generator_purchased_count(_generator_id) set(value): _ensure_registered() - GameState.set_generator_purchased_count(_generator_id, value) + game_state.set_generator_purchased_count(_generator_id, value) ## Whether this generator is unlocked for the player. var is_unlocked: bool: get: _ensure_registered() - return GameState.is_generator_unlocked(_generator_id) + return game_state.is_generator_unlocked(_generator_id) set(value): _ensure_registered() - GameState.set_generator_unlocked(_generator_id, value) + game_state.set_generator_unlocked(_generator_id, value) ## Whether this unlocked generator is currently visible/usable to the player. var is_available: bool: get: _ensure_registered() - return GameState.is_generator_available(_generator_id) + return game_state.is_generator_available(_generator_id) set(value): _ensure_registered() - GameState.set_generator_available(_generator_id, value) + game_state.set_generator_available(_generator_id, value) ## Accumulated elapsed time toward the next automatic production cycle. var cycle_progress_seconds: float = 0.0 @@ -96,12 +99,15 @@ func _ready() -> void: assert(data != null, "Data cannot be null") _generator_id = _resolve_generator_id() - #_ensure_registered() cycle_progress_seconds = 0.0 - GameState.currency_changed.connect(_on_currency_changed) - GameState.generator_state_changed.connect(_on_generated_state_changed) - _evaluate_generator_unlock_goal(false) + if game_state: + game_state.currency_changed.connect(_on_currency_changed) + game_state.generator_state_changed.connect(_on_generated_state_changed) + game_state.goal_completed.connect(_on_goal_completed) + # Research XP is awarded via game_state.add_research_xp() - buff calculation handled statically + + _ensure_registered() ## Updates click cooldown/click grants and runs automatic production cycles. func _process(delta: float) -> void: @@ -137,6 +143,23 @@ func _grant_cycle_income(cycle_count: int) -> void: var produced: BigNumber = BigNumber.from_float(per_cycle).multiply(BigNumber.from_float(float(cycle_count))) _add_currency(produced) production_tick.emit(produced, cycle_count, owned) + + if data != null and data.research_data != null: + if not _is_research_active(data.research_data.id): + return + + var research_workers: int = game_state.get_research_workers() + if research_workers < data.research_data.min_workers_for_xp: + return + + var amount_float: float = produced.mantissa * pow(10.0, float(produced.exponent)) + var base_xp: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float) + + var worker_multiplier: float = 1.0 + (float(research_workers) * data.research_data.worker_scaling_factor) + var scaled_xp: BigNumber = base_xp.multiply(BigNumber.from_float(worker_multiplier)) + + if game_state != null and scaled_xp.mantissa > 0.0: + game_state.add_research_xp(data.research_data.id, scaled_xp) ## Convenience helper for the next single-unit purchase cost. func get_next_cost() -> BigNumber: @@ -195,7 +218,7 @@ func buy_max() -> int: if purchase_currency_id == &"": return 0 - var available_currency: float = _big_number_to_float(_get_currency_amount_by_id(purchase_currency_id)) + var available_currency: float = _get_currency_amount_by_id(purchase_currency_id).to_float() var estimated_max: int = data.max_affordable(owned, available_currency) if estimated_max <= 0: return 0 @@ -219,18 +242,27 @@ func buy_max() -> int: func get_buffs() -> Array[GeneratorBuffData]: if data == null: return [] - return data.buffs + if game_state == null: + return [] + return game_state.get_buffs_for_generator(_generator_id) func get_buff_level(buff_id: StringName) -> int: _ensure_registered() - return GameState.get_generator_buff_level(_generator_id, buff_id) + if game_state == null: + return 0 + return game_state.get_buff_level(buff_id) func is_buff_unlocked(buff_id: StringName) -> bool: _ensure_registered() - return GameState.is_generator_buff_unlocked(_generator_id, buff_id) + if game_state == null: + return false + return game_state.is_buff_unlocked(buff_id) func get_buff_cost(buff_id: StringName) -> BigNumber: - var buff: GeneratorBuffData = _find_buff(buff_id) + if game_state == null: + return BigNumber.from_float(0.0) + + var buff: GeneratorBuffData = game_state.get_buff(buff_id) if buff == null: return BigNumber.from_float(0.0) if not is_buff_unlocked(buff.id): @@ -240,7 +272,10 @@ func get_buff_cost(buff_id: StringName) -> BigNumber: return buff.get_cost_for_level(level) func get_buff_cost_currency_id(buff_id: StringName) -> StringName: - var buff: GeneratorBuffData = _find_buff(buff_id) + if game_state == null: + return &"" + + var buff: GeneratorBuffData = game_state.get_buff(buff_id) if buff == null: return &"" return _resolve_buff_cost_currency_id(buff) @@ -249,7 +284,10 @@ func can_buy_buff(buff_id: StringName) -> bool: if not is_available_to_player(): return false - var buff: GeneratorBuffData = _find_buff(buff_id) + if game_state == null: + return false + + var buff: GeneratorBuffData = game_state.get_buff(buff_id) if buff == null: return false @@ -265,21 +303,24 @@ func can_buy_buff(buff_id: StringName) -> bool: return false var cost: BigNumber = buff.get_cost_for_level(current_level) - var current_amount: BigNumber = GameState.get_currency_amount_by_id(cost_currency_id) + var current_amount: BigNumber = game_state.get_currency_amount_by_id(cost_currency_id) return current_amount.is_greater_than(cost) or current_amount.is_equal_to(cost) func buy_buff(buff_id: StringName) -> bool: if not is_available_to_player(): return false - var buff: GeneratorBuffData = _find_buff(buff_id) + if game_state == null: + return false + + var buff: GeneratorBuffData = game_state.get_buff(buff_id) if buff == null: return false - if not is_buff_unlocked(buff.id): + if not game_state.is_buff_unlocked(buff.id): return false - var current_level: int = get_buff_level(buff.id) + var current_level: int = game_state.get_buff_level(buff.id) if not buff.can_purchase_next_level(current_level): return false @@ -293,25 +334,58 @@ func buy_buff(buff_id: StringName) -> bool: return false var next_level: int = current_level + 1 - GameState.set_generator_buff_level(_generator_id, buff.id, next_level) + game_state.set_buff_level(buff.id, next_level) _apply_resource_purchase_buff(buff, next_level) buff_purchased.emit(buff.id, next_level, cost, cost_currency_id) return true func is_buff_maxed(buff_id: StringName) -> bool: - var buff: GeneratorBuffData = _find_buff(buff_id) + if game_state == null: + return true + + var buff: GeneratorBuffData = game_state.get_buff(buff_id) if buff == null: return true - if not is_buff_unlocked(buff.id): + if not game_state.is_buff_unlocked(buff.id): return false - return not buff.can_purchase_next_level(get_buff_level(buff.id)) + return not buff.can_purchase_next_level(game_state.get_buff_level(buff.id)) func get_automatic_production_multiplier() -> float: return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER) func get_effective_auto_run_multiplier() -> float: - return run_multiplier * get_automatic_production_multiplier() * _get_prestige_multiplier() + return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_buff_production_multiplier() + +func get_research_multiplier() -> float: + if data == null or data.research_data == null: + return 1.0 + if game_state == null: + return 1.0 + return game_state.get_research_multiplier(data.research_data.id) + +func _is_research_active(research_id: StringName) -> bool: + if game_state == null: + return true + + var research_panel: Node = null + var parent: Node = get_parent() + while parent != null and parent != game_state.get_tree().root: + if parent.has_method("is_research_active"): + research_panel = parent + break + parent = parent.get_parent() + + if research_panel == null: + research_panel = game_state.find_child("ResearchPanel", true, false) + + if research_panel == null: + return true + + if research_panel.has_method("is_research_active"): + return research_panel.is_research_active(research_id) + + return true func reset_runtime_state_for_prestige() -> void: _is_registered = false @@ -319,8 +393,6 @@ func reset_runtime_state_for_prestige() -> void: cycle_progress_seconds = 0.0 _remaining_click_cooldown_seconds = 0.0 _ensure_registered() - _evaluate_generator_unlock_goal(false) - _evaluate_buff_unlock_goals() visible = is_available_to_player() func get_manual_click_multiplier() -> float: @@ -359,9 +431,30 @@ func _try_grant_click_currency() -> void: if _remaining_click_cooldown_seconds > 0.0: return - _add_currency(_get_click_value()) + var click_value: BigNumber = _get_click_value() + _add_currency(click_value) + _remaining_click_cooldown_seconds = _get_click_cooldown_seconds() + # Grant research XP for manual clicks + if data.research_data != null and click_value.mantissa > 0.0: + if not _is_research_active(data.research_data.id): + return + + var research_workers: int = game_state.get_research_workers() + if research_workers < data.research_data.min_workers_for_xp: + return + + var amount_float: float = click_value.mantissa * pow(10.0, float(click_value.exponent)) + var base_xp: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float) + + var worker_multiplier: float = 1.0 + (float(research_workers) * data.research_data.worker_scaling_factor) + var scaled_xp: BigNumber = base_xp.multiply(BigNumber.from_float(worker_multiplier)) + + if game_state != null and scaled_xp.mantissa > 0.0: + game_state.add_research_xp(data.research_data.id, scaled_xp) + + ## Resolves click reward from data. func _get_click_value() -> BigNumber: if data == null: @@ -384,20 +477,10 @@ func _get_click_cooldown_seconds() -> float: func _grants_click_while_hovering() -> bool: return grants_click_while_hovering -func _get_prestige_multiplier() -> float: - var manager: PrestigeManager = get_node_or_null("/root/PrestigeManager") - if manager == null: +func _get_prestige_buff_production_multiplier() -> float: + if game_state == null: return 1.0 - if not manager.get_total_multiplier(): - return 1.0 - - var raw_multiplier: Variant = manager.get_total_multiplier() - if raw_multiplier is float: - return maxf(raw_multiplier, 0.0) - if raw_multiplier is int: - return maxf(float(raw_multiplier), 0.0) - - return 1.0 + return game_state.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, _generator_id) ## Returns this generator's resolved state id. func get_generator_id() -> StringName: @@ -406,14 +489,16 @@ func get_generator_id() -> StringName: ## Returns the configured currency id. func get_currency_id() -> StringName: - return GameState.get_currency_id(currency) + if game_state == null: + return &"" + return game_state.get_currency_id(currency) func get_purchase_currency_id() -> StringName: var purchase_currency: Currency = _resolve_purchase_currency() if purchase_currency == null: return &"" - return GameState.get_currency_id(purchase_currency) + return game_state.get_currency_id(purchase_currency) ## True when the generator is both unlocked and available. func is_available_to_player() -> bool: @@ -425,13 +510,17 @@ func _add_currency(amount: BigNumber) -> void: push_error("CurrencyGenerator '%s' cannot add currency: currency resource is null." % String(name)) return - GameState.add_currency(currency, amount) + if game_state == null: + return + game_state.add_currency(currency, amount) func _add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void: if currency_id == &"": return - GameState.add_currency_by_id(currency_id, amount) + if game_state == null: + return + game_state.add_currency_by_id(currency_id, amount) ## Attempts to spend target currency; returns false when insufficient. func _spend_currency(cost: BigNumber) -> bool: @@ -439,25 +528,33 @@ func _spend_currency(cost: BigNumber) -> bool: push_error("CurrencyGenerator '%s' cannot spend currency: currency resource is null." % String(name)) return false - return GameState.spend_currency(currency, cost) + if game_state == null: + return false + return game_state.spend_currency(currency, cost) func _spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool: if currency_id == &"": return false - return GameState.spend_currency_by_id(currency_id, cost) + if game_state == null: + return false + return game_state.spend_currency_by_id(currency_id, cost) ## Returns the current amount for the configured currency type. func _get_currency_amount() -> BigNumber: if currency == null: return BigNumber.from_float(0.0) - return GameState.get_currency_amount(currency) + if game_state == null: + return BigNumber.from_float(0.0) + return game_state.get_currency_amount(currency) func _get_currency_amount_by_id(currency_id: StringName) -> BigNumber: if currency_id == &"": return BigNumber.from_float(0.0) - return GameState.get_currency_amount_by_id(currency_id) + if game_state == null: + return BigNumber.from_float(0.0) + return game_state.get_currency_amount_by_id(currency_id) ## Safely converts finite float values into BigNumber costs. func _float_to_big_number(value: float) -> BigNumber: @@ -468,29 +565,19 @@ func _float_to_big_number(value: float) -> BigNumber: return BigNumber.from_float(value) ## Converts BigNumber to float for approximate estimate math. -func _big_number_to_float(value: BigNumber) -> float: - if value.mantissa <= 0.0: - return 0.0 - if value.exponent > 308: - return INF - if value.exponent < -308: - return 0.0 - return value.mantissa * pow(10.0, float(value.exponent)) -func _find_buff(buff_id: StringName) -> GeneratorBuffData: - if data == null: - return null - return data.find_buff(buff_id) func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName: if buff == null: return &"" - var cost_currency: Resource = buff.cost_currency if buff.cost_currency != null else currency + var cost_currency: Currency = buff.cost_currency if buff.cost_currency != null else currency if cost_currency == null: return &"" - return GameState.get_currency_id(cost_currency) + if game_state == null: + return &"" + return game_state.get_currency_id(cost_currency) func _resolve_purchase_currency() -> Currency: if data == null: @@ -502,25 +589,18 @@ func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName: if buff == null: return &"" - var target_currency: Resource = buff.resource_target_currency if buff.resource_target_currency != null else currency + var target_currency: Currency = buff.resource_target_currency if buff.resource_target_currency != null else currency if target_currency == null: return &"" - return GameState.get_currency_id(target_currency) + if game_state == null: + return &"" + return game_state.get_currency_id(target_currency) func _get_multiplier_for_buff_kind(kind: GeneratorBuffData.BuffKind) -> float: - var multiplier: float = 1.0 - for buff in get_buffs(): - if buff == null: - continue - if buff.kind != kind: - continue - if not is_buff_unlocked(buff.id): - continue - - multiplier *= buff.get_effect_multiplier(get_buff_level(buff.id)) - - return maxf(multiplier, 0.0) + if game_state == null: + return 1.0 + return game_state.get_effective_multiplier(_generator_id, kind) func _apply_resource_purchase_buff(buff: GeneratorBuffData, level: int) -> void: if buff == null: @@ -550,7 +630,7 @@ func _resolve_generator_id() -> StringName: return &"generator" -## Ensures this generator has a state entry in GameState. +## Ensures this generator has a state entry in LevelGameState. func _ensure_registered() -> void: if _is_registered or _is_registering: return @@ -560,8 +640,11 @@ func _ensure_registered() -> void: if _generator_id == &"": return + if game_state == null: + return + _is_registering = true - GameState.register_generator( + game_state.register_generator( _generator_id, _default_unlocked_state(), _default_available_state(), @@ -572,6 +655,9 @@ func _ensure_registered() -> void: _is_registering = false func _register_buffs() -> void: + if game_state == null: + return + for buff in get_buffs(): if buff == null: continue @@ -581,58 +667,34 @@ func _register_buffs() -> void: continue var buff_id: StringName = StringName(buff_id_text) - GameState.register_generator_buff(_generator_id, buff_id, 0) - var persisted_level: int = GameState.get_generator_buff_level(_generator_id, buff_id) - var starts_unlocked: bool = buff.unlocked or persisted_level > 0 - GameState.register_generator_buff_unlocked(_generator_id, buff_id, starts_unlocked) + game_state.register_buff(buff) + + if not game_state.is_buff_unlocked(buff_id): + var persisted_level: int = game_state.get_buff_level(buff_id) + var starts_unlocked: bool = buff.unlocked or persisted_level > 0 + game_state.set_buff_unlocked(buff_id, starts_unlocked) - _evaluate_buff_unlock_goals() - -func _evaluate_buff_unlock_goals() -> void: - for buff in get_buffs(): - _try_unlock_buff_from_goal(buff) - -func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void: - if buff == null: - return - - var buff_id_text: String = String(buff.id).strip_edges() - if buff_id_text.is_empty(): - return - - var buff_id: StringName = StringName(buff_id_text) - if GameState.is_generator_buff_unlocked(_generator_id, buff_id): - return - if not buff.has_unlock_goal(): - return - if not buff.is_unlock_goal_met(): - return - - GameState.set_generator_buff_unlocked(_generator_id, buff_id, true) + game_state.try_unlock_buff_from_goal(buff) func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void: - _evaluate_generator_unlock_goal(false) - _evaluate_buff_unlock_goals() + if game_state == null: + return + game_state.evaluate_all_goals() -func try_unlock_from_goal() -> bool: - return _evaluate_generator_unlock_goal(true) - -func _evaluate_generator_unlock_goal(allow_manual_unlock: bool) -> bool: - if data == null: - return false +func _on_goal_completed(goal_id: StringName) -> void: + if data == null or not data.has_unlock_goal(): + return + if data.get_unlock_goal_id() != goal_id: + return if is_available_to_player(): - return false - if not data.has_unlock_goal(): - return false - if not data.unlocks_automatically_from_goal() and not allow_manual_unlock: - return false - if not data.is_unlock_goal_met(): - return false + return - GameState.set_generator_unlocked(_generator_id, true) - GameState.set_generator_available(_generator_id, true) - goal_achieved.emit(_generator_id, data.get_unlock_goal_id()) - return true + if game_state == null: + return + game_state.set_generator_unlocked(_generator_id, true) + game_state.set_generator_available(_generator_id, true) + goal_achieved.emit(_generator_id, goal_id) + _on_generated_state_changed(_generator_id, game_state._get_generator_state(_generator_id)) ## Default unlocked state loaded from generator data. func _default_unlocked_state() -> bool: @@ -665,3 +727,5 @@ func _on_area_2d_mouse_exited() -> void: func _on_generated_state_changed(generator_id: StringName, _state: Dictionary) -> void: if generator_id == _generator_id: visible = is_available_to_player() + if info_generator_container != null: + info_generator_container._refresh_generator_ui() diff --git a/core/generator/currency_generator.tscn b/core/generator/currency_generator.tscn index 94f74dd..c34cb64 100644 --- a/core/generator/currency_generator.tscn +++ b/core/generator/currency_generator.tscn @@ -1,34 +1,6 @@ [gd_scene format=3 uid="uid://jeoiinukrrsp"] [ext_resource type="Script" uid="uid://dtbxopw6ulhl8" path="res://core/generator/currency_generator.gd" id="1_4n4ca"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_5tmvy"] -[ext_resource type="Resource" uid="uid://co0mcc2kvcpo5" path="res://idles/generators/orb.tres" id="3_wx13b"] -[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="4_ruf1h"] -[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://generator_container.tscn" id="5_bb14m"] - -[sub_resource type="RectangleShape2D" id="RectangleShape2D_y5m1q"] -size = Vector2(128, 128) [node name="CurrencyGenerator" type="Node2D" unique_id=967969064] script = ExtResource("1_4n4ca") -currency = ExtResource("2_5tmvy") -data = ExtResource("3_wx13b") - -[node name="Sprite2D" type="Sprite2D" parent="." unique_id=807584513] -texture = ExtResource("4_ruf1h") - -[node name="Area2D" type="Area2D" parent="." unique_id=707349247] - -[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1572620025] -shape = SubResource("RectangleShape2D_y5m1q") - -[node name="GeneratorPanel" parent="." unique_id=1129190041 node_paths=PackedStringArray("_generator") instance=ExtResource("5_bb14m")] -visible = false -offset_left = 33.0 -offset_top = -65.0 -offset_right = 474.0 -offset_bottom = 200.5 -_generator = NodePath("..") - -[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"] -[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"] diff --git a/core/generator/currency_generator_data.gd b/core/generator/currency_generator_data.gd index 3586206..6d89250 100644 --- a/core/generator/currency_generator_data.gd +++ b/core/generator/currency_generator_data.gd @@ -1,11 +1,6 @@ class_name CurrencyGeneratorData extends Resource -enum UnlockGoalBehavior { - AUTOMATIC, - MANUAL_BUTTON, -} - @export var id: StringName = &"" @export var name: String = "" @export var starts_unlocked: bool = true @@ -13,7 +8,6 @@ enum UnlockGoalBehavior { @export var initial_owned: int = 0 @export var purchase_currency: Currency @export var unlock_goal: GoalData -@export var unlock_goal_behavior: UnlockGoalBehavior = UnlockGoalBehavior.AUTOMATIC ## Base cost and exponential growth (part I): cost_next = b * r^owned @export var initial_cost: float = 10.0 @@ -37,8 +31,7 @@ enum UnlockGoalBehavior { @export var click_exponent: int = 0 @export var click_cooldown_seconds: float = 0.2 -## Data-driven buffs purchasable for this generator. -@export var buffs: Array[GeneratorBuffData] = [] +@export var research_data: ResearchData func has_unlock_goal() -> bool: if unlock_goal == null: @@ -46,12 +39,6 @@ func has_unlock_goal() -> bool: return bool(unlock_goal.is_valid()) -func is_unlock_goal_met() -> bool: - if not has_unlock_goal(): - return false - - return bool(unlock_goal.is_met()) - func get_unlock_goal_id() -> StringName: if not has_unlock_goal(): return &"" @@ -62,22 +49,6 @@ func get_unlock_goal_id() -> StringName: return StringName(goal_id_text) -func unlocks_automatically_from_goal() -> bool: - return unlock_goal_behavior == UnlockGoalBehavior.AUTOMATIC - -func find_buff(buff_id: StringName) -> GeneratorBuffData: - var expected_id: String = String(buff_id).strip_edges() - if expected_id.is_empty(): - return null - - for buff in buffs: - if buff == null: - continue - if String(buff.id) == expected_id: - return buff - - return null - ## Returns cost of next generator func cost_next(owned: int) -> float: return cost_for_amount(owned, 1) diff --git a/core/generator/generator_buff_data.gd b/core/generator/generator_buff_data.gd index 916b94c..00d0275 100644 --- a/core/generator/generator_buff_data.gd +++ b/core/generator/generator_buff_data.gd @@ -5,11 +5,13 @@ enum BuffKind { AUTO_PRODUCTION_MULTIPLIER, MANUAL_CLICK_MULTIPLIER, RESOURCE_PURCHASE, + RESEARCH_XP_MULTIPLIER, } const HUGE_EXPONENT: int = 1000000 @export var id: StringName = &"" +@export var target_ids: Array[StringName] = [] @export var kind: BuffKind = BuffKind.AUTO_PRODUCTION_MULTIPLIER @export_multiline var text: String = "" @export var icon: Texture2D @@ -23,6 +25,10 @@ const HUGE_EXPONENT: int = 1000000 @export var base_effect: float = 1.0 @export var effect_increment: float = 0.1 +@export_group("Research XP Multiplier") +@export var research_target_id: StringName = &"" +@export var xp_multiplier_per_level: float = 0.1 + @export_group("Cost") @export var cost_currency: Currency @export var base_cost_mantissa: float = 10.0 @@ -35,6 +41,18 @@ const HUGE_EXPONENT: int = 1000000 @export var resource_purchase_base_exponent: int = 0 @export var resource_purchase_increment_multiplier: float = 1.2 +func targets_generator(generator_id: StringName) -> bool: + if target_ids.is_empty(): + return false + + if target_ids.has("*"): + return true + + return target_ids.has(generator_id) + +func get_target_generators() -> Array[StringName]: + return target_ids.duplicate() + func has_unlock_goal() -> bool: if unlock_goal == null: return false @@ -45,10 +63,10 @@ func get_unlock_goal_amount() -> BigNumber: if unlock_goal == null: return BigNumber.from_float(0.0) - var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements() - if valid_requirements.is_empty(): + var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements() + if requirements.is_empty(): return BigNumber.from_float(0.0) - var requirement_resource: GoalRequirementData = valid_requirements[0] + var requirement_resource: GoalRequirementData = requirements[0] if requirement_resource == null: return BigNumber.from_float(0.0) @@ -62,21 +80,15 @@ func get_unlock_goal_currency() -> Currency: if unlock_goal == null: return null - var valid_requirements: Array[GoalRequirementData] = unlock_goal.get_valid_requirements() - if valid_requirements.is_empty(): + var requirements: Array[GoalRequirementData] = unlock_goal.get_requirements() + if requirements.is_empty(): return null - var requirement_resource: GoalRequirementData = valid_requirements[0] + var requirement_resource: GoalRequirementData = requirements[0] if requirement_resource == null: return null return requirement_resource.currency -func is_unlock_goal_met() -> bool: - if unlock_goal == null: - return false - - return bool(unlock_goal.is_met()) - func can_purchase_next_level(current_level: int) -> bool: if max_level < 0: return true @@ -126,7 +138,9 @@ func get_effect_description(level: int) -> String: BuffKind.AUTO_PRODUCTION_MULTIPLIER, BuffKind.MANUAL_CLICK_MULTIPLIER: return "x%.2f" % get_effect_multiplier(safe_level) BuffKind.RESOURCE_PURCHASE: - return "+%s" % get_resource_purchase_amount_for_level(safe_level).to_string_suffix(2) + return "+%s" % get_resource_purchase_amount_for_level(level).to_string_suffix(2) + BuffKind.RESEARCH_XP_MULTIPLIER: + return "+%.0f%% XP" % ((get_effect_multiplier(safe_level) - 1.0) * 100) _: return "-" diff --git a/generator_container.gd b/core/generator/generator_container.gd similarity index 83% rename from generator_container.gd rename to core/generator/generator_container.gd index f2e1d4d..e6e66b7 100644 --- a/generator_container.gd +++ b/core/generator/generator_container.gd @@ -37,11 +37,10 @@ func _ready() -> void: _generator.purchase_failed.connect(_on_generator_updated) _generator.buff_purchased.connect(_on_generator_updated) _generator.buff_purchase_failed.connect(_on_generator_updated) - GameState.currency_changed.connect(_on_currency_changed) - - GameState.generator_state_changed.connect(_on_generator_state_changed) - GameState.generator_buff_level_changed.connect(_on_generator_buff_level_changed) - GameState.generator_buff_unlocked_changed.connect(_on_generator_buff_unlocked_changed) + + if _generator.game_state: + _generator.game_state.currency_changed.connect(_on_currency_changed) + _generator.game_state.generator_state_changed.connect(_on_generator_state_changed) _name_label.text = _generator.data.name if _generator.data != null else "Generator" _build_buff_rows() @@ -70,18 +69,6 @@ func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) - visible = true _refresh_generator_ui() -func _on_generator_buff_level_changed(generator_id: StringName, _buff_id: StringName, _new_level: int) -> void: - if generator_id != _generator_id: - return - - _refresh_generator_ui() - -func _on_generator_buff_unlocked_changed(generator_id: StringName, _buff_id: StringName, _unlocked: bool) -> void: - if generator_id != _generator_id: - return - - _refresh_generator_ui() - func _on_buy_buff_pressed(buff_id: StringName) -> void: _generator.buy_buff(buff_id) _refresh_generator_ui() @@ -122,10 +109,10 @@ func _refresh_buff_rows(can_interact: bool) -> void: var buff_unlocked: bool = _generator.is_buff_unlocked(buff.id) if not buff_unlocked: var locked_hint: String = "Locked" - var unlock_goal_currency: Resource = buff.get_unlock_goal_currency() - if buff.has_unlock_goal() and unlock_goal_currency != null: - var goal_currency_id: StringName = GameState.get_currency_id(unlock_goal_currency) - var currency_name: String = GameState.get_currency_name(goal_currency_id) + var unlock_goal_currency: Currency = buff.get_unlock_goal_currency() + if buff.has_unlock_goal() and unlock_goal_currency != null and _generator.game_state: + var goal_currency_id: StringName = _generator.game_state.get_currency_id(unlock_goal_currency) + var currency_name: String = _generator.game_state.get_currency_name(goal_currency_id) var required_text: String = buff.get_unlock_goal_amount().to_string_suffix(2) locked_hint = "Unlock at %s %s" % [required_text, currency_name] @@ -141,20 +128,27 @@ func _refresh_buff_rows(can_interact: bool) -> void: var cost: BigNumber = _generator.get_buff_cost(buff.id) var cost_currency_id: StringName = _generator.get_buff_cost_currency_id(buff.id) - var currency_label: String = GameState.get_currency_name(cost_currency_id) + var currency_label: String = _generator.game_state.get_currency_name(cost_currency_id) if _generator.game_state else "Unknown" var cost_text: String = "%s %s" % [cost.to_string_suffix(2), currency_label] var can_buy: bool = can_interact and _generator.can_buy_buff(buff.id) row.tile.set_runtime(level, effect_text, cost_text, can_buy, false) func _refresh_generator_ui() -> void: + if _buy_button == null or _buy_max_button == null: + return + + if _generator == null: + return + var can_interact: bool = _generator.is_available_to_player() + _buy_button.disabled = not can_interact _buy_max_button.disabled = not can_interact _owned_value.text = str(_generator.owned) if can_interact: var purchase_currency_id: StringName = _generator.get_purchase_currency_id() - var purchase_currency_name: String = GameState.get_currency_name(purchase_currency_id) + var purchase_currency_name: String = _generator.game_state.get_currency_name(purchase_currency_id) if _generator.game_state else "Unknown" if purchase_currency_name.is_empty(): purchase_currency_name = "Unconfigured" _next_cost_value.text = "%s %s" % [_generator.get_next_cost().to_string_suffix(2), purchase_currency_name] diff --git a/generator_container.gd.uid b/core/generator/generator_container.gd.uid similarity index 100% rename from generator_container.gd.uid rename to core/generator/generator_container.gd.uid diff --git a/generator_container.tscn b/core/generator/generator_container.tscn similarity index 98% rename from generator_container.tscn rename to core/generator/generator_container.tscn index 90a9c47..76f89c7 100644 --- a/generator_container.tscn +++ b/core/generator/generator_container.tscn @@ -1,6 +1,6 @@ [gd_scene format=3 uid="uid://ckos7f22pnmyh"] -[ext_resource type="Script" uid="uid://es14nqk6vrrk" path="res://generator_container.gd" id="1_8wouw"] +[ext_resource type="Script" uid="uid://es14nqk6vrrk" path="res://core/generator/generator_container.gd" id="1_8wouw"] [node name="GeneratorContainer" type="PanelContainer" unique_id=1451609580] anchors_preset = 8 diff --git a/core/generator/research_buff_calculator.gd b/core/generator/research_buff_calculator.gd new file mode 100644 index 0000000..7ddad96 --- /dev/null +++ b/core/generator/research_buff_calculator.gd @@ -0,0 +1,22 @@ +## Static utility class for research XP buff calculations +class_name ResearchBuffCalculator + +static func apply_buffs(game_state: LevelGameState, research_id: StringName, base_xp: BigNumber) -> BigNumber: + if game_state == null: + return base_xp + + var multiplier: float = _calculate_buff_multiplier(game_state, research_id) + var multiplier_bn: BigNumber = BigNumber.from_float(multiplier) + return base_xp.multiply(multiplier_bn) + +static func _calculate_buff_multiplier(game_state: LevelGameState, research_id: StringName) -> float: + var research: ResearchData = game_state.research_catalogue.get_research_by_id(research_id) + if research == null or research.associated_buff_id.is_empty(): + return 1.0 + + var buff: GeneratorBuffData = game_state.get_buff(research.associated_buff_id) + if buff == null or not game_state.is_buff_active(buff.id): + return 1.0 + + var level: int = game_state.get_buff_level(buff.id) + return buff.get_effect_multiplier(level) diff --git a/core/generator/research_buff_calculator.gd.uid b/core/generator/research_buff_calculator.gd.uid new file mode 100644 index 0000000..61136dd --- /dev/null +++ b/core/generator/research_buff_calculator.gd.uid @@ -0,0 +1 @@ +uid://bhrcgp2dna0mb diff --git a/core/goals/README.md b/core/goals/README.md new file mode 100644 index 0000000..ae5a0fe --- /dev/null +++ b/core/goals/README.md @@ -0,0 +1,192 @@ +# Goals Module Documentation + +## Overview + +The `goals/` subfolder contains the achievement system. Goals track player progress and can unlock generators or buffs when conditions are met. + +## Files + +| File | Purpose | +|------|---------| +| `goal_data.gd` | Goal definition resource | +| `goal_requirement_data.gd` | Individual requirement criteria | + +## GoalData + +Resource class defining an achievement. + +### Properties + +```gdscript +class_name GoalData +extends Resource + +@export var id: StringName = &"" # Unique identifier +@export var requirements: Array[GoalRequirementData] # Conditions to complete +@export var unlock_behavior: UnlockBehavior = MANUAL # AUTOMATIC or MANUAL +``` + +### Enum + +```gdscript +enum UnlockBehavior { + AUTOMATIC, # Goal completes immediately when met + MANUAL, # Goal requires player to click unlock button +} +``` + +### Methods + +```gdscript +func has_id() -> bool # Has non-empty id +func is_valid() -> bool # Has valid id and requirements +func get_requirements() -> Array[GoalRequirementData] # Access requirements +``` + +**Note:** State-dependent methods (`is_goal_met()`, `get_goal_progress()`, `get_goal_requirement_summary()`) are implemented in `LevelGameState` for proper decoupling. + +## GoalRequirementData + +Single condition within a goal. + +### Properties + +```gdscript +class_name GoalRequirementData +extends Resource + +@export var currency: Currency # Currency to track +@export var amount_mantissa: float = 0.0 # Required amount (scientific notation) +@export var amount_exponent: int = 0 +``` + +### Methods + +```gdscript +func get_currency() -> Currency # Currency to track +func get_amount() -> BigNumber # Required amount +func has_valid_amount() -> bool # Amount is non-negative +``` + +**Note:** State-dependent methods (`is_met()`, `get_progress()`, `get_summary_text()`) are implemented in `LevelGameState` for proper decoupling. + +### Progress Calculation + +Uses **logarithmic scaling** for better visual feedback on huge numbers: + +```gdscript +# Convert to log space +current_log = log(current_mantissa) / log(10) + current_exponent +required_log = log(required_mantissa) / log(10) + required_exponent + +# Progress ratio +progress = clampf(current_log / required_log, 0.0, 1.0) +``` + +This means reaching 50% progress on a 1e100 goal requires ~1e50 currency. + +## Goal Evaluation + +### Automatic Checking + +`LevelGameState` evaluates goals whenever currency changes: + +```gdscript +func _on_currency_changed(currency_id, new_amount): + evaluate_all_goals() +``` + +### Completion Flow + +1. Currency updated → signal emitted +2. `LevelGameState.evaluate_all_goals()` called +3. Each uncompleted goal checked via `is_goal_met(goal)` +4. If met → `goal_completed` signal emitted +5. Buffs with unlock goals checked → unlocked if goal complete + +### Manual Unlock vs Automatic + +Goals control their own unlock behavior via `unlock_behavior`: +- **AUTOMATIC**: Goal completes immediately when requirements are met +- **MANUAL**: Goal stays incomplete until player clicks unlock button + +Generators with `unlock_goal` automatically unlock when the goal completes. + +### Goal Methods in LevelGameState + +```gdscript +func is_goal_requirement_met(requirement: GoalRequirementData) -> bool +func get_goal_requirement_progress(requirement: GoalRequirementData) -> float +func get_goal_requirement_summary(requirement: GoalRequirementData) -> String +func is_goal_met(goal: GoalData) -> bool +func get_goal_progress(goal: GoalData) -> float +``` + +## Usage Example + +### Defining a Goal + +Create `res://sandbox/goals/first_million.tres`: + +``` +[gd_resource type="Resource" script_class="GoalData"] +[resource] +id = &"first_million" +requirements = [ + preload("res://sandbox/goals/first_million_req.tres") +] +``` + +### Defining a Requirement + +Create `res://sandbox/goals/first_million_req.tres`: + +``` +[gd_resource type="Resource" script_class="GoalRequirementData"] +[resource] +currency = preload("res://sandbox/currencies/gold.tres") +amount_mantissa = 1.0 +amount_exponent = 6 # 1,000,000 +``` + +### Connecting to Generator + +In `CurrencyGeneratorData`: +```gdscript +@export var unlock_goal: GoalData # Assign goal resource +``` + +The goal's `unlock_behavior` determines if the generator unlocks automatically or requires manual button click. + +## Signals + +```gdscript +signal goal_completed(goal_id: StringName) +signal goal_progress_changed(goal_id: StringName, progress: float) +``` + +## Integration Points + +### With LevelGameState +- Goals registered from `GoalCatalogue` assigned to `LevelGameState` +- Completion state saved to JSON +- Buff unlock goals checked automatically + +### With Buffs +```gdscript +# In GeneratorBuffData +@export var unlock_goal: GoalData + +# When goal completes, buff auto-unlocks: +level_game_state._try_unlock_buff_from_goal(buff) +``` + +### With Prestige +- Goal completion state persists through prestige +- Reset via `LevelGameState.reset_for_prestige()` + +## See Also + +- `core/level_game_state.gd` - Goal state management +- `core/generator/currency_generator_data.gd` - Generator unlock goals +- `core/generator/generator_buff_data.gd` - Buff unlock goals diff --git a/core/goals/current_goal_panel.gd b/core/goals/current_goal_panel.gd new file mode 100644 index 0000000..4d6080f --- /dev/null +++ b/core/goals/current_goal_panel.gd @@ -0,0 +1,153 @@ +## Single-goal tile showing the current (first uncompleted) goal from GoalCatalogue. +## Mirrors a single row from goals_debug_ui: goal name, requirements text, and a resolve button. +extends PanelContainer + +@onready var _game_state: LevelGameState = find_parent("LevelGameState") +@onready var _goal_label: Label = $VBoxContainer/GoalLabel +@onready var _requirements_label: Label = $VBoxContainer/RequirementsLabel +@onready var _resolve_button: Button = $VBoxContainer/ResolveButton + +var _current_goal: GoalData + + +func _ready() -> void: + _resolve_button.pressed.connect(_on_resolve_pressed) + if _game_state: + _game_state.currency_changed.connect(_on_currency_changed) + _game_state.generator_state_changed.connect(_on_generator_state_changed) + _game_state.goal_completed.connect(_on_goal_completed) + if not _game_state.goal_catalogue or _game_state.goal_catalogue.goals.is_empty(): + _game_state.ready.connect(_on_level_state_ready) + + _refresh_current_goal() + _refresh_ui() + _refresh_ui.call_deferred() + + +func _get_current_goal() -> GoalData: + if _game_state == null: + return null + var catalogue: GoalCatalogue = _game_state.goal_catalogue + if catalogue == null: + return null + + for goal in catalogue.goals: + if goal == null: + continue + if not goal.has_id(): + continue + if _game_state.is_goal_completed(goal.id): + continue + return goal + + return null + + +func _refresh_current_goal() -> void: + _current_goal = _get_current_goal() + + +func _refresh_ui() -> void: + if _current_goal == null: + _goal_label.text = "All goals complete!" + _requirements_label.text = "" + _resolve_button.disabled = true + _resolve_button.text = "Done" + return + + _goal_label.text = String(_current_goal.id) + + if _game_state == null: + return + + var already_completed: bool = _game_state.is_goal_completed(_current_goal.id) + + _requirements_label.text = _get_requirements_text(_current_goal) + + var met: bool = _game_state.is_goal_met(_current_goal) + var can_click: bool = met and not already_completed + + _resolve_button.disabled = not can_click + _resolve_button.text = "Done" if already_completed else "Resolve" + + +func _get_requirements_text(goal: GoalData) -> String: + if goal == null or _game_state == null: + return "" + + var parts: Array[String] = [] + for req in goal.get_requirements(): + if req == null: + continue + + var currency: Currency = req.get_currency() + if currency == null: + continue + + var currency_id: StringName = _game_state.get_currency_id(currency) + var total_amount: BigNumber = _game_state.get_total_currency_acquired_by_id(currency_id) + var required_amount: BigNumber = req.get_amount() + var currency_name: String = _game_state.get_currency_name(currency_id) + + parts.append( + "%s %s / %s" % [ + currency_name, + total_amount.to_string_suffix(2), + required_amount.to_string_suffix(2), + ] + ) + + if parts.is_empty(): + return "No requirements" + return " | ".join(parts) + + +func _on_resolve_pressed() -> void: + if _current_goal == null: + return + if _game_state == null: + return + if _game_state.is_goal_completed(_current_goal.id): + return + if not _game_state.is_goal_met(_current_goal): + return + + if _current_goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL: + _game_state._complete_goal_manually(_current_goal.id) + + _refresh_current_goal() + _refresh_ui() + + +func _on_currency_changed(_currency_id: StringName, _new_amount: BigNumber) -> void: + _refresh_current_goal() + _refresh_ui() + + +func _on_generator_state_changed(_generator_id: StringName, _state: Dictionary) -> void: + _refresh_current_goal() + _refresh_ui() + + +func _on_goal_completed(_goal_id: StringName) -> void: + _refresh_current_goal() + _refresh_ui() + + +func _on_level_state_ready() -> void: + if _game_state: + _game_state.ready.disconnect(_on_level_state_ready) + _refresh_current_goal() + _refresh_ui() + + +func _exit_tree() -> void: + if _game_state: + if _game_state.currency_changed.is_connected(_on_currency_changed): + _game_state.currency_changed.disconnect(_on_currency_changed) + if _game_state.generator_state_changed.is_connected(_on_generator_state_changed): + _game_state.generator_state_changed.disconnect(_on_generator_state_changed) + if _game_state.goal_completed.is_connected(_on_goal_completed): + _game_state.goal_completed.disconnect(_on_goal_completed) + if _game_state.ready.is_connected(_on_level_state_ready): + _game_state.ready.disconnect(_on_level_state_ready) diff --git a/core/goals/current_goal_panel.gd.uid b/core/goals/current_goal_panel.gd.uid new file mode 100644 index 0000000..5bbbdc8 --- /dev/null +++ b/core/goals/current_goal_panel.gd.uid @@ -0,0 +1 @@ +uid://dfn06haxb2yhr diff --git a/core/goals/current_goal_panel.tscn b/core/goals/current_goal_panel.tscn new file mode 100644 index 0000000..b51ea35 --- /dev/null +++ b/core/goals/current_goal_panel.tscn @@ -0,0 +1,26 @@ +[gd_scene format=3 uid="uid://dudfvxilbydas"] + +[ext_resource type="Script" uid="uid://dfn06haxb2yhr" path="res://core/goals/current_goal_panel.gd" id="1_70l2h"] + +[node name="CurrentGoalPanel" type="PanelContainer" unique_id=1791509101] +offset_left = 155.0 +offset_top = 210.0 +offset_right = 500.0 +offset_bottom = 320.0 +script = ExtResource("1_70l2h") + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=933762288] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="GoalLabel" type="Label" parent="VBoxContainer" unique_id=1859068341] +layout_mode = 2 +text = "Goal" + +[node name="RequirementsLabel" type="Label" parent="VBoxContainer" unique_id=2092922827] +layout_mode = 2 +text = "Requirements" + +[node name="ResolveButton" type="Button" parent="VBoxContainer" unique_id=760904344] +layout_mode = 2 +text = "Resolve" diff --git a/core/goals/goal_catalogue.gd b/core/goals/goal_catalogue.gd new file mode 100644 index 0000000..f736c9f --- /dev/null +++ b/core/goals/goal_catalogue.gd @@ -0,0 +1,17 @@ +class_name GoalCatalogue +extends Resource + +@export var goals: Array[GoalData] = [] + +func get_goal_by_id(id: StringName) -> GoalData: + for goal in goals: + if goal.id == id: + return goal + return null + +func get_all_ids() -> Array[StringName]: + var ids: Array[StringName] = [] + for goal in goals: + if goal.has_id(): + ids.append(goal.id) + return ids diff --git a/core/goals/goal_catalogue.gd.uid b/core/goals/goal_catalogue.gd.uid new file mode 100644 index 0000000..a798104 --- /dev/null +++ b/core/goals/goal_catalogue.gd.uid @@ -0,0 +1 @@ +uid://bfbp4mo8ys5p8 diff --git a/core/goals/goal_data.gd b/core/goals/goal_data.gd index 2ffa672..eef69f2 100644 --- a/core/goals/goal_data.gd +++ b/core/goals/goal_data.gd @@ -1,43 +1,34 @@ class_name GoalData extends Resource +enum UnlockBehavior { + AUTOMATIC, + MANUAL, +} + @export var id: StringName = &"" @export var requirements: Array[GoalRequirementData] = [] +@export var unlock_behavior: UnlockBehavior = UnlockBehavior.MANUAL func has_id() -> bool: return not String(id).strip_edges().is_empty() -func get_valid_requirements() -> Array[GoalRequirementData]: - var result: Array[GoalRequirementData] = [] - for requirement_resource in requirements: - if requirement_resource == null: - continue - if not bool(requirement_resource.is_valid()): - continue - - result.append(requirement_resource) - - return result +func get_requirements() -> Array[GoalRequirementData]: + return requirements.duplicate() func is_valid() -> bool: if not has_id(): return false - - return not get_valid_requirements().is_empty() - -func is_met() -> bool: - var valid_requirements: Array[GoalRequirementData] = get_valid_requirements() - if valid_requirements.is_empty(): + + if requirements.is_empty(): return false - - for requirement_resource in valid_requirements: - if not bool(requirement_resource.is_met()): - return false - + + for req in requirements: + if req == null: + continue + if req.get_currency() == null: + continue + if not req.has_valid_amount(): + continue + return true - -func get_first_requirement_summary() -> String: - var valid_requirements: Array[GoalRequirementData] = get_valid_requirements() - if valid_requirements.is_empty(): - return "" - return String(valid_requirements[0].get_summary_text()) diff --git a/core/goals/goal_requirement_data.gd b/core/goals/goal_requirement_data.gd index eca58ec..cb95bc6 100644 --- a/core/goals/goal_requirement_data.gd +++ b/core/goals/goal_requirement_data.gd @@ -5,38 +5,11 @@ extends Resource @export var amount_mantissa: float = 0.0 @export var amount_exponent: int = 0 -func get_currency_id() -> StringName: - if currency == null: - return &"" - - return GameState.get_currency_id(currency) +func get_currency() -> Currency: + return currency func get_amount() -> BigNumber: return BigNumber.new(amount_mantissa, amount_exponent) -func has_valid_currency() -> bool: - var currency_id: StringName = get_currency_id() - if currency_id == &"": - return false - - return GameState.is_known_currency_id(currency_id) - func has_valid_amount() -> bool: return get_amount().mantissa >= 0.0 - -func is_valid() -> bool: - return has_valid_currency() and has_valid_amount() - -func is_met() -> bool: - if not is_valid(): - return false - - var total_amount: BigNumber = GameState.get_total_currency_acquired_by_id(get_currency_id()) - return not total_amount.is_less_than(get_amount()) - -func get_summary_text() -> String: - if not is_valid(): - return "" - - var currency_name: String = GameState.get_currency_name(get_currency_id()) - return "%s %s" % [get_amount().to_string_suffix(2), currency_name] diff --git a/core/level_game_state.gd b/core/level_game_state.gd new file mode 100644 index 0000000..672ad87 --- /dev/null +++ b/core/level_game_state.gd @@ -0,0 +1,1534 @@ +## Central state authority for the idle game. Manages currencies, generators, buffs, goals, research, and persistence. +## Acts as the single source of truth for all gameplay state, emitting signals for UI and other systems to react. +class_name LevelGameState +extends Node + +# ============================================================================== +# SIGNALS +# ============================================================================== +## Emitted when any currency balance changes. [param currency_id] identifies the currency, [param new_amount] is the updated balance. +signal currency_changed(currency_id: StringName, new_amount: BigNumber) +## Emitted when a generator's state changes (owned count, unlocked status, etc.). +signal generator_state_changed(generator_id: StringName, state: Dictionary) + +## Emitted when a global buff level changes. +signal buff_level_changed(buff_id: StringName, new_level: int) +## Emitted when a global buff unlock state changes. +signal buff_unlocked_changed(buff_id: StringName, unlocked: bool) +## Emitted when a goal is completed. +signal goal_completed(goal_id: StringName) +## Emitted when goal progress changes (for UI progress bars). +signal goal_progress_changed(goal_id: StringName, progress: float) +## Emitted when research XP changes. +signal research_xp_changed(research_id: StringName, new_xp: BigNumber) +## Emitted when a research track levels up. +signal research_level_up(research_id: StringName, old_level: int, new_level: int) +## Emitted when worker count changes (derived from currency balance). +signal worker_count_changed(currency_id: StringName, new_count: int) +## Emitted when research workers change. +signal research_workers_changed(new_count: int) +## Emitted when an prestige buff node is unlocked. +signal prestige_buff_unlocked(buff_id: StringName) + +# ============================================================================== +# EXPORTED REFERENCES +# ============================================================================== +## Catalog of all defined currencies in the game. +@export var currency_catalogue: CurrencyCatalogue +## Catalog of all defined buffs in the game. +@export var buff_catalogue: BuffCatalogue +## Catalog of all defined goals in the game. +@export var goal_catalogue: GoalCatalogue +## Catalog of all defined research tracks in the game. +@export var research_catalogue: ResearchCatalogue +## Catalog of all prestige buff nodes in the graph. +@export var prestige_buff_catalogue: PrestigeBuffCatalogue +## File path for save/load persistence. Defaults to user directory. +@export var save_file_path: String = "user://level_save.json" + +# ============================================================================== +# STATE VARIABLES +# ============================================================================== +## Current balance for each currency (resets on prestige). +var _current_currency: Dictionary = {} +## Total currency acquired this run (resets on prestige). +var _total_currency_acquired: Dictionary = {} +## All-time currency acquired (never resets, persists across prestige). +var _all_time_currency_acquired: Dictionary = {} + +## Generator states (owned count, unlocked status, availability). +var generator_states: Dictionary = {} + +## External save data sections for other systems (e.g., PrestigeManager). +var _external_save_sections: Dictionary = {} + +## Global buff definitions by ID. +var _buff_definitions: Dictionary = {} +## Global buff levels (shared across all targets). +var _buff_levels: Dictionary = {} +## Global buff unlock states. +var _buff_unlocked: Dictionary = {} +## Global buff active states (true when unlocked and active). +var _buff_active: Dictionary = {} + +## Goal definitions by ID. +var _goal_definitions: Dictionary = {} +## Goal completion state (true if completed). +var _goal_completed: Dictionary = {} + +## Research XP by research ID (BigNumber). +var research_xp: Dictionary = {} +## Research levels by research ID. +var research_levels: Dictionary = {} +## Workers assigned to research (total across all tracks). +var research_workers: int = 0 +## Ascension buff unlock state: maps node_id (StringName) → bool (true if unlocked). Persists across prestige. +var _prestige_buff_unlocked: Dictionary = {} + +## Unix timestamp of last save. +var last_save_time: int = 0 + +# ============================================================================== +# SAVE / LOAD CONSTANTS +# ============================================================================== +## JSON key for save format version. +const SAVE_FORMAT_VERSION_KEY: String = "save_format_version" +## Current save format version (7 includes research workers tracking). +const CURRENT_SAVE_FORMAT_VERSION: int = 8 +## JSON key for generator owned count. +const GENERATOR_OWNED_KEY: String = "owned" +## JSON key for generator purchased count. +const GENERATOR_PURCHASED_COUNT_KEY: String = "purchased_count" +## JSON key for generator unlocked status. +const GENERATOR_UNLOCKED_KEY: String = "unlocked" +## JSON key for generator availability. +const GENERATOR_AVAILABLE_KEY: String = "available" +## JSON key for generator states section. +const GENERATOR_STATES_KEY: String = "generator_states" + +## JSON key for buff definitions section. +const BUFF_DEFINITIONS_KEY: String = "buff_definitions" +## JSON key for buff levels section. +const BUFF_LEVELS_KEY: String = "buff_levels" +## JSON key for buff unlocked section. +const BUFF_UNLOCKED_KEY: String = "buff_unlocked" +## JSON key for buff active section. +const BUFF_ACTIVE_KEY: String = "buff_active" +## JSON key for goals section. +const GOALS_KEY: String = "goals" +## JSON key for goal completed array. +const GOAL_COMPLETED_KEY: String = "completed" +## JSON key for currencies section. +const CURRENCIES_KEY: String = "currencies" +## JSON key for current currency balance. +const CURRENT_KEY: String = "current" +## JSON key for total currency acquired. +const TOTAL_KEY: String = "total" +## JSON key for all-time currency acquired. +const ALL_TIME_KEY: String = "all_time" +## JSON key for last save timestamp. +const LAST_SAVE_TIME_KEY: String = "last_save_time" +## JSON key for research XP section. +const RESEARCH_XP_KEY: String = "research_xp" +## JSON key for research levels section. +const RESEARCH_LEVELS_KEY: String = "research_levels" +## JSON key for research workers section. +const RESEARCH_WORKERS_KEY: String = "research_workers" +## JSON key for prestige buff unlocked array. +const PRESTIGE_BUFF_UNLOCKED_KEY: String = "prestige_buff_unlocked" + +## Reference to the PrestigeManager autoload node (set in _ready). +var prestige_manager: PrestigeManager + +## Called when the node enters the scene tree. Initializes all game systems, loads saved data, and restores state. +func _ready() -> void: + prestige_manager = find_child("PrestigeManager") + assert(prestige_manager != null) + + _initialize_currency_maps() + _initialize_catalogues() + _initialize_prestige_buffs() + _load_catalogue_goals() + load_game() + _try_unlock_all_buffs_from_goals() + _initialize_research() + +#region currency + +# ============================================================================== +# CURRENCY API +# ============================================================================== +## Returns the normalized ID from a Currency resource. +func get_currency_id(currency: Currency) -> StringName: + return _normalize_currency_id(currency.id if currency else &"") + +## Parses and normalizes a currency ID from raw input (handles special cases like "gem" -> "gems"). +func parse_currency_id(raw_currency: Variant) -> StringName: + var currency_text: String = String(raw_currency).to_lower().strip_edges() + if currency_text == "gem": + currency_text = "gems" + return _normalize_currency_id(StringName(currency_text)) + +## Checks if a currency ID is known in the catalogue. +func is_known_currency_id(currency_id: StringName) -> bool: + if not currency_catalogue: + return false + var ids: Array[StringName] = currency_catalogue.get_all_ids() + return ids.has(_normalize_currency_id(currency_id)) + +## Returns the Currency resource for a given ID, or null if not found. +func get_currency_resource(currency_id: StringName) -> Currency: + if not currency_catalogue: + return null + return currency_catalogue.get_currency_by_id(_normalize_currency_id(currency_id)) + +## Returns the display name for a currency ID, falling back to a humanized ID if not found. +func get_currency_name(currency_id: StringName) -> String: + var currency: Currency = get_currency_resource(currency_id) + if currency != null: + var display_name: String = String(currency.display_name).strip_edges() + if not display_name.is_empty(): + return display_name + return _humanize_currency_id(currency_id) + +## Returns the icon texture for a currency ID, or null if not found. +func get_currency_icon(currency_id: StringName) -> Texture2D: + var currency: Currency = get_currency_resource(currency_id) + if currency == null: + return null + var raw_icon: Variant = currency.icon + if raw_icon is Texture2D: + return raw_icon + return null + +## Adds currency from a Currency resource. Emits [signal currency_changed] on success. +func add_currency(currency: Currency, amount: BigNumber) -> void: + var currency_id: StringName = get_currency_id(currency) + if currency_id == &"": + push_warning("Attempted to add currency with an invalid Currency resource.") + return + add_currency_by_id(currency_id, amount) + +## Spends currency from a Currency resource. Returns false if insufficient funds. +func spend_currency(currency: Currency, cost: BigNumber) -> bool: + var currency_id: StringName = get_currency_id(currency) + if currency_id == &"": + push_warning("Attempted to spend currency with an invalid Currency resource.") + return false + return spend_currency_by_id(currency_id, cost) + +## Adds currency by ID. Updates current, total, and all-time tracking. Emits [signal currency_changed]. +func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return + + if amount.mantissa > 0.0: + _get_total_currency_ref(normalized_currency_id).add_in_place(amount) + _get_all_time_currency_ref(normalized_currency_id).add_in_place(amount) + + var current: BigNumber = _get_current_currency_ref(normalized_currency_id) + current.add_in_place(amount) + currency_changed.emit(normalized_currency_id, current) + +## Spends currency by ID. Returns true if successful, false if insufficient funds. Emits [signal currency_changed]. +func spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return false + + var current: BigNumber = _get_current_currency_ref(normalized_currency_id) + if current.is_greater_than(cost) or current.is_equal_to(cost): + var negative_cost: BigNumber = BigNumber.new(-cost.mantissa, cost.exponent) + current.add_in_place(negative_cost) + currency_changed.emit(normalized_currency_id, current) + return true + return false + +## Returns the current balance for a Currency resource. +func get_currency_amount(currency: Currency) -> BigNumber: + return get_currency_amount_by_id(get_currency_id(currency)) + +## Returns the current balance for a currency ID. +func get_currency_amount_by_id(currency_id: StringName) -> BigNumber: + return _get_current_currency_ref(currency_id) + +## Returns the total acquired this run for a Currency resource. +func get_total_currency_acquired(currency: Currency) -> BigNumber: + return get_total_currency_acquired_by_id(get_currency_id(currency)) + +## Returns the total acquired this run for a currency ID (resets on prestige). +func get_total_currency_acquired_by_id(currency_id: StringName) -> BigNumber: + return _get_total_currency_ref(currency_id) + +## Returns the all-time total for a Currency resource. +func get_all_time_currency_acquired(currency: Currency) -> BigNumber: + return get_all_time_currency_acquired_by_id(get_currency_id(currency)) + +## Returns the all-time total for a currency ID (never resets). +func get_all_time_currency_acquired_by_id(currency_id: StringName) -> BigNumber: + return _get_all_time_currency_ref(currency_id) + +#endregion + +#region generator +# ============================================================================== +# GENERATOR STATE API +# ============================================================================== +## Registers a generator with the game state. Creates default state if not exists, sanitizes existing state otherwise. +## Emits [signal generator_state_changed] on registration or update. +func register_generator(generator_id: StringName, unlocked: bool = false, available: bool = false, owned: int = 0) -> void: + var key: String = String(generator_id) + if key.is_empty(): + return + var default_owned: int = _to_non_negative_int(owned, 0) + + if generator_states.has(key): + var existing_raw: Variant = generator_states.get(key) + if existing_raw is Dictionary: + generator_states[key] = _sanitize_generator_state(existing_raw, unlocked, available, default_owned) + return + + var new_state: Dictionary = _default_generator_state(unlocked, available, default_owned) + generator_states[key] = new_state + generator_state_changed.emit(StringName(key), new_state.duplicate(true)) + +## Returns a copy of the full state dictionary for a generator. +func get_generator_state(generator_id: StringName) -> Dictionary: + return _get_generator_state(generator_id).duplicate(true) + +## Returns the owned count for a generator. +func get_generator_owned(generator_id: StringName) -> int: + var state: Dictionary = _get_generator_state(generator_id) + return int(state.get(GENERATOR_OWNED_KEY, 0)) + +## Sets the owned count for a generator. Also updates purchased_count to be at least the new owned value. +func set_generator_owned(generator_id: StringName, value: int) -> void: + var state: Dictionary = _get_generator_state(generator_id) + var next_owned: int = maxi(value, 0) + if int(state.get(GENERATOR_OWNED_KEY, 0)) == next_owned: + return + + state[GENERATOR_OWNED_KEY] = next_owned + state[GENERATOR_PURCHASED_COUNT_KEY] = maxi(int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)), next_owned) + _set_generator_state(generator_id, state) + +## Returns the total purchased count for a generator (lifetime purchases, never decreases). +func get_generator_purchased_count(generator_id: StringName) -> int: + var state: Dictionary = _get_generator_state(generator_id) + return int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)) + +## Sets the purchased count for a generator. Ensures it never drops below owned count. +func set_generator_purchased_count(generator_id: StringName, value: int) -> void: + var state: Dictionary = _get_generator_state(generator_id) + var min_purchased: int = int(state.get(GENERATOR_OWNED_KEY, 0)) + var next_purchased: int = maxi(value, min_purchased) + if int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)) == next_purchased: + return + + state[GENERATOR_PURCHASED_COUNT_KEY] = next_purchased + _set_generator_state(generator_id, state) + +## Returns true if a generator is unlocked (visible and accessible to player). +func is_generator_unlocked(generator_id: StringName) -> bool: + var state: Dictionary = _get_generator_state(generator_id) + return bool(state.get(GENERATOR_UNLOCKED_KEY, false)) + +## Sets the unlocked state for a generator. Emits [signal generator_state_changed]. +func set_generator_unlocked(generator_id: StringName, value: bool) -> void: + var state: Dictionary = _get_generator_state(generator_id) + if bool(state.get(GENERATOR_UNLOCKED_KEY, false)) == value: + return + + state[GENERATOR_UNLOCKED_KEY] = value + _set_generator_state(generator_id, state) + +## Returns true if a generator is available (may differ from unlocked for goal-locked generators). +func is_generator_available(generator_id: StringName) -> bool: + var state: Dictionary = _get_generator_state(generator_id) + return bool(state.get(GENERATOR_AVAILABLE_KEY, false)) + +## Sets the availability state for a generator. Emits [signal generator_state_changed]. +func set_generator_available(generator_id: StringName, value: bool) -> void: + var state: Dictionary = _get_generator_state(generator_id) + if bool(state.get(GENERATOR_AVAILABLE_KEY, false)) == value: + return + + state[GENERATOR_AVAILABLE_KEY] = value + _set_generator_state(generator_id, state) + +#endregion + + +#region buff +# ============================================================================== +# BUFF API +# ============================================================================== +## Registers a global buff definition for runtime lookup. Initializes level, unlock, and active state. +func register_buff(buff: GeneratorBuffData) -> void: + if buff == null: + return + var buff_id: StringName = buff.id + if buff_id == &"": + push_warning("Attempted to register buff with invalid id") + return + + _buff_definitions[buff_id] = buff + if not _buff_levels.has(buff_id): + _buff_levels[buff_id] = 0 + if not _buff_unlocked.has(buff_id): + _buff_unlocked[buff_id] = false + if not _buff_active.has(buff_id): + _buff_active[buff_id] = false + +## Returns the buff definition for a given ID, or null if not registered. +func get_buff(buff_id: StringName) -> GeneratorBuffData: + return _buff_definitions.get(buff_id, null) + +## Returns all registered global buffs as an array. +func get_all_buffs() -> Array[GeneratorBuffData]: + var result: Array[GeneratorBuffData] = [] + for buff in _buff_definitions.values(): + if buff is GeneratorBuffData: + result.append(buff) + return result + +## Returns the current level of a global buff (0 if not registered). +func get_buff_level(buff_id: StringName) -> int: + return _buff_levels.get(buff_id, 0) + +## Sets the level of a global buff. Emits [signal buff_level_changed]. +func set_buff_level(buff_id: StringName, level: int) -> void: + if level < 0: + level = 0 + + var current_level: int = _buff_levels.get(buff_id, 0) + if current_level == level: + return + + _buff_levels[buff_id] = level + buff_level_changed.emit(buff_id, level) + +## Returns true if a global buff is unlocked. +func is_buff_unlocked(buff_id: StringName) -> bool: + return _buff_unlocked.get(buff_id, false) + +## Sets the unlock state for a global buff. Emits [signal buff_unlocked_changed] and activates the buff if unlocked. +func set_buff_unlocked(buff_id: StringName, unlocked: bool) -> void: + if bool(_buff_unlocked.get(buff_id, false)) == unlocked: + return + + _buff_unlocked[buff_id] = unlocked + buff_unlocked_changed.emit(buff_id, unlocked) + + if unlocked and not _buff_active.get(buff_id, false): + _buff_active[buff_id] = true + +## Returns true if a global buff is currently active (unlocked and applied). +func is_buff_active(buff_id: StringName) -> bool: + return _buff_active.get(buff_id, false) + +## Returns the list of generators targeted by a global buff. +func get_generators_targeted_by(buff_id: StringName) -> Array[StringName]: + var buff: GeneratorBuffData = get_buff(buff_id) + if buff == null: + return [] + return buff.get_target_generators() + +## Returns all buffs that affect a specific generator. +func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]: + if buff_catalogue: + return buff_catalogue.get_buffs_for_generator(generator_id) + + var result: Array[GeneratorBuffData] = [] + for buff in _buff_definitions.values(): + if buff is GeneratorBuffData and buff.targets_generator(generator_id): + result.append(buff) + return result + +## Calculates the effective production multiplier for a generator by combining all active buffs of the specified kind. +func get_effective_multiplier(generator_id: StringName, kind: int) -> float: + var multiplier: float = 1.0 + for buff in get_buffs_for_generator(generator_id): + if buff == null: + continue + if not is_buff_active(buff.id): + continue + if buff.kind != kind: + continue + + var level: int = get_buff_level(buff.id) + if level <= 0: + continue + + var buff_multiplier: float = buff.get_effect_multiplier(level) + multiplier *= buff_multiplier + + return multiplier +#endregion + +#region research +# ============================================================================== +# RESEARCH API +# ============================================================================== +## Initializes all research tracks from the catalogue on game start. +func _initialize_research() -> void: + if research_catalogue: + for research in research_catalogue.get_all_research(): + register_research(research.id) + +## Registers a research track, initializing XP and level storage. +func register_research(research_id: StringName) -> void: + if not research_xp.has(research_id): + research_xp[research_id] = BigNumber.new(0.0, 0) + if not research_levels.has(research_id): + research_levels[research_id] = 0 + +## Returns the current XP for a research track. +func get_research_xp(research_id: StringName) -> BigNumber: + var val: Variant = research_xp.get(research_id, BigNumber.new(0.0, 0)) + return val as BigNumber + +## Returns the current level for a research track. +func get_research_level(research_id: StringName) -> int: + return research_levels.get(research_id, 0) + +## Returns the production multiplier provided by a research track at its current level. +func get_research_multiplier(research_id: StringName) -> float: + var level: int = get_research_level(research_id) + var research: ResearchData = _get_research_data(research_id) + if research == null: + return 1.0 + return research.get_multiplier_for_level(level) + +## Returns the total number of workers assigned to research. +func get_research_workers() -> int: + return research_workers + +## Assigns a worker to research (called when spending worker currency on research). +func assign_worker_to_research() -> bool: + var worker_currency: BigNumber = get_currency_amount_by_id("worker") + if worker_currency.mantissa < 1.0: + return false + + spend_currency_by_id("worker", BigNumber.from_float(1.0)) + research_workers += 1 + research_workers_changed.emit(research_workers) + return true + +## Removes a worker from research (returns to worker currency). +func remove_worker_from_research() -> bool: + if research_workers <= 0: + return false + + research_workers -= 1 + add_currency_by_id("worker", BigNumber.from_float(1.0)) + research_workers_changed.emit(research_workers) + return true + +## Adds XP to a research track, applying buff modifiers and emitting signals on level up. +func add_research_xp(research_id: StringName, xp: BigNumber) -> void: + var actual_xp: BigNumber = ResearchBuffCalculator.apply_buffs(self, research_id, xp) + + var old_level: int = get_research_level(research_id) + var current_xp: BigNumber = get_research_xp(research_id) + research_xp[research_id] = current_xp.add(actual_xp) + + var research: ResearchData = _get_research_data(research_id) + if research != null: + var new_level: int = research.get_level_for_xp(research_xp[research_id]) + + if new_level > old_level: + research_levels[research_id] = new_level + research_level_up.emit(research_id, old_level, new_level) + + research_xp_changed.emit(research_id, research_xp[research_id]) + +## Retrieves the ResearchData resource for a given ID from the catalogue. +func _get_research_data(research_id: StringName) -> ResearchData: + if research_catalogue == null: + return null + return research_catalogue.get_research_by_id(research_id) + +## Returns the current worker count (derived from "worker" currency balance). +func get_worker_count() -> int: + var worker_amount: BigNumber = get_currency_amount_by_id("worker") + return int(worker_amount.mantissa) if worker_amount.mantissa >= 0 else 0 + +#endregion + +# ============================================================================== +# EXTERNAL SAVE DATA API +# ============================================================================== +## Stores external save data from other systems (e.g., PrestigeManager). Creates or overwrites the section. +func set_external_save_data(section_key: StringName, payload: Dictionary) -> void: + var key: String = String(section_key).strip_edges() + if key.is_empty(): + return + + _external_save_sections[key] = payload.duplicate(true) + +## Retrieves a saved data section by key. Returns empty dict if not found. +func get_external_save_data(section_key: StringName) -> Dictionary: + var key: String = String(section_key).strip_edges() + if key.is_empty(): + return {} + + var raw_payload: Variant = _external_save_sections.get(key, {}) + if raw_payload is Dictionary: + return raw_payload.duplicate(true) + + return {} + +# ============================================================================== +# PRESTIGE RESET API +# ============================================================================== +## Resets all progress for a prestige reset. Optionally resets total currency, preserves specified currencies, and controls signal emission. +func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signals: bool = true, preserve_currency_ids: Array[StringName] = []) -> void: + var currency_ids: Array[StringName] = _collect_currency_ids_for_save() + for currency_id in currency_ids: + if currency_id in preserve_currency_ids: + continue + _set_current_currency(currency_id, BigNumber.from_float(0.0)) + if reset_total_currency: + _set_total_currency(currency_id, BigNumber.from_float(0.0)) + + generator_states.clear() + + for goal_id in _goal_definitions.keys(): + _goal_completed[goal_id] = false + + var research_ids: Array = research_xp.keys() + research_xp.clear() + research_levels.clear() + + for research_id in research_ids: + research_xp_changed.emit(research_id, BigNumber.from_float(0.0)) + + if not emit_currency_signals: + return + + for currency_id in currency_ids: + currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) + +## Internal helper: Emits currency_changed signals for all currencies to refresh UI after a prestige load. +func emit_currency_changed_for_all() -> void: + for currency_id in _collect_currency_ids_for_save(): + currency_changed.emit(currency_id, _get_current_currency_ref(currency_id)) + +# ========================================== +# PERSISTENCE (Save/Load) +# ========================================== +## Saves all game state to disk as JSON. +func save_game() -> void: + if save_file_path.is_empty(): + push_warning("LevelGameState: save_file_path is empty; skipping save.") + return + + var save_data = { + SAVE_FORMAT_VERSION_KEY: CURRENT_SAVE_FORMAT_VERSION, + CURRENCIES_KEY: _serialize_currency_balances(), + GENERATOR_STATES_KEY: _serialize_generator_states(), + BUFF_LEVELS_KEY: _serialize_buff_levels(), + BUFF_UNLOCKED_KEY: _serialize_buff_unlocked(), + BUFF_ACTIVE_KEY: _serialize_buff_active(), + GOALS_KEY: _serialize_goals(), + RESEARCH_XP_KEY: _serialize_research_xp(), + RESEARCH_LEVELS_KEY: research_levels.duplicate(), + RESEARCH_WORKERS_KEY: research_workers, + PRESTIGE_BUFF_UNLOCKED_KEY: _serialize_prestige_buff_unlocked(), + LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system() + } + + for section_key in _external_save_sections.keys(): + var key: String = String(section_key) + if key.is_empty(): + continue + + var payload: Variant = _external_save_sections.get(section_key) + if not (payload is Dictionary): + continue + + save_data[key] = payload + + var file: FileAccess = FileAccess.open(save_file_path, FileAccess.WRITE) + if file: + file.store_string(JSON.stringify(save_data)) + else: + push_error("LevelGameState: Failed to open save file: %s" % save_file_path) + +## Loads all game state from disk JSON. +func load_game() -> void: + if not FileAccess.file_exists(save_file_path): + return + + var file: FileAccess = FileAccess.open(save_file_path, FileAccess.READ) + if file: + var parsed: Variant = JSON.parse_string(file.get_as_text()) + if parsed is Dictionary: + var parsed_data: Dictionary = parsed + + var version: int = int(parsed_data.get(SAVE_FORMAT_VERSION_KEY, 1)) + if version < 2: + push_error("Save file version %d too old. Please start fresh." % version) + return + + if version < 3: + _migrate_goals_from_version_2(parsed_data) + + _external_save_sections.clear() + if parsed_data.has(CURRENCIES_KEY): + _load_currency_balances(parsed_data.get(CURRENCIES_KEY, {})) + + generator_states = _deserialize_generator_states(parsed_data.get(GENERATOR_STATES_KEY, {})) + _deserialize_buff_states(parsed_data) + _deserialize_goals(parsed_data.get(GOALS_KEY, {})) + + if parsed_data.has(RESEARCH_XP_KEY): + research_xp = _deserialize_research_xp(parsed_data.get(RESEARCH_XP_KEY, {})) + if parsed_data.has(RESEARCH_LEVELS_KEY): + research_levels = parsed_data.get(RESEARCH_LEVELS_KEY, {}) + if parsed_data.has(RESEARCH_WORKERS_KEY): + research_workers = int(parsed_data.get(RESEARCH_WORKERS_KEY, 0)) + + last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system()) + if parsed_data.has(PRESTIGE_BUFF_UNLOCKED_KEY): + _deserialize_prestige_buff_unlocked(parsed_data.get(PRESTIGE_BUFF_UNLOCKED_KEY)) + for save_key_variant in parsed_data.keys(): + var save_key: String = String(save_key_variant) + if save_key.is_empty(): + continue + if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, GOALS_KEY, RESEARCH_XP_KEY, RESEARCH_LEVELS_KEY, RESEARCH_WORKERS_KEY, PRESTIGE_BUFF_UNLOCKED_KEY, LAST_SAVE_TIME_KEY]: + continue + + var section_payload: Variant = parsed_data.get(save_key_variant) + if section_payload is Dictionary: + _external_save_sections[save_key] = section_payload.duplicate(true) + +## Internal helper: Loads currency balances from deserialized save data. +func _load_currency_balances(raw_currencies: Variant) -> void: + if not (raw_currencies is Dictionary): + return + + var currencies: Dictionary = raw_currencies + for raw_currency_id in currencies.keys(): + var currency_id: StringName = _normalize_currency_id(StringName(String(raw_currency_id))) + if currency_id == &"": + continue + + var raw_entry: Variant = currencies.get(raw_currency_id) + if not (raw_entry is Dictionary): + continue + + var entry: Dictionary = raw_entry + var current_amount: BigNumber = BigNumber.deserialize(entry.get(CURRENT_KEY, {})) + var total_amount: BigNumber = _deserialize_total_currency(entry.get(TOTAL_KEY, null), current_amount) + var all_time_amount: BigNumber = _deserialize_all_time_currency(entry.get(ALL_TIME_KEY, null), total_amount) + _set_current_currency(currency_id, current_amount) + _set_total_currency(currency_id, total_amount) + _set_all_time_currency(currency_id, all_time_amount) + +## Internal helper: Deserializes total currency from save data with validation. +func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) -> BigNumber: + var minimum_total: BigNumber = BigNumber.new(current_amount.mantissa, current_amount.exponent) + if minimum_total.mantissa < 0.0: + minimum_total = BigNumber.from_float(0.0) + + if not (raw_total is Dictionary): + return minimum_total + + var parsed_total: BigNumber = BigNumber.deserialize(raw_total) + if parsed_total.mantissa < 0.0: + return minimum_total + if parsed_total.is_less_than(minimum_total): + return minimum_total + return parsed_total + +## Internal helper: Deserializes all-time currency from save data with validation. +func _deserialize_all_time_currency(raw_all_time: Variant, total_amount: BigNumber) -> BigNumber: + var minimum_all_time: BigNumber = BigNumber.new(total_amount.mantissa, total_amount.exponent) + if minimum_all_time.mantissa < 0.0: + minimum_all_time = BigNumber.from_float(0.0) + + if not (raw_all_time is Dictionary): + return minimum_all_time + + var parsed_all_time: BigNumber = BigNumber.deserialize(raw_all_time) + if parsed_all_time.mantissa < 0.0: + return minimum_all_time + if parsed_all_time.is_less_than(minimum_all_time): + return minimum_all_time + return parsed_all_time + +## Internal helper: Serializes all currency balances for saving. +func _serialize_currency_balances() -> Dictionary: + var result: Dictionary = {} + + for currency_id in _collect_currency_ids_for_save(): + var key: String = String(currency_id) + if key.is_empty(): + continue + + result[key] = { + CURRENT_KEY: _get_current_currency_ref(currency_id).serialize(), + TOTAL_KEY: _get_total_currency_ref(currency_id).serialize(), + ALL_TIME_KEY: _get_all_time_currency_ref(currency_id).serialize(), + } + + return result + +## Internal helper: Collects all currency IDs for serialization. +func _collect_currency_ids_for_save() -> Array[StringName]: + var ids: Array[StringName] = [] + + for raw_id in _current_currency.keys(): + var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id))) + if normalized_currency_id == &"" or ids.has(normalized_currency_id): + continue + ids.append(normalized_currency_id) + + for raw_id in _all_time_currency_acquired.keys(): + var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id))) + if normalized_currency_id == &"" or ids.has(normalized_currency_id): + continue + ids.append(normalized_currency_id) + + if currency_catalogue: + for known_currency_id in currency_catalogue.get_all_ids(): + var normalized_currency_id: StringName = _normalize_currency_id(known_currency_id) + if normalized_currency_id == &"" or ids.has(normalized_currency_id): + continue + ids.append(normalized_currency_id) + + return ids + +## Internal helper: Initializes all currency maps from the catalogue. +func _initialize_currency_maps() -> void: + if currency_catalogue: + for currency_id in currency_catalogue.get_all_ids(): + if currency_id == &"": + continue + _set_current_currency(currency_id, BigNumber.from_float(0.0)) + _set_total_currency(currency_id, BigNumber.from_float(0.0)) + _set_all_time_currency(currency_id, BigNumber.from_float(0.0)) + +## Internal helper: Initializes all catalogues (buffs, goals). +func _initialize_catalogues() -> void: + if currency_catalogue: + for currency in currency_catalogue.currencies: + if currency == null: + continue + + if buff_catalogue: + for buff in buff_catalogue.buffs: + if buff == null: + continue + register_buff(buff) + + if goal_catalogue: + for goal in goal_catalogue.goals: + if goal == null: + continue + register_goal(goal) + +## Internal helper: Initializes prestige buff unlock state from the catalogue, filling missing node IDs as false. +func _initialize_prestige_buffs() -> void: + if prestige_buff_catalogue == null: + return + for node in prestige_buff_catalogue.nodes: + if node == null or node.id == &"": + continue + if not _prestige_buff_unlocked.has(node.id): + _prestige_buff_unlocked[node.id] = false + +## Internal helper: Loads goal definitions from the catalogue. +func _load_catalogue_goals() -> void: + if goal_catalogue: + for goal in goal_catalogue.goals: + if goal == null: + continue + if not goal.has_id(): + continue + if _goal_definitions.has(goal.id): + continue + _goal_definitions[goal.id] = goal + _goal_completed[goal.id] = false + +# ============================================================================== +# GOAL EVALUATION HELPERS +# ============================================================================== +## Internal helper: Checks if a goal requirement's currency amount has been reached. +func is_goal_requirement_met(requirement: GoalRequirementData) -> bool: + if requirement == null: + return false + + var currency: Currency = requirement.get_currency() + if currency == null: + return false + + var currency_id: StringName = get_currency_id(currency) + if currency_id == &"": + return false + + var total_amount: BigNumber = get_total_currency_acquired_by_id(currency_id) + var required_amount: BigNumber = requirement.get_amount() + + return not total_amount.is_less_than(required_amount) + +## Internal helper: Returns progress (0.0-1.0) for a goal requirement using logarithmic scaling for large numbers. +func get_goal_requirement_progress(requirement: GoalRequirementData) -> float: + if requirement == null: + return 0.0 + + var currency: Currency = requirement.get_currency() + if currency == null: + return 0.0 + + var currency_id: StringName = get_currency_id(currency) + if currency_id == &"": + return 0.0 + + var current_amount: BigNumber = get_total_currency_acquired_by_id(currency_id) + var required_amount: BigNumber = requirement.get_amount() + + if required_amount.mantissa <= 0.0: + return 1.0 + + if current_amount.is_greater_than(required_amount) or current_amount.is_equal_to(required_amount): + return 1.0 + + if current_amount.mantissa <= 0.0: + return 0.0 + + var current_log: float = log(maxf(current_amount.mantissa, 0.0001)) / log(10) + float(current_amount.exponent) + var required_log: float = log(maxf(required_amount.mantissa, 0.0001)) / log(10) + float(required_amount.exponent) + + if required_log <= 0.0: + return 1.0 + + var progress: float = current_log / required_log + return clampf(progress, 0.0, 1.0) + +## Internal helper: Returns a human-readable summary string for a goal requirement (amount + currency name). +func get_goal_requirement_summary(requirement: GoalRequirementData) -> String: + if requirement == null: + return "" + + var currency: Currency = requirement.get_currency() + if currency == null: + return "" + + var currency_id: StringName = get_currency_id(currency) + if currency_id == &"": + return "" + + var currency_name: String = get_currency_name(currency_id) + return "%s %s" % [requirement.get_amount().to_string_suffix(2), currency_name] + +## Internal helper: Returns true if all requirements for a goal are met. +func is_goal_met(goal: GoalData) -> bool: + if goal == null: + return false + + for req in goal.get_requirements(): + if not is_goal_requirement_met(req): + return false + + return true + +## Internal helper: Returns the overall progress (0.0-1.0) for a goal based on its slowest requirement. +func get_goal_progress(goal: GoalData) -> float: + if goal == null: + return 0.0 + + var requirements: Array[GoalRequirementData] = goal.get_requirements() + if requirements.is_empty(): + return 0.0 + + var min_progress: float = 1.0 + for req in requirements: + if req == null: + continue + + var req_progress: float = get_goal_requirement_progress(req) + if req_progress < min_progress: + min_progress = req_progress + + return min_progress + +## Internal helper: Normalizes a currency ID (lowercase, trimmed). +func _normalize_currency_id(currency_id: StringName) -> StringName: + var normalized: String = String(currency_id).to_lower().strip_edges() + if normalized.is_empty(): + return &"" + return StringName(normalized) + +## Internal helper: Gets or creates a reference to the current currency balance. +func _get_current_currency_ref(currency_id: StringName) -> BigNumber: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return BigNumber.from_float(0.0) + + var value: Variant = _current_currency.get(normalized_currency_id) + if value is BigNumber: + return value + + var fallback: BigNumber = BigNumber.from_float(0.0) + _current_currency[normalized_currency_id] = fallback + return fallback + +## Internal helper: Gets or creates a reference to the total currency acquired this run. +func _get_total_currency_ref(currency_id: StringName) -> BigNumber: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return BigNumber.from_float(0.0) + + var value: Variant = _total_currency_acquired.get(normalized_currency_id) + if value is BigNumber: + return value + + var fallback: BigNumber = BigNumber.from_float(0.0) + _total_currency_acquired[normalized_currency_id] = fallback + return fallback + +## Internal helper: Gets or creates a reference to the all-time currency acquired. +func _get_all_time_currency_ref(currency_id: StringName) -> BigNumber: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return BigNumber.from_float(0.0) + + var value: Variant = _all_time_currency_acquired.get(normalized_currency_id) + if value is BigNumber: + return value + + var fallback: BigNumber = BigNumber.from_float(0.0) + _all_time_currency_acquired[normalized_currency_id] = fallback + return fallback + +## Internal helper: Sets the current currency balance. +func _set_current_currency(currency_id: StringName, value: BigNumber) -> void: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return + _current_currency[normalized_currency_id] = BigNumber.new(value.mantissa, value.exponent) + +## Internal helper: Sets the total currency acquired this run. +func _set_total_currency(currency_id: StringName, value: BigNumber) -> void: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return + _total_currency_acquired[normalized_currency_id] = BigNumber.new(value.mantissa, value.exponent) + +## Internal helper: Sets the all-time currency acquired. +func _set_all_time_currency(currency_id: StringName, value: BigNumber) -> void: + var normalized_currency_id: StringName = _normalize_currency_id(currency_id) + if normalized_currency_id == &"": + return + _all_time_currency_acquired[normalized_currency_id] = BigNumber.new(value.mantissa, value.exponent) + +# ============================================================================== +# GENERATOR STATE HELPERS +# ============================================================================== +## Internal helper: Creates a default generator state dictionary with the specified values. +func _default_generator_state(unlocked: bool, available: bool, owned: int = 0) -> Dictionary: + var owned_value: int = _to_non_negative_int(owned, 0) + return { + GENERATOR_OWNED_KEY: owned_value, + GENERATOR_PURCHASED_COUNT_KEY: owned_value, + GENERATOR_UNLOCKED_KEY: unlocked, + GENERATOR_AVAILABLE_KEY: available, + } + +## Internal helper: Gets or creates the state dictionary for a generator, ensuring it exists and is valid. +func _get_generator_state(generator_id: StringName) -> Dictionary: + var key: String = String(generator_id) + if key.is_empty(): + return _default_generator_state(false, false, 0) + + if not generator_states.has(key): + register_generator(generator_id) + + var existing_raw: Variant = generator_states.get(key, _default_generator_state(false, false, 0)) + if existing_raw is Dictionary: + var state: Dictionary = existing_raw + var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false) + var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false) + var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0) + var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned) + generator_states[key] = sanitized + return sanitized + + var fallback: Dictionary = _default_generator_state(false, false, 0) + generator_states[key] = fallback + return fallback + +## Internal helper: Updates a generator's state dictionary and emits the change signal. +func _set_generator_state(generator_id: StringName, state: Dictionary) -> void: + var key: String = String(generator_id) + if key.is_empty(): + return + + var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false) + var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false) + var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0) + var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned) + generator_states[key] = sanitized + generator_state_changed.emit(StringName(key), sanitized.duplicate(true)) + +# ============================================================================== +# GENERATOR SERIALIZATION HELPERS +# ============================================================================== +## Internal helper: Serializes all generator states for saving. +func _serialize_generator_states() -> Dictionary: + var result: Dictionary = {} + for key_variant in generator_states.keys(): + var key: String = String(key_variant) + if key.is_empty(): + continue + + var value: Variant = generator_states.get(key_variant) + if value is Dictionary: + result[key] = _serialize_generator_state_entry(value) + return result + +## Internal helper: Deserializes generator states from save data. +func _deserialize_generator_states(raw_value: Variant) -> Dictionary: + var result: Dictionary = {} + if not (raw_value is Dictionary): + return result + + for key_variant in raw_value.keys(): + var key: String = String(key_variant) + if key.is_empty(): + continue + + var entry: Variant = raw_value.get(key_variant) + if entry is Dictionary: + result[key] = _deserialize_generator_state_entry(entry) + return result + + +## Internal helper: Serializes a single generator state entry for saving. +func _serialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: + var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0) + var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) + + var serialized_state: Dictionary = { + GENERATOR_OWNED_KEY: owned_value, + GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), + } + + if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool: + serialized_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY) + if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool: + serialized_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY) + + return serialized_state + +## Internal helper: Deserializes a single generator state entry from save data. +func _deserialize_generator_state_entry(raw_state: Dictionary) -> Dictionary: + var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0) + var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) + + var parsed_state: Dictionary = { + GENERATOR_OWNED_KEY: owned_value, + GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), + } + + if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool: + parsed_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY) + if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool: + parsed_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY) + + return parsed_state + +## Internal helper: Sanitizes a generator state dictionary with default values. +func _sanitize_generator_state(raw_state: Dictionary, default_unlocked: bool, default_available: bool, default_owned: int = 0) -> Dictionary: + var normalized_default_owned: int = _to_non_negative_int(default_owned, 0) + var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, normalized_default_owned), normalized_default_owned) + var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value) + + return { + GENERATOR_OWNED_KEY: owned_value, + GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value), + GENERATOR_UNLOCKED_KEY: _to_bool(raw_state.get(GENERATOR_UNLOCKED_KEY, default_unlocked), default_unlocked), + GENERATOR_AVAILABLE_KEY: _to_bool(raw_state.get(GENERATOR_AVAILABLE_KEY, default_available), default_available), + } + +## Internal helper: Converts a value to a non-negative integer. +func _to_non_negative_int(value: Variant, fallback: int = 0) -> int: + if value is int: + return maxi(value, 0) + if value is float: + return maxi(int(value), 0) + return maxi(fallback, 0) + +## Internal helper: Converts a value to a boolean with a fallback. +func _to_bool(value: Variant, fallback: bool) -> bool: + if value is bool: + return value + return fallback + +## Internal helper: Serializes all global buff levels for saving. +func _serialize_buff_levels() -> Dictionary: + var result: Dictionary = {} + for buff_id in _buff_levels.keys(): + result[buff_id] = _buff_levels[buff_id] + return result + +## Internal helper: Serializes all global buff unlock states for saving. +func _serialize_buff_unlocked() -> Dictionary: + var result: Dictionary = {} + for buff_id in _buff_unlocked.keys(): + if _buff_unlocked[buff_id]: + result[buff_id] = true + return result + +## Internal helper: Serializes all global buff active states for saving. +func _serialize_buff_active() -> Dictionary: + var result: Dictionary = {} + for buff_id in _buff_active.keys(): + if _buff_active[buff_id]: + result[buff_id] = true + return result + +## Internal helper: Deserializes all global buff states from save data. +func _deserialize_buff_states(parsed_data: Dictionary) -> void: + _buff_levels.clear() + _buff_unlocked.clear() + _buff_active.clear() + + if parsed_data.has(BUFF_LEVELS_KEY): + var raw_levels: Variant = parsed_data.get(BUFF_LEVELS_KEY) + if raw_levels is Dictionary: + for key_variant in raw_levels.keys(): + var key: String = String(key_variant) + if not key.is_empty(): + var value: Variant = raw_levels.get(key_variant) + if value is int: + _buff_levels[key] = value + + if parsed_data.has(BUFF_UNLOCKED_KEY): + var raw_unlocked: Variant = parsed_data.get(BUFF_UNLOCKED_KEY) + if raw_unlocked is Dictionary: + for key_variant in raw_unlocked.keys(): + var key: String = String(key_variant) + if not key.is_empty(): + _buff_unlocked[key] = true + + if parsed_data.has(BUFF_ACTIVE_KEY): + var raw_active: Variant = parsed_data.get(BUFF_ACTIVE_KEY) + if raw_active is Dictionary: + for key_variant in raw_active.keys(): + var key: String = String(key_variant) + if not key.is_empty(): + _buff_active[key] = true + +# ============================================================================== +# GOALS API +# ============================================================================== +## Registers a goal definition and initializes its completion state. +func register_goal(goal: GoalData) -> void: + if goal == null: + return + var goal_id: StringName = goal.id + if goal_id == &"": + push_warning("Attempted to register goal with invalid id") + return + if _goal_definitions.has(goal_id): + return + _goal_definitions[goal_id] = goal + _goal_completed[goal_id] = false + +## Returns the goal definition for a given ID, or null if not registered. +func get_goal(goal_id: StringName) -> GoalData: + return _goal_definitions.get(goal_id, null) + +## Returns all registered goals as an array. +func get_all_goals() -> Array[GoalData]: + var result: Array[GoalData] = [] + for goal in _goal_definitions.values(): + if goal is GoalData: + result.append(goal) + return result + +## Returns true if a goal has been completed. +func is_goal_completed(goal_id: StringName) -> bool: + return _goal_completed.get(goal_id, false) + +## Returns the progress (0.0-1.0) for a goal by ID. +func get_goal_progress_by_id(goal_id: StringName) -> float: + var goal: GoalData = get_goal(goal_id) + if goal == null: + return 0.0 + return get_goal_progress(goal) + +## Unlocks a single buff if its unlock goal is completed. +func try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void: + if buff == null: + return + if not buff.has_unlock_goal(): + return + if is_buff_unlocked(buff.id): + return + var goal_id: StringName = buff.unlock_goal.id + if not goal_id.is_empty() and is_goal_completed(goal_id): + set_buff_unlocked(buff.id, true) + +## Evaluates all goals for completion and unlocks buffs from completed goals. Emits [signal goal_progress_changed]. +func evaluate_all_goals() -> void: + for goal_id in _goal_completed.keys(): + if not _goal_completed[goal_id]: + _try_complete_goal(goal_id) + + _try_unlock_all_buffs_from_goals() + + goal_progress_changed.emit(&"", 0.0) + +## Internal helper: Attempts to complete a goal if its requirements are met and unlock behavior allows auto-completion. +func _try_complete_goal(goal_id: StringName) -> void: + var goal: GoalData = _goal_definitions.get(goal_id, null) + if goal == null: + return + if _goal_completed[goal_id]: + return + if is_goal_met(goal): + if goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL: + return + _goal_completed[goal_id] = true + goal_completed.emit(goal_id) + +## Internal helper: Manually completes a goal (for MANUAL unlock behavior). Emits [signal goal_completed]. +func _complete_goal_manually(goal_id: StringName) -> void: + var goal: GoalData = _goal_definitions.get(goal_id, null) + if goal == null: + return + if _goal_completed.get(goal_id, false): + return + if not is_goal_met(goal): + return + _goal_completed[goal_id] = true + goal_completed.emit(goal_id) + +## Internal helper: Unlocks all buffs from completed goals. +func _try_unlock_all_buffs_from_goals() -> void: + for buff in get_all_buffs(): + if buff == null: + continue + try_unlock_buff_from_goal(buff) + + + +# ============================================================================== +# PRESTIGE BUFF API +# ============================================================================== + +## Returns the ascension currency ID used for purchasing buff nodes. +func get_prestige_currency_id() -> StringName: + return &"ascension" + +## Attempts to purchase and unlock an prestige buff node. Returns true on success. +func purchase_prestige_buff(buff_id: StringName) -> bool: + if not can_purchase_prestige_buff(buff_id): + return false + + var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id) + if node == null: + return false + + var cost: BigNumber = node.get_cost() + var currency_id: StringName = get_prestige_currency_id() + if not spend_currency_by_id(currency_id, cost): + return false + + _prestige_buff_unlocked[buff_id] = true + prestige_buff_unlocked.emit(buff_id) + save_game() + return true + +## Checks whether an prestige buff node can be purchased (all parents unlocked, not already unlocked, enough currency). +func can_purchase_prestige_buff(buff_id: StringName) -> bool: + var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id) + if node == null: + return false + + if is_prestige_buff_unlocked(buff_id): + return false + + if not node.all_parents_unlocked(_prestige_buff_unlocked): + return false + + var cost: BigNumber = node.get_cost() + var currency_id: StringName = get_prestige_currency_id() + var balance: BigNumber = get_currency_amount_by_id(currency_id) + return balance.is_greater_than(cost) or balance.is_equal_to(cost) + +## Returns true if the given prestige buff node is unlocked. +func is_prestige_buff_unlocked(buff_id: StringName) -> bool: + return bool(_prestige_buff_unlocked.get(buff_id, false)) + +## Returns the fixed BigNumber cost for an prestige buff node. +func get_prestige_buff_cost(buff_id: StringName) -> BigNumber: + var node: PrestigeBuffNode = _get_prestige_buff_node(buff_id) + if node == null: + return BigNumber.from_float(0.0) + return node.get_cost() + +## Computes the combined multiplier from all unlocked prestige buff nodes matching the given effect type. +## [param target_id] filters to nodes targeting this ID (or global nodes when empty). +func get_prestige_buff_multiplier(effect_type: int, target_id: StringName = &"") -> float: + var multiplier: float = 1.0 + for node_id in _prestige_buff_unlocked.keys(): + if not bool(_prestige_buff_unlocked.get(node_id, false)): + continue + var node: PrestigeBuffNode = _get_prestige_buff_node(node_id) + if node == null: + continue + if node.effect_type != effect_type: + continue + if target_id != &"" and node.target_id != &"" and node.target_id != target_id: + continue + multiplier *= node.effect_value + return multiplier + +## Returns all prestige buff nodes whose prerequisites are met but are not yet unlocked. +func get_available_prestige_buffs() -> Array[PrestigeBuffNode]: + if prestige_buff_catalogue == null: + return [] + + var available: Array[PrestigeBuffNode] = [] + for node in prestige_buff_catalogue.nodes: + if node == null or node.id == &"": + continue + if is_prestige_buff_unlocked(node.id): + continue + if node.all_parents_unlocked(_prestige_buff_unlocked): + available.append(node) + return available + +## Returns the PrestigeBuffNode resource for a given ID, or null if not found. +func _get_prestige_buff_node(buff_id: StringName) -> PrestigeBuffNode: + if prestige_buff_catalogue == null: + return null + return prestige_buff_catalogue.get_node_by_id(buff_id) + +# ============================================================================== +# PERSISTENCE HELPERS +# ============================================================================== +## Serializes goal completion state for saving. +func _serialize_goals() -> Dictionary: + var result: Dictionary = {} + var completed_goals: Array[StringName] = [] + for goal_id in _goal_completed.keys(): + if _goal_completed[goal_id]: + completed_goals.append(goal_id) + result[GOAL_COMPLETED_KEY] = completed_goals + return result + +## Deserializes goal completion state from save data. +func _deserialize_goals(raw_value: Variant) -> void: + _goal_completed.clear() + + if not (raw_value is Dictionary): + return + + var raw_completed: Variant = raw_value.get(GOAL_COMPLETED_KEY, []) + if raw_completed is Array: + for goal_id_variant in raw_completed: + var goal_id: String = String(goal_id_variant) + if not goal_id.is_empty(): + _goal_completed[goal_id] = true + + for goal_id in _goal_definitions.keys(): + if not _goal_completed.has(goal_id): + _goal_completed[goal_id] = false + +## Internal helper: Migrates goal completion state from save format version 2 (generator unlock-based) to current format. +func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void: + _goal_completed.clear() + + var generator_states_data: Dictionary = parsed_data.get(GENERATOR_STATES_KEY, {}) + for gen_id_variant in generator_states_data.keys(): + var gen_id: String = String(gen_id_variant) + if gen_id.is_empty(): + continue + + var state: Dictionary = generator_states_data.get(gen_id_variant, {}) + if not state.get(GENERATOR_UNLOCKED_KEY, false): + continue + + var goal_id: StringName = _get_unlock_goal_id_for_generator(gen_id) + if not goal_id.is_empty(): + _goal_completed[goal_id] = true + + for goal_id in _goal_definitions.keys(): + if not _goal_completed.has(goal_id): + _goal_completed[goal_id] = false + +## Internal helper: Finds the goal ID that unlocks a specific generator (used in save migration). +func _get_unlock_goal_id_for_generator(generator_id: String) -> StringName: + if goal_catalogue == null: + return &"" + + for goal in goal_catalogue.goals: + if goal == null or not goal.has_id(): + continue + + var goal_id: StringName = goal.id + var requirements: Array[GoalRequirementData] = goal.get_requirements() + for req in requirements: + if req == null or req.get_currency() == null: + continue + + if String(req.get_currency().id) == generator_id: + return goal_id + + return &"" + +## Internal helper: Converts a currency ID to a human-readable display name. +func _humanize_currency_id(currency_id: StringName) -> String: + var normalized: String = String(_normalize_currency_id(currency_id)) + if normalized.is_empty(): + return "Unknown" + return normalized.replace("_", " ").capitalize() + +## Serializes research XP data for saving (includes BigNumber mantissa/exponent). +func _serialize_research_xp() -> Dictionary: + var result: Dictionary = {} + for research_id in research_xp.keys(): + var xp: BigNumber = research_xp[research_id] + result[research_id] = xp.serialize() + return result + +## Deserializes research XP data from save data. +func _deserialize_research_xp(raw: Variant) -> Dictionary: + var result: Dictionary = {} + if raw is Dictionary: + for research_id in raw.keys(): + var serialized: Variant = raw[research_id] + if serialized is Dictionary: + result[research_id] = BigNumber.deserialize(serialized) + return result + +## Serializes prestige buff unlock state as an array of unlocked node IDs. +func _serialize_prestige_buff_unlocked() -> Array: + var unlocked: Array = [] + for node_id in _prestige_buff_unlocked.keys(): + if _prestige_buff_unlocked.get(node_id, false): + unlocked.append(node_id) + return unlocked + +## Deserializes prestige buff unlock state from an array of unlocked node ID strings, then fills missing catalogue entries. +func _deserialize_prestige_buff_unlocked(raw: Variant) -> void: + _prestige_buff_unlocked.clear() + if raw is Array: + for element in raw: + var node_id: StringName = StringName(String(element)) + if node_id != &"": + _prestige_buff_unlocked[node_id] = true + _initialize_prestige_buffs() + + # Notify tiles that were waiting for state to load + for node_id in _prestige_buff_unlocked.keys(): + if _prestige_buff_unlocked.get(node_id, false): + prestige_buff_unlocked.emit(node_id) diff --git a/core/level_game_state.gd.uid b/core/level_game_state.gd.uid new file mode 100644 index 0000000..a106ca6 --- /dev/null +++ b/core/level_game_state.gd.uid @@ -0,0 +1 @@ +uid://cv2132o4hi7q3 diff --git a/core/prestige/README.md b/core/prestige/README.md new file mode 100644 index 0000000..9e7b19e --- /dev/null +++ b/core/prestige/README.md @@ -0,0 +1,467 @@ +# Prestige Module Documentation + +## Overview + +The `prestige/` subfolder implements the rebirth/reset system and the prestige buff graph — permanent upgrades purchasable with prestige currency that persist across resets. + +## Files + +| File | Purpose | +|------|---------| +| `prestige_config.gd` | Configuration resource for prestige rules (`PrestigeConfig`) | +| `prestige_manager.gd` | Core prestige state, gain calculation, and reset orchestration (`PrestigeManager`) | +| `prestige_buff_node.gd` | Single node in the prestige buff graph (`PrestigeBuffNode`) | +| `prestige_buff_catalogue.gd` | Flat list of all prestige buff nodes (`PrestigeBuffCatalogue`) | +| `prestige_buff_graph_tile.gd` | Individual tile UI for a buff node (`PrestigeBuffGraphTile`) | +| `prestige_buff_graph_tile.tscn` | Tile scene (icon, name, effect, cost, state) | +| `prestige_buff_graph_panel.gd` | Full-screen panel displaying the buff graph (`PrestigeBuffGraphPanel`) | +| `prestige_panel.gd` | UI panel for prestige reset interaction (`PrestigePanel`) | +| `prestige_panel.tscn` | Prestige panel scene | +| `prestige_progress_bar.gd` | Progress bar toward next prestige threshold | +| `prestige_progress_bar.tscn` | Progress bar scene | +| `TECH_SPEC.md` | Detailed technical specification (historical design doc) | + +## Architecture + +``` +LevelGameState (Node) +├── PrestigeManager (Node) +│ ├── Tracks prestige currency & reset count +│ ├── Calculates pending gain from config +│ └── Orchestrates reset via GameState +├── PrestigePanel (UI) +│ ├── Shows current/pending prestige & multiplier +│ └── Reset button with confirmation dialog +├── PrestigeProgressBar (UI) +│ └── Visual progress toward next threshold +├── PrestigeBuffGraphPanel (UI) +│ ├── Ascension currency balance +│ └── Tiered rows of PrestigeBuffGraphTile nodes +└── prestige_buff_catalogue (PrestigeBuffCatalogue) + └── Array of PrestigeBuffNode resources +``` + +PrestigeManager is **not** an autoload — it is a child node of `LevelGameState`. The game state finds it via `find_child("PrestigeManager")` in `_ready()`. + +--- + +# Part 1 — Prestige Reset System + +## PrestigeConfig + +Resource defining prestige rules. + +### Enums + +```gdscript +enum BasisType { + LIFETIME_TOTAL, # All-time currency acquired (single currency) + RUN_TOTAL, # Currency this run (single currency) + RUN_MAX, # Peak currency this run (single currency) + ALL_CURRENCIES # Sum of all currencies (multi-currency tracking) +} + +enum FormulaType { + POWER, # gain = scale * (ratio^exponent) + TRIANGULAR_INVERSE # gain based on triangular number inverse +} + +enum RoundingMode { + FLOOR, ROUND, CEIL +} + +enum MultiplierMode { + ADDITIVE, # +X per prestige + MULTIPLICATIVE_POWER # x^(multiplier_per_prestige) +} +``` + +### Key Properties + +```gdscript +@export var id: StringName = &"primary" +@export var display_name: String = "Ascension" +@export var prestige_currency_id: StringName = &"prestige" +@export var source_currency_id: StringName = &"magic" # What to measure (ignored for ALL_CURRENCIES) +@export var basis: BasisType = LIFETIME_TOTAL +@export var formula: FormulaType = POWER + +# Currency tracking +@export var include_currency_ids: Array[StringName] = [] # Empty = all currencies (for ALL_CURRENCIES basis) + +# Threshold: minimum source currency needed +@export var threshold_mantissa: float = 1.0 +@export var threshold_exponent: int = 4 # 1e4 = 10,000 + +# Formula parameters +@export var scale: float = 1.0 +@export var exponent: float = 0.5 # Square root + +# Multiplier growth +@export var multiplier_per_prestige: float = 0.05 # +5% per prestige +@export var multiplier_mode: MultiplierMode = ADDITIVE +``` + +## PrestigeManager + +Child node of `LevelGameState` managing prestige state. + +### Signals + +```gdscript +signal prestige_state_changed(total_prestige, pending_gain) +signal prestige_performed(gain, total_prestige) +signal prestige_threshold_changed(next_threshold) +``` + +### State Variables + +```gdscript +var total_prestige_earned: BigNumber # Lifetime prestige +var current_prestige_unspent: BigNumber # Available to spend +var prestige_resets_count: int # How many resets +var run_start_total_source_currency: BigNumber # Track run start +var run_peak_source_currency: BigNumber # Peak this run +``` + +### Key Methods + +```gdscript +# Query state +func get_total_prestige() -> BigNumber +func get_current_prestige_unspent() -> BigNumber +func calculate_pending_gain() -> BigNumber +func can_prestige() -> bool +func get_total_multiplier() -> float +func get_next_prestige_threshold() -> BigNumber + +# Perform prestige +func perform_prestige() -> bool +``` + +### Gain Calculation + +**Power Formula** (default): +```gdscript +ratio = basis_value / threshold +raw_gain = scale * (ratio^exponent) + flat_bonus + +# Cumulative basis subtracts already earned +if basis == LIFETIME_TOTAL: + gain = raw_gain - total_prestige_earned +``` + +**Triangular Inverse Formula**: +```gdscript +# Based on inverse triangular number: n = (sqrt(1 + 8k) - 1) / 2 +k = basis_value / threshold +target_total = (sqrt(1 + 8k) - 1) * 0.5 + +if basis == LIFETIME_TOTAL: + gain = target_total - total_prestige_earned +``` + +**Basis Value Calculation**: +```gdscript +if basis == ALL_CURRENCIES: + # Sum of all tracked currencies + basis_value = sum(get_total_currency_acquired(currency_id) for currency_id in tracked_currencies) +elif basis == RUN_TOTAL: + # Currency earned since last reset + basis_value = lifetime_total - run_start_total +elif basis == RUN_MAX: + # Peak currency reached this run + basis_value = run_peak_currency +else: # LIFETIME_TOTAL + # All-time currency acquired + basis_value = lifetime_total +``` + +### Multiplier Application + +**Additive Mode**: +```gdscript +multiplier = base_multiplier + (total_prestige * multiplier_per_prestige) +# e.g., 1.0 + (5 * 0.05) = 1.25 (25% bonus) +``` + +**Multiplicative Power Mode**: +```gdscript +growth_base = 1.0 + multiplier_per_prestige +multiplier = base_multiplier * (growth_base ^ total_prestige) +# e.g., 1.0 * (1.05 ^ 5) = 1.276 (27.6% bonus) +``` + +### Prestige Reset Flow + +1. Player clicks "Prestige" button +2. `perform_prestige()` called +3. Gain calculated and added to total +4. Prestige currency granted via `game_state.add_currency_by_id()` +5. `game_state.reset_for_prestige()` called (preserves prestige currency, resets generators, currencies, buff levels, goals) +6. `_reset_all_buff_levels()` — clears generator buff levels (NOT prestige buff unlocks) +7. Generator runtime state reinitialized +8. Run tracking reinitialized +9. `game_state.emit_currency_changed_for_all()` — triggers UI refresh +10. Save triggered + +## PrestigePanel + +UI component for prestige interaction. + +### Displays + +```gdscript +_total_value.text # Total prestige earned (e.g. "123.45") +_pending_value.text # Gain available now +_multiplier_value.text # Current multiplier (e.g. "x2.34") +_basis_label.text # Basis + value (e.g. "Total Currency (1.50e6)") +``` + +### Buttons + +- **Reset**: Triggers `perform_prestige()` after confirmation dialog +- **Save**: Manually saves game state + +## Generator Integration + +Generators apply prestige multipliers via `PrestigeManager.get_total_multiplier()`: + +```gdscript +func _get_prestige_multiplier() -> float: + var parent: Node = get_parent() + var prestige_manager: PrestigeManager = parent.find_child("PrestigeManager", true, false) + if prestige_manager: + return prestige_manager.get_total_multiplier() + return 1.0 +``` + +This is called from `get_effective_auto_run_multiplier()` alongside research and buff multipliers. In the future, prestige buff multipliers from the graph system will replace or augment this. + +## Save Integration + +Prestige state stored in `LevelGameState` external save: + +```json +{ + "prestige_state": { + "total_prestige_earned": {"m": 10.0, "e": 0}, + "current_prestige_unspent": {"m": 5.0, "e": 0}, + "prestige_resets_count": 3, + "run_start_total_source_currency": {"m": 1000.0, "e": 0}, + "run_peak_source_currency": {"m": 5000.0, "e": 0}, + "last_reset_time": 1234567890 + } +} +``` + +## Configuration Example + +For a typical idle game with single currency: + +```gdscript +id = &"primary" +display_name = "Ascension" +source_currency_id = &"magic" +basis = LIFETIME_TOTAL +threshold_mantissa = 1.0 +threshold_exponent = 6 # 1e6 +formula = POWER +exponent = 0.5 # Square root +multiplier_mode = ADDITIVE +multiplier_per_prestige = 0.10 +``` + +For multi-currency tracking (`ALL_CURRENCIES`): + +```gdscript +id = &"ascension" +display_name = "Ascension" +prestige_currency_id = &"ascension" +basis = ALL_CURRENCIES +include_currency_ids = [] # Empty = all currencies from catalogue +threshold_mantissa = 1.0 +threshold_exponent = 4 # 1e4 +formula = POWER +exponent = 0.5 +multiplier_mode = ADDITIVE +multiplier_per_prestige = 0.05 +``` + +--- + +# Part 2 — Prestige Buff Graph + +## Overview + +The prestige buff graph is a permanent upgrade system where players spend prestige currency to unlock nodes in a DAG-structured graph. Unlike generator buffs (which reset on prestige), prestige buff unlocks are **permanent** and survive resets. + +## PrestigeBuffNode + +Single node in the graph. Each node is a one-shot unlock — either locked or unlocked, no repeatable levels. + +### EffectType Enum + +```gdscript +enum EffectType { + CURRENCY_PRODUCTION_MULTIPLIER, # Multiply currency output by x% + BUILDING_CREATION_BONUS, # When a building is created, increase currency gain by x% + WORKER_THRESHOLD_MULTIPLIER, # When 10+ workers are assigned to a building, increase currency gain by x% + CARRYOVER_BUILDINGS, # After prestige, start with some buildings and workers already assigned + UNLOCK_AUTO_BUY_WORKER, # Unlock option to auto-buy workers + HOVER_SPEED_BOOST, # Hovering over a place speeds up workers instead of clicking + UNLOCK_BUILDING, # Unlock a new building (generator) + WORKER_PRODUCTIVITY, # Increase worker productivity by x% + RESEARCH_XP_MULTIPLIER, # Increase research XP gain by x% +} +``` + +### Exported Fields + +| Field | Type | Notes | +|-------|------|-------| +| `id` | `StringName` | Unique node identifier | +| `display_name` | `String` | UI label | +| `description` | `String` (multiline) | Tooltip | +| `icon` | `Texture2D` | Icon in graph UI | +| `parent_ids` | `Array[StringName]` | Prerequisites (DAG — multiple parents allowed) | +| `effect_type` | `EffectType` | What this buff does | +| `effect_value` | `float` | Effect magnitude (multiplier or count depending on type) | +| `target_id` | `StringName` | Target building/currency for type-specific effects (empty = global) | +| `cost_mantissa` | `float` | Prestige currency cost mantissa | +| `cost_exponent` | `int` | Prestige currency cost exponent | +| `tier` | `int` | Row in graph UI (visual grouping) | +| `x_position` | `float` | Column in graph UI | + +### Key Methods + +```gdscript +func get_cost() -> BigNumber # BigNumber from mantissa/exponent +func has_parents() -> bool # True if parent_ids is non-empty +func all_parents_unlocked(unlocked: Dictionary) -> bool # All prerequisites met? +func is_valid() -> bool # Non-empty id and positive cost +``` + +## PrestigeBuffCatalogue + +Flat resource containing all buff nodes. The graph structure emerges from `parent_ids`. + +| Field | Type | +|-------|------| +| `nodes` | `Array[PrestigeBuffNode]` | + +### Key Methods + +```gdscript +func get_node_by_id(id: StringName) -> PrestigeBuffNode +func get_all_ids() -> Array[StringName] +func get_root_nodes() -> Array[PrestigeBuffNode] # Nodes with empty parent_ids +func get_children_of(parent_id: StringName) -> Array[PrestigeBuffNode] +``` + +## State in LevelGameState + +```gdscript +@export var prestige_buff_catalogue: PrestigeBuffCatalogue +var _prestige_buff_unlocked: Dictionary = {} # {StringName: bool} — node_id → unlocked +``` + +- Never cleared by `reset_for_prestige()` (unlike `_buff_levels` which IS cleared) +- Normalized on load: missing catalogue node IDs are filled as `false` +- On initialization, iterates the catalogue and sets any missing entries to `false` + +## LevelGameState API + +```gdscript +# Purchase and query +func purchase_prestige_buff(buff_id: StringName) -> bool +func can_purchase_prestige_buff(buff_id: StringName) -> bool +func is_prestige_buff_unlocked(buff_id: StringName) -> bool +func get_prestige_buff_cost(buff_id: StringName) -> BigNumber +func get_prestige_currency_id() -> StringName # Returns &"ascension" + +# Multiplier computation — walks graph, returns combined product from unlocked nodes +func get_prestige_buff_multiplier(effect_type: int, target_id: StringName = &"") -> float + +# Available nodes (prerequisites met, not yet unlocked) +func get_available_prestige_buffs() -> Array[PrestigeBuffNode] +``` + +### Signal + +```gdscript +signal prestige_buff_unlocked(buff_id: StringName) +``` + +Prestige currency balance changes use the existing `currency_changed` signal — no separate signal needed. + +## Purchase Flow + +``` +Player clicks unlock node + → can_purchase_prestige_buff(node_id) + → Node exists in catalogue? + → Not already unlocked? + → All parent_ids unlocked? + → Enough prestige currency? + → YES: spend_currency_by_id("ascension", cost) + → _prestige_buff_unlocked[node_id] = true + → emit prestige_buff_unlocked + → save_game() +``` + +## Multiplier Computation + +`get_prestige_buff_multiplier(effect_type, target_id)` iterates all unlocked nodes matching the given effect type and multiplies their `effect_value` together. Results start at `1.0`. + +- If `target_id` is empty: all matching nodes are included (global effect) +- If `target_id` is set: only nodes with empty or matching `target_id` are included + +## Multiplier Usage In Generators + +Generators should call `get_prestige_buff_multiplier()` with their generator ID: + +```gdscript +var asc_mult: float = game_state.get_prestige_buff_multiplier( + PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, + get_generator_id() +) +``` + +This replaces the old `_get_prestige_multiplier()` call for prestige buff-driven production bonuses. + +## Save Format + +- Save format version bumped to **8** when prestige buffs were introduced. +- Key: `"prestige_buff_unlocked"` → array of unlocked node ID strings: `["gold_boost", "farm_boost"]` +- Load deserialization reads the array, sets matching entries to `true`, then calls `_initialize_prestige_buffs()` to fill in missing entries as `false`. + +## UI — PrestigeBuffGraphPanel + +A `PanelContainer`-based separate screen. + +- **Layout**: Tiered rows (`HBoxContainer` per tier), sorted by `tier` then `x_position` +- **Tiles** (`PrestigeBuffGraphTile`): show icon, name, effect description, cost, state +- **States**: + - **Locked** (greyed out): prerequisites not met + - **Available** (highlighted, button enabled): all prerequisites met, can purchase + - **Unlocked** (green check, button disabled): already owned +- **Balance**: Shows current prestige currency balance at top, refreshes on `currency_changed` +- **Graph rebuild**: Rebuilds entire graph on `prestige_buff_unlocked` to reflect newly available children + +## Content Files + +Prestige buff content lives in `docs/gyms/tiny_sword/prestige/`: + +| File | Purpose | +|------|---------| +| `prestige_buff_catalogue.tres` | Catalogue resource referencing all buff nodes | +| `prestige_buff_gold_boost.tres` | Example: +50% gold mine production | +| `prestige_buff_farm_boost.tres` | Example: +50% farm food production | + +Each `.tres` is a `PrestigeBuffNode` resource with exported fields set in the inspector. + +## Prestige Reset And Buffs + +- **Generator buffs** (`_buff_levels`, `_buff_unlocked`, `_buff_active`): reset to 0/false on prestige +- **Prestige buff unlocks** (`_prestige_buff_unlocked`): **never reset** — permanent across all prestige resets diff --git a/core/prestige/prestige_buff_catalogue.gd b/core/prestige/prestige_buff_catalogue.gd new file mode 100644 index 0000000..013ecb7 --- /dev/null +++ b/core/prestige/prestige_buff_catalogue.gd @@ -0,0 +1,40 @@ +## Flat list of all ascension buff nodes. The graph structure emerges from each node's parent_ids field. +class_name PrestigeBuffCatalogue +extends Resource + +@export var nodes: Array[PrestigeBuffNode] = [] + + +## Finds a node by its unique ID. Returns null if not found. +func get_node_by_id(id: StringName) -> PrestigeBuffNode: + for node in nodes: + if node.id == id: + return node + return null + + +## Returns all non-empty node IDs in the catalogue. +func get_all_ids() -> Array[StringName]: + var ids: Array[StringName] = [] + for node in nodes: + if node.id: + ids.append(node.id) + return ids + + +## Returns all root nodes (nodes with no prerequisites). +func get_root_nodes() -> Array[PrestigeBuffNode]: + var roots: Array[PrestigeBuffNode] = [] + for node in nodes: + if node.parent_ids.is_empty(): + roots.append(node) + return roots + + +## Returns all nodes that list the given parent_id as a prerequisite. +func get_children_of(parent_id: StringName) -> Array[PrestigeBuffNode]: + var children: Array[PrestigeBuffNode] = [] + for node in nodes: + if node.parent_ids.has(parent_id): + children.append(node) + return children diff --git a/core/prestige/prestige_buff_catalogue.gd.uid b/core/prestige/prestige_buff_catalogue.gd.uid new file mode 100644 index 0000000..354a599 --- /dev/null +++ b/core/prestige/prestige_buff_catalogue.gd.uid @@ -0,0 +1 @@ +uid://bjcvnyh2bc6l5 diff --git a/core/prestige/prestige_buff_connection_overlay.gd b/core/prestige/prestige_buff_connection_overlay.gd new file mode 100644 index 0000000..b7428ec --- /dev/null +++ b/core/prestige/prestige_buff_connection_overlay.gd @@ -0,0 +1,43 @@ +## Draws bezier curves between connected PrestigeBuffGraphTile nodes in the graph panel. +## Store pairs of tile references before calling queue_redraw(). +class_name PrestigeBuffConnectionOverlay +extends Control + +## Pairs of [parent_tile, child_tile] to connect. +var _connections: Array = [] + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + +## Replace all connections and redraw. +func set_connections(connections: Array) -> void: + _connections = connections + queue_redraw() + + +func _draw() -> void: + if _connections.is_empty(): + return + + for pair in _connections: + var parent_tile: Control = pair[0] as Control + var child_tile: Control = pair[1] as Control + if parent_tile == null or child_tile == null: + continue + + var from_pos: Vector2 = _tile_bottom_center(parent_tile) + var to_pos: Vector2 = _tile_top_center(child_tile) + draw_line(from_pos, to_pos, Color(0.6, 0.6, 0.7, 0.6), 2.0) + + +func _tile_bottom_center(tile: Control) -> Vector2: + var rect: Rect2 = tile.get_global_rect() + var global_pos: Vector2 = Vector2(rect.position.x + rect.size.x * 0.5, rect.position.y + rect.size.y) + return get_global_transform().affine_inverse() * global_pos + + +func _tile_top_center(tile: Control) -> Vector2: + var rect: Rect2 = tile.get_global_rect() + var global_pos: Vector2 = Vector2(rect.position.x + rect.size.x * 0.5, rect.position.y) + return get_global_transform().affine_inverse() * global_pos diff --git a/core/prestige/prestige_buff_connection_overlay.gd.uid b/core/prestige/prestige_buff_connection_overlay.gd.uid new file mode 100644 index 0000000..a326169 --- /dev/null +++ b/core/prestige/prestige_buff_connection_overlay.gd.uid @@ -0,0 +1 @@ +uid://uv5lxj6hmpk diff --git a/core/prestige/prestige_buff_graph_panel.gd b/core/prestige/prestige_buff_graph_panel.gd new file mode 100644 index 0000000..af93cde --- /dev/null +++ b/core/prestige/prestige_buff_graph_panel.gd @@ -0,0 +1,85 @@ +## Separate-screen panel displaying the prestige buff graph. +## Tiles are placed manually in the scene editor — each PrestigeBuffGraphTile has its PrestigeBuffNode +## resource assigned directly. The panel manages the prestige currency balance display and +## delegates connection-line drawing to a PrestigeBuffConnectionOverlay child. +class_name PrestigeBuffGraphPanel +extends PanelContainer + +@onready var _balance_label: Label = $MarginContainer/VBoxContainer/BalanceLabel +@onready var _tiles_area: Control = $MarginContainer/VBoxContainer/TilesArea +@onready var _close_button: Button = $MarginContainer/VBoxContainer/Header/CloseButton +@onready var _connection_overlay: PrestigeBuffConnectionOverlay = $MarginContainer/VBoxContainer/TilesArea/ConnectionOverlay + +var _game_state: LevelGameState + + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + if _game_state == null: + push_error("PrestigeBuffGraphPanel: Could not find LevelGameState parent") + return + + _game_state.currency_changed.connect(_on_currency_changed) + _close_button.pressed.connect(_on_close_pressed) + + visibility_changed.connect(_on_visibility_changed) + _refresh_balance() + + +## Rebuilds the connection overlay by scanning all PrestigeBuffGraphTile children and matching parent_ids. +func _refresh_connections() -> void: + if _connection_overlay == null: + return + + var tiles_by_id: Dictionary = {} + var all_tiles: Array[PrestigeBuffGraphTile] = [] + _collect_tiles(_tiles_area, all_tiles) + + for tile in all_tiles: + var node_id: StringName = tile.get_node_id() + if node_id != &"": + tiles_by_id[node_id] = tile + + var connections: Array = [] + for tile in all_tiles: + if tile.prestige_buff == null: + continue + for parent_id in tile.prestige_buff.parent_ids: + var parent_tile: PrestigeBuffGraphTile = tiles_by_id.get(parent_id, null) + if parent_tile: + connections.append([parent_tile, tile]) + + _connection_overlay.set_connections(connections) + + +## Collects all PrestigeBuffGraphTile nodes rooted at `from`, recursing into every child. +func _collect_tiles(from: Node, out_tiles: Array[PrestigeBuffGraphTile]) -> void: + var found: Array[Node] = from.find_children("*", "PrestigeBuffGraphTile", true, false) + for node in found: + var tile := node as PrestigeBuffGraphTile + if tile: + out_tiles.append(tile) + +func _refresh_balance() -> void: + if _game_state == null: + _balance_label.text = "Prestige Currency: 0" + return + + var currency_id: StringName = _game_state.get_prestige_currency_id() + var balance: BigNumber = _game_state.get_currency_amount_by_id(currency_id) + _balance_label.text = "Prestige Currency: %s" % balance.to_string_suffix(2) + +func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void: + if currency_id == _game_state.get_prestige_currency_id(): + _refresh_balance() + +func _on_close_pressed() -> void: + hide() + +func _on_visibility_changed() -> void: + if visible: + _refresh_balance() + _refresh_connections() + +func _on_prestige_buff_toggle_pressed() -> void: + show() diff --git a/core/prestige/prestige_buff_graph_panel.gd.uid b/core/prestige/prestige_buff_graph_panel.gd.uid new file mode 100644 index 0000000..1cc1237 --- /dev/null +++ b/core/prestige/prestige_buff_graph_panel.gd.uid @@ -0,0 +1 @@ +uid://n01pkakxllnj diff --git a/core/prestige/prestige_buff_graph_tile.gd b/core/prestige/prestige_buff_graph_tile.gd new file mode 100644 index 0000000..63f62f6 --- /dev/null +++ b/core/prestige/prestige_buff_graph_tile.gd @@ -0,0 +1,122 @@ +## Individual tile in the prestige buff graph UI. +## Visually represents a single PrestigeBuffNode with locked/available/unlocked states. +## The PrestigeBuffNode resource is assigned directly via the editor export. +class_name PrestigeBuffGraphTile +extends PanelContainer + +## The prestige buff node this tile represents. Set in the scene editor. +@export var prestige_buff: PrestigeBuffNode + +@onready var _button: Button = $Button +@onready var _icon: TextureRect = $Button/HBoxContainer/Icon +@onready var _name_label: Label = $Button/HBoxContainer/VBoxContainer/NameLabel +@onready var _effect_label: Label = $Button/HBoxContainer/VBoxContainer/EffectLabel +@onready var _cost_label: Label = $Button/HBoxContainer/VBoxContainer/CostLabel +@onready var _state_label: Label = $Button/HBoxContainer/StateLabel + +var _game_state: LevelGameState + + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + _button.pressed.connect(_on_button_pressed) + + if _game_state: + _game_state.prestige_buff_unlocked.connect(_on_prestige_buff_unlocked) + _game_state.currency_changed.connect(_on_currency_changed) + + if prestige_buff == null: + push_warning("PrestigeBuffGraphTile: prestige_buff is not set") + return + + _refresh_visuals() + _refresh_state() + + +func get_node_id() -> StringName: + if prestige_buff: + return prestige_buff.id + return &"" + + +func _refresh_visuals() -> void: + if prestige_buff == null: + return + + _name_label.text = prestige_buff.display_name + _cost_label.text = "Cost: %s" % prestige_buff.get_cost().to_string_suffix(2) + + if prestige_buff.icon: + _icon.texture = prestige_buff.icon + + var effect_text: String = _format_effect_description() + _effect_label.text = effect_text + _effect_label.visible = not effect_text.is_empty() + + +func _refresh_state() -> void: + if prestige_buff == null or _game_state == null: + return + + var unlocked: bool = _game_state.is_prestige_buff_unlocked(prestige_buff.id) + var available: bool = _game_state.can_purchase_prestige_buff(prestige_buff.id) + + if unlocked: + _state_label.text = "✓ Unlocked" + _button.disabled = true + modulate = Color.GREEN + elif available: + _state_label.text = "Available" + _button.disabled = false + modulate = Color.WHITE + else: + _state_label.text = "Locked" + _button.disabled = true + modulate = Color(0.5, 0.5, 0.5, 1.0) + + +func _format_effect_description() -> String: + if prestige_buff == null: + return "" + + match prestige_buff.effect_type: + PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER: + if prestige_buff.target_id != &"": + return "+%.0f%% %s production" % [((prestige_buff.effect_value - 1.0) * 100.0), prestige_buff.target_id] + return "+%.0f%% all production" % ((prestige_buff.effect_value - 1.0) * 100.0) + PrestigeBuffNode.EffectType.BUILDING_CREATION_BONUS: + return "+%.0f%% gain on build" % ((prestige_buff.effect_value - 1.0) * 100.0) + PrestigeBuffNode.EffectType.WORKER_THRESHOLD_MULTIPLIER: + return "+%.0f%% with 10+ workers" % ((prestige_buff.effect_value - 1.0) * 100.0) + PrestigeBuffNode.EffectType.CARRYOVER_BUILDINGS: + return "Keep %d buildings on prestige" % int(prestige_buff.effect_value) + PrestigeBuffNode.EffectType.UNLOCK_AUTO_BUY_WORKER: + return "Unlock auto-buy worker" + PrestigeBuffNode.EffectType.HOVER_SPEED_BOOST: + return "Hover to speed up workers" + PrestigeBuffNode.EffectType.UNLOCK_BUILDING: + return "Unlock %s building" % prestige_buff.target_id + PrestigeBuffNode.EffectType.WORKER_PRODUCTIVITY: + return "+%.0f%% worker productivity" % ((prestige_buff.effect_value - 1.0) * 100.0) + PrestigeBuffNode.EffectType.RESEARCH_XP_MULTIPLIER: + return "+%.0f%% research XP" % ((prestige_buff.effect_value - 1.0) * 100.0) + _: + return "" + + +func _on_button_pressed() -> void: + if _game_state and prestige_buff: + _game_state.purchase_prestige_buff(prestige_buff.id) + + +func _on_prestige_buff_unlocked(buff_id: StringName) -> void: + if prestige_buff and buff_id == prestige_buff.id: + _refresh_state() + + +func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void: + if _game_state == null or prestige_buff == null: + return + var asc_id: StringName = _game_state.get_prestige_currency_id() + if currency_id == asc_id: + _refresh_state() diff --git a/core/prestige/prestige_buff_graph_tile.gd.uid b/core/prestige/prestige_buff_graph_tile.gd.uid new file mode 100644 index 0000000..cf49391 --- /dev/null +++ b/core/prestige/prestige_buff_graph_tile.gd.uid @@ -0,0 +1 @@ +uid://w8ogrp1s6qsf diff --git a/core/prestige/prestige_buff_graph_tile.tscn b/core/prestige/prestige_buff_graph_tile.tscn new file mode 100644 index 0000000..b662b9f --- /dev/null +++ b/core/prestige/prestige_buff_graph_tile.tscn @@ -0,0 +1,40 @@ +[gd_scene format=3 uid="uid://bqk8rwia7lpbg"] + +[ext_resource type="Script" uid="uid://w8ogrp1s6qsf" path="res://core/prestige/prestige_buff_graph_tile.gd" id="1_script"] + +[node name="AscensionBuffGraphTile" type="PanelContainer" unique_id=582396224] +custom_minimum_size = Vector2(220, 100) +script = ExtResource("1_script") + +[node name="Button" type="Button" parent="." unique_id=34096996] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="Button" unique_id=760838153] +layout_mode = 0 + +[node name="Icon" type="TextureRect" parent="Button/HBoxContainer" unique_id=2086768939] +custom_minimum_size = Vector2(48, 48) +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="VBoxContainer" type="VBoxContainer" parent="Button/HBoxContainer" unique_id=1701436193] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="NameLabel" type="Label" parent="Button/HBoxContainer/VBoxContainer" unique_id=334883451] +layout_mode = 2 +text = "Buff Name" + +[node name="EffectLabel" type="Label" parent="Button/HBoxContainer/VBoxContainer" unique_id=1416390723] +layout_mode = 2 +text = "+10% effect" + +[node name="CostLabel" type="Label" parent="Button/HBoxContainer/VBoxContainer" unique_id=983468527] +layout_mode = 2 +text = "Cost: 0" + +[node name="StateLabel" type="Label" parent="Button/HBoxContainer" unique_id=629071282] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Locked" +horizontal_alignment = 2 diff --git a/core/prestige/prestige_buff_node.gd b/core/prestige/prestige_buff_node.gd new file mode 100644 index 0000000..6e06aa5 --- /dev/null +++ b/core/prestige/prestige_buff_node.gd @@ -0,0 +1,69 @@ +## A single node in the prestige buff graph. +## Each node represents a permanent upgrade that persists across prestige resets and is purchased with prestige currency. +class_name PrestigeBuffNode +extends Resource + +enum EffectType { + CURRENCY_PRODUCTION_MULTIPLIER, ## Multiply currency output by x% + BUILDING_CREATION_BONUS, ## When a building is created, increase currency gain by x% + WORKER_THRESHOLD_MULTIPLIER, ## When 10+ workers are assigned to a building, increase currency gain by x% + CARRYOVER_BUILDINGS, ## After prestige, start with some buildings and workers already assigned + UNLOCK_AUTO_BUY_WORKER, ## Unlock option to auto-buy workers + HOVER_SPEED_BOOST, ## Hovering over a place speeds up workers instead of clicking + UNLOCK_BUILDING, ## Unlock a new building (generator) + WORKER_PRODUCTIVITY, ## Increase worker productivity by x% + RESEARCH_XP_MULTIPLIER, ## Increase research XP gain by x% +} + +## Unique node identifier. +@export var id: StringName = &"" +## UI display label. +@export var display_name: String = "" +## Tooltip / description text. +@export_multiline var description: String = "" +## Icon shown in the graph UI. +@export var icon: Texture2D +## Prerequisite node IDs that must be unlocked before this node becomes available. +@export var parent_ids: Array[StringName] = [] +## What kind of effect this buff provides. +@export var effect_type: EffectType = EffectType.CURRENCY_PRODUCTION_MULTIPLIER +## Magnitude of the effect (interpreted per effect type). +@export var effect_value: float = 1.0 +## Target generator, building, or currency ID for type-specific effects. Empty means global. +@export var target_id: StringName = &"" +## Prestige currency cost mantissa (combined with cost_exponent for BigNumber). +@export var cost_mantissa: float = 1.0 +## Prestige currency cost exponent (combined with cost_mantissa for BigNumber). +@export var cost_exponent: int = 0 +## Visual tier / row in the graph UI. +@export var tier: int = 0 +## Visual column / horizontal position in the graph UI. +@export var x_position: float = 0.0 + + +## Builds and returns the fixed prestige currency cost as a BigNumber. +func get_cost() -> BigNumber: + return BigNumber.new(cost_mantissa, cost_exponent) + + +## Returns true if this node has any prerequisite parent nodes. +func has_parents() -> bool: + return not parent_ids.is_empty() + + +## Checks whether all prerequisites are unlocked in the provided dictionary. +## [param unlocked] maps node_id (StringName) → bool (true if unlocked). +func all_parents_unlocked(unlocked: Dictionary) -> bool: + for parent_id in parent_ids: + if not bool(unlocked.get(parent_id, false)): + return false + return true + + +## Returns true if this node has valid required data (non-empty id, positive cost). +func is_valid() -> bool: + if id == &"": + return false + if cost_mantissa <= 0.0: + return false + return true diff --git a/core/prestige/prestige_buff_node.gd.uid b/core/prestige/prestige_buff_node.gd.uid new file mode 100644 index 0000000..728262c --- /dev/null +++ b/core/prestige/prestige_buff_node.gd.uid @@ -0,0 +1 @@ +uid://mjyig8xlgki0 diff --git a/core/prestige/prestige_config.gd b/core/prestige/prestige_config.gd index b7f3915..7d1aa78 100644 --- a/core/prestige/prestige_config.gd +++ b/core/prestige/prestige_config.gd @@ -5,6 +5,7 @@ enum BasisType { LIFETIME_TOTAL, RUN_TOTAL, RUN_MAX, + ALL_CURRENCIES, } enum FormulaType { @@ -41,6 +42,7 @@ enum MultiplierMode { @export var base_multiplier: float = 1.0 @export var multiplier_per_prestige: float = 0.05 @export var multiplier_exponent: float = 1.0 +@export var include_currency_ids: Array[StringName] = [] func get_threshold() -> BigNumber: return BigNumber.new(threshold_mantissa, threshold_exponent) @@ -48,7 +50,7 @@ func get_threshold() -> BigNumber: func is_valid() -> bool: if String(id).strip_edges().is_empty(): return false - if String(source_currency_id).strip_edges().is_empty(): + if String(source_currency_id).strip_edges().is_empty() and basis != BasisType.ALL_CURRENCIES: return false if threshold_mantissa <= 0.0: return false diff --git a/core/prestige/prestige_manager.gd b/core/prestige/prestige_manager.gd index e5a77fb..9b04968 100644 --- a/core/prestige/prestige_manager.gd +++ b/core/prestige/prestige_manager.gd @@ -1,7 +1,9 @@ +class_name PrestigeManager extends Node signal prestige_state_changed(total_prestige: BigNumber, pending_gain: BigNumber) signal prestige_performed(gain: BigNumber, total_prestige: BigNumber) +signal prestige_threshold_changed(next_threshold: BigNumber) const SAVE_KEY: String = "prestige_state" const TOTAL_PRESTIGE_KEY: String = "total_prestige_earned" @@ -14,6 +16,7 @@ const LAST_RESET_TIME_KEY: String = "last_reset_time" const BASIS_LIFETIME_TOTAL: int = 0 const BASIS_RUN_TOTAL: int = 1 const BASIS_RUN_MAX: int = 2 +const BASIS_ALL_CURRENCIES: int = 3 const FORMULA_POWER: int = 0 const FORMULA_TRIANGULAR_INVERSE: int = 1 @@ -21,7 +24,8 @@ const FORMULA_TRIANGULAR_INVERSE: int = 1 const MULTIPLIER_MODE_ADDITIVE: int = 0 const MULTIPLIER_MODE_MULTIPLICATIVE_POWER: int = 1 -@export var config: Resource +@export var config: PrestigeConfig +@export var game_state: LevelGameState var total_prestige_earned: BigNumber = BigNumber.from_float(0.0) var current_prestige_unspent: BigNumber = BigNumber.from_float(0.0) @@ -32,13 +36,17 @@ var last_reset_time: int = 0 func _ready() -> void: if config == null: - config = load("res://idles/prestige/primary_prestige.tres") as Resource + config = load("res://docs/gyms/tiny_sword/prestige/primary_prestige.tres") as PrestigeConfig if not _is_config_valid(): push_warning("PrestigeManager has invalid or missing config; prestige is disabled.") return - var loaded_state: Dictionary = GameState.get_external_save_data(SAVE_KEY) + if game_state == null: + push_warning("PrestigeManager: game_state reference is not set.") + return + + var loaded_state: Dictionary = game_state.get_external_save_data(SAVE_KEY) if loaded_state.is_empty(): _initialize_run_tracking_from_current_state() else: @@ -46,8 +54,12 @@ func _ready() -> void: _validate_loaded_run_tracking() _save_state_to_game_state() - GameState.currency_changed.connect(_on_currency_changed) + game_state.currency_changed.connect(_on_currency_changed) prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain()) + prestige_threshold_changed.emit(get_next_prestige_threshold()) + +func get_config() -> PrestigeConfig: + return config func get_source_currency_id() -> StringName: if not _is_config_valid(): @@ -55,6 +67,38 @@ func get_source_currency_id() -> StringName: return _normalize_currency_id(_get_config_string_name("source_currency_id", &"")) +func get_prestige_currency_id() -> StringName: + if not _is_config_valid(): + return &"" + + return _normalize_currency_id(_get_config_string_name("prestige_currency_id", &"")) + +func get_tracked_currency_ids() -> Array[StringName]: + if not _is_config_valid(): + return [] + + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) + + if basis_type == BASIS_ALL_CURRENCIES: + var include_ids: Array[StringName] = _get_config_array("include_currency_ids", []) + if not include_ids.is_empty(): + return include_ids + if game_state and game_state.currency_catalogue: + return game_state.currency_catalogue.get_all_ids() + return [] + + return [get_source_currency_id()] + +func _get_config_array(property_name: String, fallback: Array = []) -> Array: + if config == null: + return fallback + + var value: Variant = config.get(property_name) + if value is Array: + return value + + return fallback + func get_total_prestige() -> BigNumber: return _copy_big_number(total_prestige_earned) @@ -66,7 +110,7 @@ func get_total_multiplier() -> float: return 1.0 var base: float = maxf(_get_config_float("base_multiplier", 1.0), 0.0) - var prestige_value: float = _big_number_to_float(total_prestige_earned) + var prestige_value: float = total_prestige_earned.to_float() if prestige_value <= 0.0: return base @@ -125,52 +169,119 @@ func perform_prestige() -> bool: if gain.mantissa > 0.0: total_prestige_earned.add_in_place(gain) current_prestige_unspent.add_in_place(gain) + + var prestige_currency_id: StringName = get_prestige_currency_id() + if prestige_currency_id != &"" and game_state: + game_state.add_currency_by_id(prestige_currency_id, gain) prestige_resets_count += 1 last_reset_time = Time.get_unix_time_from_system() - GameState.reset_for_prestige(true, false) + if game_state: + var preserve_currencies: Array[StringName] = [] + var prestige_currency_id: StringName = get_prestige_currency_id() + if prestige_currency_id != &"": + preserve_currencies.append(prestige_currency_id) + game_state.reset_for_prestige(true, false, preserve_currencies) + _reset_all_buff_levels() _reinitialize_generators_after_prestige_reset() - GameState.emit_currency_changed_for_all() + if game_state: + game_state.emit_currency_changed_for_all() _initialize_run_tracking_from_current_state() _save_state_to_game_state() - GameState.save_game() + if game_state: + game_state.save_game() var total: BigNumber = get_total_prestige() + var next_threshold: BigNumber = get_next_prestige_threshold() prestige_performed.emit(gain, total) prestige_state_changed.emit(total, calculate_pending_gain()) + prestige_threshold_changed.emit(next_threshold) return true +func get_next_prestige_threshold() -> BigNumber: + if not _is_config_valid(): + return BigNumber.from_float(0.0) + + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) + + match basis_type: + BASIS_RUN_TOTAL, BASIS_ALL_CURRENCIES: + return _get_config_threshold() + _: + var current_total: float = total_prestige_earned.to_float() + var next_target: float = _calculate_target_for_prestige(current_total + 1.0) + return BigNumber.from_float(next_target) + func get_basis_value_for_display() -> BigNumber: return _get_basis_value() func get_basis_label() -> String: if not _is_config_valid(): return "-" - - match _get_config_int("basis", BASIS_LIFETIME_TOTAL): + + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) + + match basis_type: BASIS_RUN_TOTAL: return "Run Total" BASIS_RUN_MAX: return "Run Max" + BASIS_ALL_CURRENCIES: + return "Total Currency" _: return "Lifetime Total" func _on_currency_changed(changed_currency_id: StringName, new_amount: BigNumber) -> void: if config == null: return - - var source_currency_id: StringName = get_source_currency_id() - if source_currency_id == &"": - return - if changed_currency_id != source_currency_id: - return - - if new_amount.is_greater_than(run_peak_source_currency): - run_peak_source_currency = _copy_big_number(new_amount) - + + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) + + if basis_type == BASIS_ALL_CURRENCIES: + if new_amount.is_greater_than(run_peak_source_currency): + run_peak_source_currency = _copy_big_number(new_amount) + else: + var source_currency_id: StringName = get_source_currency_id() + if source_currency_id == &"": + return + if changed_currency_id != source_currency_id: + return + + if new_amount.is_greater_than(run_peak_source_currency): + run_peak_source_currency = _copy_big_number(new_amount) + _save_state_to_game_state() + var next_threshold: BigNumber = get_next_prestige_threshold() prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain()) + prestige_threshold_changed.emit(next_threshold) + +func _calculate_target_for_prestige(prestige_level: float) -> float: + if config == null: + return 0.0 + + var threshold_value: float = _get_config_threshold().to_float() + if threshold_value <= 0.0: + return 0.0 + + match _get_config_int("formula", FORMULA_POWER): + FORMULA_TRIANGULAR_INVERSE: + var target_total: float = prestige_level + var k: float = target_total * (target_total + 1.0) * 0.5 + return k * threshold_value + _: + var scale: float = maxf(_get_config_float("scale", 1.0), 0.0001) + var exponent: float = maxf(_get_config_float("exponent", 1.0), 0.0001) + var flat_bonus: float = _get_config_float("flat_bonus", 0.0) + if scale <= 0.0: + scale = 0.0001 + if exponent <= 0.0: + exponent = 0.0001 + + var ratio: float = pow((prestige_level - flat_bonus) / scale, 1.0 / exponent) + if ratio <= 0.0: + return threshold_value + return ratio * threshold_value func _calculate_raw_gain_float(basis_value: BigNumber) -> float: if config == null: @@ -178,11 +289,11 @@ func _calculate_raw_gain_float(basis_value: BigNumber) -> float: match _get_config_int("formula", FORMULA_POWER): FORMULA_TRIANGULAR_INVERSE: - var threshold_value: float = _big_number_to_float(_get_config_threshold()) + var threshold_value: float = _get_config_threshold().to_float() if threshold_value <= 0.0: return 0.0 - var basis_float: float = _big_number_to_float(basis_value) + var basis_float: float = basis_value.to_float() if basis_float <= 0.0: return 0.0 @@ -192,7 +303,7 @@ func _calculate_raw_gain_float(basis_value: BigNumber) -> float: var target_total: float = (sqrt(1.0 + 8.0 * k) - 1.0) * 0.5 if _is_cumulative_basis(): - return target_total - _big_number_to_float(total_prestige_earned) + return target_total - total_prestige_earned.to_float() return target_total _: var ratio: float = _big_number_ratio_to_float(basis_value, _get_config_threshold()) @@ -201,20 +312,26 @@ func _calculate_raw_gain_float(basis_value: BigNumber) -> float: var value: float = maxf(_get_config_float("scale", 0.0), 0.0) * pow(ratio, maxf(_get_config_float("exponent", 0.0001), 0.0001)) + _get_config_float("flat_bonus", 0.0) if _is_cumulative_basis(): - return value - _big_number_to_float(total_prestige_earned) + return value - total_prestige_earned.to_float() return value func _get_basis_value() -> BigNumber: if config == null: return BigNumber.from_float(0.0) - var source_currency_id: StringName = get_source_currency_id() - if source_currency_id == &"": + if game_state == null: return BigNumber.from_float(0.0) + + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) - match _get_config_int("basis", BASIS_LIFETIME_TOTAL): + match basis_type: + BASIS_ALL_CURRENCIES: + return _get_total_all_currencies() BASIS_RUN_TOTAL: - var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id) + var source_currency_id: StringName = get_source_currency_id() + if source_currency_id == &"": + return BigNumber.from_float(0.0) + var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id) var run_total: BigNumber = lifetime_now.subtract(run_start_total_source_currency) if run_total.mantissa < 0.0: return BigNumber.from_float(0.0) @@ -222,11 +339,37 @@ func _get_basis_value() -> BigNumber: BASIS_RUN_MAX: return _copy_big_number(run_peak_source_currency) _: - return _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id)) + var source_currency_id: StringName = get_source_currency_id() + if source_currency_id == &"": + return BigNumber.from_float(0.0) + return _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id)) + +func _get_total_all_currencies() -> BigNumber: + var total: BigNumber = BigNumber.from_float(0.0) + var currency_ids: Array[StringName] = get_tracked_currency_ids() + + for currency_id in currency_ids: + var currency_total: BigNumber = game_state.get_total_currency_acquired_by_id(currency_id) + total.add_in_place(currency_total) + + return total func _initialize_run_tracking_from_current_state() -> void: if config == null: return + + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) + + if basis_type == BASIS_ALL_CURRENCIES: + run_start_total_source_currency = _get_total_all_currencies() + run_peak_source_currency = BigNumber.from_float(0.0) + var currency_ids: Array[StringName] = get_tracked_currency_ids() + for currency_id in currency_ids: + var current: BigNumber = game_state.get_currency_amount_by_id(currency_id) + if current.is_greater_than(run_peak_source_currency): + run_peak_source_currency = _copy_big_number(current) + _save_state_to_game_state() + return var source_currency_id: StringName = get_source_currency_id() if source_currency_id == &"": @@ -234,13 +377,31 @@ func _initialize_run_tracking_from_current_state() -> void: run_peak_source_currency = BigNumber.from_float(0.0) return - run_start_total_source_currency = _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id)) - run_peak_source_currency = _copy_big_number(GameState.get_currency_amount_by_id(source_currency_id)) + if game_state == null: + run_start_total_source_currency = BigNumber.from_float(0.0) + run_peak_source_currency = BigNumber.from_float(0.0) + return + + run_start_total_source_currency = _copy_big_number(game_state.get_total_currency_acquired_by_id(source_currency_id)) + run_peak_source_currency = _copy_big_number(game_state.get_currency_amount_by_id(source_currency_id)) _save_state_to_game_state() func _validate_loaded_run_tracking() -> void: if config == null: return + + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) + + if basis_type == BASIS_ALL_CURRENCIES: + var currency_ids: Array[StringName] = get_tracked_currency_ids() + var peak: BigNumber = BigNumber.from_float(0.0) + for currency_id in currency_ids: + var current: BigNumber = game_state.get_currency_amount_by_id(currency_id) + if current.is_greater_than(peak): + peak = _copy_big_number(current) + if peak.is_greater_than(run_peak_source_currency): + run_peak_source_currency = _copy_big_number(peak) + return var source_currency_id: StringName = get_source_currency_id() if source_currency_id == &"": @@ -248,11 +409,14 @@ func _validate_loaded_run_tracking() -> void: run_peak_source_currency = BigNumber.from_float(0.0) return - var current_currency: BigNumber = GameState.get_currency_amount_by_id(source_currency_id) + if game_state == null: + return + + var current_currency: BigNumber = game_state.get_currency_amount_by_id(source_currency_id) if current_currency.is_greater_than(run_peak_source_currency): run_peak_source_currency = _copy_big_number(current_currency) - var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id) + var lifetime_now: BigNumber = game_state.get_total_currency_acquired_by_id(source_currency_id) if run_start_total_source_currency.is_greater_than(lifetime_now): run_start_total_source_currency = _copy_big_number(lifetime_now) @@ -278,7 +442,18 @@ func _deserialize_state(raw_state: Dictionary) -> void: current_prestige_unspent = _copy_big_number(total_prestige_earned) func _save_state_to_game_state() -> void: - GameState.set_external_save_data(SAVE_KEY, _serialize_state()) + if game_state: + game_state.set_external_save_data(SAVE_KEY, _serialize_state()) + +func _reset_all_buff_levels() -> void: + if game_state == null: + return + + for buff in game_state.get_all_buffs(): + if buff == null: + continue + game_state.set_buff_level(buff.id, 0) + game_state.set_buff_unlocked(buff.id, false) func _reinitialize_generators_after_prestige_reset() -> void: var scene_root: Node = get_tree().current_scene @@ -316,14 +491,7 @@ func _big_number_ratio_to_float(a: BigNumber, b: BigNumber) -> float: return (a.mantissa / b.mantissa) * pow(10.0, float(exponent_delta)) -func _big_number_to_float(value: BigNumber) -> float: - if value.mantissa <= 0.0: - return 0.0 - if value.exponent > 308: - return INF - if value.exponent < -308: - return 0.0 - return value.mantissa * pow(10.0, float(value.exponent)) + func _normalize_currency_id(currency_id: StringName) -> StringName: var normalized: String = String(currency_id).to_lower().strip_edges() @@ -341,33 +509,18 @@ func _to_non_negative_int(value: Variant, fallback: int = 0) -> int: func _is_config_valid() -> bool: if config == null: return false - if not config.has_method("is_valid"): - return false - - return bool(config.call("is_valid")) + return config.is_valid() func _is_cumulative_basis() -> bool: - if config != null and config.has_method("is_cumulative_basis"): - return bool(config.call("is_cumulative_basis")) - - return _get_config_int("basis", BASIS_LIFETIME_TOTAL) != BASIS_RUN_TOTAL + var basis_type: int = _get_config_int("basis", BASIS_LIFETIME_TOTAL) + return basis_type != BASIS_RUN_TOTAL and basis_type != BASIS_ALL_CURRENCIES func _round_gain(value: float) -> float: - if config != null and config.has_method("round_value"): - var rounded: Variant = config.call("round_value", value) - if rounded is float: - return rounded - if rounded is int: - return float(rounded) - - return floorf(value) + return config.round_value(value) func _get_config_threshold() -> BigNumber: - if config != null and config.has_method("get_threshold"): - var threshold: Variant = config.call("get_threshold") - if threshold is BigNumber: - return threshold - + if config != null: + return config.get_threshold() return BigNumber.from_float(1.0) func _get_config_string_name(property_name: String, fallback: StringName = &"") -> StringName: diff --git a/core/prestige/prestige_panel.gd b/core/prestige/prestige_panel.gd index 2994d04..5530a60 100644 --- a/core/prestige/prestige_panel.gd +++ b/core/prestige/prestige_panel.gd @@ -5,44 +5,44 @@ extends PanelContainer @onready var _pending_value: Label = $MarginContainer/VBoxContainer/StatsGrid/PendingValue @onready var _multiplier_value: Label = $MarginContainer/VBoxContainer/StatsGrid/MultiplierValue @onready var _basis_label: Label = $MarginContainer/VBoxContainer/StatsGrid/BasisValue +@onready var _progress_bar: Node = $MarginContainer/VBoxContainer/PrestigeProgressBar @onready var _reset_button: Button = $MarginContainer/VBoxContainer/Buttons/ResetButton @onready var _save_button: Button = $MarginContainer/VBoxContainer/Buttons/SaveButton @onready var _confirm_dialog: ConfirmationDialog = $ConfirmDialog +@onready var game_state: LevelGameState = find_parent("LevelGameState") var _is_waiting_for_confirm: bool = false func _ready() -> void: - var manager: Node = _get_prestige_manager() - if manager != null and manager.has_signal("prestige_state_changed"): - manager.prestige_state_changed.connect(_on_prestige_state_changed) - if manager != null and manager.has_signal("prestige_performed"): - manager.prestige_performed.connect(_on_prestige_performed) - if not GameState.currency_changed.is_connected(_on_currency_changed): - GameState.currency_changed.connect(_on_currency_changed) + 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_performed.connect(_on_prestige_performed) + game_state.currency_changed.connect(_on_currency_changed) _refresh_ui() + func _on_reset_button_pressed() -> void: - var manager: Node = _get_prestige_manager() - if manager == null or not manager.has_method("can_prestige"): + var manager: PrestigeManager = _get_prestige_manager() + + if not bool(manager.can_prestige()): return - if not bool(manager.call("can_prestige")): - return - - var pending: BigNumber = manager.call("calculate_pending_gain") - var source_currency_id: StringName = manager.call("get_source_currency_id") - var source_currency_name: String = GameState.get_currency_name(source_currency_id) + var pending: BigNumber = manager.calculate_pending_gain() + var basis_label: String = String(manager.get_basis_label()) + var basis_value: BigNumber = manager.get_basis_value_for_display() var confirm_message: String = "Reset this run for +%s prestige?\n\nThis resets currencies, owned generators, and buff levels. Lifetime totals and prestige are kept." % pending.to_string_suffix(2) - if not source_currency_name.is_empty(): - confirm_message = "%s\n\nBased on %s progression." % [confirm_message, source_currency_name] + confirm_message = "%s\n\nBased on %s: %s" % [confirm_message, basis_label, basis_value.to_string_suffix(2)] _confirm_dialog.dialog_text = confirm_message _is_waiting_for_confirm = true _confirm_dialog.popup_centered_ratio(0.4) func _on_save_button_pressed() -> void: - GameState.save_game() + if game_state: + game_state.save_game() func _on_prestige_state_changed(_total: BigNumber, _pending: BigNumber) -> void: _refresh_ui() @@ -58,33 +58,33 @@ func _on_confirm_dialog_confirmed() -> void: return _is_waiting_for_confirm = false - var manager: Node = _get_prestige_manager() + var manager: PrestigeManager = _get_prestige_manager() if manager == null: return - manager.call("perform_prestige") + manager.perform_prestige() func _on_confirm_dialog_canceled() -> void: _is_waiting_for_confirm = false func _refresh_ui() -> void: - var manager: Node = _get_prestige_manager() + var manager: PrestigeManager = _get_prestige_manager() if manager == null: visible = false return visible = true - var total: BigNumber = manager.call("get_total_prestige") - var pending: BigNumber = manager.call("calculate_pending_gain") - var multiplier: float = float(manager.call("get_total_multiplier")) - var basis_text: String = String(manager.call("get_basis_label")) - var basis_value: BigNumber = manager.call("get_basis_value_for_display") + var total: BigNumber = manager.get_total_prestige() + var pending: BigNumber = manager.calculate_pending_gain() + var multiplier: float = float(manager.get_total_multiplier()) + var basis_text: String = String(manager.get_basis_label()) + var basis_value: BigNumber = manager.get_basis_value_for_display() _total_value.text = total.to_string_suffix(2) _pending_value.text = pending.to_string_suffix(2) _multiplier_value.text = "x%.2f" % multiplier _basis_label.text = "%s (%s)" % [basis_text, basis_value.to_string_suffix(2)] - _reset_button.disabled = not bool(manager.call("can_prestige")) + _reset_button.disabled = not bool(manager.can_prestige()) -func _get_prestige_manager() -> Node: - return get_node_or_null("/root/PrestigeManager") +func _get_prestige_manager() -> PrestigeManager: + return game_state.prestige_manager diff --git a/core/prestige/prestige_panel.tscn b/core/prestige/prestige_panel.tscn index 57aecd1..a7ce3e5 100644 --- a/core/prestige/prestige_panel.tscn +++ b/core/prestige/prestige_panel.tscn @@ -1,76 +1,83 @@ -[gd_scene load_steps=2 format=3] +[gd_scene format=3 uid="uid://dlidx2x0otpjg"] -[ext_resource type="Script" path="res://core/prestige/prestige_panel.gd" id="1_7fdwq"] +[ext_resource type="Script" uid="uid://dmsmmgtbrul1t" path="res://core/prestige/prestige_panel.gd" id="1_panel"] +[ext_resource type="PackedScene" path="res://core/prestige/prestige_progress_bar.tscn" id="2_7b6yg"] +[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="3_r8h5p"] -[node name="PrestigePanel" type="PanelContainer"] +[node name="PrestigePanel" type="PanelContainer" unique_id=789062217] offset_right = 420.0 offset_bottom = 206.0 -script = ExtResource("1_7fdwq") +script = ExtResource("1_panel") -[node name="MarginContainer" type="MarginContainer" parent="."] +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1133221945] layout_mode = 2 theme_override_constants/margin_left = 10 theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1014101412] layout_mode = 2 theme_override_constants/separation = 8 -[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"] +[node name="Title" type="Label" parent="MarginContainer/VBoxContainer" unique_id=738033951] layout_mode = 2 text = "Prestige" -[node name="StatsGrid" type="GridContainer" parent="MarginContainer/VBoxContainer"] +[node name="StatsGrid" type="GridContainer" parent="MarginContainer/VBoxContainer" unique_id=1121463897] layout_mode = 2 columns = 2 -[node name="TotalTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="TotalTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1462148351] layout_mode = 2 text = "Total" -[node name="TotalValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="TotalValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1607797034] layout_mode = 2 text = "0" -[node name="PendingTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="PendingTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1999999111] layout_mode = 2 text = "Pending" -[node name="PendingValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="PendingValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1731674897] layout_mode = 2 text = "0" -[node name="MultiplierTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="MultiplierTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1338227256] layout_mode = 2 text = "Multiplier" -[node name="MultiplierValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="MultiplierValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1407185183] layout_mode = 2 text = "x1.00" -[node name="BasisTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="BasisTitle" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1092652344] layout_mode = 2 text = "Basis" -[node name="BasisValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid"] +[node name="BasisValue" type="Label" parent="MarginContainer/VBoxContainer/StatsGrid" unique_id=1421212102] layout_mode = 2 text = "-" -[node name="Buttons" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="PrestigeProgressBar" parent="MarginContainer/VBoxContainer" unique_id=619882824 instance=ExtResource("2_7b6yg")] +layout_mode = 2 +config = ExtResource("3_r8h5p") + +[node name="Buttons" type="HBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=634533362] layout_mode = 2 theme_override_constants/separation = 8 -[node name="ResetButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons"] +[node name="ResetButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons" unique_id=379577642] layout_mode = 2 text = "Prestige Reset" -[node name="SaveButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons"] +[node name="SaveButton" type="Button" parent="MarginContainer/VBoxContainer/Buttons" unique_id=1151017798] layout_mode = 2 text = "Save" -[node name="ConfirmDialog" type="ConfirmationDialog" parent="."] +[node name="ConfirmDialog" type="ConfirmationDialog" parent="." unique_id=1893210048] +oversampling_override = 1.0 title = "Prestige Reset" ok_button_text = "Prestige" dialog_text = "Reset this run?" diff --git a/core/prestige/prestige_progress_bar.gd b/core/prestige/prestige_progress_bar.gd new file mode 100644 index 0000000..1ce3463 --- /dev/null +++ b/core/prestige/prestige_progress_bar.gd @@ -0,0 +1,72 @@ +extends HBoxContainer + +@export var config: PrestigeConfig + +@onready var _label_start = $LabelStart +@onready var _label_end = $LabelEnd +@onready var _progress_bar = $ProgressBar + +var _basis_value: BigNumber +var _next_threshold: BigNumber +var _connected: bool = false +var _game_state: LevelGameState +var _manager: PrestigeManager + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + _game_state.ready.connect(_on_game_state_ready) + +func _on_game_state_ready() -> void: + _manager = _game_state.prestige_manager + + if _manager == null: + push_warning("PrestigeProgressBar '%s' cannot find PrestigeManager; progress bar disabled." % String(name)) + visible = false + return + + if not _manager.has_signal("prestige_state_changed"): + push_warning("PrestigeManager does not have 'prestige_state_changed' signal; progress bar will not update.") + visible = false + return + + if config == null: + config = _manager.get_config() + + if config == null: + push_warning("PrestigeProgressBar '%s' has no config; progress bar disabled." % String(name)) + visible = false + return + + if _manager.has_signal("prestige_threshold_changed"): + _manager.prestige_threshold_changed.connect(_on_prestige_threshold_changed) + _manager.prestige_state_changed.connect(_on_prestige_state_changed) + _connected = true + _update_progress() + +func _on_prestige_state_changed(_total: BigNumber, _pending: BigNumber) -> void: + _update_progress() + +func _on_prestige_threshold_changed(_threshold: BigNumber) -> void: + _update_progress() + +func _update_progress() -> void: + if not _connected: + return + + if _manager == null or config == null: + return + + _basis_value = _manager.get_basis_value_for_display() + _next_threshold = _manager.get_next_prestige_threshold() + + var basis_float: float = _basis_value.to_float() + var threshold_float: float = _next_threshold.to_float() + + if threshold_float > 0.0 and basis_float >= 0.0: + var ratio: float = minf(basis_float / threshold_float, 1.0) + _progress_bar.value = ratio * 100.0 + else: + _progress_bar.value = 0.0 + + _label_start.text = _basis_value.to_string_suffix(2) + _label_end.text = _next_threshold.to_string_suffix(2) diff --git a/core/prestige/prestige_progress_bar.gd.uid b/core/prestige/prestige_progress_bar.gd.uid new file mode 100644 index 0000000..0b9e974 --- /dev/null +++ b/core/prestige/prestige_progress_bar.gd.uid @@ -0,0 +1 @@ +uid://dsq8yix7kkyaj diff --git a/core/prestige/prestige_progress_bar.tscn b/core/prestige/prestige_progress_bar.tscn new file mode 100644 index 0000000..f10dfd3 --- /dev/null +++ b/core/prestige/prestige_progress_bar.tscn @@ -0,0 +1,22 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://core/prestige/prestige_progress_bar.gd" id="1_pbg"] + +[node name="PrestigeProgressBar" type="HBoxContainer"] +offset_right = 298.0 +offset_bottom = 49.0 +alignment = 1 +script = ExtResource("1_pbg") + +[node name="LabelStart" type="Label" parent="."] +layout_mode = 2 +text = "0" + +[node name="ProgressBar" type="ProgressBar" parent="."] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +show_percentage = true + +[node name="LabelEnd" type="Label" parent="."] +layout_mode = 2 +text = "0" diff --git a/core/research/README.md b/core/research/README.md new file mode 100644 index 0000000..44737cc --- /dev/null +++ b/core/research/README.md @@ -0,0 +1,488 @@ +# Research Module Documentation + +## Overview + +The `research/` subfolder implements a production-based research system where generator output earns +research XP, automatically leveling up to provide production multipliers. Research tracks are tied to +specific generators and can be enhanced with purchasable buffs that increase XP gain. + +## Files + +| File | Purpose | +|------|---------| +| `research_catalogue.gd` | Resource holding all research configurations | +| `research_panel.gd` | UI panel showing all research tracks | +| `research_row.gd` | UI row component for individual research display | +| `../generator/research_buff_calculator.gd` | Static utility for buff multiplier calculations | + +## Architecture + +``` +LevelGameState (Node2D) +├── research_xp: Dictionary +├── research_levels: Dictionary +├── research_catalogue: ResearchCatalogue +│ └── ResearchPanel (UI, always visible) +│ ├── ResearchRow (for each research track) +│ │ ├── Name & icon +│ │ ├── Level display +│ │ ├── Progress bar (percentage) +│ │ ├── Multiplier indicator +│ │ └── Active buff display +└── (buff calculation delegated to ResearchBuffCalculator static methods) +``` + +## Data Flow + +``` +Generator Production (currency_per_cycle) + │ + ▼ +LevelGameState.add_research_xp() + 1. base_xp = currency_produced * xp_per_currency_produced + 2. buff_multiplier = ResearchBuffCalculator.apply_buffs(research, buffs) + 3. actual_xp = base_xp * buff_multiplier + 4. research_xp += actual_xp + 5. Check level threshold → auto-level + 6. Emit research_level_up signal + │ + ▼ +ResearchState {xp: 150.5, level: 3} + │ + ▼ +CurrencyGenerator.get_research_multiplier() → 1.3 (30% bonus) + │ + ▼ +Production *= 1.3 +``` + +## ResearchData + +Resource class defining research configuration for a generator (defined in `core/generator/research_data.gd`). + +### Key Properties + +```gdscript +@export var id: StringName # Unique research identifier +@export var generator_id: StringName # Which generator this affects +@export var name: String # Display name +@export_multiline var description: String = "" # Description text +@export var icon: Texture2D # Icon for UI + +# XP Configuration +@export var xp_per_currency_produced: float = 0.01 # XP per 1 currency unit +@export var base_xp_required: float = 100.0 # XP needed for level 1 +@export var xp_growth_multiplier: float = 1.5 # XP requirement growth per level + +# Multiplier Configuration +@export var base_multiplier: float = 1.0 # Production multiplier at level 0 +@export var multiplier_per_level: float = 0.1 # +10% production per level + +# Associated Buff (single buff per research track) +@export var associated_buff_id: StringName # Buff that increases XP gain +``` + +### Key Methods + +```gdscript +func get_xp_required_for_level(level: int) -> float: + """XP needed to reach this level from previous""" + return base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0))) + +func get_xp_required_for_level_big(level: int) -> BigNumber: + """XP needed to reach this level from previous (BigNumber)""" + return BigNumber.from_float(base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0)))) + +func get_total_xp_for_level(level: int) -> BigNumber: + """Total cumulative XP needed to reach this level from level 0 (BigNumber)""" + if level <= 0: return BigNumber.new(0.0, 0) + var total: BigNumber = BigNumber.new(0.0, 0) + for l in range(1, level + 1): + total = total.add(get_xp_required_for_level_big(l)) + return total + +func get_level_for_xp(xp: BigNumber) -> int: + """Calculate level from total accumulated BigNumber XP (loop-based)""" + if xp.mantissa == 0.0: return 0 + var level: int = 0 + while true: + var next_xp: BigNumber = get_total_xp_for_level(level + 1) + if xp.compare_to(next_xp) < 0: break + level += 1 + return level + +func get_multiplier_for_level(level: int) -> float: + """Production multiplier at given level""" + return base_multiplier + (multiplier_per_level * float(maxi(level, 0))) + +func get_xp_progress(xp: float) -> float: + """Returns 0.0-1.0 progress to next level""" +``` + +## ResearchCatalogue + +Resource class holding all research configurations. + +### Structure + +```gdscript +class_name ResearchCatalogue +extends Resource + +@export var research_entries: Array[ResearchData] = [] + +func get_research_by_id(research_id: StringName) -> ResearchData +func get_research_by_generator_id(generator_id: StringName) -> ResearchData +func get_all_research() -> Array[ResearchData] +``` + +### Usage + +Referenced directly by `LevelGameState` via `@export var research_catalogue: ResearchCatalogue`. + +## ResearchBuffCalculator + +Static utility class for calculating research XP buff multipliers. + +### Location + +`res://core/generator/research_buff_calculator.gd` + +### Key Methods + +```gdscript +static func apply_buffs(research: ResearchData, game_state: LevelGameState) -> float: + """Calculate total buff multiplier for research XP gain. Returns multiplicative factor.""" + var multiplier: float = 1.0 + if research.associated_buff_id.is_empty(): + return multiplier + + var buff: GeneratorBuffData = game_state.get_buff(research.associated_buff_id) + if buff != null and game_state.is_buff_active(buff.id): + var buff_level: int = game_state.get_buff_level(buff.id) + multiplier *= ResearchBuffCalculator._calculate_buff_multiplier(buff, buff_level) + + return multiplier + +static func _calculate_buff_multiplier(buff: GeneratorBuffData, level: int) -> float: + """Calculate buff effect multiplier at given level.""" + return buff.get_effect_multiplier(level) +``` + +### Usage + +Called from `LevelGameState.add_research_xp()`: + +```gdscript +func add_research_xp(research_id: StringName, xp: BigNumber) -> void: + var research: ResearchData = research_catalogue.get_research_by_id(research_id) + if research == null: + return + + # Apply buff multipliers + var buff_multiplier: float = ResearchBuffCalculator.apply_buffs(research, self) + var multiplier_bn: BigNumber = BigNumber.from_float(buff_multiplier) + var actual_xp: BigNumber = xp.multiply(multiplier_bn) + + # Store XP and check for level-up + var old_level: int = get_research_level(research_id) + research_xp[research_id] = research_xp.get(research_id, BigNumber.new(0.0, 0)).add(actual_xp) + var new_level: int = research.get_level_for_xp(research_xp[research_id]) + + if new_level > old_level: + research_levels[research_id] = new_level + research_level_up.emit(research_id, old_level, new_level) + + research_xp_changed.emit(research_id, research_xp[research_id]) +``` + +## LevelGameState Extensions + +### State Variables +```gdscript +var research_xp: Dictionary = {} # {research_id: BigNumber} +var research_levels: Dictionary = {} # {research_id: level_int} +# research_tracker removed - buff calculation moved to static utility +``` + +### Signals +```gdscript +signal research_xp_changed(research_id: StringName, new_xp: BigNumber) +signal research_level_up(research_id: StringName, old_level: int, new_level: int) +``` + +### Key Methods +```gdscript +func register_research(research_id: StringName) -> void: + """Initialize research state from catalogue (BigNumber XP)""" + if not research_xp.has(research_id): + research_xp[research_id] = BigNumber.new(0.0, 0) + +func get_research_xp(research_id: StringName) -> BigNumber + +func get_research_level(research_id: StringName) -> int + +func get_research_multiplier(research_id: StringName) -> float: + """Returns production multiplier from research level""" + var level: int = get_research_level(research_id) + return research.get_multiplier_for_level(level) + +func add_research_xp(research_id: StringName, xp: BigNumber) -> void: + """Add XP (BigNumber) with buff multipliers applied, auto-level if threshold reached""" + var research: ResearchData = research_catalogue.get_research_by_id(research_id) + if research == null: + return + + var old_level: int = get_research_level(research_id) + var current_xp: BigNumber = get_research_xp(research_id) + + # Apply buff multipliers via static utility + var buff_multiplier: float = ResearchBuffCalculator.apply_buffs(research, self) + var multiplier_bn: BigNumber = BigNumber.from_float(buff_multiplier) + var actual_xp: BigNumber = xp.multiply(multiplier_bn) + + research_xp[research_id] = current_xp.add(actual_xp) + var new_level: int = research.get_level_for_xp(research_xp[research_id]) + + if new_level > old_level: + research_levels[research_id] = new_level + research_level_up.emit(research_id, old_level, new_level) + + research_xp_changed.emit(research_id, research_xp[research_id]) +``` + +### Save/Load Integration +```gdscript +const RESEARCH_XP_KEY = "research_xp" +const RESEARCH_LEVELS_KEY = "research_levels" +const CURRENT_SAVE_FORMAT_VERSION = 5 # Bumped from 4 to support BigNumber + +# In save_game(): +var save_data = { + # ... existing fields ... + RESEARCH_XP_KEY: _serialize_research_xp(), # Serialize BigNumber as {m, e} + RESEARCH_LEVELS_KEY: research_levels.duplicate(), +} + +func _serialize_research_xp() -> Dictionary: + var result: Dictionary = {} + for research_id in research_xp.keys(): + var xp: BigNumber = research_xp[research_id] + result[research_id] = xp.serialize() + return result + +# In load_game(): +research_xp = _deserialize_research_xp(parsed_data.get(RESEARCH_XP_KEY, {})) +research_levels = parsed_data.get(RESEARCH_LEVELS_KEY, {}) + +func _deserialize_research_xp(raw: Variant) -> Dictionary: + var result: Dictionary = {} + if raw is Dictionary: + for research_id in raw.keys(): + var serialized: Variant = raw[research_id] + if serialized is Dictionary: + result[research_id] = BigNumber.deserialize(serialized) + return result + +# In reset_for_prestige(): +research_xp.clear() +research_levels.clear() +``` + +## CurrencyGenerator Extensions + +### Production Tick Integration +```gdscript +func _grant_cycle_income(cycle_count: int) -> void: + var produced: BigNumber = ... + _add_currency(produced) + production_tick.emit(produced, cycle_count, owned) + + # Award research XP (BigNumber) - buff multipliers applied in LevelGameState + if data.research_data != null: + var amount_float: float = produced.mantissa * pow(10.0, float(produced.exponent)) + var xp: BigNumber = BigNumber.from_float(data.research_data.xp_per_currency_produced * amount_float) + game_state.add_research_xp(data.research_data.id, xp) +``` + +### Research Multiplier + +```gdscript +func get_research_multiplier() -> float: + if data.research_data == null: + return 1.0 + return game_state.get_research_multiplier(data.research_data.id) + +func get_effective_auto_run_multiplier() -> float: + return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_multiplier() +``` + +## GeneratorBuffData Extensions + +### New Buff Kind + +```gdscript +enum BuffKind { + AUTO_PRODUCTION_MULTIPLIER, + MANUAL_CLICK_MULTIPLIER, + RESOURCE_PURCHASE, + RESEARCH_XP_MULTIPLIER # Increases research XP gain +} +``` + +### New Properties (for RESEARCH_XP_MULTIPLIER buffs) + +```gdscript +@export var research_target_id: StringName # Which research track this buff affects +@export var xp_multiplier_per_level: float = 0.1 # +10% XP per level +``` + +## ResearchPanel + +UI component showing all research tracks (always visible, like GeneratorPanel). + +### Layout + +``` +ResearchPanel (ScrollContainer) +└── VBoxContainer + ├── TitleLabel ("Research") + └── ResearchRows (VBoxContainer) + └── ResearchRow (for each research track) +``` + +### Key Methods + +```gdscript +func _build_research_rows() -> void: + for research in research_catalogue.get_all_research(): + var row = RESEARCH_ROW_SCENE.instantiate() + row.setup(research) + _research_rows[research.id] = row + +func _refresh_row(row) -> void: + var research_id: StringName = row.get_research_id() + var xp: BigNumber = game_state.get_research_xp(research_id) + var level: int = game_state.get_research_level(research_id) + var progress: float = research.get_xp_progress(xp) + var multiplier: float = research.get_multiplier_for_level(level) + var xp_current: BigNumber = xp.subtract(research.get_total_xp_for_level(level)) + var xp_needed: BigNumber = research.get_xp_required_for_level_big(level + 1) + row.update_display(level, progress, multiplier, xp_current, xp_needed) +``` + +### Signals Connected + +- `research_xp_changed` - Refresh progress bars +- `research_level_up` - Refresh level and multiplier displays + +## ResearchRow + +UI row component for individual research display. + +### UI Layout + +``` +ResearchRow (HBoxContainer) +├── Icon (TextureRect) +├── VBoxContainer +│ ├── NameLabel ("Gold Mine Research") +│ └── LevelLabel ("Level 3") +├── ProgressBar (with percentage label "75%") +├── MultiplierLabel ("+30%") +└── BuffLabel ("Mining Expert: +20% XP") +``` + +### Key Methods + +```gdscript +func setup(research: ResearchData) -> void: + _name_label.text = research.name + _icon.texture = research.icon + +func update_display(level: int, progress: float, multiplier: float, xp_current: BigNumber, xp_needed: BigNumber) -> void: + _level_label.text = "Level %d" % level + _progress_bar.value = progress * 100 + _progress_label.text = "%d%%" % int(progress * 100) + _multiplier_label.text = "+%d%%" % int((multiplier - 1.0) * 100) + + # Update buff label if buff is active + if _research.associated_buff_id != &"": + var buff: GeneratorBuffData = game_state.get_buff(_research.associated_buff_id) + if is_buff_active(buff.id): + _buff_label.text = "%s: +%d%% XP" % [buff.text, int(effect * 100)] +``` + +## Formulas + +### XP Calculation + +``` +base_xp = currency_produced * xp_per_currency_produced +actual_xp = base_xp * buff_multiplier +``` + +### Level Progression + +``` +xp_required_for_level(n) = base_xp_required * (xp_growth_multiplier ^ (n - 1)) +total_xp_for_level(n) = sum(xp_required_for_level(i) for i in 1..n) +level = get_level_for_xp(total_xp) +``` + +### Production Multiplier + +``` +multiplier = base_multiplier + (multiplier_per_level * level) +effective_production = base_production * multiplier +``` + +### Buff XP Multiplier + +``` +xp_multiplier = 1.0 +for each active buff targeting this research: + xp_multiplier *= buff.get_effect_multiplier(level) +actual_xp = base_xp * xp_multiplier +``` + +## Design Decisions + +| Decision | Value | +|----------|-------| +| ResearchCatalogue reference | Direct `@export var research_catalogue: ResearchCatalogue` in LevelGameState | +| Buff calculation | Static utility (`ResearchBuffCalculator`) - no dedicated tracker node | +| Progress bar display | Percentage format (e.g., "75%") | +| Buff display | Shows buff name and effect (e.g., "Mining Expert: +20% XP") | +| Research unlock | Starts unlocked | +| XP formula | Production-based: `XP = currency_produced * xp_per_currency_produced` | +| Level-up | Auto-level when XP threshold reached | +| Buffs | Single buff per research track, multiplicative XP bonus | +| Prestige | Research levels and XP reset to 0 | +| ResearchPanel visibility | Always visible (like GeneratorPanel) | + +## System Behavior + +1. **Generator produces currency** → `production_tick` signal emitted +2. **CurrencyGenerator calls** → `game_state.add_research_xp()` with base XP +3. **LevelGameState applies buffs** → `ResearchBuffCalculator.apply_buffs()` calculates multiplier +4. **XP awarded** → stored in `research_xp` dictionary +5. **Level check** → if XP exceeds threshold, auto-level and emit `research_level_up` +6. **Multiplier applied** → `get_research_multiplier()` returns updated production bonus +7. **UI refresh** → `ResearchPanel` updates progress bars and level displays + +## Prestige Integration + +On prestige reset: +1. `research_xp.clear()` - All XP lost +2. `research_levels.clear()` - All levels reset to 0 +3. Buff purchases persist (separate state) +4. Research tracks remain unlocked + +## See Also + +- `core/level_game_state.gd` - Research state storage and management +- `core/generator/currency_generator_data.gd` - Links to ResearchData +- `core/generator/generator_buff_data.gd` - RESEARCH_XP_MULTIPLIER buff type +- `core/generator/research_buff_calculator.gd` - Static buff calculation utility +- `core/research/research_catalogue.gd` - Research configuration resource diff --git a/core/research/TODO.md b/core/research/TODO.md new file mode 100644 index 0000000..5b441cd --- /dev/null +++ b/core/research/TODO.md @@ -0,0 +1,616 @@ +# Research Feature Implementation Plan + +## Overview + +A production-based research system where: +- **Generator production → Research XP → Auto-level → Production multiplier** +- **Research buffs** (purchased with currency) increase XP gain multiplicatively +- **One research track per generator** with a single associated buff +- **Progress bar shows percentage** to next level +- **Buff display shows name and effect** +- **Research tracks start unlocked** +- **Resets on prestige** (levels and XP lost) + +--- + +## System Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ LevelGameState │ +│ - research_xp: Dictionary │ +│ - research_levels: Dictionary │ +│ - research_catalogue: ResearchCatalogue │ +│ - add_research_xp() applies buffs via ResearchBuffCalculator │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ CurrencyGen │ │ ResearchData │ │ ResearchPanel │ +│ (Gold Mine) │──│ (Resource) │──│ (Always visible)│ +│ - produces gold │ │ - XP config │ │ - Shows all │ +│ - emits signal │ │ - Multiplier │ │ research │ +└─────────────────┘ └─────────────────┘ │ - Progress bars │ + └─────────────────┘ + │ + ▼ +┌───────────────────────────────────────────────────────────────┐ +│ ResearchBuffCalculator (static utility) │ +│ - apply_buffs() calculates XP multiplier from active buffs │ +└───────────────────────────────────────────────────────────────┘ +``` + +--- + +## Data Flow + +``` +Generator Production (currency_per_cycle) + │ + ▼ +┌──────────────────────┐ +│ CurrencyGen │ +│ emits production_tick │ +└──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ LevelGameState.add_research_xp() │ +│ 1. base_xp = currency_produced * rate │ +│ 2. buff_mult = ResearchBuffCalculator.apply_buffs() +│ 3. actual_xp = base_xp * buff_mult │ +│ 4. research_xp += actual_xp │ +│ 5. Check level threshold → auto-level │ +│ 6. Emit research_level_up signal │ +└─────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────┐ +│ ResearchState │ +│ {xp: 150.5, level: 3}│ +└──────────────────────┘ + │ + ▼ +┌──────────────────────┐ +│ CurrencyGen │ +│ get_research_multiplier() → 1.3 (30% bonus) +└──────────────────────┘ + │ + ▼ +┌──────────────────────┐ +│ Production *= 1.3 │ +└──────────────────────┘ +``` + +--- + +## Component Specifications + +### 1. ResearchData (core/generator/research_data.gd) + +**Purpose:** Resource defining research configuration for a generator + +**Location:** `res://core/generator/research_data.gd` + +**Key Properties:** +```gdscript +@export var id: StringName # Unique research identifier +@export var generator_id: StringName # Which generator this research affects +@export var name: String # Display name +@export_multiline var description: String = "" # Description text +@export var icon: Texture2D # Icon for UI + +# XP Configuration +@export var xp_per_currency_produced: float = 0.01 # XP per 1 currency unit produced +@export var base_xp_required: float = 100.0 # XP needed for level 1 +@export var xp_growth_multiplier: float = 1.5 # XP requirement growth per level + +# Multiplier Configuration +@export var base_multiplier: float = 1.0 # Production multiplier at level 0 +@export var multiplier_per_level: float = 0.1 # +10% production per level + +# Associated Buff (single buff per research track) +@export var associated_buff_id: StringName # Buff that increases XP gain +``` + +**Key Methods:** +```gdscript +func get_xp_required_for_level(level: int) -> float: + """XP needed to reach this level from previous""" + return base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0))) + +func get_total_xp_for_level(level: int) -> float: + """Total cumulative XP needed to reach this level from level 0""" + if level <= 0: + return 0.0 + var total: float = 0.0 + for l in range(1, level + 1): + total += get_xp_required_for_level(l) + return total + +func get_level_for_xp(xp: float) -> int: + """Calculate level from total accumulated XP""" + var level: int = 0 + var cumulative_xp: float = 0.0 + while true: + var next_xp: float = cumulative_xp + get_xp_required_for_level(level + 1) + if xp < next_xp: + break + level += 1 + cumulative_xp = next_xp + return level + +func get_multiplier_for_level(level: int) -> float: + """Production multiplier at given level""" + return base_multiplier + (multiplier_per_level * float(maxi(level, 0))) + +func get_xp_progress(xp: float) -> float: + """Returns 0.0-1.0 progress to next level""" + if xp <= 0.0: + return 0.0 + var current_level: int = get_level_for_xp(xp) + if current_level < 0: + return 0.0 + + var xp_at_current_level: float = get_total_xp_for_level(current_level) + var xp_needed_for_next: float = get_xp_required_for_level(current_level + 1) + var xp_in_current_level: float = xp - xp_at_current_level + + return clampf(xp_in_current_level / xp_needed_for_next, 0.0, 1.0) +``` + +--- + +### 2. ResearchCatalogue (core/research/research_catalogue.gd) + +**Purpose:** Resource holding all research configurations + +**Location:** `res://core/research/research_catalogue.gd` + +**Structure:** +```gdscript +class_name ResearchCatalogue +extends Resource + +@export var research_entries: Array[ResearchData] = [] + +func get_research_by_id(research_id: StringName) -> ResearchData: + for entry in research_entries: + if entry.id == research_id: + return entry + return null + +func get_research_by_generator_id(generator_id: StringName) -> ResearchData: + for entry in research_entries: + if entry.generator_id == generator_id: + return entry + return null + +func get_all_research() -> Array[ResearchData]: + return research_entries.duplicate() +``` + +**Usage:** Referenced directly by LevelGameState via `@export var research_catalogue: ResearchCatalogue` + +--- + +### 3. ResearchBuffCalculator (core/generator/research_buff_calculator.gd) + +**Purpose:** Static utility class for calculating research XP buff multipliers + +**Location:** `res://core/generator/research_buff_calculator.gd` + +**Key Methods:** +```gdscript +static func apply_buffs(research: ResearchData, game_state: LevelGameState) -> float: + """Calculate total buff multiplier for research XP gain. Returns multiplicative factor.""" + var multiplier: float = 1.0 + if research.associated_buff_id.is_empty(): + return multiplier + + var buff: GeneratorBuffData = game_state.get_buff(research.associated_buff_id) + if buff != null and game_state.is_buff_active(buff.id): + var buff_level: int = game_state.get_buff_level(buff.id) + multiplier *= _calculate_buff_multiplier(buff, buff_level) + + return multiplier + +static func _calculate_buff_multiplier(buff: GeneratorBuffData, level: int) -> float: + """Calculate buff effect multiplier at given level.""" + return buff.get_effect_multiplier(level) +``` + +**Usage:** Called from `LevelGameState.add_research_xp()` to apply buffs before storing XP + +--- + +### 4. LevelGameState Extensions (core/level_game_state.gd) + +**New State Variables:** +```gdscript +var research_xp: Dictionary = {} # {research_id: BigNumber} +var research_levels: Dictionary = {} # {research_id: level_int} +# research_tracker removed - buff calculation moved to static utility +``` + +**New Signals:** +```gdscript +signal research_xp_changed(research_id: StringName, new_xp: BigNumber) +signal research_level_up(research_id: StringName, old_level: int, new_level: int) +``` + +**New Properties:** +```gdscript +@export var research_catalogue: ResearchCatalogue +``` + +**New Methods:** +```gdscript +func register_research(research_id: StringName) -> void: + if not research_xp.has(research_id): + research_xp[research_id] = BigNumber.new(0.0, 0) + if not research_levels.has(research_id): + research_levels[research_id] = 0 + +func get_research_xp(research_id: StringName) -> BigNumber: + return research_xp.get(research_id, BigNumber.new(0.0, 0)) + +func get_research_level(research_id: StringName) -> int: + return research_levels.get(research_id, 0) + +func get_research_multiplier(research_id: StringName) -> float: + var level: int = get_research_level(research_id) + var research: ResearchData = _get_research_data(research_id) + if research == null: + return 1.0 + return research.get_multiplier_for_level(level) + +func add_research_xp(research_id: StringName, xp: BigNumber) -> void: + """Add XP with buff multipliers applied, auto-level if threshold reached""" + var research: ResearchData = research_catalogue.get_research_by_id(research_id) + if research == null: + return + + var old_level: int = get_research_level(research_id) + var current_xp: BigNumber = get_research_xp(research_id) + + # Apply buff multipliers via static utility + var buff_multiplier: float = ResearchBuffCalculator.apply_buffs(research, self) + var multiplier_bn: BigNumber = BigNumber.from_float(buff_multiplier) + var actual_xp: BigNumber = xp.multiply(multiplier_bn) + + research_xp[research_id] = current_xp.add(actual_xp) + var new_level: int = research.get_level_for_xp(research_xp[research_id]) + + if new_level > old_level: + research_levels[research_id] = new_level + research_level_up.emit(research_id, old_level, new_level) + + research_xp_changed.emit(research_id, research_xp[research_id]) + +func _get_research_data(research_id: StringName) -> ResearchData: + if research_catalogue == null: + return null + return research_catalogue.get_research_by_id(research_id) +``` + +**Save/Load Integration:** +```gdscript +const RESEARCH_XP_KEY = "research_xp" +const RESEARCH_LEVELS_KEY = "research_levels" + +# In save_game(): +var save_data = { + # ... existing fields ... + RESEARCH_XP_KEY: research_xp.duplicate(), + RESEARCH_LEVELS_KEY: research_levels.duplicate(), +} + +# In load_game(): +if parsed_data.has(RESEARCH_XP_KEY): + research_xp = parsed_data.get(RESEARCH_XP_KEY, {}) +if parsed_data.has(RESEARCH_LEVELS_KEY): + research_levels = parsed_data.get(RESEARCH_LEVELS_KEY, {}) + +# In reset_for_prestige(): +research_xp.clear() +research_levels.clear() +``` + +**Initialization in _ready():** +```gdscript +# Initialize research state from catalogue +if research_catalogue: + for research in research_catalogue.get_all_research(): + register_research(research.id) +``` + +--- + +### 5. CurrencyGeneratorData Extensions (core/generator/currency_generator_data.gd) + +**New Property:** +```gdscript +@export var research_data: ResearchData # Link to research configuration +``` + +--- + +### 6. CurrencyGenerator Extensions (core/generator/currency_generator.gd) + +**In `_grant_cycle_income()`:** +```gdscript +func _grant_cycle_income(cycle_count: int) -> void: + if data == null or cycle_count <= 0: + return + + var effective_run_multiplier: float = get_effective_auto_run_multiplier() + var per_cycle: float = data.production_per_cycle(owned, effective_run_multiplier, purchased_count) + if per_cycle <= 0.0: + return + + var produced: BigNumber = BigNumber.from_float(per_cycle).multiply(BigNumber.from_float(float(cycle_count))) + _add_currency(produced) + production_tick.emit(produced, cycle_count, owned) + + # Award research XP - buff multipliers applied in LevelGameState + if data.research_data != null: + var xp: float = data.research_data.xp_per_currency_produced * produced.to_float() + game_state.add_research_xp(data.research_data.id, BigNumber.from_float(xp)) +``` + +**New Method:** +```gdscript +func get_research_multiplier() -> float: + if data == null or data.research_data == null: + return 1.0 + if game_state == null: + return 1.0 + return game_state.get_research_multiplier(data.research_data.id) +``` + +**Modify `get_effective_auto_run_multiplier()`:** +```gdscript +func get_effective_auto_run_multiplier() -> float: + return run_multiplier * get_research_multiplier() * get_automatic_production_multiplier() * _get_prestige_multiplier() +``` + +--- + +### 7. GeneratorBuffData Extensions (core/generator/generator_buff_data.gd) + +**New Enum Value:** +```gdscript +enum BuffKind { + AUTO_PRODUCTION_MULTIPLIER, + MANUAL_CLICK_MULTIPLIER, + RESOURCE_PURCHASE, + RESEARCH_XP_MULTIPLIER # NEW: Increases research XP gain +} +``` + +**New Properties (for RESEARCH_XP_MULTIPLIER buffs):** +```gdscript +@export var research_target_id: StringName # Which research track this buff affects +@export var xp_multiplier_per_level: float = 0.1 # +10% XP per level (uses get_effect_multiplier) +``` + +**Note:** Buff names are configured in the `text` field of each `GeneratorBuffData` instance + +--- + +### 8. ResearchRow (core/research/research_row.tscn + .gd) + +**Location:** +- Script: `res://core/research/research_row.gd` +- Scene: `res://core/research/research_row.tscn` + +**UI Layout:** +``` +ResearchRow (HBoxContainer) +├── Icon (TextureRect) +├── VBoxContainer +│ ├── NameLabel ("Gold Mine Research") +│ └── LevelLabel ("Level 3") +├── ProgressBar (with percentage label "75%") +├── MultiplierLabel ("+30%") +└── BuffLabel ("Mining Expert: +20% XP") +``` + +**Script Methods:** +```gdscript +func setup(research: ResearchData) -> void: + """Initialize row with research data""" + _research = research + _name_label.text = research.name + _icon.texture = research.icon + +func update_display(level: int, progress: float, multiplier: float, xp_current: float, xp_needed: float) -> void: + """Update UI elements""" + _level_label.text = "Level %d" % level + _progress_bar.value = progress * 100 + _progress_label.text = "%d%%" % int(progress * 100) + _multiplier_label.text = "+%d%%" % int((multiplier - 1.0) * 100) + + # Update buff label if buff is active + if _research.associated_buff_id != &"": + var buff: GeneratorBuffData = game_state.get_buff(_research.associated_buff_id) + if buff != null and game_state.is_buff_active(buff.id): + var level: int = game_state.get_buff_level(buff.id) + var effect: float = buff.get_effect_multiplier(level) - 1.0 + _buff_label.text = "%s: +%d%% XP" % [buff.text, int(effect * 100)] +``` + +--- + +### 9. ResearchPanel (core/research/research_panel.tscn + .gd) + +**Location:** +- Script: `res://core/research/research_panel.gd` +- Scene: `res://core/research/research_panel.tscn` + +**Layout:** +``` +ResearchPanel (ScrollContainer) +├── VBoxContainer +│ ├── TitleLabel ("Research") +│ └── ResearchRows (VBoxContainer) +│ └── ResearchRow (for each research track) +``` + +**Script Methods:** +```gdscript +func _ready() -> void: + _game_state = find_parent("LevelGameState") + if _game_state == null: + return + + _game_state.research_xp_changed.connect(_on_research_xp_changed) + _game_state.research_level_up.connect(_on_research_level_up) + + _build_research_rows() + _refresh_all() + +func _build_research_rows() -> void: + for child in _research_rows_container.get_children(): + child.queue_free() + _research_rows.clear() + + if _game_state.research_catalogue: + for research in _game_state.research_catalogue.get_all_research(): + var row = RESEARCH_ROW_SCENE.instantiate() + _research_rows_container.add_child(row) + row.setup(research) + _research_rows[research.id] = row + +func _refresh_all() -> void: + for row in _research_rows.values(): + _refresh_row(row) + +func _refresh_row(row) -> void: + var research_id: StringName = row.get_research_id() + var xp: float = _game_state.get_research_xp(research_id) + var level: int = _game_state.get_research_level(research_id) + var research: ResearchData = _game_state.research_catalogue.get_research_by_id(research_id) + + if research != null: + var progress: float = research.get_xp_progress(xp) + var multiplier: float = research.get_multiplier_for_level(level) + var xp_current: float = xp - research.get_total_xp_for_level(level) + var xp_needed: float = research.get_xp_required_for_level(level + 1) + + row.update_display(level, progress, multiplier, xp_current, xp_needed) + +func _on_research_xp_changed(research_id: StringName, _new_xp: float) -> void: + if _research_rows.has(research_id): + _refresh_row(_research_rows[research_id]) + +func _on_research_level_up(research_id: StringName, _old_level: int, _new_level: int) -> void: + if _research_rows.has(research_id): + _refresh_row(_research_rows[research_id]) +``` + +--- + +## Implementation Sequence + +| Step | Task | Files | Priority | Status | +|------|------|-------|----------|--------| +| 1 | Create `ResearchData` resource class | `core/generator/research_data.gd` | High | ⬜ | +| 2 | Create `ResearchCatalogue` resource class | `core/research/research_catalogue.gd` | High | ⬜ | +| 3 | Create `ResearchBuffCalculator` static utility | `core/generator/research_buff_calculator.gd` | High | ✅ Done | +| 4 | Add research state to `LevelGameState` | `core/level_game_state.gd` | High | ✅ Done | +| 5 | Add `RESEARCH_XP_MULTIPLIER` buff kind | `core/generator/generator_buff_data.gd` | High | ⬜ | +| 6 | Add `research_data` export to `CurrencyGeneratorData` | `core/generator/currency_generator_data.gd` | High | ⬜ | +| 7 | Connect production to research XP in `CurrencyGenerator` | `core/generator/currency_generator.gd` | High | ✅ Done | +| 8 | Create `ResearchRow` scene & script | `core/research/research_row.tscn`, `.gd` | Medium | ⬜ | +| 9 | Create `ResearchPanel` scene & script | `core/research/research_panel.tscn`, `.gd` | Medium | ⬜ | +| 10 | Add save/load support for research state | `core/level_game_state.gd` | Medium | ⬜ | +| 11 | Add prestige reset for research | `core/level_game_state.gd` | Medium | ⬜ | +| 12 | Create sample `ResearchData` resource | `doc/gyms/tiny_sword/resources/` | Low | ⬜ | +| 13 | Test integration | Manual testing | Low | ⬜ | + +--- + +## Design Decisions (Confirmed) + +| Decision | Value | +|----------|-------| +| ResearchCatalogue reference | Direct `@export var research_catalogue: ResearchCatalogue` in LevelGameState | +| ResearchCatalogue location | `core/research/research_catalogue.gd` (resource, referenced directly) | +| Buff calculation | Static utility (`ResearchBuffCalculator`) - no dedicated tracker node | +| Progress bar display | Percentage format (e.g., "75%") | +| Buff display | Shows buff name and effect (e.g., "Mining Expert: +20% XP") | +| Research unlock | Starts unlocked | +| XP formula | Production-based: `XP = currency_produced * xp_per_currency_produced` | +| Level-up | Auto-level when XP threshold reached | +| Buffs | Single buff per research track, multiplicative XP bonus | +| Prestige | Research levels and XP reset to 0 | +| ResearchData location | `res://doc/gyms/tiny_sword/resources/` | +| ResearchPanel visibility | Always visible (like GeneratorPanel) | + +--- + +## Formulas + +### XP Calculation +``` +base_xp = currency_produced * xp_per_currency_produced +actual_xp = base_xp * buff_multiplier +``` + +### Level Progression +``` +xp_required_for_level(n) = base_xp_required * (xp_growth_multiplier ^ (n - 1)) +total_xp_for_level(n) = sum(xp_required_for_level(i) for i in 1..n) +level = get_level_for_xp(total_xp) +``` + +### Production Multiplier +``` +multiplier = base_multiplier + (multiplier_per_level * level) +effective_production = base_production * multiplier +``` + +### Buff XP Multiplier +``` +xp_multiplier = 1.0 +for each active buff targeting this research: + xp_multiplier *= buff.get_effect_multiplier(level) +actual_xp = base_xp * xp_multiplier +``` + +--- + +## Notes + +- **ResearchData instances** should be created as `.tres` resources in `doc/gyms/tiny_sword/resources/` +- **Associated buffs** should be created as `GeneratorBuffData` with `BuffKind.RESEARCH_XP_MULTIPLIER` +- **Buff names** are configured in the `text` field of each `GeneratorBuffData` +- **ResearchPanel** should be added as a child node in the main game scene (always visible) +- **Save format version** should be incremented to 5 when adding research support (BigNumber XP) +- **ResearchXPTracker removed** - buff calculation moved to static `ResearchBuffCalculator` utility + +--- + +## Testing Checklist + +- [ ] Generator produces currency → research XP increases +- [ ] Buff multipliers correctly applied to XP gain +- [ ] Auto-level triggers at correct XP thresholds +- [ ] Production multiplier updates after level-up +- [ ] ResearchPanel displays correct progress and level +- [ ] Save/load preserves research state +- [ ] Prestige resets research levels and XP +- [ ] Multiple research tracks work independently +- [ ] Buff purchase correctly affects XP gain + +--- + +## Dependencies + +- `CurrencyGenerator` must emit `production_tick` signal +- `GeneratorBuffData` must support `RESEARCH_XP_MULTIPLIER` kind +- `LevelGameState` must support buff tracking and signals +- Save format version must be updated diff --git a/core/research/research_catalogue.gd b/core/research/research_catalogue.gd new file mode 100644 index 0000000..65f89b8 --- /dev/null +++ b/core/research/research_catalogue.gd @@ -0,0 +1,19 @@ +class_name ResearchCatalogue +extends Resource + +@export var research_entries: Array[ResearchData] = [] + +func get_research_by_id(research_id: StringName) -> ResearchData: + for entry in research_entries: + if entry.id == research_id: + return entry + return null + +func get_research_by_generator_id(generator_id: StringName) -> ResearchData: + for entry in research_entries: + if entry.generator_id == generator_id: + return entry + return null + +func get_all_research() -> Array[ResearchData]: + return research_entries.duplicate() diff --git a/core/research/research_catalogue.gd.uid b/core/research/research_catalogue.gd.uid new file mode 100644 index 0000000..dc92b19 --- /dev/null +++ b/core/research/research_catalogue.gd.uid @@ -0,0 +1 @@ +uid://d2v6t6w2todfy diff --git a/core/research/research_data.gd b/core/research/research_data.gd new file mode 100644 index 0000000..1b5f179 --- /dev/null +++ b/core/research/research_data.gd @@ -0,0 +1,70 @@ +class_name ResearchData +extends Resource + +@export var id: StringName = &"" +@export var generator_id: StringName = &"" +@export var name: String = "" +@export_multiline var description: String = "" +@export var icon: Texture2D + +## XP Configuration +@export var xp_per_currency_produced: float = 0.01 +@export var base_xp_required: float = 100.0 +@export var xp_growth_multiplier: float = 1.5 + +## Multiplier Configuration +@export var base_multiplier: float = 1.0 +@export var multiplier_per_level: float = 0.1 + +## Associated Buff (single buff per research track) +@export var associated_buff_id: StringName = &"" + +## Worker Scaling Configuration +@export var worker_scaling_factor: float = 0.01 +@export var min_workers_for_xp: int = 1 + +func get_xp_required_for_level(level: int) -> float: + return base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0))) + +func get_xp_required_for_level_big(level: int) -> BigNumber: + var float_val: float = base_xp_required * pow(xp_growth_multiplier, float(maxi(level - 1, 0))) + return BigNumber.from_float(float_val) + +func get_total_xp_for_level(level: int) -> BigNumber: + if level <= 0: + return BigNumber.new(0.0, 0) + var total: BigNumber = BigNumber.new(0.0, 0) + for l in range(1, level + 1): + total = total.add(get_xp_required_for_level_big(l)) + return total + +func get_level_for_xp(xp: BigNumber) -> int: + if xp.mantissa == 0.0: + return 0 + var level: int = 0 + while true: + var next_xp: BigNumber = get_total_xp_for_level(level + 1) + if xp.compare_to(next_xp) < 0: + break + level += 1 + return level + +func get_multiplier_for_level(level: int) -> float: + return base_multiplier + (multiplier_per_level * float(maxi(level, 0))) + +func get_xp_progress(xp: BigNumber) -> float: + if xp.mantissa == 0.0: + return 0.0 + var current_level: int = get_level_for_xp(xp) + if current_level < 0: + return 0.0 + + var xp_at_current_level: BigNumber = get_total_xp_for_level(current_level) + var xp_needed_for_next: BigNumber = get_xp_required_for_level_big(current_level + 1) + var xp_in_current_level: BigNumber = xp.subtract(xp_at_current_level) + + if xp_needed_for_next.mantissa == 0.0: + return 1.0 + + var ratio: float = (xp_in_current_level.mantissa / xp_needed_for_next.mantissa) * pow(10.0, float(xp_in_current_level.exponent - xp_needed_for_next.exponent)) + return clampf(ratio, 0.0, 1.0) diff --git a/core/research/research_data.gd.uid b/core/research/research_data.gd.uid new file mode 100644 index 0000000..664f5b6 --- /dev/null +++ b/core/research/research_data.gd.uid @@ -0,0 +1 @@ +uid://m7baywrfnpn0 diff --git a/core/research/research_panel.gd b/core/research/research_panel.gd new file mode 100644 index 0000000..81e14cd --- /dev/null +++ b/core/research/research_panel.gd @@ -0,0 +1,155 @@ +class_name ResearchPanel +extends PanelContainer + +const RESEARCH_ROW_SCENE: PackedScene = preload("res://core/research/research_row.tscn") + +@onready var _title_label: Label = $ScrollContainer/VBoxContainer/TitleLabel +@onready var _research_rows_container: VBoxContainer = $ScrollContainer/VBoxContainer/ResearchRows +@onready var _buy_worker_button: Button = $ScrollContainer/VBoxContainer/HBoxContainer/BuyWorkerButton + +var _game_state: LevelGameState +var _research_rows: Dictionary = {} +var _active_research_id: StringName = &"" + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + if _game_state == null: + push_error("ResearchPanel: Could not find LevelGameState parent") + return + + _game_state.research_xp_changed.connect(_on_research_xp_changed) + _game_state.research_level_up.connect(_on_research_level_up) + _game_state.currency_changed.connect(_on_currency_changed) + _game_state.research_workers_changed.connect(_on_research_workers_changed) + + _build_research_rows() + _refresh_all() + _refresh_worker_button() + +func _build_research_rows() -> void: + for child in _research_rows_container.get_children(): + child.queue_free() + _research_rows.clear() + _active_research_id = &"" + + if _game_state.research_catalogue: + for research in _game_state.research_catalogue.get_all_research(): + var row: ResearchRow = RESEARCH_ROW_SCENE.instantiate() + _research_rows_container.add_child(row) + row.setup(research) + row.active_changed.connect(_on_research_active_changed.bind(research.id)) + _research_rows[research.id] = row + + _load_active_research_state() + +func _refresh_all() -> void: + for row in _research_rows.values(): + _refresh_row(row) + +func _refresh_row(row: ResearchRow) -> void: + var research_id: StringName = row.get_research_id() + var xp: BigNumber = _game_state.get_research_xp(research_id) + var level: int = _game_state.get_research_level(research_id) + var research: ResearchData = _game_state.research_catalogue.get_research_by_id(research_id) + + if research != null: + var progress: float = research.get_xp_progress(xp) + var multiplier: float = research.get_multiplier_for_level(level) + var xp_current: BigNumber = xp.subtract(research.get_total_xp_for_level(level)) + var xp_needed: BigNumber = research.get_xp_required_for_level_big(level + 1) + + row.update_display(level, progress, multiplier, xp_current, xp_needed) + +func _on_research_xp_changed(research_id: StringName, _new_xp: BigNumber) -> void: + if _research_rows.has(research_id): + _refresh_row(_research_rows[research_id]) + +func _on_research_level_up(research_id: StringName, _old_level: int, _new_level: int) -> void: + if _research_rows.has(research_id): + _refresh_row(_research_rows[research_id]) + +func _on_research_active_changed(is_active: bool, research_id: StringName) -> void: + if is_active: + if _active_research_id != &"" and _active_research_id != research_id: + var existing_row: ResearchRow = _research_rows.get(_active_research_id) + if existing_row != null: + existing_row.set_active(false, false) + _active_research_id = research_id + _save_active_research_state() + else: + if _active_research_id == research_id: + _active_research_id = &"" + _save_active_research_state() + +func _load_active_research_state() -> void: + if _game_state == null: + return + + var save_data: Dictionary = _game_state.get_external_save_data("research_active") + if save_data.has("active_research_id"): + var loaded_id: String = String(save_data.get("active_research_id", "")) + if not loaded_id.is_empty(): + _active_research_id = StringName(loaded_id) + var row: ResearchRow = _research_rows.get(_active_research_id) + if row != null: + row.set_active(true, false) + +func _save_active_research_state() -> void: + if _game_state == null: + return + + var save_data: Dictionary = {} + if _active_research_id != &"": + save_data["active_research_id"] = String(_active_research_id) + _game_state.set_external_save_data("research_active", save_data) + +func get_active_research_id() -> StringName: + return _active_research_id + +func is_research_active(research_id: StringName) -> bool: + return _active_research_id == research_id + +func activate_research(research_id: StringName) -> void: + if _research_rows.has(research_id): + var row: ResearchRow = _research_rows[research_id] + row.set_active(true) + +func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void: + if currency_id == &"worker": + _refresh_worker_button() + +func _on_research_workers_changed(_new_count: int) -> void: + _refresh_worker_button() + +func _can_buy_worker() -> bool: + if _game_state == null: + return false + + var worker_currency: BigNumber = _game_state.get_currency_amount_by_id(&"worker") + + return worker_currency.mantissa >= 1.0 + +func _buy_worker() -> void: + if _game_state == null: + return + + _game_state.assign_worker_to_research() + +func _on_buy_worker_pressed() -> void: + _buy_worker() + _refresh_worker_button() + +func _refresh_worker_button() -> void: + if _buy_worker_button == null: + return + + if _game_state == null: + _buy_worker_button.disabled = true + return + + var can_buy: bool = _can_buy_worker() + _buy_worker_button.disabled = not can_buy + + var worker_currency: BigNumber = _game_state.get_currency_amount_by_id(&"worker") + var research_workers: int = _game_state.get_research_workers() + _buy_worker_button.text = "Assign Worker to Research (%d available, %d assigned)" % [int(worker_currency.mantissa), research_workers] diff --git a/core/research/research_panel.gd.uid b/core/research/research_panel.gd.uid new file mode 100644 index 0000000..400ee9b --- /dev/null +++ b/core/research/research_panel.gd.uid @@ -0,0 +1 @@ +uid://fdl7ftxw8w1e diff --git a/core/research/research_panel.tscn b/core/research/research_panel.tscn new file mode 100644 index 0000000..0ee16e8 --- /dev/null +++ b/core/research/research_panel.tscn @@ -0,0 +1,34 @@ +[gd_scene format=3 uid="uid://6d101h70mcx"] + +[ext_resource type="Script" uid="uid://fdl7ftxw8w1e" path="res://core/research/research_panel.gd" id="1_script"] +[ext_resource type="PackedScene" uid="uid://ckr805xqy6h4w" path="res://core/research/worker_summary_label.tscn" id="3_worker"] + +[node name="ResearchPanel" type="PanelContainer" unique_id=600021293] +custom_minimum_size = Vector2(400, 100) +offset_right = 400.0 +offset_bottom = 100.0 +script = ExtResource("1_script") + +[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=973132184] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1679714014] +layout_mode = 2 + +[node name="TitleLabel" type="Label" parent="ScrollContainer/VBoxContainer" unique_id=990297493] +layout_mode = 2 +text = "Research" + +[node name="HBoxContainer" type="HBoxContainer" parent="ScrollContainer/VBoxContainer" unique_id=1419318708] +layout_mode = 2 + +[node name="WorkerSummaryLabel" parent="ScrollContainer/VBoxContainer/HBoxContainer" unique_id=13607753 instance=ExtResource("3_worker")] + +[node name="BuyWorkerButton" type="Button" parent="ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1083899286] +layout_mode = 2 +text = "Invest Worker (0 available)" + +[node name="ResearchRows" type="VBoxContainer" parent="ScrollContainer/VBoxContainer" unique_id=2066050169] +layout_mode = 2 + +[connection signal="pressed" from="ScrollContainer/VBoxContainer/HBoxContainer/BuyWorkerButton" to="." method="_buy_worker"] diff --git a/core/research/research_row.gd b/core/research/research_row.gd new file mode 100644 index 0000000..3519e41 --- /dev/null +++ b/core/research/research_row.gd @@ -0,0 +1,98 @@ +class_name ResearchRow +extends HBoxContainer + +signal active_changed(is_active: bool) + +@onready var _name_label: Label = $VBoxContainer/NameLabel +@onready var _level_label: Label = $VBoxContainer/HBoxContainer/LevelLabel +@onready var _progress_bar: ProgressBar = $VBoxContainer/HBoxContainer/ProgressBar +@onready var _multiplier_label: Label = $VBoxContainer/HBoxContainer/MultiplierLabel +@onready var _buff_label: Label = $VBoxContainer/HBoxContainer/BuffLabel +@onready var _check_button: CheckButton = $CheckButton + +var _research: ResearchData +var _game_state: LevelGameState +var _is_active: bool = false + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + if _game_state == null: + push_error("ResearchRow: Could not find LevelGameState parent") + return + + _game_state.research_xp_changed.connect(_on_research_xp_changed) + _game_state.research_level_up.connect(_on_research_level_up) + _check_button.toggled.connect(_on_check_button_toggled) + +func setup(research: ResearchData) -> void: + _research = research + if _name_label: + _name_label.text = research.name + +func get_research_id() -> StringName: + if _research == null: + return &"" + return _research.id + +func update_display(level: int, progress: float, multiplier: float, xp_current: BigNumber, xp_needed: BigNumber) -> void: + if _level_label: + _level_label.text = "Level %d" % level + + if _progress_bar: + _progress_bar.value = progress * 100 + + if _multiplier_label: + _multiplier_label.text = "+%d%%" % int((multiplier - 1.0) * 100) + + if _buff_label and _research != null: + if not _research.associated_buff_id.is_empty(): + var buff: GeneratorBuffData = _game_state.get_buff(_research.associated_buff_id) + if buff != null and _game_state.is_buff_active(buff.id): + var buff_level: int = _game_state.get_buff_level(buff.id) + var effect: float = buff.get_effect_multiplier(buff_level) - 1.0 + _buff_label.text = "%s: +%d%% XP" % [buff.text, int(effect * 100)] + else: + _buff_label.text = "" + else: + _buff_label.text = "" + +func _on_research_xp_changed(research_id: StringName, _new_xp: BigNumber) -> void: + if _research != null and research_id == _research.id: + _update_from_state() + +func _on_research_level_up(research_id: StringName, _old_level: int, _new_level: int) -> void: + if _research != null and research_id == _research.id: + _update_from_state() + +func _update_from_state() -> void: + if _research == null or _game_state == null: + return + + var xp: BigNumber = _game_state.get_research_xp(_research.id) + var level: int = _game_state.get_research_level(_research.id) + + var progress: float = _research.get_xp_progress(xp) + var multiplier: float = _research.get_multiplier_for_level(level) + var xp_current: BigNumber = xp.subtract(_research.get_total_xp_for_level(level)) + var xp_needed: BigNumber = _research.get_xp_required_for_level_big(level + 1) + + update_display(level, progress, multiplier, xp_current, xp_needed) + +func _on_check_button_toggled(toggled_on: bool) -> void: + if toggled_on != _is_active: + _is_active = toggled_on + active_changed.emit(_is_active) + +func set_active(is_active: bool, emit_signal: bool = true) -> void: + _is_active = is_active + _check_button.button_pressed = is_active + if emit_signal: + active_changed.emit(is_active) + +func is_research_active() -> bool: + return _is_active + +func get_research_id_public() -> StringName: + if _research == null: + return &"" + return _research.id diff --git a/core/research/research_row.gd.uid b/core/research/research_row.gd.uid new file mode 100644 index 0000000..108beb5 --- /dev/null +++ b/core/research/research_row.gd.uid @@ -0,0 +1 @@ +uid://gk3mr3k5yvp7 diff --git a/core/research/research_row.tscn b/core/research/research_row.tscn new file mode 100644 index 0000000..78da542 --- /dev/null +++ b/core/research/research_row.tscn @@ -0,0 +1,42 @@ +[gd_scene format=3 uid="uid://bpo8pl5tipav"] + +[ext_resource type="Script" uid="uid://gk3mr3k5yvp7" path="res://core/research/research_row.gd" id="1_script"] + +[node name="ResearchRow" type="HBoxContainer" unique_id=810714290] +mouse_filter = 2 +script = ExtResource("1_script") + +[node name="Icon" type="TextureRect" parent="." unique_id=1376308659] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 0 + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=468422769] +layout_mode = 2 + +[node name="NameLabel" type="Label" parent="VBoxContainer" unique_id=1742937834] +layout_mode = 2 +text = "Research Name" + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" unique_id=819270678] +layout_mode = 2 +alignment = 2 + +[node name="LevelLabel" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=1108328348] +layout_mode = 2 +text = "Level 0" + +[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/HBoxContainer" unique_id=1855503166] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 + +[node name="MultiplierLabel" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=135310321] +layout_mode = 2 +text = "+0%" + +[node name="BuffLabel" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=2124734446] +layout_mode = 2 + +[node name="CheckButton" type="CheckButton" parent="." unique_id=3456789012] +layout_mode = 2 +text = "Active" diff --git a/core/research/worker_summary_label.gd b/core/research/worker_summary_label.gd new file mode 100644 index 0000000..26ce4f2 --- /dev/null +++ b/core/research/worker_summary_label.gd @@ -0,0 +1,27 @@ +class_name WorkerSummaryLabel +extends Label + +@onready var _game_state: LevelGameState = find_parent("LevelGameState") + +signal research_workers_changed(new_count: int) + +func _ready() -> void: + if _game_state == null: + push_error("WorkerSummaryLabel: Could not find LevelGameState parent") + return + + _game_state.currency_changed.connect(_on_currency_changed) + _game_state.research_workers_changed.connect(_on_research_workers_changed) + _update_worker_display() + +func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void: + if currency_id == "worker": + _update_worker_display() + +func _on_research_workers_changed(new_count: int) -> void: + _update_worker_display() + +func _update_worker_display() -> void: + var worker_currency: int = int(_game_state.get_currency_amount_by_id("worker").mantissa) + var research_workers: int = _game_state.get_research_workers() + text = "Workers: %d available, %d assigned" % [worker_currency, research_workers] diff --git a/core/research/worker_summary_label.gd.uid b/core/research/worker_summary_label.gd.uid new file mode 100644 index 0000000..3b68f1d --- /dev/null +++ b/core/research/worker_summary_label.gd.uid @@ -0,0 +1 @@ +uid://brvoy607b3nft diff --git a/core/research/worker_summary_label.tscn b/core/research/worker_summary_label.tscn new file mode 100644 index 0000000..1a36862 --- /dev/null +++ b/core/research/worker_summary_label.tscn @@ -0,0 +1,8 @@ +[gd_scene format=3 uid="uid://ckr8z5xqy6h4w"] + +[ext_resource type="Script" path="res://core/research/worker_summary_label.gd" id="1_script"] + +[node name="WorkerSummaryLabel" type="Label"] +layout_mode = 2 +script = ExtResource("1_script") +text = "Workers: 0" diff --git a/currency_label.gd b/currency_label.gd index efe32fa..dd50cf0 100644 --- a/currency_label.gd +++ b/currency_label.gd @@ -1,7 +1,8 @@ class_name CurrencyTile extends HBoxContainer -@export var currency: Resource +@export var currency: Currency +@export var game_state: LevelGameState @onready var _icon: TextureRect = $CurrencyIcon @onready var _label: Label = $Label @@ -11,26 +12,36 @@ extends HBoxContainer var _currency_id: StringName = &"" func _ready() -> void: - if currency == null: - currency = CurrencyDatabase.get_currency_resource(GameState.GOLD_CURRENCY_ID) if currency == null: push_warning("CurrencyTile '%s' has no currency configured." % String(name)) return - _currency_id = GameState.get_currency_id(currency) - _label.text = "%s:" % GameState.get_currency_name(_currency_id) - _icon.texture = GameState.get_currency_icon(_currency_id) - _debug_button.text = "+100 %s" % GameState.get_currency_name(_currency_id) + if game_state == null: + push_error("CurrencyTile '%s' missing game_state reference" % String(name)) + return - GameState.currency_changed.connect(_on_currency_changed) - _on_currency_changed(_currency_id, GameState.get_currency_amount_by_id(_currency_id)) + game_state.ready.connect(_on_level_game_state_ready) + +func _on_level_game_state_ready() -> void: + _currency_id = game_state.get_currency_id(currency) + + var label_string = game_state.get_currency_name(_currency_id) + _label.text = "%s:" % label_string + _icon.texture = game_state.get_currency_icon(_currency_id) + _debug_button.text = "+100 %s" % label_string + + game_state.currency_changed.connect(_on_currency_changed) + _on_currency_changed(_currency_id, game_state.get_currency_amount_by_id(_currency_id)) func _on_currency_changed(changed_currency_id: StringName, amount: BigNumber) -> void: if changed_currency_id != _currency_id: return _currency_label.text = amount.to_string_suffix(2) + visible = currency.always_visible or not amount.is_equal_to(BigNumber.from_float(0.0)) func _on_debug_income_button_pressed() -> void: if currency == null: return - GameState.add_currency(currency, BigNumber.from_float(100)) + if game_state == null: + return + game_state.add_currency(currency, BigNumber.from_float(100)) diff --git a/currency_tile.tscn b/currency_tile.tscn index 6cc8b54..0006666 100644 --- a/currency_tile.tscn +++ b/currency_tile.tscn @@ -1,15 +1,17 @@ [gd_scene format=3 uid="uid://btkxru2gdjsgc"] [ext_resource type="Script" uid="uid://sp67wcb6s5nw" path="res://currency_label.gd" id="1_0hv07"] +[ext_resource type="Texture2D" uid="uid://d3nymghus6x3" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_03.png" id="2_otq0d"] [node name="CurrencyTile" type="HBoxContainer" unique_id=1440583137] theme_override_constants/separation = 12 script = ExtResource("1_0hv07") [node name="CurrencyIcon" type="TextureRect" parent="." unique_id=935299369] -visible = false custom_minimum_size = Vector2(20, 20) layout_mode = 2 +texture = ExtResource("2_otq0d") +expand_mode = 2 stretch_mode = 5 [node name="Label" type="Label" parent="." unique_id=89234840] diff --git a/docs/gyms/generator_gym.tscn b/docs/gyms/generator_gym.tscn deleted file mode 100644 index 5354038..0000000 --- a/docs/gyms/generator_gym.tscn +++ /dev/null @@ -1,78 +0,0 @@ -[gd_scene format=3 uid="uid://cfryecxmcg8hw"] - -[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_6pne4"] -[ext_resource type="Resource" uid="uid://bnhqk8b31mm4e" path="res://idles/currencies/knowledge.tres" id="2_8qilt"] -[ext_resource type="Resource" uid="uid://l0pn6mlcer7t" path="res://idles/currencies/spirit.tres" id="2_jlqd0"] -[ext_resource type="Resource" uid="uid://04pmc034qupd" path="res://idles/generators/library.tres" id="3_4ly0e"] -[ext_resource type="Resource" uid="uid://cythfovqgqlyh" path="res://idles/currencies/wood.tres" id="4_2xpf5"] -[ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="4_6ri4a"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="5_dl3gy"] -[ext_resource type="Resource" uid="uid://df5k58yu1g6rf" path="res://idles/generators/forestry.tres" id="5_u3cug"] -[ext_resource type="PackedScene" uid="uid://dirvi76rkoowf" path="res://goals_debug_ui.tscn" id="7_7i63j"] -[ext_resource type="PackedScene" path="res://core/prestige/prestige_panel.tscn" id="9_q5vce"] - -[node name="GeneratorGym" type="Node2D" unique_id=1219373683] - -[node name="MagicGenerator" parent="." unique_id=967969064 instance=ExtResource("1_6pne4")] -position = Vector2(262, 301) - -[node name="KnowledgeGenerator" parent="." unique_id=2139088546 instance=ExtResource("1_6pne4")] -visible = false -position = Vector2(262, 626) -currency = ExtResource("2_8qilt") -data = ExtResource("3_4ly0e") -press_buys_generator = false - -[node name="WoodGenerator" parent="." unique_id=29858558 instance=ExtResource("1_6pne4")] -visible = false -position = Vector2(1046, 626) -currency = ExtResource("4_2xpf5") -data = ExtResource("5_u3cug") -press_buys_generator = false - -[node name="UI" type="Control" parent="." unique_id=452530906] -layout_mode = 3 -anchors_preset = 0 -offset_right = 40.0 -offset_bottom = 40.0 - -[node name="MagicCurrencyTile" parent="UI" unique_id=1440583137 instance=ExtResource("4_6ri4a")] -layout_mode = 0 -offset_right = 160.0 -offset_bottom = 31.0 -currency = ExtResource("5_dl3gy") - -[node name="SpiritCurrencyTile" parent="UI" unique_id=797237056 instance=ExtResource("4_6ri4a")] -layout_mode = 0 -offset_top = 32.0 -offset_right = 160.0 -offset_bottom = 63.0 -currency = ExtResource("2_jlqd0") - -[node name="KnowledgeCurrencyTile" parent="UI" unique_id=1977342362 instance=ExtResource("4_6ri4a")] -layout_mode = 0 -offset_top = 63.0 -offset_right = 160.0 -offset_bottom = 94.0 -currency = ExtResource("2_8qilt") - -[node name="WoodCurrencyTile" parent="UI" unique_id=1210103101 instance=ExtResource("4_6ri4a")] -layout_mode = 0 -offset_top = 94.0 -offset_right = 160.0 -offset_bottom = 125.0 -currency = ExtResource("4_2xpf5") - -[node name="GoalsDebugUI" parent="UI" unique_id=4710697 instance=ExtResource("7_7i63j")] -layout_mode = 1 -offset_left = 1253.0 -offset_top = 817.0 -offset_right = 1913.0 -offset_bottom = 1076.0 - -[node name="PrestigePanel" parent="UI" unique_id=401213142 instance=ExtResource("9_q5vce")] -layout_mode = 0 -offset_left = 12.0 -offset_top = 822.0 -offset_right = 432.0 -offset_bottom = 1028.0 diff --git a/docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres b/docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres new file mode 100644 index 0000000..bffab84 --- /dev/null +++ b/docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres @@ -0,0 +1,19 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://cg7os1rfknw05"] + +[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="1_qy8mo"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_pac24"] +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_t5xb7"] +[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_r8eth"] + +[resource] +script = ExtResource("3_t5xb7") +id = &"farm_auto_flux" +target_ids = Array[StringName]([&"farm"]) +text = "Farm Dynamo" +max_level = 10 +unlock_goal = ExtResource("4_r8eth") +effect_increment = 1.0 +cost_currency = ExtResource("1_qy8mo") +base_cost_mantissa = 100.0 +cost_multiplier = 1.7 +resource_target_currency = ExtResource("2_pac24") diff --git a/docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres b/docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres new file mode 100644 index 0000000..544d97a --- /dev/null +++ b/docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres @@ -0,0 +1,20 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://kamgujbqqhg3"] + +[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="1_pdm0x"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_rnlmu"] +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_y6252"] +[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_y6252"] + +[resource] +script = ExtResource("3_y6252") +id = &"forestry_auto_flux" +target_ids = Array[StringName]([&"forestry"]) +text = "Forest Dynamo +" +max_level = 10 +unlock_goal = ExtResource("4_y6252") +effect_increment = 1.0 +cost_currency = ExtResource("1_pdm0x") +base_cost_mantissa = 250.0 +cost_multiplier = 1.7 +resource_target_currency = ExtResource("2_rnlmu") diff --git a/docs/gyms/tiny_sword/buffs/gold_auto_flux_buff.tres b/docs/gyms/tiny_sword/buffs/gold_auto_flux_buff.tres new file mode 100644 index 0000000..7edec80 --- /dev/null +++ b/docs/gyms/tiny_sword/buffs/gold_auto_flux_buff.tres @@ -0,0 +1,19 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://c3op2mqy02sow"] + +[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="1_j3saa"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_hvfo8"] +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_bwcdh"] +[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="4_f52il"] + +[resource] +script = ExtResource("3_bwcdh") +id = &"gold_auto_flux" +target_ids = Array[StringName]([&"goldmine"]) +text = "Gold Dynamo" +max_level = 10 +unlock_goal = ExtResource("4_f52il") +effect_increment = 1.0 +cost_currency = ExtResource("1_j3saa") +base_cost_mantissa = 25.0 +cost_multiplier = 1.7 +resource_target_currency = ExtResource("2_hvfo8") diff --git a/docs/gyms/tiny_sword/buffs/gold_click_buff.tres b/docs/gyms/tiny_sword/buffs/gold_click_buff.tres new file mode 100644 index 0000000..e896dd9 --- /dev/null +++ b/docs/gyms/tiny_sword/buffs/gold_click_buff.tres @@ -0,0 +1,20 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://dlq2ke6i2pfob"] + +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="1_8h747"] +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="2_aejsu"] +[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="3_aejsu"] + +[resource] +script = ExtResource("2_aejsu") +id = &"gold_click_focus" +target_ids = Array[StringName]([&"goldmine"]) +kind = 1 +text = "Gold Clicker!" +max_level = 4 +unlock_goal = ExtResource("3_aejsu") +effect_increment = 1.0 +cost_currency = ExtResource("1_8h747") +base_cost_mantissa = 2.5 +base_cost_exponent = 1 +cost_multiplier = 2.0 +resource_target_currency = ExtResource("1_8h747") diff --git a/docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres b/docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres new file mode 100644 index 0000000..c95e7cd --- /dev/null +++ b/docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres @@ -0,0 +1,21 @@ +[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://bjc6qmvr7pe12"] + +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="1_we7j4"] +[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="2_s45cj"] +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_tgtcn"] +[ext_resource type="Resource" uid="uid://ik7t0r3su633" path="res://docs/gyms/tiny_sword/goals/gold_total_30_goal.tres" id="4_kqmci"] + +[resource] +script = ExtResource("3_tgtcn") +id = &"summon_worker" +kind = 2 +text = "Summon Worker +" +unlock_goal = ExtResource("4_kqmci") +effect_increment = 1.0 +cost_currency = ExtResource("1_we7j4") +base_cost_mantissa = 200.0 +cost_multiplier = 1.15 +resource_target_currency = ExtResource("2_s45cj") +resource_purchase_base_mantissa = 1.0 +resource_purchase_increment_multiplier = 1.0 diff --git a/docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres b/docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres new file mode 100644 index 0000000..00ba8fb --- /dev/null +++ b/docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="BuffCatalogue" format=3 uid="uid://bgmj3lep0wmtf"] + +[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_up80l"] +[ext_resource type="Resource" uid="uid://dlq2ke6i2pfob" path="res://docs/gyms/tiny_sword/buffs/gold_click_buff.tres" id="2_dtvfl"] +[ext_resource type="Script" uid="uid://ctc5yjlnvi0ok" path="res://core/buff_catalogue.gd" id="2_iuxc1"] +[ext_resource type="Resource" uid="uid://c3op2mqy02sow" path="res://docs/gyms/tiny_sword/buffs/gold_auto_flux_buff.tres" id="3_0bb41"] +[ext_resource type="Resource" uid="uid://bjc6qmvr7pe12" path="res://docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres" id="4_hnq3m"] +[ext_resource type="Resource" uid="uid://cg7os1rfknw05" path="res://docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres" id="5_mii30"] +[ext_resource type="Resource" uid="uid://kamgujbqqhg3" path="res://docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres" id="6_k5yi4"] + +[resource] +script = ExtResource("2_iuxc1") +buffs = Array[ExtResource("1_up80l")]([ExtResource("2_dtvfl"), ExtResource("3_0bb41"), ExtResource("4_hnq3m"), ExtResource("5_mii30"), ExtResource("6_k5yi4")]) +metadata/_custom_type_script = "uid://ctc5yjlnvi0ok" diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/README.md b/docs/gyms/tiny_sword/buildings/alchemy_tower/README.md new file mode 100644 index 0000000..d32f141 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/README.md @@ -0,0 +1,55 @@ +# Alchemy Tower + +The Alchemy Tower is a building that passively produces **magic gold** and allows **crafting** other currencies through alchemical recipes. + +## Passive Production + +The tower runs a continuous production cycle. Once a cycle completes, magic gold is added to your balance and the next cycle begins with a slightly longer duration. + +| Data field | Default | Description | +|---|---|---| +| `base_production_time_seconds` | 5.0 | Duration of the first cycle (seconds) | +| `production_time_growth_multiplier` | 1.2 | Each completed cycle multiplies the next cycle's duration by this amount | +| `magic_gold_per_cycle` | 1.0 | Base magic gold produced per cycle | + +Production is only active when at least one worker is assigned and the tower is available in the game state. + +## Workers + +Workers are assigned by spending 1 **worker** currency per worker. Each assigned worker provides a **+10% speed bonus** to production (configurable via `base_worker_speed_bonus`). + +- Max workers: `max_workers` (default 10) +- The speed multiplier formula: `1.0 + (worker_count * base_worker_speed_bonus)` +- This multiplier affects both the rate time advances and the final cycle output + +The UI panel includes a worker counter and an "Assign Worker" button. The button is disabled when you have no workers available or have reached the max. + +## Crafting + +The crafting panel shows recipes from an `AlchemyCraftCatalogue` resource. Each recipe (`AlchemyCraftRecipe`) defines: + +- `id` — unique recipe identifier +- `output_currency` / `output_amount` — what you get +- `cost_entries` — array of `CurrencyCostEntry` resources, each specifying a currency and an amount + +To craft, click the recipe's button. The tower deducts all costs from your balance via `game_state.spend_currency()`. If any cost can't be covered, the craft fails silently and nothing is deducted. On success, the output currency is added to your balance. + +## Signals + +| Signal | Emitted when | +|---|---| +| `production_progress_updated(progress: float)` | Every frame during production; 0.0–1.0 | +| `production_completed(amount: BigNumber)` | A production cycle finishes | +| `magic_gold_balance_updated(amount: BigNumber)` | Magic gold balance changes | + +## Key Files + +| File | Purpose | +|---|---| +| `alchemy_tower.gd` | Main tower logic: production loop, worker management, crafting | +| `alchemy_tower_data.gd` | Configuration resource (`AlchemyTowerData`) | +| `alchemy_craftable_panel.gd` | UI panel (`AlchemyCurrenciesPanel`) for progress bar, balance, workers, and recipe list | +| `alchemy_craftable_panel_tile.gd` | Individual recipe tile in the crafting list | +| `recipes/alchemy_craft_catalogue.gd` | Catalogue resource holding all available recipes | +| `recipes/alchemy_craft_recipe.gd` | Single recipe resource | +| `recipes/currency_cost_entry.gd` | Cost entry for a single currency in a recipe | diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/Tower.png b/docs/gyms/tiny_sword/buildings/alchemy_tower/Tower.png new file mode 100644 index 0000000000000000000000000000000000000000..cf740f09f253bb49d7b36197cc3130dfa10ee1f0 GIT binary patch literal 6496 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 void: + assert(craft_recipes != null, "Craft recipes cannot be null") + + _game_state = find_parent("LevelGameState") + if _game_state == null: + push_error("AlchemyCurrenciesPanel: Could not find LevelGameState") + return + + # Find AlchemyTower in parent + _alchemy_tower = _find_alchemy_tower() + + # Set up UI labels + _currency_name_label.text = currency.display_name if currency else "Magic Gold" + + # Connect to tower signals + if _alchemy_tower: + _alchemy_tower.production_progress_updated.connect(_on_production_progress) + _alchemy_tower.magic_gold_balance_updated.connect(_on_magic_gold_changed) + _alchemy_tower.production_completed.connect(_on_production_completed) + + # Connect worker button + if _assign_button: + _assign_button.pressed.connect(_on_assign_button_pressed) + + # Initialize displays + _update_magic_gold_display() + _update_production_progress(0.0) + _update_worker_display() + + # Connect to worker currency changes + _game_state.currency_changed.connect(_on_currency_changed) + + # Clear and populate craft tiles + for child in _craft_list.get_children(): + child.queue_free() + + for recipe in craft_recipes.recipes: + var instance: AlchemyCraftableTile = _tile_scene.instantiate() as AlchemyCraftableTile + instance.setup(recipe) + instance.craft_button_pressed.connect(_on_craft_button_pressed) + _craft_list.add_child(instance) + +func _find_alchemy_tower() -> AlchemyTower: + var parent: Node = get_parent() + while parent != null: + if parent is AlchemyTower: + return parent as AlchemyTower + parent = parent.get_parent() + return null + +func _update_magic_gold_display() -> void: + if _alchemy_tower == null or _game_state == null: + return + + var magic_gold_currency: Currency = _get_magic_gold_currency() + if magic_gold_currency == null: + _balance_label.text = "0" + return + + var balance: BigNumber = _game_state.get_currency_amount(magic_gold_currency) + _balance_label.text = balance.to_string_suffix(2) + +func _update_production_progress(progress: float) -> void: + if _progress_bar == null: + return + _progress_bar.value = progress * 100.0 + +func _update_worker_display() -> void: + if _worker_label == null: + return + + _worker_label.text = "Alchemy Workers: %d" % _alchemy_worker_count + + if _assign_button != null: + var worker_currency: Currency = _get_worker_currency() + var can_assign: bool = false + + if worker_currency != null and _alchemy_worker_count < 10: + var worker_balance: BigNumber = _game_state.get_currency_amount(worker_currency) + can_assign = worker_balance.mantissa >= 1.0 + + _assign_button.disabled = not can_assign + _assign_button.text = "Assign Worker" if can_assign else "No Workers Available" + +func _get_magic_gold_currency() -> Currency: + if _game_state == null or _game_state.currency_catalogue == null: + return null + + for currency in _game_state.currency_catalogue.currencies: + if currency == null: + continue + if currency.id == &"magic_gold": + return currency + + return null + +func _get_worker_currency() -> Currency: + if _game_state == null or _game_state.currency_catalogue == null: + return null + + for currency in _game_state.currency_catalogue.currencies: + if currency == null: + continue + if currency.id == &"worker": + return currency + + return null + +func _on_production_progress(progress: float) -> void: + _update_production_progress(progress) + +func _on_magic_gold_changed(amount: BigNumber) -> void: + _update_magic_gold_display() + _update_worker_display() + +func _on_production_completed(_amount: BigNumber) -> void: + _update_magic_gold_display() + +func _on_currency_changed(currency_id: StringName, _amount: BigNumber) -> void: + if currency_id == &"worker": + _update_worker_display() + +func _on_assign_button_pressed() -> void: + if _alchemy_tower == null or _game_state == null: + return + + var worker_currency: Currency = _get_worker_currency() + if worker_currency == null: + return + + # Spend a worker + if _game_state.spend_currency(worker_currency, BigNumber.from_float(1.0)): + _alchemy_worker_count += 1 + _update_worker_display() + alchemy_workers_changed.emit(_alchemy_worker_count) + +func _on_craft_button_pressed(recipe: Variant) -> void: + craft_requested.emit(recipe) diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.gd.uid new file mode 100644 index 0000000..27e1f16 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.gd.uid @@ -0,0 +1 @@ +uid://do1i3hrd0ueb6 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.tscn b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.tscn new file mode 100644 index 0000000..1cb2988 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.tscn @@ -0,0 +1,40 @@ +[gd_scene format=3 uid="uid://cbp6vpth8x4rw"] + +[ext_resource type="Script" uid="uid://do1i3hrd0ueb6" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.gd" id="1_rg8ty"] + +[node name="AlchemyCraftablePanel" type="PanelContainer" unique_id=1000001] +custom_minimum_size = Vector2(400, 300) +script = ExtResource("1_rg8ty") + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1115194594] +layout_mode = 2 + +[node name="HBoxWorker" type="HBoxContainer" parent="VBoxContainer" unique_id=1415354976] +layout_mode = 2 + +[node name="WorkerLabel" type="Label" parent="VBoxContainer/HBoxWorker" unique_id=4000008] +layout_mode = 2 +text = "Alchemy Workers: 0" + +[node name="AssignButton" type="Button" parent="VBoxContainer/HBoxWorker" unique_id=4000009] +layout_mode = 2 +text = "Assign Worker" + +[node name="HBoxCurrency" type="HBoxContainer" parent="VBoxContainer" unique_id=1815331819] +layout_mode = 2 + +[node name="NameLabel" type="Label" parent="VBoxContainer/HBoxCurrency" unique_id=4000005] +layout_mode = 2 +text = "Magic Gold" + +[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/HBoxCurrency" unique_id=4000006] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 + +[node name="AmountLabel" type="Label" parent="VBoxContainer/HBoxCurrency" unique_id=4000007] +layout_mode = 2 +text = "0" + +[node name="CraftList" type="VBoxContainer" parent="VBoxContainer" unique_id=3000005] +layout_mode = 2 +size_flags_vertical = 8 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd new file mode 100644 index 0000000..a9a4f14 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd @@ -0,0 +1,81 @@ +class_name AlchemyCraftableTile +extends HBoxContainer + +signal craft_button_pressed(recipe: Variant) + +@onready var _name_label: Label = $NameLabel +@onready var _balance_label: Label = $BalanceLabel +@onready var _cost_label: Label = $CostLabel +@onready var _button: Button = $Button + +var _recipe: Variant +var _game_state: LevelGameState +var _initialized: bool = false + +func _ready() -> void: + _game_state = find_parent("LevelGameState") + _game_state.ready.connect(_on_game_state_ready) + + if not _button.pressed.is_connected(_on_button_pressed): + _button.pressed.connect(_on_button_pressed) + + _initialized = true + +func setup(recipe: AlchemyCraftRecipe) -> void: + _recipe = recipe + +func _on_game_state_ready() -> void: + # Validate labels before use + if _name_label == null: + push_error("AlchemyCraftableTile: _name_label is null in setup") + return + + # Set up display + _name_label.text = str(_recipe.output_currency.display_name) if _recipe.output_currency else "Unknown" + _update_cost_display() + + # Connect to currency changes for balance updates + if _game_state != null: + _game_state.currency_changed.connect(_on_currency_changed) + + # Initial balance update + _update_balance() + +func _update_cost_display() -> void: + if _cost_label == null or _recipe == null: + return + + if _recipe.cost_entries.is_empty(): + _cost_label.text = "No cost" + return + + var cost_text: String = "" + for entry in _recipe.cost_entries: + if entry == null: + continue + if not cost_text.is_empty(): + cost_text += " + " + cost_text += "%d %s" % [entry.amount, entry.currency.display_name] + + _cost_label.text = cost_text + +func _update_balance() -> void: + if _balance_label == null or _recipe == null or _game_state == null: + return + + var balance: BigNumber = _game_state.get_currency_amount(_recipe.output_currency) + _balance_label.text = balance.to_string_suffix(0) + +func _on_button_pressed() -> void: + if _recipe == null: + push_warning("AlchemyCraftableTile: No recipe configured") + return + + craft_button_pressed.emit(_recipe) + +func _on_currency_changed(currency_id: StringName, _amount: BigNumber) -> void: + # Update balance if this currency changed + if _recipe != null: + var output_currency_id: StringName = _recipe.output_currency.id if _recipe.output_currency else &"" + if currency_id == output_currency_id: + _update_balance() diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd.uid new file mode 100644 index 0000000..2ca7984 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd.uid @@ -0,0 +1 @@ +uid://c63g772y4kxwm diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.tscn b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.tscn new file mode 100644 index 0000000..78c63f8 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.tscn @@ -0,0 +1,31 @@ +[gd_scene format=3 uid="uid://cxusq0tunvjlb"] + +[ext_resource type="Script" uid="uid://c63g772y4kxwm" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel_tile.gd" id="1_hw1b0"] + +[node name="AlchemyCraftableTile" type="HBoxContainer" unique_id=1715275035] +offset_left = 10.0 +offset_top = 10.0 +offset_right = 280.0 +offset_bottom = 41.0 +script = ExtResource("1_hw1b0") + +[node name="NameLabel" type="Label" parent="." unique_id=2031343677] +layout_mode = 2 +size_flags_horizontal = 8 +text = "Currency" + +[node name="BalanceLabel" type="Label" parent="." unique_id=1367089480] +layout_mode = 2 +size_flags_horizontal = 8 +text = "0" + +[node name="CostLabel" type="Label" parent="." unique_id=1649255694] +layout_mode = 2 +size_flags_horizontal = 8 +text = "10 Magic Gold" + +[node name="Button" type="Button" parent="." unique_id=1649255693] +layout_mode = 2 +text = "Craft" + +[connection signal="pressed" from="Button" to="." method="_on_button_pressed"] diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_currencies_panel_old.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_currencies_panel_old.gd.uid new file mode 100644 index 0000000..3aedd89 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_currencies_panel_old.gd.uid @@ -0,0 +1 @@ +uid://bsfgq4apkidak diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd new file mode 100644 index 0000000..fddca0d --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd @@ -0,0 +1,202 @@ +class_name AlchemyTower +extends Node2D + +## Emitted when production progress changes (0.0 to 1.0) +signal production_progress_updated(progress: float) +## Emitted when magic gold balance changes +signal magic_gold_balance_updated(amount: BigNumber) +## Emitted when a production cycle completes +signal production_completed(amount: BigNumber) + +## Configuration data for this tower +@export var data: AlchemyTowerData +## Available craft recipes +@export var craft_recipes: AlchemyCraftCatalogue + +## Reference to game state +@onready var game_state: LevelGameState = find_parent("LevelGameState") +@onready var _alchemy_currencies_panel: AlchemyCurrenciesPanel = $AlchemyCurrenciesPanel + +## Current production state +var production_time_elapsed: float = 0.0 +var current_production_time: float +var cycle_count: int = 0 + +## Number of workers assigned to this tower +var _worker_count: int = 0 + +## UI panel reference +@onready var alchemy_panel: AlchemyCurrenciesPanel = $AlchemyCurrenciesPanel + +func _ready() -> void: + assert(data != null, "AlchemyTowerData is required") + assert(craft_recipes != null, "Craft recipes catalogue is required") + assert(game_state != null, "LevelGameState is required") + + # Initialize production time + current_production_time = data.base_production_time_seconds + + # Register this tower as an available generator + game_state.set_generator_available(get_generator_id(), true) + + # Connect to magic gold balance changes + game_state.currency_changed.connect(_on_currency_changed) + + # Connect panel signals if available + if alchemy_panel: + alchemy_panel.craft_requested.connect(_on_craft_requested) + alchemy_panel.alchemy_workers_changed.connect(_on_alchemy_workers_changed) + + # Initial progress update + production_progress_updated.emit(0.0) + +func _process(delta: float) -> void: + if not _is_production_active(): + return + + var speed_multiplier: float = _get_worker_speed_multiplier() + + # Add elapsed time scaled by worker count + production_time_elapsed += delta * speed_multiplier + + # Update progress bar every frame + production_progress_updated.emit(_get_production_progress()) + + # Check if production is complete + if production_time_elapsed >= current_production_time: + _try_complete_production() + +func _is_production_active() -> bool: + if not game_state.is_generator_available(get_generator_id()): + return false + return _get_worker_count() > 0 + +func _try_complete_production() -> void: + # Calculate how many cycles completed + var completed_cycles: int = floori(production_time_elapsed / current_production_time) + + if completed_cycles <= 0: + return + + # Reset elapsed time + production_time_elapsed -= current_production_time * float(completed_cycles) + + # Calculate output + var output_per_cycle: BigNumber = BigNumber.from_float(data.magic_gold_per_cycle) + var total_output: BigNumber = output_per_cycle.multiply(BigNumber.from_float(float(completed_cycles))) + + # Apply worker speed bonus (affects output, not time) + var worker_multiplier: float = _get_worker_speed_multiplier() + if worker_multiplier > 1.0: + total_output = total_output.multiply(BigNumber.from_float(worker_multiplier)) + + # Add magic gold + _add_magic_gold(total_output) + + # Increment cycle count + cycle_count += completed_cycles + + # Increase production time (mild exponential growth) + current_production_time *= pow(data.production_time_growth_multiplier, float(completed_cycles)) + + # Emit signals + production_completed.emit(total_output) + production_progress_updated.emit(_get_production_progress()) + +func _add_magic_gold(amount: BigNumber) -> void: + if data == null or game_state == null: + return + + var magic_gold_currency: Currency = _get_magic_gold_currency() + if magic_gold_currency == null: + push_error("AlchemyTower: Could not find magic gold currency") + return + + game_state.add_currency(magic_gold_currency, amount) + +func _get_magic_gold_currency() -> Currency: + # Find magic gold in the currency catalogue + if game_state == null or game_state.currency_catalogue == null: + return null + + for currency in game_state.currency_catalogue.currencies: + if currency == null: + continue + if currency.id == &"magic_gold": + return currency + + return null + +func _get_worker_count() -> int: + return mini(_worker_count, data.max_workers) + +func _get_worker_speed_multiplier() -> float: + if game_state == null: + return 1.0 + + var worker_count: int = _get_worker_count() + var multiplier: float = 1.0 + (float(worker_count) * data.base_worker_speed_bonus) + + return multiplier + +func _get_worker_currency() -> Currency: + if game_state == null or game_state.currency_catalogue == null: + return null + + for currency in game_state.currency_catalogue.currencies: + if currency == null: + continue + if currency.id == &"worker": + return currency + + return null + +func _get_production_progress() -> float: + if current_production_time <= 0.0: + return 1.0 + + var progress: float = production_time_elapsed / current_production_time + return clampf(progress, 0.0, 1.0) + +func get_generator_id() -> StringName: + return data.id + +func craft_item(recipe: Variant) -> bool: + if recipe == null: + return false + + # Check and pay all costs + for entry in recipe.cost_entries: + if entry == null or entry.currency == null: + continue + if not game_state.spend_currency(entry.currency, BigNumber.from_float(float(entry.amount))): + return false + + # Success - add output + game_state.add_currency(recipe.output_currency, BigNumber.from_float(float(recipe.output_amount))) + + return true + +func _on_currency_changed(currency_id: StringName, _amount: BigNumber) -> void: + if currency_id == &"magic_gold": + magic_gold_balance_updated.emit(_amount) + +func _on_alchemy_workers_changed(count: int) -> void: + _worker_count = count + production_progress_updated.emit(_get_production_progress()) + +func _on_craft_requested(recipe: Variant) -> void: + if recipe == null: + return + + var success: bool = craft_item(recipe) + if not success: + push_warning("AlchemyTower: Failed to craft %s - insufficient funds" % recipe.output_currency.display_name) + + +func _on_area_2d_mouse_entered() -> void: + _alchemy_currencies_panel.visible = true + + +func _on_area_2d_mouse_exited() -> void: + _alchemy_currencies_panel.visible = false diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd.uid new file mode 100644 index 0000000..ae7c32e --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd.uid @@ -0,0 +1 @@ +uid://bddaaj76msmvj diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.tscn b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.tscn new file mode 100644 index 0000000..7f00c41 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.tscn @@ -0,0 +1,37 @@ +[gd_scene format=3 uid="uid://bp5ng4vu4ot4a"] + +[ext_resource type="Texture2D" uid="uid://cieg8i3c8hca6" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/Tower.png" id="1_vbhae"] +[ext_resource type="PackedScene" uid="uid://cbp6vpth8x4rw" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_craftable_panel.tscn" id="2_8pntr"] +[ext_resource type="Script" uid="uid://bddaaj76msmvj" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.gd" id="3_tower"] +[ext_resource type="Resource" uid="uid://cedxqlirp3imi" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.tres" id="4_data"] +[ext_resource type="Resource" uid="uid://diboykfbbxpfs" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.tres" id="5_recipes"] +[ext_resource type="Resource" uid="uid://dpbndqxvsffa0" path="res://docs/gyms/tiny_sword/currencies/magic_gold.tres" id="7_b8p5n"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_8pntr"] +size = Vector2(109, 182) + +[node name="AlchemyTower" type="Node2D" unique_id=51852160] +script = ExtResource("3_tower") +data = ExtResource("4_data") +craft_recipes = ExtResource("5_recipes") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1160485854] +texture = ExtResource("1_vbhae") + +[node name="Area2D" type="Area2D" parent="." unique_id=911128362] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1059910507] +position = Vector2(0.5, 11) +shape = SubResource("RectangleShape2D_8pntr") + +[node name="AlchemyCurrenciesPanel" parent="." unique_id=731368154 instance=ExtResource("2_8pntr")] +visible = false +offset_left = 65.0 +offset_top = -75.0 +offset_right = 65.0 +offset_bottom = -75.0 +currency = ExtResource("7_b8p5n") +craft_recipes = ExtResource("5_recipes") + +[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"] +[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"] diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd new file mode 100644 index 0000000..67172b1 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd @@ -0,0 +1,23 @@ +class_name AlchemyTowerData +extends Resource + +@export_group("Identification") +@export var id: StringName = &"alchemy_tower" +@export var name: String = "Alchemy Tower" + +@export_group("Production") +## Base time to produce one magic gold (first production) +@export var base_production_time_seconds: float = 5.0 + +## Multiplier applied to production time each cycle (1.2 = 20% increase) +@export var production_time_growth_multiplier: float = 1.2 + +## Amount of magic gold produced per cycle +@export var magic_gold_per_cycle: float = 1.0 + +@export_group("Workers") +## Speed bonus per alchemy worker (0.1 = +10% speed per worker) +@export var base_worker_speed_bonus: float = 0.1 + +## Maximum workers that can be assigned +@export var max_workers: int = 10 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd.uid new file mode 100644 index 0000000..bc38cec --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd.uid @@ -0,0 +1 @@ +uid://dxgpic67x08c6 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.tres new file mode 100644 index 0000000..deb69f2 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.tres @@ -0,0 +1,6 @@ +[gd_resource type="Resource" script_class="AlchemyTowerData" format=3 uid="uid://cedxqlirp3imi"] + +[ext_resource type="Script" uid="uid://dxgpic67x08c6" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower_data.gd" id="1_data"] + +[resource] +script = ExtResource("1_data") diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd new file mode 100644 index 0000000..d520a6c --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd @@ -0,0 +1,18 @@ +class_name AlchemyCraftCatalogue +extends Resource + +## Catalogue of alchemy craft recipes +@export var recipes: Array[AlchemyCraftRecipe] = [] + +func get_recipe_by_id(id: StringName) -> Variant: + for recipe in recipes: + if recipe != null and recipe.id == id: + return recipe + return null + +func get_all_ids() -> Array[StringName]: + var ids: Array[StringName] = [] + for recipe in recipes: + if recipe != null and recipe.id != &"": + ids.append(recipe.id) + return ids diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd.uid new file mode 100644 index 0000000..c1f39e4 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd.uid @@ -0,0 +1 @@ +uid://biljlhsxr3rsc diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.tres new file mode 100644 index 0000000..e3c2cca --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="AlchemyCraftCatalogue" format=3 uid="uid://diboykfbbxpfs"] + +[ext_resource type="Script" uid="uid://biljlhsxr3rsc" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_catalogue.gd" id="1_cat"] +[ext_resource type="Resource" uid="uid://by8qmuhvo38nn" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_recipe.tres" id="2_mana_recipe"] +[ext_resource type="Resource" uid="uid://dliibkgb2mom0" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_recipe.tres" id="3_cognite_recipe"] +[ext_resource type="Resource" uid="uid://nfpkv6a2u0wf" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_recipe.tres" id="3_pbygp"] + +[resource] +script = ExtResource("1_cat") +recipes = [ExtResource("2_mana_recipe"), ExtResource("3_cognite_recipe"), ExtResource("3_pbygp")] diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd new file mode 100644 index 0000000..e2109f3 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd @@ -0,0 +1,10 @@ +class_name AlchemyCraftRecipe +extends Resource + +## Recipe for crafting currencies at the Alchemy Tower +@export var id: StringName = &"" +@export var output_currency: Currency +@export var output_amount: int = 1 + +## Multi-currency cost to perform this craft +@export var cost_entries: Array[CurrencyCostEntry] = [] diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd.uid new file mode 100644 index 0000000..6d03f6d --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd.uid @@ -0,0 +1 @@ +uid://ddlnlpb0p750q diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_cost_entry.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_cost_entry.tres new file mode 100644 index 0000000..a4506cb --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_cost_entry.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="CurrencyCostEntry" format=3] + +[ext_resource type="Script" uid="uid://ba8e403mb7hp0" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd" id="1_cost"] +[ext_resource type="Resource" uid="uid://dpbndqxvsffa0" path="res://docs/gyms/tiny_sword/currencies/magic_gold.tres" id="2_magic_gold"] + +[resource] +script = ExtResource("1_cost") +currency = ExtResource("2_magic_gold") +amount = 25 +metadata/_custom_type_script = "res://docs/gyms/tiny_sword/buildings/alchemy_tower/currency_cost_entry.gd" diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_recipe.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_recipe.tres new file mode 100644 index 0000000..82083d1 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_recipe.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="AlchemyCraftRecipe" format=3 uid="uid://dliibkgb2mom0"] + +[ext_resource type="Script" uid="uid://ddlnlpb0p750q" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd" id="1_recipe"] +[ext_resource type="Resource" uid="uid://t6du7gm2ywbi" path="res://docs/gyms/tiny_sword/currencies/cognite.tres" id="2_xltlj"] +[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/cognite_cost_entry.tres" id="3_cost"] + +[resource] +script = ExtResource("1_recipe") +id = &"cognite_recipe" +output_currency = ExtResource("2_xltlj") +cost_entries = [ExtResource("3_cost")] diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd new file mode 100644 index 0000000..c5aedd5 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd @@ -0,0 +1,6 @@ +class_name CurrencyCostEntry +extends Resource + +## Entry for multi-currency cost in alchemy recipes +@export var currency: Currency +@export var amount: int = 1 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd.uid new file mode 100644 index 0000000..7158d86 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd.uid @@ -0,0 +1 @@ +uid://ba8e403mb7hp0 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_cost_entry.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_cost_entry.tres new file mode 100644 index 0000000..6e7cc21 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_cost_entry.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="CurrencyCostEntry" format=3] + +[ext_resource type="Script" uid="uid://ba8e403mb7hp0" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd" id="1_cost"] +[ext_resource type="Resource" uid="uid://dpbndqxvsffa0" path="res://docs/gyms/tiny_sword/currencies/magic_gold.tres" id="2_magic_gold"] + +[resource] +script = ExtResource("1_cost") +currency = ExtResource("2_magic_gold") +amount = 10 +metadata/_custom_type_script = "res://docs/gyms/tiny_sword/buildings/alchemy_tower/currency_cost_entry.gd" diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_recipe.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_recipe.tres new file mode 100644 index 0000000..9acf62c --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_recipe.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="AlchemyCraftRecipe" format=3 uid="uid://by8qmuhvo38nn"] + +[ext_resource type="Script" uid="uid://ddlnlpb0p750q" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd" id="1_recipe"] +[ext_resource type="Resource" uid="uid://brctmnpmhjas6" path="res://docs/gyms/tiny_sword/currencies/mana_stone.tres" id="2_mana_stone"] +[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/mana_stone_cost_entry.tres" id="3_cost"] + +[resource] +script = ExtResource("1_recipe") +id = &"mana_stone_recipe" +output_currency = ExtResource("2_mana_stone") +cost_entries = [ExtResource("3_cost")] diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_0.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_0.tres new file mode 100644 index 0000000..2178897 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_0.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="CurrencyCostEntry" format=3 uid="uid://cheww15gwet47"] + +[ext_resource type="Resource" uid="uid://brctmnpmhjas6" path="res://docs/gyms/tiny_sword/currencies/mana_stone.tres" id="1_8mjcf"] +[ext_resource type="Script" uid="uid://ba8e403mb7hp0" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd" id="1_cost"] + +[resource] +script = ExtResource("1_cost") +currency = ExtResource("1_8mjcf") +amount = 10 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_1.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_1.tres new file mode 100644 index 0000000..f9858f3 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_1.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="CurrencyCostEntry" format=3 uid="uid://bprij85vf66at"] + +[ext_resource type="Resource" uid="uid://t6du7gm2ywbi" path="res://docs/gyms/tiny_sword/currencies/cognite.tres" id="1_3oqnk"] +[ext_resource type="Script" uid="uid://ba8e403mb7hp0" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/currency_cost_entry.gd" id="2_q6dwm"] + +[resource] +script = ExtResource("2_q6dwm") +currency = ExtResource("1_3oqnk") +amount = 10 diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_recipe.tres b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_recipe.tres new file mode 100644 index 0000000..fbf026c --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_recipe.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="AlchemyCraftRecipe" format=3 uid="uid://nfpkv6a2u0wf"] + +[ext_resource type="Resource" uid="uid://cheww15gwet47" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/philosoper_stone_cost_entry_0.tres" id="1_jbac4"] +[ext_resource type="Resource" uid="uid://co4fiqgluwit0" path="res://docs/gyms/tiny_sword/currencies/philosoper_stone.tres" id="1_o0mnq"] +[ext_resource type="Script" uid="uid://ddlnlpb0p750q" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/recipes/alchemy_craft_recipe.gd" id="1_pxgqn"] + +[resource] +script = ExtResource("1_pxgqn") +id = &"philosoper_stone_recipe" +output_currency = ExtResource("1_o0mnq") +cost_entries = [ExtResource("1_jbac4")] +metadata/_custom_type_script = "uid://ddlnlpb0p750q" diff --git a/docs/gyms/tiny_sword/buildings/alchemy_tower/test_panel.gd.uid b/docs/gyms/tiny_sword/buildings/alchemy_tower/test_panel.gd.uid new file mode 100644 index 0000000..77c0785 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/alchemy_tower/test_panel.gd.uid @@ -0,0 +1 @@ +uid://d5wkgk7kfrma diff --git a/docs/gyms/tiny_sword/buildings/castle/Castle.png b/docs/gyms/tiny_sword/buildings/castle/Castle.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb93f59678473602ead8ec0547d4cdfb05bfa70 GIT binary patch literal 17094 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 J);T3K0RZj~a=`!q literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/castle/Castle.png.import b/docs/gyms/tiny_sword/buildings/castle/Castle.png.import new file mode 100644 index 0000000..d413ced --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/castle/Castle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cry4arn00kgrt" +path="res://.godot/imported/Castle.png-e8b352108f4d5f1f213446b1f212024a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/castle/Castle.png" +dest_files=["res://.godot/imported/Castle.png-e8b352108f4d5f1f213446b1f212024a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/castle/castle.gd b/docs/gyms/tiny_sword/buildings/castle/castle.gd new file mode 100644 index 0000000..f68c4cc --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/castle/castle.gd @@ -0,0 +1,20 @@ +extends Node2D + +@onready var _prestige_panel: PrestigePanel = $PrestigePanel + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + pass + +func _on_area_2d_mouse_entered() -> void: + ## TODO: remove this comment to enable prestige panel + #_prestige_panel.visible = true + pass + +func _on_area_2d_mouse_exited() -> void: + _prestige_panel.visible = false diff --git a/docs/gyms/tiny_sword/buildings/castle/castle.gd.uid b/docs/gyms/tiny_sword/buildings/castle/castle.gd.uid new file mode 100644 index 0000000..393df4e --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/castle/castle.gd.uid @@ -0,0 +1 @@ +uid://cjc0l13b07802 diff --git a/docs/gyms/tiny_sword/buildings/castle/castle.tscn b/docs/gyms/tiny_sword/buildings/castle/castle.tscn new file mode 100644 index 0000000..11a0274 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/castle/castle.tscn @@ -0,0 +1,72 @@ +[gd_scene format=3 uid="uid://cl05bdri38mxf"] + +[ext_resource type="Script" uid="uid://cjc0l13b07802" path="res://docs/gyms/tiny_sword/buildings/castle/castle.gd" id="1_cjtps"] +[ext_resource type="Texture2D" uid="uid://cry4arn00kgrt" path="res://docs/gyms/tiny_sword/buildings/castle/Castle.png" id="1_tgvch"] +[ext_resource type="PackedScene" uid="uid://dlidx2x0otpjg" path="res://core/prestige/prestige_panel.tscn" id="3_l7gct"] +[ext_resource type="Script" uid="uid://cpfmctd4xsfho" path="res://docs/gyms/tiny_sword/buildings/castle/test_buff.gd" id="4_l7gct"] +[ext_resource type="PackedScene" uid="uid://dudfvxilbydas" path="res://core/goals/current_goal_panel.tscn" id="4_r36om"] +[ext_resource type="Resource" uid="uid://bjc6qmvr7pe12" path="res://docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres" id="5_s3a5k"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_tgvch"] +size = Vector2(312, 198) + +[node name="Castle" type="Node2D" unique_id=1966557957] +script = ExtResource("1_cjtps") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1365573096] +texture = ExtResource("1_tgvch") + +[node name="Area2D" type="Area2D" parent="." unique_id=66203052] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1342657556] +position = Vector2(1, 22) +shape = SubResource("RectangleShape2D_tgvch") + +[node name="PrestigePanel" parent="." unique_id=245519778 instance=ExtResource("3_l7gct")] +offset_left = 161.0 +offset_top = -232.0 +offset_right = 581.0 +offset_bottom = -3.0 + +[node name="PanelContainer" type="PanelContainer" parent="." unique_id=549346899] +offset_left = 160.0 +offset_top = 5.0 +offset_right = 575.0 +offset_bottom = 201.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer" unique_id=321038386] +layout_mode = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer" unique_id=450633732] +layout_mode = 2 +size_flags_vertical = 0 +theme_override_constants/separation = 4 +alignment = 1 + +[node name="TestBuff" type="PanelContainer" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=1906280979] +layout_mode = 2 +script = ExtResource("4_l7gct") +buff_data = ExtResource("5_s3a5k") + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/TestBuff" unique_id=1293262110] +layout_mode = 2 + +[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/TestBuff/HBoxContainer" unique_id=1519392687] +layout_mode = 2 +text = "Label" + +[node name="Button" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/TestBuff/HBoxContainer" unique_id=1254643833] +layout_mode = 2 +text = "Button" + +[node name="CurrentGoalPanel" parent="PanelContainer/MarginContainer/VBoxContainer" unique_id=1627561697 instance=ExtResource("4_r36om")] +layout_mode = 2 + +[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"] +[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"] diff --git a/docs/gyms/tiny_sword/buildings/castle/test_buff.gd b/docs/gyms/tiny_sword/buildings/castle/test_buff.gd new file mode 100644 index 0000000..a83233a --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/castle/test_buff.gd @@ -0,0 +1,118 @@ +extends PanelContainer + +@export var buff_data: GeneratorBuffData + +@onready var _game_state: LevelGameState = find_parent("LevelGameState") +@onready var _label: Label = $HBoxContainer/Label +@onready var _button: Button = $HBoxContainer/Button + +var _current_level: int = 0 + + +func _ready() -> void: + if buff_data != null: + _label.text = buff_data.text if not buff_data.text.is_empty() else str(buff_data.id) + _button.text = "Buy" + _button.disabled = true + else: + _label.text = "No Buff" + _button.text = "Disabled" + _button.disabled = true + + _button.pressed.connect(_on_button_pressed) + if _game_state: + _game_state.buff_level_changed.connect(_on_buff_level_changed) + _game_state.buff_unlocked_changed.connect(_on_buff_unlocked_changed) + _game_state.currency_changed.connect(_on_currency_changed) + _update_ui() + + +func _on_button_pressed() -> void: + if buff_data == null: + return + + var cost_currency_id: StringName = _resolve_buff_cost_currency_id(buff_data) + if cost_currency_id == &"": + push_warning("test_buff: Invalid cost currency") + return + + var cost: BigNumber = buff_data.get_cost_for_level(_current_level) + if _game_state and _game_state.spend_currency_by_id(cost_currency_id, cost): + var next_level: int = _current_level + 1 + _game_state.set_buff_level(buff_data.id, next_level) + _apply_buff_effect(buff_data, next_level) + _current_level = next_level + _update_ui() + else: + push_warning("test_buff: Not enough currency to buy buff") + + +func _apply_buff_effect(buff: GeneratorBuffData, level: int) -> void: + if buff.kind != GeneratorBuffData.BuffKind.RESOURCE_PURCHASE: + return + + var target_currency_id: StringName = _resolve_buff_target_currency_id(buff) + if target_currency_id == &"": + return + + var purchased_amount: BigNumber = buff.get_resource_purchase_amount_for_level(level) + if purchased_amount.mantissa > 0.0 and _game_state: + _game_state.add_currency_by_id(target_currency_id, purchased_amount) + + +func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName: + if buff.resource_target_currency == null: + return &"" + + if _game_state == null: + return &"" + return _game_state.get_currency_id(buff.resource_target_currency) + + +func _on_buff_level_changed(buff_id: StringName, new_level: int) -> void: + if buff_data != null and buff_id == buff_data.id: + _current_level = new_level + _update_ui() + + +func _on_buff_unlocked_changed(buff_id: StringName, unlocked: bool) -> void: + if buff_data != null and buff_id == buff_data.id: + _update_ui() + +func _on_currency_changed(_changed_currency_id: StringName, _new_value: BigNumber) -> void: + _update_ui() + +func _update_ui() -> void: + if buff_data == null: + return + + if _game_state and not _game_state.is_buff_unlocked(buff_data.id): + _button.disabled = true + _button.text = "Locked" + return + + var is_maxed: bool = _current_level >= buff_data.max_level if buff_data.max_level >= 0 else false + var can_buy: bool = not is_maxed + + var cost_currency_id: StringName = _resolve_buff_cost_currency_id(buff_data) + if cost_currency_id != &"": + var cost: BigNumber = buff_data.get_cost_for_level(_current_level) + var current_amount: BigNumber = _game_state.get_currency_amount_by_id(cost_currency_id) if _game_state else BigNumber.from_float(0.0) + can_buy = can_buy and (current_amount.is_greater_than(cost) or current_amount.is_equal_to(cost)) + + _button.disabled = not can_buy + + if is_maxed: + _button.text = "Maxed" + else: + var cost: BigNumber = buff_data.get_cost_for_level(_current_level) + _button.text = "Buy (%s)" % cost.to_string_suffix(2) + + +func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName: + if buff.cost_currency == null: + return &"" + + if _game_state == null: + return &"" + return _game_state.get_currency_id(buff.cost_currency) diff --git a/docs/gyms/tiny_sword/buildings/castle/test_buff.gd.uid b/docs/gyms/tiny_sword/buildings/castle/test_buff.gd.uid new file mode 100644 index 0000000..4fedb91 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/castle/test_buff.gd.uid @@ -0,0 +1 @@ +uid://cpfmctd4xsfho diff --git a/docs/gyms/tiny_sword/buildings/farm/House1.png b/docs/gyms/tiny_sword/buildings/farm/House1.png new file mode 100644 index 0000000000000000000000000000000000000000..c3f536453b5276a3e5a55cc166ce198723cb31ec GIT binary patch literal 5282 zcma)g_dDBf)c-pq#Hvt56(OZ)rKM&EV$Uk7R;^NMRJB&D5?jOwYHzh#t1Yods97sT z=|`=S8dWP4F(2RWAMjkyxvu*@*ZJvQuXA3n`(O?9HCY&W7y$ra(biHkI`^3KL4?ws z&x4MxrRNUhX{4zFR1Nd40sx1lwwkiBZ}z&?kgL@s8(ePYSz-$DhuL*A6MS5F>qjez zc(7O(gGEuKIJ?SO*vp!g67ZN?-3eECv#56s>=Uh+nM~+c^RB+G!c&Y^Pfz~ig7uOj zrsug!Ls=P8%WJdyxx=i?6RrC>0UOkl;lXToOyvJ=&R;&YNs%tytZF%W=d<~9sGV9HX(u@2CjIi}?Wfq3&2 z;4g-^8w8f>ljGlWp~B+cbE2?3_t#BqT_AX!Yz{8H8fb|Tz~6~?`g2y9|H?vL!kZ>v z&C1$*SU;geG7gb`9pu}eP`v*FNJndgBn@) z@?8`J2Ij6xQ|zB&!*D=eZ*VdWdzeqt{yC_^f>lDNE&Q#RRl!vI z{$E=GX`%h!r%b+$XDVL0E9}-A?U=?fDG9Xy{mKqAbHIA-;b5+Ge{ZIS>ysgB;C;8i zdLcDtcE8geq%1cRXp3J4>nmi#*svw{Houq(G0U`p&2fELH+H(ur)`^VBW9(C!`WJf zlgjTbL=d&(+?wq@-M!cE^4`Du;!q**kQ=zrj8mX#XbcL7KFFvUc!ge^n0I$2YRBAg zI_^YjMEQI@qq?kcmz`++lQ3S4^E3Uw0y3RMY8d;X;pzDh|22u=6r!7T*_{O#uqWt9wO@h zcJnk8I}z5O!AWOn(2tfOoT^TwiV#PJA~f2WC5Ar4Vr)1QgH^_B1jdZ8{yI;bikG&m zb?0E6jxVK8T=T+!FkD9XnLkWhTQBouenofwTPr!w-OXIF#7~Tayh5-RCf=CQt}%hB*cp< z%T?9cKv$5-9q)2h1O~^n!=mG8FYvDmne+6&Ia#)enp0nfy{-Bs`0NR=t3B1n zw+T&LqPV$l5VPD@gQgc3S^3wQ`|9aJRiF$D^WWl|C!YA>#X-olw4+auy z#mH}W`|1GsyFKsTDC%lT?H6xtt=hll#USJ;y;CD&O6aF-ZD%j#t|NIwT|RY8uVyHZ z2;1x%xcq2NXUgxox|hpW*6QA3`Y^2iTIs~W&CD=CTXQWHz}G516Xs#42L$rBeqYz= zpNz&aR5iT-k@o#44btuTJr@qvxtF(sXfdjj7>ANdQ!RB9=KLY=8W#n*BYsNlbI0ME zXnn=tvwQGhFi<@Mv@cT?h_8f6(}gA_jXNwjG~|g4JWOi9vKMSFX;R2}dRnT)dOD(x zVmrKPA#3rdp(}`Z%AOcDHsT<2MIhG97TvCFatLR?0U9n zRX{wPjG4Q5YQ=*f9Rn?Ofso*=QW3U}i2M{Z-193#i&jIiFGl2wWZhduuVdKBp0-q}|E%*>!!v%XX(YVH)=rQ2(pg~LeE!&M0-$h397 zz$f+$YzT*Q!2Ap^`%_4AZA6kwqfq#?Mn8og*In?bPwa)zObjIVuLAs!6E^3q5slS9 zulVn$fUs*KqKPv zN)N4u{s~Bm4(DkNwP&NvZvMD}n2=i9SgmAAET2gm&E38BR0j0_b14su^>sr!j9-OXGvO&Y*5UaYy_hPWz_znL+zV%+42s}% z6w-J{1&k_FwIrE{KTC9XnEpI_s8gnKowKu%3Af}4ydy8irUVQ#;b*lwcFNiKq)JdH z!TpM9g_S_8zN$h9@Qz@zSCiVZ}{+*EhMi;jQ4;dU&P;qF%M%OcG^uIBBy zF-}q#QtH)%Z><~j2dDtH0qdObJCe2l4%kP?YWf0Bv6kcktsVNqP2|-cXIAc z1iq=g-H1%&#(@k4E`3S-!>z*x>b&yTT=X)Ovj&iPUTs97_80+snTJ_x_3E#E z)oiiXHNlJFAHH-5Hz}^NySRCZSzdpT_HO6*m=?ENr#7PDwY};feaVBdZsDy4Yxw@< zrk2mlKeO(+BoxrU&ob;nG;3nzK$WPTMVca?w(-V`Q`)rH!{Z&9oko}eEVE(j?i@e8 zTs9rI_34J7iDy=e9$fLw^k$Jk|5+M0O)25zBo7ifXQEbnB^--irZsRk4lX35jEidNs*16cW&ok<>DHAYjzgx zkYW${5Kq?wj%>NA?(%25HBwZ(oOsgowvLk{62r+jH^Q2HR|?H&`}-pt)b8o6ml@G* z0rHww@=3n-k{?O2XN@J34q-lL)ONBoD01hw`Omxsc9oqQMh0CF0r63paPz~2%1xEoA?r?LJXcY3yHzJ{P#8FUBq$AQ-65k zm<>K=7(Aa-uZbSso38IWqu^K(o|n_;-k{4ILTA2ADa71V$N#;jauby`u$zAqA#v=EdwW(l_u9Afx`}GWFAilz}QQq?YseM=%f-W+lof zey@D_7Mfop6#~r}FXkKLY1UTNsnlHXsjoZF18DK1PM&Z}K{sVHhg>^Q45NUTmXy*d zp8&7IGCo1;Qixm*FB|DJ7XM_tu5#5FNIF|qZD2n{!1J?#idr2kD$I*N%=q*ZqK_Sj z+ASFMZF??V%|YcAlq44=u3e~Agjq+Lq>g1WQWge7PEti&j@F6V`&*%KlJ= z&F2DzACg+H!EJB%X^wM%PzT}O+Th0!V*fsK?)bGioDl+x6={Njh-EN4HM}^6RH*BE zm`bM&yeID~H-NRlvrB?}wuLHq3sYn8W3WJCb%*CQ2~BaTB)EExI&mW2T{?NQDKE~O zUJMcKEaRsR3yjWsb7L`gOj+2se?RGz3O|!P{Mo0SHyWOJrTwIMvp$qt2MZAE=vu^8 zRifE34tomp>K^Q(N#2Xm$H{vK+ zcWWXLv(dM_S?CALi-Vd}zWcpOI7hLnREjeW7k|wk{|t1W?0WLEm`2fM4@r@0qdp~P z*74~L)&SK=WoNW4GCV~b6k26OCr>VGV45fE5)>l?F@_-WVIa2e$A6lm5;9z>FZEz+ zj6kQuSHJ61jQQzcihVrx6Idm0Qx^!O$;c(iJ(`E=9(+ z6up}(vJw+9_Jg;o*GFZ-jb$^TTk?KE+2T>9auV;aM$<@jY;F(k=p$O(7`{aihJsUneASMkl#z%9jB8R{EHpoRWh zSUgna6F_tobfN*GFM`wuZ4F2eQrF7BeX?hAYE4z%yN8t3dw$kBor~;P{3ucY}M-F?6qH^6|O~C8ZO10a*i=$@0j{ZBJdTj#dRt#v}v4?ICOt56hB-M zf6(?e(iAK#P|0D};5f5eI7NANou(WgwA0+KR$AG8p}0S7bkxQ7Yobi9d-WTPLw_8i z8bss+nj^Kc5M+T8OiKVaQu!dgZgh?fiy*-BlOXv7A#ZwoHd?1SwQFN)2rXMvfm-=# zlo@9E<4dZQ^*xU-7ZQf%`7!PB^Z-%)baUm z<}ozuPV;>`b-QWS6WO(+CZ8LRSz|w<-iX4P7p{hI$goBhX|W{|E^-GglIl;rmBieI z4ZCPsrh>P;v0gn8$t$ycT)`v`gDNykO^o$8Lx(+a733 zzPqx+dmi>5{VQe6lD>zl1Ax1XG&;3-Qxk*r1dvq3L>Q2w+5T$8+(U5?_Qw$+Uaf_M2qaY19@3B z=kp-3DkA$!?wwkzQM*X*W|-=-vOu2L?s%1ODh#mk&adRKSw)!$Fwxq{Ip~WND zke2RLc|)^{wyL9oI_aNJ5*rv%IR8!2$<}tbK;rcDKCZn!KCe2?`Ve&eokj9{$Em4n zc)~XIL2Q7J(cfXApm;cVXZu0A`HyLoCl!deelgj%44A@hFjZpr%XBFk2fu7Lfm waN(8|i$EzldBuR!)(JH?r~iY=s~jkSx~}eC<_qvSZ#n|n>iTL`Dz>5j0~$;DSO5S3 literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/farm/House1.png.import b/docs/gyms/tiny_sword/buildings/farm/House1.png.import new file mode 100644 index 0000000..a32825f --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/farm/House1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dfdh8r03sj7qv" +path="res://.godot/imported/House1.png-8d2a4466ed180272dd4fd5a1906cd2d6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/farm/House1.png" +dest_files=["res://.godot/imported/House1.png-8d2a4466ed180272dd4fd5a1906cd2d6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/farm/farm.tscn b/docs/gyms/tiny_sword/buildings/farm/farm.tscn new file mode 100644 index 0000000..8abd88b --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/farm/farm.tscn @@ -0,0 +1,38 @@ +[gd_scene format=3 uid="uid://djedqovgngrx5"] + +[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_rvsna"] +[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="2_djlc5"] +[ext_resource type="Resource" uid="uid://d08h51y0pnsnf" path="res://docs/gyms/tiny_sword/buildings/farm/farm_generator.tres" id="3_82rmq"] +[ext_resource type="Texture2D" uid="uid://dfdh8r03sj7qv" path="res://docs/gyms/tiny_sword/buildings/farm/House1.png" id="4_djlc5"] +[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://core/generator/generator_container.tscn" id="5_82rmq"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_82rmq"] +size = Vector2(106, 157) + +[node name="Farm" type="Node2D" unique_id=283335851] + +[node name="CurrencyGenerator" parent="." unique_id=967969064 node_paths=PackedStringArray("info_generator_container") instance=ExtResource("1_rvsna")] +currency = ExtResource("2_djlc5") +data = ExtResource("3_82rmq") +press_buys_generator = false +info_generator_container = NodePath("../GeneratorContainer") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1103280443] +texture = ExtResource("4_djlc5") + +[node name="Area2D" type="Area2D" parent="." unique_id=529600485] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=204675694] +position = Vector2(0, -2.5) +shape = SubResource("RectangleShape2D_82rmq") + +[node name="GeneratorContainer" parent="." unique_id=1451609580 node_paths=PackedStringArray("_generator") instance=ExtResource("5_82rmq")] +visible = false +offset_left = 138.0 +offset_top = -100.0 +offset_right = 402.0 +offset_bottom = 110.0 +_generator = NodePath("../CurrencyGenerator") + +[connection signal="mouse_entered" from="Area2D" to="CurrencyGenerator" method="_on_area_2d_mouse_entered"] +[connection signal="mouse_exited" from="Area2D" to="CurrencyGenerator" method="_on_area_2d_mouse_exited"] diff --git a/docs/gyms/tiny_sword/buildings/farm/farm_generator.tres b/docs/gyms/tiny_sword/buildings/farm/farm_generator.tres new file mode 100644 index 0000000..2e779bf --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/farm/farm_generator.tres @@ -0,0 +1,21 @@ +[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://d08h51y0pnsnf"] + +[ext_resource type="Resource" uid="uid://50yq2hl3wfwq" path="res://docs/gyms/tiny_sword/research/farm_research.tres" id="2_hprai"] +[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="3_bbypn"] +[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="4_0kvfm"] +[ext_resource type="Resource" uid="uid://cts0407h130d6" path="res://docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres" id="5_qiy1b"] + +[resource] +script = ExtResource("4_0kvfm") +id = &"farm" +name = "Farm" +starts_unlocked = false +starts_available = false +initial_owned = 1 +purchase_currency = ExtResource("3_bbypn") +unlock_goal = ExtResource("5_qiy1b") +initial_cost = 1.0 +coefficient = 1.0 +initial_productivity = 20.0 +research_data = ExtResource("2_hprai") +metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/docs/gyms/tiny_sword/buildings/forestry/House3.png b/docs/gyms/tiny_sword/buildings/forestry/House3.png new file mode 100644 index 0000000000000000000000000000000000000000..f410672159bef8b14e6b648f2e44738343f33815 GIT binary patch literal 4418 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 UJ~ZuDPZ2=>o}o^SmVLzk07ea4CIA2c literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/forestry/House3.png.import b/docs/gyms/tiny_sword/buildings/forestry/House3.png.import new file mode 100644 index 0000000..7b45513 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/forestry/House3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://doxauaa3vn3lm" +path="res://.godot/imported/House3.png-86e0e955279bc70e5c6c622bf20a793e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/forestry/House3.png" +dest_files=["res://.godot/imported/House3.png-86e0e955279bc70e5c6c622bf20a793e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/forestry/forestry.gd b/docs/gyms/tiny_sword/buildings/forestry/forestry.gd new file mode 100644 index 0000000..1e243a3 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/forestry/forestry.gd @@ -0,0 +1,13 @@ +extends Node2D + +@onready var _generator_container: GeneratorPanel = $GeneratorContainer + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + _generator_container.visible = false + +func _on_area_2d_mouse_entered() -> void: + _generator_container.visible = true + +func _on_area_2d_mouse_exited() -> void: + _generator_container.visible = false diff --git a/docs/gyms/tiny_sword/buildings/forestry/forestry.gd.uid b/docs/gyms/tiny_sword/buildings/forestry/forestry.gd.uid new file mode 100644 index 0000000..9b18cfb --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/forestry/forestry.gd.uid @@ -0,0 +1 @@ +uid://cckf4nacd4qcr diff --git a/docs/gyms/tiny_sword/buildings/forestry/forestry.tscn b/docs/gyms/tiny_sword/buildings/forestry/forestry.tscn new file mode 100644 index 0000000..d96e43a --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/forestry/forestry.tscn @@ -0,0 +1,40 @@ +[gd_scene format=3 uid="uid://cf01wvy3f17i"] + +[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_7j1xc"] +[ext_resource type="Script" uid="uid://cckf4nacd4qcr" path="res://docs/gyms/tiny_sword/buildings/forestry/forestry.gd" id="1_l4bxt"] +[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="2_23x2t"] +[ext_resource type="Resource" uid="uid://v6hjoa2vky5k" path="res://docs/gyms/tiny_sword/buildings/forestry/forestry_generator.tres" id="3_pby1g"] +[ext_resource type="Texture2D" uid="uid://doxauaa3vn3lm" path="res://docs/gyms/tiny_sword/buildings/forestry/House3.png" id="4_5yp33"] +[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://core/generator/generator_container.tscn" id="5_rh3m6"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_v4aq3"] +size = Vector2(123, 157) + +[node name="Forestry" type="Node2D" unique_id=726284577] +script = ExtResource("1_l4bxt") + +[node name="CurrencyGenerator" parent="." unique_id=967969064 node_paths=PackedStringArray("info_generator_container") instance=ExtResource("1_7j1xc")] +currency = ExtResource("2_23x2t") +data = ExtResource("3_pby1g") +press_buys_generator = false +info_generator_container = NodePath("../GeneratorContainer") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1575721003] +texture = ExtResource("4_5yp33") + +[node name="Area2D" type="Area2D" parent="." unique_id=987240531] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=849105822] +position = Vector2(0.5, -2.5) +shape = SubResource("RectangleShape2D_v4aq3") + +[node name="GeneratorContainer" parent="." unique_id=1451609580 node_paths=PackedStringArray("_generator") instance=ExtResource("5_rh3m6")] +visible = false +offset_left = 128.0 +offset_top = -100.0 +offset_right = 392.0 +offset_bottom = 110.0 +_generator = NodePath("../CurrencyGenerator") + +[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"] +[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"] diff --git a/docs/gyms/tiny_sword/buildings/forestry/forestry_generator.tres b/docs/gyms/tiny_sword/buildings/forestry/forestry_generator.tres new file mode 100644 index 0000000..e954ea0 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/forestry/forestry_generator.tres @@ -0,0 +1,21 @@ +[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://v6hjoa2vky5k"] + +[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="2_fs80u"] +[ext_resource type="Resource" uid="uid://cvow4sj6a5h23" path="res://docs/gyms/tiny_sword/research/forestry_research.tres" id="2_n4bxc"] +[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_mgk3v"] +[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="4_rij12"] + +[resource] +script = ExtResource("4_rij12") +id = &"forestry" +name = "Forestry" +starts_unlocked = false +starts_available = false +initial_owned = 1 +purchase_currency = ExtResource("2_fs80u") +unlock_goal = ExtResource("4_mgk3v") +initial_cost = 1.0 +coefficient = 1.0 +initial_productivity = 20.0 +research_data = ExtResource("2_n4bxc") +metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png new file mode 100644 index 0000000000000000000000000000000000000000..ad35201c683c4dce2ceffd12565cee4fa20881cc GIT binary patch literal 802 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>V7lY!;uumf z=k3kCd6xquj(udmGM7)o+sLq_!AwZ}f}vng*aQt`@dKn6E)8i)2vy+t5{Iq9JJ@d?cK{yKE7|edtQDuf8*=flTRI8uT`@` zSFxZv`BKftwI9^mU;q5~E+eY4PTHxU^lJQr{FU1OT-QDCJk3y2^|!UVzG#QsU2(g= z$C5ejY~8xyhrE5M%F*uovcB5w%-jchqwQ5a@7t|^`rC$=eVvs<#Glg%N4LLq*Wc&y z=_=>7SNxgl)}GaQ&lH+8`QU~hirkIjKt*0z+kpDLe_LJeTYUUp<)5#Y_Fw;hAeEu- zoB5}=YtQ-w{nd^aU4L}Cj4zY*-hU34qF>rDgzb>OZ|^Z_)6E6Hq|*KC&7_~)X0NvX z`S0!B%QgWwQ{FtJt6!@^&fkrR2^U3=EX%CvQa(EWHPf?i&DXu- zZ_lfnEl}njTrPXysf>eG|Lynj_o83@+3>ac+7E`5$%Px^`Gb$ky?LuKn>Xt@Q{qSA z_05N`Y`AQqcPk=GV#2d_rs%V)rO$V-ZcuxnRe3^*F=xuLa)!*EdI>@Q8E0H^nqaQS zq_A}UksH=EKivIwSaLP@J<(v8n7H5ytC(iaD(#A#|C1IogmjiRu9|xA(Z`LSZAIdX zj5w!U>UQK(YkBtb@a##CCOcN2x4(72L$prgCs=!s5zA7B8ISB+!~V1^Tz*J8oN>ZA z=Lx>Yj<-6vG$$yo(AqaWbic%Q^EACp36DQ^>K92jY!DW)Uwyglko0%P31a+?v$^hb m&5f&nka|{*aH?Wp@cY9SF2KNfsO{}akaAB~KbLh*2~7a~_EhBn literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png.import b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png.import new file mode 100644 index 0000000..5c96762 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cfj3i3o3fkyb3" +path="res://.godot/imported/Gold Stone 1.png-7042dbb8b3bf15fcc9c607c8de62d4ac.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1.png" +dest_files=["res://.godot/imported/Gold Stone 1.png-7042dbb8b3bf15fcc9c607c8de62d4ac.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1_Highlight.png b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 1_Highlight.png new file mode 100644 index 0000000000000000000000000000000000000000..7ecde6ba06978752ff7e3f996ac9ba4107cd0496 GIT binary patch literal 2150 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 literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3_Highlight.png.import b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3_Highlight.png.import new file mode 100644 index 0000000..e732bb3 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3_Highlight.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjqe3efu2aat6" +path="res://.godot/imported/Gold Stone 3_Highlight.png-539c0e6cdbe76410543390c2f48e7dd9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 3_Highlight.png" +dest_files=["res://.godot/imported/Gold Stone 3_Highlight.png-539c0e6cdbe76410543390c2f48e7dd9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4.png b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4.png new file mode 100644 index 0000000000000000000000000000000000000000..1178f7c217887e7928703c52b113ce241dc55c45 GIT binary patch literal 1364 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 MLoNr4>`vYIAH1(YasU7T literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4_Highlight.png.import b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4_Highlight.png.import new file mode 100644 index 0000000..fe79e4f --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4_Highlight.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://befcjyx8tmc4a" +path="res://.godot/imported/Gold Stone 4_Highlight.png-f9cc5b1396d5ea96bb589d305b45bc4a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 4_Highlight.png" +dest_files=["res://.godot/imported/Gold Stone 4_Highlight.png-f9cc5b1396d5ea96bb589d305b45bc4a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png new file mode 100644 index 0000000000000000000000000000000000000000..c633394303cce7a1da3d7c0616fb504f39899e3b GIT binary patch literal 2099 zcmZuyc`)0H7XBq_2~`QP)KVH-sY`0BcWa4MYNxfnY9+agKsrf{I##;OhJD&D=9{&Np+;nfcDlneUwBt4`M9qVl2u0EpY# zSh*Y{>4%F59mlukzYH7$D8j`W4bTS_mjOU{)z-?=Eh>NYWxJE2Ej*Foxd8oV9Vi%$ zLh@hp0kVoc8EYRTYzjz_iq*I6c%Vg)$jb9JvduekEHHo?<6e$Ih)_ zSFqXinGd@~Fb9JJgBYjT`;`@~XTis$e`hG=sLXw!W$@^Tf8ZK(G}(DXVCUauoBZ!Ev)-Yw&6`CBNQjy8Atp+#XDWD0sYU_XVnl!N+|986(|AuY#nyE zf$wczTj?cmTO(B|CX~#RiyjXm>iYvC0&cFUwxtAu5p^)NY?Xab5skinQsFl^D~3E- zu8$6t+GGO8T#0+K`J9(jr;&-d>)hD-e-!yMzQAB3ygsiQ(<@QZS&%ylchNlh~<{BBK){{sK5dL zZUq&;g;joJe<0YK?`9)A{qQS%{?eWw6V4=XKdCumJM>;8mH-*+bVA6j6K_UIQzAd8 zng>^U8wk*cW%(NB?J-z=%Xj8(L75+~7v)1B1W=92N%^ZshpFjXW&YAiwS_bR?3PO+&#$O;2w@uE4~@fM6c z8;0P=orL9Ag)!MSr`2<>q=72BpJ0sQTO4Ix#;tv8qs%3V&5DdVoTbU)r7XtrX`CX? za+e|evszo+nP;Jw#3_}aX*hMKGg8fhwZvT1nKEP~1`*-BdUat%W(DGy^(ZDDLAPy8^J!TWWdUb1x!w#L_bbSq!yhNo}12f0V{a9GSE7x+BNMM!Pj7 zeMnX9b)yYYCB)!Yk35tSpLu=LWXSeCtX!XwS{j6c1io`W++q}<*iwBDvi$^a%@elh zSazy|OM1HyzFB5IW5R4UCB^v5RK|wJAO06)~ZQ0bYFgmzq;PE+o;s`TeG$hQ^G&$7gUY zE`TJ&h-A0okiR@ylfGIUuuGtu1&duXBK7~V20l3Kqi!C|_{%#e3rhT+ed1$JQ^-Z@ z!J!cW70Fe1vLLI9+zdtBwkM-2jR!}nx>9&#^Ge)P6 z)}6K4K4Mlk;Q741vd?w z@jXrogPPFz&P>UaTRgrzpE>mXTB4J8(eB{SUfn}7hspZ#zx~P6j^}+Nyh7y(=Jcqk zb0e(cSF}i!0OG$*N8bBAb+s5jPvGnr{W0c=%UHW8jVO=^U*b#fv2pXSmTST)E)bI* zhD?fdGg++;e?sTv_Z-D(TD8`I>(rRDS4se0w2MHO3$)`mzV{MnzEiNeaC~(H6}r?T z(;(=;C&!aU?T-5Wlx_xYUjLA%Ye_sKcYW!xvcgiSt0ip-kqXi|MHNYX_2HKh)-7fn zrTW*V_&1?JSzhZoP=nJ&uBBV+VCxTl>&#F*us{=FE8>Zc|8Ar+ba)pS_sxj{{ zROju_SsHqR40&clvhp4@O?X_aEFsn>WM?+1@vx<=cl>cp?N*bP`O|O#UcKT$>#Keio9pQGDCnFKUlnd7d$| zE`!A_)^Xi@IYOrvJtokO)eZMNHYI(KTROxv=-F6^jQauof_NdM7AJx1FfkX6jL|YH zBQFF_F1R?R^)0C$Cg_44uaqIJ(;-oBDg`+j!%glZdyklT9U038Z&FaOu`XRO(^tzV z+-yEYG^tf=B2KNC&N*k?SUFOSc%|`eXw1I1;bm$|e3j^M{hMi;r_gZHjjh||)sGg7=bRNb zdjYM)#+*`Tra?QBWY15(esR(ivr=PQDvK%9+H5tyMH@o2eoX|S)Ahy0{_es4e-8Rq X?~|BbfTIxE^hd!K<77og;~)G5_XO#a literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png.import b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png.import new file mode 100644 index 0000000..16041a1 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjaqlfshs823o" +path="res://.godot/imported/Gold Stone 5.png-c2b9b25484a63f524f46419e5fcf3e8f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5.png" +dest_files=["res://.godot/imported/Gold Stone 5.png-c2b9b25484a63f524f46419e5fcf3e8f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5_Highlight.png b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 5_Highlight.png new file mode 100644 index 0000000000000000000000000000000000000000..af98329231b1d48af0be5a7a6818f9d8c391d6da GIT binary patch literal 6192 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 E1I(cCE&u=k literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6.png.import b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6.png.import new file mode 100644 index 0000000..d36c5fd --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqf6yhtsnmjad" +path="res://.godot/imported/Gold Stone 6.png-0e048b3da07c839cd632ec6705baf4dc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6.png" +dest_files=["res://.godot/imported/Gold Stone 6.png-0e048b3da07c839cd632ec6705baf4dc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png new file mode 100644 index 0000000000000000000000000000000000000000..735bbe08e265003fc8f064b47065030ea8dd1827 GIT binary patch literal 5802 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 PU(sx>9jr<$PG0#hF+XHu literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png.import b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png.import new file mode 100644 index 0000000..4b1b558 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c3ssnuxmsqyuq" +path="res://.godot/imported/Gold Stone 6_Highlight.png-782ec372f9d78c1068ab0c8f07008eeb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/gold_mine/Gold Stone 6_Highlight.png" +dest_files=["res://.godot/imported/Gold Stone 6_Highlight.png-782ec372f9d78c1068ab0c8f07008eeb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stones.aseprite b/docs/gyms/tiny_sword/buildings/gold_mine/Gold Stones.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..d019cebda3c0518fe45bfa1778cbf48e8305ed51 GIT binary patch literal 35957 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 L4K?bJ4l(}&vG9m_ literal 0 HcmV?d00001 diff --git a/docs/gyms/tiny_sword/buildings/monastery/Monastery.png.import b/docs/gyms/tiny_sword/buildings/monastery/Monastery.png.import new file mode 100644 index 0000000..0ed877d --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/monastery/Monastery.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dlmjd2berkb6v" +path="res://.godot/imported/Monastery.png-5f49b9547f0b0c767155d570a4d6cc55.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/gyms/tiny_sword/buildings/monastery/Monastery.png" +dest_files=["res://.godot/imported/Monastery.png-5f49b9547f0b0c767155d570a4d6cc55.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/gyms/tiny_sword/buildings/monastery/monastery.gd b/docs/gyms/tiny_sword/buildings/monastery/monastery.gd new file mode 100644 index 0000000..ad93604 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/monastery/monastery.gd @@ -0,0 +1,14 @@ +extends Node2D + +@onready var _research_panel: ResearchPanel = $ResearchPanel + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + _research_panel.visible = false + +func _on_area_2d_mouse_entered() -> void: + _research_panel.visible = true + + +func _on_area_2d_mouse_exited() -> void: + _research_panel.visible = false diff --git a/docs/gyms/tiny_sword/buildings/monastery/monastery.gd.uid b/docs/gyms/tiny_sword/buildings/monastery/monastery.gd.uid new file mode 100644 index 0000000..d951735 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/monastery/monastery.gd.uid @@ -0,0 +1 @@ +uid://cj0sxoceqqyrm diff --git a/docs/gyms/tiny_sword/buildings/monastery/monastery.tscn b/docs/gyms/tiny_sword/buildings/monastery/monastery.tscn new file mode 100644 index 0000000..c5ceae6 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/monastery/monastery.tscn @@ -0,0 +1,30 @@ +[gd_scene format=3 uid="uid://bf8lqbexvnx6e"] + +[ext_resource type="Script" uid="uid://cj0sxoceqqyrm" path="res://docs/gyms/tiny_sword/buildings/monastery/monastery.gd" id="1_e17qd"] +[ext_resource type="Texture2D" uid="uid://dlmjd2berkb6v" path="res://docs/gyms/tiny_sword/buildings/monastery/Monastery.png" id="1_yyw6r"] +[ext_resource type="PackedScene" uid="uid://dd8roif0mqirl" path="res://core/research/research_panel.tscn" id="3_c6gi6"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_yyw6r"] +size = Vector2(160, 254) + +[node name="Monastery" type="Node2D" unique_id=1545930496] +script = ExtResource("1_e17qd") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=2097167417] +texture = ExtResource("1_yyw6r") + +[node name="Area2D" type="Area2D" parent="." unique_id=926221537] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1198096629] +position = Vector2(1, 16) +shape = SubResource("RectangleShape2D_yyw6r") + +[node name="ResearchPanel" parent="." unique_id=1105274967 instance=ExtResource("3_c6gi6")] +visible = false +offset_left = 78.0 +offset_top = -98.0 +offset_right = 478.0 +offset_bottom = 128.0 + +[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"] +[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"] diff --git a/docs/gyms/tiny_sword/buildings/monastery/monastery_generator.tres b/docs/gyms/tiny_sword/buildings/monastery/monastery_generator.tres new file mode 100644 index 0000000..a132f84 --- /dev/null +++ b/docs/gyms/tiny_sword/buildings/monastery/monastery_generator.tres @@ -0,0 +1,7 @@ +[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://56bnik1oe3ao"] + +[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="1_mnd3i"] + +[resource] +script = ExtResource("1_mnd3i") +metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/docs/gyms/tiny_sword/currencies/ascension.tres b/docs/gyms/tiny_sword/currencies/ascension.tres new file mode 100644 index 0000000..453a21b --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/ascension.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://bfrb0ayrljac2"] + +[ext_resource type="Texture2D" uid="uid://d3nymghus6x3" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_03.png" id="1_icon"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="2_script"] + +[resource] +script = ExtResource("2_script") +id = &"ascension" +display_name = "Ascension" +icon = ExtResource("1_icon") +metadata/_custom_type_script = "uid://dtgqjf3bl7pm8" diff --git a/docs/gyms/tiny_sword/currencies/cognite.tres b/docs/gyms/tiny_sword/currencies/cognite.tres new file mode 100644 index 0000000..c8d9cbd --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/cognite.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://t6du7gm2ywbi"] + +[ext_resource type="Texture2D" uid="uid://c1gnh8jgctrcx" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_07.png" id="1_axihf"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="2_fxby5"] + +[resource] +script = ExtResource("2_fxby5") +id = &"cognite" +display_name = "Cognite" +icon = ExtResource("1_axihf") +metadata/_custom_type_script = "uid://dtgqjf3bl7pm8" diff --git a/docs/gyms/tiny_sword/currencies/food.tres b/docs/gyms/tiny_sword/currencies/food.tres new file mode 100644 index 0000000..ae2b84e --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/food.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://bxg2au0ijp242"] + +[ext_resource type="Texture2D" uid="uid://dkt4s1eyw8puy" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_04.png" id="1_4u6o6"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="2_anstu"] + +[resource] +script = ExtResource("2_anstu") +id = &"food" +display_name = "Food" +icon = ExtResource("1_4u6o6") diff --git a/docs/gyms/tiny_sword/currencies/gold.tres b/docs/gyms/tiny_sword/currencies/gold.tres new file mode 100644 index 0000000..b075373 --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/gold.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://w4u1hddplb4e"] + +[ext_resource type="Texture2D" uid="uid://d3nymghus6x3" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_03.png" id="1_n656e"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="2_vohw6"] + +[resource] +script = ExtResource("2_vohw6") +id = &"gold" +display_name = "Gold" +icon = ExtResource("1_n656e") +always_visible = true diff --git a/docs/gyms/tiny_sword/currencies/magic_gold.tres b/docs/gyms/tiny_sword/currencies/magic_gold.tres new file mode 100644 index 0000000..4565024 --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/magic_gold.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://dpbndqxvsffa0"] + +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_v6rd4"] +[ext_resource type="Texture2D" uid="uid://c1gnh8jgctrcx" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_07.png" id="1_wi1w3"] + +[resource] +script = ExtResource("1_v6rd4") +id = &"magic_gold" +display_name = "Magic Gold" +icon = ExtResource("1_wi1w3") +metadata/_custom_type_script = "uid://dtgqjf3bl7pm8" diff --git a/docs/gyms/tiny_sword/currencies/mana_stone.tres b/docs/gyms/tiny_sword/currencies/mana_stone.tres new file mode 100644 index 0000000..1e256c0 --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/mana_stone.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://brctmnpmhjas6"] + +[ext_resource type="Texture2D" uid="uid://c1gnh8jgctrcx" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_07.png" id="1_c7yxi"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_fahg0"] + +[resource] +script = ExtResource("1_fahg0") +id = &"mana_stone" +display_name = "Mana Stone" +icon = ExtResource("1_c7yxi") +metadata/_custom_type_script = "uid://dtgqjf3bl7pm8" diff --git a/docs/gyms/tiny_sword/currencies/philosoper_stone.tres b/docs/gyms/tiny_sword/currencies/philosoper_stone.tres new file mode 100644 index 0000000..ea75161 --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/philosoper_stone.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://co4fiqgluwit0"] + +[ext_resource type="Texture2D" uid="uid://c1gnh8jgctrcx" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_07.png" id="1_cks7i"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="2_ih4no"] + +[resource] +script = ExtResource("2_ih4no") +id = &"philosoper_stone" +display_name = "Philosoper Stone" +icon = ExtResource("1_cks7i") +metadata/_custom_type_script = "uid://dtgqjf3bl7pm8" diff --git a/docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres b/docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres new file mode 100644 index 0000000..ff00aa2 --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres @@ -0,0 +1,18 @@ +[gd_resource type="Resource" script_class="CurrencyCatalogue" format=3 uid="uid://iqphilgfp2i7"] + +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_501l6"] +[ext_resource type="Script" uid="uid://621tus0uvbrd" path="res://core/currency/currency_catalogue.gd" id="2_hmju3"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_ucsji"] +[ext_resource type="Resource" uid="uid://dfxk30o34qe2s" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="3_7hx6u"] +[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="4_c77gh"] +[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="5_gyp05"] +[ext_resource type="Resource" uid="uid://bfrb0ayrljac2" path="res://docs/gyms/tiny_sword/currencies/ascension.tres" id="6_ascension"] +[ext_resource type="Resource" uid="uid://dpbndqxvsffa0" path="res://docs/gyms/tiny_sword/currencies/magic_gold.tres" id="7_vu05v"] +[ext_resource type="Resource" uid="uid://brctmnpmhjas6" path="res://docs/gyms/tiny_sword/currencies/mana_stone.tres" id="8_p3urk"] +[ext_resource type="Resource" uid="uid://t6du7gm2ywbi" path="res://docs/gyms/tiny_sword/currencies/cognite.tres" id="9_ejnoj"] +[ext_resource type="Resource" uid="uid://co4fiqgluwit0" path="res://docs/gyms/tiny_sword/currencies/philosoper_stone.tres" id="10_ejnoj"] + +[resource] +script = ExtResource("2_hmju3") +currencies = Array[ExtResource("1_501l6")]([ExtResource("2_ucsji"), ExtResource("3_7hx6u"), ExtResource("4_c77gh"), ExtResource("5_gyp05"), ExtResource("6_ascension"), ExtResource("7_vu05v"), ExtResource("8_p3urk"), ExtResource("9_ejnoj"), ExtResource("10_ejnoj")]) +metadata/_custom_type_script = "uid://621tus0uvbrd" diff --git a/docs/gyms/tiny_sword/currencies/wood.tres b/docs/gyms/tiny_sword/currencies/wood.tres new file mode 100644 index 0000000..729262d --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/wood.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://bgsk8h4w80h45"] + +[ext_resource type="Texture2D" uid="uid://bovjusa7o0l1m" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_02.png" id="1_gm6cf"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="2_jj4s4"] + +[resource] +script = ExtResource("2_jj4s4") +id = &"wood" +display_name = "Wood" +icon = ExtResource("1_gm6cf") diff --git a/docs/gyms/tiny_sword/currencies/worker.tres b/docs/gyms/tiny_sword/currencies/worker.tres new file mode 100644 index 0000000..18f416e --- /dev/null +++ b/docs/gyms/tiny_sword/currencies/worker.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://dfxk30o34qe2s"] + +[ext_resource type="Texture2D" uid="uid://bx7tj1rd3vo3a" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_05.png" id="1_cbwhh"] +[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_ovrsl"] + +[resource] +script = ExtResource("1_ovrsl") +id = &"worker" +display_name = "Worker" +icon = ExtResource("1_cbwhh") +metadata/_custom_type_script = "uid://dtgqjf3bl7pm8" diff --git a/docs/gyms/tiny_sword/currency_panel.tscn b/docs/gyms/tiny_sword/currency_panel.tscn new file mode 100644 index 0000000..7635dd7 --- /dev/null +++ b/docs/gyms/tiny_sword/currency_panel.tscn @@ -0,0 +1,15 @@ +[gd_scene format=3 uid="uid://cur_panel_scene_uid"] + +[ext_resource type="Script" uid="uid://9_vbox" path="res://core/currency/currency_panel.gd" id="1_cpnl"] + +[node name="CurrencyPanel" type="PanelContainer" unique_id=3000000001] +custom_minimum_size = Vector2(250, 340) +offset_left = 5.0 +offset_top = 5.0 +offset_right = 255.0 +offset_bottom = 345.0 +script = ExtResource("1_cpnl") + +[node name="CurrencyTiles" type="VBoxContainer" parent="." unique_id=3000000002] +layout_mode = 2 +theme_override_constants/separation = 4 diff --git a/docs/gyms/tiny_sword/goals/gold_350k_wood_20k_goal.tres b/docs/gyms/tiny_sword/goals/gold_350k_wood_20k_goal.tres new file mode 100644 index 0000000..ca2dbf7 --- /dev/null +++ b/docs/gyms/tiny_sword/goals/gold_350k_wood_20k_goal.tres @@ -0,0 +1,24 @@ +[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://b6gb6vk0kgxnw"] + +[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_68ddp"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_2b0j8"] +[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="3_7tb0t"] +[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="4_20y5g"] + +[sub_resource type="Resource" id="Resource_tvl3d"] +script = ExtResource("1_68ddp") +currency = ExtResource("2_2b0j8") +amount_mantissa = 3.5 +amount_exponent = 5 + +[sub_resource type="Resource" id="Resource_hyry2"] +script = ExtResource("1_68ddp") +currency = ExtResource("3_7tb0t") +amount_mantissa = 2.0 +amount_exponent = 4 +metadata/_custom_type_script = "uid://r4js5eajolio" + +[resource] +script = ExtResource("4_20y5g") +id = &"gold_350k_wood_20k" +requirements = Array[ExtResource("1_68ddp")]([SubResource("Resource_tvl3d"), SubResource("Resource_hyry2")]) diff --git a/docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres b/docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres new file mode 100644 index 0000000..f4ef655 --- /dev/null +++ b/docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://cts0407h130d6"] + +[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_pahmu"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_oek0g"] +[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_3fgtm"] + +[sub_resource type="Resource" id="Resource_tvl3d"] +script = ExtResource("1_pahmu") +currency = ExtResource("2_oek0g") +amount_mantissa = 1.3 +amount_exponent = 3 + +[resource] +script = ExtResource("3_3fgtm") +id = &"gold_total_1300" +requirements = Array[ExtResource("1_pahmu")]([SubResource("Resource_tvl3d")]) diff --git a/docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres b/docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres new file mode 100644 index 0000000..7fe32b8 --- /dev/null +++ b/docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://bo463s6jt0ep7"] + +[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_hj5d1"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_w5qg8"] +[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_daoxn"] + +[sub_resource type="Resource" id="Resource_tvl3d"] +script = ExtResource("1_hj5d1") +currency = ExtResource("2_w5qg8") +amount_mantissa = 1.3 +amount_exponent = 4 + +[resource] +script = ExtResource("3_daoxn") +id = &"gold_total_13k" +requirements = Array[ExtResource("1_hj5d1")]([SubResource("Resource_tvl3d")]) diff --git a/docs/gyms/tiny_sword/goals/gold_total_30_goal.tres b/docs/gyms/tiny_sword/goals/gold_total_30_goal.tres new file mode 100644 index 0000000..985d28c --- /dev/null +++ b/docs/gyms/tiny_sword/goals/gold_total_30_goal.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://ik7t0r3su633"] + +[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_udubw"] +[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_lde8k"] +[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_ilpcf"] + +[sub_resource type="Resource" id="Resource_v4v16"] +script = ExtResource("1_udubw") +currency = ExtResource("2_lde8k") +amount_mantissa = 30.0 + +[resource] +script = ExtResource("3_ilpcf") +id = &"gold_total_30" +requirements = Array[ExtResource("1_udubw")]([SubResource("Resource_v4v16")]) +unlock_behavior = 0 diff --git a/docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres b/docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres new file mode 100644 index 0000000..d5067d8 --- /dev/null +++ b/docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres @@ -0,0 +1,12 @@ +[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://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://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://b6gb6vk0kgxnw" path="res://docs/gyms/tiny_sword/goals/gold_350k_wood_20k_goal.tres" id="5_or88d"] + +[resource] +script = ExtResource("2_84pbn") +goals = Array[ExtResource("1_6p6ue")]([ExtResource("2_xvtf2"), ExtResource("3_wb626"), ExtResource("4_3oaoj"), ExtResource("5_or88d")]) diff --git a/docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres b/docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres new file mode 100644 index 0000000..33ecc25 --- /dev/null +++ b/docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="PrestigeBuffCatalogue" format=3] + +[ext_resource type="Script" path="res://core/prestige/prestige_buff_node.gd" id="1_node"] +[ext_resource type="Script" path="res://core/prestige/prestige_buff_catalogue.gd" id="2_cat"] +[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres" id="3_gold"] +[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres" id="4_farm"] + +[resource] +script = ExtResource("2_cat") +nodes = Array[ExtResource("1_node")]([ExtResource("3_gold"), ExtResource("4_farm")]) diff --git a/docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres b/docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres new file mode 100644 index 0000000..d9bd1e2 --- /dev/null +++ b/docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="PrestigeBuffNode" format=3 uid="uid://cvogxo0msis2w"] + +[ext_resource type="Script" uid="uid://mjyig8xlgki0" path="res://core/prestige/prestige_buff_node.gd" id="1_script"] + +[resource] +script = ExtResource("1_script") +id = &"farm_boost" +display_name = "Fertile Grounds" +description = "Permanently increases farm food production." +parent_ids = Array[StringName]([&"gold_boost"]) +effect_value = 2.0 +target_id = &"farm" +x_position = 1.0 diff --git a/docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres b/docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres new file mode 100644 index 0000000..9fab50b --- /dev/null +++ b/docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="PrestigeBuffNode" format=3 uid="uid://vl3ot4t876pc"] + +[ext_resource type="Script" uid="uid://mjyig8xlgki0" path="res://core/prestige/prestige_buff_node.gd" id="1_script"] + +[resource] +script = ExtResource("1_script") +id = &"gold_boost" +display_name = "Golden Touch" +description = "Permanently increases gold mine production." +effect_value = 2.0 +target_id = &"goldmine" diff --git a/docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn b/docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn new file mode 100644 index 0000000..f05637f --- /dev/null +++ b/docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn @@ -0,0 +1,69 @@ +[gd_scene format=3 uid="uid://dprbgokqitaav"] + +[ext_resource type="Script" uid="uid://n01pkakxllnj" path="res://core/prestige/prestige_buff_graph_panel.gd" id="1_panel"] +[ext_resource type="Script" uid="uid://uv5lxj6hmpk" path="res://core/prestige/prestige_buff_connection_overlay.gd" id="2_overlay"] +[ext_resource type="PackedScene" uid="uid://bqk8rwia7lpbg" path="res://core/prestige/prestige_buff_graph_tile.tscn" id="3_tile"] +[ext_resource type="Resource" uid="uid://vl3ot4t876pc" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres" id="4_gold"] +[ext_resource type="Resource" uid="uid://cvogxo0msis2w" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres" id="5_farm"] + +[node name="PrestigeBuffGraphPanel" type="PanelContainer" unique_id=947835716] +custom_minimum_size = Vector2(600, 400) +script = ExtResource("1_panel") + +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=829451203] +layout_mode = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 12 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=637192845] +layout_mode = 2 + +[node name="Header" type="HBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=372819465] +layout_mode = 2 + +[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer/Header" unique_id=518293746] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Prestige Buffs" + +[node name="CloseButton" type="Button" parent="MarginContainer/VBoxContainer/Header" unique_id=184756392] +layout_mode = 2 +size_flags_horizontal = 10 +theme_override_font_sizes/font_size = 16 +text = "✕" + +[node name="BalanceLabel" type="Label" parent="MarginContainer/VBoxContainer" unique_id=293847561] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +text = "Prestige Currency: 0" + +[node name="TilesArea" type="Control" parent="MarginContainer/VBoxContainer" unique_id=847291034] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 1 + +[node name="ConnectionOverlay" type="Control" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=563829104] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("2_overlay") + +[node name="GoldBoostTile" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=756192834 instance=ExtResource("3_tile")] +layout_mode = 0 +offset_right = 220.0 +offset_bottom = 100.0 +prestige_buff = ExtResource("4_gold") + +[node name="FarmBoostTile" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=619283475 instance=ExtResource("3_tile")] +layout_mode = 0 +offset_left = 3.0 +offset_top = 121.0 +offset_right = 223.0 +offset_bottom = 221.0 +prestige_buff = ExtResource("5_farm") diff --git a/docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres b/docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres new file mode 100644 index 0000000..7a07a44 --- /dev/null +++ b/docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="PrestigeBuffNode" format=3 uid="uid://bif136o485oqh"] + +[ext_resource type="Script" uid="uid://mjyig8xlgki0" path="res://core/prestige/prestige_buff_node.gd" id="1_42hk5"] + +[resource] +script = ExtResource("1_42hk5") +id = &"wood_boost" +display_name = "Wood Touch" +description = "Permanently increases forestry production." +parent_ids = Array[StringName]([&"farm_boost"]) +effect_value = 2.0 +target_id = &"forestry" diff --git a/docs/gyms/tiny_sword/prestige/primary_prestige.tres b/docs/gyms/tiny_sword/prestige/primary_prestige.tres new file mode 100644 index 0000000..60e81a8 --- /dev/null +++ b/docs/gyms/tiny_sword/prestige/primary_prestige.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="PrestigeConfig" format=3 uid="uid://dwmfvmusfskk6"] + +[ext_resource type="Script" uid="uid://w34t58rpkoui" path="res://core/prestige/prestige_config.gd" id="1_3tg3a"] + +[resource] +script = ExtResource("1_3tg3a") +id = &"ascension" +prestige_currency_id = &"ascension" +basis = 3 diff --git a/docs/gyms/tiny_sword/research/farm_research.tres b/docs/gyms/tiny_sword/research/farm_research.tres new file mode 100644 index 0000000..8ed9eea --- /dev/null +++ b/docs/gyms/tiny_sword/research/farm_research.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ResearchData" format=3 uid="uid://50yq2hl3wfwq"] + +[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="1_supp3"] +[ext_resource type="Script" uid="uid://m7baywrfnpn0" path="res://core/research/research_data.gd" id="2_rgx73"] + +[resource] +script = ExtResource("2_rgx73") +id = &"farm_research" +generator_id = &"farm" +name = "Farm Research" +description = "Research that improves farm production" +icon = ExtResource("1_supp3") +metadata/_custom_type_script = "uid://m7baywrfnpn0" diff --git a/docs/gyms/tiny_sword/research/forestry_research.tres b/docs/gyms/tiny_sword/research/forestry_research.tres new file mode 100644 index 0000000..d7c2741 --- /dev/null +++ b/docs/gyms/tiny_sword/research/forestry_research.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ResearchData" format=3 uid="uid://cvow4sj6a5h23"] + +[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="1_qvnpw"] +[ext_resource type="Script" uid="uid://m7baywrfnpn0" path="res://core/research/research_data.gd" id="2_6lndp"] + +[resource] +script = ExtResource("2_6lndp") +id = &"forestry_research" +generator_id = &"forestry" +name = "Forestry Research" +description = "Research that improves forestry production" +icon = ExtResource("1_qvnpw") +metadata/_custom_type_script = "uid://m7baywrfnpn0" diff --git a/docs/gyms/tiny_sword/research/gold_mine_research.tres b/docs/gyms/tiny_sword/research/gold_mine_research.tres new file mode 100644 index 0000000..31640e6 --- /dev/null +++ b/docs/gyms/tiny_sword/research/gold_mine_research.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ResearchData" format=3 uid="uid://dif6w6kesmw6u"] + +[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="1_2tci1"] +[ext_resource type="Script" uid="uid://m7baywrfnpn0" path="res://core/research/research_data.gd" id="1_yn72k"] + +[resource] +script = ExtResource("1_yn72k") +id = &"gold_mine_research" +generator_id = &"goldmine" +name = "Gold Mine Research" +description = "Research that improves gold mine production" +icon = ExtResource("1_2tci1") +metadata/_custom_type_script = "uid://m7baywrfnpn0" diff --git a/docs/gyms/tiny_sword/research/ts_research_catalogue.tres b/docs/gyms/tiny_sword/research/ts_research_catalogue.tres new file mode 100644 index 0000000..432e718 --- /dev/null +++ b/docs/gyms/tiny_sword/research/ts_research_catalogue.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="ResearchCatalogue" format=3 uid="uid://umi37hotcq8m"] + +[ext_resource type="Script" uid="uid://m7baywrfnpn0" path="res://core/research/research_data.gd" id="1_wscpr"] +[ext_resource type="Resource" uid="uid://dif6w6kesmw6u" path="res://docs/gyms/tiny_sword/research/gold_mine_research.tres" id="2_migxn"] +[ext_resource type="Script" uid="uid://d2v6t6w2todfy" path="res://core/research/research_catalogue.gd" id="2_yh8dx"] +[ext_resource type="Resource" uid="uid://50yq2hl3wfwq" path="res://docs/gyms/tiny_sword/research/farm_research.tres" id="3_pdfmu"] +[ext_resource type="Resource" uid="uid://cvow4sj6a5h23" path="res://docs/gyms/tiny_sword/research/forestry_research.tres" id="4_jnaqq"] + +[resource] +script = ExtResource("2_yh8dx") +research_entries = Array[ExtResource("1_wscpr")]([ExtResource("2_migxn"), ExtResource("3_pdfmu"), ExtResource("4_jnaqq")]) +metadata/_custom_type_script = "uid://d2v6t6w2todfy" diff --git a/docs/gyms/tiny_sword/tiny_sword.tscn b/docs/gyms/tiny_sword/tiny_sword.tscn new file mode 100644 index 0000000..91b15d9 --- /dev/null +++ b/docs/gyms/tiny_sword/tiny_sword.tscn @@ -0,0 +1,125 @@ +[gd_scene format=3 uid="uid://dadeowsvaywpp"] + +[ext_resource type="Script" uid="uid://cv2132o4hi7q3" path="res://core/level_game_state.gd" id="1_tma5j"] +[ext_resource type="PackedScene" uid="uid://brgkkw2n3o1ox" path="res://docs/gyms/tiny_sword/buildings/gold_mine/gold_mine.tscn" id="2_2j2oc"] +[ext_resource type="Resource" uid="uid://iqphilgfp2i7" path="res://docs/gyms/tiny_sword/currencies/ts_currency_catalogue.tres" id="2_dwrof"] +[ext_resource type="PackedScene" uid="uid://cl05bdri38mxf" path="res://docs/gyms/tiny_sword/buildings/castle/castle.tscn" id="3_5ru58"] +[ext_resource type="Resource" uid="uid://bgmj3lep0wmtf" path="res://docs/gyms/tiny_sword/buffs/ts_buff_catalogue.tres" id="3_wl838"] +[ext_resource type="Resource" uid="uid://cgt1mjir1v4br" path="res://docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres" id="4_x77df"] +[ext_resource type="Resource" uid="uid://umi37hotcq8m" path="res://docs/gyms/tiny_sword/research/ts_research_catalogue.tres" id="5_v0pty"] +[ext_resource type="Script" uid="uid://srkiu4qe8s2m" path="res://core/prestige/prestige_manager.gd" id="5_x77df"] +[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres" id="6_asc"] +[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="6_xnhlc"] +[ext_resource type="PackedScene" path="res://docs/gyms/tiny_sword/currency_panel.tscn" id="9_cpnl"] +[ext_resource type="PackedScene" uid="uid://djedqovgngrx5" path="res://docs/gyms/tiny_sword/buildings/farm/farm.tscn" id="10_1lv5i"] +[ext_resource type="PackedScene" uid="uid://dirvi76rkoowf" path="res://goals_debug_ui.tscn" id="10_qifrv"] +[ext_resource type="PackedScene" uid="uid://cf01wvy3f17i" path="res://docs/gyms/tiny_sword/buildings/forestry/forestry.tscn" id="11_pyqyw"] +[ext_resource type="PackedScene" uid="uid://bf8lqbexvnx6e" path="res://docs/gyms/tiny_sword/buildings/monastery/monastery.tscn" id="13_no27p"] +[ext_resource type="PackedScene" uid="uid://rejxvjwybkll" path="res://sandbox/tiny_swords/Terrain/Resources/Wood/Trees/tree_1.tscn" id="14_0cs5o"] +[ext_resource type="PackedScene" uid="uid://bp5ng4vu4ot4a" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.tscn" id="14_hum8s"] +[ext_resource type="Script" uid="uid://b6systw05frh0" path="res://core/edge_scroll_camera.gd" id="17_x77df"] +[ext_resource type="PackedScene" uid="uid://dprbgokqitaav" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn" id="18_pnl"] + +[node name="TinySwords" type="Node" unique_id=498237642] + +[node name="LevelGameState" type="Node" parent="." unique_id=1257082478] +script = ExtResource("1_tma5j") +currency_catalogue = ExtResource("2_dwrof") +buff_catalogue = ExtResource("3_wl838") +goal_catalogue = ExtResource("4_x77df") +research_catalogue = ExtResource("5_v0pty") +prestige_buff_catalogue = ExtResource("6_asc") +save_file_path = "user://tiny_sword_save.json" + +[node name="PrestigeManager" type="Node" parent="LevelGameState" unique_id=1019028144 node_paths=PackedStringArray("game_state")] +script = ExtResource("5_x77df") +config = ExtResource("6_xnhlc") +game_state = NodePath("..") + +[node name="CanvasLayer" type="CanvasLayer" parent="LevelGameState" unique_id=993804078] + +[node name="UI" type="Control" parent="LevelGameState/CanvasLayer" unique_id=1299828389] +layout_mode = 3 +anchors_preset = 0 + +[node name="CurrencyPanel" parent="LevelGameState/CanvasLayer/UI" unique_id=2000000001 instance=ExtResource("9_cpnl")] +layout_mode = 1 +offset_left = 0.0 +offset_top = 429.0 +offset_right = 250.0 +offset_bottom = 769.0 + +[node name="GoalsDebugUI" parent="LevelGameState/CanvasLayer/UI" unique_id=1494700185 instance=ExtResource("10_qifrv")] +visible = false +layout_mode = 1 +offset_left = 1433.0 +offset_top = 5.0 +offset_right = 1913.0 +offset_bottom = 263.0 + +[node name="PrestigeBuffToggle" type="Button" parent="LevelGameState/CanvasLayer/UI" unique_id=293847102] +layout_mode = 1 +offset_left = 1730.0 +offset_top = 64.0 +offset_right = 1857.0 +offset_bottom = 100.0 +toggle_mode = true +text = "Prestige Buffs" + +[node name="PrestigeBuffGraphPanel" parent="LevelGameState/CanvasLayer/UI" unique_id=847291035 instance=ExtResource("18_pnl")] +visible = false +layout_mode = 1 +offset_left = 400.0 +offset_top = 200.0 +offset_right = 1000.0 +offset_bottom = 700.0 + +[node name="World" type="Node2D" parent="LevelGameState" unique_id=2118297724] + +[node name="ForestProps" type="Node2D" parent="LevelGameState/World" unique_id=649424542] + +[node name="Tree1" parent="LevelGameState/World/ForestProps" unique_id=1917119095 instance=ExtResource("14_0cs5o")] +position = Vector2(1486, 543) + +[node name="Tree2" parent="LevelGameState/World/ForestProps" unique_id=1429193881 instance=ExtResource("14_0cs5o")] +position = Vector2(1510, 586) + +[node name="Tree3" parent="LevelGameState/World/ForestProps" unique_id=856028883 instance=ExtResource("14_0cs5o")] +position = Vector2(1600, 586) + +[node name="Tree4" parent="LevelGameState/World/ForestProps" unique_id=547138642 instance=ExtResource("14_0cs5o")] +position = Vector2(1482, 655) + +[node name="Tree5" parent="LevelGameState/World/ForestProps" unique_id=660535343 instance=ExtResource("14_0cs5o")] +position = Vector2(1568, 656) + +[node name="Tree6" parent="LevelGameState/World/ForestProps" unique_id=894701042 instance=ExtResource("14_0cs5o")] +position = Vector2(1665, 650) + +[node name="GoldMine" parent="LevelGameState/World" unique_id=341660167 instance=ExtResource("2_2j2oc")] +position = Vector2(274, 429) + +[node name="Castle" parent="LevelGameState/World" unique_id=1966557957 instance=ExtResource("3_5ru58")] +position = Vector2(853, 357) + +[node name="Farm" parent="LevelGameState/World" unique_id=283335851 instance=ExtResource("10_1lv5i")] +position = Vector2(380, 684) + +[node name="Forestry" parent="LevelGameState/World" unique_id=726284577 instance=ExtResource("11_pyqyw")] +position = Vector2(825, 915) + +[node name="Monastery" parent="LevelGameState/World" unique_id=1545930496 instance=ExtResource("13_no27p")] +position = Vector2(105, 920) + +[node name="AlchemyTower" parent="LevelGameState/World" unique_id=51852160 instance=ExtResource("14_hum8s")] +position = Vector2(1239, 775) + +[node name="Camera2D" type="Camera2D" parent="." unique_id=1494908331] +position = Vector2(5, 0) +offset = Vector2(960, 540) +script = ExtResource("17_x77df") +clamp_enabled = true +clamp_min = Vector2(-1920, -1080) +clamp_max = Vector2(1920, 1080) + +[connection signal="pressed" from="LevelGameState/CanvasLayer/UI/PrestigeBuffToggle" to="LevelGameState/CanvasLayer/UI/PrestigeBuffGraphPanel" method="_on_prestige_buff_toggle_pressed"] diff --git a/docs/museums/big_number_museum.tscn b/docs/museums/big_number_museum.tscn index f5ce5fe..d8d394b 100644 --- a/docs/museums/big_number_museum.tscn +++ b/docs/museums/big_number_museum.tscn @@ -2,7 +2,7 @@ [ext_resource type="Script" uid="uid://coasop1lyw5rh" path="res://docs/museums/big_number_museum.gd" id="1_muwei"] [ext_resource type="PackedScene" uid="uid://btkxru2gdjsgc" path="res://currency_tile.tscn" id="2_a7k0t"] -[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://generator_container.tscn" id="2_h2bxk"] +[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://core/generator/generator_container.tscn" id="2_h2bxk"] [node name="BigNumberMuseum" type="Control" unique_id=1249411984] layout_mode = 3 diff --git a/docs/museums/edge_scroll_camera.tscn b/docs/museums/edge_scroll_camera.tscn new file mode 100644 index 0000000..108e56f --- /dev/null +++ b/docs/museums/edge_scroll_camera.tscn @@ -0,0 +1,42 @@ +[gd_scene format=3 uid="uid://5vli1vqiqdrl"] + +[ext_resource type="Script" uid="uid://b6systw05frh0" path="res://core/edge_scroll_camera.gd" id="1_08uhj"] +[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="2_c5cli"] + +[node name="EdgeScrollCamera" type="Node2D" unique_id=1977543223] + +[node name="Camera2D" type="Camera2D" parent="." unique_id=455629291] +offset = Vector2(960, 540) +script = ExtResource("1_08uhj") +clamp_enabled = true +clamp_max = Vector2(3000, 2000) + +[node name="Icon" type="Sprite2D" parent="." unique_id=629901180] +position = Vector2(988, 485) +texture = ExtResource("2_c5cli") + +[node name="Icon2" type="Sprite2D" parent="." unique_id=919808260] +position = Vector2(1714, 130) +texture = ExtResource("2_c5cli") + +[node name="Icon3" type="Sprite2D" parent="." unique_id=368045162] +position = Vector2(1750, 957) +texture = ExtResource("2_c5cli") + +[node name="Icon6" type="Sprite2D" parent="." unique_id=1755994416] +position = Vector2(2641, 1655) +rotation = 0.9242716 +texture = ExtResource("2_c5cli") + +[node name="Icon7" type="Sprite2D" parent="." unique_id=460486605] +position = Vector2(895.00024, 1641) +rotation = 0.9242716 +texture = ExtResource("2_c5cli") + +[node name="Icon4" type="Sprite2D" parent="." unique_id=957351194] +position = Vector2(134, 962) +texture = ExtResource("2_c5cli") + +[node name="Icon5" type="Sprite2D" parent="." unique_id=2077463270] +position = Vector2(137, 129) +texture = ExtResource("2_c5cli") diff --git a/goals_debug_ui.gd b/goals_debug_ui.gd index 6cfa94e..e6443d1 100644 --- a/goals_debug_ui.gd +++ b/goals_debug_ui.gd @@ -1,11 +1,9 @@ extends PanelContainer class GoalDefinition extends RefCounted: - var target_generator_id: StringName var goal: GoalData - func _init(target_id: StringName, goal_data: GoalData) -> void: - target_generator_id = target_id + func _init(goal_data: GoalData) -> void: goal = goal_data const GOAL_DATA_SCRIPT: Script = preload("res://core/goals/goal_data.gd") @@ -23,69 +21,59 @@ class GoalRow extends RefCounted: requirements_label = requirements button = unlock_button -@onready var _summary_label: Label = $MarginContainer/VBoxContainer/SummaryLabel -@onready var _goals_list: VBoxContainer = $MarginContainer/VBoxContainer/ScrollContainer/GoalsList +@onready var _summary_label: Label = $MarginContainer/ScrollContainer/VBoxContainer/SummaryLabel +@onready var _goals_list: VBoxContainer = $MarginContainer/ScrollContainer/VBoxContainer/ScrollContainer/GoalsList +@onready var game_state: LevelGameState = find_parent("LevelGameState") var _loaded_goals: Array[GoalDefinition] = [] var _rows_by_goal_id: Dictionary = {} -var _known_generator_ids: Dictionary = {} -var _warned_unknown_target_ids: Dictionary = {} -var _generators_by_id: Dictionary = {} func _ready() -> void: - _collect_known_generator_ids() - _load_goals() - _build_goal_rows() - GameState.currency_changed.connect(_on_currency_changed) - GameState.generator_state_changed.connect(_on_generator_state_changed) + if game_state: + game_state.currency_changed.connect(_on_currency_changed) + game_state.generator_state_changed.connect(_on_generator_state_changed) + game_state.goal_completed.connect(_on_goal_completed) + + if not game_state.get_all_goals().is_empty(): + _load_goals() + _build_goal_rows() + else: + game_state.ready.connect(_on_level_state_ready) + _refresh_ui() _refresh_ui.call_deferred() func _on_currency_changed(_currency_id: StringName, _new_amount: BigNumber) -> void: _refresh_ui() -func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) -> void: - var generator_key: String = String(generator_id) - if not generator_key.is_empty(): - _known_generator_ids[generator_key] = true - _refresh_generator_lookup(generator_id) +func _on_goal_completed(goal_id: StringName) -> void: + _refresh_ui() + +func _on_generator_state_changed(_generator_id: StringName, _state: Dictionary) -> void: + _refresh_ui() + +func _on_level_state_ready() -> void: + _load_goals() + _build_goal_rows() _refresh_ui() func _load_goals() -> void: _loaded_goals.clear() - _generators_by_id.clear() - var scene_root: Node = get_tree().current_scene - var search_root: Node = scene_root if scene_root != null else self - var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false) + var all_goals_raw: Array = game_state.get_all_goals() if game_state else [] var seen_goal_ids: Dictionary = {} - for generator_node in generator_nodes: - var generator: CurrencyGenerator = generator_node as CurrencyGenerator - if generator == null: + + for goal_raw in all_goals_raw: + var goal: GoalData = goal_raw as GoalData + if goal == null or not goal.has_id(): continue - - var generator_id: StringName = generator.get_generator_id() - if generator_id == &"": - continue - _generators_by_id[String(generator_id)] = generator - - if generator.data == null: - continue - if not generator.data.has_unlock_goal(): - continue - - var resolved_goal: GoalData = generator.data.unlock_goal - if resolved_goal == null: - continue - if not bool(resolved_goal.is_valid()): - continue - var goal_key: String = String(resolved_goal.get("id")) + + var goal_key: String = String(goal.id) if seen_goal_ids.has(goal_key): - push_warning("Goals debug UI: duplicate goal id '%s' skipped" % goal_key) continue - + seen_goal_ids[goal_key] = true - _loaded_goals.append(GoalDefinition.new(generator_id, resolved_goal)) + _loaded_goals.append(GoalDefinition.new(goal)) func _build_goal_rows() -> void: _rows_by_goal_id.clear() @@ -102,7 +90,7 @@ func _build_goal_rows() -> void: var title_label: Label = Label.new() title_label.custom_minimum_size = Vector2(220.0, 0.0) - title_label.text = "%s -> %s" % [String(goal.goal.id), String(goal.target_generator_id)] + title_label.text = String(goal.goal.id) var status_label: Label = Label.new() status_label.custom_minimum_size = Vector2(95.0, 0.0) @@ -116,10 +104,10 @@ func _build_goal_rows() -> void: unlock_button.disabled = true unlock_button.pressed.connect(_on_unlock_pressed.bind(goal.goal.id)) + row_container.add_child(unlock_button) row_container.add_child(title_label) row_container.add_child(status_label) row_container.add_child(requirements_label) - row_container.add_child(unlock_button) _goals_list.add_child(row_container) _rows_by_goal_id[String(goal.goal.id)] = GoalRow.new(goal, status_label, requirements_label, unlock_button) @@ -150,29 +138,20 @@ func _refresh_ui() -> void: func _refresh_goal_row(row: GoalRow) -> void: var goal: GoalDefinition = row.goal var already_unlocked: bool = _is_goal_completed(goal) - var can_resolve_target: bool = _can_resolve_target(goal.target_generator_id) var met: bool = _is_goal_met(goal) - var generator: CurrencyGenerator = _get_generator_for_goal(goal) - var is_manual_goal: bool = _is_manual_goal_generator(generator) - var can_unlock: bool = can_resolve_target and met and not already_unlocked and is_manual_goal + var can_click: bool = met and not already_unlocked row.requirements_label.text = _get_requirements_text(goal) - row.button.disabled = not can_unlock + row.button.disabled = not can_click if already_unlocked: row.button.text = "Done" - elif is_manual_goal: - row.button.text = "Unlock" else: - row.button.text = "Auto" + row.button.text = "Unlock" if already_unlocked: row.status_label.text = "Unlocked" return - if not can_resolve_target: - row.status_label.text = "Missing" - return - if met: row.status_label.text = "Ready" return @@ -184,14 +163,17 @@ func _is_goal_met(goal: GoalDefinition) -> bool: return false if goal.goal == null: return false + if game_state == null: + return false - return bool(goal.goal.is_met()) + return bool(game_state.is_goal_met(goal.goal)) func _is_goal_completed(goal: GoalDefinition) -> bool: - return ( - GameState.is_generator_unlocked(goal.target_generator_id) - and GameState.is_generator_available(goal.target_generator_id) - ) + if goal.goal == null or goal.goal.id.is_empty(): + return false + if game_state == null: + return false + return game_state.is_goal_completed(goal.goal.id) func _on_unlock_pressed(goal_id: StringName) -> void: var row: GoalRow = _rows_by_goal_id.get(String(goal_id)) @@ -201,91 +183,18 @@ func _on_unlock_pressed(goal_id: StringName) -> void: var goal: GoalDefinition = row.goal if _is_goal_completed(goal): return - if not _can_resolve_target(goal.target_generator_id): - return if not _is_goal_met(goal): return - var generator: CurrencyGenerator = _get_generator_for_goal(goal) - if generator == null: - return - - if not generator.try_unlock_from_goal(): + if game_state == null: return - print("[GoalsDebugUI] Unlocked goal '%s' -> %s" % [String(goal.goal.id), String(goal.target_generator_id)]) + if goal.goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL: + game_state._complete_goal_manually(goal.goal.id) + + print("[GoalsDebugUI] Unlocked goal '%s'" % String(goal.goal.id)) _refresh_ui() -func _can_resolve_target(target_generator_id: StringName) -> bool: - var target_key: String = String(target_generator_id) - if target_key.is_empty(): - return false - - if _known_generator_ids.has(target_key) or GameState.generator_states.has(target_key): - return true - - if not _warned_unknown_target_ids.has(target_key): - _warned_unknown_target_ids[target_key] = true - push_warning("Goals debug UI target generator not found in scene/state: '%s'" % target_key) - return false - -func _collect_known_generator_ids() -> void: - _known_generator_ids.clear() - _generators_by_id.clear() - - var scene_root: Node = get_tree().current_scene - var search_root: Node = scene_root if scene_root != null else self - var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false) - for generator_node in generator_nodes: - var generator: CurrencyGenerator = generator_node as CurrencyGenerator - if generator == null: - continue - - var generator_key: String = String(generator.get_generator_id()) - if generator_key.is_empty(): - continue - - _known_generator_ids[generator_key] = true - _generators_by_id[generator_key] = generator - -func _refresh_generator_lookup(generator_id: StringName) -> void: - var generator_key: String = String(generator_id) - if generator_key.is_empty(): - return - - var scene_root: Node = get_tree().current_scene - var search_root: Node = scene_root if scene_root != null else self - var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false) - for generator_node in generator_nodes: - var generator: CurrencyGenerator = generator_node as CurrencyGenerator - if generator == null: - continue - if generator.get_generator_id() != generator_id: - continue - - _generators_by_id[generator_key] = generator - return - -func _get_generator_for_goal(goal: GoalDefinition) -> CurrencyGenerator: - if goal == null: - return null - - var key: String = String(goal.target_generator_id) - if key.is_empty(): - return null - - return _generators_by_id.get(key) as CurrencyGenerator - -func _is_manual_goal_generator(generator: CurrencyGenerator) -> bool: - if generator == null: - return false - if generator.data == null: - return false - if not generator.data.has_unlock_goal(): - return false - - return not generator.data.unlocks_automatically_from_goal() - func _get_requirements_text(goal: GoalDefinition) -> String: if goal == null: return "" @@ -293,16 +202,22 @@ func _get_requirements_text(goal: GoalDefinition) -> String: return "" if goal.goal.get_script() != GOAL_DATA_SCRIPT: return "" + if game_state == null: + return "" var parts: Array[String] = [] - for requirement_resource in goal.goal.get_valid_requirements(): + for requirement_resource in goal.goal.get_requirements(): if requirement_resource == null: continue if requirement_resource.get_script() != GOAL_REQUIREMENT_SCRIPT: continue - var requirement_currency_id: StringName = requirement_resource.get_currency_id() - var total_amount: BigNumber = GameState.get_total_currency_acquired_by_id(requirement_currency_id) + var currency: Currency = requirement_resource.get_currency() + if currency == null: + continue + + var requirement_currency_id: StringName = game_state.get_currency_id(currency) + var total_amount: BigNumber = game_state.get_total_currency_acquired_by_id(requirement_currency_id) var required_amount: BigNumber = requirement_resource.get_amount() parts.append( "%s %s / %s" % [ @@ -311,7 +226,19 @@ func _get_requirements_text(goal: GoalDefinition) -> String: required_amount.to_string_suffix(2) ] ) + + if parts.is_empty(): + return "No requirements" return " | ".join(parts) func _currency_label(currency_id: StringName) -> String: - return GameState.get_currency_name(currency_id) + if game_state == null: + return "Unknown" + return game_state.get_currency_name(currency_id) + +func _exit_tree() -> void: + if game_state: + game_state.currency_changed.disconnect(_on_currency_changed) + game_state.generator_state_changed.disconnect(_on_generator_state_changed) + game_state.goal_completed.disconnect(_on_goal_completed) + game_state.ready.disconnect(_on_level_state_ready) diff --git a/goals_debug_ui.tscn b/goals_debug_ui.tscn index 23306f7..667f40e 100644 --- a/goals_debug_ui.tscn +++ b/goals_debug_ui.tscn @@ -14,26 +14,29 @@ theme_override_constants/margin_top = 8 theme_override_constants/margin_right = 8 theme_override_constants/margin_bottom = 8 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1555356181] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer" unique_id=2143335122] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/ScrollContainer" unique_id=1555356181] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_override_constants/separation = 6 -[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer" unique_id=811777252] +[node name="TitleLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer" unique_id=811777252] layout_mode = 2 text = "Goals Debug" -[node name="SummaryLabel" type="Label" parent="MarginContainer/VBoxContainer" unique_id=17196021] +[node name="SummaryLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer" unique_id=17196021] layout_mode = 2 text = "Goals: 0 | Ready: 0 | Unlocked: 0" -[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer" unique_id=1105060075] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/ScrollContainer/VBoxContainer" unique_id=1105060075] custom_minimum_size = Vector2(0, 170) layout_mode = 2 size_flags_vertical = 3 -[node name="GoalsList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer" unique_id=1684633480] +[node name="GoalsList" type="VBoxContainer" parent="MarginContainer/ScrollContainer/VBoxContainer/ScrollContainer" unique_id=1684633480] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 diff --git a/idles/buffs/bigger_forest.tres b/idles/buffs/bigger_forest.tres deleted file mode 100644 index 5b4050e..0000000 --- a/idles/buffs/bigger_forest.tres +++ /dev/null @@ -1,18 +0,0 @@ -[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://mvgfe3nc7uwa"] - -[ext_resource type="Resource" uid="uid://bnhqk8b31mm4e" path="res://idles/currencies/knowledge.tres" id="1_1db6v"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_pg1j1"] -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_t80he"] -[ext_resource type="Resource" uid="uid://bmlhoeasl7xor" path="res://idles/goals/magic_total_13k.tres" id="4_83vra"] - -[resource] -script = ExtResource("3_t80he") -id = &"bigger_forest" -text = "Bigger Forest" -max_level = 10 -unlock_goal = ExtResource("4_83vra") -effect_increment = 1.0 -cost_currency = ExtResource("1_1db6v") -base_cost_mantissa = 250.0 -cost_multiplier = 1.7 -resource_target_currency = ExtResource("2_pg1j1") diff --git a/idles/buffs/library_auto_flux.tres b/idles/buffs/library_auto_flux.tres deleted file mode 100644 index 7608580..0000000 --- a/idles/buffs/library_auto_flux.tres +++ /dev/null @@ -1,18 +0,0 @@ -[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://tu63jy51yigb"] - -[ext_resource type="Resource" uid="uid://bnhqk8b31mm4e" path="res://idles/currencies/knowledge.tres" id="1_78qkq"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_b57xf"] -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_nc8nr"] -[ext_resource type="Resource" uid="uid://bmlhoeasl7xor" path="res://idles/goals/magic_total_13k.tres" id="4_78qkq"] - -[resource] -script = ExtResource("3_nc8nr") -id = &"library_auto_flux" -text = "Library Dynamo" -max_level = 10 -unlock_goal = ExtResource("4_78qkq") -effect_increment = 1.0 -cost_currency = ExtResource("1_78qkq") -base_cost_mantissa = 100.0 -cost_multiplier = 1.7 -resource_target_currency = ExtResource("2_b57xf") diff --git a/idles/buffs/orb_auto_flux.tres b/idles/buffs/orb_auto_flux.tres deleted file mode 100644 index 858388e..0000000 --- a/idles/buffs/orb_auto_flux.tres +++ /dev/null @@ -1,18 +0,0 @@ -[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://ceugcxmassmpk"] - -[ext_resource type="Resource" uid="uid://bnhqk8b31mm4e" path="res://idles/currencies/knowledge.tres" id="1_lnp8f"] -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_r7ak1"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_h3we5"] -[ext_resource type="Resource" uid="uid://qyxct5gbrxwa" path="res://idles/goals/magic_total_30.tres" id="4_000a0"] - -[resource] -script = ExtResource("1_r7ak1") -id = &"magic_auto_flux" -text = "Arcane Dynamo" -max_level = 10 -unlock_goal = ExtResource("4_000a0") -effect_increment = 1.0 -cost_currency = ExtResource("1_lnp8f") -base_cost_mantissa = 25.0 -cost_multiplier = 1.7 -resource_target_currency = ExtResource("2_h3we5") diff --git a/idles/buffs/orb_click_focus.tres b/idles/buffs/orb_click_focus.tres deleted file mode 100644 index f7b11e6..0000000 --- a/idles/buffs/orb_click_focus.tres +++ /dev/null @@ -1,18 +0,0 @@ -[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://6i3fcygusuqf"] - -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_1wyaq"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_0j56j"] -[ext_resource type="Resource" uid="uid://qyxct5gbrxwa" path="res://idles/goals/magic_total_30.tres" id="3_bsqao"] - -[resource] -script = ExtResource("1_1wyaq") -id = &"magic_click_focus" -kind = 1 -text = "Apprentice Gloves" -max_level = 4 -unlock_goal = ExtResource("3_bsqao") -effect_increment = 1.0 -cost_currency = ExtResource("2_0j56j") -base_cost_mantissa = 8.0 -cost_multiplier = 1.55 -resource_target_currency = ExtResource("2_0j56j") diff --git a/idles/buffs/orb_summon_spirit.tres b/idles/buffs/orb_summon_spirit.tres deleted file mode 100644 index e160870..0000000 --- a/idles/buffs/orb_summon_spirit.tres +++ /dev/null @@ -1,20 +0,0 @@ -[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://coi7k1cx4p4hr"] - -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="1_aawxd"] -[ext_resource type="Resource" uid="uid://l0pn6mlcer7t" path="res://idles/currencies/spirit.tres" id="2_0eqxt"] -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="3_0ov1n"] -[ext_resource type="Resource" uid="uid://qyxct5gbrxwa" path="res://idles/goals/magic_total_30.tres" id="4_ifsqd"] - -[resource] -script = ExtResource("3_0ov1n") -id = &"summon_spirit" -kind = 2 -text = "Summon Spirit" -unlock_goal = ExtResource("4_ifsqd") -effect_increment = 1.0 -cost_currency = ExtResource("1_aawxd") -base_cost_mantissa = 200.0 -cost_multiplier = 1.15 -resource_target_currency = ExtResource("2_0eqxt") -resource_purchase_base_mantissa = 1.0 -resource_purchase_increment_multiplier = 1.0 diff --git a/idles/currencies/knowledge.tres b/idles/currencies/knowledge.tres deleted file mode 100644 index 0164e38..0000000 --- a/idles/currencies/knowledge.tres +++ /dev/null @@ -1,10 +0,0 @@ -[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://bnhqk8b31mm4e"] - -[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_72iuq"] -[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="2_x2h5x"] - -[resource] -script = ExtResource("1_72iuq") -id = &"knowledge" -display_name = "Knowledge" -icon = ExtResource("2_x2h5x") diff --git a/idles/currencies/magic.tres b/idles/currencies/magic.tres deleted file mode 100644 index 5b7d307..0000000 --- a/idles/currencies/magic.tres +++ /dev/null @@ -1,10 +0,0 @@ -[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://brqaojindcxa5"] - -[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_x4uiu"] -[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="2_52ar0"] - -[resource] -script = ExtResource("1_x4uiu") -id = &"magic" -display_name = "Magic" -icon = ExtResource("2_52ar0") diff --git a/idles/currencies/spirit.tres b/idles/currencies/spirit.tres deleted file mode 100644 index 6497a2b..0000000 --- a/idles/currencies/spirit.tres +++ /dev/null @@ -1,9 +0,0 @@ -[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://l0pn6mlcer7t"] - -[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="1_m14p5"] - -[resource] -script = ExtResource("1_m14p5") -id = &"spirit" -display_name = "Spirit" -metadata/_custom_type_script = "uid://dtgqjf3bl7pm8" diff --git a/idles/currencies/wood.tres b/idles/currencies/wood.tres deleted file mode 100644 index 41b6ffa..0000000 --- a/idles/currencies/wood.tres +++ /dev/null @@ -1,10 +0,0 @@ -[gd_resource type="Resource" script_class="Currency" format=3 uid="uid://cythfovqgqlyh"] - -[ext_resource type="Texture2D" uid="uid://bgtt3wu43tajh" path="res://icon.svg" id="1_t8m5x"] -[ext_resource type="Script" uid="uid://dtgqjf3bl7pm8" path="res://core/currency/currency.gd" id="2_137dc"] - -[resource] -script = ExtResource("2_137dc") -id = &"wood" -display_name = "Wood" -icon = ExtResource("1_t8m5x") diff --git a/idles/generators/forestry.tres b/idles/generators/forestry.tres deleted file mode 100644 index d51df71..0000000 --- a/idles/generators/forestry.tres +++ /dev/null @@ -1,22 +0,0 @@ -[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://df5k58yu1g6rf"] - -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_ffn26"] -[ext_resource type="Resource" uid="uid://mvgfe3nc7uwa" path="res://idles/buffs/bigger_forest.tres" id="2_rptf4"] -[ext_resource type="Resource" uid="uid://l0pn6mlcer7t" path="res://idles/currencies/spirit.tres" id="3_rptf4"] -[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="5_43n1y"] -[ext_resource type="Resource" uid="uid://bmlhoeasl7xor" path="res://idles/goals/magic_total_13k.tres" id="6_ffn26"] - -[resource] -script = ExtResource("5_43n1y") -id = &"Wood" -name = "Forestry" -starts_unlocked = false -starts_available = false -initial_owned = 1 -purchase_currency = ExtResource("3_rptf4") -unlock_goal = ExtResource("6_ffn26") -unlock_goal_behavior = 1 -initial_cost = 1.0 -initial_productivity = 20.0 -buffs = Array[ExtResource("1_ffn26")]([ExtResource("2_rptf4")]) -metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/idles/generators/library.tres b/idles/generators/library.tres deleted file mode 100644 index 0e5100a..0000000 --- a/idles/generators/library.tres +++ /dev/null @@ -1,25 +0,0 @@ -[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://04pmc034qupd"] - -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_jemvk"] -[ext_resource type="Resource" uid="uid://tu63jy51yigb" path="res://idles/buffs/library_auto_flux.tres" id="2_fcbji"] -[ext_resource type="Resource" uid="uid://l0pn6mlcer7t" path="res://idles/currencies/spirit.tres" id="3_fcbji"] -[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="3_xj12v"] -[ext_resource type="Resource" uid="uid://c4mkxj4ubhsi0" path="res://idles/goals/magic_total_1300.tres" id="4_1h03m"] - -[resource] -script = ExtResource("3_xj12v") -id = &"Knowledge" -name = "Library" -starts_unlocked = false -starts_available = false -initial_owned = 1 -purchase_currency = ExtResource("3_fcbji") -unlock_goal = ExtResource("4_1h03m") -unlock_goal_behavior = 1 -initial_cost = 1.0 -coefficient = 1.0 -initial_time = 3.0 -initial_revenue = 60.0 -initial_productivity = 20.0 -buffs = Array[ExtResource("1_jemvk")]([ExtResource("2_fcbji")]) -metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/idles/generators/orb.tres b/idles/generators/orb.tres deleted file mode 100644 index 0420434..0000000 --- a/idles/generators/orb.tres +++ /dev/null @@ -1,19 +0,0 @@ -[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://co0mcc2kvcpo5"] - -[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="1_c6y77"] -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_t6lg0"] -[ext_resource type="Resource" uid="uid://ceugcxmassmpk" path="res://idles/buffs/orb_auto_flux.tres" id="2_x505b"] -[ext_resource type="Resource" uid="uid://6i3fcygusuqf" path="res://idles/buffs/orb_click_focus.tres" id="3_fsxdm"] -[ext_resource type="Resource" uid="uid://coi7k1cx4p4hr" path="res://idles/buffs/orb_summon_spirit.tres" id="4_5v0af"] -[ext_resource type="Resource" uid="uid://l0pn6mlcer7t" path="res://idles/currencies/spirit.tres" id="4_jpwus"] - -[resource] -script = ExtResource("1_c6y77") -id = &"Magic" -name = "Magic Orb" -purchase_currency = ExtResource("4_jpwus") -initial_cost = 1.0 -coefficient = 1.0 -initial_productivity = 20.0 -buffs = Array[ExtResource("1_t6lg0")]([ExtResource("2_x505b"), ExtResource("3_fsxdm"), ExtResource("4_5v0af")]) -metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/idles/generators/spirit_factory.tres b/idles/generators/spirit_factory.tres deleted file mode 100644 index 61ab4b3..0000000 --- a/idles/generators/spirit_factory.tres +++ /dev/null @@ -1,18 +0,0 @@ -[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3 uid="uid://ri1ggb756753"] - -[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_tgm58"] -[ext_resource type="Script" uid="uid://b00tqsuhxdy0d" path="res://core/generator/currency_generator_data.gd" id="2_1qpvc"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_chq14"] -[ext_resource type="Resource" uid="uid://qyxct5gbrxwa" path="res://idles/goals/magic_total_30.tres" id="4_tj1lt"] - -[resource] -script = ExtResource("2_1qpvc") -id = &"spirit_factory" -name = "Spirit Factory" -starts_unlocked = false -starts_available = false -purchase_currency = ExtResource("2_chq14") -unlock_goal = ExtResource("4_tj1lt") -unlock_goal_behavior = 1 -initial_cost = 200.0 -metadata/_custom_type_script = "uid://b00tqsuhxdy0d" diff --git a/idles/goals/magic_350k_wood_20k.tres b/idles/goals/magic_350k_wood_20k.tres deleted file mode 100644 index 227e33a..0000000 --- a/idles/goals/magic_350k_wood_20k.tres +++ /dev/null @@ -1,24 +0,0 @@ -[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://oh1a4tneuons"] - -[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_pj6se"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_lsxf0"] -[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_hyry2"] -[ext_resource type="Resource" uid="uid://cythfovqgqlyh" path="res://idles/currencies/wood.tres" id="3_lsxf0"] - -[sub_resource type="Resource" id="Resource_tvl3d"] -script = ExtResource("1_pj6se") -currency = ExtResource("2_lsxf0") -amount_mantissa = 3.5 -amount_exponent = 5 - -[sub_resource type="Resource" id="Resource_hyry2"] -script = ExtResource("1_pj6se") -currency = ExtResource("3_lsxf0") -amount_mantissa = 2.0 -amount_exponent = 4 -metadata/_custom_type_script = "uid://r4js5eajolio" - -[resource] -script = ExtResource("3_hyry2") -id = &"magic_350k_wood_20k" -requirements = Array[ExtResource("1_pj6se")]([SubResource("Resource_tvl3d"), SubResource("Resource_hyry2")]) diff --git a/idles/goals/magic_total_1300.tres b/idles/goals/magic_total_1300.tres deleted file mode 100644 index 49e8568..0000000 --- a/idles/goals/magic_total_1300.tres +++ /dev/null @@ -1,16 +0,0 @@ -[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://c4mkxj4ubhsi0"] - -[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="1_b11ou"] -[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="2_d507t"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="3_tu3fx"] - -[sub_resource type="Resource" id="Resource_tvl3d"] -script = ExtResource("2_d507t") -currency = ExtResource("3_tu3fx") -amount_mantissa = 1.3 -amount_exponent = 3 - -[resource] -script = ExtResource("1_b11ou") -id = &"magic_total_1300" -requirements = Array[ExtResource("2_d507t")]([SubResource("Resource_tvl3d")]) diff --git a/idles/goals/magic_total_13k.tres b/idles/goals/magic_total_13k.tres deleted file mode 100644 index c2eb65e..0000000 --- a/idles/goals/magic_total_13k.tres +++ /dev/null @@ -1,16 +0,0 @@ -[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://bmlhoeasl7xor"] - -[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_185pw"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="2_ox6xg"] -[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="3_ef7ki"] - -[sub_resource type="Resource" id="Resource_tvl3d"] -script = ExtResource("1_185pw") -currency = ExtResource("2_ox6xg") -amount_mantissa = 1.3 -amount_exponent = 4 - -[resource] -script = ExtResource("3_ef7ki") -id = &"magic_total_13k" -requirements = Array[ExtResource("1_185pw")]([SubResource("Resource_tvl3d")]) diff --git a/idles/goals/magic_total_30.tres b/idles/goals/magic_total_30.tres deleted file mode 100644 index bf525b4..0000000 --- a/idles/goals/magic_total_30.tres +++ /dev/null @@ -1,15 +0,0 @@ -[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://qyxct5gbrxwa"] - -[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="1_rh4uj"] -[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="2_58n5q"] -[ext_resource type="Resource" uid="uid://brqaojindcxa5" path="res://idles/currencies/magic.tres" id="3_qm5e7"] - -[sub_resource type="Resource" id="Resource_v4v16"] -script = ExtResource("2_58n5q") -currency = ExtResource("3_qm5e7") -amount_mantissa = 30.0 - -[resource] -script = ExtResource("1_rh4uj") -id = &"magic_total_30" -requirements = Array[ExtResource("2_58n5q")]([SubResource("Resource_v4v16")]) diff --git a/idles/prestige/primary_prestige.tres b/idles/prestige/primary_prestige.tres deleted file mode 100644 index 19a8794..0000000 --- a/idles/prestige/primary_prestige.tres +++ /dev/null @@ -1,25 +0,0 @@ -[gd_resource type="Resource" script_class="PrestigeConfig" format=3] - -[ext_resource type="Script" path="res://core/prestige/prestige_config.gd" id="1_3tg3a"] - -[resource] -script = ExtResource("1_3tg3a") -id = &"ascension" -display_name = "Ascension" -prestige_currency_id = &"ascension" -source_currency_id = &"magic" -basis = 0 -formula = 0 -threshold_mantissa = 1.0 -threshold_exponent = 4 -scale = 1.0 -exponent = 0.5 -flat_bonus = 0.0 -minimum_gain = 0 -rounding_mode = 0 -allow_prestige_without_gain = false -multiplier_mode = 0 -base_multiplier = 1.0 -multiplier_per_prestige = 0.05 -multiplier_exponent = 1.0 -metadata/_custom_type_script = "res://core/prestige/prestige_config.gd" diff --git a/project.godot b/project.godot index 14f2d76..fe3dde7 100644 --- a/project.godot +++ b/project.godot @@ -12,15 +12,10 @@ config_version=5 config/name="Idles" config/version="0.0.1" +run/main_scene="uid://dadeowsvaywpp" config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" -[autoload] - -CurrencyDatabase="*res://core/currency_database.gd" -GameState="*uid://d2j7tvlgxr2jp" -PrestigeManager="*res://core/prestige/prestige_manager.gd" - [display] window/size/viewport_width=1920 diff --git a/scripts/check_syntax.sh b/scripts/check_syntax.sh new file mode 100755 index 0000000..1dbbfdc --- /dev/null +++ b/scripts/check_syntax.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Godot Syntax Checker +# Usage: ./scripts/check_syntax.sh +# Example: ./scripts/check_syntax.sh res://tests/test_gold_mine_click.gd + +SCRIPT="$1" +GODOT_BIN="${GODOT_BIN:-godot}" + +if [ -z "$SCRIPT" ]; then + echo "Usage: $0 " + echo "Example: $0 res://tests/test_gold_mine_click.gd" + exit 1 +fi + +if [ ! -f "$SCRIPT" ]; then + echo "Error: Script not found: $SCRIPT" + exit 1 +fi + +echo "==========================================" +echo "Checking syntax: $SCRIPT" +echo "==========================================" +echo "" + +"$GODOT_BIN" --headless --path . --check-only --script "$SCRIPT" +EXIT_CODE=$? + +echo "" +echo "==========================================" +if [ $EXIT_CODE -eq 0 ]; then + echo "✓ Syntax valid" +else + echo "✗ Syntax errors found" +fi +echo "==========================================" + +exit $EXIT_CODE diff --git a/scripts/run_all.sh b/scripts/run_all.sh new file mode 100755 index 0000000..40032d2 --- /dev/null +++ b/scripts/run_all.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Godot Test Runner - All Tests +# Usage: ./scripts/run_all.sh + +set -e + +GODOT_BIN="${GODOT_BIN:-godot}" + +echo "==========================================" +echo "Running All Tests" +echo "==========================================" +echo "" + +"$GODOT_BIN" --headless --path . -s res://tests/test_runner.gd +EXIT_CODE=$? + +echo "" +echo "==========================================" +if [ $EXIT_CODE -eq 0 ]; then + echo "✓ All tests passed" +else + echo "✗ Some tests failed" +fi +echo "==========================================" + +exit $EXIT_CODE diff --git a/scripts/run_test.sh b/scripts/run_test.sh new file mode 100755 index 0000000..80d52f7 --- /dev/null +++ b/scripts/run_test.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Godot Test Runner - Single Test +# Usage: ./scripts/run_test.sh +# Example: ./scripts/run_test.sh res://tests/test_gold_mine_click.gd + +set -e + +TEST_SCRIPT="$1" +GODOT_BIN="${GODOT_BIN:-godot}" + +if [ -z "$TEST_SCRIPT" ]; then + echo "Usage: $0 " + echo "Example: $0 res://tests/test_gold_mine_click.gd" + exit 1 +fi + +if [ ! -f "$TEST_SCRIPT" ]; then + echo "Error: Test script not found: $TEST_SCRIPT" + exit 1 +fi + +echo "==========================================" +echo "Running test: $TEST_SCRIPT" +echo "==========================================" +echo "" + +"$GODOT_BIN" --headless --path . -s "$TEST_SCRIPT" +EXIT_CODE=$? + +echo "" +echo "==========================================" +if [ $EXIT_CODE -eq 0 ]; then + echo "✓ Test passed" +else + echo "✗ Test failed with exit code: $EXIT_CODE" +fi +echo "==========================================" + +exit $EXIT_CODE diff --git a/test_multi_currency_prestige.gd b/test_multi_currency_prestige.gd new file mode 100644 index 0000000..984221b --- /dev/null +++ b/test_multi_currency_prestige.gd @@ -0,0 +1,61 @@ +@tool +extends Node + +func _ready() -> void: + if not Engine.is_editor_hint(): + test_multi_currency_prestige() + get_tree().quit() + +func test_multi_currency_prestige() -> void: + var game_state: LevelGameState = find_child("LevelGameState") + var prestige_manager: PrestigeManager = game_state.find_child("PrestigeManager") + + print("\n=== MULTI-CURRENCY PRESTIGE TEST ===\n") + + # Check config + var config = prestige_manager.get_config() + var basis_type = config.get("basis", 0) + print("Basis type: %d (expected 3 for ALL_CURRENCIES)" % basis_type) + + # Check tracked currencies + var tracked_ids = prestige_manager.get_tracked_currency_ids() + print("Tracked currency IDs: %s" % str(tracked_ids)) + + # Add some test currency amounts + var gold = game_state.get_currency_amount_by_id(&"gold") + print("Initial gold: %s" % gold.to_string_suffix(2)) + + # Simulate earning currencies + game_state.add_currency_by_id(&"gold", BigNumber.from_float(1000.0)) + game_state.add_currency_by_id(&"wood", BigNumber.from_float(2000.0)) + game_state.add_currency_by_id(&"food", BigNumber.from_float(1500.0)) + game_state.add_currency_by_id(&"worker", BigNumber.from_float(500.0)) + + # Check total + var total = prestige_manager.get_basis_value_for_display() + print("Total currency sum: %s" % total.to_string_suffix(2)) + + # Check threshold + var threshold = prestige_manager.get_next_prestige_threshold() + print("Prestige threshold: %s" % threshold.to_string_suffix(2)) + + # Check if can prestige + var can_prestige = prestige_manager.can_prestige() + print("Can prestige: %s" % str(can_prestige)) + + # Check pending gain + var pending_gain = prestige_manager.calculate_pending_gain() + print("Pending gain: %s" % pending_gain.to_string_suffix(2)) + + # Perform prestige + if can_prestige: + var result = prestige_manager.perform_prestige() + print("Prestige performed: %s" % str(result)) + + var total_prestige = prestige_manager.get_total_prestige() + print("Total prestige earned: %s" % total_prestige.to_string_suffix(2)) + + var ascension = game_state.get_currency_amount_by_id(&"ascension") + print("Ascension currency: %s" % ascension.to_string_suffix(2)) + + print("\n=== TEST COMPLETE ===\n") diff --git a/test_multi_currency_prestige.gd.uid b/test_multi_currency_prestige.gd.uid new file mode 100644 index 0000000..92ef00e --- /dev/null +++ b/test_multi_currency_prestige.gd.uid @@ -0,0 +1 @@ +uid://c5ubwdte41tsf diff --git a/tests/test_ascension.gd b/tests/test_ascension.gd new file mode 100644 index 0000000..cb9ec39 --- /dev/null +++ b/tests/test_ascension.gd @@ -0,0 +1,318 @@ +extends Node + +var passed: int = 0 +var failed: int = 0 +var _game_root: Node = null + + +func _ready() -> void: + await run() + + +func run() -> void: + print("\n=== TEST: Ascension Buff System ===\n") + TestUtils.set_test_name("ascension") + + _test_node_resource() + _test_node_validation() + _test_node_parent_checking() + _test_catalogue_queries() + _test_purchase_flow() + _test_prestige_does_not_reset_prestige_buffs() + _test_multiplier_computation() + _test_save_load_roundtrip() + + _print_summary() + + +#region Node Resource Tests +func _test_node_resource() -> void: + print("--- Node Resource ---") + + var node: PrestigeBuffNode = PrestigeBuffNode.new() + node.id = &"test_node" + node.display_name = "Test Node" + node.effect_type = PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER + node.effect_value = 2.0 + node.cost_mantissa = 5.0 + node.cost_exponent = 2 # cost = 5e2 = 500 + + var cost: BigNumber = node.get_cost() + TestUtils.assert_equals(5.0, cost.mantissa, "cost mantissa matches") + TestUtils.assert_equals(2, cost.exponent, "cost exponent matches") + TestUtils.assert_false(node.has_parents(), "node with no parents returns false") + + +func _test_node_validation() -> void: + print("--- Node Validation ---") + + var valid: PrestigeBuffNode = PrestigeBuffNode.new() + valid.id = &"valid_id" + valid.cost_mantissa = 1.0 + TestUtils.assert_true(valid.is_valid(), "node with id and positive cost is valid") + + var no_id: PrestigeBuffNode = PrestigeBuffNode.new() + no_id.cost_mantissa = 1.0 + TestUtils.assert_false(no_id.is_valid(), "node with empty id is invalid") + + var zero_cost: PrestigeBuffNode = PrestigeBuffNode.new() + zero_cost.id = &"has_id" + zero_cost.cost_mantissa = 0.0 + TestUtils.assert_false(zero_cost.is_valid(), "node with zero cost is invalid") + + +func _test_node_parent_checking() -> void: + print("--- Parent Checking ---") + + var unlocked: Dictionary = {} + unlocked[&"parent_a"] = true + unlocked[&"parent_b"] = false + + var node: PrestigeBuffNode = PrestigeBuffNode.new() + node.id = &"child" + node.parent_ids = [&"parent_a"] + + TestUtils.assert_true(node.all_parents_unlocked(unlocked), "all parents unlocked when all are true") + + node.parent_ids = [&"parent_a", &"parent_b"] + TestUtils.assert_false(node.all_parents_unlocked(unlocked), "not all parents unlocked when one is false") + + node.parent_ids = [&"nonexistent"] + TestUtils.assert_false(node.all_parents_unlocked(unlocked), "missing parent treated as not unlocked") +#endregion + + +#region Catalogue Tests +func _test_catalogue_queries() -> void: + print("--- Catalogue Queries ---") + + var root: PrestigeBuffNode = _make_node(&"root", [], 0, 0.0) + var child_a: PrestigeBuffNode = _make_node(&"child_a", [&"root"], 1, 0.0) + var child_b: PrestigeBuffNode = _make_node(&"child_b", [&"root"], 1, 1.0) + var grandchild: PrestigeBuffNode = _make_node(&"grandchild", [&"child_a", &"child_b"], 2, 0.0) + + var cat: PrestigeBuffCatalogue = PrestigeBuffCatalogue.new() + cat.nodes = [root, child_a, child_b, grandchild] + + TestUtils.assert_equals(root, cat.get_node_by_id(&"root"), "get_node_by_id finds root") + TestUtils.assert_equals(null, cat.get_node_by_id(&"nonexistent"), "get_node_by_id returns null for unknown id") + + var ids: Array[StringName] = cat.get_all_ids() + TestUtils.assert_equals(4, ids.size(), "get_all_ids returns 4 ids") + + var roots: Array[PrestigeBuffNode] = cat.get_root_nodes() + TestUtils.assert_equals(1, roots.size(), "one root node") + TestUtils.assert_equals(&"root", roots[0].id, "root node id is 'root'") + + var children: Array[PrestigeBuffNode] = cat.get_children_of(&"root") + TestUtils.assert_equals(2, children.size(), "root has 2 children") + + var grandkids: Array[PrestigeBuffNode] = cat.get_children_of(&"child_a") + TestUtils.assert_equals(1, grandkids.size(), "child_a has 1 child") +#endregion + + +#region Purchase Flow Tests +func _test_purchase_flow() -> void: + print("--- Purchase Flow ---") + + var scene: PackedScene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + _game_root = scene.instantiate() + add_child(_game_root) + await _wait() + + var gs: LevelGameState = _game_root.find_child("LevelGameState") + if gs == null: + print("[ERROR] LevelGameState not found") + return + + # Give ascension currency for purchasing + gs.add_currency_by_id(&"ascension", BigNumber.from_float(100.0)) + await _wait() + + # Verify catalogue has our two nodes + var cat: PrestigeBuffCatalogue = gs.prestige_buff_catalogue + TestUtils.assert_not_null(cat, "prestige_buff_catalogue is set") + if cat == null: + return + + TestUtils.assert_true(cat.nodes.size() >= 2, "catalogue has at least 2 nodes") + + # Verify nodes initialized in state + TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost starts locked") + TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost starts locked") + + # Check can_purchase (both are root nodes with no parents, should be purchaseable) + TestUtils.assert_true(gs.can_purchase_prestige_buff(&"gold_boost"), "gold_boost is purchasable") + TestUtils.assert_true(gs.can_purchase_prestige_buff(&"farm_boost"), "farm_boost is purchasable") + + # Check cost + var cost: BigNumber = gs.get_prestige_buff_cost(&"gold_boost") + TestUtils.assert_greater_than(cost.mantissa, 0.0, "gold_boost cost > 0") + + # Purchase gold_boost + var purchased: bool = gs.purchase_prestige_buff(&"gold_boost") + TestUtils.assert_true(purchased, "purchase_prestige_buff returns true") + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost is now unlocked") + TestUtils.assert_false(gs.can_purchase_prestige_buff(&"gold_boost"), "cannot repurchase already unlocked node") + + # farm_boost should still be available (both are roots, independent) + TestUtils.assert_true(gs.can_purchase_prestige_buff(&"farm_boost"), "farm_boost is still purchasable") + + # Verify available buffs + var available: Array[PrestigeBuffNode] = gs.get_available_prestige_buffs() + var found_farm: bool = false + var found_gold: bool = false + for node in available: + if node.id == &"farm_boost": + found_farm = true + if node.id == &"gold_boost": + found_gold = true + TestUtils.assert_true(found_farm, "farm_boost appears in available buffs") + TestUtils.assert_false(found_gold, "gold_boost does NOT appear in available buffs (already unlocked)") + + # Test cannot purchase with insufficient currency + gs._prestige_buff_unlocked.clear() + gs._initialize_prestige_buffs() + # Drain ascension currency + var bal: BigNumber = gs.get_currency_amount_by_id(&"ascension") + gs.spend_currency_by_id(&"ascension", bal) + TestUtils.assert_false(gs.can_purchase_prestige_buff(&"gold_boost"), "cannot purchase with no currency") + + # Restore currency, re-unlock gold_boost for later tests + gs.add_currency_by_id(&"ascension", BigNumber.from_float(100.0)) + gs.purchase_prestige_buff(&"gold_boost") + await _wait() +#endregion + + +func _test_prestige_does_not_reset_prestige_buffs() -> void: + print("--- Prestige Does Not Reset Prestige ---") + + if _game_root == null: + return + + var gs: LevelGameState = _game_root.find_child("LevelGameState") + if gs == null: + return + + # gold_boost should be unlocked from previous test + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost unlocked before prestige") + + # Give enough currency to prestige + gs.add_currency_by_id(&"gold", BigNumber.new(1.0, 10)) # 1e10 gold + await _wait() + + var pm: PrestigeManager = _game_root.find_child("PrestigeManager") as PrestigeManager + if pm == null: + print("[WARN] PrestigeManager not found; skipping prestige test") + return + + # Perform prestige if possible + if pm.can_prestige(): + pm.perform_prestige() + await _wait() + + # After prestige, gold_boost should STILL be unlocked + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost still unlocked after prestige") + else: + # Manual simulation: call reset_for_prestige directly and verify ascension survives + gs.reset_for_prestige(true, false, [&"ascension"]) + await _wait() + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost still unlocked after reset_for_prestige") +#endregion + + +func _test_multiplier_computation() -> void: + print("--- Multiplier Computation ---") + + if _game_root == null: + return + + var gs: LevelGameState = _game_root.find_child("LevelGameState") + if gs == null: + return + + # gold_boost is already unlocked (from purchase test). Effect type 1 = GENERATOR_PRODUCTION_MULTIPLIER + var mult: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, &"goldmine") + TestUtils.assert_greater_than(mult, 1.0, "multiplier for goldmine is > 1.0 with gold_boost unlocked") + + # farm_boost is NOT unlocked, multiplier for farm should be 1.0 + var farm_mult: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, &"farm") + TestUtils.assert_equals(1.0, farm_mult, "multiplier for farm is 1.0 (farm_boost not unlocked)") + + # Unlock farm_boost and check again + gs.add_currency_by_id(&"ascension", BigNumber.from_float(100.0)) + gs.purchase_prestige_buff(&"farm_boost") + await _wait() + + var farm_mult2: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.CURRENCY_PRODUCTION_MULTIPLIER, &"farm") + TestUtils.assert_greater_than(farm_mult2, 1.0, "multiplier for farm is > 1.0 after unlocking farm_boost") + + # Check that unrelated effect type returns 1.0 + var unrelated: float = gs.get_prestige_buff_multiplier(PrestigeBuffNode.EffectType.RESEARCH_XP_MULTIPLIER, &"") + TestUtils.assert_equals(1.0, unrelated, "unrelated effect type returns 1.0") +#endregion + + +func _test_save_load_roundtrip() -> void: + print("--- Save/Load Roundtrip ---") + + if _game_root == null: + return + + var gs: LevelGameState = _game_root.find_child("LevelGameState") + if gs == null: + return + + # Both gold_boost and farm_boost should be unlocked + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost unlocked before save") + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost unlocked before save") + + # Save + gs.save_game() + await _wait() + + # Clear state manually to simulate fresh load + gs._prestige_buff_unlocked.clear() + TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost false after clear") + TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost false after clear") + + # Load + gs.load_game() + await _wait() + + # Both should be restored + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost restored after load") + TestUtils.assert_true(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost restored after load") +#endregion + + +#region Helpers +func _make_node(id: StringName, parent_ids: Array[StringName], tier: int, x_pos: float) -> PrestigeBuffNode: + var n: PrestigeBuffNode = PrestigeBuffNode.new() + n.id = id + n.display_name = String(id) + n.parent_ids = parent_ids + n.tier = tier + n.x_position = x_pos + n.cost_mantissa = 1.0 + return n + + +func _wait() -> void: + await get_tree().create_timer(0.3).timeout + + +func _print_summary() -> void: + TestUtils.print_result() + passed = TestUtils.get_passed() + failed = TestUtils.get_failed() + + +func get_passed() -> int: + return passed + + +func get_failed() -> int: + return failed diff --git a/tests/test_ascension.gd.uid b/tests/test_ascension.gd.uid new file mode 100644 index 0000000..8f55de3 --- /dev/null +++ b/tests/test_ascension.gd.uid @@ -0,0 +1 @@ +uid://cwmf4h7be356h diff --git a/tests/test_goals_prestige_reset.gd b/tests/test_goals_prestige_reset.gd new file mode 100644 index 0000000..75a7d64 --- /dev/null +++ b/tests/test_goals_prestige_reset.gd @@ -0,0 +1,146 @@ +@tool +extends Node + +var _game_root: Node = null + +func _ready() -> void: + if not Engine.is_editor_hint(): + test_goals_prestige_reset() + get_tree().quit() + +func test_goals_prestige_reset() -> void: + print("\n=== TEST: Goals Prestige Reset ===\n") + TestUtils.set_test_name("goals_prestige_reset") + + # Load the tiny_sword scene directly + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + _game_root = scene.instantiate() + add_child(_game_root) + + print("[TARGET] children_after_load: %d" % get_child_count()) + + var game_state: LevelGameState = _game_root.find_child("LevelGameState") + if game_state == null: + print("[ERROR] LevelGameState not found") + _print_summary(0, 1) + return + + print("[TARGET] game_state_found: true") + + var goals: Array[GoalData] = game_state.get_all_goals() + if goals.is_empty(): + print("[ERROR] No goals found in game state") + _print_summary(0, 1) + return + + var test_goal: GoalData = goals[0] + print("[TARGET] test_goal_id: %s" % String(test_goal.id)) + + var gold: BigNumber = BigNumber.from_float(1000000.0) + game_state.add_currency_by_id(&"gold", gold) + + var is_met_before: bool = game_state.is_goal_met(test_goal) + print("[TARGET] goal_met_before_prestige: %s" % str(is_met_before)) + + TestUtils.assert_true( + is_met_before, + "Goal should be met before prestige (with high currency)" + ) + + var is_completed_before: bool = game_state.is_goal_completed(test_goal.id) + print("[TARGET] goal_completed_before_prestige: %s" % str(is_completed_before)) + + game_state.reset_for_prestige(true, true, []) + + var is_completed_after_reset: bool = game_state.is_goal_completed(test_goal.id) + print("[TARGET] goal_completed_after_prestige: %s" % str(is_completed_after_reset)) + + TestUtils.assert_true( + not is_completed_after_reset, + "Goal should NOT be completed after prestige reset (BUG FIX VERIFICATION)" + ) + + var gold_after: BigNumber = game_state.get_currency_amount_by_id(&"gold") + print("[TARGET] gold_after_prestige: %s" % gold_after.to_string_suffix(2)) + + TestUtils.assert_equals( + 0.0, + gold_after.mantissa, + "Gold should be reset to 0 after prestige" + ) + + var total_gold_after: BigNumber = game_state.get_total_currency_acquired_by_id(&"gold") + print("[TARGET] total_gold_after_prestige: %s" % total_gold_after.to_string_suffix(2)) + + TestUtils.assert_equals( + 0.0, + total_gold_after.mantissa, + "Total gold should be reset to 0 after prestige" + ) + + var is_met_after: bool = game_state.is_goal_met(test_goal) + print("[TARGET] goal_met_after_prestige: %s" % str(is_met_after)) + + TestUtils.assert_true( + not is_met_after, + "Goal should NOT be met after prestige (currency reset to 0)" + ) + + var gold_for_unlock: BigNumber = BigNumber.from_float(1000000.0) + game_state.add_currency_by_id(&"gold", gold_for_unlock) + + var is_met_again: bool = game_state.is_goal_met(test_goal) + print("[TARGET] goal_met_after_reaching_currency: %s" % str(is_met_again)) + + TestUtils.assert_true( + is_met_again, + "Goal should be met again after adding sufficient currency" + ) + + var is_completed_auto: bool = game_state.is_goal_completed(test_goal.id) + print("[TARGET] goal_completed_auto_check: %s" % str(is_completed_auto)) + + # This goal has AUTOMATIC unlock behavior, so it should be auto-completed when met + if test_goal.unlock_behavior == GoalData.UnlockBehavior.AUTOMATIC: + TestUtils.assert_true( + is_completed_auto, + "Goal with AUTOMATIC unlock should be auto-completed when met" + ) + +# Try manual completion (should be a no-op if already auto-completed) + game_state._complete_goal_manually(test_goal.id) + + var is_completed: bool = game_state.is_goal_completed(test_goal.id) + print("[TARGET] goal_completed_after_attempt: %s" % str(is_completed)) + + TestUtils.assert_true( + is_completed, + "Goal should be completed (either auto or manual)" + ) + + game_state.reset_for_prestige(true, true, []) + + var is_completed_after_second_prestige: bool = game_state.is_goal_completed(test_goal.id) + print("[TARGET] goal_completed_after_second_prestige: %s" % str(is_completed_after_second_prestige)) + + TestUtils.assert_true( + not is_completed_after_second_prestige, + "Goal should NOT be completed after second prestige reset" + ) + + var is_met_final: bool = game_state.is_goal_met(test_goal) + print("[TARGET] goal_met_final: %s" % str(is_met_final)) + + TestUtils.assert_true( + not is_met_final, + "Goal should NOT be met after second prestige reset" + ) + + _print_summary(TestUtils.get_passed(), TestUtils.get_failed()) + +func _print_summary(passed_count: int, failed_count: int) -> void: + TestUtils.print_result() + if failed_count == 0: + print("[RESULT] PASS") + else: + print("[RESULT] FAIL") diff --git a/tests/test_goals_prestige_reset.gd.uid b/tests/test_goals_prestige_reset.gd.uid new file mode 100644 index 0000000..88880be --- /dev/null +++ b/tests/test_goals_prestige_reset.gd.uid @@ -0,0 +1 @@ +uid://d23axeulmeeih diff --git a/tests/test_goals_prestige_reset.tscn b/tests/test_goals_prestige_reset.tscn new file mode 100644 index 0000000..d604b14 --- /dev/null +++ b/tests/test_goals_prestige_reset.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3 uid="uid://test_goals_prestige_reset"] + +[ext_resource type="Script" path="res://tests/test_goals_prestige_reset.gd" id="1_test"] + +[node name="TestRoot" type="Node"] +script = ExtResource("1_test") diff --git a/tests/test_gold_mine_click.gd b/tests/test_gold_mine_click.gd new file mode 100644 index 0000000..8c64b7c --- /dev/null +++ b/tests/test_gold_mine_click.gd @@ -0,0 +1,75 @@ +extends Node + +var passed: int = 0 +var failed: int = 0 +var _game_root: Node = null + +func _ready(): + await run() + # Don't quit here - let test_runner handle it + +func run(): + print("\n=== TEST: Gold Mine Click ===\n") + TestUtils.set_test_name("gold_mine_click") + + # Load tiny_sword scene + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + _game_root = scene.instantiate() + add_child(_game_root) + + await _wait() + + var game_state: LevelGameState = _game_root.find_child("LevelGameState") + var gold_mine = _game_root.find_child("GoldMine") as CurrencyGenerator + + if game_state == null: + print("[ERROR] LevelGameState not found") + _print_summary() + return + + if gold_mine == null: + print("[ERROR] GoldMine node not found") + _print_summary() + return + + # Record initial gold + var initial_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold") + print("[TARGET] initial_gold: %s" % initial_gold.to_string_suffix(2)) + + # Simulate clicking the generator + gold_mine._on_pressed() + + await _wait() + + # Validate gold increased + var final_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold") + print("[TARGET] final_gold: %s" % final_gold.to_string_suffix(2)) + + TestUtils.assert_greater_than( + final_gold.mantissa, + initial_gold.mantissa, + "Gold increased after generator click" + ) + + # Check specific value (adjust based on click_mantissa in data) + var expected_min: float = initial_gold.mantissa + 1.0 + TestUtils.assert_true( + final_gold.mantissa >= expected_min, + "Gold increased by at least 1.0" + ) + + _print_summary() + +func _wait() -> void: + await get_tree().create_timer(0.5).timeout + +func _print_summary(): + TestUtils.print_result() + passed = TestUtils.get_passed() + failed = TestUtils.get_failed() + +func get_passed() -> int: + return passed + +func get_failed() -> int: + return failed diff --git a/tests/test_gold_mine_click.gd.uid b/tests/test_gold_mine_click.gd.uid new file mode 100644 index 0000000..f0103d4 --- /dev/null +++ b/tests/test_gold_mine_click.gd.uid @@ -0,0 +1 @@ +uid://c2xborf2klkc4 diff --git a/tests/test_prestige.gd b/tests/test_prestige.gd new file mode 100644 index 0000000..0569763 --- /dev/null +++ b/tests/test_prestige.gd @@ -0,0 +1,92 @@ +extends Node + +var passed: int = 0 +var failed: int = 0 +var _game_root: Node = null + +func _ready(): + await run() + # Let test_runner handle exit + pass + +func run(): + print("\n=== TEST: Prestige Mechanics ===\n") + TestUtils.set_test_name("prestige") + + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + _game_root = scene.instantiate() + add_child(_game_root) + + await _wait() + + var game_state: LevelGameState = _game_root.find_child("LevelGameState") + var prestige_manager = _game_root.find_child("PrestigeManager") as PrestigeManager + + if game_state == null: + print("[ERROR] LevelGameState not found") + _print_summary() + return + + if prestige_manager == null: + print("[ERROR] PrestigeManager not found") + _print_summary() + return + + # Setup: Accumulate currencies to meet prestige threshold + game_state.add_currency_by_id(&"gold", BigNumber.from_float(100000.0)) + game_state.add_currency_by_id(&"gems", BigNumber.from_float(100.0)) + + await _wait() + + # Check if can prestige + var can_prestige: bool = prestige_manager.can_prestige() + print("[TARGET] can_prestige: %s" % str(can_prestige)) + + TestUtils.assert_true(can_prestige, "Should be able to prestige after accumulating currencies") + + if can_prestige: + # Perform prestige + prestige_manager.perform_prestige() + + await _wait() + + # Check ascension currency + var ascension: BigNumber = game_state.get_currency_amount_by_id(&"ascension") + print("[TARGET] ascension_currency: %s" % ascension.to_string_suffix(2)) + + TestUtils.assert_true( + ascension.mantissa > 0, + "Should receive ascension currency after prestige" + ) + + # Check prestige multiplier increased + var total_multiplier = prestige_manager.get_total_multiplier() + print("[TARGET] total_multiplier: %s" % str(total_multiplier)) + + var mult_value: float + if total_multiplier is float: + mult_value = total_multiplier + else: + mult_value = float(total_multiplier) + + TestUtils.assert_greater_than( + mult_value, + 1.0, + "Prestige multiplier should be > 1.0" + ) + + _print_summary() + +func _wait() -> void: + await get_tree().create_timer(0.5).timeout + +func _print_summary(): + TestUtils.print_result() + passed = TestUtils.get_passed() + failed = TestUtils.get_failed() + +func get_passed() -> int: + return passed + +func get_failed() -> int: + return failed diff --git a/tests/test_prestige.gd.uid b/tests/test_prestige.gd.uid new file mode 100644 index 0000000..dfec39d --- /dev/null +++ b/tests/test_prestige.gd.uid @@ -0,0 +1 @@ +uid://mm5vcjq44wf1 diff --git a/tests/test_research_activation.gd b/tests/test_research_activation.gd new file mode 100644 index 0000000..74c0c6b --- /dev/null +++ b/tests/test_research_activation.gd @@ -0,0 +1,159 @@ +extends SceneTree + +var passed: int = 0 +var failed: int = 0 +var _game_root: Node = null +var _local_passed: int = 0 +var _local_failed: int = 0 + +func _init(): + await run() + +func run(): + print("\n=== TEST: Research Activation ===\n") + TestUtils.set_test_name("research_activation") + + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + _game_root = scene.instantiate() + root.add_child(_game_root) + + await _wait() + + var game_state: LevelGameState = _game_root.find_child("LevelGameState") + var research_panel: ResearchPanel = _game_root.find_child("ResearchPanel") as ResearchPanel + + if game_state == null or research_panel == null: + print("[ERROR] Missing LevelGameState or ResearchPanel node") + _print_summary() + quit(1) + return + + var gold_mine_node = game_state.find_child("GoldMine") + var gold_mine: CurrencyGenerator = gold_mine_node.find_child("CurrencyGenerator") as CurrencyGenerator if gold_mine_node else null + + if gold_mine == null: + print("[ERROR] GoldMine node not found") + _print_summary() + quit(1) + return + + # Test 1: Verify initial state - no research should be active + print("[TARGET] initial_active_research_id: %s" % research_panel.get_active_research_id()) + TestUtils.assert_true( + research_panel.get_active_research_id() == &"", + "No research active initially" + ) + + # Test 2: Verify gold mine research exists and is linked to goldmine generator + var gold_research = game_state.research_catalogue.get_research_by_id(&"gold_research") + TestUtils.assert_not_null( + gold_research, + "Gold research exists in catalogue" + ) + + if gold_research != null: + TestUtils.assert_true( + gold_research.generator_id == &"goldmine", + "Gold research is linked to goldmine generator" + ) + + # Test 3: Activate gold research + research_panel.activate_research(&"gold_research") + await _wait() + + print("[TARGET] active_after_gold_activation: %s" % research_panel.get_active_research_id()) + TestUtils.assert_true( + research_panel.get_active_research_id() == &"gold_research", + "Gold research is active after activation" + ) + + TestUtils.assert_true( + research_panel.is_research_active(&"gold_research"), + "is_research_active returns true for gold_research" + ) + + # Test 4: Verify research XP is granted when research is active + var initial_gold_research_xp = game_state.get_research_xp(&"gold_research") + print("[TARGET] initial_gold_research_xp: %s" % initial_gold_research_xp.to_string_suffix(2)) + + # Simulate gold mine production (gold mine grants XP to gold_research) + gold_mine._on_pressed() + await _wait() + + var final_gold_research_xp_active = game_state.get_research_xp(&"gold_research") + print("[TARGET] final_gold_research_xp_active: %s" % final_gold_research_xp_active.to_string_suffix(2)) + + TestUtils.assert_true( + final_gold_research_xp_active.mantissa > initial_gold_research_xp.mantissa, + "Gold research XP increases when active (gold mine produces)" + ) + + # Test 5: Deactivate gold research by activating another research + var farm_research = game_state.research_catalogue.get_research_by_id(&"farm_research") + TestUtils.assert_not_null( + farm_research, + "Farm research exists in catalogue" + ) + + if farm_research != null: + # Activate farm research (should deactivate gold research) + research_panel.activate_research(&"farm_research") + await _wait() + + print("[TARGET] active_after_farm_activation: %s" % research_panel.get_active_research_id()) + TestUtils.assert_true( + research_panel.get_active_research_id() == &"farm_research", + "Farm research is active after activation" + ) + + TestUtils.assert_false( + research_panel.is_research_active(&"gold_research"), + "Gold research is deactivated when farm is activated" + ) + + # Test 6: Verify research XP is NOT granted when research is inactive + var initial_gold_research_xp_inactive = game_state.get_research_xp(&"gold_research") + print("[TARGET] initial_gold_research_xp_inactive: %s" % initial_gold_research_xp_inactive.to_string_suffix(2)) + + # Simulate gold mine production (gold mine should NOT grant XP to inactive gold_research) + gold_mine._on_pressed() + await _wait() + + var final_gold_research_xp_inactive = game_state.get_research_xp(&"gold_research") + print("[TARGET] final_gold_research_xp_inactive: %s" % final_gold_research_xp_inactive.to_string_suffix(2)) + + TestUtils.assert_equals( + final_gold_research_xp_inactive.mantissa, + initial_gold_research_xp_inactive.mantissa, + "Gold research XP does not increase when inactive (gold mine produces)" + ) + + # Test 7: Test mutual exclusivity with multiple activations + research_panel.activate_research(&"gold_research") + await _wait() + research_panel.activate_research(&"farm_research") + await _wait() + research_panel.activate_research(&"gold_research") + await _wait() + + print("[TARGET] final_active_research_id: %s" % research_panel.get_active_research_id()) + TestUtils.assert_true( + research_panel.get_active_research_id() == &"gold_research", + "Only one research can be active at a time" + ) + + _print_summary() + if failed == 0: + quit(0) + else: + quit(1) + +func _wait() -> void: + await create_timer(0.5).timeout + +func _print_summary(): + TestUtils.print_result() + _local_passed = TestUtils.get_passed() + _local_failed = TestUtils.get_failed() + passed = _local_passed + failed = _local_failed diff --git a/tests/test_research_activation.gd.uid b/tests/test_research_activation.gd.uid new file mode 100644 index 0000000..fd20761 --- /dev/null +++ b/tests/test_research_activation.gd.uid @@ -0,0 +1 @@ +uid://cf23bjqannrog diff --git a/tests/test_research_prestige_reset.gd b/tests/test_research_prestige_reset.gd new file mode 100644 index 0000000..63a6e4a --- /dev/null +++ b/tests/test_research_prestige_reset.gd @@ -0,0 +1,128 @@ +extends SceneTree + +var passed: int = 0 +var failed: int = 0 +var _game_root: Node = null +var _test_complete: bool = false + +func _init(): + print("\n=== TEST: Research Level and XP Reset After Prestige ===\n") + TestUtils.set_test_name("research_prestige_reset") + + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + change_scene_to_packed(scene) + + await _wait() + + _game_root = root.get_child(0) + + var game_state: LevelGameState = _game_root.find_child("LevelGameState") + var prestige_manager = _game_root.find_child("PrestigeManager") as PrestigeManager + + if game_state == null: + print("[ERROR] LevelGameState not found") + _print_summary() + _test_complete = true + quit(1) + return + + if prestige_manager == null: + print("[ERROR] PrestigeManager not found") + _print_summary() + _test_complete = true + quit(1) + return + + var research_id: StringName = &"gold_research" + + var initial_xp: BigNumber = game_state.get_research_xp(research_id) + var initial_level: int = game_state.get_research_level(research_id) + print("[TARGET] initial_research_xp: mantissa=%s, exponent=%s" % [initial_xp.mantissa, initial_xp.exponent]) + print("[TARGET] initial_research_level: %d" % initial_level) + + TestUtils.assert_equals( + 0, + initial_xp.mantissa, + "Initial research XP should be 0" + ) + + TestUtils.assert_equals( + 0, + initial_level, + "Initial research level should be 0" + ) + + game_state.add_currency_by_id(&"gold", BigNumber.from_float(100000.0)) + game_state.add_currency_by_id(&"gems", BigNumber.from_float(100.0)) + + await _wait() + + for i in range(10): + game_state.add_research_xp(research_id, BigNumber.from_float(500.0)) + await _wait(0.05) + + var pre_prestige_xp: BigNumber = game_state.get_research_xp(research_id) + var pre_prestige_level: int = game_state.get_research_level(research_id) + print("[TARGET] pre_prestige_research_xp: mantissa=%s, exponent=%s" % [pre_prestige_xp.mantissa, pre_prestige_xp.exponent]) + print("[TARGET] pre_prestige_research_level: %d" % pre_prestige_level) + + TestUtils.assert_true( + pre_prestige_xp.mantissa > 0, + "Research XP should be > 0 before prestige" + ) + + TestUtils.assert_true( + pre_prestige_level > 0, + "Research level should be > 0 before prestige" + ) + + var can_prestige: bool = prestige_manager.can_prestige() + print("[TARGET] can_prestige: %s" % str(can_prestige)) + + TestUtils.assert_true( + can_prestige, + "Should be able to prestige after accumulating currencies" + ) + + if can_prestige: + prestige_manager.perform_prestige() + + await _wait() + + var post_prestige_xp: BigNumber = game_state.get_research_xp(research_id) + var post_prestige_level: int = game_state.get_research_level(research_id) + print("[TARGET] post_prestige_research_xp: mantissa=%s, exponent=%s" % [post_prestige_xp.mantissa, post_prestige_xp.exponent]) + print("[TARGET] post_prestige_research_level: %d" % post_prestige_level) + + TestUtils.assert_equals( + 0, + post_prestige_xp.mantissa, + "Research XP should be reset to 0 after prestige" + ) + + TestUtils.assert_equals( + 0, + post_prestige_level, + "Research level should be reset to 0 after prestige" + ) + + _print_summary() + _test_complete = true + if failed == 0: + quit(0) + else: + quit(1) + +func _wait(seconds: float = 0.1) -> void: + await create_timer(seconds).timeout + +func _print_summary(): + TestUtils.print_result() + passed = TestUtils.get_passed() + failed = TestUtils.get_failed() + +func get_passed() -> int: + return passed + +func get_failed() -> int: + return failed diff --git a/tests/test_research_prestige_reset.gd.uid b/tests/test_research_prestige_reset.gd.uid new file mode 100644 index 0000000..467ed3e --- /dev/null +++ b/tests/test_research_prestige_reset.gd.uid @@ -0,0 +1 @@ +uid://j2a4c8w1qrgj diff --git a/tests/test_research_xp_acquisition.gd b/tests/test_research_xp_acquisition.gd new file mode 100644 index 0000000..8593383 --- /dev/null +++ b/tests/test_research_xp_acquisition.gd @@ -0,0 +1,146 @@ +extends SceneTree + +var passed: int = 0 +var failed: int = 0 +var _game_root: Node = null +var _test_complete: bool = false + +func _init(): + print("\n=== TEST: Research XP Acquisition ===\n") + TestUtils.set_test_name("research_xp_acquisition") + + # Load tiny_sword scene + var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn") + change_scene_to_packed(scene) + + await _wait() + + _game_root = root.get_child(0) + + var game_state: LevelGameState = _game_root.find_child("LevelGameState") + + if game_state == null: + print("[ERROR] LevelGameState not found") + _print_summary() + _test_complete = true + quit(1) + return + + # Find GoldMine CurrencyGenerator + var gold_mine: CurrencyGenerator = null + var world = game_state.get_node_or_null("World") + if world: + for child in world.get_children(): + if child.get_name() == "GoldMine": + for gc_child in child.get_children(): + if gc_child is CurrencyGenerator: + gold_mine = gc_child + break + + if gold_mine == null: + print("[ERROR] GoldMine CurrencyGenerator node not found") + _print_summary() + _test_complete = true + quit(1) + return + + # Give the generator initial ownership so clicks work properly + if gold_mine.owned <= 0: + game_state.add_currency_by_id(&"worker", BigNumber.from_float(100.0)) + await _wait(0.05) + gold_mine.buy(1) + await _wait(0.05) + + # Get initial gold currency + var initial_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold") + print("[TARGET] initial_gold: mantissa=%s, exponent=%s" % [initial_gold.mantissa, initial_gold.exponent]) + + # Get initial research XP + var initial_xp: BigNumber = game_state.get_research_xp(&"gold_research") + print("[TARGET] initial_research_xp: mantissa=%s, exponent=%s" % [initial_xp.mantissa, initial_xp.exponent]) + + # Set press_buys_generator to false so clicks grant currency instead of buying + gold_mine.press_buys_generator = false + + # Simulate clicking the generator multiple times to produce currency + var click_count: int = 5 + for i in range(click_count): + gold_mine._on_pressed() + await _wait(0.1) + + # Check gold increased + var final_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold") + print("[TARGET] final_gold: mantissa=%s, exponent=%s" % [final_gold.mantissa, final_gold.exponent]) + + # Check owned count + var owned: int = gold_mine.owned + print("[TARGET] gold_mine_owned: %d" % owned) + + # Validate research XP increased + var final_xp: BigNumber = game_state.get_research_xp(&"gold_research") + print("[TARGET] final_research_xp: mantissa=%s, exponent=%s" % [final_xp.mantissa, final_xp.exponent]) + + TestUtils.assert_true( + final_xp.is_greater_than(initial_xp), + "Research XP increased after generator clicks" + ) + + # Test level calculation + var level: int = game_state.get_research_level(&"gold_research") + print("[TARGET] research_level: %d" % level) + + TestUtils.assert_true( + level >= 0, + "Research level is non-negative" + ) + + # Test research multiplier + var multiplier: float = game_state.get_research_multiplier(&"gold_research") + print("[TARGET] research_multiplier: %f" % multiplier) + + TestUtils.assert_greater_than( + multiplier, + 0.0, + "Research multiplier is positive" + ) + + # Test BigNumber XP progress calculation + var research_data: ResearchData = game_state.research_catalogue.get_research_by_id(&"gold_research") + if research_data != null: + var progress: float = research_data.get_xp_progress(final_xp) + print("[TARGET] xp_progress: %f" % progress) + + TestUtils.assert_true( + progress >= 0.0 and progress <= 1.0, + "XP progress is between 0.0 and 1.0" + ) + + # Test level calculation with BigNumber + var calculated_level: int = research_data.get_level_for_xp(final_xp) + print("[TARGET] calculated_level: %d" % calculated_level) + + TestUtils.assert_true( + calculated_level >= 0, + "Calculated level is non-negative" + ) + + _print_summary() + _test_complete = true + if failed == 0: + quit(0) + else: + quit(1) + +func _wait(seconds: float = 0.1) -> void: + await create_timer(seconds).timeout + +func _print_summary(): + TestUtils.print_result() + passed = TestUtils.get_passed() + failed = TestUtils.get_failed() + +func get_passed() -> int: + return passed + +func get_failed() -> int: + return failed diff --git a/tests/test_research_xp_acquisition.gd.uid b/tests/test_research_xp_acquisition.gd.uid new file mode 100644 index 0000000..d04c8b8 --- /dev/null +++ b/tests/test_research_xp_acquisition.gd.uid @@ -0,0 +1 @@ +uid://cq65acistj7ar diff --git a/tests/test_runner.gd b/tests/test_runner.gd new file mode 100644 index 0000000..2ffd46a --- /dev/null +++ b/tests/test_runner.gd @@ -0,0 +1,79 @@ +extends SceneTree + +var test_scripts: Array[String] = [] +var test_index: int = 0 +var total_passed: int = 0 +var total_failed: int = 0 + +func _init(): + print("\n=== GODOT TEST RUNNER ===\n") + _discover_tests() + + if test_scripts.is_empty(): + print("[ERROR] No test scripts found in res://tests/") + print("[RESULT] FAIL") + quit(1) + return + + print("Found %d test(s): %s\n" % [test_scripts.size(), str(test_scripts)]) + + await _run_next_test() + + print("\n=== TEST SUMMARY ===") + print("Total passed: %d" % total_passed) + print("Total failed: %d" % total_failed) + + if total_failed == 0: + print("[OVERALL_RESULT] PASS") + quit(0) + else: + print("[OVERALL_RESULT] FAIL") + quit(1) + +func _discover_tests() -> void: + var dir = DirAccess.open("res://tests") + if dir: + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + if file_name.ends_with(".gd") and file_name.begins_with("test_") and \ + file_name != "test_runner.gd" and file_name != "test_utils.gd": + test_scripts.append("res://tests/%s" % file_name) + file_name = dir.get_next() + dir.list_dir_end() + test_scripts.sort() + +func _run_next_test() -> void: + if test_index >= test_scripts.size(): + return + + var test_script = test_scripts[test_index] + print("--- Running test %d/%d: %s ---" % [test_index + 1, test_scripts.size(), test_script]) + print("") + + # Load and run test + var test_instance = load(test_script).new() + get_root().add_child(test_instance) + + # Check if test has run() method (new pattern) or uses _ready() (old pattern) + if test_instance.has_method("run"): + await test_instance.run() + # Clean up test instance + test_instance.queue_free() + else: + # For old pattern, wait a bit for _ready() to complete + await create_timer(0.2).timeout + # Clean up test instance + test_instance.queue_free() + + # Get results if available + if test_instance.has_method("get_passed"): + total_passed += test_instance.get_passed() + total_failed += test_instance.get_failed() + + # Wait for cleanup + await create_timer(0.1).timeout + + print("") + test_index += 1 + await _run_next_test() diff --git a/tests/test_runner.gd.uid b/tests/test_runner.gd.uid new file mode 100644 index 0000000..06fc00f --- /dev/null +++ b/tests/test_runner.gd.uid @@ -0,0 +1 @@ +uid://chame3fmeexw6 diff --git a/tests/test_utils.gd b/tests/test_utils.gd new file mode 100644 index 0000000..a1a8519 --- /dev/null +++ b/tests/test_utils.gd @@ -0,0 +1,74 @@ +class_name TestUtils +extends RefCounted + +static var _test_name: String = "" +static var _passed: int = 0 +static var _failed: int = 0 + +static func set_test_name(name: String) -> void: + _test_name = name + _passed = 0 + _failed = 0 + +static func assert_equals(expected: Variant, actual: Variant, message: String) -> void: + if expected == actual: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s (expected %s, got %s)" % [ + _test_name, message, str(expected), str(actual) + ]) + _failed += 1 + +static func assert_true(condition: bool, message: String) -> void: + if condition: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s" % [_test_name, message]) + _failed += 1 + +static func assert_false(condition: bool, message: String) -> void: + if not condition: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s (expected false, got true)" % [_test_name, message]) + _failed += 1 + +static func assert_greater_than(a: float, b: float, message: String) -> void: + if a > b: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s (expected %s > %s)" % [_test_name, message, str(a), str(b)]) + _failed += 1 + +static func assert_greater_or_equal(a: float, b: float, message: String) -> void: + if a >= b: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s (expected %s >= %s)" % [_test_name, message, str(a), str(b)]) + _failed += 1 + +static func assert_not_null(value: Variant, message: String) -> void: + if value != null: + print("[PASS] %s: %s" % [_test_name, message]) + _passed += 1 + else: + print("[FAIL] %s: %s" % [_test_name, message]) + _failed += 1 + +static func get_passed() -> int: + return _passed + +static func get_failed() -> int: + return _failed + +static func print_result() -> void: + print("[SUMMARY] %s: %d passed, %d failed" % [_test_name, _passed, _failed]) + if _failed == 0: + print("[RESULT] PASS") + else: + print("[RESULT] FAIL") diff --git a/tests/test_utils.gd.uid b/tests/test_utils.gd.uid new file mode 100644 index 0000000..da74c32 --- /dev/null +++ b/tests/test_utils.gd.uid @@ -0,0 +1 @@ +uid://ous5m4c7jotn