extends Node const UI := preload("res://scripts/planets/expedition_ui.gd") const CONTACT := preload("res://scripts/ui/orbital_contact_style.gd") const PROFILES := preload("res://scripts/planets/planet_profiles.gd") const FREIGHTER_SIZE := 285.0 const CONTACT_RANGE := preload("res://scripts/planets/orbital_contact_range.gd") var game: Node2D var player: CharacterBody2D var freighter: Node2D var screen: Control var _layer: CanvasLayer var _prompt: PanelContainer var _text: Label var _contact_label: Label var _prompt_target: Node2D var _freighter_half_size := Vector2.ZERO var _land_button: Button var _near_planet: Node2D var _message := "" var _message_time := 0.0 var _previous_pause := false var _previous_docked := false var _hud_was_visible := true var _prompt_refresh_time := 0.0 var _last_combat_blocked := false var _needs_prompt_refresh := true func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS name = "PlanetExpeditions" get_tree().auto_accept_quit = false freighter = Node2D.new() freighter.name = "Freighter" freighter.add_to_group("scanner_freighters") freighter.z_index = -3 # Keep the player's hull, engines and shield above the frigate. freighter.position = Vector2(480,-270) game.add_child(freighter) var sprite := Sprite2D.new() sprite.texture = preload("res://assets/planets/freighter.png") sprite.scale = Vector2.ONE * FREIGHTER_SIZE / maxf(sprite.texture.get_width(),sprite.texture.get_height()) _freighter_half_size = Vector2(sprite.texture.get_image().get_used_rect().size)*sprite.scale*0.5 freighter.add_child(sprite) _layer = CanvasLayer.new() _layer.layer = 60 add_child(_layer) _prompt = UI.panel(_layer,Vector2(300,0)) _prompt.add_theme_stylebox_override("panel",CONTACT.panel_style(UI.CYAN)) var column := VBoxContainer.new() column.add_theme_constant_override("separation",7) _prompt.add_child(column) _contact_label = UI.label(column,"● ŁĄCZE ORBITALNE",10,UI.CYAN) _text = UI.label(column,"",11) _text.add_theme_constant_override("line_spacing",3) _text.custom_minimum_size.x = 276 _text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART var row := HBoxContainer.new() row.add_theme_constant_override("separation",7) column.add_child(row) _land_button = UI.button(row,"LĄDUJ [L]",land_nearby) for button in [_land_button]: CONTACT.compact_button(button,UI.CYAN) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL _prompt.hide() get_viewport().size_changed.connect(_layout_prompt) func combat_blocked() -> bool: if float(player.get("landing_combat_lock")) > 0: return true for enemy in get_tree().get_nodes_in_group("enemies"): if enemy is Node2D and not enemy.is_queued_for_deletion() and float(enemy.get("hull")) > 0.0 and player.global_position.distance_to(enemy.global_position) < 650: return true return false func _process(delta: float) -> void: _message_time = maxf(0, _message_time - delta) if is_instance_valid(screen) or get_tree().paused or bool(player.get("_is_destroyed")): _prompt.hide() _needs_prompt_refresh = true return var blocked := combat_blocked() if blocked != _last_combat_blocked or _needs_prompt_refresh: _prompt_refresh_time = 0.1 _last_combat_blocked = blocked _needs_prompt_refresh = false if _prompt.visible: _layout_prompt() _prompt_refresh_time += delta # Proximity hints do not need to rebuild at the rendering frame rate. if delta > 0 and _prompt_refresh_time < 0.1: return _prompt_refresh_time = 0 _refresh_prompt() func _refresh_prompt() -> void: _near_planet = null var best := INF for planet in get_tree().get_nodes_in_group("scanner_planets"): if not bool(planet.get("earthlike")): continue var distance: float = player.global_position.distance_to(planet.global_position) - float(planet.get("radius")) if CONTACT_RANGE.at_planet(player,planet) and distance < best: best = distance _near_planet = planet var nearby_freighter := CONTACT_RANGE.at_freighter(player,freighter) if nearby_freighter: _prompt_target = freighter elif is_instance_valid(_near_planet): _prompt_target = _near_planet else: _prompt_target = player var has_destination := nearby_freighter or is_instance_valid(_near_planet) if not has_destination and _message_time <= 0: _prompt.hide() # Never accumulate combat text while the hint is hidden. if not _text.text.is_empty(): _text.text = "" return var blocked := combat_blocked() if blocked: _prompt.hide() return if is_instance_valid(_near_planet): var profile: Dictionary = _near_planet.planet_profile if not GameState.captain.expedition.scans.has(profile.id): GameState.captain.expedition.scan(profile) var text := "" if _message_time > 0: text = _message _contact_label.text = "● RAPORT WYPRAWY" _prompt.tooltip_text = "" elif nearby_freighter: _contact_label.text = "● FREGATA / POŁĄCZENIE GOTOWE" text = "FRACHTOWIEC\nHangar · Warsztat · Magazyn" _prompt.tooltip_text = "Zadokuj, aby naprawić lądownik, rozbudować statek i przenieść wyposażenie." else: var profile: Dictionary = _near_planet.planet_profile _contact_label.text = "● TELEMETRIA / DANE PLANETY" text = _compact_scan(profile) _prompt.tooltip_text = PROFILES.scan_description(profile,GameState.captain) # A hidden card must be laid out again even if the target and text stayed the same. var changed := not _prompt.visible or _text.text != text or _land_button.visible != has_destination if _text.text != text: _text.text = text _land_button.visible = has_destination _land_button.text = "DOKUJ [L]" if nearby_freighter else "LĄDUJ [L]" _land_button.disabled = blocked _prompt.show() if changed: _layout_prompt() # A standalone PanelContainer otherwise keeps its previous large height. _layout_prompt.call_deferred() func _layout_prompt() -> void: if not is_instance_valid(_prompt): return # Do not invalidate the wrapped report's container layout every frame. var desired_size := Vector2(300, _prompt.get_combined_minimum_size().y) if not _prompt.size.is_equal_approx(desired_size): _prompt.size = desired_size var viewport_size := get_viewport().get_visible_rect().size # Return reports belong to the screen, not to a moving planet or ship. # Object avoidance can switch sides abruptly as the camera moves. if _message_time > 0: _prompt.position = Vector2( maxf(10, (viewport_size.x - _prompt.size.x) * 0.5), maxf(preload("res://scripts/ui/resource_bar.gd").HEIGHT + 12, viewport_size.y - _prompt.size.y - 160) ) return var target: Node2D = _prompt_target if is_instance_valid(_prompt_target) else player var canvas := target.get_global_transform_with_canvas() var radius := _freighter_half_size.x if target == freighter else float(target.get("radius")) if target != player else 30.0 var vertical := _freighter_half_size.y if target == freighter else radius var obstacles: Array[Rect2] = CONTACT.hud_obstacles(game.get_node_or_null("HUD/HUDRoot")) var services: Control = game.get("services_menu") if is_instance_valid(services) and services.is_visible_in_tree(): obstacles.append(services.get_global_rect().grow(8)) _prompt.position = CONTACT.beside(canvas.origin,radius*canvas.x.length(),_prompt.size,viewport_size,obstacles,true,vertical*canvas.y.length()) func _compact_scan(profile: Dictionary) -> String: return PROFILES.scan_description(profile,GameState.captain) func _unhandled_key_input(event: InputEvent) -> void: if is_instance_valid(screen) or get_tree().paused or not event.pressed or event.echo: return if event.keycode == KEY_L: land_nearby() else: return get_viewport().set_input_as_handled() func land_nearby() -> void: _refresh_prompt() if is_instance_valid(screen) or bool(player.get("_is_destroyed")): return if combat_blocked(): return if CONTACT_RANGE.at_freighter(player,freighter): open_freighter() return if not is_instance_valid(_near_planet): return var progress: Resource = GameState.captain.expedition if not progress.begin_visit(_near_planet.planet_profile): _notice("Lądownik wymaga naprawy do 100% we frachtowcu.") return _enter() screen = Control.new() screen.set_script(preload("res://scripts/planets/surface_screen.gd")) screen.captain = GameState.captain screen.progress = progress screen.flight_ship = player screen.planet_texture = _near_planet.body_texture screen.finished.connect(finish_surface) _layer.add_child(screen) func open_freighter() -> void: if is_instance_valid(screen): return _enter() screen = preload("res://scenes/planets/freighter_interior.tscn").instantiate() screen.captain = GameState.captain screen.inventory = player.get_node("ShipInventory") screen.closed.connect(_leave) _layer.add_child(screen) func _enter() -> void: _previous_pause = get_tree().paused _previous_docked = player.is_docked _hud_was_visible = game.get_node("HUD/HUDRoot").visible game.get_node("HUD/HUDRoot").hide() player.is_docked = true player.clear_auxiliary_shield() player.velocity = Vector2.ZERO get_tree().paused = true _prompt.hide() func finish_surface(success: bool) -> void: var progress: Resource = GameState.captain.expedition var result: Dictionary = progress.finish_visit(GameState.captain,success) progress.deliver_modules(player.get_node("ShipInventory")) _leave() var returned: Dictionary = result.get("materials",{}).duplicate() if int(result.get("parts",0)) > 0: returned.parts = result.parts if int(result.get("missiles",0)) > 0: returned.missile = result.missiles if int(result.get("credits",0)) > 0: returned.credits = int(returned.get("credits",0))+int(result.credits) var cargo_text := UI.material_text(returned) _notice(("POWRÓT Z WYPRAWY · " + (cargo_text if not cargo_text.is_empty() else "brak łupu")) if result.get("success",false) else "LĄDOWNIK ZNISZCZONY · Utracono łup bieżącej wyprawy.") if not progress.delivered_modules.is_empty(): _message += "\nBrak miejsca: %d modułów czeka we frachtowcu." % progress.delivered_modules.size() func _leave() -> void: _needs_prompt_refresh = true if is_instance_valid(screen): _layer.remove_child(screen) screen.queue_free() screen = null player.is_docked = _previous_docked game.get_node("HUD/HUDRoot").visible = _hud_was_visible get_tree().paused = _previous_pause GameState.save_game() func _notice(message: String) -> void: _message = message _message_time = 7 _prompt_refresh_time = 0.1 func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST: if GameState.captain.expedition.active: GameState.captain.expedition.finish_visit(GameState.captain,true) GameState.save_game() get_tree().quit() func _exit_tree() -> void: if is_instance_valid(GameState.captain) and GameState.captain.expedition.active: GameState.captain.expedition.finish_visit(GameState.captain,true) if is_instance_valid(screen): get_tree().paused = _previous_pause get_tree().auto_accept_quit = true func request_distant_travel(map_id: String) -> bool: if not is_instance_valid(screen) or GameState.captain.expedition.active: return false if not game.travel_to_distant(map_id): return false _leave() return true func request_system_travel(level: int) -> bool: if not GameState.active_distant_map.is_empty(): return false if not is_instance_valid(screen) or GameState.captain.expedition.active or level == GameState.system_level or not GameState.can_visit_system(level): return false if not game.travel_to_system(level): return false _leave() return true func request_distant_return() -> bool: if not is_instance_valid(screen) or GameState.active_distant_map.is_empty(): return false var sector := game.get_node_or_null("DistantSector") if sector == null or not sector._return_home(): return false _leave() return true