systems rework: buffs, prestige graph, research, modular architecture

Decouples core systems from centralized globals in favor of catalogue-based architecture and data-driven content.

   Key changes:
   - Prestige: node-based buff tech tree, graph panel, progress bar
   - Research: multi-worker system, per-generator research, xp generation
   - Ascension: meta-currency layer via multi-currency prestige resets
   - Buffs: refactored as generator-independent via catalogues
   - UI: currency panel, edge-scrolling camera + zoom, current-goal panel
   - Alchemy Tower: crafting building with recipe/cost system
   - Testing: 7 test suites (ascension, prestige, research, goals)
   - Content: migrated from hardcoded idles/ to gym format (tiny_sword)
This commit was merged in pull request #1.
This commit is contained in:
2026-05-07 20:36:21 +00:00
parent 2743fd314a
commit 81a4058b04
242 changed files with 11226 additions and 2242 deletions

318
tests/test_ascension.gd Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
uid://cwmf4h7be356h

View File

@@ -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")

View File

@@ -0,0 +1 @@
uid://d23axeulmeeih

View File

@@ -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")

View File

@@ -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

View File

@@ -0,0 +1 @@
uid://c2xborf2klkc4

92
tests/test_prestige.gd Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
uid://mm5vcjq44wf1

View File

@@ -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

View File

@@ -0,0 +1 @@
uid://cf23bjqannrog

View File

@@ -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

View File

@@ -0,0 +1 @@
uid://j2a4c8w1qrgj

View File

@@ -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

View File

@@ -0,0 +1 @@
uid://cq65acistj7ar

79
tests/test_runner.gd Normal file
View File

@@ -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()

1
tests/test_runner.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://chame3fmeexw6

74
tests/test_utils.gd Normal file
View File

@@ -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")

1
tests/test_utils.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://ous5m4c7jotn