extends Node2D const BUILDING_DEFINITIONS = preload("res://buildings/BuildingDefinitions.gd") const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd") const ALL: int = 1 << 30 const HUD_COLUMN_MARGIN := 18.0 const HUD_ROW_GAP := 16.0 @export var prompt_debounce_time: float = 0.15 @onready var fleet: Node2D = get_node("../Fleet") @onready var building_field: Node2D = get_node("../BuildingField") @onready var planet_field: Node2D = get_node("../PlanetField") @onready var landing_controller: Node2D = get_node("../LandingController") @onready var world_map: Control = get_node("../UI/WorldMap") @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 transfer_button: IconButton = get_node("../UI/TransferButton") @onready var hangar_button: IconButton = get_node("../UI/HangarButton") # Menu roots are scene APIs. BuildController deliberately never reaches into # their child trees; each scene owns its controls, layout and input routing. @onready var build_menu: Control = get_node("../UI/BuildMenu") @onready var craft_menu: Control = get_node("../UI/CraftMenu") @onready var lab_menu: Control = get_node("../UI/LabMenu") @onready var transfer_menu: Control = get_node("../UI/TransferMenu") @onready var hangar_menu: Control = get_node("../UI/HangarMenu") @onready var formation_menu: Control = get_node("../UI/FormationMenu") @onready var skill_menu: Control = get_node("../UI/SkillMenu") @onready var planning_menu: Control = get_node("../UI/PlanningMenu") @onready var garrison_menu: Control = get_node("../UI/GarrisonMenu") # #216: where a hub or a workshop is told how many craft to keep. @onready var station_menu: Control = get_node("../UI/StationMenu") # #220: what the fleet costs to keep, and the arithmetic behind it. @onready var upkeep_menu: Control = get_node("../UI/FleetUpkeepMenu") # Where research data comes from, shown when a player finds out they have # none - the tree used to refuse in silence and the lab used to blame the # queue. @onready var research_points_menu: Control = get_node("../UI/ResearchPointsMenu") @onready var scanner_menu: Control = get_node("../UI/ScannerMenu") var pending_build_type: String = "" var craft_building_id: int = 0 var lab_building_id: int = 0 var lab_vessel: String = GameState.VESSEL_INTERCEPTOR var lab_choice: int = 0 var lab_can_buy: bool = false var transfer_building_id: int = 0 var transfer_extractor_index: int = -1 var garrison_building_id: int = 0 var scanner_building_id: int = 0 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 func _ready() -> void: get_viewport().size_changed.connect(_on_viewport_resized) _seat_hud_columns() build_button.pressed.connect(_on_build_pressed) action_button.pressed.connect(_on_context_action_pressed) building_action_button.pressed.connect(_on_action_pressed) transfer_button.pressed.connect(_on_transfer_pressed) hangar_button.pressed.connect(_on_hangar_pressed) build_menu.building_selected.connect(_show_building_info) build_menu.build_confirmed.connect(_confirm_build) build_menu.closed.connect(_on_build_menu_closed) craft_menu.craft_requested.connect(_on_craft_vessel) craft_menu.queue_cancel_requested.connect(_on_queue_slot) craft_menu.closed.connect(func(): craft_building_id = 0) lab_menu.vessel_selected.connect(_on_lab_vessel) lab_menu.upgrade_selected.connect(_on_lab_upgrade) lab_menu.closed.connect(func(): lab_building_id = 0) transfer_menu.transfer_requested.connect(_on_resource_transfer) transfer_menu.closed.connect(_clear_transfer_context) hangar_menu.crew_requested.connect(_on_hangar_crew_type) hangar_menu.store_requested.connect(_on_hangar_store_type) formation_menu.formation_changed.connect(_on_formation_changed) formation_menu.reset_requested.connect(_on_reset_formation) skill_menu.research_requested.connect(_on_unlock_requested) scanner_menu.scan_requested.connect(_on_scan_requested) scanner_menu.closed.connect(func(): scanner_building_id = 0) for control in [action_button, building_action_button, landing_button, transfer_button, hangar_button]: control.visible = false if GameState.intro_active: _hide_all_actions() _layout_context_buttons() # HudScale reapplies each control's authored offsets whenever the window # changes, which wipes the two layouts this controller writes at runtime. # Re-running them after a resize is what keeps a desktop window that gets # dragged to another size from leaving the context row where it was. func _on_viewport_resized() -> void: # Deferred: HudScale reapplies on the same signal, and which of two # siblings connected first is not a thing to depend on. By the end of the # frame its offsets are in, and these two can seat against them. _relayout_hud.call_deferred() func _relayout_hud() -> void: _seat_hud_columns() _layout_context_buttons() func _seat_hud_columns() -> void: var readout: ResourceReadout = get_node_or_null("../UI/ResourceReadout") if readout == null: return var buttons: Array = [build_button, transfer_button, action_button, landing_button, building_action_button, hangar_button] var highest: float = INF for button in buttons: if button != null: highest = minf(highest, button.position.y) if is_inf(highest): return # The margin is authored in the same canvas units as everything else, so # it scales with the HUD (#228) or it would be the one gap that stayed # phone-sized on a desktop. var shift: float = readout.content_bottom() + HUD_COLUMN_MARGIN * readout.scale.y - highest if shift <= 0.0: return for button in buttons: if button != null: button.position.y += shift func _menu_is_open() -> bool: for menu in [build_menu, craft_menu, lab_menu, transfer_menu, hangar_menu, formation_menu, skill_menu, planning_menu, garrison_menu, scanner_menu, station_menu, upkeep_menu, research_points_menu]: if menu.visible: return true return false func is_showing_building_prompt() -> bool: return _menu_is_open() func _unhandled_input(event: InputEvent) -> void: if context_actions_expanded and (event is InputEventScreenTouch and event.pressed \ or event is InputEventMouseButton and event.pressed \ and event.button_index == MOUSE_BUTTON_LEFT): _collapse_context_actions() if skill_menu.handle_unhandled_input(event): get_viewport().set_input_as_handled() return if formation_menu.handle_unhandled_input(event): get_viewport().set_input_as_handled() return if world_map.visible or _menu_is_open(): get_viewport().set_input_as_handled() func _process(delta: float) -> void: _check_fleet_wipe() if GameState.intro_active: _hide_all_actions() return if not build_button.visible: build_button.visible = true if build_menu.visible: build_menu.refresh_option_states(_planet_under_fleet()) if craft_menu.visible: craft_menu.refresh() if lab_menu.visible: _refresh_lab() if transfer_menu.visible: transfer_menu.refresh() # Both of these move on their own while they are open - a transporter # wears down, an office starves, the fleet changes under the bill - so # they follow the same rule as the menus above: refreshed while visible, # untouched when not. if station_menu.visible: station_menu.refresh() if upkeep_menu.visible: upkeep_menu.refresh() if world_map.visible or _menu_is_open(): _clear_detected_context() return var detected: Node2D = building_field.get_building_fleet_is_on() var detected_extractor: int = -1 if detected == null: var planet: Node2D = _planet_under_fleet() if planet != null: detected_extractor = GameState.extractor_index_at(planet.planet_id) if detected != pending_building or detected_extractor != pending_extractor_index: pending_building = detected pending_extractor_index = detected_extractor pending_time = 0.0 else: pending_time += delta var detected_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 detected_planet == current_landing_planet): return current_building = pending_building current_extractor_index = pending_extractor_index current_landing_planet = detected_planet context_actions_expanded = false hangar_button.visible = false building_action_button.visible = false 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: building_action_button.icon = _building_action_icon(current_building) building_action_button.queue_redraw() 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() func _clear_detected_context() -> void: current_building = null pending_building = null current_extractor_index = -1 pending_extractor_index = -1 current_landing_planet = null for control in [action_button, building_action_button, landing_button, transfer_button, hangar_button]: control.visible = false context_actions_expanded = false func _layout_context_buttons() -> void: # position is in the UI layer's space and size is in the button's own, # and since #228 those differ by the HUD scale on a short canvas. Every # gap and stride below is therefore scaled, while the RECTS keep their # authored size - HudScale renders them down rather than shrinking them. # Mixing the two is what this function got wrong twice before (see # test_hud_layout), and a desktop canvas makes it wrong again. var k: float = build_button.scale.y var row_top: float = build_button.position.y + build_button.size.y * k + HUD_ROW_GAP * k var row_height: float = build_button.size.y action_button.offset_top = row_top action_button.offset_bottom = row_top + row_height var buttons: Array = _context_action_buttons() if not context_actions_expanded: for button in buttons: button.visible = false return for i in range(buttons.size()): var button: Control = buttons[i] button.visible = true button.offset_left = (188.0 + float(i) * 164.0) * k button.offset_right = button.offset_left + 148.0 button.offset_top = row_top button.offset_bottom = row_top + row_height func _context_action_buttons() -> Array: var buttons: Array = [] if current_building != null: for available_action in current_building.available_actions(): var button: IconButton = _button_for_building_action(available_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: return current_extractor_index >= 0 or current_building != null \ and current_building.available_actions().has("transfer") func _building_action_icon(building: Node) -> int: if building.primary_action() == "pause": return IconButton.Icon.RESUME if GameState.is_building_paused(building.building_id) \ else IconButton.Icon.PAUSE return int(building.definition.action_icon) func _button_for_building_action(available_action: String) -> IconButton: if available_action == "transfer": return null if available_action == "hangar": return hangar_button return building_action_button func _on_context_action_pressed() -> void: 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] == landing_button: landing_controller._on_enter_pressed() elif actions[0] == hangar_button: _on_hangar_pressed() else: _on_action_pressed() return context_actions_expanded = not context_actions_expanded _layout_context_buttons() func _collapse_context_actions() -> void: if context_actions_expanded: context_actions_expanded = false _layout_context_buttons() func _check_fleet_wipe() -> void: if fleet.ships.is_empty(): var spawn_pos: Variant = GameState.get_mothership_position() fleet.spawn_ships(1, spawn_pos if spawn_pos != null else Vector2.ZERO) func _on_action_pressed() -> void: if current_building == null: return var building_id: int = current_building.building_id var building_action: String = current_building.primary_action() if building_action == "pause": var paused: bool = GameState.toggle_building_paused(building_id) build_menu.set_status("%s %s" % [current_building.definition.display_name, "paused" if paused else "running"]) building_action_button.icon = _building_action_icon(current_building) building_action_button.queue_redraw() elif building_action == "research": skill_menu.open_for(building_id) elif building_action == "craft": craft_building_id = building_id craft_menu.open_for(building_id) elif building_action == "lab": lab_building_id = building_id lab_menu.open_for(building_id) _refresh_lab() elif building_action == "formation": formation_menu.open_menu() elif building_action == "planning": planning_menu.open_menu() elif building_action == "garrison": garrison_building_id = building_id garrison_menu.open_for(building_id) elif building_action == "scanner": scanner_building_id = building_id scanner_menu.open_menu() elif building_action == "staff": station_menu.open_for(building_id) elif building_action == "upkeep": upkeep_menu.open_for(building_id) func _on_scan_requested(definition_id: String) -> void: var scanner: Dictionary = GameState.find_building(scanner_building_id) if scanner.is_empty(): scanner_menu.show_failure(definition_id) return var result: Dictionary = GameState.scan_for_planet(definition_id, Vector2(scanner.position)) if result.is_empty(): scanner_menu.show_failure(definition_id) return scanner_menu.close_menu() world_map.open_map() world_map.focus_world_position(Vector2(result.world_position)) func _on_build_pressed() -> void: pending_build_type = "" build_menu.open_menu() _refresh_build_status() func _on_build_menu_closed() -> void: pending_build_type = "" func _planet_under_fleet() -> Node2D: return planet_field.get_planet_fleet_is_on() func cost_of(type: String) -> int: return int(costs_of(type).get(GameState.RESOURCE_ORE, 0)) func costs_of(type: String) -> Dictionary: return GameState.building_costs(type) func _refresh_build_status() -> void: build_menu.refresh(_planet_under_fleet()) func _refresh_build_option_states() -> void: build_menu.refresh_option_states(_planet_under_fleet()) func _layout_build_menu() -> void: build_menu.refresh(_planet_under_fleet()) func menu_columns(count: int) -> int: return build_menu.menu_columns(count) func menu_rows(count: int) -> int: return build_menu.menu_rows(count) func menu_row_top(row: int, rows: int, columns: int) -> float: return build_menu.menu_row_top(row, rows, columns) func menu_column_spacing(columns: int) -> float: return build_menu.menu_column_spacing(columns) func _show_building_info(type: String) -> void: var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) if definition == null: return var costs: Dictionary = GameState.building_costs(type) var lines: Array = [definition.description] var can_build: bool = true if not GameState.is_building_unlocked(type): lines.append("Locked. Research %s at your mothership." % GameState.research_for_building(type)) can_build = false if can_build and definition.placement_mode == "planet": var planet: Node2D = _planet_under_fleet() if planet == null: lines.append("No planet in reach - fly over one.") can_build = false elif not GameState.is_planet_conquered(planet.planet_id): lines.append("%s is not captured yet - take all its capture points." % planet.planet_name) can_build = false elif GameState.has_extractor_at(planet.planet_id): lines.append("%s already has an extractor." % planet.planet_name) can_build = false else: lines.append("Will mine %s from %s." % [ GameState.resource_name(planet.resource_type), planet.planet_name]) elif can_build: if definition.unique and GameState.has_building_of_type(type): lines.append("WARNING: only one may exist - this destroys your current one.") # A unique structure replaces itself, so the one being demolished is # not in the way. var blocker: Dictionary = GameState.building_blocking(fleet.get_centroid(), type if definition.unique else "") if not blocker.is_empty(): lines.append("Too close to your %s - fly clear and try again." \ % _building_name(String(blocker.get("type", "")))) can_build = false if can_build and not GameState.can_afford_resources(costs, fleet.get_centroid()): can_build = false pending_build_type = type if can_build else "" build_menu.show_info(definition.display_name, "\n\n".join(lines), costs, can_build) func _close_info() -> void: pending_build_type = "" build_menu.close_info() func _confirm_build() -> void: var type: String = pending_build_type if type == "": return pending_build_type = "" build_menu.close_info() if not GameState.is_building_unlocked(type): return var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) if definition == null: return if definition.placement_mode == "planet": var planet: Node2D = _planet_under_fleet() if planet == null or GameState.has_extractor_at(planet.planet_id): build_menu.set_status("No planet in reach any more") return if not GameState.is_planet_conquered(planet.planet_id): build_menu.set_status("%s is not captured yet" % planet.planet_name) return if String(planet.resource_type) == "": build_menu.set_status("%s has nothing to extract" % planet.planet_name) return if not GameState.spend_resources(GameState.building_costs(type), fleet.get_centroid()): build_menu.set_status(_cannot_afford(type)) return GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type, planet.planet_id) build_menu.close_menu() return # BEFORE the demolition below, not after: a unique structure replaces # itself by removing the old one first, so a blocked placement discovered # afterwards would leave the player with neither. The old one is excluded # because it is the thing being replaced. var blocker: Dictionary = GameState.building_blocking(fleet.get_centroid(), type if definition.unique else "") if not blocker.is_empty(): build_menu.set_status("Too close to your %s" \ % _building_name(String(blocker.get("type", "")))) return if definition.unique and GameState.has_building_of_type(type): if not GameState.can_afford_resources(GameState.building_costs(type), fleet.get_centroid()): build_menu.set_status(_cannot_afford(type)) return GameState.remove_buildings_of_type(type) building_field.rebuild_all() _try_place(type) func _building_name(type: String) -> String: var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) return String(definition.display_name) if definition != null else type.capitalize() func _cannot_afford(type: String) -> String: var missing: Array = [] for resource_type in GameState.building_costs(type): var id: String = String(resource_type) var want: int = int(GameState.building_costs(type)[resource_type]) var have: int = GameState.available_resource_at(id, fleet.get_centroid()) if have < want: missing.append("%s (need %d, have %d)" % [GameState.resource_name(id), want, have]) return "" if missing.is_empty() else "Not enough %s" % " and ".join(missing) func _try_place(type: String) -> void: # Re-checked here and not only in the preview: the fleet keeps moving # between opening the menu and confirming, and this is the one place # every space structure is actually placed. var blocker: Dictionary = GameState.building_blocking(fleet.get_centroid()) if not blocker.is_empty(): build_menu.set_status("Too close to your %s" \ % _building_name(String(blocker.get("type", "")))) return if not GameState.spend_resources(GameState.building_costs(type), fleet.get_centroid()): build_menu.set_status(_cannot_afford(type)) return building_field.place(type, fleet.get_centroid()) build_menu.close_menu() func _on_unlock_requested(id: String) -> void: var research_building: Dictionary = GameState.find_building(skill_menu.building_id) if research_building.is_empty(): skill_menu.set_status("The research building is no longer available") return var payment_position: Variant = research_building.get("position", null) if not GameState.unlock(id, payment_position): # A doctrine can be refused for several reasons, but only one of them # is invisible on the node itself: the price in ORE is written there, # the prerequisite is drawn as an unlit link, and research data is # just a number the player has never been told how to get. var points_short: int = GameState.skill_research_point_cost(id) if GameState.research_points < points_short: research_points_menu.open_for_shortfall(points_short, String(GameState.skill_node(id).get("name", "This doctrine"))) skill_menu.refresh(id) return if int(GameState.skill_node(id).get("fleet_bonus", 0)) > 0: fleet.apply_formation() skill_menu.research_completed(id) func _on_unlock_pressed() -> void: var id: String = skill_menu.selected_id() if id != "": _on_unlock_requested(id) func _refresh_skill_menu(id: String = "") -> void: skill_menu.refresh(id) func _format_costs(costs: Dictionary) -> String: var parts := PackedStringArray() for resource_type in GameState.resource_types(): var amount: int = int(costs.get(resource_type, 0)) if amount > 0: parts.append("%d %s" % [amount, GameState.resource_name(resource_type).to_lower()]) return " · ".join(parts) if not parts.is_empty() else "free" func _format_missing_costs(costs: Dictionary) -> String: var parts := PackedStringArray() for resource_type in GameState.resource_types(): var amount: int = int(costs.get(resource_type, 0)) if amount > 0: parts.append("%d more %s" % [amount, GameState.resource_name(resource_type).to_lower()]) return " · ".join(parts) func _on_craft_interceptor() -> void: _on_craft_vessel(GameState.VESSEL_INTERCEPTOR) func _on_craft_shield() -> void: _on_craft_vessel(GameState.VESSEL_SHIELD) func _on_craft_vessel(vessel: String) -> void: if craft_building_id == 0: craft_menu.say("No crafter selected - reopen this panel from the building") return var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel) if definition == null: return if not GameState.queue_order(craft_building_id, vessel): if not GameState.vessel_is_unlocked(vessel): craft_menu.say("%s must be unlocked at a ship lab first" % definition.display_name) return var missing: Dictionary = GameState.outstanding_costs(craft_building_id) if missing.is_empty(): craft_menu.say("%s ordered - building now" % GameState.vessel_name(vessel)) else: craft_menu.say("%s queued - deliver %s here" % [GameState.vessel_name(vessel), _format_missing_costs(missing)]) func _on_queue_slot(index: int) -> void: var orders: Array = GameState.order_list(craft_building_id) if index >= orders.size(): return var vessel: String = GameState.vessel_name(String(orders[index].product)) var refunded: bool = bool(orders[index].started) if GameState.cancel_order(craft_building_id, index): craft_menu.say("%s cancelled%s" % [vessel, " - resources refunded here" if refunded else ""]) func _refresh_craft_status() -> void: if craft_building_id != 0 and craft_menu.building_id != craft_building_id: craft_menu.building_id = craft_building_id craft_menu.resource_bar.building_id = craft_building_id craft_menu.refresh() func _lab_options() -> Array: var options: Array = [] var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel) if definition == null: return options if not GameState.vessel_is_unlocked(lab_vessel): options.append({"unlock": true, "id": definition.unlock_id, "name": definition.unlock_name, "desc": definition.unlock_description, "costs": GameState.research_costs(definition.unlock_id), "icon": definition.unlock_icon if definition.unlock_icon >= 0 else definition.icon}) return options for upgrade in GameState.upgrades_for(lab_vessel): options.append({"unlock": false, "id": upgrade.id, "name": upgrade.name, "desc": upgrade.desc}) return options func _on_lab_vessel(index: int) -> void: var definitions: Array = VESSEL_DEFINITIONS.all() if index >= 0 and index < definitions.size(): lab_vessel = definitions[index].definition_id lab_choice = 0 _refresh_lab() func _on_lab_upgrade(index: int) -> void: if index < 0 or index >= _lab_options().size(): return var confirming: bool = index == lab_choice lab_choice = index lab_menu.set_status("") if confirming and lab_can_buy: _on_lab_buy() else: _refresh_lab() func _on_lab_buy() -> void: var options: Array = _lab_options() if options.is_empty(): return var selected: Dictionary = options[lab_choice] var id: String = String(selected.id) var lab_points: int = GameState.research_point_cost(id) if GameState.research_points < lab_points: # Checked BEFORE the queue call, which would otherwise refuse this # and report it as "already queued or complete" - which it is not. research_points_menu.open_for_shortfall(lab_points, String(selected.name)) return if not GameState.queue_lab_research(lab_building_id, id): lab_menu.set_status("Research is already queued or complete") return lab_menu.set_status("%s queued - deliver %s to this lab" % [ selected.name, _format_costs(GameState.research_costs(id))]) _refresh_lab() func _refresh_lab() -> void: lab_menu.set_status("") lab_menu.hide_research_progress() var definitions: Array = VESSEL_DEFINITIONS.all() lab_menu.render_vessels(definitions, lab_vessel) lab_menu.vessel_heading.text = "%s research · %d RP" % [ GameState.vessel_name(lab_vessel), GameState.research_points] var options: Array = _lab_options() lab_choice = clampi(lab_choice, 0, maxi(options.size() - 1, 0)) var rendered: Array = [] for option in options: var maxed: bool = not bool(option.unlock) and GameState.upgrade_maxed(option.id) var level_note: String = "" if bool(option.unlock) else " %d/%d" % [ GameState.upgrade_level(option.id), int(GameState.upgrade_def(option.id).max_level)] rendered.append({"label": "%s%s" % [option.name, level_note], "costs": option.costs if bool(option.unlock) else GameState.research_costs(option.id), "points": GameState.research_point_cost(String(option.id)), "icon": int(option.get("icon", IconButton.Icon.SHIP_LAB)), "maxed": maxed}) var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel) lab_menu.render_options(rendered, lab_choice, definition.glow_color if definition != null else Color(0.25, 0.85, 1.0)) if options.is_empty(): lab_menu.clear_detail() lab_can_buy = false return var selected: Dictionary = options[lab_choice] lab_menu.show_detail(String(selected.name), String(selected.desc)) var queued: Dictionary = GameState.lab_research_order(lab_building_id) lab_can_buy = queued.is_empty() and (selected.unlock \ or not GameState.upgrade_maxed(selected.id)) if not queued.is_empty(): var queued_points: int = GameState.research_point_cost(String(queued.id)) var progress: float = float(queued.elapsed) \ / maxf(GameState.lab_research_time(), 0.001) if not bool(queued.started) and GameState.research_points < queued_points: lab_menu.show_research_progress( "Need %d research point%s (have %d)" % [queued_points, "" if queued_points == 1 else "s", GameState.research_points], progress) else: lab_menu.show_research_progress( "Researching" if bool(queued.started) else "Waiting for resources", progress) func _on_transfer_pressed() -> void: if current_building == null and current_extractor_index < 0: return transfer_building_id = current_building.building_id if current_building != null else 0 transfer_extractor_index = current_extractor_index transfer_menu.open_destination(transfer_building_id, transfer_extractor_index) func _clear_transfer_context() -> void: transfer_building_id = 0 transfer_extractor_index = -1 func _on_transfer(to_building: bool, amount: int) -> void: _on_resource_transfer(GameState.RESOURCE_ORE, to_building, amount) func _on_gas_transfer(to_building: bool) -> void: _on_resource_transfer(GameState.RESOURCE_GAS, to_building) func _on_resource_transfer(resource_type: String, to_building: bool, amount: int = ALL) -> void: if transfer_building_id == 0 and transfer_extractor_index < 0: transfer_menu.show_status("No resource store selected - reopen this panel from its action") return var moved: int if transfer_extractor_index >= 0: moved = GameState.transfer_resource_to_extractor(transfer_extractor_index, resource_type, amount) if to_building else \ GameState.transfer_resource_from_extractor(transfer_extractor_index, resource_type, amount) else: moved = GameState.transfer_resource_to_building(transfer_building_id, resource_type, amount) if to_building else \ GameState.transfer_resource_to_fleet(transfer_building_id, resource_type, amount) var resource_name: String = GameState.resource_name(resource_type).to_lower() if moved <= 0: transfer_menu.show_status("The fleet is carrying nothing to unload (no %s)" \ % resource_name if to_building else "%s has no %s to load" \ % [_transfer_place_name().capitalize(), resource_name]) elif to_building: transfer_menu.show_status("Moved %d %s into %s" % [ moved, resource_name, _transfer_place_name()]) else: transfer_menu.show_status("Loaded %d %s into the fleet" % [moved, resource_name]) transfer_menu.refresh() func _transfer_place_name() -> String: return transfer_menu.place_name() func _refresh_transfer() -> void: if transfer_menu.building_id != transfer_building_id \ or transfer_menu.extractor_index != transfer_extractor_index: transfer_menu.building_id = transfer_building_id transfer_menu.extractor_index = transfer_extractor_index transfer_menu.refresh() func _on_hangar_pressed() -> void: hangar_menu.open_for(fleet) func _hangar_types() -> Array: var types: Array = [] for definition in VESSEL_DEFINITIONS.fleet(): types.append(String(definition.definition_id)) return types func _resolve_hangar_type(selection: Variant) -> String: if selection is String: return String(selection) var index: int = int(selection) var types: Array = _hangar_types() return String(types[index]) if index >= 0 and index < types.size() else "" func _on_hangar_crew_type(selection: Variant) -> void: var vessel_type: String = _resolve_hangar_type(selection) if vessel_type == "": return if fleet.ships.size() >= GameState.max_fleet_size(): hangar_menu.set_status("Fleet is full (%d/%d) - research capacity at the mothership" \ % [fleet.ships.size(), GameState.max_fleet_size()]) return var stored_index: int = GameState.stored_vessels.find(vessel_type) if stored_index < 0: hangar_menu.set_status("No %s available" % GameState.vessel_name(vessel_type)) return if not fleet.add_ship(GameState.take_stored_vessel(stored_index)): hangar_menu.set_status("Could not crew that vessel") return hangar_menu.set_status("%s joined the fleet" % GameState.vessel_name(vessel_type)) hangar_menu.refresh(fleet) func _on_hangar_store_type(selection: Variant) -> void: var vessel_type: String = _resolve_hangar_type(selection) if vessel_type == "": return var ship_index: int = -1 for i in range(fleet.ships.size()): if String(fleet.ships[i].vessel_type) == vessel_type: ship_index = i break if ship_index < 0: hangar_menu.set_status("No %s in the fleet" % GameState.vessel_name(vessel_type)) return var stored: String = fleet.store_ship(ship_index) if stored == "": hangar_menu.set_status("Keep one ship deployed") return GameState.stored_vessels.append(stored) hangar_menu.set_status("%s stored in the hangar" % GameState.vessel_name(stored)) hangar_menu.refresh(fleet) func _refresh_hangar() -> void: hangar_menu.refresh(fleet) func _layout_hangar() -> void: hangar_menu._layout_sections() func _refresh_garrison() -> void: garrison_menu.refresh() func _refresh_planning() -> void: planning_menu.refresh() func _on_formation_changed() -> void: fleet.apply_formation() func _on_reset_formation() -> void: GameState.reset_formation() fleet.apply_formation() formation_menu.redraw() func _hide_all_actions() -> void: _clear_detected_context() build_button.visible = false