Offer only orderable vessels, and read out crew condition

A ship crafter shows what can actually be ordered there: a hull whose
research is done, and - for the support craft - one with somewhere to be
assigned. A transporter with no hub and a drone with no workshop have
nowhere to go and would sit in the hangar as spent resources, so the
structure that uses one is part of its availability. Hidden rather than
greyed, as the build menu already treats an unresearched structure, and
gated in queue_order() too so the model refuses what the menu declines to
show.

The craft buttons bound their handlers to a definition captured at startup.
With a list that changes length that is a live bug - a slot would order
whatever hull was there when the menu was built - so they bind to the slot
and resolve through the current offer.

The hub and workshop crew menus now NAME what is left in each vessel:
"TRANSPORTER 2  9 / 20 RUNS", worst first, with one wide bar for the vessel
closest to spent. The per-vessel bars they replaced drew about seven pixels
tall on a phone and carried no figure, so the number the player is deciding
on could not be read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
Loïc Wisniewski 2026-09-06 11:58:15 +02:00
commit 296b49c8b8
10 changed files with 359 additions and 41 deletions

View file

@ -44,6 +44,17 @@ func _initialize() -> void:
# Office. This test flies more than one vessel, so it needs the offices
# standing before the fleet spawns into them.
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
# The crafter shows only what can be ordered there, so the vessels this
# test inspects have to be orderable: every hull researched, and a hub and
# a workshop standing for the two support craft. Section E in particular
# needs a TWO-resource price on offer - it is about a price that used to
# run off the end of its strip - and every ore-only hull would prove
# nothing.
for definition in load("res://vessels/VesselDefinitions.gd").craftable():
if String(definition.unlock_id) != "":
game_state.vessel_levels[String(definition.unlock_id)] = 1
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(4000.0, 4000.0))
game_state.add_building(GameState.BUILDING_WORKSHOP, Vector2(4600.0, 4000.0))
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
@ -72,10 +83,18 @@ func _process(delta: float) -> bool:
# exact match rather than by hunting for resource words in it: a
# Gas Bomber is named after a resource and always will be, and
# what #111 removed was the PRICE spelled out underneath.
var fleet_defs: Array = VesselDefinitions.craftable()
for i in range(mini(controller.craft_menu.vessel_labels.size(), fleet_defs.size())):
# Read against what is ON OFFER, not the whole catalogue: a hull
# that is unresearched, or a support craft with nowhere to be
# assigned, is hidden rather than greyed, so slot i holds the ith
# OFFERED vessel and the hidden slots keep whatever text the
# scene was authored with.
var offered: PackedStringArray = controller.craft_menu.shown_types
if offered.is_empty():
return _fail("the crafter offers nothing at all, so this proves nothing")
for i in range(offered.size()):
var caption: String = String(controller.craft_menu.vessel_labels[i].text)
var expected: String = String(fleet_defs[i].display_name)
var expected: String = String(VesselDefinitions.get_definition(
String(offered[i])).display_name)
if caption != expected:
return _fail("a craft caption reads \"%s\", expected just \"%s\"" \
% [caption, expected])
@ -97,10 +116,10 @@ func _process(delta: float) -> bool:
controller.craft_menu.vessel_buttons.size()])
var priced: int = 0
var stacked: int = 0
for i in range(controller.craft_menu.price_tags.size()):
# #216 added the two support craft to the crafter, so the rows
# are craftable() now - fleet hulls first, then those.
var definition: Resource = VesselDefinitions.craftable()[i]
# Over the OFFERED vessels, for the same reason as the captions
# above: a hidden strip belongs to no vessel.
for i in range(mini(offered.size(), controller.craft_menu.price_tags.size())):
var definition: Resource = VesselDefinitions.get_definition(String(offered[i]))
var costs: Dictionary = game_state.product_costs(definition.definition_id)
var strip: Control = controller.craft_menu.price_tags[i]
if costs.is_empty():