Refine context actions and preserve fleet landings
This commit is contained in:
parent
18a7d44db4
commit
663648d949
20 changed files with 197 additions and 103 deletions
|
|
@ -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
|
||||
|
|
|
|||
20
Fleet.gd
20
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
23
Main.tscn
23
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
37
tests/_context_actions_preview.gd
Normal file
37
tests/_context_actions_preview.gd
Normal file
|
|
@ -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
|
||||
1
tests/_context_actions_preview.gd.uid
Normal file
1
tests/_context_actions_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://r4kyx6d4s60i
|
||||
6
tests/_context_actions_preview.tscn
Normal file
6
tests/_context_actions_preview.tscn
Normal file
|
|
@ -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")
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue