diff --git a/BuildController.gd b/BuildController.gd index 18e81cd..7b7da87 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -6,10 +6,8 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") # Space-side building UI. Owns: # - the build menu (place a mothership, ship crafter or command centre) # - the shared ACTION button, which adapts to whatever the fleet is -# standing on (craft ships at a crafter, configure formation at a -# command centre). The enter-atmosphere prompt uses the same slot, and -# PlanetAtmosphereController suppresses itself while a building prompt -# is showing so the two can never fight over it. +# standing on (craft ships, configure formation, or land). One action +# fires directly; multiple actions unfold horizontally. # - rebuilding a wiped-out fleet at the mothership # # Buildings themselves live in GameState (they must outlive the trip to a @@ -23,6 +21,8 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") @onready var build_button: IconButton = get_node("../UI/BuildButton") @onready var action_button: IconButton = get_node("../UI/BuildActionButton") @onready var building_action_button: IconButton = get_node("../UI/BuildingActionButton") +@onready var landing_button: IconButton = get_node("../UI/EnterAtmosphereButton") +@onready var landing_controller: Node2D = get_node("../LandingController") @onready var build_menu: Control = get_node("../UI/BuildMenu") @onready var build_close_button: IconButton = get_node("../UI/BuildMenu/CloseButton") @@ -113,8 +113,9 @@ var lab_building_id: int = 0 # --- the transfer panel (#24) --------------------------------------------- # Ore used to be moved from inside whichever panel happened to be open: "load # ore" lived in the research screen, "store ore here" in the ship crafter. -# Both are now one top-level button that works at every building and in both -# directions, so moving ore is never buried under an unrelated menu. +# Both are now one top-level shortcut beside Build at resource-bearing +# locations and work in both directions, so moving cargo is never buried +# under an unrelated action foldout. @onready var transfer_button: IconButton = get_node("../UI/TransferButton") @onready var transfer_menu: Control = get_node("../UI/TransferMenu") @onready var transfer_status: Label = get_node("../UI/TransferMenu/StatusLabel") @@ -165,6 +166,7 @@ var current_building: Node2D = null var pending_building: Node2D = null var current_extractor_index: int = -1 var pending_extractor_index: int = -1 +var current_landing_planet: Node2D = null var pending_time: float = 0.0 var context_actions_expanded: bool = false @@ -218,6 +220,7 @@ func _ready() -> void: transfer_menu.visible = false action_button.visible = false building_action_button.visible = false + landing_button.visible = false transfer_button.visible = false hangar_button.visible = false hangar_menu.visible = false @@ -239,10 +242,10 @@ func _configure_build_menu_entries() -> void: build_menu_entries.append([definition.definition_id, button, label, cost_number, definition]) -# True while a building prompt or panel owns the shared action slot, so the -# enter-atmosphere prompt knows to stay out of the way. +# True while a modal panel owns the shared context controls. A plain building +# prompt is not modal: landing can coexist with its actions in the same foldout. func is_showing_building_prompt() -> bool: - return current_building != null or build_menu.visible or craft_menu.visible \ + return build_menu.visible or craft_menu.visible \ or formation_menu.visible or skill_menu.visible or transfer_menu.visible \ or hangar_menu.visible @@ -309,8 +312,10 @@ func _process(delta: float) -> void: pending_building = null current_extractor_index = -1 pending_extractor_index = -1 + current_landing_planet = null action_button.visible = false building_action_button.visible = false + landing_button.visible = false transfer_button.visible = false hangar_button.visible = false context_actions_expanded = false @@ -332,28 +337,27 @@ func _process(delta: float) -> void: else: pending_time += delta + var detected_landing_planet: Node2D = landing_controller.current_planet if pending_time < prompt_debounce_time or (pending_building == current_building \ - and pending_extractor_index == current_extractor_index): + and pending_extractor_index == current_extractor_index \ + and detected_landing_planet == current_landing_planet): return current_building = pending_building current_extractor_index = pending_extractor_index + current_landing_planet = detected_landing_planet context_actions_expanded = false hangar_button.visible = false - # Every building can hold ore, so the transfer button rides alongside the - # action button rather than being one of the things the action button - # turns into - the two are independent choices at the same structure. - transfer_button.visible = false building_action_button.visible = false - action_button.visible = current_building != null or current_extractor_index >= 0 + landing_button.visible = false + transfer_button.visible = _has_transfer_context() + var actions: Array = _context_action_buttons() + action_button.visible = not actions.is_empty() if current_building != null: - var actions: Array = _context_action_buttons() building_action_button.icon = int(current_building.definition.action_icon) building_action_button.queue_redraw() - action_button.icon = building_action_button.icon if actions.size() == 1 else IconButton.Icon.ACTION_MENU - action_button.queue_redraw() - elif current_extractor_index >= 0: - action_button.icon = IconButton.Icon.TRANSFER + if not actions.is_empty(): + action_button.icon = int(actions[0].icon) if actions.size() == 1 else IconButton.Icon.ACTION_MENU action_button.queue_redraw() _layout_context_buttons() @@ -381,33 +385,41 @@ func _layout_context_buttons() -> void: func _context_action_buttons() -> Array: - if current_extractor_index >= 0: - return [transfer_button] - if current_building == null: - return [] var buttons: Array = [] - for action in current_building.available_actions(): - var button: IconButton = _button_for_building_action(action) - if button != null and button not in buttons: - buttons.append(button) + if current_building != null: + for action in current_building.available_actions(): + var button: IconButton = _button_for_building_action(action) + if button != null and button not in buttons: + buttons.append(button) + if current_landing_planet != null: + buttons.append(landing_button) return buttons +func _has_transfer_context() -> bool: + if current_extractor_index >= 0: + return true + return current_building != null and current_building.available_actions().has("transfer") + + func _button_for_building_action(action: String) -> IconButton: if action == "transfer": - return transfer_button + return null if action == "hangar": return hangar_button return building_action_button func _on_context_action_pressed() -> void: - if current_building == null and current_extractor_index < 0: + if current_building == null and current_extractor_index < 0 \ + and current_landing_planet == null: return var actions: Array = _context_action_buttons() if actions.size() == 1: - if actions[0] == transfer_button: - _on_transfer_pressed() + if actions[0] == landing_button: + landing_controller._on_enter_pressed() + elif actions[0] == hangar_button: + _on_hangar_pressed() else: _on_action_pressed() return diff --git a/Fleet.gd b/Fleet.gd index b108dd1..d613164 100644 --- a/Fleet.gd +++ b/Fleet.gd @@ -4,10 +4,9 @@ extends Node2D # centre) rather than being fixed here, so a layout the player arranged # survives scene swaps and fleet rebuilds. Ships are always # spawned programmatically (spawn_ships), using however many of these -# offsets are needed - both for a fresh start (5 ships) and for restoring -# a possibly-smaller surviving fleet after returning from a planet's -# surface, so both cases share one code path instead of two that could -# drift out of sync. +# offsets are needed. The optional vessel-type list preserves the exact +# mixed fleet through scene swaps; a fresh start still defaults each slot +# to an interceptor. # The fleet's cruise speed. This is the speed of the FLEET as a whole: the # anchor below advances at exactly this rate, and individual ships are # allowed to fly faster than it (Ship.max_speed) so they can close on their @@ -44,7 +43,8 @@ func _on_vessel_upgraded(_id: String) -> void: ship.apply_vessel_stats() -func spawn_ships(count: int, center: Vector2 = Vector2.ZERO) -> void: +func spawn_ships(count: int, center: Vector2 = Vector2.ZERO, + vessel_types: PackedStringArray = PackedStringArray()) -> void: for ship in ships: if is_instance_valid(ship): ship.queue_free() @@ -55,6 +55,8 @@ func spawn_ships(count: int, center: Vector2 = Vector2.ZERO) -> void: for i in range(n): var offset: Vector2 = GameState.formation[i] var ship: Area2D = ship_scene.instantiate() + if i < vessel_types.size(): + ship.vessel_type = vessel_types[i] ship.position = center + offset ship.glow_color = ship_glow_color ship.core_color = ship_core_color @@ -67,6 +69,14 @@ func spawn_ships(count: int, center: Vector2 = Vector2.ZERO) -> void: fleet_target = center +func vessel_types() -> PackedStringArray: + var result: PackedStringArray = PackedStringArray() + for ship in ships: + if is_instance_valid(ship): + result.append(String(ship.vessel_type)) + return result + + func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool: # Adds one vessel in the next free formation slot (bought from a ship # crafter). Returns false when the wedge is already full. diff --git a/GameState.gd b/GameState.gd index 03c35e4..ec36e3d 100644 --- a/GameState.gd +++ b/GameState.gd @@ -14,6 +14,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") var has_return_position: bool = false var surviving_ship_count: int = 5 +var surviving_vessel_types: PackedStringArray = PackedStringArray() var return_world_position: Vector2 = Vector2.ZERO var return_camera_zoom: float = 1.0 diff --git a/Main.tscn b/Main.tscn index d6dd326..1be9bc7 100644 --- a/Main.tscn +++ b/Main.tscn @@ -148,20 +148,19 @@ digit_height = 38.0 [node name="PlanetNameLabel" type="Label" parent="UI"] visible = false offset_left = 24 -offset_top = 156 +offset_top = 480 offset_right = 696 -offset_bottom = 282 +offset_bottom = 532 text = "" horizontal_alignment = 1 +theme_override_font_sizes/font_size = 32 [node name="EnterAtmosphereButton" type="Control" parent="UI"] visible = false -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 24 -offset_right = 172 -offset_top = -720 -offset_bottom = -572 +offset_left = 188 +offset_top = 324 +offset_right = 336 +offset_bottom = 472 script = ExtResource("10") icon = 3 @@ -203,10 +202,10 @@ glow_color = Color(0.45, 0.9, 0.55, 1) [node name="TransferButton" type="Control" parent="UI"] visible = false -offset_left = 24 -offset_right = 172 -offset_top = 488 -offset_bottom = 636 +offset_left = 188 +offset_top = 160 +offset_right = 336 +offset_bottom = 308 script = ExtResource("10") icon = 14 glow_color = Color(1, 0.78, 0.25, 1) diff --git a/PlanetAtmosphereController.gd b/PlanetAtmosphereController.gd index 9363981..36ac960 100644 --- a/PlanetAtmosphereController.gd +++ b/PlanetAtmosphereController.gd @@ -34,7 +34,8 @@ func _ready() -> void: enter_button.pressed.connect(_on_enter_pressed) if GameState.has_return_position: - fleet.spawn_ships(GameState.surviving_ship_count, GameState.return_world_position) + fleet.spawn_ships(GameState.surviving_ship_count, GameState.return_world_position, + GameState.surviving_vessel_types) camera.position = GameState.return_world_position camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom) GameState.has_return_position = false @@ -57,16 +58,12 @@ func _process(delta: float) -> void: return if world_map.visible or build_controller.is_showing_building_prompt(): - # EnterAtmosphereButton shares one "action" slot with the map's - # RecenterButton and with BuildController's building prompt - it doesn't make sense to enter - # a planet's atmosphere while looking at the star map anyway, so - # suppress the prompt entirely rather than let it sit hidden - # behind the map's backdrop only to pop back in the instant the - # map closes. + # Context actions belong to BuildController. This controller owns the + # debounced planet selection and label, and suppresses both while a modal + # screen is open. current_planet = null pending_planet = null planet_name_label.visible = false - enter_button.visible = false return # Debounced: only commit a visible/hidden change once the raw @@ -90,10 +87,8 @@ func _process(delta: float) -> void: if current_planet != null: planet_name_label.text = current_planet.planet_name planet_name_label.visible = true - enter_button.visible = true else: planet_name_label.visible = false - enter_button.visible = false func _on_enter_pressed() -> void: @@ -105,6 +100,7 @@ func _on_enter_pressed() -> void: GameState.has_return_position = true GameState.surviving_ship_count = fleet.ships.size() + GameState.surviving_vessel_types = fleet.vessel_types() # The planet's own position, not fleet.get_centroid() - the fleet only # needs to be somewhere within the planet's landing radius (not # centered exactly on it) for the prompt to show, so the centroid can diff --git a/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 7c683d4..294f5f1 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -3,9 +3,9 @@ extends Node2D const SURFACE_PROFILES = preload("res://PlanetSurfaceProfiles.gd") const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") -# Bootstraps the surviving fleet on this scene's own startup (from -# GameState, since this is a fresh scene tree - nothing here carries -# over automatically) and handles the always-available exit prompt that +# Bootstraps the surviving fleet, including each vessel type, on this scene's +# own startup (from GameState, since this is a fresh scene tree - nothing here +# carries over automatically) and handles the always-available exit prompt that # fades back out to Main.tscn, restoring the fleet at wherever it left # space from. @@ -25,7 +25,8 @@ var transitioning: bool = false func _ready() -> void: var planet_definition: Resource = PLANET_DEFINITIONS.get_definition(GameState.planet_definition_id) _compose_surface(SURFACE_PROFILES.get_profile(planet_definition.surface_profile_id)) - fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO) + fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO, + GameState.surviving_vessel_types) camera.position = Vector2.ZERO camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom) planet_name_label.text = GameState.planet_name @@ -57,6 +58,7 @@ func _on_exit_pressed() -> void: transitioning = true GameState.surviving_ship_count = fleet.ships.size() + GameState.surviving_vessel_types = fleet.vessel_types() var tween: Tween = create_tween() tween.tween_property(fade_rect, "color:a", 1.0, fade_duration) diff --git a/TransportHubBuildingVisual.tscn b/TransportHubBuildingVisual.tscn index 1426761..29dbb10 100644 --- a/TransportHubBuildingVisual.tscn +++ b/TransportHubBuildingVisual.tscn @@ -1,11 +1,6 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=2 format=3] [ext_resource type="Script" path="res://TransportHubBuildingVisual.gd" id="1"] -[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"] [node name="Visual" type="Node2D"] script = ExtResource("1") - -[node name="ActionProvider" type="Node" parent="."] -script = ExtResource("2") -actions = PackedStringArray("transfer") diff --git a/tests/_context_actions_preview.gd b/tests/_context_actions_preview.gd new file mode 100644 index 0000000..1957336 --- /dev/null +++ b/tests/_context_actions_preview.gd @@ -0,0 +1,37 @@ +extends Node + +# Visual-only phone preview for the top-level context controls. It composes a +# mothership action set with a landing action so the horizontal foldout and +# the independent Transfer shortcut can be inspected in one screenshot. + + +func _ready() -> void: + GameState.buildings.clear() + GameState.extractors.clear() + var main: Node2D = load("res://Main.tscn").instantiate() + add_child(main) + main.get_node("AsteroidField").set_process(false) + main.get_node("EnemySpawner").set_process(false) + main.get_node("LandingController").set_process(false) + + var building: Node2D = load("res://Building.tscn").instantiate() + building.definition_id = GameState.BUILDING_MOTHERSHIP + building.position = Vector2(0, 420) + main.add_child(building) + + var controller: Node2D = main.get_node("BuildController") + controller.set_process(false) + controller.current_building = building + var planet: Node2D = Node2D.new() + main.add_child(planet) + controller.current_landing_planet = planet + controller.building_action_button.icon = int(building.definition.action_icon) + controller.transfer_button.visible = controller._has_transfer_context() + controller.action_button.visible = true + controller.action_button.icon = IconButton.Icon.ACTION_MENU + controller.context_actions_expanded = true + controller._layout_context_buttons() + + var label: Label = main.get_node("UI/PlanetNameLabel") + label.text = "PLANET ACTIONS" + label.visible = true diff --git a/tests/_context_actions_preview.gd.uid b/tests/_context_actions_preview.gd.uid new file mode 100644 index 0000000..aed6b62 --- /dev/null +++ b/tests/_context_actions_preview.gd.uid @@ -0,0 +1 @@ +uid://r4kyx6d4s60i diff --git a/tests/_context_actions_preview.tscn b/tests/_context_actions_preview.tscn new file mode 100644 index 0000000..a79a95b --- /dev/null +++ b/tests/_context_actions_preview.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/_context_actions_preview.gd" id="1"] + +[node name="ContextActionsPreview" type="Node"] +script = ExtResource("1") diff --git a/tests/test_building_composition.gd b/tests/test_building_composition.gd index 7cadf5d..616095d 100644 --- a/tests/test_building_composition.gd +++ b/tests/test_building_composition.gd @@ -21,8 +21,12 @@ func _initialize() -> void: ]) return var provider: Node = building.visual.get_node_or_null("ActionProvider") - if provider == null or not provider.actions.has("transfer"): - _fail("%s has no composed action provider" % definition.definition_id) + if definition.definition_id == GameState.BUILDING_TRANSPORT_HUB: + if provider != null or not building.available_actions().is_empty(): + _fail("transport hub composed a manual action") + return + elif provider == null or not provider.actions.has("transfer"): + _fail("%s has no composed transfer action" % definition.definition_id) return building.free() diff --git a/tests/test_crafter_feedback.gd b/tests/test_crafter_feedback.gd index e104c27..ddd1882 100644 --- a/tests/test_crafter_feedback.gd +++ b/tests/test_crafter_feedback.gd @@ -96,10 +96,9 @@ func _process(delta: float) -> bool: print("TEST_CRAFTER_FEEDBACK: FAIL (fleet parked on the crafter never registered)") quit(1) return true - # A crafter now has both Craft and Transfer, so the shared action - # button unfolds those choices before the craft action is selected. + # Transfer has its own shortcut beside Build, leaving Craft as the + # single contextual action that fires directly. _tap(controller.action_button) - _tap(controller.building_action_button) if not controller.craft_menu.visible: print("TEST_CRAFTER_FEEDBACK: FAIL (the action button did not open the crafter panel)") quit(1) diff --git a/tests/test_extractor_transfer.gd b/tests/test_extractor_transfer.gd index 97151e4..5f5c838 100644 --- a/tests/test_extractor_transfer.gd +++ b/tests/test_extractor_transfer.gd @@ -48,11 +48,13 @@ func _process(delta: float) -> bool: 1: if elapsed < 1.2: return false - if controller.current_extractor_index != extractor_index or not controller.action_button.visible: - return _fail("extractor did not expose a contextual action") - if not scene.get_node("UI/EnterAtmosphereButton").visible: - return _fail("extractor action hid the separate landing action") - controller._on_context_action_pressed() + if controller.current_extractor_index != extractor_index or not controller.transfer_button.visible: + return _fail("extractor did not expose the transfer shortcut") + if not controller.action_button.visible \ + or controller.action_button.icon != controller.landing_button.icon \ + or controller.landing_button.visible: + return _fail("landing was not folded into the shared action control") + controller._on_transfer_pressed() if not controller.transfer_menu.visible or controller.transfer_list.extractor_index != extractor_index: return _fail("transfer panel did not bind to the extractor") var before: int = state.fleet_resource(resource_type) @@ -61,7 +63,7 @@ func _process(delta: float) -> bool: return _fail("fleet did not fetch five resources from extractor") if state.extractor_resource(extractor_index, resource_type) < 7: return _fail("extractor transfer removed too much stock") - print("fleet fetched %s from extractor while landing remained available" % resource_type) + print("fleet fetched %s while landing remained the contextual action" % resource_type) print("TEST_EXTRACTOR_TRANSFER: PASS") quit(0) return true diff --git a/tests/test_hangar_management.gd b/tests/test_hangar_management.gd index 4de3cac..3bc5ff6 100644 --- a/tests/test_hangar_management.gd +++ b/tests/test_hangar_management.gd @@ -63,24 +63,30 @@ func _process(delta: float) -> bool: _fail("hangar allowed the last deployed ship to be stored") return true - # Context actions are compact until expanded. A mothership has three - # choices: research, transfer cargo and hangar, which unfold in their - # own non-overlapping berths to the right only after expansion. + # Context actions are compact until expanded. A mothership unfolds + # research and hangar; transfer stays as a persistent shortcut beside + # Build and is not duplicated in the foldout. var mothership: Node2D = load("res://Building.tscn").instantiate() mothership.set("building_type", GameState.BUILDING_MOTHERSHIP) mothership._ready() controller.current_building = mothership controller.context_actions_expanded = true + controller.transfer_button.visible = controller._has_transfer_context() controller._layout_context_buttons() - var buttons: Array = [controller.building_action_button, controller.transfer_button, controller.hangar_button] + var buttons: Array = [controller.building_action_button, controller.hangar_button] for i in range(buttons.size() - 1): if buttons[i].offset_right > buttons[i + 1].offset_left: _fail("context buttons %d and %d overlap" % [i, i + 1]) return true controller._collapse_context_actions() - if controller.building_action_button.visible or controller.transfer_button.visible or controller.hangar_button.visible: + if controller.building_action_button.visible or controller.hangar_button.visible: _fail("collapsing the context button left an action visible") return true + if not controller.transfer_button.visible \ + or controller.transfer_button.offset_top != controller.build_button.offset_top \ + or controller.transfer_button.offset_left <= controller.build_button.offset_left: + _fail("transfer shortcut is not beside Build") + return true mothership.free() print("hangar crews and stores typed vessels; contextual actions expand and collapse") print("TEST_HANGAR_MANAGEMENT: PASS") diff --git a/tests/test_planet_atmosphere_transition.gd b/tests/test_planet_atmosphere_transition.gd index ed3b10c..f972a8b 100644 --- a/tests/test_planet_atmosphere_transition.gd +++ b/tests/test_planet_atmosphere_transition.gd @@ -24,7 +24,13 @@ var target_chunk: Vector2i var position_before_leaving: Vector2 var cached_fade_duration: float = 0.4 var returned_ship_count: int = -1 +var returned_vessel_types: PackedStringArray = PackedStringArray() var game_state: Node +var expected_vessel_types: PackedStringArray = PackedStringArray([ + GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, + GameState.VESSEL_TRANSPORTER, GameState.VESSEL_SHIELD, + GameState.VESSEL_INTERCEPTOR, +]) func _initialize() -> void: @@ -58,7 +64,7 @@ func _initialize() -> void: landing_controller = main_scene.get_node("LandingController") asteroid_field_ref = main_scene.get_node("AsteroidField") planet_name_label = main_scene.get_node("UI/PlanetNameLabel") - enter_button = main_scene.get_node("UI/EnterAtmosphereButton") + enter_button = main_scene.get_node("UI/BuildActionButton") func _process(delta: float) -> bool: @@ -85,6 +91,10 @@ func _process(delta: float) -> bool: # collide with a ship mid-test. Clear the existing ones too. for a in get_nodes_in_group("asteroids"): a.queue_free() + # A mixed fleet proves the handoff preserves composition rather + # than merely reconstructing the same number of interceptors. + fleet.spawn_ships(expected_vessel_types.size(), fleet.get_centroid(), + expected_vessel_types) # Get the fleet into the target chunk's vicinity first so # PlanetField's spawn radius covers it - the planet's own exact @@ -151,12 +161,13 @@ func _process(delta: float) -> bool: var surface_fleet: Node = current_scene.get_node("Fleet") var ship_count_ok: bool = surface_fleet.ships.size() == 5 + var composition_ok: bool = surface_fleet.vessel_types() == expected_vessel_types var no_asteroids: bool = get_nodes_in_group("asteroids").is_empty() var no_enemies: bool = get_nodes_in_group("enemies").is_empty() print("ship_count_ok=%s (count=%d) no_asteroids=%s no_enemies=%s" % [ ship_count_ok, surface_fleet.ships.size(), no_asteroids, no_enemies ]) - if not (ship_count_ok and no_asteroids and no_enemies): + if not (ship_count_ok and composition_ok and no_asteroids and no_enemies): print("TEST_PLANET_ATMOSPHERE_TRANSITION: FAIL (surface scene contents wrong)") quit(1) return true @@ -217,6 +228,7 @@ func _process(delta: float) -> bool: for a in get_nodes_in_group("asteroids"): a.queue_free() returned_ship_count = space_fleet.ships.size() + returned_vessel_types = space_fleet.vessel_types() # Ships respawn in a wedge and then steer into formation, so the # centroid only means anything once they have actually stopped. @@ -236,12 +248,13 @@ func _process(delta: float) -> bool: # across correctly and a fleet came back. var handoff_ok: bool = game_state.surviving_ship_count == 5 var ship_count_ok: bool = handoff_ok and returned_ship_count >= 1 + var composition_ok: bool = returned_vessel_types == expected_vessel_types var position_ok: bool = space_fleet.get_centroid().distance_to(position_before_leaving) < 50.0 print("handoff=%d returned_ships=%d ship_count_ok=%s position_ok=%s centroid=%s expected=%s" % [ game_state.surviving_ship_count, returned_ship_count, ship_count_ok, position_ok, space_fleet.get_centroid(), position_before_leaving ]) - if ship_count_ok and position_ok: + if ship_count_ok and composition_ok and position_ok: print("TEST_PLANET_ATMOSPHERE_TRANSITION: PASS") quit(0) else: diff --git a/tests/test_planet_prompt_map_interaction.gd b/tests/test_planet_prompt_map_interaction.gd index f5de4c4..a3a7cc9 100644 --- a/tests/test_planet_prompt_map_interaction.gd +++ b/tests/test_planet_prompt_map_interaction.gd @@ -2,14 +2,8 @@ extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_planet_prompt_map_interaction.gd -# Regression test for a real bug: EnterAtmosphereButton (shown while -# standing on a planet, bottom-center) and the world map's own -# RecenterButton (also bottom-center) occupy the exact same screen -# position. Opening the map while standing on a planet must suppress the -# enter-atmosphere prompt entirely - not just let it sit hidden behind -# the map's backdrop, since closing the map would then abruptly reveal it -# at the same instant RecenterButton disappears (which is what most -# plausibly explains "buttons blinking/disappearing"). +# Opening the map while standing on a planet must suppress the shared +# contextual action and planet label, then restore them after closing. var main_scene: Node var fleet: Node @@ -48,7 +42,7 @@ func _initialize() -> void: world_map = main_scene.get_node("UI/WorldMap") map_button = main_scene.get_node("UI/MapButton") planet_name_label = main_scene.get_node("UI/PlanetNameLabel") - enter_button = main_scene.get_node("UI/EnterAtmosphereButton") + enter_button = main_scene.get_node("UI/BuildActionButton") func _process(delta: float) -> bool: diff --git a/tests/test_prompt_debounce.gd b/tests/test_prompt_debounce.gd index 12e2db7..f275903 100644 --- a/tests/test_prompt_debounce.gd +++ b/tests/test_prompt_debounce.gd @@ -47,7 +47,7 @@ func _initialize() -> void: root.add_child(main_scene) fleet = main_scene.get_node("Fleet") planet_field = main_scene.get_node("PlanetField") - enter_button = main_scene.get_node("UI/EnterAtmosphereButton") + enter_button = main_scene.get_node("UI/BuildActionButton") main_scene.get_node("AsteroidField").set_process(false) main_scene.get_node("EnemySpawner").set_process(false) diff --git a/tests/test_resource_transfer.gd b/tests/test_resource_transfer.gd index f5035bc..e33bdd9 100644 --- a/tests/test_resource_transfer.gd +++ b/tests/test_resource_transfer.gd @@ -7,8 +7,8 @@ extends SceneTree # research screen. # # Verifies: -# 1. the transfer button appears alongside the action button whenever the -# fleet is standing on a building, and opens the transfer panel bound to +# 1. the transfer shortcut appears beside Build whenever the fleet is +# standing on a transferable building, and opens the panel bound to # THAT building - the `== 0` guard failure that made every crafter # button look dead is the same trap here # 2. ore moves both ways at an ordinary building, capped by what is @@ -73,8 +73,6 @@ func _tap(button: Control) -> void: func _open_transfer() -> void: - # Contextual choices collapse into one button until it is opened. - _tap(controller.action_button) _tap(controller.transfer_button) @@ -108,6 +106,8 @@ func _process(delta: float) -> bool: return false _expect(controller.current_building != null, "parking on the crafter never registered") + _expect(controller.transfer_button.visible, + "transfer shortcut did not appear beside Build") _open_transfer() _expect(controller.transfer_menu.visible, "tapping the transfer button did not open the panel") diff --git a/tests/test_return_position_matches_planet.gd b/tests/test_return_position_matches_planet.gd index d6cc6be..c7550c3 100644 --- a/tests/test_return_position_matches_planet.gd +++ b/tests/test_return_position_matches_planet.gd @@ -44,7 +44,7 @@ func _initialize() -> void: fleet = main_scene.get_node("Fleet") planet_field = main_scene.get_node("PlanetField") landing_controller = main_scene.get_node("LandingController") - enter_button = main_scene.get_node("UI/EnterAtmosphereButton") + enter_button = main_scene.get_node("UI/BuildActionButton") func _process(delta: float) -> bool: diff --git a/tests/test_transport_hub_building.gd b/tests/test_transport_hub_building.gd index dcb03a6..5f344f5 100644 --- a/tests/test_transport_hub_building.gd +++ b/tests/test_transport_hub_building.gd @@ -24,6 +24,7 @@ var scene: Node var field: Node var controller: Node var map_content: Node2D +var hub_building: Node2D var hub: Node2D var stage: int = 0 var elapsed: float = 0.0 @@ -101,11 +102,19 @@ func _process(delta: float) -> bool: return false for b in field.spawned: if is_instance_valid(b) and b.building_type == GameState.BUILDING_TRANSPORT_HUB: + hub_building = b hub = b.visual if hub == null: print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)") quit(1) return true + if not hub_building.available_actions().is_empty(): + print("TEST_TRANSPORT_HUB_BUILDING: FAIL (offers a manual resource transfer)") + quit(1) + return true + for ship in scene.get_node("Fleet").ships: + ship.position = HUB_AT + scene.get_node("Fleet").move_to(HUB_AT) # --- 3. drawn as itself, not via the crafter catch-all ------- # _hub_traffic only exists if _ready() took the transport-hub @@ -142,6 +151,14 @@ func _process(delta: float) -> bool: # --- 4. animates without ever redrawing (#10) ---------------- if elapsed < 2.0: return false + if controller.current_building != hub_building \ + or controller.action_button.visible or controller.transfer_button.visible: + print("TEST_TRANSPORT_HUB_BUILDING: FAIL (actionless hub context current=%s expected=%s action=%s transfer=%s)" % [ + controller.current_building, hub_building, controller.action_button.visible, + controller.transfer_button.visible, + ]) + quit(1) + return true 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)