diff --git a/buildings/GasBurnerBuildingDefinition.tres b/buildings/GasBurnerBuildingDefinition.tres index dacd8a9..59589aa 100644 --- a/buildings/GasBurnerBuildingDefinition.tres +++ b/buildings/GasBurnerBuildingDefinition.tres @@ -9,6 +9,7 @@ script = ExtResource("3") input_buffers = { "gas": 40 } +collected_outputs = PackedStringArray("energy") conversion_input = "gas" conversion_output = "energy" conversion_rate = 1.0 @@ -27,6 +28,7 @@ build_resource_costs = { action_icon = 33 menu_icon = 33 visual_scene = ExtResource("2") +capabilities = PackedStringArray("gas_burner") logistics = SubResource("Resource_logistics") map_color = Color(0.95, 0.35, 0.15, 1) map_sides = 5 diff --git a/controllers/BuildController.gd b/controllers/BuildController.gd index d454d63..f226f8d 100644 --- a/controllers/BuildController.gd +++ b/controllers/BuildController.gd @@ -2,216 +2,46 @@ extends Node2D const BUILDING_DEFINITIONS = preload("res://buildings/BuildingDefinitions.gd") const VESSEL_DEFINITIONS = preload("res://vessels/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, 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 -# planet's surface); this only drives the UI and spends resources. +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 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") -@onready var build_status: Label = get_node("../UI/BuildMenu/StatusLabel") -@onready var lab_menu: Control = get_node("../UI/LabMenu") -@onready var lab_vessel_buttons: Array = [ - get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"), - get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"), - get_node("../UI/LabMenu/VesselE"), get_node("../UI/LabMenu/VesselF"), - get_node("../UI/LabMenu/VesselG"), get_node("../UI/LabMenu/VesselH"), -] -@onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel") -@onready var lab_vessel_labels: Array = [ - get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"), - get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"), - get_node("../UI/LabMenu/VesselELabel"), get_node("../UI/LabMenu/VesselFLabel"), - get_node("../UI/LabMenu/VesselGLabel"), get_node("../UI/LabMenu/VesselHLabel"), -] -@onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel") -@onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel") -# What the buy button's `enabled` used to carry. The button is gone (#164) but -# the question it answered - is this option actually purchasable right now - -# still gates the confirming tap. -var lab_can_buy: bool = false -@onready var lab_close_button: IconButton = get_node("../UI/LabMenu/CloseButton") -@onready var lab_upgrade_buttons: Array = [ - get_node("../UI/LabMenu/Upgrade0"), get_node("../UI/LabMenu/Upgrade1"), - get_node("../UI/LabMenu/Upgrade2"), get_node("../UI/LabMenu/Upgrade3"), -] -@onready var lab_upgrade_labels: Array = [ - get_node("../UI/LabMenu/Upgrade0Label"), get_node("../UI/LabMenu/Upgrade1Label"), - get_node("../UI/LabMenu/Upgrade2Label"), get_node("../UI/LabMenu/Upgrade3Label"), -] -@onready var craft_shield_button: IconButton = get_node("../UI/CraftMenu/ShieldButton") -@onready var queue_slots: Array = [ - get_node("../UI/CraftMenu/QueueSlot0"), get_node("../UI/CraftMenu/QueueSlot1"), - get_node("../UI/CraftMenu/QueueSlot2"), get_node("../UI/CraftMenu/QueueSlot3"), -] -@onready var queue_detail: Label = get_node("../UI/CraftMenu/QueueDetail") +@onready var transfer_button: IconButton = get_node("../UI/TransferButton") @onready var hangar_button: IconButton = get_node("../UI/HangarButton") -@onready var hangar_menu: Control = get_node("../UI/HangarMenu") -@onready var hangar_status: Label = get_node("../UI/HangarMenu/StatusLabel") -@onready var hangar_available_title: Label = get_node("../UI/HangarMenu/AvailableTitle") -@onready var hangar_fleet_title: Label = get_node("../UI/HangarMenu/FleetTitle") -@onready var hangar_close_button: IconButton = get_node("../UI/HangarMenu/CloseButton") -@onready var hangar_available_slots: Array = [ - get_node("../UI/HangarMenu/AvailableInterceptor"), - get_node("../UI/HangarMenu/AvailableShield"), - get_node("../UI/HangarMenu/AvailableGasBomber"), - get_node("../UI/HangarMenu/AvailableCarrier"), - get_node("../UI/HangarMenu/AvailableLaser"), - get_node("../UI/HangarMenu/AvailableDistorder"), -] -@onready var hangar_available_labels: Array = [ - get_node("../UI/HangarMenu/AvailableInterceptorLabel"), - get_node("../UI/HangarMenu/AvailableShieldLabel"), - get_node("../UI/HangarMenu/AvailableGasBomberLabel"), - get_node("../UI/HangarMenu/AvailableCarrierLabel"), - get_node("../UI/HangarMenu/AvailableLaserLabel"), - get_node("../UI/HangarMenu/AvailableDistorderLabel"), -] -@onready var hangar_fleet_slots: Array = [ - get_node("../UI/HangarMenu/FleetInterceptor"), - get_node("../UI/HangarMenu/FleetShield"), - get_node("../UI/HangarMenu/FleetGasBomber"), - get_node("../UI/HangarMenu/FleetCarrier"), - get_node("../UI/HangarMenu/FleetLaser"), - get_node("../UI/HangarMenu/FleetDistorder"), -] -@onready var hangar_fleet_labels: Array = [ - get_node("../UI/HangarMenu/FleetInterceptorLabel"), - get_node("../UI/HangarMenu/FleetShieldLabel"), - get_node("../UI/HangarMenu/FleetGasBomberLabel"), - get_node("../UI/HangarMenu/FleetCarrierLabel"), - get_node("../UI/HangarMenu/FleetLaserLabel"), - get_node("../UI/HangarMenu/FleetDistorderLabel"), -] -var build_menu_entries: Array = [] -# Which vessel the lab screen is showing, and which visible upgrade card is -# selected. Each vessel gets its own small set of direct cards; the detail and -# Buy controls below them only describe the one the player tapped. +# 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") + +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_building_id: int = 0 -@onready var planet_field: Node2D = get_node("../PlanetField") -@onready var info_popup: Control = get_node("../UI/BuildMenu/InfoPopup") -@onready var info_title: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/TitleLabel") -@onready var info_message: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/MessageLabel") -# Typed Control, not PriceTag, and deliberately. PriceTag reads the GameState -# autoload, and an `extends SceneTree` test script that so much as names a -# class like that fails to compile - GameState is not an identifier in that -# context - which then poisons the scene's copy of the same script. The nodes -# are duck-typed everywhere for that reason; see tests/test_menu_price_unification.gd. -@onready var info_cost: Control = get_node("../UI/BuildMenu/InfoPopup/Panel/CostNumber") -@onready var info_build_button: IconButton = get_node("../UI/BuildMenu/InfoPopup/Panel/BuildButton") -@onready var info_close_button: IconButton = get_node("../UI/BuildMenu/InfoPopup/Panel/CloseButton") - -@onready var craft_menu: Control = get_node("../UI/CraftMenu") -@onready var craft_interceptor_button: IconButton = get_node("../UI/CraftMenu/InterceptorButton") -@onready var craft_close_button: IconButton = get_node("../UI/CraftMenu/CloseButton") -@onready var craft_status: Label = get_node("../UI/CraftMenu/StatusLabel") -@onready var craft_interceptor_label: Label = get_node("../UI/CraftMenu/InterceptorLabel") -@onready var craft_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel") -@onready var craft_gas_bomber_button: IconButton = get_node("../UI/CraftMenu/GasBomberButton") -@onready var craft_gas_bomber_label: Label = get_node("../UI/CraftMenu/GasBomberLabel") -@onready var craft_carrier_button: IconButton = get_node("../UI/CraftMenu/CarrierButton") -@onready var craft_carrier_label: Label = get_node("../UI/CraftMenu/CarrierLabel") -@onready var craft_laser_button: IconButton = get_node("../UI/CraftMenu/LaserButton") -@onready var craft_laser_label: Label = get_node("../UI/CraftMenu/LaserLabel") -@onready var craft_distorder_button: IconButton = get_node("../UI/CraftMenu/DistorderButton") -@onready var craft_distorder_label: Label = get_node("../UI/CraftMenu/DistorderLabel") -@onready var craft_vessel_buttons: Array = [ - craft_interceptor_button, craft_shield_button, craft_gas_bomber_button, craft_carrier_button, - craft_laser_button, craft_distorder_button, -] -@onready var craft_vessel_labels: Array = [ - craft_interceptor_label, craft_shield_label, craft_gas_bomber_label, craft_carrier_label, - craft_laser_label, craft_distorder_label, -] - -# --- 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 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") -@onready var transfer_close_button: IconButton = get_node("../UI/TransferMenu/CloseButton") -@onready var transfer_list: ResourceTransferList = get_node("../UI/TransferMenu/ResourceList") - -# Every building menu carries the same header (#24). Held as a list because -# the only thing any of them needs is to be told which building it is -# reporting on when its panel opens. -@onready var resource_bars: Array = [ - get_node("../UI/CraftMenu/Bar"), get_node("../UI/SkillMenu/Bar"), - get_node("../UI/LabMenu/Bar"), -] - -# #103: the outpost's own panel. garrison_building_id is remembered for the -# same reason craft_building_id is - _process() clears current_building while -# a menu is open, so by the next frame the panel would not know which outpost -# it belongs to and every button in it would bail out at its `== 0` guard. -@onready var garrison_menu: Control = get_node("../UI/GarrisonMenu") -@onready var garrison_panel: Control = get_node("../UI/GarrisonMenu/Panel") -@onready var garrison_status: Label = get_node("../UI/GarrisonMenu/StatusLabel") -@onready var garrison_close_button: IconButton = get_node("../UI/GarrisonMenu/CloseButton") -var garrison_building_id: int = 0 - -# #106: the standing-order board. No building id is remembered because the -# plan is global - there is one plan for the whole game, and a second -# planning centre is another place to read it rather than a competing one. -@onready var planning_menu: Control = get_node("../UI/PlanningMenu") -@onready var planning_panel: Control = get_node("../UI/PlanningMenu/Panel") -@onready var planning_status: Label = get_node("../UI/PlanningMenu/StatusLabel") -@onready var planning_close_button: IconButton = get_node("../UI/PlanningMenu/CloseButton") - -@onready var skill_menu: Control = get_node("../UI/SkillMenu") -@onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree") -@onready var skill_detail: Label = get_node("../UI/SkillMenu/DetailLabel") -@onready var skill_cost: Control = get_node("../UI/SkillMenu/CostNumber") -@onready var skill_status: Label = get_node("../UI/SkillMenu/StatusLabel") -@onready var skill_close_button: IconButton = get_node("../UI/SkillMenu/CloseButton") - -@onready var formation_menu: Control = get_node("../UI/FormationMenu") -@onready var formation_editor: Control = get_node("../UI/FormationMenu/Editor") -@onready var formation_close_button: IconButton = get_node("../UI/FormationMenu/CloseButton") -@onready var formation_reset_button: IconButton = get_node("../UI/FormationMenu/ResetButton") - -# Which structure the popup is currently offering to build, or "" when it is -# only explaining something that cannot be built right now. -var pending_build_type: String = "" -# Which crafter the craft menu is showing (see _on_action_pressed). -var craft_building_id: int = 0 -# The craft panel refreshes every frame while it is open (so a delivery -# arriving visibly works the queue down). That overwrote any message the -# moment after it was set, which made a working button look dead - the store -# button in particular. Messages therefore have to outlive a refresh. -var craft_message: String = "" -var craft_message_until: int = 0 - -# Which building the transfer panel is moving ore in and out of. Remembered -# for the same reason craft_building_id is: _process() clears -# current_building while any menu is open, so by the next frame the panel -# would not know which building it belongs to. +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 current_building: Node2D = null var pending_building: Node2D = null @@ -222,49 +52,52 @@ var pending_time: float = 0.0 var context_actions_expanded: bool = false -# Gap between the last resource row and the first row of HUD buttons. -const HUD_COLUMN_MARGIN: float = 18.0 -# Vertical gap between one row of HUD buttons and the next. -const HUD_ROW_GAP: float = 16.0 +func _ready() -> void: + _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) + + 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() -# Seats the top-anchored HUD button columns below the resource readout instead -# of at the fixed y the scene authored. -# -# ResourceReadout builds one row per resource type AT RUNTIME, so how far down -# it reaches is not knowable when the scene is laid out - the scene's guess -# was 154, and four resources actually reach 270, straight over the build -# button sitting at a hardcoded 160. Every resource type added since made it -# worse. Deriving the column top from the readout's real bottom makes the -# overlap impossible rather than merely fixed for today's resource count. -# -# This TRANSLATES the block rather than re-flowing it, so the rows keep their -# spacing relative to one another - BuildingActionButton and HangarButton -# deliberately share a row, being mutually exclusive, and a flow layout would -# pull them apart. func _seat_hud_columns() -> void: var readout: ResourceReadout = get_node_or_null("../UI/ResourceReadout") if readout == null: return - # Both columns, not just the left one: the readout spans the full width - # (the fleet hold column sits at x 356), so the second column of buttons - # collides with it exactly as the first does. - var buttons: Array = [ - build_button, transfer_button, - action_button, landing_button, - building_action_button, hangar_button, - ] + 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 - var shift: float = readout.content_bottom() + HUD_COLUMN_MARGIN - highest - # Only ever pushed down, never pulled up: with few enough resources the - # authored layout already clears the readout, and there is no reason to - # drag the HUD away from where the scene deliberately put it. if shift <= 0.0: return for button in buttons: @@ -272,225 +105,70 @@ func _seat_hud_columns() -> void: button.position.y += shift -func _ready() -> void: - _seat_hud_columns() - _layout_hangar() - _build_craft_price_strips() - _build_lab_price_tags() - _layout_craft_menu() - build_button.pressed.connect(_on_build_pressed) - build_close_button.pressed.connect(_close_build_menu) - garrison_close_button.pressed.connect(_close_garrison_menu) - garrison_panel.changed.connect(_refresh_garrison) - planning_close_button.pressed.connect(_close_planning_menu) - planning_panel.changed.connect(_refresh_planning) - _configure_build_menu_entries() - lab_close_button.pressed.connect(func(): lab_menu.visible = false) - for i in range(lab_vessel_buttons.size()): - lab_vessel_buttons[i].pressed.connect(_on_lab_vessel.bind(i)) - for i in range(lab_upgrade_buttons.size()): - lab_upgrade_buttons[i].pressed.connect(_on_lab_upgrade.bind(i)) - info_close_button.pressed.connect(_close_info) - info_build_button.pressed.connect(_confirm_build) - - action_button.pressed.connect(_on_context_action_pressed) - building_action_button.pressed.connect(_on_action_pressed) - - for i in range(mini(craft_vessel_buttons.size(), VESSEL_DEFINITIONS.fleet().size())): - var definition: Resource = VESSEL_DEFINITIONS.fleet()[i] - craft_vessel_buttons[i].pressed.connect(_on_craft_vessel.bind(definition.definition_id)) - craft_close_button.pressed.connect(func(): craft_menu.visible = false) - - transfer_button.pressed.connect(_on_transfer_pressed) - hangar_button.pressed.connect(_on_hangar_pressed) - hangar_close_button.pressed.connect(func(): hangar_menu.visible = false) - for i in range(hangar_available_slots.size()): - hangar_available_slots[i].pressed.connect(_on_hangar_crew_type.bind(i)) - hangar_fleet_slots[i].pressed.connect(_on_hangar_store_type.bind(i)) - for i in range(queue_slots.size()): - queue_slots[i].pressed.connect(_on_queue_slot.bind(i)) - transfer_close_button.pressed.connect(func(): transfer_menu.visible = false) - transfer_list.transfer_requested.connect(_on_resource_transfer) - - formation_close_button.pressed.connect(func(): formation_menu.visible = false) - formation_reset_button.pressed.connect(_on_reset_formation) - formation_editor.formation_changed.connect(_on_formation_changed) - - skill_close_button.pressed.connect(func(): skill_menu.visible = false) - skill_tree.node_picked.connect(_on_skill_node_picked) - skill_tree.node_confirmed.connect(_on_skill_node_confirmed) - - build_menu.visible = false - info_popup.visible = false - craft_menu.visible = false - formation_menu.visible = false - skill_menu.visible = false - lab_menu.visible = false - 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 - # #128: the prologue's "movement and nothing else" used to be applied by - # _process on the first frame. A dialog now stops the tree, and the - # prologue opens ON one - so that first frame never came and every action - # button was left on screen behind the opening line. Applied here as well, - # where it does not depend on the game running. - if GameState.intro_active: - _hide_all_actions() - _layout_context_buttons() +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]: + if menu.visible: + return true + return false -func _configure_build_menu_entries() -> void: - build_menu_entries.clear() - for definition in BUILDING_DEFINITIONS.all(): - if definition.menu_button_name == "": - continue - var button: IconButton = get_node("../UI/BuildMenu/%s" % definition.menu_button_name) - var label: Label = get_node("../UI/BuildMenu/%s" % definition.menu_label_name) - var cost_tag: Control = get_node("../UI/BuildMenu/%s" % definition.menu_cost_name) - button.icon = definition.menu_icon - button.pressed.connect(_show_building_info.bind(String(definition.definition_id))) - label.text = definition.display_name - cost_tag.show_price(GameState.building_costs(String(definition.definition_id))) - build_menu_entries.append([definition.definition_id, button, label, cost_tag, definition]) - - -# 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 build_menu.visible or craft_menu.visible \ - or formation_menu.visible or skill_menu.visible or transfer_menu.visible \ - or hangar_menu.visible + 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): - # Buttons consume their own touch; reaching this handler means the tap - # landed somewhere else, which dismisses an expanded action list. + or event is InputEventMouseButton and event.pressed \ + and event.button_index == MOUSE_BUTTON_LEFT): _collapse_context_actions() - - # The formation editor and the skill tree are driven from here rather than - # via _gui_input, for the same reason as the joystick: a Control that - # consumes touches through the GUI system swallows them before they get - # here, which is what made dragging a formation slot silently do nothing. - if skill_menu.visible: - if event is InputEventScreenTouch: - skill_tree.handle_touch(event) - elif event is InputEventScreenDrag: - skill_tree.handle_drag(event) - elif event is InputEventMouseButton: - # #93: a click is not a touch. The project no longer emulates one - # from the other, so without this branch the whole menu is inert - # under a mouse - nothing selects, nothing scrolls, and there is - # no other way to research anything on a desktop. - skill_tree.handle_mouse_button(event) - elif event is InputEventMouseMotion: - skill_tree.handle_mouse_motion(event) - # Handled either way. A touch that lands on the panel but misses a - # node is still a touch ON THE PANEL - letting it through moved the - # fleet and pinch-zoomed the world behind the menu, because - # GameController sits on the root node and sees whatever this leaves - # unhandled. + if skill_menu.handle_unhandled_input(event): get_viewport().set_input_as_handled() return - if formation_menu.visible: - if event is InputEventScreenTouch: - formation_editor.handle_touch(event) - elif event is InputEventScreenDrag: - formation_editor.handle_drag(event) - elif event is InputEventMouseButton: - # #121: a click is not a touch. Same routing gap as #93's - # research tree - the panel is transparent to GUI input so the - # raw handlers can see the drag, which means nothing turns a - # click into one on the way. - formation_editor.handle_mouse_button(event) - elif event is InputEventMouseMotion: - formation_editor.handle_mouse_motion(event) + if formation_menu.handle_unhandled_input(event): get_viewport().set_input_as_handled() return - if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible \ - or transfer_menu.visible or hangar_menu.visible: - # These panels have no raw-input handler of their own, but the world - # behind them must still not react. + if world_map.visible or _menu_is_open(): get_viewport().set_input_as_handled() func _process(delta: float) -> void: _check_fleet_wipe() - - # #122: the prologue leaves the player movement and nothing else. Handled - # at the top, before any of the context detection below, so nothing can - # turn a button back on further down. if GameState.intro_active: _hide_all_actions() return - # Build is permanently under the resource counter (see - # _layout_context_buttons) - it is the one action button nothing else - # ever hides, so the prologue taking it away is the only thing that has - # to give it back. if not build_button.visible: build_button.visible = true - if build_menu.visible: - # Keep the greyed state live while the menu is open, so flying onto - # a planet lights the option up without reopening it. - _refresh_build_option_states() - + build_menu.refresh_option_states(_planet_under_fleet()) if craft_menu.visible: - # Live, so a delivery arriving while the menu is open visibly works - # the queue down instead of looking stuck. - _refresh_craft_status() - + craft_menu.refresh() if transfer_menu.visible: - # Also live: a hauler banking ore while the panel is open should make - # the "to fleet" side become available without a reopen. - _refresh_transfer() - - if world_map.visible or build_menu.visible or craft_menu.visible \ - or formation_menu.visible or skill_menu.visible or lab_menu.visible \ - or transfer_menu.visible or hangar_menu.visible: - current_building = null - 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 + transfer_menu.refresh() + if world_map.visible or _menu_is_open(): + _clear_detected_context() return - # Debounced exactly like the enter-atmosphere prompt, so the action - # button can't flicker while the fleet drifts on the interact boundary. 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_landing_planet: Node2D = landing_controller.current_planet + 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_landing_planet == current_landing_planet): + and detected_planet == current_landing_planet): return - current_building = pending_building current_extractor_index = pending_extractor_index - current_landing_planet = detected_landing_planet + current_landing_planet = detected_planet context_actions_expanded = false hangar_button.visible = false building_action_button.visible = false @@ -502,21 +180,25 @@ func _process(delta: float) -> void: 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.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: - # Build is permanently under the resource counter. The contextual berth - # directly below it is compact until it has more than one real choice. - # - # "Directly below it" is MEASURED from the build button rather than - # written down as a y. This used to hardcode 324, which was the right - # answer only while the build button sat at the 160 the scene authored - - # once _seat_hud_columns() started moving the whole block down to clear - # the resource readout, this ran on the next context change and dropped - # the action row straight back on top of the build button. var row_top: float = build_button.position.y + build_button.size.y + HUD_ROW_GAP var row_height: float = build_button.size.y action_button.offset_top = row_top @@ -526,9 +208,6 @@ func _layout_context_buttons() -> void: for button in buttons: button.visible = false return - # Expand sideways so the controls stay in the same visual band instead of - # pushing the player down a vertical stack. Three actions fit across the - # 720px phone layout with the same generous 148px touch targets. for i in range(buttons.size()): var button: Control = buttons[i] button.visible = true @@ -541,8 +220,8 @@ func _layout_context_buttons() -> void: func _context_action_buttons() -> Array: var buttons: Array = [] if current_building != null: - for action in current_building.available_actions(): - var button: IconButton = _button_for_building_action(action) + 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: @@ -551,14 +230,10 @@ func _context_action_buttons() -> Array: func _has_transfer_context() -> bool: - if current_extractor_index >= 0: - return true - return current_building != null and current_building.available_actions().has("transfer") + return current_extractor_index >= 0 or current_building != null \ + and current_building.available_actions().has("transfer") -# A pause action's icon shows what the button will DO, not what the building -# currently is: two bars while it runs, a play triangle while it is stopped. -# Everything else uses the icon its definition names. func _building_action_icon(building: Node) -> int: if building.primary_action() == "pause": return IconButton.Icon.RESUME if GameState.is_building_paused(building.building_id) \ @@ -566,10 +241,10 @@ func _building_action_icon(building: Node) -> int: return int(building.definition.action_icon) -func _button_for_building_action(action: String) -> IconButton: - if action == "transfer": +func _button_for_building_action(available_action: String) -> IconButton: + if available_action == "transfer": return null - if action == "hangar": + if available_action == "hangar": return hangar_button return building_action_button @@ -592,433 +267,60 @@ func _on_context_action_pressed() -> void: func _collapse_context_actions() -> void: - if not context_actions_expanded: - return - context_actions_expanded = false - _layout_context_buttons() + if context_actions_expanded: + context_actions_expanded = false + _layout_context_buttons() func _check_fleet_wipe() -> void: - if not fleet.ships.is_empty(): - return - # Whole fleet destroyed: rebuild a single vessel at the mothership, or - # at the world origin when none has been built yet. - var spawn_pos: Variant = GameState.get_mothership_position() - fleet.spawn_ships(1, spawn_pos if spawn_pos != null else Vector2.ZERO) + 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 - # Whichever panel opens, its header reports on the building it was opened - # from. Set once here rather than in each branch, so a new building menu - # cannot forget to do it and silently show a blank HERE column. - for bar in resource_bars: - bar.building_id = current_building.building_id - - var action: String = current_building.primary_action() - if action == "pause": - # No panel: the button IS the control. Toggling here rather than - # opening something keeps a one-bit setting to a single tap. - var paused: bool = GameState.toggle_building_paused(current_building.building_id) - build_status.text = "%s %s" % [current_building.definition.display_name, - "paused" if paused else "running"] + 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() - return - if action == "research": - skill_menu.visible = true - skill_tree.reset_scroll() - _refresh_skill_menu() - elif action == "craft": - # Remembered explicitly: _process() clears current_building while any - # menu is open, so by the next frame the panel would no longer know - # which crafter it belongs to. Losing this line makes every button in - # the panel bail out at its `== 0` guard while the panel itself still - # opens - which reads as "the buttons do nothing". - craft_building_id = current_building.building_id - craft_menu.visible = true - _refresh_craft_status() - elif action == "lab": - lab_building_id = current_building.building_id - lab_menu.visible = true + 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 action == "formation": - formation_menu.visible = true - formation_editor.queue_redraw() - elif action == "planning": - planning_menu.visible = true - _refresh_planning() - elif action == "garrison": - garrison_building_id = current_building.building_id - garrison_panel.building_id = garrison_building_id - garrison_menu.visible = true - _refresh_garrison() + 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) func _on_build_pressed() -> void: - build_menu.visible = true - info_popup.visible = false pending_build_type = "" + build_menu.open_menu() _refresh_build_status() +func _on_build_menu_closed() -> void: + pending_build_type = "" + + func _planet_under_fleet() -> Node2D: - # An extractor mines a specific planet, so one has to be within reach. return planet_field.get_planet_fleet_is_on() -# Grid geometry, shared with the layout below. The menu is rebuilt from -# whatever is currently researched (#21) rather than being a fixed -# arrangement with holes punched in it. -# -# Two profiles, because the grid ran out of room. #54 already tightened the -# rows to 223px to fit a fourth, explicitly to avoid a scrollable menu, and a -# ninth structure (#106's planning centre) needs a FIFTH row that will not -# fit at any spacing a button, a label and a cost can share. So past eight -# entries the grid goes to three columns instead, which takes nine into three -# rows - fewer than it uses today, not more. Narrower columns mean narrower -# labels, so those get a taller box to wrap into. -# The build menu's grid. -# -# #136: rows used to be a written-down list of tops, read with -# `tops[mini(row, tops.size() - 1)]`. That clamp silently stacked whatever -# would not fit - the gas burner drawn exactly on top of the transport hub. -# -# #144: and the band the rows live in was written down too, as a pair of -# constants measured against a 1280-tall canvas. The header above it is -# anchored to the TOP of the panel and the grid is anchored to its CENTRE, -# so the gap between them is half the canvas height - which means the -# constants were only ever right at one window shape. At 1080 tall the grid -# started 4px ABOVE the status line and drew straight through it, which is -# what the ticket reported. -# -# So nothing about the geometry is written down against a screen size any -# more. The band is measured from the header that is actually there, the row -# pitch closes up to fit it (never below a row's own height, which is what -# makes overlapping impossible rather than unlikely), and the column count -# rises until the rows fit - a short panel gets a wide grid instead of one -# that runs off the bottom of it. -const MENU_BUTTON_HALF: float = 64.0 -const MENU_BUTTON_H: float = 100.0 -const MENU_COST_HALF: float = 74.0 -# #147: a price is one resource per line, so a row has to leave room for the -# most any structure asks for - three, at the workshop. -const MENU_COST_H: float = 86.0 -const MENU_LABEL_OFFSET: float = 106.0 -const MENU_LABEL_GAP: float = 4.0 - -# The widest a column is ever given, however much room there is: past this -# the entries stop reading as a grid and start reading as a scattering. -const MENU_NATURAL_SPACING: float = 280.0 -# The narrowest a column may be squeezed to - a button plus a little air. -const MENU_MIN_SPACING: float = MENU_BUTTON_HALF * 2.0 + 12.0 -# A caption gets most of its column, not all of it. -const MENU_LABEL_SHARE: float = 0.46 -# Below this a caption is assumed to need two lines. -const MENU_ONE_LINE_HALF: float = 120.0 -const MENU_LABEL_H_ONE: float = 52.0 -const MENU_LABEL_H_TWO: float = 68.0 -# Breathing room around a measured caption and a measured price, so glyphs -# never sit flush against the row below them. -const MENU_LABEL_PADDING: float = 10.0 -const MENU_COST_PADDING: float = 12.0 - -const MENU_SIDE_MARGIN: float = 24.0 -const MENU_HEADER_GAP: float = 16.0 -const MENU_BOTTOM_MARGIN: float = 28.0 -const MENU_MAX_COLUMNS: int = 6 - - -# A centre-anchored grid living under a top-anchored header, which is the -# shape both the build menu and the craft menu have (#144, #147). The gap -# between the two depends on the panel's HEIGHT, so it can never be written -# down as a constant - it has to be measured from the header that is -# actually there. -func grid_band(header_bottom: float, panel_height: float) -> Vector2: - var half: float = panel_height * 0.5 - if half <= 0.0: - half = 640.0 - return Vector2(header_bottom + MENU_HEADER_GAP - half, half - MENU_BOTTOM_MARGIN) - - -# Where one row of such a grid starts. The pitch closes up to fit the band -# and is floored at the row's own height, so rows can tighten but never land -# on each other. -func grid_row_top(row: int, rows: int, row_height: float, band: Vector2, - natural_pitch: float) -> float: - var pitch: float = natural_pitch - if rows > 1: - pitch = minf(pitch, (band.y - band.x - row_height) / float(rows - 1)) - return band.x + maxf(pitch, row_height) * float(row) - - -# The vertical band the grid may occupy, in the menu's own centre-anchored -# space: (top, bottom). -# -# MEASURED, not written down (#144). The header is anchored to the top of the -# panel and the grid to its centre, so the two only agree at one canvas -# height - and the pair of constants this replaces happened to be the ones -# for 1280. Reading the status line's own bottom edge and the panel's own -# height makes the answer right at every window shape, including the one the -# player is actually running. -func menu_band() -> Vector2: - return grid_band(build_status.offset_bottom, build_menu.size.y) - - -# How wide one column is given. Never wider than looks like a grid, never -# narrower than a button, and otherwise whatever the panel has room for. -func menu_column_spacing(columns: int) -> float: - var room: float = build_menu.size.x - MENU_SIDE_MARGIN * 2.0 - if room <= 0.0: - room = 672.0 - return maxf(MENU_MIN_SPACING, minf(MENU_NATURAL_SPACING, room / float(columns))) - - -func menu_label_half(columns: int) -> float: - return menu_column_spacing(columns) * MENU_LABEL_SHARE - - -# How tall a caption of `text` really is at this column width, measured -# against the font that will draw it. -# -# This used to be a guess from the column width alone: wide column, assume one -# line; narrow column, assume two. That is right for most names and wrong for -# the long ones - "Spacial Workshop" wrapped to two lines in a column the rule -# called wide enough for one, and the second line came down on top of the -# price underneath it (#164). A name is as tall as it is; asking the font is -# both simpler and correct for names nobody has written yet. -func measured_label_height(label: Label, text: String, columns: int) -> float: - var font: Font = label.get_theme_font("font") - var size: int = label.get_theme_font_size("font_size") - if font == null or size <= 0 or text == "": - return MENU_LABEL_H_ONE - var width: float = menu_label_half(columns) * 2.0 - var height: float = font.get_multiline_string_size(text, - HORIZONTAL_ALIGNMENT_CENTER, width, size).y - # A floor of the one-line height keeps single-line captions on the rhythm - # the menu already had, rather than tightening around the glyphs. - return maxf(MENU_LABEL_H_ONE, height + MENU_LABEL_PADDING) - - -# The tallest caption among the entries currently shown. The grid is a grid - -# every row uses the same offsets - so one long name sets the height for all -# of them, which is what keeps the columns lined up. -func menu_label_height(columns: int) -> float: - var tallest: float = MENU_LABEL_H_ONE - for entry in build_menu_entries: - var label: Label = entry[2] - if label.visible: - tallest = maxf(tallest, measured_label_height(label, label.text, columns)) - return tallest - - -# The tallest price among the entries currently shown, asked of the tags -# themselves rather than assumed. A structure costing three resources stacks -# three lines (see PriceTag), and the fixed height this replaces was sized for -# two - so the third line ran into whatever the grid put underneath (#164). -func menu_cost_height() -> float: - var tallest: float = MENU_COST_H - for entry in build_menu_entries: - var tag: Control = entry[3] - if tag.visible and tag.has_method("wanted_height"): - tallest = maxf(tallest, float(tag.wanted_height()) + MENU_COST_PADDING) - return tallest - - -func menu_cost_offset(columns: int) -> float: - return MENU_LABEL_OFFSET + menu_label_height(columns) + MENU_LABEL_GAP - - -# How tall one row is, end to end - the cost strip is the last thing in it. -func menu_row_height(columns: int) -> float: - return menu_cost_offset(columns) + menu_cost_height() - - -# The fewest columns whose rows fit the band. Two is the shape the menu is -# meant to have; wider is what a full catalog - or a short window - forces. -func menu_columns(count: int) -> int: - var widest: int = maxi(mini(MENU_MAX_COLUMNS, - int(floor((build_menu.size.x - MENU_SIDE_MARGIN * 2.0) / MENU_MIN_SPACING))), 2) - for columns in range(2, widest + 1): - if _rows_fit(int(ceil(float(count) / float(columns))), columns): - return columns - return widest - - -func menu_rows(count: int) -> int: - var columns: int = menu_columns(count) - return maxi(int(ceil(float(count) / float(columns))), 1) - - -func _rows_fit(rows: int, columns: int) -> bool: - var height: float = menu_row_height(columns) - var band: Vector2 = menu_band() - if rows <= 1: - return band.y - band.x >= height - return (band.y - band.x - height) / float(rows - 1) >= height - - -# Where one row starts. Top of the band, with the pitch closed up when it is -# tight - but never below the row height, so two rows cannot land on each -# other however many structures there are, or however short the window (#136, -# #144). -func menu_row_top(row: int, rows: int, columns: int) -> float: - return grid_row_top(row, rows, menu_row_height(columns), menu_band(), - MENU_NATURAL_SPACING) - - -func _place_entry(node: Control, cx: float, top: float, half: float, height: float) -> void: - node.offset_left = cx - half - node.offset_right = cx + half - node.offset_top = top - node.offset_bottom = top + height - - -func _layout_build_menu() -> void: - # Structures the player has not researched are hidden outright (#21), and - # what remains is repacked so there are no gaps where they used to be. - # This reverses the earlier decision to always list everything greyed - # out; an option you cannot explain is apparently worse than one you - # cannot see yet. - var shown: Array = [] - for entry in build_menu_entries: - var visible: bool = GameState.is_building_unlocked(entry[0]) - entry[1].visible = visible - entry[2].visible = visible - entry[3].visible = visible - if visible: - # Priced BEFORE anything is measured. The row's height now comes - # from the price tag's own wanted_height(), and _refresh_build_ - # status() sets prices only after calling this - so on the first - # layout every tag was still empty and every row was sized for a - # price that was not there yet. show_price() early-outs when the - # price has not changed, so restating it here costs nothing. - entry[3].show_price(GameState.building_costs(String(entry[0]))) - shown.append(entry) - - var columns: int = menu_columns(shown.size()) - var rows: int = menu_rows(shown.size()) - var spacing: float = menu_column_spacing(columns) - var label_half: float = menu_label_half(columns) - var label_h: float = menu_label_height(columns) - var cost_offset: float = menu_cost_offset(columns) - var cost_height: float = menu_cost_height() - - for i in range(shown.size()): - var row: int = i / columns - # A short last row is CENTRED rather than left-packed, so it does not - # read as a row with something missing beside it. - var in_row: int = mini(shown.size() - row * columns, columns) - var col: int = i % columns - var cx: float = (float(col) - (float(in_row) - 1.0) * 0.5) * spacing - var top: float = menu_row_top(row, rows, columns) - _place_entry(shown[i][1], cx, top, MENU_BUTTON_HALF, MENU_BUTTON_H) - _place_entry(shown[i][2], cx, top + MENU_LABEL_OFFSET, label_half, label_h) - _place_entry(shown[i][3], cx, top + cost_offset, MENU_COST_HALF, cost_height) - - -func _refresh_build_status() -> void: - _layout_build_menu() - # Researched but not usable right here is still shown, greyed: an - # extractor needs a planet under the fleet, which is a "not now" rather - # than a "not yet". - _refresh_build_option_states() - - # Prices are shown under each option now, so this only needs to say - # whether the fleet is somewhere an extractor could go. - var planet: Node2D = _planet_under_fleet() - if planet == null: - build_status.text = "" - elif not GameState.is_planet_conquered(planet.planet_id): - build_status.text = "Over %s - capture it before building an extractor" % planet.planet_name - else: - build_status.text = "Over %s - an extractor can be built here" % planet.planet_name - - -func _refresh_build_option_states() -> void: - for entry in build_menu_entries: - var definition: Resource = entry[4] - # #138: a price now names several resources, and what the player - # holds of each is on the tag - so it is re-stated here, live, the - # way the craft menu's already was. Re-stating the same price is - # cheap; PriceTag only rebuilds when the price itself changes. - entry[3].show_price(GameState.building_costs(String(definition.definition_id))) - var enabled: bool = GameState.is_building_unlocked(definition.definition_id) - if definition.placement_mode == "planet": - var planet: Node2D = _planet_under_fleet() - enabled = enabled and planet != null and GameState.is_planet_conquered(planet.planet_id) - entry[1].enabled = enabled - entry[1].queue_redraw() - - -# Everything the player can PRESS, as opposed to steer with. Used by the -# prologue (#122), which takes them all away at once. -func _hide_all_actions() -> void: - current_building = null - pending_building = null - current_extractor_index = -1 - pending_extractor_index = -1 - current_landing_planet = null - context_actions_expanded = false - for control in [build_button, action_button, building_action_button, - landing_button, transfer_button, hangar_button]: - if control != null: - control.visible = false - - -func _close_build_menu() -> void: - # The popup is a child panel of this menu; leaving it "open" while its - # parent is hidden is what blocked every button in the game. - info_popup.visible = false - pending_build_type = "" - build_menu.visible = false - - -# --- the outpost's garrison panel (#103) ----------------------------------- - -func _close_garrison_menu() -> void: - garrison_menu.visible = false - garrison_building_id = 0 - - -func _refresh_garrison() -> void: - if garrison_building_id == 0: - return - garrison_panel.building_id = garrison_building_id - garrison_panel.refresh() - var ordered: int = GameState.garrison_desired_total(garrison_building_id) - var crewed: int = GameState.garrison_crew(garrison_building_id).size() - if ordered == 0: - garrison_status.text = "No vessels ordered. Only armed vessels can hold a position." - elif crewed >= ordered: - garrison_status.text = "Garrison at full strength" - else: - garrison_status.text = "%d of %d berths crewed - vessels are sent from the mothership as they become available" \ - % [crewed, ordered] - - -# --- the planning centre's standing orders (#106) --------------------------- - -func _close_planning_menu() -> void: - planning_menu.visible = false - - -func _refresh_planning() -> void: - planning_panel.refresh() - var short: PackedStringArray = PackedStringArray() - for vessel_type in GameState.planned_vessel_types(): - var missing: int = GameState.vessel_shortfall(String(vessel_type)) - if missing > 0: - short.append("%d %s" % [missing, GameState.vessel_name(String(vessel_type))]) - if short.is_empty(): - planning_status.text = "Every standing order is met or already being built" - else: - planning_status.text = "Short: %s" % ", ".join(short) - - -# The ORE half of a price, which is all some callers want (the info popup's -# headline, the tests). costs_of() below is the whole of it. func cost_of(type: String) -> int: return int(costs_of(type).get(GameState.RESOURCE_ORE, 0)) @@ -1027,6 +329,34 @@ 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: @@ -1034,15 +364,9 @@ func _show_building_info(type: String) -> void: var costs: Dictionary = GameState.building_costs(type) var lines: Array = [definition.description] var can_build: bool = true - - # Locked structures still list themselves, greyed, so the player can see - # what exists and what research would open it up. if not GameState.is_building_unlocked(type): lines.append("Locked. Research %s at your mothership." % GameState.research_for_building(type)) can_build = false - - # Anything that would stop this particular placement is spelled out in - # the same popup, rather than silently failing after the press. if can_build and definition.placement_mode == "planet": var planet: Node2D = _planet_under_fleet() if planet == null: @@ -1055,30 +379,19 @@ func _show_building_info(type: String) -> void: 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]) + lines.append("Will mine %s from %s." % [ + GameState.resource_name(planet.resource_type), planet.planet_name]) elif can_build and definition.unique and GameState.has_building_of_type(type): lines.append("WARNING: only one may exist - this destroys your current one.") - - # The same pooled test the build itself uses. Asking the bank here while - # charging bank-plus-hold below would hide the build button for something - # the player can in fact afford. if can_build and not GameState.can_afford_resources(costs): - # #137: no sentence about the shortfall. The price tag below carries - # what is held over what is wanted, in red - saying it twice, in two - # different shapes, is what the ticket is about. can_build = false - - info_title.text = definition.display_name - info_message.text = "\n\n".join(lines) - info_cost.show_price(costs) - info_build_button.visible = can_build pending_build_type = type if can_build else "" - info_popup.visible = true + build_menu.show_info(definition.display_name, "\n\n".join(lines), costs, can_build) func _close_info() -> void: - info_popup.visible = false pending_build_type = "" + build_menu.close_info() func _confirm_build() -> void: @@ -1086,54 +399,39 @@ func _confirm_build() -> void: if type == "": return pending_build_type = "" - info_popup.visible = false - - # Everything is re-checked here rather than trusted from when the popup - # opened: the fleet keeps flying while it is up, so it can drift off the - # planet an extractor was going to mine. + 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_status.text = "No planet in reach any more" + build_menu.set_status("No planet in reach any more") return if not GameState.is_planet_conquered(planet.planet_id): - build_status.text = "%s is not captured yet" % planet.planet_name + build_menu.set_status("%s is not captured yet" % planet.planet_name) return if String(planet.resource_type) == "": - # #63: a research_lab planet's whole point is that it yields - # nothing to mine - an extractor there would sit on an empty - # resource id with no ResourceDefinition, breaking its own - # glyph/HUD rendering the moment it produced anything. - build_status.text = "%s has nothing to extract" % planet.planet_name + build_menu.set_status("%s has nothing to extract" % planet.planet_name) return if not GameState.spend_resources(GameState.building_costs(type)): - build_status.text = _cannot_afford(type) + build_menu.set_status(_cannot_afford(type)) return - GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type, planet.planet_id) - build_menu.visible = false + GameState.add_extractor(planet.planet_name, planet.position, + planet.resource_type, planet.planet_id) + build_menu.close_menu() return - if definition.unique and GameState.has_building_of_type(type): if not GameState.can_afford_resources(GameState.building_costs(type)): - build_status.text = _cannot_afford(type) + build_menu.set_status(_cannot_afford(type)) return - # Only now is the old one destroyed - never before the new one is - # known to be affordable, or the player would be left with none. GameState.remove_buildings_of_type(type) building_field.rebuild_all() - _try_place(type) -# #138: a structure can want more than rocks, so the refusal has to name -# which of them is short rather than always saying "rocks". Built from the -# price itself, so a cost gaining a resource needs nothing here. func _cannot_afford(type: String) -> String: var missing: Array = [] for resource_type in GameState.building_costs(type): @@ -1142,109 +440,48 @@ func _cannot_afford(type: String) -> String: var have: int = GameState.available_resource(id) if have < want: missing.append("%s (need %d, have %d)" % [GameState.resource_name(id), want, have]) - if missing.is_empty(): - return "" - return "Not enough %s" % " and ".join(missing) + return "" if missing.is_empty() else "Not enough %s" % " and ".join(missing) func _try_place(type: String) -> void: if not GameState.spend_resources(GameState.building_costs(type)): - build_status.text = _cannot_afford(type) + build_menu.set_status(_cannot_afford(type)) return building_field.place(type, fleet.get_centroid()) - build_menu.visible = false + build_menu.close_menu() -# --- research (the mothership's skill tree) -------------------------------- - -# A second tap on the node already selected buys it (#83). Routed into the -# same handler the buy button used, so the affordability re-check and the -# failure message it does are not duplicated here. -func _on_skill_node_confirmed(_id: String) -> void: - _on_unlock_pressed() - - -func _on_skill_node_picked(id: String) -> void: - _refresh_skill_menu(id) - - -func _refresh_skill_menu(id: String = "") -> void: - if id == "": - id = skill_tree.selected_id - if id == "": - # Nothing chosen yet: point the player at the tree rather than - # leaving the panel blank, which reads as broken. - skill_detail.text = "Select a node to see what it unlocks." - skill_cost.show_price({}) - skill_status.text = "Fleet capacity %d · Research points %d" \ - % [GameState.max_fleet_size(), GameState.research_points] +func _on_unlock_requested(id: String) -> void: + if not GameState.unlock(id): + skill_menu.refresh(id) return - - var node: Dictionary = GameState.skill_node(id) - if node.is_empty(): - return - var points_needed: int = GameState.skill_research_point_cost(id) - # #134: the explanation and the price no longer share a column. The text - # is the left half of the band and the tag is the right half, so a long - # description can grow without ending up on top of the number - which is - # what it used to do, the two being centred over each other. - # - # Both shortfalls it used to spell out - ore and research points - are - # now entries on the tag, held-over-needed, in red when short. One place, - # and it says WHICH one is missing without a sentence per case. - skill_detail.text = "%s\n%s" % [node.name, node.desc] - skill_cost.show_price(GameState.skill_costs(id), points_needed) - - if GameState.is_unlocked(id): - skill_status.text = "Researched" - elif not GameState.prerequisite_met(id): - skill_status.text = "Needs %s first" % GameState.skill_node(node.requires).name - elif not skill_cost.is_affordable(): - # Deliberately blank. The red on the tag is the whole message; the - # line here used to repeat it in words and, being centred, repeat it - # on top of the number as well. - skill_status.text = "" - else: - # The button that used to appear here was the only thing saying "this - # is buyable". With it gone the line has to say so itself, or an - # affordable node and an unaffordable one look identical. - skill_status.text = "Tap again to research" + 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_tree.selected_id - if id == "": - return - if not GameState.unlock(id): - # Re-check rather than trust the button: rocks can be spent elsewhere - # while this menu is open, since haulers keep delivering. - _refresh_skill_menu(id) - return - # A capacity upgrade has to reach the fleet that is already flying, not - # just ones rebuilt later. - if int(GameState.skill_node(id).get("fleet_bonus", 0)) > 0: - fleet.apply_formation() - skill_tree.queue_redraw() - _refresh_skill_menu(id) + var id: String = skill_menu.selected_id() + if id != "": + _on_unlock_requested(id) -func _say(message: String) -> void: - craft_message = message - craft_message_until = Time.get_ticks_msec() + 3000 - _refresh_craft_status() +func _refresh_skill_menu(id: String = "") -> void: + skill_menu.refresh(id) func _format_costs(costs: Dictionary) -> String: - var parts: PackedStringArray = PackedStringArray() + 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()]) + 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 = PackedStringArray() + var parts := PackedStringArray() for resource_type in GameState.resource_types(): var amount: int = int(costs.get(resource_type, 0)) if amount > 0: @@ -1253,249 +490,52 @@ func _format_missing_costs(costs: Dictionary) -> String: return " · ".join(parts) -# --- the craft menu's own layout (#111) ------------------------------------ -# -# Six vessels were authored as one row of 65px buttons with 105px captions -# overlapping each other, which left "Energy Propulser" clipped and the price -# crushed onto a second line under it. Laid out here instead, in two rows of -# three, so a cell is wide enough for the name to breathe - and the price is -# glyphs rather than words (see PriceTag, which every menu now shares). -# -# #147: "prices are far from the button they are about, and when many -# different resources are needed the costs overlap." Both were true, and -# both were the same cause - a cell laid out with more slack than the row -# pitch had room for. The caption box was 62px for a one-line name, which -# pushed the price 70px clear of the button it belonged to; and the price -# was given 184px of width in a 190px column, so a two-resource price ran -# out of its own box and into its neighbour's. -# -# The caption is one line tall now and the price sits directly under it, and -# PriceTag puts each resource on its own line when they will not fit across -# (which in a column this narrow means whenever there is more than one). -# Rows are placed off the measured band rather than a written-down pair of -# tops, for the same reason the build menu's are - see grid_band(). -const CRAFT_COLUMNS: int = 3 -const CRAFT_ROW_PITCH: float = 260.0 -const CRAFT_COL_X: float = 190.0 -const CRAFT_BUTTON_HALF: float = 56.0 -const CRAFT_BUTTON_H: float = 100.0 -const CRAFT_LABEL_HALF: float = 92.0 -const CRAFT_LABEL_H: float = 44.0 -const CRAFT_LABEL_OFFSET: float = 106.0 -const CRAFT_PRICE_HALF: float = 92.0 -# Room for the tallest price any vessel has, stacked. Read from the tag -# rather than assumed, so a vessel priced in three things still fits. -const CRAFT_PRICE_H: float = 86.0 -const CRAFT_PRICE_OFFSET: float = 154.0 - -var craft_price_strips: Array = [] -# #137: the ship lab's own price tags, built here rather than authored in the -# scene for the same reason the craft menu's are - four identical widgets -# differing only in where they sit. -var lab_price_tags: Array = [] - - -func _build_craft_price_strips() -> void: - var strip_script: Script = load("res://ui/PriceTag.gd") - for i in range(craft_vessel_buttons.size()): - var strip: Control = strip_script.new() - strip.name = "CraftPrice%d" % i - # Centre anchors, matching the buttons and captions authored in the - # scene - _place_entry writes offsets, and an offset means something - # different against a top-left anchor. - strip.anchor_left = 0.5 - strip.anchor_right = 0.5 - strip.anchor_top = 0.5 - strip.anchor_bottom = 0.5 - craft_menu.add_child(strip) - craft_price_strips.append(strip) - - -# One tag per upgrade slot, seated in the gap the caption's second line used -# to occupy. The captions above them shrink to one line to make room, so the -# option block is the same height it was and the row below it does not move. -const LAB_TAG_H: float = 38.0 -const LAB_LABEL_H: float = 40.0 -# Room around a stacked price, so its last line never sits flush against the -# option underneath. -const LAB_TAG_PADDING: float = 8.0 -# Between the bottom of one option's price and the top of the option below. -const LAB_ROW_GAP: float = 18.0 - - -# How tall the lab's price strips need to be: the tallest any of them wants, -# asked of the tags rather than assumed. Every strip gets the same height so -# the two columns stay level with each other. -# Re-seats the option grid around whatever the prices actually need. The two -# rows are authored at fixed offsets in the scene, which is fine while every -# price is one line and wrong the moment one stacks - the second row's buttons -# sat where the first row's price had grown into (#164). Row one keeps its -# authored place; row two is pushed to clear it. -# The vessel strip is authored at fixed offsets, which fits exactly as many -# vessels as there were when it was written. The catalog has grown since - the -# eighth ran off the right edge, its caption clipped in half - so the row is -# spaced from the panel's actual width and centred, the way every other grid -# in this menu already is. -const LAB_VESSEL_HALF: float = 30.0 -const LAB_VESSEL_NATURAL_SPACING: float = 90.0 -const LAB_VESSEL_MARGIN: float = 24.0 - - -func _layout_lab_vessels() -> void: - var shown: Array = [] - for i in range(lab_vessel_buttons.size()): - if lab_vessel_buttons[i].visible: - shown.append(i) - if shown.is_empty(): - return - var room: float = lab_menu.size.x - LAB_VESSEL_MARGIN * 2.0 - if room <= 0.0: - room = 672.0 - var spacing: float = minf(LAB_VESSEL_NATURAL_SPACING, room / float(shown.size())) - for slot in range(shown.size()): - var i: int = shown[slot] - var cx: float = (float(slot) - (float(shown.size()) - 1.0) * 0.5) * spacing - lab_vessel_buttons[i].offset_left = cx - LAB_VESSEL_HALF - lab_vessel_buttons[i].offset_right = cx + LAB_VESSEL_HALF - # The caption is allowed the full column, so two-word names wrap - # inside their own slot instead of over the vessel beside them. - lab_vessel_labels[i].offset_left = cx - spacing * 0.5 - lab_vessel_labels[i].offset_right = cx + spacing * 0.5 - - -func _resize_lab_price_tags() -> void: - var height: float = lab_tag_height() - for tag in lab_price_tags: - if tag != null: - tag.offset_bottom = tag.offset_top + height - if lab_price_tags.size() < 3 or lab_upgrade_buttons.size() < 3: - return - var first_row_bottom: float = lab_price_tags[0].offset_bottom - var shift: float = first_row_bottom + LAB_ROW_GAP - lab_upgrade_buttons[2].offset_top - if absf(shift) < 0.5: - return - for i in range(2, lab_upgrade_buttons.size()): - for node in [lab_upgrade_buttons[i], lab_upgrade_labels[i], lab_price_tags[i]]: - if node == null: - continue - node.offset_top += shift - node.offset_bottom += shift - - -func lab_tag_height() -> float: - var tallest: float = LAB_TAG_H - for tag in lab_price_tags: - if tag != null and tag.has_method("wanted_height"): - tallest = maxf(tallest, float(tag.wanted_height()) + LAB_TAG_PADDING) - return tallest - - -func _build_lab_price_tags() -> void: - var tag_script: Script = load("res://ui/PriceTag.gd") - for i in range(lab_upgrade_labels.size()): - var label: Control = lab_upgrade_labels[i] - var tag: Control = tag_script.new() - tag.name = "LabPrice%d" % i - tag.anchor_left = label.anchor_left - tag.anchor_right = label.anchor_right - tag.anchor_top = label.anchor_top - tag.anchor_bottom = label.anchor_bottom - tag.offset_left = label.offset_left - tag.offset_right = label.offset_right - # Under the caption, which is cut to a single line here so the two - # together occupy what the two-line caption used to. - label.offset_bottom = label.offset_top + LAB_LABEL_H - tag.offset_top = label.offset_bottom + 2.0 - # Tall enough for the tallest price any upgrade carries, not for the - # one the constant was written against. An upgrade costing ore AND a - # research point stacks two lines (see PriceTag), and the second was - # landing on the option below it (#164). - tag.offset_bottom = tag.offset_top + lab_tag_height() - lab_menu.add_child(tag) - lab_price_tags.append(tag) - - -func craft_row_height() -> float: - return CRAFT_PRICE_OFFSET + CRAFT_PRICE_H - - -func _layout_craft_menu() -> void: - var rows: int = maxi(int(ceil(float(craft_vessel_buttons.size()) - / float(CRAFT_COLUMNS))), 1) - var band: Vector2 = grid_band(craft_status.offset_bottom, craft_menu.size.y) - for i in range(craft_vessel_buttons.size()): - var row: int = i / CRAFT_COLUMNS - var column: int = i % CRAFT_COLUMNS - var cx: float = (float(column) - 1.0) * CRAFT_COL_X - var top: float = grid_row_top(row, rows, craft_row_height(), band, - CRAFT_ROW_PITCH) - _place_entry(craft_vessel_buttons[i], cx, top, CRAFT_BUTTON_HALF, CRAFT_BUTTON_H) - _place_entry(craft_vessel_labels[i], cx, top + CRAFT_LABEL_OFFSET, - CRAFT_LABEL_HALF, CRAFT_LABEL_H) - if i < craft_price_strips.size(): - _place_entry(craft_price_strips[i], cx, top + CRAFT_PRICE_OFFSET, - CRAFT_PRICE_HALF, CRAFT_PRICE_H) - - -func _refresh_craft_status() -> void: - # Prices live with the buttons they describe. The top of the screen is - # intentionally reserved for the shared resource bar and a short action - # message; repeating fleet, queue and transport counts here buried the UI. - craft_status.text = "" - var definitions: Array = VESSEL_DEFINITIONS.fleet() - for i in range(mini(definitions.size(), craft_vessel_buttons.size())): - var definition: Resource = definitions[i] - var button: IconButton = craft_vessel_buttons[i] - # #111: the name alone. The price is the strip under it, in glyphs. - craft_vessel_labels[i].text = String(definition.display_name) - if i < craft_price_strips.size(): - craft_price_strips[i].set_costs( - GameState.product_costs(definition.definition_id)) - button.icon = definition.icon - button.glow_color = definition.glow_color - button.enabled = GameState.vessel_is_unlocked(definition.definition_id) - button.queue_redraw() - _refresh_queue() - if craft_message != "": - if Time.get_ticks_msec() < craft_message_until: - craft_status.text = "%s\n%s" % [craft_message, craft_status.text] - else: - craft_message = "" - - 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: - _say("No crafter selected - reopen this panel from the building") + 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): - _say("%s must be unlocked at a ship lab first" % definition.display_name) + craft_menu.say("%s must be unlocked at a ship lab first" % definition.display_name) return - _report_order(vessel) - - -func _report_order(product: String) -> void: var missing: Dictionary = GameState.outstanding_costs(craft_building_id) - if not missing.is_empty(): - _say("%s queued - deliver %s here" % [GameState.vessel_name(product), - _format_missing_costs(missing)]) + if missing.is_empty(): + craft_menu.say("%s ordered - building now" % GameState.vessel_name(vessel)) else: - _say("%s ordered - building now" % GameState.vessel_name(product)) + craft_menu.say("%s queued - deliver %s here" % [GameState.vessel_name(vessel), + _format_missing_costs(missing)]) -# --- ship lab (#5) -------------------------------------------------------- +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: - # The shield VESSEL unlock is offered on the shield page as an extra - # entry, because unlocking the type and upgrading it belong in the same - # place - and its upgrades are pointless until it exists. var options: Array = [] var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel) if definition == null: @@ -1506,144 +546,84 @@ func _lab_options() -> Array: "costs": GameState.research_costs(definition.unlock_id), "icon": definition.unlock_icon if definition.unlock_icon >= 0 else definition.icon}) return options - for u in GameState.upgrades_for(lab_vessel): - options.append({"unlock": false, "id": u.id, "name": u.name, "desc": u.desc}) + 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 or index >= definitions.size(): - return - lab_vessel = definitions[index].definition_id - lab_choice = 0 - _refresh_lab() + if index >= 0 and index < definitions.size(): + lab_vessel = definitions[index].definition_id + lab_choice = 0 + _refresh_lab() -# #164: the separate buy button is gone, so the option itself is the control. -# Same rule as the research tree (#83): the first tap selects and fills in the -# detail below it, a second tap on the SAME option buys - no timed double-tap -# to miss, and never a blind commitment, because the first tap always shows -# what the second will cost. 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_status.text = "" + lab_menu.set_status("") if confirming and lab_can_buy: _on_lab_buy() - return - _refresh_lab() + else: + _refresh_lab() func _on_lab_buy() -> void: var options: Array = _lab_options() if options.is_empty(): return - var choice: Dictionary = options[lab_choice] - var id: String = String(choice.id) + var selected: Dictionary = options[lab_choice] + var id: String = String(selected.id) if not GameState.queue_lab_research(lab_building_id, id): - lab_status.text = "Research is already queued or complete" + lab_menu.set_status("Research is already queued or complete") return - lab_status.text = "%s queued - deliver %s to this lab" % [choice.name, - _format_costs(GameState.research_costs(id))] + 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: var definitions: Array = VESSEL_DEFINITIONS.all() - for i in range(mini(lab_vessel_buttons.size(), definitions.size())): - var definition: Resource = definitions[i] - var this_vessel: String = definition.definition_id - lab_vessel_buttons[i].enabled = true - lab_vessel_buttons[i].icon = definition.icon - lab_vessel_buttons[i].glow_color = definition.glow_color - lab_vessel_labels[i].text = definition.display_name - lab_vessel_buttons[i].highlighted = this_vessel == lab_vessel - lab_vessel_buttons[i].queue_redraw() - lab_vessel_label.text = "%s research · %d RP" % [ + 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() - for i in range(lab_upgrade_buttons.size()): - var visible: bool = i < options.size() - lab_upgrade_buttons[i].visible = visible - lab_upgrade_labels[i].visible = visible - if i < lab_price_tags.size(): - lab_price_tags[i].visible = visible - if not visible: - continue - var option: Dictionary = options[i] - # #137: the price is a tag under the option, the way it is under a - # build option and under a vessel in the craft menu. It used to be a - # second line of the caption - "25 ore · 10 gas · 2 RP" - which is - # the same information in a shape no other menu used. + 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)] - lab_upgrade_labels[i].text = "%s%s" % [option.name, level_note] - if i < lab_price_tags.size(): - var tag: Control = lab_price_tags[i] - tag.visible = not maxed - if maxed: - tag.show_price({}) - else: - tag.show_price(option.costs if bool(option.unlock) \ - else GameState.research_costs(option.id), - GameState.research_point_cost(String(option.id))) - lab_upgrade_buttons[i].icon = _upgrade_icon(option) - lab_upgrade_buttons[i].glow_color = _vessel_colour(lab_vessel) - lab_upgrade_buttons[i].highlighted = i == lab_choice - lab_upgrade_buttons[i].enabled = true - lab_upgrade_buttons[i].queue_redraw() - _layout_lab_vessels() - - # Re-height the strips now that every price on show is set. They are - # created before any price exists, so their built-in size is only a - # starting point. - _resize_lab_price_tags() - + 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_detail.text = "" + lab_menu.clear_detail() lab_can_buy = false return - lab_choice = clampi(lab_choice, 0, options.size() - 1) - - var choice: Dictionary = options[lab_choice] - var choice_points: int = GameState.research_point_cost(String(choice.id)) - lab_detail.text = "%s\n%s" % [choice.name, choice.desc] - if choice_points > 0: - lab_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, choice_points] + var selected: Dictionary = options[lab_choice] + var points: int = GameState.research_point_cost(String(selected.id)) + lab_menu.show_detail(String(selected.name), String(selected.desc), points) var queued: Dictionary = GameState.lab_research_order(lab_building_id) - lab_can_buy = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.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)) if not bool(queued.started) and GameState.research_points < queued_points: - # #63: distinct from "Waiting for resources" - flying more ore/gas - # to this lab cannot fix a research-point shortfall, only - # conquering a research lab elsewhere can. - lab_status.text = "Need %d research point%s (have %d)" \ - % [queued_points, "" if queued_points == 1 else "s", GameState.research_points] + lab_menu.set_status("Need %d research point%s (have %d)" % [queued_points, + "" if queued_points == 1 else "s", GameState.research_points]) else: - lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))] - - -func _upgrade_icon(option: Dictionary) -> IconButton.Icon: - return int(option.get("icon", IconButton.Icon.SHIP_LAB)) - - -func _on_craft_shield() -> void: - _on_craft_vessel(GameState.VESSEL_SHIELD) - - -# --- moving ore, in both directions, at any building (#24) ---------------- - -# Sentinel for the "All" buttons. The transfer primitives cap what they move -# by what is actually there, so asking for more than could possibly exist is -# how you say "everything" without the UI having to work out how much that -# is first - and the answer cannot go stale between the read and the press. -const ALL: int = 1 << 30 + lab_menu.set_status("%s %d%%" % ["Researching" if bool(queued.started) + else "Waiting for resources", int(100.0 * float(queued.elapsed) + / maxf(GameState.lab_research_time(), 0.001))]) func _on_transfer_pressed() -> void: @@ -1651,323 +631,136 @@ func _on_transfer_pressed() -> void: return transfer_building_id = current_building.building_id if current_building != null else 0 transfer_extractor_index = current_extractor_index - for bar in resource_bars: - bar.building_id = transfer_building_id - transfer_menu.visible = true - transfer_status.text = "" - _refresh_transfer() + 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_resource_transfer(resource_type: String, to_building: bool, amount: int = ALL) -> void: - if transfer_building_id == 0 and transfer_extractor_index < 0: - transfer_status.text = "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() - - # Nothing moving is the common case at an empty end, and a silent no-op - # is the dead-button problem this panel exists to remove. Say which side - # was empty rather than just refusing. - if moved <= 0: - if to_building: - transfer_status.text = "The fleet is carrying nothing to unload (no %s)" % resource_name - else: - transfer_status.text = "%s has no %s to load" % [_transfer_place_name().capitalize(), resource_name] - elif to_building: - transfer_status.text = "Moved %d %s into %s" % [moved, resource_name, _transfer_place_name()] - else: - transfer_status.text = "Loaded %d %s into the fleet" % [moved, resource_name] - _refresh_transfer() - func _on_gas_transfer(to_building: bool) -> void: _on_resource_transfer(GameState.RESOURCE_GAS, to_building) -func _transfer_place_name() -> String: +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: - return "the extractor" - var b: Dictionary = GameState.find_building(transfer_building_id) - if b.is_empty(): - return "this building" - if GameState.building_has_capability(b, "bank"): - # Worth naming as the bank rather than as a structure: it is the - # shared pool every building's transporters draw from, not just - # another store. (It is no longer where ore has to be to be SPENT - - # purchases now draw on the fleet hold too.) - return "the bank" - return "this building" + 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_extractor_index >= 0: - transfer_list.set_extractor_destination(transfer_extractor_index, "Planet extractor") - return - var building: Dictionary = GameState.find_building(transfer_building_id) - var heading: String = "This building" - if GameState.building_has_capability(building, "bank"): - heading = "Mothership bank" - transfer_list.set_destination(transfer_building_id, heading) + 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 _refresh_queue() -> void: - # The queue down the right of the craft panel: one slot per pending - # vessel, the first being the one actually under construction. Tapping a - # slot cancels it (#23). - var orders: Array = GameState.order_list(craft_building_id) - for i in range(queue_slots.size()): - var slot: IconButton = queue_slots[i] - slot.visible = i < orders.size() - if not slot.visible: - continue - var order: Dictionary = orders[i] - var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product)) - slot.icon = definition.icon - slot.glow_color = _vessel_colour(String(order.product)) - # Only the one under construction reads as active; the rest are - # waiting, and greyed says so without needing a caption each. - slot.enabled = bool(order.started) - slot.queue_redraw() - - if orders.is_empty(): - queue_detail.text = "Nothing queued" - return - var head: Dictionary = orders[0] - if bool(head.started): - queue_detail.text = "Building %s %d%%\nTap to cancel" % [ - GameState.vessel_name(String(head.product)), - int(round(float(head.progress) * 100.0)), - ] - else: - queue_detail.text = "%s waiting for resources\nTap to cancel" % \ - GameState.vessel_name(String(head.product)) - - -func _vessel_colour(product: String) -> Color: - var definition: Resource = VESSEL_DEFINITIONS.get_definition(product) - return definition.glow_color if definition != null else Color(0.25, 0.85, 1.0) - - -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 not GameState.cancel_order(craft_building_id, index): - return - _say("%s cancelled%s" % [vessel, " - resources refunded here" if refunded else ""]) - _refresh_queue() - - -# --- hangar (#23) --------------------------------------------------------- - func _on_hangar_pressed() -> void: - hangar_status.text = "" - hangar_menu.visible = true - _refresh_hangar() - - -func _on_hangar_crew_type(index: int) -> void: - var type: String = _hangar_types()[index] - if fleet.ships.size() >= GameState.max_fleet_size(): - hangar_status.text = "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(type) - if stored_index < 0: - hangar_status.text = "No %s available" % GameState.vessel_name(type) - return - if not fleet.add_ship(GameState.take_stored_vessel(stored_index)): - hangar_status.text = "Could not crew that vessel" - return - hangar_status.text = "%s joined the fleet" % GameState.vessel_name(type) - _refresh_hangar() - - -func _on_hangar_store_type(index: int) -> void: - var type: String = _hangar_types()[index] - var ship_index: int = -1 - for i in range(fleet.ships.size()): - if String(fleet.ships[i].vessel_type) == type: - ship_index = i - break - if ship_index < 0: - hangar_status.text = "No %s in the fleet" % GameState.vessel_name(type) - return - var stored: String = fleet.store_ship(ship_index) - if stored == "": - hangar_status.text = "Keep one ship deployed" - return - GameState.stored_vessels.append(stored) - hangar_status.text = "%s stored in the hangar" % GameState.vessel_name(stored) - _refresh_hangar() + hangar_menu.open_for(fleet) func _hangar_types() -> Array: var types: Array = [] for definition in VESSEL_DEFINITIONS.fleet(): - types.append(definition.definition_id) + types.append(String(definition.definition_id)) return types -# How many vessel types sit side by side in the hangar. Six in a single row -# gave each one about 100px while its two-line label needs 155, so the names -# ran into each other - "InterceptSheld VessGas Bomber". Three columns give -# each cell room, and the second row costs nothing: the lower half of the -# menu was empty. -const HANGAR_COLUMNS: int = 3 -# #120: room for a two-line caption under each icon, and air around the rule -# that separates the two halves. -const HANGAR_LABEL_GAP: float = 2.0 -const HANGAR_DIVIDER_GAP: float = 10.0 - -# Where the rule between "available" and "in the fleet" is drawn. Computed by -# the layout and read by the menu's own _draw, so the line cannot drift away -# from the sections it divides. -var hangar_divider_y: float = 0.0 +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 "" -# Arranges both hangar sections as grids sized from the space actually -# available, rather than from the single row the scene authored. A seventh -# vessel type now wraps instead of squeezing the other six. -func _layout_hangar() -> void: - var count: int = hangar_available_slots.size() - if count == 0: +func _on_hangar_crew_type(selection: Variant) -> void: + var vessel_type: String = _resolve_hangar_type(selection) + if vessel_type == "": return - var width: float = hangar_menu.size.x - var columns: int = mini(HANGAR_COLUMNS, count) - var rows: int = int(ceil(float(count) / float(columns))) - - # The band between the first section's heading and the status line under - # both of them. Derived, so moving either in the scene moves the grids. - var top: float = hangar_available_title.position.y + hangar_available_title.size.y + 8.0 - var bottom: float = hangar_status.position.y - 12.0 - var title_height: float = hangar_fleet_title.size.y - var section_height: float = (bottom - top - title_height - 16.0) * 0.5 - if section_height <= 0.0: + 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 cell_width: float = width / float(columns) - var cell_height: float = section_height / float(rows) - # #120: the icon gives up height so the caption under it has somewhere to - # go. Vessel names are two and three words long and were wrapping into a - # box barely tall enough for one line, so the second line was clipped and - # a "Gas Bomber" read as "Gas". - var slot_size: float = minf(cell_height * 0.46, cell_width * 0.40) - # #143: TWO gaps, not one - above the caption and below it. With a single - # gap the box ended exactly HANGAR_LABEL_GAP past the bottom of its own - # cell, which is inside the next row's icon. Two pixels of overhang is - # not much on its own, but it is the difference between "the caption - # cannot reach the row below" and "the caption starts in it". - var label_height: float = maxf( - cell_height - slot_size - HANGAR_LABEL_GAP * 2.0, 1.0) - - _layout_hangar_section(hangar_available_slots, hangar_available_labels, - top, columns, cell_width, cell_height, slot_size, label_height, width) - # AFTER the first section's last row, not before it. Placing the heading - # by subtracting from the second section's top put it back inside the - # first one, straight over its bottom row. - var available_bottom: float = top + section_height - # #120: a rule between the two halves. They are different questions - - # what is parked here, and what is flying - and with only a heading - # between them the grids read as one long list. - hangar_divider_y = available_bottom + HANGAR_DIVIDER_GAP - hangar_fleet_title.position.y = hangar_divider_y + HANGAR_DIVIDER_GAP - var fleet_top: float = hangar_fleet_title.position.y + title_height + 8.0 - _layout_hangar_section(hangar_fleet_slots, hangar_fleet_labels, - fleet_top, columns, cell_width, cell_height, slot_size, label_height, width) - hangar_menu.queue_redraw() - - -func _layout_hangar_section(slots: Array, labels: Array, top: float, columns: int, - cell_width: float, cell_height: float, slot_size: float, - label_height: float, width: float) -> void: - # A zero here is fatal on the web and silent everywhere else: wasm TRAPS - # on integer % 0 ("remainder by zero") and kills the engine mid-frame, - # while a native build shrugs it off. That difference is why this class - # of bug survives every test and only appears in a browser. - if columns <= 0: + 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 - for i in range(slots.size()): - var column: int = i % columns - var row: int = i / columns - # Control.position is in PARENT space, whatever the node's anchors - # are - the authored offsets being negative (anchored to the menu's - # centre) does not make position centre-relative too. - var cell_x: float = float(column) * cell_width - var cell_y: float = top + float(row) * cell_height - var slot: Control = slots[i] - slot.position = Vector2(cell_x + (cell_width - slot_size) * 0.5, cell_y) - slot.size = Vector2(slot_size, slot_size) - var label: Label = labels[i] - label.position = Vector2(cell_x, cell_y + slot_size + HANGAR_LABEL_GAP) - label.size = Vector2(cell_width, label_height) - label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - label.vertical_alignment = VERTICAL_ALIGNMENT_TOP - label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - # #143: a caption is "Gas Bomber\n2 available" - two lines before - # autowrap, three after it in a narrow column - at a font size fixed - # in the scene. A Label does not clip by default, so the third line - # was drawn straight over the icon of the row below it. - # - # Two things, and both are wanted. The size is fitted to the box the - # layout above worked out, so the text has somewhere to go; and the - # box clips, so however the catalog grows - a longer vessel name, a - # fourth line - nothing can ever reach the next row again. The first - # is what keeps it legible, the second is what keeps it correct. - label.add_theme_font_size_override("font_size", - _hangar_caption_size(label_height)) - label.clip_text = true + 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) -# The largest caption that still fits HANGAR_CAPTION_LINES lines into the -# height the grid could spare, within what is readable at arm's length. -const HANGAR_CAPTION_LINES: float = 3.0 -const HANGAR_CAPTION_MIN: int = 13 -const HANGAR_CAPTION_MAX: int = 22 -# Godot lays a line out taller than its nominal font size; measured against -# the project font rather than guessed, this is the ratio that keeps three -# lines inside the box. -const HANGAR_LINE_RATIO: float = 1.25 - - -func _hangar_caption_size(label_height: float) -> int: - return clampi(int(label_height / (HANGAR_CAPTION_LINES * HANGAR_LINE_RATIO)), - HANGAR_CAPTION_MIN, HANGAR_CAPTION_MAX) +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: - var types: Array = _hangar_types() - for i in range(types.size()): - var type: String = types[i] - var definition: Resource = VESSEL_DEFINITIONS.get_definition(type) - var available: int = GameState.stored_vessels.count(type) - var deployed: int = 0 - for ship in fleet.ships: - if is_instance_valid(ship) and String(ship.vessel_type) == type: - deployed += 1 - hangar_available_labels[i].text = "%s\n%d available" % [definition.display_name, available] - hangar_fleet_labels[i].text = "%s\n%d in fleet" % [definition.display_name, deployed] - hangar_available_slots[i].icon = definition.icon - hangar_fleet_slots[i].icon = definition.icon - hangar_available_slots[i].glow_color = definition.glow_color - hangar_fleet_slots[i].glow_color = definition.glow_color - hangar_available_slots[i].enabled = available > 0 and fleet.ships.size() < GameState.max_fleet_size() - hangar_fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1 - hangar_available_slots[i].queue_redraw() - hangar_fleet_slots[i].queue_redraw() + 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: @@ -1977,4 +770,9 @@ func _on_formation_changed() -> void: func _on_reset_formation() -> void: GameState.reset_formation() fleet.apply_formation() - formation_editor.queue_redraw() + formation_menu.redraw() + + +func _hide_all_actions() -> void: + _clear_detected_context() + build_button.visible = false diff --git a/tests/test_build_menu_hides_locked.gd b/tests/test_build_menu_hides_locked.gd index ca09841..80e5503 100644 --- a/tests/test_build_menu_hides_locked.gd +++ b/tests/test_build_menu_hides_locked.gd @@ -46,9 +46,10 @@ func _initialize() -> void: func _shown() -> Array: var out: Array = [] - for entry in controller.build_menu_entries: - if entry[1].visible: - out.append(entry) + for entry in controller.build_menu.entries: + if entry.button.visible: + out.append([entry.definition.definition_id, entry.button, + entry.label, entry.cost, entry.definition]) return out diff --git a/tests/test_carrier_vessel.gd b/tests/test_carrier_vessel.gd index 0806d4c..d274f3f 100644 --- a/tests/test_carrier_vessel.gd +++ b/tests/test_carrier_vessel.gd @@ -105,7 +105,7 @@ func _process(delta: float) -> bool: # --- D. locked state in the crafter UI ------------------------- controller.craft_building_id = crafter_id controller._refresh_craft_status() - if controller.craft_vessel_buttons[FLEET_UI_INDEX].enabled: + if controller.craft_menu.vessel_buttons[FLEET_UI_INDEX].enabled: return _fail("a locked carrier shows enabled in the crafter menu") stage = 1 @@ -126,8 +126,8 @@ func _process(delta: float) -> bool: # --- D. unlocked state in the crafter UI ------------------------ controller._refresh_craft_status() - var craft_button: IconButton = controller.craft_vessel_buttons[FLEET_UI_INDEX] - var craft_label: Label = controller.craft_vessel_labels[FLEET_UI_INDEX] + var craft_button: IconButton = controller.craft_menu.vessel_buttons[FLEET_UI_INDEX] + var craft_label: Label = controller.craft_menu.vessel_labels[FLEET_UI_INDEX] if not craft_button.enabled: return _fail("an unlocked carrier still shows disabled in the crafter menu") if craft_button.icon != definition.icon or craft_button.glow_color != definition.glow_color: @@ -148,8 +148,8 @@ func _process(delta: float) -> bool: # --- D. hangar display ------------------------------------------- controller._refresh_hangar() - var available_slot: IconButton = controller.hangar_available_slots[FLEET_UI_INDEX] - var available_label: Label = controller.hangar_available_labels[FLEET_UI_INDEX] + var available_slot: IconButton = controller.hangar_menu.available_slots[FLEET_UI_INDEX] + var available_label: Label = controller.hangar_menu.available_labels[FLEET_UI_INDEX] if available_slot.icon != definition.icon or available_slot.glow_color != definition.glow_color: return _fail("hangar carrier slot does not match its definition's icon/colour") var expected_available: String = "%s\n1 available" % definition.display_name @@ -177,8 +177,8 @@ func _process(delta: float) -> bool: # --- D. ship lab display ------------------------------------------- controller.lab_vessel = GameState.VESSEL_CARRIER controller._refresh_lab() - var lab_button: IconButton = controller.lab_vessel_buttons[LAB_UI_INDEX] - var lab_label: Label = controller.lab_vessel_labels[LAB_UI_INDEX] + var lab_button: IconButton = controller.lab_menu.vessel_buttons[LAB_UI_INDEX] + var lab_label: Label = controller.lab_menu.vessel_labels[LAB_UI_INDEX] if lab_button.icon != definition.icon or lab_button.glow_color != definition.glow_color: return _fail("ship lab carrier slot does not match its definition's icon/colour") if lab_label.text != definition.display_name: diff --git a/tests/test_crafter_feedback.gd b/tests/test_crafter_feedback.gd index 648dc88..26c4f47 100644 --- a/tests/test_crafter_feedback.gd +++ b/tests/test_crafter_feedback.gd @@ -67,7 +67,7 @@ func _tap(button: Control) -> void: func _first_line() -> String: - return controller.craft_status.text.split("\n")[0] + return controller.craft_menu.status.text.split("\n")[0] func _process(delta: float) -> bool: diff --git a/tests/test_extractor_transfer.gd b/tests/test_extractor_transfer.gd index 02895d4..e3bcbaf 100644 --- a/tests/test_extractor_transfer.gd +++ b/tests/test_extractor_transfer.gd @@ -55,7 +55,7 @@ func _process(delta: float) -> bool: 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: + if not controller.transfer_menu.visible or controller.transfer_menu.resource_list.extractor_index != extractor_index: return _fail("transfer panel did not bind to the extractor") var before: int = state.fleet_resource(resource_type) controller._on_resource_transfer(resource_type, false, 5) diff --git a/tests/test_hangar_management.gd b/tests/test_hangar_management.gd index 1c10e4a..3184cda 100644 --- a/tests/test_hangar_management.gd +++ b/tests/test_hangar_management.gd @@ -45,7 +45,7 @@ func _process(delta: float) -> bool: if elapsed < 0.2: return false # Available cards name the vessel and count what can be crewed. - var available_label: Label = controller.hangar_available_labels[0] + var available_label: Label = controller.hangar_menu.available_labels[0] if "Interceptor" not in available_label.text or "1 available" not in available_label.text: _fail("available interceptor card says '%s'" % available_label.text) return true diff --git a/tests/test_menu_polish.gd b/tests/test_menu_polish.gd index be4cc1d..505bf25 100644 --- a/tests/test_menu_polish.gd +++ b/tests/test_menu_polish.gd @@ -69,34 +69,34 @@ func _process(delta: float) -> bool: # Gas Bomber is named after a resource and always will be, and # what #111 removed was the PRICE spelled out underneath. var fleet_defs: Array = VesselDefinitions.fleet() - for i in range(mini(controller.craft_vessel_labels.size(), fleet_defs.size())): - var caption: String = String(controller.craft_vessel_labels[i].text) + for i in range(mini(controller.craft_menu.vessel_labels.size(), fleet_defs.size())): + var caption: String = String(controller.craft_menu.vessel_labels[i].text) var expected: String = String(fleet_defs[i].display_name) if caption != expected: return _fail("a craft caption reads \"%s\", expected just \"%s\"" \ % [caption, expected]) # Captions must not sit on top of one another, which is what # made the old single row unreadable. - for i in range(controller.craft_vessel_labels.size()): - for j in range(i + 1, controller.craft_vessel_labels.size()): - var a: Control = controller.craft_vessel_labels[i] - var b: Control = controller.craft_vessel_labels[j] + for i in range(controller.craft_menu.vessel_labels.size()): + for j in range(i + 1, controller.craft_menu.vessel_labels.size()): + var a: Control = controller.craft_menu.vessel_labels[i] + var b: Control = controller.craft_menu.vessel_labels[j] if Rect2(a.position, a.size).intersects(Rect2(b.position, b.size)): return _fail("craft captions %d and %d overlap" % [i, j]) print("craft captions: %d, one line each, none overlapping" \ - % controller.craft_vessel_labels.size()) + % controller.craft_menu.vessel_labels.size()) # --- B. and the price is really there --------------------- - if controller.craft_price_strips.size() != controller.craft_vessel_buttons.size(): + if controller.craft_menu.price_tags.size() != controller.craft_menu.vessel_buttons.size(): return _fail("%d price strips for %d vessels" \ - % [controller.craft_price_strips.size(), - controller.craft_vessel_buttons.size()]) + % [controller.craft_menu.price_tags.size(), + controller.craft_menu.vessel_buttons.size()]) var priced: int = 0 var stacked: int = 0 - for i in range(controller.craft_price_strips.size()): + for i in range(controller.craft_menu.price_tags.size()): var definition: Resource = VesselDefinitions.fleet()[i] var costs: Dictionary = game_state.product_costs(definition.definition_id) - var strip: Control = controller.craft_price_strips[i] + var strip: Control = controller.craft_menu.price_tags[i] if costs.is_empty(): continue if strip.is_free(): @@ -122,7 +122,7 @@ func _process(delta: float) -> bool: % [definition.definition_id, box.position.x, box.end.x, strip.size.x]) # ...and the price sits with the button it is about, not # adrift below the caption. - var button: Control = controller.craft_vessel_buttons[i] + var button: Control = controller.craft_menu.vessel_buttons[i] var gap: float = strip.position.y - (button.position.y + button.size.y) if gap > 80.0: return _fail("%s's price sits %.0fpx below its button" \ @@ -135,10 +135,10 @@ func _process(delta: float) -> bool: % [priced, stacked]) # --- F. #147 - and the rows do not run into each other ------- - for i in range(controller.craft_vessel_buttons.size()): - for j in range(i + 1, controller.craft_vessel_buttons.size()): - for part in [controller.craft_vessel_buttons, - controller.craft_vessel_labels, controller.craft_price_strips]: + for i in range(controller.craft_menu.vessel_buttons.size()): + for j in range(i + 1, controller.craft_menu.vessel_buttons.size()): + for part in [controller.craft_menu.vessel_buttons, + controller.craft_menu.vessel_labels, controller.craft_menu.price_tags]: if i >= part.size() or j >= part.size(): continue var a: Control = part[i] @@ -146,9 +146,9 @@ func _process(delta: float) -> bool: if Rect2(a.position, a.size).intersects(Rect2(b.position, b.size)): return _fail("craft entries %d and %d overlap" % [i, j]) # A price must not reach the button of the row below it. - var mine: Control = controller.craft_price_strips[i] - for k in range(controller.craft_vessel_buttons.size()): - var other: Control = controller.craft_vessel_buttons[k] + var mine: Control = controller.craft_menu.price_tags[i] + for k in range(controller.craft_menu.vessel_buttons.size()): + var other: Control = controller.craft_menu.vessel_buttons[k] if other.position.y > mine.position.y \ and Rect2(mine.position, mine.size).intersects( Rect2(other.position, other.size)): @@ -157,26 +157,26 @@ func _process(delta: float) -> bool: # --- C. the hangar --------------------------------------- controller._layout_hangar() - if controller.hangar_divider_y <= 0.0: + if controller.hangar_menu.divider_y <= 0.0: return _fail("the hangar laid out with no divider between its halves") var available_bottom: float = 0.0 var fleet_top: float = 1.0e9 - for label in controller.hangar_available_labels: + for label in controller.hangar_menu.available_labels: available_bottom = maxf(available_bottom, label.position.y + label.size.y) - for slot in controller.hangar_fleet_slots: + for slot in controller.hangar_menu.fleet_slots: fleet_top = minf(fleet_top, slot.position.y) - if not (controller.hangar_divider_y > available_bottom - 40.0 - and controller.hangar_divider_y < fleet_top): + if not (controller.hangar_menu.divider_y > available_bottom - 40.0 + and controller.hangar_menu.divider_y < fleet_top): return _fail("the divider at %.0f is not between the two grids (%.0f / %.0f)" \ - % [controller.hangar_divider_y, available_bottom, fleet_top]) + % [controller.hangar_menu.divider_y, available_bottom, fleet_top]) # Two lines of caption. The font is 24-ish, so anything under # about 44 cannot show a second line at all. - var caption_height: float = controller.hangar_available_labels[0].size.y + var caption_height: float = controller.hangar_menu.available_labels[0].size.y if caption_height < 44.0: return _fail("hangar captions are %.0fpx tall - a two-word name is clipped" \ % caption_height) print("hangar: %.0fpx captions, divider at %.0f between %.0f and %.0f" \ - % [caption_height, controller.hangar_divider_y, available_bottom, fleet_top]) + % [caption_height, controller.hangar_menu.divider_y, available_bottom, fleet_top]) # --- D. #143 - and the caption stays in its box -------------- # The caption is two lines before autowrap and three after it in @@ -185,8 +185,8 @@ func _process(delta: float) -> bool: # the box now, and the box clips, so nothing can reach the next # row however the vessel names grow. controller._refresh_hangar() - for pair in [[controller.hangar_available_labels, controller.hangar_available_slots], - [controller.hangar_fleet_labels, controller.hangar_fleet_slots]]: + for pair in [[controller.hangar_menu.available_labels, controller.hangar_menu.available_slots], + [controller.hangar_menu.fleet_labels, controller.hangar_menu.fleet_slots]]: for i in range(pair[0].size()): var caption: Label = pair[0][i] if not caption.clip_text: @@ -205,7 +205,7 @@ func _process(delta: float) -> bool: and Rect2(caption.position, caption.size).intersects(below): return _fail("caption \"%s\" runs into the icon below it" \ % caption.text.replace("\n", " / ")) - var probe: Label = controller.hangar_available_labels[0] + var probe: Label = controller.hangar_menu.available_labels[0] print("captions fitted to %dpx and clipped; \"%s\" fits its %.0fpx box" \ % [probe.get_theme_font_size("font_size"), probe.text.replace("\n", " / "), probe.size.y]) diff --git a/tests/test_menu_price_unification.gd b/tests/test_menu_price_unification.gd index 82e138e..5e70a1c 100644 --- a/tests/test_menu_price_unification.gd +++ b/tests/test_menu_price_unification.gd @@ -107,12 +107,12 @@ func _process(delta: float) -> bool: # --- B. one price widget everywhere ---------------------- var tags: Array = [] - for entry in controller.build_menu_entries: - tags.append(entry[3]) - tags.append_array(controller.craft_price_strips) - tags.append_array(controller.lab_price_tags) - tags.append(controller.info_cost) - tags.append(controller.skill_cost) + for entry in controller.build_menu.entries: + tags.append(entry.cost) + tags.append_array(controller.craft_menu.price_tags) + tags.append_array(controller.lab_menu.price_tags) + tags.append(controller.build_menu.info_cost) + tags.append(controller.skill_menu.cost) if tags.size() < 15: return _fail("only %d price widgets found - the menus cannot all be covered" % tags.size()) for tag in tags: @@ -124,7 +124,7 @@ func _process(delta: float) -> bool: # --- C. have over cost, smaller, red when short ---------- game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 3) game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0) - var probe: Control = controller.info_cost + var probe: Control = controller.build_menu.info_cost probe.show_price({GameState.RESOURCE_ORE: 25}) if probe.priced(GameState.RESOURCE_ORE) != 25: return _fail("the tag does not carry the price it was given") @@ -165,8 +165,8 @@ func _process(delta: float) -> bool: if String(node.desc).length() > longest.length(): longest = String(node.desc) controller._refresh_skill_menu(String(node.id)) - var detail: Rect2 = _rect(controller.skill_detail) - var price: Rect2 = _rect(controller.skill_cost) + var detail: Rect2 = _rect(controller.skill_menu.detail) + var price: Rect2 = _rect(controller.skill_menu.cost) if detail.intersects(price): return _fail("the explanation (%s) overlaps the price (%s)" % [detail, price]) if detail.position.x >= price.position.x: @@ -179,11 +179,11 @@ func _process(delta: float) -> bool: game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0) game_state.unlocked.clear() controller._refresh_skill_menu("shipyards") - if controller.skill_cost.is_affordable(): + if controller.skill_menu.cost.is_affordable(): return _fail("a node reads as affordable with no ore at all") - if controller.skill_status.text.to_lower().contains("not enough"): + if controller.skill_menu.status.text.to_lower().contains("not enough"): return _fail("the shortfall is still written out as well: '%s'" \ - % controller.skill_status.text) + % controller.skill_menu.status.text) print("the shortfall is shown once, on the price, and not repeated in words") print("TEST_MENU_PRICE_UNIFICATION: PASS") diff --git a/tests/test_menu_scene_boundaries.gd b/tests/test_menu_scene_boundaries.gd new file mode 100644 index 0000000..d825ec2 --- /dev/null +++ b/tests/test_menu_scene_boundaries.gd @@ -0,0 +1,42 @@ +extends SceneTree + +const MENUS: Array[String] = [ + "BuildMenu", "CraftMenu", "LabMenu", "TransferMenu", "HangarMenu", + "FormationMenu", "SkillMenu", "PlanningMenu", "GarrisonMenu", +] + + +func _initialize() -> void: + call_deferred("_verify") + + +func _verify() -> void: + var packed: PackedScene = load("res://world/Main.tscn") + if packed == null: + return _fail("Main.tscn did not load") + var main: Node = packed.instantiate() + root.add_child(main) + for menu_name in MENUS: + var menu: Node = main.get_node_or_null("UI/%s" % menu_name) + if menu == null: + return _fail("%s is missing" % menu_name) + var expected: String = "res://ui/menus/%s.tscn" % menu_name + if menu.scene_file_path != expected: + return _fail("%s is inline in Main.tscn instead of instancing %s" \ + % [menu_name, expected]) + if menu.get_script() == null: + return _fail("%s has no owning script" % menu_name) + + var source: String = FileAccess.get_file_as_string( + "res://controllers/BuildController.gd") + for menu_name in MENUS: + if "../UI/%s/" % menu_name in source: + return _fail("BuildController reaches into %s children" % menu_name) + print("%d menu scenes own their controls; BuildController uses root APIs" % MENUS.size()) + print("TEST_MENU_SCENE_BOUNDARIES: PASS") + quit(0) + + +func _fail(message: String) -> void: + print("TEST_MENU_SCENE_BOUNDARIES: FAIL (%s)" % message) + quit(1) diff --git a/tests/test_menu_scene_boundaries.gd.uid b/tests/test_menu_scene_boundaries.gd.uid new file mode 100644 index 0000000..83cfb7a --- /dev/null +++ b/tests/test_menu_scene_boundaries.gd.uid @@ -0,0 +1 @@ +uid://bufrmk7en8qxd diff --git a/tests/test_payments_use_fleet_hold.gd b/tests/test_payments_use_fleet_hold.gd index 033987d..9b5f539 100644 --- a/tests/test_payments_use_fleet_hold.gd +++ b/tests/test_payments_use_fleet_hold.gd @@ -253,7 +253,7 @@ func _process(delta: float) -> bool: if game_state.buildings.size() != before + 1: return _fail("building a mothership from the fleet's own hold did not place one (%d -> %d): %s" \ - % [before, game_state.buildings.size(), build_controller.build_status.text]) + % [before, game_state.buildings.size(), build_controller.build_menu.status.text]) # Checked as a TOTAL, not as the hold alone: a mothership standing # where the fleet is starts taking delivery of the hold on the # very next tick, so ore legitimately moves from hold to bank diff --git a/tests/test_research_double_tap.gd b/tests/test_research_double_tap.gd index 044f2af..fa33d5e 100644 --- a/tests/test_research_double_tap.gd +++ b/tests/test_research_double_tap.gd @@ -154,13 +154,13 @@ func _process(delta: float) -> bool: controller._refresh_skill_menu(second) # #134: the refusal is shown on the price rather than written out in a # sentence below it - what you hold over what it costs, in red. - if controller.skill_cost.is_affordable(): + if controller.skill_menu.cost.is_affordable(): return _fail("a node the player cannot pay for reads as affordable") - if not controller.skill_cost.is_short(GameState.RESOURCE_ORE): + if not controller.skill_menu.cost.is_short(GameState.RESOURCE_ORE): return _fail("the ore the player does not have is not marked short") print("unaffordable node refused, and its price says so: %d/%d ore" \ - % [controller.skill_cost.held(GameState.RESOURCE_ORE), - controller.skill_cost.priced(GameState.RESOURCE_ORE)]) + % [controller.skill_menu.cost.held(GameState.RESOURCE_ORE), + controller.skill_menu.cost.priced(GameState.RESOURCE_ORE)]) print("TEST_RESEARCH_DOUBLE_TAP: PASS") quit(0) diff --git a/tests/test_research_lab_extractor_gate.gd b/tests/test_research_lab_extractor_gate.gd index 7ff1798..8ef3d09 100644 --- a/tests/test_research_lab_extractor_gate.gd +++ b/tests/test_research_lab_extractor_gate.gd @@ -98,9 +98,9 @@ func _process(delta: float) -> bool: controller._confirm_build() if game_state.extractors.size() != before: return _fail("an extractor was built on a research_lab planet") - if not ("nothing to extract" in controller.build_status.text): - return _fail("refusal did not explain itself, status was '%s'" % controller.build_status.text) - print("extractor refused on a research_lab planet, explained: '%s'" % controller.build_status.text) + if not ("nothing to extract" in controller.build_menu.status.text): + return _fail("refusal did not explain itself, status was '%s'" % controller.build_menu.status.text) + print("extractor refused on a research_lab planet, explained: '%s'" % controller.build_menu.status.text) print("TEST_RESEARCH_LAB_EXTRACTOR_GATE: PASS") quit(0) diff --git a/tests/test_resource_transfer.gd b/tests/test_resource_transfer.gd index 953f313..908c109 100644 --- a/tests/test_resource_transfer.gd +++ b/tests/test_resource_transfer.gd @@ -127,16 +127,14 @@ func _process(delta: float) -> bool: % [controller.transfer_building_id, crafter_id]) # --- 6. the shared bar follows the panel -------------------- - for bar in controller.resource_bars: - _expect(bar.building_id == crafter_id, - "a resource bar is reporting building %d, not the crafter %d" - % [bar.building_id, crafter_id]) + _expect(controller.transfer_menu.building_id == crafter_id, + "transfer scene did not own the selected building context") # --- 5. an empty hold explains itself ----------------------- game_state.fleet_cargo = 0 controller._on_transfer(true, GameState.CARGO_LOAD_BATCH) - _expect("carrying nothing" in controller.transfer_status.text, - "unloading an empty hold said: %s" % controller.transfer_status.text) + _expect("carrying nothing" in controller.transfer_menu.status.text, + "unloading an empty hold said: %s" % controller.transfer_menu.status.text) # --- 2. both directions at an ordinary building ------------- # Baseline taken AFTER the last hand-set figure: the conservation diff --git a/tests/test_skill_tree.gd b/tests/test_skill_tree.gd index ac2a14f..77df9e3 100644 --- a/tests/test_skill_tree.gd +++ b/tests/test_skill_tree.gd @@ -125,19 +125,19 @@ func _process(delta: float) -> bool: return true # The tree is a tall, vertically scrolling canvas. A branch keeps a # stable lane as it rises, and dragging exposes higher depths. - var freight: Vector2 = controller.skill_tree.node_screen_pos(game_state.skill_node("freight")) - var yards: Vector2 = controller.skill_tree.node_screen_pos(game_state.skill_node("freight_yards")) + var freight: Vector2 = controller.skill_menu.tree.node_screen_pos(game_state.skill_node("freight")) + var yards: Vector2 = controller.skill_menu.tree.node_screen_pos(game_state.skill_node("freight_yards")) if absf(freight.x - yards.x) > 1.0 or yards.y >= freight.y: print("TEST_SKILL_TREE: FAIL (freight branch does not rise vertically)") quit(1) return true - var before_scroll: float = controller.skill_tree.scroll_y + var before_scroll: float = controller.skill_menu.tree.scroll_y var drag: InputEventScreenDrag = InputEventScreenDrag.new() # Start at the root at the bottom; pull the canvas down to reveal # the branches rising above it. drag.relative = Vector2(0.0, 180.0) - controller.skill_tree.handle_drag(drag) - if controller.skill_tree.scroll_y >= before_scroll: + controller.skill_menu.tree.handle_drag(drag) + if controller.skill_menu.tree.scroll_y >= before_scroll: print("TEST_SKILL_TREE: FAIL (tree did not scroll upward)") quit(1) return true diff --git a/ui/HangarMenuPanel.gd b/ui/HangarMenuPanel.gd deleted file mode 100644 index 87734fb..0000000 --- a/ui/HangarMenuPanel.gd +++ /dev/null @@ -1,20 +0,0 @@ -extends Control - -# #120: the rule between the hangar's two halves. -# -# The panel itself draws it rather than a separate node, because the y comes -# from BuildController._layout_hangar() - which is also what places the two -# grids - and a divider that had to be positioned independently would drift -# away from the sections it divides the first time either grid changed size. - -const RULE_COLOR: Color = Color(0.45, 0.9, 0.55) - -@onready var controller: Node = get_node("../../BuildController") - - -func _draw() -> void: - if controller == null or controller.hangar_divider_y <= 0.0: - return - var y: float = controller.hangar_divider_y - NeonDraw.draw_glowing_line(self, Vector2(24.0, y), Vector2(size.x - 24.0, y), - RULE_COLOR, 0.5, 0.4) diff --git a/ui/HangarMenuPanel.gd.uid b/ui/HangarMenuPanel.gd.uid deleted file mode 100644 index bf02bf0..0000000 --- a/ui/HangarMenuPanel.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwqdhbksv0xgf diff --git a/ui/menus/BuildMenu.gd b/ui/menus/BuildMenu.gd new file mode 100644 index 0000000..d9bc2f7 --- /dev/null +++ b/ui/menus/BuildMenu.gd @@ -0,0 +1,216 @@ +extends "res://ui/menus/ModalMenu.gd" + +signal building_selected(type: String) +signal build_confirmed + +const BUILDING_DEFINITIONS = preload("res://buildings/BuildingDefinitions.gd") +const MENU_LAYOUT = preload("res://ui/menus/MenuLayout.gd") + +const BUTTON_HALF: float = 64.0 +const BUTTON_HEIGHT: float = 100.0 +const COST_HALF: float = 74.0 +const COST_HEIGHT: float = 86.0 +const LABEL_OFFSET: float = 106.0 +const LABEL_GAP: float = 4.0 +const NATURAL_SPACING: float = 280.0 +const MIN_SPACING: float = BUTTON_HALF * 2.0 + 12.0 +const LABEL_SHARE: float = 0.46 +const LABEL_HEIGHT: float = 52.0 +const LABEL_PADDING: float = 10.0 +const COST_PADDING: float = 12.0 +const SIDE_MARGIN: float = 24.0 +const MAX_COLUMNS: int = 6 + +@onready var status: Label = $StatusLabel +@onready var info_popup: Control = $InfoPopup +@onready var info_title: Label = $InfoPopup/Panel/TitleLabel +@onready var info_message: Label = $InfoPopup/Panel/MessageLabel +@onready var info_cost: Control = $InfoPopup/Panel/CostNumber +@onready var info_build_button: Control = $InfoPopup/Panel/BuildButton +@onready var info_close_button: Control = $InfoPopup/Panel/CloseButton + +var entries: Array = [] + + +func _ready() -> void: + super._ready() + _configure_entries() + info_close_button.pressed.connect(close_info) + info_build_button.pressed.connect(func(): build_confirmed.emit()) + info_popup.visible = false + + +func _configure_entries() -> void: + entries.clear() + for definition in BUILDING_DEFINITIONS.all(): + if definition.menu_button_name == "": + continue + var button: Control = get_node(String(definition.menu_button_name)) + var label: Label = get_node(String(definition.menu_label_name)) + var cost_tag: Control = get_node(String(definition.menu_cost_name)) + button.icon = definition.menu_icon + button.pressed.connect( + func(): building_selected.emit(String(definition.definition_id))) + label.text = definition.display_name + cost_tag.show_price(GameState.building_costs(String(definition.definition_id))) + entries.append({"definition": definition, "button": button, + "label": label, "cost": cost_tag}) + + +func open_menu() -> void: + super.open_menu() + close_info() + + +func close_menu() -> void: + close_info() + super.close_menu() + + +func close_info() -> void: + info_popup.visible = false + + +func show_info(title: String, message: String, costs: Dictionary, + can_build: bool) -> void: + info_title.text = title + info_message.text = message + info_cost.show_price(costs) + info_build_button.visible = can_build + info_popup.visible = true + + +func set_status(message: String) -> void: + status.text = message + + +func refresh(planet: Node2D) -> void: + _layout_entries() + refresh_option_states(planet) + if planet == null: + status.text = "" + elif not GameState.is_planet_conquered(planet.planet_id): + status.text = "Over %s - capture it before building an extractor" % planet.planet_name + else: + status.text = "Over %s - an extractor can be built here" % planet.planet_name + + +func refresh_option_states(planet: Node2D) -> void: + for entry in entries: + var definition: Resource = entry.definition + entry.cost.show_price(GameState.building_costs(String(definition.definition_id))) + var enabled: bool = GameState.is_building_unlocked(definition.definition_id) + if definition.placement_mode == "planet": + enabled = enabled and planet != null \ + and GameState.is_planet_conquered(planet.planet_id) + entry.button.enabled = enabled + entry.button.queue_redraw() + + +func menu_band() -> Vector2: + return MENU_LAYOUT.grid_band(status.offset_bottom, size.y) + + +func menu_column_spacing(columns: int) -> float: + var room: float = size.x - SIDE_MARGIN * 2.0 + if room <= 0.0: + room = 672.0 + return maxf(MIN_SPACING, minf(NATURAL_SPACING, room / float(columns))) + + +func menu_label_half(columns: int) -> float: + return menu_column_spacing(columns) * LABEL_SHARE + + +func measured_label_height(label: Label, text: String, columns: int) -> float: + var font: Font = label.get_theme_font("font") + var font_size: int = label.get_theme_font_size("font_size") + if font == null or font_size <= 0 or text == "": + return LABEL_HEIGHT + var height: float = font.get_multiline_string_size(text, + HORIZONTAL_ALIGNMENT_CENTER, menu_label_half(columns) * 2.0, font_size).y + return maxf(LABEL_HEIGHT, height + LABEL_PADDING) + + +func menu_label_height(columns: int) -> float: + var tallest: float = LABEL_HEIGHT + for entry in entries: + if entry.label.visible: + tallest = maxf(tallest, + measured_label_height(entry.label, entry.label.text, columns)) + return tallest + + +func menu_cost_height() -> float: + var tallest: float = COST_HEIGHT + for entry in entries: + if entry.cost.visible and entry.cost.has_method("wanted_height"): + tallest = maxf(tallest, float(entry.cost.wanted_height()) + COST_PADDING) + return tallest + + +func menu_cost_offset(columns: int) -> float: + return LABEL_OFFSET + menu_label_height(columns) + LABEL_GAP + + +func menu_row_height(columns: int) -> float: + return menu_cost_offset(columns) + menu_cost_height() + + +func menu_columns(count: int) -> int: + var widest: int = maxi(mini(MAX_COLUMNS, + int(floor((size.x - SIDE_MARGIN * 2.0) / MIN_SPACING))), 2) + for columns in range(2, widest + 1): + if _rows_fit(int(ceil(float(count) / float(columns))), columns): + return columns + return widest + + +func menu_rows(count: int) -> int: + var columns: int = menu_columns(count) + return maxi(int(ceil(float(count) / float(columns))), 1) + + +func _rows_fit(rows: int, columns: int) -> bool: + var height: float = menu_row_height(columns) + var band: Vector2 = menu_band() + if rows <= 1: + return band.y - band.x >= height + return (band.y - band.x - height) / float(rows - 1) >= height + + +func menu_row_top(row: int, rows: int, columns: int) -> float: + return MENU_LAYOUT.grid_row_top(row, rows, menu_row_height(columns), + menu_band(), NATURAL_SPACING) + + +func _layout_entries() -> void: + var shown: Array = [] + for entry in entries: + var definition: Resource = entry.definition + var entry_visible: bool = GameState.is_building_unlocked(definition.definition_id) + entry.button.visible = entry_visible + entry.label.visible = entry_visible + entry.cost.visible = entry_visible + if entry_visible: + entry.cost.show_price(GameState.building_costs(String(definition.definition_id))) + shown.append(entry) + + var columns: int = menu_columns(shown.size()) + var rows: int = menu_rows(shown.size()) + var spacing: float = menu_column_spacing(columns) + var label_half: float = menu_label_half(columns) + var label_height: float = menu_label_height(columns) + var cost_offset: float = menu_cost_offset(columns) + var cost_height: float = menu_cost_height() + for i in range(shown.size()): + var row: int = i / columns + var in_row: int = mini(shown.size() - row * columns, columns) + var column: int = i % columns + var cx: float = (float(column) - (float(in_row) - 1.0) * 0.5) * spacing + var top: float = menu_row_top(row, rows, columns) + MENU_LAYOUT.place_entry(shown[i].button, cx, top, BUTTON_HALF, BUTTON_HEIGHT) + MENU_LAYOUT.place_entry(shown[i].label, cx, top + LABEL_OFFSET, + label_half, label_height) + MENU_LAYOUT.place_entry(shown[i].cost, cx, top + cost_offset, + COST_HALF, cost_height) diff --git a/ui/menus/BuildMenu.gd.uid b/ui/menus/BuildMenu.gd.uid new file mode 100644 index 0000000..69d1bf3 --- /dev/null +++ b/ui/menus/BuildMenu.gd.uid @@ -0,0 +1 @@ +uid://b4fmal0r6so5 diff --git a/ui/menus/BuildMenu.tscn b/ui/menus/BuildMenu.tscn new file mode 100644 index 0000000..960e378 --- /dev/null +++ b/ui/menus/BuildMenu.tscn @@ -0,0 +1,544 @@ +[gd_scene load_steps=6 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/NeonPanel.gd" id="20"] +[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"] +[ext_resource type="Script" path="res://ui/PriceTag.gd" id="price_tag"] +[ext_resource type="Script" path="res://ui/menus/BuildMenu.gd" id="menu_script"] + +[node name="BuildMenu" type="Control" groups=["modal_menus"]] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 20 +offset_right = -24 +offset_bottom = 140 +text = "Build" +horizontal_alignment = 1 + +[node name="Bar" type="Control" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24.0 +offset_top = 148 +offset_right = -24.0 +offset_bottom = 330 +script = ExtResource("24") + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 338 +offset_right = -24 +offset_bottom = 418 +theme_override_font_sizes/font_size = 40 +autowrap_mode = 2 +text = "" +horizontal_alignment = 1 + +[node name="MothershipButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -204 +offset_right = -76 +offset_top = -300 +offset_bottom = -172 +script = ExtResource("10") +icon = 8 +glow_color = Color(0.45, 0.9, 0.55, 1) + +[node name="MothershipLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -290 +offset_right = 10 +offset_top = -166 +offset_bottom = -100 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Mothership" +horizontal_alignment = 1 + +[node name="MothershipCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -200 +offset_right = -80 +offset_top = -96 +offset_bottom = -44 +script = ExtResource("price_tag") + +[node name="CrafterButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 76 +offset_right = 204 +offset_top = -300 +offset_bottom = -172 +script = ExtResource("10") +icon = 9 +glow_color = Color(1, 0.72, 0.25, 1) + +[node name="CrafterLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -10 +offset_right = 290 +offset_top = -166 +offset_bottom = -100 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Ship Crafter" +horizontal_alignment = 1 + +[node name="CrafterCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 80 +offset_right = 200 +offset_top = -96 +offset_bottom = -44 +script = ExtResource("price_tag") + +[node name="CommandButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -204 +offset_right = -76 +offset_top = -10 +offset_bottom = 118 +script = ExtResource("10") +icon = 10 +glow_color = Color(0.55, 0.6, 1, 1) + +[node name="CommandLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -290 +offset_right = 10 +offset_top = 124 +offset_bottom = 190 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Command Center" +horizontal_alignment = 1 + +[node name="CommandCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -200 +offset_right = -80 +offset_top = 194 +offset_bottom = 246 +script = ExtResource("price_tag") + +[node name="ExtractorButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 76 +offset_right = 204 +offset_top = -10 +offset_bottom = 118 +script = ExtResource("10") +icon = 11 +glow_color = Color(0, 0.62, 0.7, 1) + +[node name="ExtractorLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -10 +offset_right = 290 +offset_top = 124 +offset_bottom = 190 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Extractor" +horizontal_alignment = 1 + +[node name="ExtractorCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 80 +offset_right = 200 +offset_top = 194 +offset_bottom = 246 +script = ExtResource("price_tag") + +[node name="TransportHubButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -64 +offset_right = 64 +offset_top = 280 +offset_bottom = 408 +script = ExtResource("10") +icon = 12 +glow_color = Color(0.95, 0.55, 0.85, 1) + +[node name="TransportHubLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -150 +offset_right = 150 +offset_top = 414 +offset_bottom = 480 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Transport Hub" +horizontal_alignment = 1 + +[node name="TransportHubCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -60 +offset_right = 60 +offset_top = 484 +offset_bottom = 536 +script = ExtResource("price_tag") + +[node name="ShipLabButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 76 +offset_right = 204 +offset_top = 280 +offset_bottom = 408 +script = ExtResource("10") +icon = 13 +glow_color = Color(0.55, 0.95, 0.9, 1) + +[node name="ShipLabLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -10 +offset_right = 290 +offset_top = 414 +offset_bottom = 480 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Ship Lab" +horizontal_alignment = 1 + +[node name="ShipLabCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 80 +offset_right = 200 +offset_top = 484 +offset_bottom = 536 +script = ExtResource("price_tag") + +[node name="GasBurnerButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -64 +offset_right = 64 +offset_top = 570 +offset_bottom = 698 +script = ExtResource("10") +icon = 33 +glow_color = Color(0.95, 0.35, 0.15, 1) + +[node name="GasBurnerLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -150 +offset_right = 150 +offset_top = 704 +offset_bottom = 770 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Gas Burner" +horizontal_alignment = 1 + +[node name="GasBurnerCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -60 +offset_right = 60 +offset_top = 774 +offset_bottom = 826 +script = ExtResource("price_tag") + +[node name="OutpostButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -64 +offset_right = 64 +offset_top = 830 +offset_bottom = 958 +script = ExtResource("10") +icon = 40 +glow_color = Color(0.72, 0.86, 0.45, 1) + +[node name="OutpostLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -150 +offset_right = 150 +offset_top = 964 +offset_bottom = 1030 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Outpost" +horizontal_alignment = 1 + +[node name="OutpostCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -60 +offset_right = 60 +offset_top = 1034 +offset_bottom = 1086 +script = ExtResource("price_tag") + +[node name="PlanningButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -64 +offset_right = 64 +offset_top = 1090 +offset_bottom = 1218 +script = ExtResource("10") +icon = 41 +glow_color = Color(0.62, 0.74, 0.92, 1) + +[node name="PlanningLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -150 +offset_right = 150 +offset_top = 1224 +offset_bottom = 1290 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Planning Center" +horizontal_alignment = 1 + +[node name="PlanningCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -60 +offset_right = 60 +offset_top = 1294 +offset_bottom = 1346 +script = ExtResource("price_tag") + +[node name="WorkshopButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -64 +offset_right = 64 +offset_top = 1350 +offset_bottom = 1478 +script = ExtResource("10") +icon = 42 +glow_color = Color(0.5, 0.95, 0.8, 1) + +[node name="WorkshopLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -150 +offset_right = 150 +offset_top = 1484 +offset_bottom = 1550 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "Spacial Workshop" +horizontal_alignment = 1 + +[node name="WorkshopCost" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -60 +offset_right = 60 +offset_top = 1554 +offset_bottom = 1606 +script = ExtResource("price_tag") + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -172 +offset_right = -24 +offset_top = 24 +offset_bottom = 172 +script = ExtResource("10") +icon = 1 + +[node name="InfoPopup" type="Control" parent="." groups=["modal_menus"]] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Dim" type="ColorRect" parent="InfoPopup"] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.01, 0.01, 0.03, 0.55) + +[node name="Panel" type="Control" parent="InfoPopup"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -352 +offset_right = 352 +offset_top = -600 +offset_bottom = 600 +script = ExtResource("20") +glow_color = Color(0, 0.62, 0.7, 1) + +[node name="TitleLabel" type="Label" parent="InfoPopup/Panel"] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 44 +offset_right = -44 +offset_top = 30 +offset_bottom = 150 +text = "" +horizontal_alignment = 1 + +[node name="MessageLabel" type="Label" parent="InfoPopup/Panel"] +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 44 +offset_right = -44 +offset_top = 160 +offset_bottom = -440 +theme_override_font_sizes/font_size = 48 +text = "" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 2 + +[node name="CostNumber" type="Control" parent="InfoPopup/Panel"] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = -70 +offset_right = 90 +offset_top = -424 +offset_bottom = -334 +script = ExtResource("price_tag") + +[node name="BuildButton" type="Control" parent="InfoPopup/Panel"] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = -200 +offset_right = -40 +offset_top = -300 +offset_bottom = -140 +script = ExtResource("10") +icon = 5 +glow_color = Color(0.45, 0.9, 0.55, 1) + +[node name="CloseButton" type="Control" parent="InfoPopup/Panel"] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 40 +offset_right = 200 +offset_top = -300 +offset_bottom = -140 +script = ExtResource("10") +icon = 1 +glow_color = Color(0, 0.62, 0.7, 1) diff --git a/ui/menus/CraftMenu.gd b/ui/menus/CraftMenu.gd new file mode 100644 index 0000000..0000008 --- /dev/null +++ b/ui/menus/CraftMenu.gd @@ -0,0 +1,144 @@ +extends "res://ui/menus/ModalMenu.gd" + +signal craft_requested(vessel_type: String) +signal queue_cancel_requested(index: int) + +const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd") +const MENU_LAYOUT = preload("res://ui/menus/MenuLayout.gd") +const PRICE_TAG = preload("res://ui/PriceTag.gd") +const COLUMNS := 3 +const ROW_PITCH := 260.0 +const COLUMN_X := 190.0 +const BUTTON_HALF := 56.0 +const BUTTON_HEIGHT := 100.0 +const LABEL_HALF := 92.0 +const LABEL_HEIGHT := 44.0 +const LABEL_OFFSET := 106.0 +const PRICE_HALF := 92.0 +const PRICE_HEIGHT := 86.0 +const PRICE_OFFSET := 154.0 + +@onready var status: Label = $StatusLabel +@onready var queue_detail: Label = $QueueDetail +@onready var resource_bar: Control = $Bar +@onready var vessel_buttons: Array = [$InterceptorButton, $ShieldButton, + $GasBomberButton, $CarrierButton, $LaserButton, $DistorderButton] +@onready var vessel_labels: Array = [$InterceptorLabel, $ShieldLabel, + $GasBomberLabel, $CarrierLabel, $LaserLabel, $DistorderLabel] +@onready var queue_slots: Array = [$QueueSlot0, $QueueSlot1, $QueueSlot2, $QueueSlot3] + +var building_id: int = 0 +var message: String = "" +var message_until: int = 0 +var price_tags: Array = [] + + +func _ready() -> void: + super._ready() + _build_price_tags() + for i in range(mini(vessel_buttons.size(), VESSEL_DEFINITIONS.fleet().size())): + var definition: Resource = VESSEL_DEFINITIONS.fleet()[i] + vessel_buttons[i].pressed.connect( + func(): craft_requested.emit(String(definition.definition_id))) + for i in range(queue_slots.size()): + queue_slots[i].pressed.connect(func(): queue_cancel_requested.emit(i)) + _layout_entries() + + +func open_for(next_building_id: int) -> void: + building_id = next_building_id + resource_bar.building_id = building_id + open_menu() + refresh() + + +func close_menu() -> void: + super.close_menu() + building_id = 0 + + +func say(next_message: String) -> void: + message = next_message + message_until = Time.get_ticks_msec() + 3000 + refresh() + + +func refresh() -> void: + status.text = "" + var definitions: Array = VESSEL_DEFINITIONS.fleet() + for i in range(mini(definitions.size(), vessel_buttons.size())): + var definition: Resource = definitions[i] + var button: Control = vessel_buttons[i] + vessel_labels[i].text = String(definition.display_name) + if i < price_tags.size(): + price_tags[i].set_costs(GameState.product_costs(definition.definition_id)) + button.icon = definition.icon + button.glow_color = definition.glow_color + button.enabled = GameState.vessel_is_unlocked(definition.definition_id) + button.queue_redraw() + refresh_queue() + if message != "": + if Time.get_ticks_msec() < message_until: + status.text = "%s\n%s" % [message, status.text] + else: + message = "" + + +func refresh_queue() -> void: + var orders: Array = GameState.order_list(building_id) + for i in range(queue_slots.size()): + var slot: Control = queue_slots[i] + slot.visible = i < orders.size() + if not slot.visible: + continue + var order: Dictionary = orders[i] + var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product)) + slot.icon = definition.icon + slot.glow_color = vessel_colour(String(order.product)) + slot.enabled = bool(order.started) + slot.queue_redraw() + if orders.is_empty(): + queue_detail.text = "Nothing queued" + return + var head: Dictionary = orders[0] + if bool(head.started): + queue_detail.text = "Building %s %d%%\nTap to cancel" % [ + GameState.vessel_name(String(head.product)), + int(round(float(head.progress) * 100.0))] + else: + queue_detail.text = "%s waiting for resources\nTap to cancel" \ + % GameState.vessel_name(String(head.product)) + + +func vessel_colour(vessel_type: String) -> Color: + var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type) + return definition.glow_color if definition != null else Color(0.25, 0.85, 1.0) + + +func _build_price_tags() -> void: + for i in range(vessel_buttons.size()): + var tag: Control = PRICE_TAG.new() + tag.name = "CraftPrice%d" % i + tag.set_anchors_preset(Control.PRESET_CENTER) + add_child(tag) + price_tags.append(tag) + + +func row_height() -> float: + return PRICE_OFFSET + PRICE_HEIGHT + + +func _layout_entries() -> void: + var rows: int = maxi(int(ceil(float(vessel_buttons.size()) / float(COLUMNS))), 1) + var band: Vector2 = MENU_LAYOUT.grid_band(status.offset_bottom, size.y) + for i in range(vessel_buttons.size()): + var row: int = i / COLUMNS + var column: int = i % COLUMNS + var cx: float = (float(column) - 1.0) * COLUMN_X + var top: float = MENU_LAYOUT.grid_row_top(row, rows, row_height(), band, ROW_PITCH) + MENU_LAYOUT.place_entry(vessel_buttons[i], cx, top, BUTTON_HALF, BUTTON_HEIGHT) + MENU_LAYOUT.place_entry(vessel_labels[i], cx, top + LABEL_OFFSET, + LABEL_HALF, LABEL_HEIGHT) + if i < price_tags.size(): + MENU_LAYOUT.place_entry(price_tags[i], cx, top + PRICE_OFFSET, + PRICE_HALF, PRICE_HEIGHT) diff --git a/ui/menus/CraftMenu.gd.uid b/ui/menus/CraftMenu.gd.uid new file mode 100644 index 0000000..a34a7bf --- /dev/null +++ b/ui/menus/CraftMenu.gd.uid @@ -0,0 +1 @@ +uid://5oywglpqem62 diff --git a/ui/menus/CraftMenu.tscn b/ui/menus/CraftMenu.tscn new file mode 100644 index 0000000..8769b77 --- /dev/null +++ b/ui/menus/CraftMenu.tscn @@ -0,0 +1,308 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"] +[ext_resource type="Script" path="res://ui/menus/CraftMenu.gd" id="menu_script"] + +[node name="CraftMenu" type="Control" groups=["modal_menus"]] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 20 +offset_right = -200 +offset_bottom = 82 +text = "Build a ship" +horizontal_alignment = 1 + +[node name="Bar" type="Control" parent="."] +anchor_right = 1.0 +mouse_filter = 2 +offset_left = 24 +offset_top = 148 +offset_right = -24 +offset_bottom = 330 +script = ExtResource("24") + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 344 +offset_right = -200 +offset_bottom = 490 +theme_override_font_sizes/font_size = 36 +autowrap_mode = 2 +text = "" +horizontal_alignment = 1 + +[node name="InterceptorButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -310 +offset_right = -245 +offset_top = -100 +offset_bottom = 48 +script = ExtResource("10") +icon = 2 + +[node name="InterceptorLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -330 +offset_right = -225 +offset_top = 56 +offset_bottom = 136 +theme_override_font_sizes/font_size = 24 +text = "Interceptor" +horizontal_alignment = 1 + +[node name="ShieldButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -221 +offset_right = -156 +offset_top = -100 +offset_bottom = 48 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.45, 0.8, 1, 1) + +[node name="ShieldLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -241 +offset_right = -136 +offset_top = 56 +offset_bottom = 136 +theme_override_font_sizes/font_size = 24 +autowrap_mode = 2 +text = "Shield Vessel" +horizontal_alignment = 1 + +[node name="GasBomberButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -132 +offset_right = -67 +offset_top = -100 +offset_bottom = 28 +script = ExtResource("10") +icon = 31 +glow_color = Color(0.96, 0.32, 0.78, 1) + +[node name="GasBomberLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -152 +offset_right = -47 +offset_top = 56 +offset_bottom = 150 +theme_override_font_sizes/font_size = 24 +autowrap_mode = 2 +text = "Gas Bomber" +horizontal_alignment = 1 + +[node name="CarrierButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -43 +offset_right = 22 +offset_top = -100 +offset_bottom = 48 +script = ExtResource("10") +icon = 34 +glow_color = Color(1, 0.78, 0.3, 1) + +[node name="CarrierLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -63 +offset_right = 42 +offset_top = 56 +offset_bottom = 136 +theme_override_font_sizes/font_size = 24 +autowrap_mode = 2 +text = "Carrier" +horizontal_alignment = 1 + +[node name="LaserButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 46 +offset_right = 111 +offset_top = -100 +offset_bottom = 48 +script = ExtResource("10") +icon = 35 +glow_color = Color(0.3, 0.9, 1, 1) + +[node name="LaserLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 26 +offset_right = 131 +offset_top = 56 +offset_bottom = 136 +theme_override_font_sizes/font_size = 24 +autowrap_mode = 2 +text = "Laser Ship" +horizontal_alignment = 1 + +[node name="DistorderButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 135 +offset_right = 200 +offset_top = -100 +offset_bottom = 48 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + +[node name="DistorderLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 115 +offset_right = 220 +offset_top = 56 +offset_bottom = 136 +theme_override_font_sizes/font_size = 24 +autowrap_mode = 2 +text = "Distorder" +horizontal_alignment = 1 + +[node name="QueueLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 1.0 +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +offset_left = -200 +offset_right = -10 +offset_top = -450 +offset_bottom = -390 +theme_override_font_sizes/font_size = 26 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Queue" + +[node name="QueueDetail" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 24 +offset_right = -24 +offset_top = -150 +offset_bottom = -30 +theme_override_font_sizes/font_size = 26 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="QueueSlot0" type="Control" parent="."] +visible = false +anchor_left = 1.0 +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +offset_left = -140 +offset_right = -30 +offset_top = -380 +offset_bottom = -270 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="QueueSlot1" type="Control" parent="."] +visible = false +anchor_left = 1.0 +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +offset_left = -140 +offset_right = -30 +offset_top = -262 +offset_bottom = -152 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="QueueSlot2" type="Control" parent="."] +visible = false +anchor_left = 1.0 +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +offset_left = -140 +offset_right = -30 +offset_top = -144 +offset_bottom = -34 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="QueueSlot3" type="Control" parent="."] +visible = false +anchor_left = 1.0 +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +offset_left = -140 +offset_right = -30 +offset_top = -26 +offset_bottom = 84 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -140 +offset_right = -24 +offset_top = 20 +offset_bottom = 136 +script = ExtResource("10") +icon = 1 diff --git a/ui/menus/FormationMenu.gd b/ui/menus/FormationMenu.gd new file mode 100644 index 0000000..02dec18 --- /dev/null +++ b/ui/menus/FormationMenu.gd @@ -0,0 +1,36 @@ +extends "res://ui/menus/ModalMenu.gd" + +signal formation_changed +signal reset_requested + +@onready var editor: Control = $Editor +@onready var reset_button: Control = $ResetButton + + +func _ready() -> void: + super._ready() + reset_button.pressed.connect(func(): reset_requested.emit()) + editor.formation_changed.connect(func(): formation_changed.emit()) + + +func open_menu() -> void: + super.open_menu() + editor.queue_redraw() + + +func redraw() -> void: + editor.queue_redraw() + + +func handle_unhandled_input(event: InputEvent) -> bool: + if not visible: + return false + if event is InputEventScreenTouch: + editor.handle_touch(event) + elif event is InputEventScreenDrag: + editor.handle_drag(event) + elif event is InputEventMouseButton: + editor.handle_mouse_button(event) + elif event is InputEventMouseMotion: + editor.handle_mouse_motion(event) + return true diff --git a/ui/menus/FormationMenu.gd.uid b/ui/menus/FormationMenu.gd.uid new file mode 100644 index 0000000..5b37666 --- /dev/null +++ b/ui/menus/FormationMenu.gd.uid @@ -0,0 +1 @@ +uid://bigv0ubmyddob diff --git a/ui/menus/FormationMenu.tscn b/ui/menus/FormationMenu.tscn new file mode 100644 index 0000000..b8891d1 --- /dev/null +++ b/ui/menus/FormationMenu.tscn @@ -0,0 +1,90 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/FormationEditor.gd" id="17"] +[ext_resource type="Script" path="res://ui/menus/FormationMenu.gd" id="menu_script"] + +[node name="FormationMenu" type="Control" groups=["modal_menus"]] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 70 +offset_right = -24 +offset_bottom = 220 +theme_override_font_sizes/font_size = 44 +autowrap_mode = 2 +text = "Fleet formation - drag to arrange" +horizontal_alignment = 1 + +[node name="Editor" type="Control" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -300 +offset_right = 300 +offset_top = -300 +offset_bottom = 300 +script = ExtResource("17") + +[node name="ResetButton" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = -74 +offset_right = 74 +offset_top = -270 +offset_bottom = -122 +script = ExtResource("10") +icon = 3 + +[node name="ResetLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = -180 +offset_right = 180 +offset_top = -114 +offset_bottom = -8 +theme_override_font_sizes/font_size = 60 +text = "Reset" +horizontal_alignment = 1 + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -172 +offset_right = -24 +offset_top = 24 +offset_bottom = 172 +script = ExtResource("10") +icon = 1 + +[node name="FadeOverlay" type="CanvasLayer" parent="."] +layer = 10 + +[node name="FadeRect" type="ColorRect" parent="FadeOverlay"] +mouse_filter = 2 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0) diff --git a/ui/menus/GarrisonMenu.gd b/ui/menus/GarrisonMenu.gd new file mode 100644 index 0000000..4464726 --- /dev/null +++ b/ui/menus/GarrisonMenu.gd @@ -0,0 +1,39 @@ +extends "res://ui/menus/ModalMenu.gd" + +@onready var panel: Control = $Panel +@onready var status: Label = $StatusLabel + +var building_id: int = 0 + + +func _ready() -> void: + super._ready() + panel.changed.connect(refresh) + + +func open_for(next_building_id: int) -> void: + building_id = next_building_id + panel.building_id = building_id + open_menu() + refresh() + + +func close_menu() -> void: + super.close_menu() + building_id = 0 + panel.building_id = 0 + + +func refresh() -> void: + if building_id == 0: + return + panel.building_id = building_id + panel.refresh() + var ordered: int = GameState.garrison_desired_total(building_id) + var crewed: int = GameState.garrison_crew(building_id).size() + if ordered == 0: + status.text = "No vessels ordered. Only armed vessels can hold a position." + elif crewed >= ordered: + status.text = "Garrison at full strength" + else: + status.text = "%d of %d berths crewed - vessels are sent from the mothership as they become available" % [crewed, ordered] diff --git a/ui/menus/GarrisonMenu.gd.uid b/ui/menus/GarrisonMenu.gd.uid new file mode 100644 index 0000000..f71f957 --- /dev/null +++ b/ui/menus/GarrisonMenu.gd.uid @@ -0,0 +1 @@ +uid://cdfkcdqvwdsh3 diff --git a/ui/menus/GarrisonMenu.tscn b/ui/menus/GarrisonMenu.tscn new file mode 100644 index 0000000..8b4ea82 --- /dev/null +++ b/ui/menus/GarrisonMenu.tscn @@ -0,0 +1,62 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/GarrisonPanel.gd" id="33"] +[ext_resource type="Script" path="res://ui/menus/GarrisonMenu.gd" id="menu_script"] + +[node name="GarrisonMenu" type="Control" groups=["modal_menus"]] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24.0 +offset_top = 20.0 +offset_right = -200.0 +offset_bottom = 82.0 +text = "Outpost Garrison" +horizontal_alignment = 1 + +[node name="Panel" type="Control" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 40.0 +offset_top = 120.0 +offset_right = -40.0 +offset_bottom = 620.0 +script = ExtResource("33") + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 32.0 +offset_top = -160.0 +offset_right = -32.0 +offset_bottom = -24.0 +theme_override_font_sizes/font_size = 30 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -140.0 +offset_top = 20.0 +offset_right = -24.0 +offset_bottom = 136.0 +script = ExtResource("10") +icon = 1 +glow_color = Color(0.72, 0.86, 0.45, 1) diff --git a/ui/menus/HangarMenu.gd b/ui/menus/HangarMenu.gd new file mode 100644 index 0000000..080ca30 --- /dev/null +++ b/ui/menus/HangarMenu.gd @@ -0,0 +1,138 @@ +extends "res://ui/menus/ModalMenu.gd" + +signal crew_requested(vessel_type: String) +signal store_requested(vessel_type: String) + +const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd") +const RULE_COLOR := Color(0.45, 0.9, 0.55) +const COLUMNS := 3 +const LABEL_GAP := 2.0 +const DIVIDER_GAP := 10.0 +const CAPTION_LINES := 3.0 +const CAPTION_MIN := 13 +const CAPTION_MAX := 22 +const LINE_RATIO := 1.25 + +@onready var status: Label = $StatusLabel +@onready var available_title: Label = $AvailableTitle +@onready var fleet_title: Label = $FleetTitle +@onready var available_slots: Array = [$AvailableInterceptor, $AvailableShield, + $AvailableGasBomber, $AvailableCarrier, $AvailableLaser, $AvailableDistorder] +@onready var available_labels: Array = [$AvailableInterceptorLabel, $AvailableShieldLabel, + $AvailableGasBomberLabel, $AvailableCarrierLabel, $AvailableLaserLabel, + $AvailableDistorderLabel] +@onready var fleet_slots: Array = [$FleetInterceptor, $FleetShield, $FleetGasBomber, + $FleetCarrier, $FleetLaser, $FleetDistorder] +@onready var fleet_labels: Array = [$FleetInterceptorLabel, $FleetShieldLabel, + $FleetGasBomberLabel, $FleetCarrierLabel, $FleetLaserLabel, $FleetDistorderLabel] + +var divider_y: float = 0.0 + + +func _ready() -> void: + super._ready() + for i in range(available_slots.size()): + available_slots[i].pressed.connect( + func(): crew_requested.emit(_vessel_types()[i])) + fleet_slots[i].pressed.connect( + func(): store_requested.emit(_vessel_types()[i])) + _layout_sections() + + +func open_for(fleet: Node) -> void: + status.text = "" + open_menu() + refresh(fleet) + + +func set_status(message: String) -> void: + status.text = message + + +func refresh(fleet: Node) -> void: + var types: Array = _vessel_types() + for i in range(mini(types.size(), available_slots.size())): + var vessel_type: String = types[i] + var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type) + var available: int = GameState.stored_vessels.count(vessel_type) + var deployed: int = 0 + for ship in fleet.ships: + if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type: + deployed += 1 + available_labels[i].text = "%s\n%d available" % [definition.display_name, available] + fleet_labels[i].text = "%s\n%d in fleet" % [definition.display_name, deployed] + for slot in [available_slots[i], fleet_slots[i]]: + slot.icon = definition.icon + slot.glow_color = definition.glow_color + available_slots[i].enabled = available > 0 \ + and fleet.ships.size() < GameState.max_fleet_size() + fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1 + available_slots[i].queue_redraw() + fleet_slots[i].queue_redraw() + + +func _vessel_types() -> Array: + var types: Array = [] + for definition in VESSEL_DEFINITIONS.fleet(): + types.append(String(definition.definition_id)) + return types + + +func _layout_sections() -> void: + var count: int = available_slots.size() + if count == 0: + return + var columns: int = mini(COLUMNS, count) + var rows: int = int(ceil(float(count) / float(columns))) + var top: float = available_title.position.y + available_title.size.y + 8.0 + var bottom: float = status.position.y - 12.0 + var title_height: float = fleet_title.size.y + var section_height: float = (bottom - top - title_height - 16.0) * 0.5 + if section_height <= 0.0: + return + var cell_width: float = size.x / float(columns) + var cell_height: float = section_height / float(rows) + var slot_size: float = minf(cell_height * 0.46, cell_width * 0.40) + var label_height: float = maxf(cell_height - slot_size - LABEL_GAP * 2.0, 1.0) + _layout_section(available_slots, available_labels, top, columns, cell_width, + cell_height, slot_size, label_height) + var available_bottom: float = top + section_height + divider_y = available_bottom + DIVIDER_GAP + fleet_title.position.y = divider_y + DIVIDER_GAP + var fleet_top: float = fleet_title.position.y + title_height + 8.0 + _layout_section(fleet_slots, fleet_labels, fleet_top, columns, cell_width, + cell_height, slot_size, label_height) + queue_redraw() + + +func _layout_section(slots: Array, labels: Array, top: float, columns: int, + cell_width: float, cell_height: float, slot_size: float, + label_height: float) -> void: + if columns <= 0: + return + for i in range(slots.size()): + var column: int = i % columns + var row: int = i / columns + var cell_x: float = float(column) * cell_width + var cell_y: float = top + float(row) * cell_height + slots[i].position = Vector2(cell_x + (cell_width - slot_size) * 0.5, cell_y) + slots[i].size = Vector2(slot_size, slot_size) + labels[i].position = Vector2(cell_x, cell_y + slot_size + LABEL_GAP) + labels[i].size = Vector2(cell_width, label_height) + labels[i].horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + labels[i].vertical_alignment = VERTICAL_ALIGNMENT_TOP + labels[i].autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + labels[i].add_theme_font_size_override("font_size", caption_size(label_height)) + labels[i].clip_text = true + + +func caption_size(label_height: float) -> int: + return clampi(int(label_height / (CAPTION_LINES * LINE_RATIO)), + CAPTION_MIN, CAPTION_MAX) + + +func _draw() -> void: + if divider_y <= 0.0: + return + NeonDraw.draw_glowing_line(self, Vector2(24.0, divider_y), + Vector2(size.x - 24.0, divider_y), RULE_COLOR, 0.5, 0.4) diff --git a/ui/menus/HangarMenu.gd.uid b/ui/menus/HangarMenu.gd.uid new file mode 100644 index 0000000..89d30c0 --- /dev/null +++ b/ui/menus/HangarMenu.gd.uid @@ -0,0 +1 @@ +uid://c386w72vtqmdw diff --git a/ui/menus/HangarMenu.tscn b/ui/menus/HangarMenu.tscn new file mode 100644 index 0000000..c0faf7a --- /dev/null +++ b/ui/menus/HangarMenu.tscn @@ -0,0 +1,388 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/menus/HangarMenu.gd" id="39"] + +[node name="HangarMenu" type="Control" groups=["modal_menus"]] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("39") + +[node name="Backdrop" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 60 +offset_right = -210 +offset_bottom = 170 +text = "Hangar" +horizontal_alignment = 1 + +[node name="ListLabel" type="Label" parent="."] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 32 +offset_top = 200 +offset_right = -32 +offset_bottom = -420 +theme_override_font_sizes/font_size = 30 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 32 +offset_top = -410 +offset_right = -32 +offset_bottom = -300 +theme_override_font_sizes/font_size = 30 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="NextButton" type="Control" parent="."] +visible = false +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -230 +offset_right = -110 +offset_top = -280 +offset_bottom = -160 +script = ExtResource("10") +icon = 6 +glow_color = Color(0.55, 0.95, 0.9, 1) + +[node name="CrewButton" type="Control" parent="."] +visible = false +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = 110 +offset_right = 230 +offset_top = -280 +offset_bottom = -160 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.45, 0.9, 0.55, 1) + +[node name="HintLabel" type="Label" parent="."] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 24 +offset_top = -150 +offset_right = -24 +offset_bottom = -60 +theme_override_font_sizes/font_size = 28 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Next Crew" + +[node name="AvailableTitle" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_top = 190 +offset_bottom = 240 +theme_override_font_sizes/font_size = 28 +text = "AVAILABLE — TAP TO CREW" +horizontal_alignment = 1 + +[node name="AvailableInterceptor" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -290 +offset_right = -215 +offset_top = 255 +offset_bottom = 375 +script = ExtResource("10") +icon = 2 + +[node name="AvailableInterceptorLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -330 +offset_right = -175 +offset_top = 380 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="AvailableShield" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -189 +offset_right = -114 +offset_top = 255 +offset_bottom = 375 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.45, 0.8, 1, 1) + +[node name="AvailableShieldLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -229 +offset_right = -74 +offset_top = 380 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="AvailableGasBomber" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -88 +offset_right = -13 +offset_top = 255 +offset_bottom = 375 +script = ExtResource("10") +icon = 31 +glow_color = Color(0.96, 0.32, 0.78, 1) + +[node name="AvailableGasBomberLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -128 +offset_right = 27 +offset_top = 380 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="AvailableCarrier" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 13 +offset_right = 88 +offset_top = 255 +offset_bottom = 375 +script = ExtResource("10") +icon = 34 +glow_color = Color(1, 0.78, 0.3, 1) + +[node name="AvailableCarrierLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -27 +offset_right = 128 +offset_top = 380 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="AvailableLaser" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 114 +offset_right = 189 +offset_top = 255 +offset_bottom = 375 +script = ExtResource("10") +icon = 35 +glow_color = Color(0.3, 0.9, 1, 1) + +[node name="AvailableLaserLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 74 +offset_right = 229 +offset_top = 380 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="AvailableDistorder" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 215 +offset_right = 290 +offset_top = 255 +offset_bottom = 375 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + +[node name="AvailableDistorderLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 175 +offset_right = 330 +offset_top = 380 +offset_bottom = 460 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="FleetTitle" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_top = 500 +offset_bottom = 550 +theme_override_font_sizes/font_size = 28 +text = "FLEET — TAP TO STORE" +horizontal_alignment = 1 + +[node name="FleetInterceptor" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -290 +offset_right = -215 +offset_top = 565 +offset_bottom = 685 +script = ExtResource("10") +icon = 2 + +[node name="FleetInterceptorLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -330 +offset_right = -175 +offset_top = 690 +offset_bottom = 770 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="FleetShield" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -189 +offset_right = -114 +offset_top = 565 +offset_bottom = 685 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.45, 0.8, 1, 1) + +[node name="FleetShieldLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -229 +offset_right = -74 +offset_top = 690 +offset_bottom = 770 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="FleetGasBomber" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -88 +offset_right = -13 +offset_top = 565 +offset_bottom = 685 +script = ExtResource("10") +icon = 31 +glow_color = Color(0.96, 0.32, 0.78, 1) + +[node name="FleetGasBomberLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -128 +offset_right = 27 +offset_top = 690 +offset_bottom = 770 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="FleetCarrier" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 13 +offset_right = 88 +offset_top = 565 +offset_bottom = 685 +script = ExtResource("10") +icon = 34 +glow_color = Color(1, 0.78, 0.3, 1) + +[node name="FleetCarrierLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -27 +offset_right = 128 +offset_top = 690 +offset_bottom = 770 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="FleetLaser" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 114 +offset_right = 189 +offset_top = 565 +offset_bottom = 685 +script = ExtResource("10") +icon = 35 +glow_color = Color(0.3, 0.9, 1, 1) + +[node name="FleetLaserLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 74 +offset_right = 229 +offset_top = 690 +offset_bottom = 770 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="FleetDistorder" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 215 +offset_right = 290 +offset_top = 565 +offset_bottom = 685 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + +[node name="FleetDistorderLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 175 +offset_right = 330 +offset_top = 690 +offset_bottom = 770 +theme_override_font_sizes/font_size = 22 +horizontal_alignment = 1 + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -172 +offset_top = 24 +offset_right = -24 +offset_bottom = 172 +script = ExtResource("10") +icon = 1 +glow_color = Color(0.45, 0.9, 0.55, 1) diff --git a/ui/menus/LabMenu.gd b/ui/menus/LabMenu.gd new file mode 100644 index 0000000..9366f38 --- /dev/null +++ b/ui/menus/LabMenu.gd @@ -0,0 +1,157 @@ +extends "res://ui/menus/ModalMenu.gd" + +signal vessel_selected(index: int) +signal upgrade_selected(index: int) + +const PRICE_TAG = preload("res://ui/PriceTag.gd") +const TAG_HEIGHT := 38.0 +const LABEL_HEIGHT := 40.0 +const TAG_PADDING := 8.0 +const ROW_GAP := 18.0 +const VESSEL_HALF := 30.0 +const VESSEL_SPACING := 90.0 +const VESSEL_MARGIN := 24.0 + +@onready var vessel_buttons: Array = [$VesselA, $VesselB, $VesselC, $VesselD, + $VesselE, $VesselF, $VesselG, $VesselH] +@onready var vessel_labels: Array = [$VesselALabel, $VesselBLabel, $VesselCLabel, + $VesselDLabel, $VesselELabel, $VesselFLabel, $VesselGLabel, $VesselHLabel] +@onready var upgrade_buttons: Array = [$Upgrade0, $Upgrade1, $Upgrade2, $Upgrade3] +@onready var upgrade_labels: Array = [$Upgrade0Label, $Upgrade1Label, + $Upgrade2Label, $Upgrade3Label] +@onready var vessel_heading: Label = $VesselLabel +@onready var detail: Label = $DetailLabel +@onready var status: Label = $StatusLabel +@onready var resource_bar: Control = $Bar + +var building_id: int = 0 +var price_tags: Array = [] + + +func _ready() -> void: + super._ready() + _build_price_tags() + for i in range(vessel_buttons.size()): + vessel_buttons[i].pressed.connect(func(): vessel_selected.emit(i)) + for i in range(upgrade_buttons.size()): + upgrade_buttons[i].pressed.connect(func(): upgrade_selected.emit(i)) + + +func open_for(next_building_id: int) -> void: + building_id = next_building_id + resource_bar.building_id = building_id + open_menu() + + +func close_menu() -> void: + super.close_menu() + building_id = 0 + + +func render_vessels(definitions: Array, selected_type: String) -> void: + for i in range(mini(vessel_buttons.size(), definitions.size())): + var definition: Resource = definitions[i] + vessel_buttons[i].enabled = true + vessel_buttons[i].icon = definition.icon + vessel_buttons[i].glow_color = definition.glow_color + vessel_buttons[i].highlighted = String(definition.definition_id) == selected_type + vessel_labels[i].text = definition.display_name + vessel_buttons[i].queue_redraw() + _layout_vessels() + + +func render_options(options: Array, selected: int, vessel_colour: Color) -> void: + for i in range(upgrade_buttons.size()): + var shown: bool = i < options.size() + upgrade_buttons[i].visible = shown + upgrade_labels[i].visible = shown + price_tags[i].visible = shown + if not shown: + continue + var option: Dictionary = options[i] + var maxed: bool = bool(option.get("maxed", false)) + upgrade_labels[i].text = String(option.label) + price_tags[i].visible = not maxed + price_tags[i].show_price({} if maxed else option.costs, + 0 if maxed else int(option.points)) + upgrade_buttons[i].icon = int(option.icon) + upgrade_buttons[i].glow_color = vessel_colour + upgrade_buttons[i].highlighted = i == selected + upgrade_buttons[i].enabled = true + upgrade_buttons[i].queue_redraw() + _resize_price_tags() + + +func show_detail(heading: String, description: String, points: int) -> void: + detail.text = "%s\n%s" % [heading, description] + if points > 0: + detail.text += "\nResearch points: %d/%d" % [GameState.research_points, points] + + +func clear_detail() -> void: + detail.text = "" + + +func set_status(message: String) -> void: + status.text = message + + +func _build_price_tags() -> void: + for i in range(upgrade_labels.size()): + var label: Control = upgrade_labels[i] + var tag: Control = PRICE_TAG.new() + tag.name = "LabPrice%d" % i + tag.anchor_left = label.anchor_left + tag.anchor_right = label.anchor_right + tag.anchor_top = label.anchor_top + tag.anchor_bottom = label.anchor_bottom + tag.offset_left = label.offset_left + tag.offset_right = label.offset_right + label.offset_bottom = label.offset_top + LABEL_HEIGHT + tag.offset_top = label.offset_bottom + 2.0 + tag.offset_bottom = tag.offset_top + tag_height() + add_child(tag) + price_tags.append(tag) + + +func tag_height() -> float: + var tallest: float = TAG_HEIGHT + for tag in price_tags: + if tag != null and tag.has_method("wanted_height"): + tallest = maxf(tallest, float(tag.wanted_height()) + TAG_PADDING) + return tallest + + +func _layout_vessels() -> void: + var shown: Array = [] + for i in range(vessel_buttons.size()): + if vessel_buttons[i].visible: + shown.append(i) + if shown.is_empty(): + return + var room: float = size.x - VESSEL_MARGIN * 2.0 + if room <= 0.0: + room = 672.0 + var spacing: float = minf(VESSEL_SPACING, room / float(shown.size())) + for slot in range(shown.size()): + var i: int = shown[slot] + var cx: float = (float(slot) - (float(shown.size()) - 1.0) * 0.5) * spacing + vessel_buttons[i].offset_left = cx - VESSEL_HALF + vessel_buttons[i].offset_right = cx + VESSEL_HALF + vessel_labels[i].offset_left = cx - spacing * 0.5 + vessel_labels[i].offset_right = cx + spacing * 0.5 + + +func _resize_price_tags() -> void: + var height: float = tag_height() + for tag in price_tags: + tag.offset_bottom = tag.offset_top + height + if price_tags.size() < 3: + return + var shift: float = price_tags[0].offset_bottom + ROW_GAP - upgrade_buttons[2].offset_top + if absf(shift) < 0.5: + return + for i in range(2, upgrade_buttons.size()): + for node in [upgrade_buttons[i], upgrade_labels[i], price_tags[i]]: + node.offset_top += shift + node.offset_bottom += shift diff --git a/ui/menus/LabMenu.gd.uid b/ui/menus/LabMenu.gd.uid new file mode 100644 index 0000000..f53a844 --- /dev/null +++ b/ui/menus/LabMenu.gd.uid @@ -0,0 +1 @@ +uid://dq44uh8vf0qp0 diff --git a/ui/menus/LabMenu.tscn b/ui/menus/LabMenu.tscn new file mode 100644 index 0000000..adf32c5 --- /dev/null +++ b/ui/menus/LabMenu.tscn @@ -0,0 +1,426 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"] +[ext_resource type="Script" path="res://ui/menus/LabMenu.gd" id="menu_script"] + +[node name="LabMenu" type="Control" groups=["modal_menus"]] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 20 +offset_right = -200 +offset_bottom = 82 +text = "Ship Lab" +horizontal_alignment = 1 + +[node name="Bar" type="Control" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 148 +offset_right = -24 +offset_bottom = 330 +script = ExtResource("24") + +[node name="VesselA" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -300 +offset_right = -240 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="VesselB" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -210 +offset_right = -150 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 2 +glow_color = Color(0.45, 0.8, 1, 1) + +[node name="VesselC" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -120 +offset_right = -60 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 31 +glow_color = Color(0.96, 0.32, 0.78, 1) + +[node name="VesselD" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -30 +offset_right = 30 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 12 +glow_color = Color(0.95, 0.55, 0.85, 1) + +[node name="VesselE" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 60 +offset_right = 120 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 34 +glow_color = Color(1, 0.78, 0.3, 1) + +[node name="VesselF" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 150 +offset_right = 210 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 35 +glow_color = Color(0.3, 0.9, 1, 1) + +[node name="VesselG" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 240 +offset_right = 300 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 37 +glow_color = Color(0.7, 0.3, 1, 1) + +[node name="VesselH" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 320 +offset_right = 380 +offset_top = 346 +offset_bottom = 466 +script = ExtResource("10") +icon = 43 +glow_color = Color(0.5, 0.95, 0.8, 1) + +[node name="VesselLabel" type="Label" parent="."] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 470 +offset_right = -24 +offset_bottom = 528 +theme_override_font_sizes/font_size = 34 +horizontal_alignment = 1 +text = "" + +[node name="VesselALabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -310 +offset_right = -230 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Interceptor" + +[node name="VesselBLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -220 +offset_right = -140 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Shield" + +[node name="VesselCLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -130 +offset_right = -50 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Gas Bomber" + +[node name="VesselDLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -40 +offset_right = 40 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Transporter" + +[node name="VesselELabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 50 +offset_right = 130 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Carrier" + +[node name="VesselFLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 140 +offset_right = 220 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Laser Ship" + +[node name="VesselGLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 230 +offset_right = 310 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Distorder" + +[node name="VesselHLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 310 +offset_right = 390 +offset_top = 470 +offset_bottom = 542 +theme_override_font_sizes/font_size = 15 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Repair Drone" + +[node name="VesselUpgradeSeparator" type="ColorRect" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 48 +offset_top = 550 +offset_right = -48 +offset_bottom = 554 +color = Color(0.25, 0.85, 1, 0.6) + +[node name="UpgradeList" type="Label" parent="."] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 32 +offset_top = 562 +offset_right = -32 +offset_bottom = -390 +theme_override_font_sizes/font_size = 30 +autowrap_mode = 2 +text = "" + +[node name="DetailLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 32 +offset_top = -300 +offset_right = -32 +offset_bottom = -180 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 32 +offset_top = -174 +offset_right = -32 +offset_bottom = -120 +theme_override_font_sizes/font_size = 30 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="NextButton" type="Control" parent="."] +visible = false +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -230 +offset_right = -110 +offset_top = -112 +offset_bottom = -22 +script = ExtResource("10") +icon = 6 +glow_color = Color(0.55, 0.95, 0.9, 1) + +[node name="HintLabel" type="Label" parent="."] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 24 +offset_top = -102 +offset_right = -24 +offset_bottom = -22 +theme_override_font_sizes/font_size = 28 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "Next upgrade Buy" + +[node name="Upgrade0" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -240 +offset_right = -120 +offset_top = 562 +offset_bottom = 672 +script = ExtResource("10") +icon = 13 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="Upgrade0Label" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -310 +offset_right = -50 +offset_top = 676 +offset_bottom = 746 +theme_override_font_sizes/font_size = 22 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="Upgrade1" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 120 +offset_right = 240 +offset_top = 562 +offset_bottom = 672 +script = ExtResource("10") +icon = 13 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="Upgrade1Label" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 50 +offset_right = 310 +offset_top = 676 +offset_bottom = 746 +theme_override_font_sizes/font_size = 22 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="Upgrade2" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -240 +offset_right = -120 +offset_top = 790 +offset_bottom = 900 +script = ExtResource("10") +icon = 13 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="Upgrade2Label" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -310 +offset_right = -50 +offset_top = 904 +offset_bottom = 974 +theme_override_font_sizes/font_size = 22 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="Upgrade3" type="Control" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 120 +offset_right = 240 +offset_top = 790 +offset_bottom = 900 +script = ExtResource("10") +icon = 13 +glow_color = Color(0.25, 0.85, 1, 1) + +[node name="Upgrade3Label" type="Label" parent="."] +mouse_filter = 2 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = 50 +offset_right = 310 +offset_top = 904 +offset_bottom = 974 +theme_override_font_sizes/font_size = 22 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -140 +offset_top = 20 +offset_right = -24 +offset_bottom = 136 +script = ExtResource("10") +icon = 1 +glow_color = Color(0.55, 0.95, 0.9, 1) diff --git a/ui/menus/MenuLayout.gd b/ui/menus/MenuLayout.gd new file mode 100644 index 0000000..45319e2 --- /dev/null +++ b/ui/menus/MenuLayout.gd @@ -0,0 +1,27 @@ +extends RefCounted + +const HEADER_GAP: float = 16.0 +const BOTTOM_MARGIN: float = 28.0 + + +static func grid_band(header_bottom: float, panel_height: float) -> Vector2: + var half: float = panel_height * 0.5 + if half <= 0.0: + half = 640.0 + return Vector2(header_bottom + HEADER_GAP - half, half - BOTTOM_MARGIN) + + +static func grid_row_top(row: int, rows: int, row_height: float, band: Vector2, + natural_pitch: float) -> float: + var pitch: float = natural_pitch + if rows > 1: + pitch = minf(pitch, (band.y - band.x - row_height) / float(rows - 1)) + return band.x + maxf(pitch, row_height) * float(row) + + +static func place_entry(node: Control, cx: float, top: float, half: float, + height: float) -> void: + node.offset_left = cx - half + node.offset_right = cx + half + node.offset_top = top + node.offset_bottom = top + height diff --git a/ui/menus/MenuLayout.gd.uid b/ui/menus/MenuLayout.gd.uid new file mode 100644 index 0000000..4311016 --- /dev/null +++ b/ui/menus/MenuLayout.gd.uid @@ -0,0 +1 @@ +uid://mhjg8jhbmy6f diff --git a/ui/menus/ModalMenu.gd b/ui/menus/ModalMenu.gd new file mode 100644 index 0000000..f4e56a5 --- /dev/null +++ b/ui/menus/ModalMenu.gd @@ -0,0 +1,24 @@ +extends Control + +signal closed + +@onready var close_button: Control = get_node_or_null("CloseButton") + + +func _ready() -> void: + visible = false + if close_button != null and close_button.has_signal("pressed"): + close_button.pressed.connect(close_menu) + + +func open_menu() -> void: + visible = true + + +func close_menu() -> void: + visible = false + closed.emit() + + +func handle_unhandled_input(_event: InputEvent) -> bool: + return visible diff --git a/ui/menus/ModalMenu.gd.uid b/ui/menus/ModalMenu.gd.uid new file mode 100644 index 0000000..f034e7d --- /dev/null +++ b/ui/menus/ModalMenu.gd.uid @@ -0,0 +1 @@ +uid://dkpcfwj4unbbf diff --git a/ui/menus/PlanningMenu.gd b/ui/menus/PlanningMenu.gd new file mode 100644 index 0000000..bf8083f --- /dev/null +++ b/ui/menus/PlanningMenu.gd @@ -0,0 +1,25 @@ +extends "res://ui/menus/ModalMenu.gd" + +@onready var panel: Control = $Panel +@onready var status: Label = $StatusLabel + + +func _ready() -> void: + super._ready() + panel.changed.connect(refresh) + + +func open_menu() -> void: + super.open_menu() + refresh() + + +func refresh() -> void: + panel.refresh() + var short: PackedStringArray = PackedStringArray() + for vessel_type in GameState.planned_vessel_types(): + var missing: int = GameState.vessel_shortfall(String(vessel_type)) + if missing > 0: + short.append("%d %s" % [missing, GameState.vessel_name(String(vessel_type))]) + status.text = "Every standing order is met or already being built" if short.is_empty() \ + else "Short: %s" % ", ".join(short) diff --git a/ui/menus/PlanningMenu.gd.uid b/ui/menus/PlanningMenu.gd.uid new file mode 100644 index 0000000..8eac393 --- /dev/null +++ b/ui/menus/PlanningMenu.gd.uid @@ -0,0 +1 @@ +uid://b80lmxhmmqw8a diff --git a/ui/menus/PlanningMenu.tscn b/ui/menus/PlanningMenu.tscn new file mode 100644 index 0000000..b34e58e --- /dev/null +++ b/ui/menus/PlanningMenu.tscn @@ -0,0 +1,63 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/PlanningPanel.gd" id="35"] +[ext_resource type="Script" path="res://ui/menus/PlanningMenu.gd" id="menu_script"] + +[node name="PlanningMenu" type="Control" groups=["modal_menus"]] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24.0 +offset_top = 20.0 +offset_right = -200.0 +offset_bottom = 82.0 +text = "Planning Center" +horizontal_alignment = 1 + +[node name="Panel" type="Control" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 40.0 +offset_top = 120.0 +offset_right = -40.0 +offset_bottom = -180.0 +anchor_bottom = 1.0 +script = ExtResource("35") + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 32.0 +offset_top = -160.0 +offset_right = -32.0 +offset_bottom = -24.0 +theme_override_font_sizes/font_size = 28 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -140.0 +offset_top = 20.0 +offset_right = -24.0 +offset_bottom = 136.0 +script = ExtResource("10") +icon = 1 +glow_color = Color(0.62, 0.74, 0.92, 1) diff --git a/ui/menus/SkillMenu.gd b/ui/menus/SkillMenu.gd new file mode 100644 index 0000000..bbe8166 --- /dev/null +++ b/ui/menus/SkillMenu.gd @@ -0,0 +1,70 @@ +extends "res://ui/menus/ModalMenu.gd" + +signal research_requested(id: String) + +@onready var tree: Control = $Tree +@onready var detail: Label = $DetailLabel +@onready var cost: Control = $CostNumber +@onready var status: Label = $StatusLabel +@onready var resource_bar: Control = $Bar + + +func _ready() -> void: + super._ready() + tree.node_picked.connect(refresh) + tree.node_confirmed.connect(func(id: String): research_requested.emit(id)) + + +func open_for(building_id: int) -> void: + resource_bar.building_id = building_id + open_menu() + tree.reset_scroll() + refresh() + + +func selected_id() -> String: + return String(tree.selected_id) + + +func research_completed(id: String) -> void: + tree.queue_redraw() + refresh(id) + + +func refresh(id: String = "") -> void: + if id == "": + id = String(tree.selected_id) + if id == "": + detail.text = "Select a node to see what it unlocks." + cost.show_price({}) + status.text = "Fleet capacity %d · Research points %d" \ + % [GameState.max_fleet_size(), GameState.research_points] + return + + var node: Dictionary = GameState.skill_node(id) + if node.is_empty(): + return + detail.text = "%s\n%s" % [node.name, node.desc] + cost.show_price(GameState.skill_costs(id), GameState.skill_research_point_cost(id)) + if GameState.is_unlocked(id): + status.text = "Researched" + elif not GameState.prerequisite_met(id): + status.text = "Needs %s first" % GameState.skill_node(node.requires).name + elif not cost.is_affordable(): + status.text = "" + else: + status.text = "Tap again to research" + + +func handle_unhandled_input(event: InputEvent) -> bool: + if not visible: + return false + if event is InputEventScreenTouch: + tree.handle_touch(event) + elif event is InputEventScreenDrag: + tree.handle_drag(event) + elif event is InputEventMouseButton: + tree.handle_mouse_button(event) + elif event is InputEventMouseMotion: + tree.handle_mouse_motion(event) + return true diff --git a/ui/menus/SkillMenu.gd.uid b/ui/menus/SkillMenu.gd.uid new file mode 100644 index 0000000..c47d88b --- /dev/null +++ b/ui/menus/SkillMenu.gd.uid @@ -0,0 +1 @@ +uid://bhgf1risnbb03 diff --git a/ui/menus/SkillMenu.tscn b/ui/menus/SkillMenu.tscn new file mode 100644 index 0000000..b6eba14 --- /dev/null +++ b/ui/menus/SkillMenu.tscn @@ -0,0 +1,92 @@ +[gd_scene load_steps=6 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/SkillTreeView.gd" id="21"] +[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"] +[ext_resource type="Script" path="res://ui/PriceTag.gd" id="price_tag"] +[ext_resource type="Script" path="res://ui/menus/SkillMenu.gd" id="menu_script"] + +[node name="SkillMenu" type="Control" groups=["modal_menus"]] +visible = false +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="Bar" type="Control" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24.0 +offset_top = 20 +offset_right = -160.0 +offset_bottom = 202 +script = ExtResource("24") + +[node name="Tree" type="Control" parent="."] +mouse_filter = 2 +clip_contents = true +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 24.0 +offset_top = 214 +offset_right = -24.0 +offset_bottom = -280.0 +script = ExtResource("21") + +[node name="DetailLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 32.0 +offset_top = -272.0 +offset_right = -300.0 +offset_bottom = -152.0 +theme_override_font_sizes/font_size = 34 +autowrap_mode = 2 +horizontal_alignment = 0 +text = "" + +[node name="CostNumber" type="Control" parent="."] +alignment = 2 +mouse_filter = 2 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -288.0 +offset_top = -272.0 +offset_right = -32.0 +offset_bottom = -152.0 +script = ExtResource("price_tag") + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 32.0 +offset_top = -76.0 +offset_right = -32.0 +offset_bottom = -24.0 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -140.0 +offset_top = 20.0 +offset_right = -24.0 +offset_bottom = 136.0 +script = ExtResource("10") +icon = 1 +glow_color = Color(0.55, 0.6, 1, 1) diff --git a/ui/menus/TransferMenu.gd b/ui/menus/TransferMenu.gd new file mode 100644 index 0000000..939c3b3 --- /dev/null +++ b/ui/menus/TransferMenu.gd @@ -0,0 +1,56 @@ +extends "res://ui/menus/ModalMenu.gd" + +signal transfer_requested(resource_type: String, to_building: bool, amount: int) + +@onready var status: Label = $StatusLabel +@onready var resource_list: Control = $ResourceList + +var building_id: int = 0 +var extractor_index: int = -1 + + +func _ready() -> void: + super._ready() + resource_list.transfer_requested.connect( + func(resource_type: String, to_building: bool, amount: int): + transfer_requested.emit(resource_type, to_building, amount)) + + +func open_destination(next_building_id: int, next_extractor_index: int) -> void: + building_id = next_building_id + extractor_index = next_extractor_index + status.text = "" + open_menu() + refresh() + + +func close_menu() -> void: + super.close_menu() + building_id = 0 + extractor_index = -1 + + +func refresh() -> void: + if extractor_index >= 0: + resource_list.set_extractor_destination(extractor_index, "Planet extractor") + return + var building: Dictionary = GameState.find_building(building_id) + var heading: String = "This building" + if GameState.building_has_capability(building, "bank"): + heading = "Mothership bank" + resource_list.set_destination(building_id, heading) + + +func place_name() -> String: + if extractor_index >= 0: + return "the extractor" + var building: Dictionary = GameState.find_building(building_id) + if building.is_empty(): + return "this building" + if GameState.building_has_capability(building, "bank"): + return "the bank" + return "this building" + + +func show_status(message: String) -> void: + status.text = message diff --git a/ui/menus/TransferMenu.gd.uid b/ui/menus/TransferMenu.gd.uid new file mode 100644 index 0000000..867f22b --- /dev/null +++ b/ui/menus/TransferMenu.gd.uid @@ -0,0 +1 @@ +uid://bw1aiy8jt817c diff --git a/ui/menus/TransferMenu.tscn b/ui/menus/TransferMenu.tscn new file mode 100644 index 0000000..7f86ddf --- /dev/null +++ b/ui/menus/TransferMenu.tscn @@ -0,0 +1,73 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] +[ext_resource type="Script" path="res://ui/ResourceTransferList.gd" id="27"] +[ext_resource type="Script" path="res://ui/menus/TransferMenu.gd" id="menu_script"] + +[node name="TransferMenu" type="Control" groups=["modal_menus"]] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("menu_script") + +[node name="Backdrop" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.03, 0.03, 0.08, 0.94) + +[node name="TitleLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 36 +offset_right = -200 +offset_bottom = 166 +text = "Transfer cargo" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 2 +theme_override_font_sizes/font_size = 44 + +[node name="StatusLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 184 +offset_right = -24 +offset_bottom = 284 +theme_override_font_sizes/font_size = 32 +autowrap_mode = 2 +text = "" +horizontal_alignment = 1 + +[node name="ResourceList" type="Control" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 16 +offset_top = 294 +offset_right = -16 +offset_bottom = 1080 +script = ExtResource("27") + +[node name="CloseButton" type="Control" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -172 +offset_right = -24 +offset_top = 24 +offset_bottom = 172 +script = ExtResource("10") +icon = 1 +glow_color = Color(1, 0.78, 0.25, 1) + +[node name="HintLabel" type="Label" parent="."] +mouse_filter = 2 +anchor_right = 1.0 +offset_left = 24 +offset_top = 1092 +offset_right = -24 +offset_bottom = 1150 +text = "Arrows move the selected amount from that side" +horizontal_alignment = 1 +theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1) +theme_override_font_sizes/font_size = 23 diff --git a/world/Main.tscn b/world/Main.tscn index 364bc0c..e859eda 100644 --- a/world/Main.tscn +++ b/world/Main.tscn @@ -1,4 +1,14 @@ -[gd_scene load_steps=34 format=3] +[gd_scene load_steps=43 format=3] +[ext_resource type="PackedScene" path="res://ui/menus/BuildMenu.tscn" id="menu_build"] +[ext_resource type="PackedScene" path="res://ui/menus/HangarMenu.tscn" id="menu_hangar"] +[ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"] +[ext_resource type="PackedScene" path="res://ui/menus/CraftMenu.tscn" id="menu_craft"] +[ext_resource type="PackedScene" path="res://ui/menus/TransferMenu.tscn" id="menu_transfer"] +[ext_resource type="PackedScene" path="res://ui/menus/FormationMenu.tscn" id="menu_formation"] +[ext_resource type="PackedScene" path="res://ui/menus/PlanningMenu.tscn" id="menu_planning"] +[ext_resource type="PackedScene" path="res://ui/menus/GarrisonMenu.tscn" id="menu_garrison"] +[ext_resource type="PackedScene" path="res://ui/menus/SkillMenu.tscn" id="menu_skill"] + [ext_resource type="Script" path="res://world/Starfield.gd" id="1"] [ext_resource type="Script" path="res://vessels/Fleet.gd" id="2"] @@ -16,24 +26,15 @@ [ext_resource type="Script" path="res://buildings/BuildingField.gd" id="14"] [ext_resource type="Script" path="res://controllers/BuildController.gd" id="15"] [ext_resource type="Script" path="res://ui/ResourceReadout.gd" id="16"] -[ext_resource type="Script" path="res://ui/FormationEditor.gd" id="17"] -[ext_resource type="Script" path="res://ui/NeonNumber.gd" id="19"] -[ext_resource type="Script" path="res://ui/NeonPanel.gd" id="20"] -[ext_resource type="Script" path="res://ui/SkillTreeView.gd" id="21"] [ext_resource type="Script" path="res://ui/WaveAlert.gd" id="32"] -[ext_resource type="Script" path="res://ui/GarrisonPanel.gd" id="33"] [ext_resource type="Script" path="res://vessels/GarrisonField.gd" id="34"] -[ext_resource type="Script" path="res://ui/PlanningPanel.gd" id="35"] [ext_resource type="Script" path="res://ui/NarrationBanner.gd" id="36"] [ext_resource type="Script" path="res://world/IntroDirector.gd" id="37"] [ext_resource type="Script" path="res://ui/DevWaveButton.gd" id="38"] -[ext_resource type="Script" path="res://ui/HangarMenuPanel.gd" id="39"] [ext_resource type="Script" path="res://logistics/RepairDroneField.gd" id="40"] [ext_resource type="Script" path="res://ui/BreachOverlay.gd" id="41"] [ext_resource type="Script" path="res://logistics/TransporterField.gd" id="23"] -[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"] [ext_resource type="Script" path="res://logistics/VesselDeliveryField.gd" id="26"] -[ext_resource type="Script" path="res://ui/ResourceTransferList.gd" id="27"] [ext_resource type="Script" path="res://controllers/MouseCombatController.gd" id="mouse_ctrl"] [ext_resource type="Script" path="res://controllers/FireStickController.gd" id="fire_ctrl"] [ext_resource type="Script" path="res://controllers/GasBombController.gd" id="29"] @@ -41,7 +42,6 @@ [ext_resource type="Script" path="res://ui/SpeedFader.gd" id="31"] [ext_resource type="Script" path="res://world/QuestDirector.gd" id="quest_dir"] [ext_resource type="Script" path="res://ui/QuestPanel.gd" id="quest_panel"] -[ext_resource type="Script" path="res://ui/PriceTag.gd" id="price_tag"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("5") @@ -453,1995 +453,15 @@ offset_bottom = 172 script = ExtResource("10") icon = 1 -[node name="BuildMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/BuildMenu"] -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 20 -offset_right = -24 -offset_bottom = 140 -text = "Build" -horizontal_alignment = 1 - -[node name="Bar" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24.0 -offset_top = 148 -offset_right = -24.0 -offset_bottom = 330 -script = ExtResource("24") - -[node name="StatusLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 338 -offset_right = -24 -offset_bottom = 418 -theme_override_font_sizes/font_size = 40 -autowrap_mode = 2 -text = "" -horizontal_alignment = 1 - -[node name="MothershipButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -204 -offset_right = -76 -offset_top = -300 -offset_bottom = -172 -script = ExtResource("10") -icon = 8 -glow_color = Color(0.45, 0.9, 0.55, 1) - -[node name="MothershipLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -290 -offset_right = 10 -offset_top = -166 -offset_bottom = -100 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Mothership" -horizontal_alignment = 1 - -[node name="MothershipCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -200 -offset_right = -80 -offset_top = -96 -offset_bottom = -44 -script = ExtResource("price_tag") - -[node name="CrafterButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 76 -offset_right = 204 -offset_top = -300 -offset_bottom = -172 -script = ExtResource("10") -icon = 9 -glow_color = Color(1, 0.72, 0.25, 1) - -[node name="CrafterLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -10 -offset_right = 290 -offset_top = -166 -offset_bottom = -100 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Ship Crafter" -horizontal_alignment = 1 - -[node name="CrafterCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 80 -offset_right = 200 -offset_top = -96 -offset_bottom = -44 -script = ExtResource("price_tag") - -[node name="CommandButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -204 -offset_right = -76 -offset_top = -10 -offset_bottom = 118 -script = ExtResource("10") -icon = 10 -glow_color = Color(0.55, 0.6, 1, 1) - -[node name="CommandLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -290 -offset_right = 10 -offset_top = 124 -offset_bottom = 190 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Command Center" -horizontal_alignment = 1 - -[node name="CommandCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -200 -offset_right = -80 -offset_top = 194 -offset_bottom = 246 -script = ExtResource("price_tag") - -[node name="ExtractorButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 76 -offset_right = 204 -offset_top = -10 -offset_bottom = 118 -script = ExtResource("10") -icon = 11 -glow_color = Color(0, 0.62, 0.7, 1) - -[node name="ExtractorLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -10 -offset_right = 290 -offset_top = 124 -offset_bottom = 190 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Extractor" -horizontal_alignment = 1 - -[node name="ExtractorCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 80 -offset_right = 200 -offset_top = 194 -offset_bottom = 246 -script = ExtResource("price_tag") - -[node name="TransportHubButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = 280 -offset_bottom = 408 -script = ExtResource("10") -icon = 12 -glow_color = Color(0.95, 0.55, 0.85, 1) - -[node name="TransportHubLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -150 -offset_right = 150 -offset_top = 414 -offset_bottom = 480 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Transport Hub" -horizontal_alignment = 1 - -[node name="TransportHubCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -60 -offset_right = 60 -offset_top = 484 -offset_bottom = 536 -script = ExtResource("price_tag") - -[node name="ShipLabButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 76 -offset_right = 204 -offset_top = 280 -offset_bottom = 408 -script = ExtResource("10") -icon = 13 -glow_color = Color(0.55, 0.95, 0.9, 1) - -[node name="ShipLabLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -10 -offset_right = 290 -offset_top = 414 -offset_bottom = 480 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Ship Lab" -horizontal_alignment = 1 - -[node name="ShipLabCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 80 -offset_right = 200 -offset_top = 484 -offset_bottom = 536 -script = ExtResource("price_tag") - -[node name="GasBurnerButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = 570 -offset_bottom = 698 -script = ExtResource("10") -icon = 33 -glow_color = Color(0.95, 0.35, 0.15, 1) - -[node name="GasBurnerLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -150 -offset_right = 150 -offset_top = 704 -offset_bottom = 770 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Gas Burner" -horizontal_alignment = 1 - -[node name="GasBurnerCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -60 -offset_right = 60 -offset_top = 774 -offset_bottom = 826 -script = ExtResource("price_tag") - -[node name="OutpostButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = 830 -offset_bottom = 958 -script = ExtResource("10") -icon = 40 -glow_color = Color(0.72, 0.86, 0.45, 1) - -[node name="OutpostLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -150 -offset_right = 150 -offset_top = 964 -offset_bottom = 1030 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Outpost" -horizontal_alignment = 1 - -[node name="OutpostCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -60 -offset_right = 60 -offset_top = 1034 -offset_bottom = 1086 -script = ExtResource("price_tag") - -[node name="PlanningButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = 1090 -offset_bottom = 1218 -script = ExtResource("10") -icon = 41 -glow_color = Color(0.62, 0.74, 0.92, 1) - -[node name="PlanningLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -150 -offset_right = 150 -offset_top = 1224 -offset_bottom = 1290 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Planning Center" -horizontal_alignment = 1 - -[node name="PlanningCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -60 -offset_right = 60 -offset_top = 1294 -offset_bottom = 1346 -script = ExtResource("price_tag") - -[node name="WorkshopButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = 1350 -offset_bottom = 1478 -script = ExtResource("10") -icon = 42 -glow_color = Color(0.5, 0.95, 0.8, 1) - -[node name="WorkshopLabel" type="Label" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -150 -offset_right = 150 -offset_top = 1484 -offset_bottom = 1550 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "Spacial Workshop" -horizontal_alignment = 1 - -[node name="WorkshopCost" type="Control" parent="UI/BuildMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -60 -offset_right = 60 -offset_top = 1554 -offset_bottom = 1606 -script = ExtResource("price_tag") - -[node name="CloseButton" type="Control" parent="UI/BuildMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -172 -offset_right = -24 -offset_top = 24 -offset_bottom = 172 -script = ExtResource("10") -icon = 1 - -[node name="InfoPopup" type="Control" parent="UI/BuildMenu" groups=["modal_menus"]] -visible = false -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Dim" type="ColorRect" parent="UI/BuildMenu/InfoPopup"] -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.01, 0.01, 0.03, 0.55) - -[node name="Panel" type="Control" parent="UI/BuildMenu/InfoPopup"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -352 -offset_right = 352 -offset_top = -600 -offset_bottom = 600 -script = ExtResource("20") -glow_color = Color(0, 0.62, 0.7, 1) - -[node name="TitleLabel" type="Label" parent="UI/BuildMenu/InfoPopup/Panel"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 44 -offset_right = -44 -offset_top = 30 -offset_bottom = 150 -text = "" -horizontal_alignment = 1 - -[node name="MessageLabel" type="Label" parent="UI/BuildMenu/InfoPopup/Panel"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 44 -offset_right = -44 -offset_top = 160 -offset_bottom = -440 -theme_override_font_sizes/font_size = 48 -text = "" -horizontal_alignment = 1 -vertical_alignment = 1 -autowrap_mode = 2 - -[node name="CostNumber" type="Control" parent="UI/BuildMenu/InfoPopup/Panel"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -70 -offset_right = 90 -offset_top = -424 -offset_bottom = -334 -script = ExtResource("price_tag") - -[node name="BuildButton" type="Control" parent="UI/BuildMenu/InfoPopup/Panel"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -200 -offset_right = -40 -offset_top = -300 -offset_bottom = -140 -script = ExtResource("10") -icon = 5 -glow_color = Color(0.45, 0.9, 0.55, 1) - -[node name="CloseButton" type="Control" parent="UI/BuildMenu/InfoPopup/Panel"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 40 -offset_right = 200 -offset_top = -300 -offset_bottom = -140 -script = ExtResource("10") -icon = 1 -glow_color = Color(0, 0.62, 0.7, 1) - -[node name="HangarMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -script = ExtResource("39") - -[node name="Backdrop" type="ColorRect" parent="UI/HangarMenu"] -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 60 -offset_right = -210 -offset_bottom = 170 -text = "Hangar" -horizontal_alignment = 1 - -[node name="ListLabel" type="Label" parent="UI/HangarMenu"] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 32 -offset_top = 200 -offset_right = -32 -offset_bottom = -420 -theme_override_font_sizes/font_size = 30 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="StatusLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 32 -offset_top = -410 -offset_right = -32 -offset_bottom = -300 -theme_override_font_sizes/font_size = 30 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="NextButton" type="Control" parent="UI/HangarMenu"] -visible = false -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -230 -offset_right = -110 -offset_top = -280 -offset_bottom = -160 -script = ExtResource("10") -icon = 6 -glow_color = Color(0.55, 0.95, 0.9, 1) - -[node name="CrewButton" type="Control" parent="UI/HangarMenu"] -visible = false -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = 110 -offset_right = 230 -offset_top = -280 -offset_bottom = -160 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.45, 0.9, 0.55, 1) - -[node name="HintLabel" type="Label" parent="UI/HangarMenu"] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 24 -offset_top = -150 -offset_right = -24 -offset_bottom = -60 -theme_override_font_sizes/font_size = 28 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Next Crew" - -[node name="AvailableTitle" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_top = 190 -offset_bottom = 240 -theme_override_font_sizes/font_size = 28 -text = "AVAILABLE — TAP TO CREW" -horizontal_alignment = 1 - -[node name="AvailableInterceptor" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -290 -offset_right = -215 -offset_top = 255 -offset_bottom = 375 -script = ExtResource("10") -icon = 2 - -[node name="AvailableInterceptorLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -330 -offset_right = -175 -offset_top = 380 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="AvailableShield" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -189 -offset_right = -114 -offset_top = 255 -offset_bottom = 375 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.45, 0.8, 1, 1) - -[node name="AvailableShieldLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -229 -offset_right = -74 -offset_top = 380 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -88 -offset_right = -13 -offset_top = 255 -offset_bottom = 375 -script = ExtResource("10") -icon = 31 -glow_color = Color(0.96, 0.32, 0.78, 1) - -[node name="AvailableGasBomberLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -128 -offset_right = 27 -offset_top = 380 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="AvailableCarrier" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 13 -offset_right = 88 -offset_top = 255 -offset_bottom = 375 -script = ExtResource("10") -icon = 34 -glow_color = Color(1, 0.78, 0.3, 1) - -[node name="AvailableCarrierLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -27 -offset_right = 128 -offset_top = 380 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="AvailableLaser" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 114 -offset_right = 189 -offset_top = 255 -offset_bottom = 375 -script = ExtResource("10") -icon = 35 -glow_color = Color(0.3, 0.9, 1, 1) - -[node name="AvailableLaserLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 74 -offset_right = 229 -offset_top = 380 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="AvailableDistorder" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 215 -offset_right = 290 -offset_top = 255 -offset_bottom = 375 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - -[node name="AvailableDistorderLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 175 -offset_right = 330 -offset_top = 380 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="FleetTitle" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_top = 500 -offset_bottom = 550 -theme_override_font_sizes/font_size = 28 -text = "FLEET — TAP TO STORE" -horizontal_alignment = 1 - -[node name="FleetInterceptor" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -290 -offset_right = -215 -offset_top = 565 -offset_bottom = 685 -script = ExtResource("10") -icon = 2 - -[node name="FleetInterceptorLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -330 -offset_right = -175 -offset_top = 690 -offset_bottom = 770 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="FleetShield" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -189 -offset_right = -114 -offset_top = 565 -offset_bottom = 685 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.45, 0.8, 1, 1) - -[node name="FleetShieldLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -229 -offset_right = -74 -offset_top = 690 -offset_bottom = 770 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -88 -offset_right = -13 -offset_top = 565 -offset_bottom = 685 -script = ExtResource("10") -icon = 31 -glow_color = Color(0.96, 0.32, 0.78, 1) - -[node name="FleetGasBomberLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -128 -offset_right = 27 -offset_top = 690 -offset_bottom = 770 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="FleetCarrier" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 13 -offset_right = 88 -offset_top = 565 -offset_bottom = 685 -script = ExtResource("10") -icon = 34 -glow_color = Color(1, 0.78, 0.3, 1) - -[node name="FleetCarrierLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -27 -offset_right = 128 -offset_top = 690 -offset_bottom = 770 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="FleetLaser" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 114 -offset_right = 189 -offset_top = 565 -offset_bottom = 685 -script = ExtResource("10") -icon = 35 -glow_color = Color(0.3, 0.9, 1, 1) - -[node name="FleetLaserLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 74 -offset_right = 229 -offset_top = 690 -offset_bottom = 770 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="FleetDistorder" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 215 -offset_right = 290 -offset_top = 565 -offset_bottom = 685 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - -[node name="FleetDistorderLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 175 -offset_right = 330 -offset_top = 690 -offset_bottom = 770 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="CloseButton" type="Control" parent="UI/HangarMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -172 -offset_top = 24 -offset_right = -24 -offset_bottom = 172 -script = ExtResource("10") -icon = 1 -glow_color = Color(0.45, 0.9, 0.55, 1) - -[node name="LabMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/LabMenu"] -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 20 -offset_right = -200 -offset_bottom = 82 -text = "Ship Lab" -horizontal_alignment = 1 - -[node name="Bar" type="Control" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 148 -offset_right = -24 -offset_bottom = 330 -script = ExtResource("24") - -[node name="VesselA" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -300 -offset_right = -240 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="VesselB" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -210 -offset_right = -150 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.45, 0.8, 1, 1) - -[node name="VesselC" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -120 -offset_right = -60 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 31 -glow_color = Color(0.96, 0.32, 0.78, 1) - -[node name="VesselD" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -30 -offset_right = 30 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 12 -glow_color = Color(0.95, 0.55, 0.85, 1) - -[node name="VesselE" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 60 -offset_right = 120 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 34 -glow_color = Color(1, 0.78, 0.3, 1) - -[node name="VesselF" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 150 -offset_right = 210 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 35 -glow_color = Color(0.3, 0.9, 1, 1) - -[node name="VesselG" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 240 -offset_right = 300 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - -[node name="VesselH" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 320 -offset_right = 380 -offset_top = 346 -offset_bottom = 466 -script = ExtResource("10") -icon = 43 -glow_color = Color(0.5, 0.95, 0.8, 1) - -[node name="VesselLabel" type="Label" parent="UI/LabMenu"] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 470 -offset_right = -24 -offset_bottom = 528 -theme_override_font_sizes/font_size = 34 -horizontal_alignment = 1 -text = "" - -[node name="VesselALabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -310 -offset_right = -230 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Interceptor" - -[node name="VesselBLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -220 -offset_right = -140 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Shield" - -[node name="VesselCLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -130 -offset_right = -50 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Gas Bomber" - -[node name="VesselDLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -40 -offset_right = 40 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Transporter" - -[node name="VesselELabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 50 -offset_right = 130 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Carrier" - -[node name="VesselFLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 140 -offset_right = 220 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Laser Ship" - -[node name="VesselGLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 230 -offset_right = 310 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Distorder" - -[node name="VesselHLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 310 -offset_right = 390 -offset_top = 470 -offset_bottom = 542 -theme_override_font_sizes/font_size = 15 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Repair Drone" - -[node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 48 -offset_top = 550 -offset_right = -48 -offset_bottom = 554 -color = Color(0.25, 0.85, 1, 0.6) - -[node name="UpgradeList" type="Label" parent="UI/LabMenu"] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 32 -offset_top = 562 -offset_right = -32 -offset_bottom = -390 -theme_override_font_sizes/font_size = 30 -autowrap_mode = 2 -text = "" - -[node name="DetailLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 32 -offset_top = -300 -offset_right = -32 -offset_bottom = -180 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="StatusLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 32 -offset_top = -174 -offset_right = -32 -offset_bottom = -120 -theme_override_font_sizes/font_size = 30 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="NextButton" type="Control" parent="UI/LabMenu"] -visible = false -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -230 -offset_right = -110 -offset_top = -112 -offset_bottom = -22 -script = ExtResource("10") -icon = 6 -glow_color = Color(0.55, 0.95, 0.9, 1) - -[node name="HintLabel" type="Label" parent="UI/LabMenu"] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 24 -offset_top = -102 -offset_right = -24 -offset_bottom = -22 -theme_override_font_sizes/font_size = 28 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Next upgrade Buy" - -[node name="Upgrade0" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -240 -offset_right = -120 -offset_top = 562 -offset_bottom = 672 -script = ExtResource("10") -icon = 13 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="Upgrade0Label" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -310 -offset_right = -50 -offset_top = 676 -offset_bottom = 746 -theme_override_font_sizes/font_size = 22 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="Upgrade1" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 120 -offset_right = 240 -offset_top = 562 -offset_bottom = 672 -script = ExtResource("10") -icon = 13 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="Upgrade1Label" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 50 -offset_right = 310 -offset_top = 676 -offset_bottom = 746 -theme_override_font_sizes/font_size = 22 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="Upgrade2" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -240 -offset_right = -120 -offset_top = 790 -offset_bottom = 900 -script = ExtResource("10") -icon = 13 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="Upgrade2Label" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -310 -offset_right = -50 -offset_top = 904 -offset_bottom = 974 -theme_override_font_sizes/font_size = 22 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="Upgrade3" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 120 -offset_right = 240 -offset_top = 790 -offset_bottom = 900 -script = ExtResource("10") -icon = 13 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="Upgrade3Label" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 50 -offset_right = 310 -offset_top = 904 -offset_bottom = 974 -theme_override_font_sizes/font_size = 22 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="CloseButton" type="Control" parent="UI/LabMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -140 -offset_top = 20 -offset_right = -24 -offset_bottom = 136 -script = ExtResource("10") -icon = 1 -glow_color = Color(0.55, 0.95, 0.9, 1) - -[node name="CraftMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/CraftMenu"] -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 20 -offset_right = -200 -offset_bottom = 82 -text = "Build a ship" -horizontal_alignment = 1 - -[node name="Bar" type="Control" parent="UI/CraftMenu"] -anchor_right = 1.0 -mouse_filter = 2 -offset_left = 24 -offset_top = 148 -offset_right = -24 -offset_bottom = 330 -script = ExtResource("24") - -[node name="StatusLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 344 -offset_right = -200 -offset_bottom = 490 -theme_override_font_sizes/font_size = 36 -autowrap_mode = 2 -text = "" -horizontal_alignment = 1 - -[node name="InterceptorButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -310 -offset_right = -245 -offset_top = -100 -offset_bottom = 48 -script = ExtResource("10") -icon = 2 - -[node name="InterceptorLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -330 -offset_right = -225 -offset_top = 56 -offset_bottom = 136 -theme_override_font_sizes/font_size = 24 -text = "Interceptor" -horizontal_alignment = 1 - -[node name="ShieldButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -221 -offset_right = -156 -offset_top = -100 -offset_bottom = 48 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.45, 0.8, 1, 1) - -[node name="ShieldLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -241 -offset_right = -136 -offset_top = 56 -offset_bottom = 136 -theme_override_font_sizes/font_size = 24 -autowrap_mode = 2 -text = "Shield Vessel" -horizontal_alignment = 1 - -[node name="GasBomberButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -132 -offset_right = -67 -offset_top = -100 -offset_bottom = 28 -script = ExtResource("10") -icon = 31 -glow_color = Color(0.96, 0.32, 0.78, 1) - -[node name="GasBomberLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -152 -offset_right = -47 -offset_top = 56 -offset_bottom = 150 -theme_override_font_sizes/font_size = 24 -autowrap_mode = 2 -text = "Gas Bomber" -horizontal_alignment = 1 - -[node name="CarrierButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -43 -offset_right = 22 -offset_top = -100 -offset_bottom = 48 -script = ExtResource("10") -icon = 34 -glow_color = Color(1, 0.78, 0.3, 1) - -[node name="CarrierLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -63 -offset_right = 42 -offset_top = 56 -offset_bottom = 136 -theme_override_font_sizes/font_size = 24 -autowrap_mode = 2 -text = "Carrier" -horizontal_alignment = 1 - -[node name="LaserButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 46 -offset_right = 111 -offset_top = -100 -offset_bottom = 48 -script = ExtResource("10") -icon = 35 -glow_color = Color(0.3, 0.9, 1, 1) - -[node name="LaserLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 26 -offset_right = 131 -offset_top = 56 -offset_bottom = 136 -theme_override_font_sizes/font_size = 24 -autowrap_mode = 2 -text = "Laser Ship" -horizontal_alignment = 1 - -[node name="DistorderButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 135 -offset_right = 200 -offset_top = -100 -offset_bottom = 48 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - -[node name="DistorderLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 115 -offset_right = 220 -offset_top = 56 -offset_bottom = 136 -theme_override_font_sizes/font_size = 24 -autowrap_mode = 2 -text = "Distorder" -horizontal_alignment = 1 - -[node name="QueueLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 1.0 -anchor_top = 0.5 -anchor_right = 1.0 -anchor_bottom = 0.5 -offset_left = -200 -offset_right = -10 -offset_top = -450 -offset_bottom = -390 -theme_override_font_sizes/font_size = 26 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "Queue" - -[node name="QueueDetail" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 24 -offset_right = -24 -offset_top = -150 -offset_bottom = -30 -theme_override_font_sizes/font_size = 26 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="QueueSlot0" type="Control" parent="UI/CraftMenu"] -visible = false -anchor_left = 1.0 -anchor_top = 0.5 -anchor_right = 1.0 -anchor_bottom = 0.5 -offset_left = -140 -offset_right = -30 -offset_top = -380 -offset_bottom = -270 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="QueueSlot1" type="Control" parent="UI/CraftMenu"] -visible = false -anchor_left = 1.0 -anchor_top = 0.5 -anchor_right = 1.0 -anchor_bottom = 0.5 -offset_left = -140 -offset_right = -30 -offset_top = -262 -offset_bottom = -152 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="QueueSlot2" type="Control" parent="UI/CraftMenu"] -visible = false -anchor_left = 1.0 -anchor_top = 0.5 -anchor_right = 1.0 -anchor_bottom = 0.5 -offset_left = -140 -offset_right = -30 -offset_top = -144 -offset_bottom = -34 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="QueueSlot3" type="Control" parent="UI/CraftMenu"] -visible = false -anchor_left = 1.0 -anchor_top = 0.5 -anchor_right = 1.0 -anchor_bottom = 0.5 -offset_left = -140 -offset_right = -30 -offset_top = -26 -offset_bottom = 84 -script = ExtResource("10") -icon = 2 -glow_color = Color(0.25, 0.85, 1, 1) - -[node name="CloseButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -140 -offset_right = -24 -offset_top = 20 -offset_bottom = 136 -script = ExtResource("10") -icon = 1 - -[node name="TransferMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/TransferMenu"] -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 36 -offset_right = -200 -offset_bottom = 166 -text = "Transfer cargo" -horizontal_alignment = 1 -vertical_alignment = 1 -autowrap_mode = 2 -theme_override_font_sizes/font_size = 44 - -[node name="StatusLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 184 -offset_right = -24 -offset_bottom = 284 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -text = "" -horizontal_alignment = 1 - -[node name="ResourceList" type="Control" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 16 -offset_top = 294 -offset_right = -16 -offset_bottom = 1080 -script = ExtResource("27") - -[node name="CloseButton" type="Control" parent="UI/TransferMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -172 -offset_right = -24 -offset_top = 24 -offset_bottom = 172 -script = ExtResource("10") -icon = 1 -glow_color = Color(1, 0.78, 0.25, 1) - -[node name="HintLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 1092 -offset_right = -24 -offset_bottom = 1150 -text = "Arrows move the selected amount from that side" -horizontal_alignment = 1 -theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1) -theme_override_font_sizes/font_size = 23 - -[node name="FormationMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/FormationMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/FormationMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 70 -offset_right = -24 -offset_bottom = 220 -theme_override_font_sizes/font_size = 44 -autowrap_mode = 2 -text = "Fleet formation - drag to arrange" -horizontal_alignment = 1 - -[node name="Editor" type="Control" parent="UI/FormationMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -300 -offset_right = 300 -offset_top = -300 -offset_bottom = 300 -script = ExtResource("17") - -[node name="ResetButton" type="Control" parent="UI/FormationMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -74 -offset_right = 74 -offset_top = -270 -offset_bottom = -122 -script = ExtResource("10") -icon = 3 - -[node name="ResetLabel" type="Label" parent="UI/FormationMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -180 -offset_right = 180 -offset_top = -114 -offset_bottom = -8 -theme_override_font_sizes/font_size = 60 -text = "Reset" -horizontal_alignment = 1 - -[node name="CloseButton" type="Control" parent="UI/FormationMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -172 -offset_right = -24 -offset_top = 24 -offset_bottom = 172 -script = ExtResource("10") -icon = 1 - -[node name="FadeOverlay" type="CanvasLayer" parent="."] -layer = 10 - -[node name="FadeRect" type="ColorRect" parent="FadeOverlay"] -mouse_filter = 2 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -color = Color(0, 0, 0, 0) - -[node name="PlanningMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/PlanningMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/PlanningMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24.0 -offset_top = 20.0 -offset_right = -200.0 -offset_bottom = 82.0 -text = "Planning Center" -horizontal_alignment = 1 - -[node name="Panel" type="Control" parent="UI/PlanningMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 40.0 -offset_top = 120.0 -offset_right = -40.0 -offset_bottom = -180.0 -anchor_bottom = 1.0 -script = ExtResource("35") - -[node name="StatusLabel" type="Label" parent="UI/PlanningMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 32.0 -offset_top = -160.0 -offset_right = -32.0 -offset_bottom = -24.0 -theme_override_font_sizes/font_size = 28 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="CloseButton" type="Control" parent="UI/PlanningMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -140.0 -offset_top = 20.0 -offset_right = -24.0 -offset_bottom = 136.0 -script = ExtResource("10") -icon = 1 -glow_color = Color(0.62, 0.74, 0.92, 1) - -[node name="GarrisonMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/GarrisonMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="TitleLabel" type="Label" parent="UI/GarrisonMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24.0 -offset_top = 20.0 -offset_right = -200.0 -offset_bottom = 82.0 -text = "Outpost Garrison" -horizontal_alignment = 1 - -[node name="Panel" type="Control" parent="UI/GarrisonMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 40.0 -offset_top = 120.0 -offset_right = -40.0 -offset_bottom = 620.0 -script = ExtResource("33") - -[node name="StatusLabel" type="Label" parent="UI/GarrisonMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 32.0 -offset_top = -160.0 -offset_right = -32.0 -offset_bottom = -24.0 -theme_override_font_sizes/font_size = 30 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="CloseButton" type="Control" parent="UI/GarrisonMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -140.0 -offset_top = 20.0 -offset_right = -24.0 -offset_bottom = 136.0 -script = ExtResource("10") -icon = 1 -glow_color = Color(0.72, 0.86, 0.45, 1) - -[node name="SkillMenu" type="Control" parent="UI" groups=["modal_menus"]] -visible = false -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Backdrop" type="ColorRect" parent="UI/SkillMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.03, 0.03, 0.08, 0.94) - -[node name="Bar" type="Control" parent="UI/SkillMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24.0 -offset_top = 20 -offset_right = -160.0 -offset_bottom = 202 -script = ExtResource("24") - -[node name="Tree" type="Control" parent="UI/SkillMenu"] -mouse_filter = 2 -clip_contents = true -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 24.0 -offset_top = 214 -offset_right = -24.0 -offset_bottom = -280.0 -script = ExtResource("21") - -[node name="DetailLabel" type="Label" parent="UI/SkillMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 32.0 -offset_top = -272.0 -offset_right = -300.0 -offset_bottom = -152.0 -theme_override_font_sizes/font_size = 34 -autowrap_mode = 2 -horizontal_alignment = 0 -text = "" - -[node name="CostNumber" type="Control" parent="UI/SkillMenu"] -alignment = 2 -mouse_filter = 2 -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = -288.0 -offset_top = -272.0 -offset_right = -32.0 -offset_bottom = -152.0 -script = ExtResource("price_tag") - -[node name="StatusLabel" type="Label" parent="UI/SkillMenu"] -mouse_filter = 2 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = 32.0 -offset_top = -76.0 -offset_right = -32.0 -offset_bottom = -24.0 -theme_override_font_sizes/font_size = 32 -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="CloseButton" type="Control" parent="UI/SkillMenu"] -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -140.0 -offset_top = 20.0 -offset_right = -24.0 -offset_bottom = 136.0 -script = ExtResource("10") -icon = 1 -glow_color = Color(0.55, 0.6, 1, 1) +[node name="BuildMenu" parent="UI" instance=ExtResource("menu_build")] +[node name="HangarMenu" parent="UI" instance=ExtResource("menu_hangar")] +[node name="LabMenu" parent="UI" instance=ExtResource("menu_lab")] +[node name="CraftMenu" parent="UI" instance=ExtResource("menu_craft")] +[node name="TransferMenu" parent="UI" instance=ExtResource("menu_transfer")] +[node name="FormationMenu" parent="UI" instance=ExtResource("menu_formation")] +[node name="PlanningMenu" parent="UI" instance=ExtResource("menu_planning")] +[node name="GarrisonMenu" parent="UI" instance=ExtResource("menu_garrison")] +[node name="SkillMenu" parent="UI" instance=ExtResource("menu_skill")] [node name="QuestPanel" type="Control" parent="UI" groups=["modal_menus"]] visible = false mouse_filter = 2