Add research lab planets and research points (#63)

This commit is contained in:
Loïc Wisniewski 2026-08-16 20:14:29 +02:00
commit 3f3a22227d
43 changed files with 459 additions and 30 deletions

View file

@ -8,7 +8,13 @@ extends Node2D
# ... and again at 720x960 to check a shorter screen format.
func _ready() -> void:
var configured: bool = false
func _process(_delta: float) -> void:
if configured:
return
configured = true
GameState.buildings.clear()
GameState.extractors.clear()
GameState.transporters.clear()
@ -16,9 +22,12 @@ func _ready() -> void:
GameState.gas = 36
GameState.fleet_cargo = 24
GameState.fleet_gas_cargo = 12
GameState.unlocked = ["shipyards", "logistics", "freight"]
GameState.research_points = 0
GameState.unlocked = ["shipyards"]
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
main.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0))
var mothership_id: int = int(GameState.buildings[-1].id)
var bc: Node = main.get_node("BuildController")
@ -27,5 +36,5 @@ func _ready() -> void:
main.get_node("UI/SkillMenu").visible = true
# Select a node so the description, price and status all have text - an
# empty description cannot collide with anything.
bc.skill_tree.selected_id = "freight"
bc._refresh_skill_menu("freight")
bc.skill_tree.selected_id = "coordination"
bc._refresh_skill_menu("coordination")

View file

@ -48,6 +48,7 @@ func _initialize() -> void:
game_state.reset_progression()
game_state.build_time_scale = 0.02
game_state.rocks = 500
game_state.research_points = 1
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")

View file

@ -21,6 +21,7 @@ func _initialize() -> void:
game_state.extractors.clear()
game_state.transporters.clear()
game_state.reset_progression()
game_state.research_points = 1
game_state.add_extractor("Test Planet", EXTRACTOR_AT)
extractor_index = 0

View file

@ -27,6 +27,7 @@ func _initialize() -> void:
game_state.fleet_cargo = 0
game_state.fleet_gas_cargo = 0
game_state.build_time_scale = 0.01
game_state.research_points = 1
# #60: after research/crafting resets the fleet hold below, top it up
# with fleet-carried gas for the shot itself (ammunition, not the ore/gas
# spent researching or crafting the bomber).

View file

@ -10,6 +10,7 @@ func _initialize() -> void:
game.vessel_levels.clear()
game.build_time_scale = 0.02
game.rocks = 500
game.research_points = 1
lab_id = game.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO)
if not game.queue_lab_research(lab_id, "int_damage"):
_fail("could not queue research")

View file

@ -12,6 +12,7 @@ func _initialize() -> void:
state.transporters.clear()
state.reset_progression()
state.rocks = 100
state.research_points = 1
state.build_time_scale = 0.02
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))

View file

@ -10,6 +10,7 @@ func _initialize() -> void:
state.reset_progression()
state.rocks = 0
state.gas = 10
state.research_points = 1
state.build_time_scale = 0.01
state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)

View file

@ -0,0 +1,143 @@
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 exactly one point, once (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://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://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()
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()
game_state.register_capture_points("test_lab_planet", 2, "research_lab")
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 exactly 1 point: %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 1 point and fired the signal 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)

View file

@ -81,6 +81,7 @@ func _process(delta: float) -> bool:
print("no lab -> no upgrades")
game_state.unlocked = ["shipyards", "vessel_science", "coordination", "wing", "squadron"]
game_state.research_points = 10
field.place(GameState.BUILDING_SHIP_LAB, Vector2(-700.0, 0.0))
field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(700.0, 0.0))
crafter_id = int(game_state.buildings[-1].id)

View file

@ -155,6 +155,7 @@ func _process(delta: float) -> bool:
# --- fleet capacity -------------------------------------------
var base_cap: int = game_state.max_fleet_size()
game_state.research_points = 2
game_state.unlock("coordination")
var wing_ok: bool = game_state.unlock("wing")
var raised_cap: int = game_state.max_fleet_size()