diff --git a/BuildController.gd b/BuildController.gd index 4747cf1..e291e76 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -27,6 +27,7 @@ extends Node2D @onready var build_close_button: IconButton = get_node("../UI/BuildMenu/CloseButton") @onready var build_status: Label = get_node("../UI/BuildMenu/StatusLabel") @onready var build_extractor_button: IconButton = get_node("../UI/BuildMenu/ExtractorButton") +@onready var build_hub_button: IconButton = get_node("../UI/BuildMenu/TransportHubButton") @onready var planet_field: Node2D = get_node("../PlanetField") @onready var info_popup: Control = get_node("../UI/BuildMenu/InfoPopup") @onready var info_title: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/TitleLabel") @@ -76,6 +77,10 @@ const BUILDING_INFO: Dictionary = { "title": "Command Center", "body": "Fly the fleet onto it to drag your fleet into a new formation. Rebuilt fleets keep it.", }, + GameState.BUILDING_TRANSPORT_HUB: { + "title": "Transport Hub", + "body": "Stations transport vessels. They fetch ore from your mothership and deliver it to whichever building is waiting on it.", + }, GameState.BUILDING_EXTRACTOR: { "title": "Extractor", "body": "Mines a planet and runs a hauler to your mothership. It keeps earning while you are away.", @@ -100,6 +105,7 @@ func _ready() -> void: build_crafter_button.pressed.connect(_on_place_crafter) build_command_button.pressed.connect(_on_place_command_center) build_extractor_button.pressed.connect(_on_place_extractor) + build_hub_button.pressed.connect(_on_place_transport_hub) info_close_button.pressed.connect(_close_info) info_build_button.pressed.connect(_confirm_build) @@ -194,6 +200,8 @@ func _action_icon_for(type: String) -> int: match type: GameState.BUILDING_SHIP_CRAFTER: return IconButton.Icon.SHIP + GameState.BUILDING_TRANSPORT_HUB: + return IconButton.Icon.TRANSPORT_HUB GameState.BUILDING_MOTHERSHIP: return IconButton.Icon.MOTHERSHIP _: @@ -243,6 +251,7 @@ func _refresh_build_status() -> void: # explain itself in the info popup. build_crafter_button.enabled = GameState.is_building_unlocked(GameState.BUILDING_SHIP_CRAFTER) build_command_button.enabled = GameState.is_building_unlocked(GameState.BUILDING_COMMAND_CENTER) + build_hub_button.enabled = GameState.is_building_unlocked(GameState.BUILDING_TRANSPORT_HUB) build_extractor_button.enabled = _planet_under_fleet() != null \ and GameState.is_building_unlocked(GameState.BUILDING_EXTRACTOR) @@ -266,6 +275,10 @@ func _on_place_command_center() -> void: _show_building_info(GameState.BUILDING_COMMAND_CENTER) +func _on_place_transport_hub() -> void: + _show_building_info(GameState.BUILDING_TRANSPORT_HUB) + + func _on_place_extractor() -> void: # The option stays pressable while greyed, so this is also how a # not-over-a-planet extractor explains itself. @@ -274,6 +287,8 @@ func _on_place_extractor() -> void: func cost_of(type: String) -> int: match type: + GameState.BUILDING_TRANSPORT_HUB: + return GameState.TRANSPORT_HUB_COST GameState.BUILDING_MOTHERSHIP: return GameState.MOTHERSHIP_COST GameState.BUILDING_SHIP_CRAFTER: diff --git a/Building.gd b/Building.gd index 6dfc0b4..54e5d1a 100644 --- a/Building.gd +++ b/Building.gd @@ -28,6 +28,19 @@ extends Node2D const MOTHERSHIP_COLOR: Color = Color(0.45, 0.9, 0.55) const CRAFTER_COLOR: Color = Color(1.0, 0.72, 0.25) const COMMAND_COLOR: Color = Color(0.55, 0.6, 1.0) +const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85) + +# Transport hub: an octagonal dock ring with berths around it and a freight +# core in the middle. Deliberately radial-but-flat, so it does not read as a +# small mothership - the mothership is the only thing with swept arms. +const HUB_RING_RADIUS: float = 74.0 +const HUB_DECK_RADIUS: float = 54.0 +const HUB_CORE_RADIUS: float = 22.0 +const HUB_BERTH_COUNT: int = 8 +const HUB_BERTH_LENGTH: float = 20.0 +const HUB_DOCKED_COUNT: int = 3 +const HUB_DOCKED_ORBIT: float = 96.0 +const HUB_SPIN_SPEED: float = 0.22 # The vessel on the assembly line is drawn in the fleet's own colour, so it # reads as "one of yours being built" rather than as shipyard machinery. const HULL_COLOR: Color = Color(0.25, 0.85, 1.0) @@ -85,6 +98,8 @@ var _hoist: NeonPiece = null var _sparks: Node2D = null var _hull_holder: Node2D = null var _hull_pieces: Array = [] +# Rotating holder for the transports stationed at a transport hub (#18). +var _hub_traffic: Node2D = null var _flash_ring: NeonPiece = null var _wash: NeonPiece = null @@ -103,6 +118,8 @@ func _ready() -> void: _build_crafter_pieces() elif building_type == GameState.BUILDING_MOTHERSHIP: _build_mothership_pieces() + elif building_type == GameState.BUILDING_TRANSPORT_HUB: + _build_hub_pieces() queue_redraw() @@ -121,6 +138,8 @@ func apply_animation() -> void: _animate_mothership() GameState.BUILDING_SHIP_CRAFTER: _animate_crafter() + GameState.BUILDING_TRANSPORT_HUB: + _animate_transport_hub() # Where the satellites are, and how open the reactor core is. Both are read @@ -161,6 +180,8 @@ func _draw() -> void: _draw_command_center() GameState.BUILDING_MOTHERSHIP: _draw_mothership_static(self) + GameState.BUILDING_TRANSPORT_HUB: + _draw_transport_hub_static(self) _: _draw_crafter_static(self) @@ -203,6 +224,50 @@ const CMD_TOKEN_RADIUS: float = 9.0 const CMD_STUD_RADIUS: float = 7.0 +# --- transport hub -------------------------------------------------------- + +func _draw_transport_hub_static(target: CanvasItem) -> void: + var c: Color = TRANSPORT_HUB_COLOR + + # Dock ring and the deck nested inside it. + NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_RING_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 1.0) + NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_DECK_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 0.45, NeonDraw.DEFAULT_CORE, 0.7) + + # Freight core: a squared-off hold rather than a reactor, so it reads as + # storage and not as another power plant. + NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_CORE_RADIUS, 4, PI / 4.0), c, 0.95) + + for i in range(HUB_BERTH_COUNT): + var a: float = (float(i) / HUB_BERTH_COUNT) * TAU + PI / 8.0 + var dir: Vector2 = Vector2(cos(a), sin(a)) + NeonDraw.draw_glowing_line(target, dir * HUB_CORE_RADIUS, dir * HUB_DECK_RADIUS, c, 0.5, 0.32) + # Berth arm standing off the ring, where a transporter parks. + NeonDraw.draw_glowing_line(target, dir * HUB_RING_RADIUS, + dir * (HUB_RING_RADIUS + HUB_BERTH_LENGTH), c, 0.75, 0.38) + + +func _build_hub_pieces() -> void: + # Stationed transports, as NeonPiece children of one holder that turns. + # Per #10 the geometry is rasterised once and only the holder's rotation + # changes, so the traffic costs no redraws at all. + _hub_traffic = Node2D.new() + add_child(_hub_traffic) + for i in range(HUB_DOCKED_COUNT): + var a: float = (float(i) / HUB_DOCKED_COUNT) * TAU + var at: Vector2 = Vector2(cos(a), sin(a)) * HUB_DOCKED_ORBIT + var piece: NeonPiece = NeonPiece.create(_hub_traffic, at, TRANSPORT_HUB_COLOR) + # A stubby freighter: blunt, boxy, nothing like the fleet's darts. + piece.add_polygon(PackedVector2Array([ + Vector2(-9, -5), Vector2(9, -5), Vector2(12, 0), Vector2(9, 5), Vector2(-9, 5), + ])) + piece.rotation = a + PI / 2.0 + piece.queue_redraw() + + +func _animate_transport_hub() -> void: + _hub_traffic.rotation = phase_time * HUB_SPIN_SPEED + + func _diamond(centre: Vector2, r: float) -> PackedVector2Array: return PackedVector2Array([ centre + Vector2(0, -r), centre + Vector2(r, 0), diff --git a/GameState.gd b/GameState.gd index a7a793f..8e6827e 100644 --- a/GameState.gd +++ b/GameState.gd @@ -101,9 +101,13 @@ const BUILDING_COMMAND_CENTER: String = "command_center" # Extractors are not placed in space like the others - they are built ON a # planet's surface and tracked separately (see `extractors`). const BUILDING_EXTRACTOR: String = "extractor" +# Hosts the transport vessels that supply other buildings (#18). Built in +# space like the rest; it is the transporters (#19) that do the moving. +const BUILDING_TRANSPORT_HUB: String = "transport_hub" const COMMAND_CENTER_COST: int = 15 const EXTRACTOR_COST: int = 25 +const TRANSPORT_HUB_COST: int = 30 var buildings: Array = [] @@ -136,6 +140,11 @@ const SKILL_TREE: Array = [ "x": 0.82, "unlocks": BUILDING_COMMAND_CENTER, "desc": "Build command centers, where the fleet formation is arranged.", }, + { + "id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics", + "x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, + "desc": "Build transport hubs, which supply your other buildings with ore.", + }, { "id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination", "x": 0.82, "fleet_bonus": 1, diff --git a/IconButton.gd b/IconButton.gd index 93cc525..2fdc552 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -15,7 +15,7 @@ enum Icon { # build menu used to pick from the generic set, which left the mothership # and the command centre sharing a folded-map glyph and neither of them # looking anything like the structure they build. - MOTHERSHIP, CRAFTER, COMMAND_CENTER, EXTRACTOR, + MOTHERSHIP, CRAFTER, COMMAND_CENTER, EXTRACTOR, TRANSPORT_HUB, } # Size the glyphs were originally drawn against. Every glyph dimension is @@ -119,6 +119,8 @@ func _draw() -> void: _draw_command_icon(center, pulse, s) Icon.EXTRACTOR: _draw_extractor_icon(center, pulse, s) + Icon.TRANSPORT_HUB: + _draw_transport_hub_icon(center, pulse, s) func _draw_close(center: Vector2, pulse: float, s: float) -> void: @@ -204,6 +206,32 @@ func _draw_mothership_icon(center: Vector2, pulse: float, s: float) -> void: NeonDraw.draw_glowing_polygon(self, ring, _active_color(), pulse, core_color) +func _draw_transport_hub_icon(center: Vector2, pulse: float, s: float) -> void: + # A ring of docking cradles with a freight box in the middle: reads as + # "things are stationed here and go out from here", which is what the hub + # does, and shares no silhouette with the crafter's open cradle. + var ring_r: float = 11.0 * s + var ring: PackedVector2Array = PackedVector2Array() + for i in range(8): + var a: float = (float(i) / 8.0) * TAU + PI / 8.0 + ring.append(center + Vector2(cos(a), sin(a)) * ring_r) + NeonDraw.draw_glowing_polygon(self, ring, _active_color(), pulse, core_color) + + var box: float = 4.2 * s + NeonDraw.draw_glowing_polygon(self, PackedVector2Array([ + center + Vector2(-box, -box), center + Vector2(box, -box), + center + Vector2(box, box), center + Vector2(-box, box), + ]), _active_color(), pulse, core_color) + + # Three stubby berths, so the ring reads as occupied rather than as a + # plain octagon. + for i in range(3): + var a: float = (float(i) / 3.0) * TAU - PI / 2.0 + var dir: Vector2 = Vector2(cos(a), sin(a)) + NeonDraw.draw_glowing_line(self, center + dir * (box * 1.5), + center + dir * (ring_r - 1.5 * s), _active_color(), pulse, 0.35 * s, core_color) + + func _draw_crafter_icon(center: Vector2, pulse: float, s: float) -> void: # Reactor core, truss spine, and an open cradle holding a hull. var core_centre: Vector2 = center + Vector2(-11, 0) * s diff --git a/Main.tscn b/Main.tscn index dca95e7..38e9f7e 100644 --- a/Main.tscn +++ b/Main.tscn @@ -270,10 +270,10 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -214 -offset_right = -66 -offset_top = -254 -offset_bottom = -106 +offset_left = -204 +offset_right = -76 +offset_top = -300 +offset_bottom = -172 script = ExtResource("10") icon = 8 glow_color = Color(0.45, 0.9, 0.55, 1) @@ -286,9 +286,9 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -290 offset_right = 10 -offset_top = -96 -offset_bottom = 4 -theme_override_font_sizes/font_size = 36 +offset_top = -166 +offset_bottom = -100 +theme_override_font_sizes/font_size = 32 autowrap_mode = 2 text = "Mothership" horizontal_alignment = 1 @@ -299,23 +299,23 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -204 -offset_right = -64 -offset_top = 14 -offset_bottom = 78 +offset_left = -200 +offset_right = -80 +offset_top = -96 +offset_bottom = -44 script = ExtResource("19") amount = 20 -digit_height = 52.0 +digit_height = 40.0 [node name="CrafterButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 66 -offset_right = 214 -offset_top = -254 -offset_bottom = -106 +offset_left = 76 +offset_right = 204 +offset_top = -300 +offset_bottom = -172 script = ExtResource("10") icon = 9 glow_color = Color(1, 0.72, 0.25, 1) @@ -328,9 +328,9 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -10 offset_right = 290 -offset_top = -96 -offset_bottom = 4 -theme_override_font_sizes/font_size = 36 +offset_top = -166 +offset_bottom = -100 +theme_override_font_sizes/font_size = 32 autowrap_mode = 2 text = "Ship Crafter" horizontal_alignment = 1 @@ -341,23 +341,23 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 76 -offset_right = 216 -offset_top = 14 -offset_bottom = 78 +offset_left = 80 +offset_right = 200 +offset_top = -96 +offset_bottom = -44 script = ExtResource("19") amount = 12 -digit_height = 52.0 +digit_height = 40.0 [node name="CommandButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -214 -offset_right = -66 -offset_top = 118 -offset_bottom = 266 +offset_left = -204 +offset_right = -76 +offset_top = -10 +offset_bottom = 118 script = ExtResource("10") icon = 10 glow_color = Color(0.55, 0.6, 1, 1) @@ -370,9 +370,9 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -290 offset_right = 10 -offset_top = 276 -offset_bottom = 376 -theme_override_font_sizes/font_size = 36 +offset_top = 124 +offset_bottom = 190 +theme_override_font_sizes/font_size = 32 autowrap_mode = 2 text = "Command Center" horizontal_alignment = 1 @@ -383,23 +383,23 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -204 -offset_right = -64 -offset_top = 386 -offset_bottom = 450 +offset_left = -200 +offset_right = -80 +offset_top = 194 +offset_bottom = 246 script = ExtResource("19") amount = 15 -digit_height = 52.0 +digit_height = 40.0 [node name="ExtractorButton" type="Control" parent="UI/BuildMenu"] anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 66 -offset_right = 214 -offset_top = 118 -offset_bottom = 266 +offset_left = 76 +offset_right = 204 +offset_top = -10 +offset_bottom = 118 script = ExtResource("10") icon = 11 glow_color = Color(0, 0.62, 0.7, 1) @@ -412,9 +412,9 @@ anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -10 offset_right = 290 -offset_top = 276 -offset_bottom = 376 -theme_override_font_sizes/font_size = 36 +offset_top = 124 +offset_bottom = 190 +theme_override_font_sizes/font_size = 32 autowrap_mode = 2 text = "Extractor" horizontal_alignment = 1 @@ -425,13 +425,55 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 76 -offset_right = 216 -offset_top = 386 -offset_bottom = 450 +offset_left = 80 +offset_right = 200 +offset_top = 194 +offset_bottom = 246 script = ExtResource("19") amount = 25 -digit_height = 52.0 +digit_height = 40.0 + +[node name="TransportHubButton" type="Control" parent="UI/BuildMenu"] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -64 +offset_right = 64 +offset_top = 280 +offset_bottom = 408 +script = ExtResource("10") +icon = 12 +glow_color = Color(0.95, 0.55, 0.85, 1) + +[node name="TransportHubLabel" type="Label" parent="UI/BuildMenu"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -150 +offset_right = 150 +offset_top = 414 +offset_bottom = 480 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Transport Hub" +horizontal_alignment = 1 + +[node name="TransportHubCost" type="Control" parent="UI/BuildMenu"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -60 +offset_right = 60 +offset_top = 484 +offset_bottom = 536 +script = ExtResource("19") +amount = 30 +digit_height = 40.0 [node name="CloseButton" type="Control" parent="UI/BuildMenu"] anchor_left = 1.0 diff --git a/WorldMapContent.gd b/WorldMapContent.gd index 1398021..b8b1ad2 100644 --- a/WorldMapContent.gd +++ b/WorldMapContent.gd @@ -10,6 +10,7 @@ const WORLD_TO_LOCAL: float = 0.04 const MOTHERSHIP_MARKER_COLOR: Color = Color(0.45, 0.9, 0.55) const CRAFTER_MARKER_COLOR: Color = Color(1.0, 0.72, 0.25) const COMMAND_MARKER_COLOR: Color = Color(0.55, 0.6, 1.0) +const TRANSPORT_HUB_MARKER_COLOR: Color = Color(0.95, 0.55, 0.85) # A structure built on a planet's surface (see GameState.extractors) is not # itself a map marker - it is a small badge on the existing planet marker, @@ -50,6 +51,8 @@ func _building_marker_color(type: String) -> Color: return MOTHERSHIP_MARKER_COLOR GameState.BUILDING_SHIP_CRAFTER: return CRAFTER_MARKER_COLOR + GameState.BUILDING_TRANSPORT_HUB: + return TRANSPORT_HUB_MARKER_COLOR _: return COMMAND_MARKER_COLOR @@ -115,6 +118,8 @@ func _draw() -> void: _draw_marker_polygon(_regular_polygon(marker.position, 8.0, 6, PI / 6.0), marker.color) GameState.BUILDING_SHIP_CRAFTER: _draw_marker_polygon(_regular_polygon(marker.position, 7.0, 4, 0.0), marker.color) + GameState.BUILDING_TRANSPORT_HUB: + _draw_marker_polygon(_regular_polygon(marker.position, 7.5, 8, PI / 8.0), marker.color) _: _draw_marker_polygon(_regular_polygon(marker.position, 6.0, 4, PI / 4.0), marker.color) diff --git a/tests/_build_menu_preview.gd b/tests/_build_menu_preview.gd new file mode 100644 index 0000000..5132bbf --- /dev/null +++ b/tests/_build_menu_preview.gd @@ -0,0 +1,20 @@ +extends Node2D + +# Preview, not a test. Opens the build menu itself, so the three-row layout +# added for the Transport Hub (#18) can be checked for fit on the real phone +# viewport - every button, label and price moved, and a bad offset is +# invisible to a headless test. +# +# bash tests/screenshot.sh res://tests/_build_menu_preview.tscn /tmp/menu.png 20 720x1280 + + +func _ready() -> void: + GameState.buildings.clear() + GameState.extractors.clear() + GameState.rocks = 500 + # Everything researched, so nothing is greyed out and the whole menu is + # legible at full strength. + GameState.unlocked = ["shipyards", "logistics", "freight", "coordination"] + var main: Node = load("res://Main.tscn").instantiate() + add_child(main) + main.get_node("UI/BuildMenu").visible = true diff --git a/tests/_build_menu_preview.tscn b/tests/_build_menu_preview.tscn new file mode 100644 index 0000000..419b3ea --- /dev/null +++ b/tests/_build_menu_preview.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/_build_menu_preview.gd" id="1"] + +[node name="BuildMenuPreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/_ingame_buildings_preview.gd b/tests/_ingame_buildings_preview.gd index aa7f619..d586710 100644 --- a/tests/_ingame_buildings_preview.gd +++ b/tests/_ingame_buildings_preview.gd @@ -13,8 +13,9 @@ func _ready() -> void: GameState.buildings.clear() GameState.extractors.clear() GameState.reset_formation() - GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(-230.0, 0.0)) - GameState.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(240.0, 0.0)) + GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(-330.0, 0.0)) + GameState.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(140.0, 0.0)) + GameState.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(480.0, 0.0)) var main: Node = load("res://Main.tscn").instantiate() add_child(main) @@ -24,4 +25,4 @@ func _ready() -> void: # far enough to see them whole. main.get_node("Fleet").move_to(Vector2(0.0, 210.0)) var cam: Camera2D = main.get_node("Camera2D") - cam.zoom = Vector2(1.35, 1.35) + cam.zoom = Vector2(1.0, 1.0) diff --git a/tests/test_transport_hub_building.gd b/tests/test_transport_hub_building.gd new file mode 100644 index 0000000..b2cc4eb --- /dev/null +++ b/tests/test_transport_hub_building.gd @@ -0,0 +1,157 @@ +extends SceneTree + +# 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]: + var icon: int = controller._action_icon_for(type) + 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: + hub = b + 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 + if m.color == map_content.CRAFTER_MARKER_COLOR \ + or m.color == map_content.MOTHERSHIP_MARKER_COLOR \ + or m.color == map_content.COMMAND_MARKER_COLOR: + 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