Randoloot/scripts/planets/expedition_controller.gd
2026-10-26 21:06:32 +01:00

297 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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:
var captain: Resource = GameState.captain
var report: Dictionary = PROFILES.threat_report(profile,captain)
var loot := PROFILES.poi_loot_report(profile,captain)
return "%s · %s\nTLEN %d%% · TEMP %d°C · BIO %d/50\nZAGROŻENIE: %.1f%% / 300%%\nŁUP POI: ×%.2f · odzysk %.0f%%\nPOI: podleć i naciśnij E\nLądowanie −%d HP · Lądownik %d%%" % [profile.name,PROFILES.biome_name(profile),profile.oxygen,profile.temperature,profile.flora,report.total,loot.multiplier,loot.recovery*100.0,profile.landing_damage,ceili(captain.expedition.lander_hp)]
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
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)
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