Complete lab, quest, garrison, and energy tickets

This commit is contained in:
Loïc Wisniewski 2026-09-03 00:12:08 +02:00
commit 0f7edff15a
20 changed files with 238 additions and 45 deletions

View file

@ -1,7 +1,7 @@
extends Node2D
# Preview, not a test - the ship lab at the real 720x1280 viewport, with a
# vessel selected and its upgrades listed.
# vessel selected, its upgrades listed, and active research in progress.
#
# bash tests/screenshot.sh res://tests/_lab_menu_preview.tscn /tmp/lab.png 25 720x1280
@ -13,10 +13,21 @@ func _ready() -> void:
GameState.bank_inventory.set_amount(GameState.RESOURCE_ORE, 310)
GameState.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 47)
GameState.research_points = 4
GameState.build_time_scale = 1.0
var lab_id: int = GameState.add_building(GameState.BUILDING_SHIP_LAB,
Vector2.ZERO)
GameState.queue_lab_research(lab_id, "int_rate")
GameState.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE,
GameState.upgrade_cost("int_rate"))
GameState.call("_advance_lab_research", GameState.lab_research_time() * 0.42)
# Freeze the model so a slow software-rendered capture cannot finish the
# research before it takes the frame this preview exists to show.
GameState.set_process(false)
var main: Node = load("res://world/Main.tscn").instantiate()
add_child(main)
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
var build: Node = main.get_node("BuildController")
build.lab_menu.visible = true
build.lab_building_id = lab_id
build.lab_menu.open_for(lab_id)
build._refresh_lab()

View file

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

View file

@ -53,6 +53,8 @@ func _initialize() -> void:
var logistics: Resource = game_state.building_logistics_profile(game_state.find_building(burner_id))
conversion_rate = logistics.conversion_rate
conversion_yield = logistics.conversion_yield
if not is_equal_approx(conversion_rate, 0.2):
_fail("burner conversion rate is %.2f/s, expected the reduced 0.20/s" % conversion_rate)
func _process(delta: float) -> bool:
@ -72,13 +74,13 @@ func _process(delta: float) -> bool:
print("energy is generic: in resource_types(), own definition, own colour %s" % energy_color)
# --- B. produces energy from local gas at the configured rate -
if elapsed < 2.5:
if elapsed < 5.5:
return false
var gas_left: int = game_state.building_resource(burner_id, GameState.RESOURCE_GAS)
var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
var gas_burned: int = 20 - gas_left
if gas_burned < 2:
return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, conversion_rate])
if gas_burned < 1:
return _fail("burner burned no gas in 5.5s at rate %.2f/s" % conversion_rate)
if energy_made != gas_burned * conversion_yield:
return _fail("burner made %d energy for %d gas burned, expected %dx" % [
energy_made, gas_burned, conversion_yield

View file

@ -84,7 +84,7 @@ func _process(delta: float) -> bool:
return false
1:
# --- B. running, it converts ----------------------------------
if elapsed < 1.5:
if elapsed < 5.5:
return false
if _stock(GameState.RESOURCE_ENERGY) <= 0:
return _fail("a running burner made no energy in %.1fs" % elapsed)
@ -119,7 +119,7 @@ func _process(delta: float) -> bool:
elapsed = 0.0
return false
3:
if elapsed < 1.5:
if elapsed < 5.5:
return false
if _stock(GameState.RESOURCE_ENERGY) <= energy_at_pause:
return _fail("a resumed burner made no further energy")

View file

@ -21,6 +21,8 @@ extends SceneTree
# E. it goes back to patrolling once the enemy is gone
# F. losing a garrison vessel frees the berth, so it gets crewed again
# G. enemies fight back - a garrison vessel is a target like any other
# H. #186 - with no hostile nearby, the same weapon-target contract lets a
# garrison shoot asteroids for ore
var game_state: Node
var main: Node
@ -30,6 +32,7 @@ var outpost_id: int = 0
var guard: Node2D
var component: Node
var enemy: Node2D
var asteroid: Node2D
var stage: int = 0
var elapsed: float = 0.0
var orbit_min: float = INF
@ -231,14 +234,40 @@ func _process(delta: float) -> bool:
return _fail("the garrison is still fighting an enemy that is gone")
print("stood down and went back to its patrol")
# --- H. asteroids are valid lower-priority targets -----------
asteroid = load("res://world/Asteroid.tscn").instantiate()
asteroid.position = Vector2(300.0, 0.0)
asteroid.velocity = Vector2.ZERO
main.add_child(asteroid)
asteroid.hp = 100000
dummy_hp_before = asteroid.hp
elapsed = 0.0
stage = 6
return false
6:
if elapsed < 2.5:
return false
if not component.is_engaging():
return _fail("an asteroid beside the outpost was not engaged")
if asteroid.hp >= dummy_hp_before:
return _fail("the garrison engaged an asteroid but never shot it")
print("garrison shot a nearby asteroid through the shared target contract")
asteroid.queue_free()
elapsed = 0.0
stage = 7
return false
7:
if elapsed < 0.5:
return false
# --- F. a lost vessel frees its berth ------------------------
if game_state.garrison_crew(outpost_id).size() != 1:
return _fail("the crew is not 1 before the kill")
guard.take_damage(1)
elapsed = 0.0
stage = 6
stage = 8
return false
6:
8:
if elapsed < 0.3:
return false
if not game_state.garrison_crew(outpost_id).is_empty():

View file

@ -133,6 +133,23 @@ func _catalogue_cases() -> bool:
return _fail("the quest after the mothership is %s, not exploration" % after.id)
print("the mothership answers with distance, and exploration follows it")
# #185: the final wide search has a propulsion lesson immediately before
# it. The goal uses the stable vessel id while the player-facing copy uses
# the renamed Energy Propulser name.
var trail: Dictionary = QUESTS.by_id("trail")
var trail_at: int = QUESTS.CHAIN.find(trail)
if trail_at <= 0:
return _fail("the Chart 150 quest is missing or opens the chain")
var before_trail: Dictionary = QUESTS.at(trail_at - 1)
if String(before_trail.id) != "propulser" \
or String(before_trail.goal.kind) != QUESTS.GOAL_FLEET_VESSEL \
or String(before_trail.goal.vessel) != GameState.VESSEL_DISTORDER:
return _fail("Chart 150 is preceded by %s, not the Energy Propulser vessel quest" \
% before_trail.get("id", "nothing"))
if not "Energy Propulser" in String(before_trail.objective):
return _fail("the propulsion quest does not name the Energy Propulser to the player")
print("the Energy Propulser vessel quest leads directly into Chart 150")
# --- R. #145: every structure quest can actually be finished ---------
# The extractor quest could not be. Extractors are placed onto a planet
# from space and live in their own array, not in `buildings`, so the

View file

@ -25,6 +25,7 @@ var scene: Node
var fleet: Node
var field: Node
var controller: Node
var lab_id: int = 0
var crafter_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
@ -83,6 +84,7 @@ func _process(delta: float) -> bool:
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))
lab_id = int(game_state.buildings[-1].id)
field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(700.0, 0.0))
crafter_id = int(game_state.buildings[-1].id)
elapsed = 0.0
@ -130,6 +132,29 @@ func _process(delta: float) -> bool:
return true
print("lab cards are vessel-specific and selectable")
# #184: a queued research uses the bar as its changing value. The
# status names the state but never repeats it as a percentage.
var lab_progress: ProgressBar = controller.get_node(
"../UI/LabMenu/ResearchProgress")
var lab_status: Label = controller.get_node("../UI/LabMenu/StatusLabel")
controller.lab_building_id = lab_id
if not game_state.queue_lab_research(lab_id, "int_rate"):
return _fail("could not queue research for the progress-bar check")
controller.call("_refresh_lab")
if not lab_progress.visible or lab_progress.show_percentage:
return _fail("pending research did not show a percentage-free progress bar")
if "%" in lab_status.text:
return _fail("pending research still prints a percentage: '%s'" % lab_status.text)
game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE,
game_state.upgrade_cost("int_rate"))
game_state.call("_advance_lab_research", game_state.lab_research_time() * 0.25)
controller.call("_refresh_lab")
if lab_progress.value < 0.2 or lab_progress.value > 0.3 \
or lab_status.text != "Researching":
return _fail("research bar did not track active work (value %.2f, status '%s')" \
% [lab_progress.value, lab_status.text])
print("pending lab research is shown by a live progress bar, without percentage text")
# --- 3. buying raises the level and charges ------------------
var before_rocks: int = game_state.rocks
var first_cost: int = game_state.upgrade_cost("int_damage")
@ -225,3 +250,9 @@ func _process(delta: float) -> bool:
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_SHIP_LAB: FAIL (%s)" % message)
quit(1)
return true