Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
extends SceneTree
|
|
|
|
|
|
2026-08-14 21:44:28 +02:00
|
|
|
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
|
|
|
|
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
# Headless gameplay test: run with
|
|
|
|
|
# godot --headless -s res://tests/test_transport_hub_building.gd
|
|
|
|
|
# Forgejo #18 (part 5 of #12): the Transport Hub is a placeable building of
|
|
|
|
|
# its own. Only the building here - the vessels that do the hauling are #19.
|
|
|
|
|
#
|
|
|
|
|
# Verifies:
|
|
|
|
|
# 1. it is gated behind its research node, like every other structure
|
|
|
|
|
# 2. researching it makes it buildable, and placing it charges its cost
|
|
|
|
|
# 3. it is drawn as ITSELF, not as the crafter - Building._draw()'s match
|
|
|
|
|
# has a catch-all that renders anything unrecognised as a ship crafter,
|
|
|
|
|
# so a missing case is silent and looks almost right
|
|
|
|
|
# 4. its geometry is rasterised ONCE however long it animates (#10), and
|
|
|
|
|
# its stationed transports really do move
|
|
|
|
|
# 5. it has its own build-menu icon and its own map marker rather than
|
|
|
|
|
# borrowing another building's - the menu was once unreadable because
|
|
|
|
|
# two buildings shared a glyph
|
|
|
|
|
|
|
|
|
|
var game_state: Node
|
|
|
|
|
var scene: Node
|
|
|
|
|
var field: Node
|
|
|
|
|
var controller: Node
|
|
|
|
|
var map_content: Node2D
|
|
|
|
|
var hub: Node2D
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
var spin_at_start: float = 0.0
|
|
|
|
|
|
|
|
|
|
const HUB_AT: Vector2 = Vector2(700.0, 0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
game_state = root.get_node("GameState")
|
|
|
|
|
game_state.buildings.clear()
|
|
|
|
|
game_state.extractors.clear()
|
|
|
|
|
game_state.reset_progression()
|
|
|
|
|
game_state.rocks = 500
|
|
|
|
|
game_state.fleet_cargo = 0
|
|
|
|
|
scene = load("res://Main.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
field = scene.get_node("BuildingField")
|
|
|
|
|
controller = scene.get_node("BuildController")
|
|
|
|
|
map_content = scene.get_node("UI/WorldMap/MapContent")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
|
|
|
elapsed += delta
|
|
|
|
|
match stage:
|
|
|
|
|
0:
|
|
|
|
|
scene.get_node("AsteroidField").set_process(false)
|
|
|
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
|
|
|
|
|
|
|
|
# --- 1. locked until researched ------------------------------
|
|
|
|
|
if game_state.is_building_unlocked(GameState.BUILDING_TRANSPORT_HUB):
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (buildable with no research at all)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
var gate: String = game_state.research_for_building(GameState.BUILDING_TRANSPORT_HUB)
|
|
|
|
|
if gate == "":
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (no research node gates it)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("locked behind research: '%s'" % gate)
|
|
|
|
|
|
|
|
|
|
# --- 5. its own icon, distinct from every other building -----
|
|
|
|
|
var icons: Dictionary = {}
|
|
|
|
|
for type in [GameState.BUILDING_MOTHERSHIP, GameState.BUILDING_SHIP_CRAFTER,
|
|
|
|
|
GameState.BUILDING_COMMAND_CENTER, GameState.BUILDING_TRANSPORT_HUB]:
|
2026-08-14 21:44:28 +02:00
|
|
|
var icon: int = BUILDING_DEFINITIONS.get_definition(type).action_icon
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
if icons.has(icon):
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (%s and %s share an icon)" % [type, icons[icon]])
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
icons[icon] = type
|
|
|
|
|
print("action icons distinct across %d buildings" % icons.size())
|
|
|
|
|
|
|
|
|
|
# --- 2. research it, then build it ---------------------------
|
|
|
|
|
game_state.unlocked = ["shipyards", "logistics", "freight"]
|
|
|
|
|
if not game_state.is_building_unlocked(GameState.BUILDING_TRANSPORT_HUB):
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (still locked after researching '%s')" % gate)
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
var before: int = game_state.rocks
|
|
|
|
|
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
|
|
|
|
game_state.spend(controller.cost_of(GameState.BUILDING_TRANSPORT_HUB))
|
|
|
|
|
if game_state.rocks != before - GameState.TRANSPORT_HUB_COST:
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (cost %d charged, expected %d)" % [
|
|
|
|
|
before - game_state.rocks, GameState.TRANSPORT_HUB_COST
|
|
|
|
|
])
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("researched and placed for %d ore" % GameState.TRANSPORT_HUB_COST)
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 1
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
for b in field.spawned:
|
|
|
|
|
if is_instance_valid(b) and b.building_type == GameState.BUILDING_TRANSPORT_HUB:
|
2026-08-14 21:44:28 +02:00
|
|
|
hub = b.visual
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
if hub == null:
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# --- 3. drawn as itself, not via the crafter catch-all -------
|
|
|
|
|
# _hub_traffic only exists if _ready() took the transport-hub
|
|
|
|
|
# branch; a missing case falls through to the crafter and would
|
|
|
|
|
# leave it null while still rendering something plausible.
|
|
|
|
|
if hub._hub_traffic == null:
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (rendered through the crafter catch-all, not as a hub)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# --- 5. its own map marker ----------------------------------
|
|
|
|
|
var markers: Array = map_content.building_markers()
|
|
|
|
|
var found: bool = false
|
|
|
|
|
for m in markers:
|
|
|
|
|
if m.type == GameState.BUILDING_TRANSPORT_HUB:
|
|
|
|
|
found = true
|
2026-08-14 21:44:28 +02:00
|
|
|
if m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_SHIP_CRAFTER).map_color \
|
|
|
|
|
or m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_MOTHERSHIP).map_color \
|
|
|
|
|
or m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_COMMAND_CENTER).map_color:
|
Transport Hub building
Forgejo #18, part 5 of #12. The building only - the vessels that do the
hauling are #19. Placeable, costed at 30, gated behind a new "Freight
Handling" research node hanging off Logistics, with its own info popup,
build-menu glyph and map marker.
Drawn as an octagonal dock ring with berth arms and a squared-off freight
core: deliberately radial-but-flat so it does not read as a small
mothership, which is the only structure with swept arms. Its stationed
transports are NeonPiece children of one rotating holder, per #10 - the
geometry is rasterised once and only the holder's rotation changes, so
the traffic costs no redraws.
Building._draw()'s match has a catch-all that renders anything
unrecognised as a ship crafter, so a missing case is silent and looks
almost right. The test checks for the hub's own _hub_traffic holder
rather than trusting that something plausible appeared - which caught
exactly that: a _ready() patch had silently not applied and the hub was
rendering through the crafter branch.
The build menu grew a third row. Every button, label and price moved:
the old row occupied 332px against a 372px pitch, and three of those do
not fit above a 1280px screen. Rows are now shorter (128px buttons, 40px
price digits) at a 290px pitch, ending at y=1176. Rendered and looked at
- the first attempt tightened the pitch alone and left every price
overlapping the row beneath it, which no headless test would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:45:07 +02:00
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (map marker reuses another building's colour)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
if not found:
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (no map marker - see #11)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("has its own map marker")
|
|
|
|
|
|
|
|
|
|
spin_at_start = hub._hub_traffic.rotation
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 2
|
|
|
|
|
return false
|
|
|
|
|
2:
|
|
|
|
|
# --- 4. animates without ever redrawing (#10) ----------------
|
|
|
|
|
if elapsed < 2.0:
|
|
|
|
|
return false
|
|
|
|
|
print("rasterisations after %.1fs: %d" % [elapsed, hub.draw_count])
|
|
|
|
|
if hub.draw_count != 1:
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (redrew its geometry %d times to animate)" % hub.draw_count)
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
var spun: float = absf(hub._hub_traffic.rotation - spin_at_start)
|
|
|
|
|
print("stationed transports turned %.3f rad" % spun)
|
|
|
|
|
if spun < 0.05:
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (nothing moved - a frozen hub trivially passes the redraw check)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("TEST_TRANSPORT_HUB_BUILDING: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|