extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_research_points.gd # Forgejo #63: a rare enemy planet type that yields no resources but grants # one research point when conquered - a required currency for mid/late # skill-tree nodes and vessel/lab research, gated alongside their existing # ore/resource costs. # # Covers: # A. research labs generate deterministically and are rare # (structural) the definition yields no resources and its surface has no # resource provider at all # F. a fresh game can still reach a ship crafter and an extractor with # zero research points # D. a research-point-gated skill node is refused while unaffordable, and # explains why # E. conquering a research_lab awards what its MAP says it is worth, once. # The reward used to be a flat 1 on the planet definition; it now comes # entirely from the authored setup, so a hard lab can pay more than an # easy one and the value is editable in the map editor. The planet type # contributing nothing of its own is part of what is checked (guarded # even against a redundant re-mark), emits the signal once, and a # non-research_lab planet's conquest never awards one # D continued: unlocking spends the points and the balance updates # G. the ship-lab research path gates and spends research points the # same way, independent of the skill tree var game_state: Node func _initialize() -> void: game_state = root.get_node("GameState") # --- A. deterministic and rare ------------------------------------------- var lab_count: int = 0 var total_planets: int = 0 for x in range(-40, 40): for y in range(-40, 40): var chunk: Vector2i = Vector2i(x, y) if not WorldGen.has_planet_at_chunk(chunk): continue total_planets += 1 var first: Dictionary = WorldGen.planet_data_at_chunk(chunk) var second: Dictionary = WorldGen.planet_data_at_chunk(chunk) if first.definition_id != second.definition_id or first.local_pos != second.local_pos: return _fail("planet_data_at_chunk(%s) is not deterministic" % chunk) if first.definition_id == "research_lab": lab_count += 1 print("scanned %d planet chunks: %d research labs" % [total_planets, lab_count]) if lab_count == 0: return _fail("no research labs generated across a wide scan - the rare outcome never fires") if lab_count * 3 >= total_planets: return _fail("research labs (%d of %d) are not rare" % [lab_count, total_planets]) print("research labs are deterministic and rare (%d of %d planet chunks)" % [lab_count, total_planets]) # --- structural: yields nothing at all ------------------------------------ var lab_def: Resource = load("res://planets/PlanetDefinitions.gd").get_definition("research_lab") if String(lab_def.resource_id) != "": return _fail("research_lab planet's resource_id is '%s', expected empty" % lab_def.resource_id) var profile: Resource = load("res://surface/PlanetSurfaceProfiles.gd").get_profile("research_lab") if profile.features_scene != null: return _fail("research_lab surface profile has a features_scene - it must yield nothing") print("research_lab yields no resource id and no surface feature provider") # --- F. a fresh game still reaches a crafter and an extractor ------------- game_state.reset_new_game() # #108: reset_new_game() arms the prologue, and the prologue opens with a # convoy rather than the game's own starting fleet. This test is about # what the GAME starts from, which is where the prologue hands over - # see test_intro_scene.gd for the prologue itself. game_state.intro_active = false # can_unlock() also checks the ore price, and a new game now banks # nothing - the opening mining run is the point. Stock the ore so this # checks what it says it checks: reachability at ZERO RESEARCH POINTS. game_state.rocks = 500 if not game_state.can_unlock("shipyards"): return _fail("shipyards is not reachable with 0 research points") game_state.unlock("shipyards") if not game_state.can_unlock("logistics"): return _fail("logistics is not reachable with 0 research points after shipyards") game_state.unlock("logistics") print("fresh game reaches a ship crafter (shipyards) and an extractor (logistics) at 0 research points") # --- D. a points-gated node is refused and explains why ------------------- if game_state.can_unlock("coordination"): return _fail("coordination (costs research points) was unlockable with 0 research points") print("a research-point-gated node (coordination) correctly refused at 0 points") # --- E. earning exactly one point, once, with the signal ------------------ var totals_seen: Array = [] game_state.research_points_changed.connect(func(total): totals_seen.append(total)) game_state.planet_capture.clear() # The planet type itself must no longer carry a reward - that is the # delegation. If it crept back, a lab would pay its map's value PLUS a # hidden one, or pay something even with no map authored. var lab_definition: Resource = load("res://planets/PlanetDefinitions.gd").get_definition("research_lab") if int(lab_definition.conquest_research_points) != 0: return _fail("the research_lab planet type still grants %d point(s) of its own - the reward belongs to the authored map now" \ % int(lab_definition.conquest_research_points)) game_state.register_capture_points("test_lab_no_reward", 1, "research_lab") game_state.mark_point_captured("test_lab_no_reward", 0) if game_state.research_points != 0: return _fail("a lab whose map states no reward still awarded %d" % game_state.research_points) print("the planet type grants nothing by itself; an unpriced map awards nothing") # What an authored map is worth is passed through as the override, exactly # as PlanetSetupHost does from the setup's research_point_reward. game_state.register_capture_points("test_lab_planet", 2, "research_lab", 1) if game_state.is_planet_conquered("test_lab_planet"): return _fail("a lab with 2 real capture points started conquered") game_state.mark_point_captured("test_lab_planet", 0) if game_state.research_points != 0: return _fail("a research point was awarded before every point was captured") game_state.mark_point_captured("test_lab_planet", 1) if game_state.research_points != 1: return _fail("conquering a research_lab did not award the 1 point its map is worth: %d" % game_state.research_points) if totals_seen != [1]: return _fail("research_points_changed did not fire exactly once with the new total: %s" % [totals_seen]) print("conquering a research_lab awarded exactly what its map is worth, once") game_state.mark_point_captured("test_lab_planet", 0) # already captured - redundant re-mark if game_state.research_points != 1: return _fail("re-marking an already-captured point re-awarded a research point: %d" % game_state.research_points) print("double-award guarded against") game_state.register_capture_points("test_rocky_planet", 1, "rocky") game_state.mark_point_captured("test_rocky_planet", 0) if game_state.research_points != 1: return _fail("conquering a non-research_lab planet awarded a research point: %d" % game_state.research_points) print("a non-research_lab planet's conquest never awards a point") # --- D continued: unlocking spends the points, balance updates ------------ if not game_state.can_unlock("coordination"): return _fail("coordination is still not unlockable with 1 research point and enough ore") if not game_state.unlock("coordination"): return _fail("unlock(coordination) failed despite can_unlock() being true") if game_state.research_points != 0: return _fail("unlocking a 1-point node did not spend the point: %d remaining" % game_state.research_points) print("unlocking a research-point-gated skill node spent the point, balance now 0") # --- G. the ship-lab research path gates and spends the same currency ----- var carrier_def: Resource = game_state.vessel_definition(GameState.VESSEL_CARRIER) var point_cost: int = int(carrier_def.research_point_cost) if point_cost <= 0: return _fail("carrier production data has no research-point cost") game_state.vessel_levels.erase(carrier_def.unlock_id) var lab_id: int = game_state.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO) if not game_state.queue_lab_research(lab_id, carrier_def.unlock_id): return _fail("could not queue carrier research") game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, 1000) game_state._advance_lab_research(100.0) if game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER): return _fail("carrier research completed despite 0 research points") print("lab research correctly stalled on a research-point shortfall") game_state.research_points = point_cost game_state._advance_lab_research(100.0) if not game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER): return _fail("carrier research did not complete once research points were available") if game_state.research_points != 0: return _fail("lab research did not spend the research points: %d remaining" % game_state.research_points) print("lab research spent the research points once affordable, and completed") print("TEST_RESEARCH_POINTS: PASS") quit(0) func _fail(message: String) -> void: print("TEST_RESEARCH_POINTS: FAIL (%s)" % message) quit(1)