Randoloot/scripts/planets/surface_screen.gd
2026-10-19 22:33:24 +02:00

403 lines
17 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 Control
signal finished(success: bool)
const UI := preload("res://scripts/planets/expedition_ui.gd")
const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
const EVENTS := preload("res://scripts/planets/surface_events.gd")
const GENERATOR := preload("res://scripts/planets/surface_generator.gd")
const POI_CARD := preload("res://scripts/planets/poi_presentation.gd")
var captain: Resource
var progress: Resource
var world: Node2D
var lander: Sprite2D
var camera: Camera2D
var weather: Node2D
var flight_effects: Node2D
var _hp: ProgressBar
var _cargo: Label
var _threat: Label
var _status: Label
var _dialog: PanelContainer
var _choices: VBoxContainer
var _poi: Dictionary = {}
var _rng := RandomNumberGenerator.new()
var _motion := preload("res://scripts/planets/lander_motion.gd").new()
var flight_ship: Node
var planet_texture: Texture2D
var _telemetry: PanelContainer
var _ending := false
var _hud_refresh_time := 0.0
var _previous_position := Vector2.ZERO
var _active_event: Dictionary = {}
var _event_armed := true
const SURFACE_RENDER_SCALE := 1.5
const SURFACE_CAMERA_ZOOM := 0.8
var _surface_viewport: SubViewport
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
_rng.seed = progress.visit_seed ^ 90317
if planet_texture == null:
var planets: Array[Texture2D] = PROFILES.earthlike_textures()
planet_texture = planets[posmod(int(progress.active_profile.seed),planets.size())]
if is_instance_valid(flight_ship):
_motion.configure(flight_ship)
var container := TextureRect.new()
container.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
container.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
container.stretch_mode = TextureRect.STRETCH_SCALE
container.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
container.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(container)
var viewport := SubViewport.new()
_surface_viewport = viewport
viewport.size = Vector2i(get_viewport_rect().size * SURFACE_RENDER_SCALE)
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
viewport.world_2d = World2D.new()
container.add_child(viewport)
container.texture = viewport.get_texture()
world = Node2D.new()
world.set_script(preload("res://scripts/planets/surface_world.gd"))
world.data = GENERATOR.generate(progress.visit_seed, progress.active_profile)
viewport.add_child(world)
lander = Sprite2D.new()
lander.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_ON
lander.texture = preload("res://assets/planets/lander.png")
lander.scale = Vector2.ONE * 100.0 / lander.texture.get_width()
lander.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
var sprite_material := ShaderMaterial.new()
sprite_material.shader = preload("res://resources/effects/lander_detail.gdshader")
lander.material = sprite_material
lander.z_index = 30
viewport.add_child(lander)
lander.reset_physics_interpolation()
var engine := Node2D.new()
engine.name = "EngineVisual"
engine.set_script(preload("res://scripts/planets/lander_engine_visual.gd"))
engine.source = self
engine.scale = Vector2.ONE / lander.scale
engine.z_index = -1
lander.add_child(engine)
var audio := AudioStreamPlayer.new()
audio.name = "EngineAudio"
audio.set_script(preload("res://scripts/game/ship_engine_audio.gd"))
audio.thrust_source = self
add_child(audio)
camera = Camera2D.new()
camera.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF
camera.process_callback = Camera2D.CAMERA2D_PROCESS_IDLE
camera.zoom = Vector2.ONE * SURFACE_CAMERA_ZOOM * SURFACE_RENDER_SCALE
camera.position_smoothing_enabled = true
camera.position_smoothing_speed = 6
camera.limit_left = -1920
camera.limit_top = -1920
camera.limit_right = 1920
camera.limit_bottom = 1920
viewport.add_child(camera)
var atmosphere := Node2D.new()
atmosphere.name = "SurfaceAtmosphere"
atmosphere.set_script(preload("res://scripts/planets/surface_clouds.gd"))
atmosphere.seed_value = progress.visit_seed
viewport.add_child(atmosphere)
weather = Node2D.new()
weather.set_script(preload("res://scripts/planets/surface_weather.gd"))
weather.source = self
viewport.add_child(weather)
flight_effects = Node2D.new()
flight_effects.set_script(preload("res://scripts/planets/surface_flight_effects.gd"))
flight_effects.source = self
flight_effects.weather = weather
viewport.add_child(flight_effects)
var nature_audio := Node.new()
nature_audio.name = "SurfaceAudio"
nature_audio.set_script(preload("res://scripts/planets/surface_audio.gd"))
nature_audio.source = self
add_child(nature_audio)
resized.connect(_resize_surface_viewport)
_telemetry = PanelContainer.new()
_telemetry.set_script(preload("res://scripts/planets/surface_telemetry.gd"))
_telemetry.source = self
add_child(_telemetry)
_hp = _telemetry.health
_status = _telemetry.status
_cargo = _telemetry.cargo
_threat = _telemetry.threat
var back := POI_CARD.button(self, "POWRÓT NA STATEK [L]", func(): _return(true))
back.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
back.position = Vector2(get_viewport_rect().size.x-285, 24)
back.size.x = 260
var help := UI.panel(self)
help.name = "SurfaceControls"
help.custom_minimum_size.x = 350
help.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
help.position = Vector2(24, get_viewport_rect().size.y-84)
POI_CARD.panel_style(help)
POI_CARD.text(help, "W — ciąg · S — hamuj / wstecz · A/D — obrót\nQ/E — ruch boczny · TAB — dopalanie · F — zbadaj", 13)
var map_frame := UI.panel(self, Vector2(195,195))
map_frame.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
map_frame.position = get_viewport_rect().size-Vector2(220,220)
var minimap := Control.new()
minimap.set_script(preload("res://scripts/planets/surface_minimap.gd"))
minimap.world = world
minimap.lander = lander
map_frame.add_child(minimap)
_dialog = UI.panel(self)
_dialog.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
var scroll := ScrollContainer.new()
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_dialog.add_child(scroll)
_choices = VBoxContainer.new()
_choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_choices.add_theme_constant_override("separation", 10)
scroll.add_child(_choices)
_layout_dialog()
_dialog.hide()
_update_hud()
func _physics_process(delta: float) -> void:
if _ending:
return
_previous_position = lander.position
if _dialog.visible:
_motion.velocity = Vector2.ZERO
_motion.angular_velocity = 0
return
_motion.step(delta, Input.get_axis("ship_turn_left","ship_turn_right"),
Input.is_action_pressed("ship_thrust"), Input.is_action_pressed("ship_brake"),
Input.get_axis("ship_strafe_left","ship_strafe_right"), Input.is_action_pressed("ship_mini_boost"))
lander.rotation = _motion.rotation
var next := lander.position + _motion.velocity * delta
var bounded := next.clamp(Vector2(-1780,-1780),Vector2(1780,1780))
if next.x != bounded.x:
_motion.velocity.x = 0
if next.y != bounded.y:
_motion.velocity.y = 0
lander.position = bounded
func _process(delta: float) -> void:
if _ending:
return
# Camera and sprite must follow the same interpolated pose between physics ticks.
camera.position = _previous_position.lerp(lander.position, Engine.get_physics_interpolation_fraction())
_hud_refresh_time += delta
if _hud_refresh_time >= 0.1:
_hud_refresh_time = 0
world.refresh_pois(lander.position)
_update_hud()
_check_events()
func _input(event: InputEvent) -> void:
if not event is InputEventKey or not event.pressed or event.echo or _ending:
return
if event.is_action("ship_mini_boost"):
get_viewport().set_input_as_handled()
return
if event.keycode == KEY_L:
_return(true)
elif event.keycode == KEY_ESCAPE:
if _dialog.visible:
_dialog.hide()
else:
_return(true)
elif event.keycode == KEY_F and not _dialog.visible:
for encounter in world.data.events:
if not encounter.complete and lander.position.distance_to(encounter.position) <= 180:
_event_armed = false
_show_event(encounter)
get_viewport().set_input_as_handled()
return
for poi in world.data.pois:
if not poi.complete and lander.position.distance_to(poi.position) <= 150:
_poi = poi
_show_poi()
break
else:
return
get_viewport().set_input_as_handled()
func _show_poi(message := "") -> void:
for child in _choices.get_children():
_choices.remove_child(child)
child.queue_free()
_dialog.show()
POI_CARD.panel_style(_dialog)
_choices.add_theme_constant_override("separation",8)
_layout_dialog()
var stage := clampi(int(_poi.stage),0,2)
POI_CARD.header(_choices,str(_poi.kind),stage,bool(_poi.complete))
if not message.is_empty():
POI_CARD.text(_choices,message,13,POI_CARD.GOLD)
var report := PROFILES.threat_report(progress.active_profile,captain)
if not _poi.complete:
var chance := PROFILES.success_chance(progress.active_profile,captain,_poi.stage)
var stages: Dictionary = preload("res://scripts/planets/poi_rules.gd").STAGES
POI_CARD.text(_choices,"DO ODZYSKANIA",11,POI_CARD.MUTED)
POI_CARD.text(_choices,preload("res://scripts/planets/poi_rules.gd").PREVIEWS[_poi.kind][stage],14,POI_CARD.GOLD)
POI_CARD.text(_choices,"Powodzenie %.1f%% · Zagrożenie %.1f%% / 300%%" % [chance*100,report.total],12,POI_CARD.MUTED)
POI_CARD.text(_choices,"Porażka uszkadza lądownik i zamyka to miejsce. Łup zachowasz po powrocie.",12,POI_CARD.MUTED)
var attempt := POI_CARD.button(_choices,str(stages[_poi.kind][stage]),_attempt,true)
attempt.disabled = chance <= 0.0
if attempt.disabled:
attempt.tooltip_text = "Brak szans przy obecnym zagrożeniu. Potrzebujesz lepszego wyposażenia."
POI_CARD.button(_choices,"Zakończ badanie tego miejsca",func(): _poi.complete = true; _dialog.hide())
POI_CARD.button(_choices, "Wróć do wyprawy", func(): _dialog.hide())
func _attempt() -> void:
if _ending:
return
var result: Dictionary = progress.resolve_poi(captain, _poi, _rng)
if result.is_empty():
return
if result.get("blocked",false):
_show_poi(str(result.reason))
return
var message := ""
if result.success:
message = "SUKCES · " + UI.material_text(result.materials)
if result.module != null:
message += "\nZnaleziono moduł statku, poziom %d." % result.module.item_level
if _poi.complete:
message += "\nMiejsce w pełni zbadane."
else:
message = "NIEPOWODZENIE · utrata %d HP.\nDalsze badanie tego miejsca jest niemożliwe." % result.damage
_update_hud()
_show_poi(message)
if progress.lander_hp <= 0:
await _destroy_lander()
func _destroy_lander() -> void:
if _ending:
return
_ending = true
_status.text = "LĄDOWNIK ZNISZCZONY — utracono łup wyprawy"
_dialog.hide()
var explosion := Node2D.new()
explosion.set_script(preload("res://scripts/game/player_destruction_effect.gd"))
explosion.configure(lander,Vector2.ZERO,camera)
lander.get_parent().add_child(explosion)
lander.hide()
await explosion.finished
finished.emit(false)
func _update_hud() -> void:
_hp.value = progress.lander_hp
if not _ending:
_status.text = "Lądownik %d / 100 HP · Energia %d%%" % [ceili(progress.lander_hp),roundi(_motion.energy)] + ("\nPogoda: " + weather.display_name() if is_instance_valid(weather) else "")
_threat.text = "Ryzyko %.1f%% / 300%% · Śr. POI %.1f%%" % [PROFILES.global_threat(progress.active_profile,captain),PROFILES.average_success_chance(progress.active_profile,captain)*100.0]
var loot_data: Dictionary = progress.cargo.duplicate()
if progress.cargo_parts > 0: loot_data.parts = progress.cargo_parts
if progress.cargo_missiles > 0: loot_data.missile = progress.cargo_missiles
var loot := UI.material_text(loot_data)
if progress.cargo_credits > 0:
loot += (" · " if not loot.is_empty() else "") + "%d kr." % progress.cargo_credits
_cargo.text = "Łup: " + (loot if not loot.is_empty() else "brak") + " · Moduły: %d" % progress.cargo_modules.size()
_cargo.tooltip_text = _cargo.text
func _return(success: bool) -> void:
if _ending:
return
_ending = true
finished.emit(success)
func engine_thrust_active() -> bool:
return not _ending and not _dialog.visible and (_motion.boost_active or (Input.is_action_pressed("ship_thrust") and not Input.is_action_pressed("ship_brake")))
func _check_events() -> void:
for event in world.data.events:
if event.complete:
continue
var distance: float = lander.position.distance_to(event.position)
if distance > 260:
_event_armed = true
elif distance <= 180 and _event_armed and not _dialog.visible:
_event_armed = false
_show_event(event)
return
func _event_text(text: String, color := Color("d5e4e8"), font_size := 15) -> Label:
var label := POI_CARD.text(_choices,text,font_size,color)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
return label
func _show_event(event: Dictionary, result: Dictionary = {}) -> void:
_active_event = event
EVENTS.prepare(event,captain,progress)
for child in _choices.get_children():
_choices.remove_child(child)
child.queue_free()
_choices.get_parent().set("scroll_vertical",0)
_dialog.show()
POI_CARD.panel_style(_dialog)
_layout_dialog()
_event_text(str(EVENTS.TITLES.get(event.kind,"Spotkanie")).to_upper(),UI.CYAN,22)
_event_text(str(EVENTS.STORIES.get(event.kind,"")))
var threat := PROFILES.threat_report(progress.active_profile,captain)
_event_text("Zagrożenie po sprzęcie: %.1f%% / 300%% (bez sprzętu %.1f%%)." % [threat.total,threat.raw_total],Color("efca8a"),13)
var reward_profile: Dictionary = event.get("reward_profile",progress.active_profile)
var bonus: Vector2i = EVENTS.REWARDS.module_bonus_range(reward_profile)
_event_text("Premia biomu: materiały ×%.1f · moduły +%d…+%d poz.\nLiczba amunicji i rakiet nie wzrasta." % [EVENTS.REWARDS.material_multiplier(reward_profile),bonus.x,bonus.y],Color("9bc4c7"),13)
if not result.is_empty():
_event_text(str(result.get("message",result.get("reason",""))),Color("efca8a"))
if not event.complete:
_event_text("Kredyty: %d · części: %d\nWydatki pobierane są od razu. Nowe nagrody zachowasz po bezpiecznym powrocie." % [EVENTS.stock(captain,progress,"credits"),EVENTS.stock(captain,progress,"parts")],Color("9bc4c7"),13)
if event.kind == "scan_cache":
_event_text(EVENTS.scan_text(event),Color("efca8a"))
if event.kind == "service" and progress.lander_hp >= 100:
_event_text("Kadłub jest w pełni sprawny. Naprawa nie jest potrzebna.",Color("efca8a"))
if event.kind == "trader":
_event_text("Możesz wybrać tylko jedną ofertę.",Color("efca8a"))
if event.kind in ["prototype","rescue","evacuation"]:
var limits: Vector2i = EVENTS.damage_range(event,progress)
_event_text("Sukces: %.2f%% · w razie porażki: −%d–%d HP\nSzansa uwzględnia warunki planety i sprzęt kapitana." % [EVENTS.chance(event,captain,progress)*100,limits.x,limits.y],Color("efca8a"))
if event.kind in ["blind_cache","safe_bet"]:
_event_text("Tajemniczy łup: 50% części, 35% minerały, 10% rakiety, 5% moduł. Wartość może być niższa od opłaty.",Color("9bc4c7"),13)
for option in EVENTS.options(event,captain,progress):
var action: String = option.id
var text: String = option.label
if not option.cost.is_empty():
text += "\nKoszt: "+EVENTS.reward_text(option.cost)
if not option.reward.is_empty():
text += "\n"+("Nagroda za sukces: " if event.kind in ["prototype","rescue","evacuation"] else "Otrzymasz: ")+EVENTS.reward_text(option.reward)
var button := POI_CARD.button(_choices,text,func(): _event_action(action))
button.add_theme_font_size_override("font_size",12)
button.clip_text = true
button.tooltip_text = str(option.get("tooltip",text))
button.disabled = not EVENTS.can_pay(captain,progress,option.cost)
if event.kind in ["prototype","rescue","evacuation"] and action != "stop" and EVENTS.chance(event,captain,progress) <= 0.0:
button.disabled = true
button.tooltip_text += "\nTen etap ma 0% szans przy obecnym zagrożeniu."
if button.disabled:
button.tooltip_text += "\nSprawdź wymagane zasoby i aktualne zagrożenie."
POI_CARD.button(_choices,"Może później",func(): _dialog.hide())
else:
_event_text("Spotkanie zakończone. Nagrody w ładowni wyprawy przepadną po zniszczeniu lądownika.",Color("9bc4c7"),13)
POI_CARD.button(_choices,"Kontynuuj wyprawę",func(): _dialog.hide())
func _event_action(action: String) -> void:
if _ending:
return
var result: Dictionary = EVENTS.resolve(_active_event,action,captain,progress)
_show_event(_active_event,result)
world.refresh_pois(lander.position)
_update_hud()
if progress.lander_hp <= 0:
await _destroy_lander()
func _layout_dialog() -> void:
if not is_instance_valid(_dialog):
return
var view := get_viewport_rect().size
var width := minf(450,maxf(280,view.x-32))
var height := minf(600,maxf(180,view.y-48))
_dialog.size = Vector2(width,height)
_dialog.position = Vector2(view.x-width-20,maxf(16,(view.y-height)*0.5))
func _resize_surface_viewport() -> void:
_layout_dialog()
if is_instance_valid(_surface_viewport):
_surface_viewport.size = Vector2i(Vector2(maxf(1,size.x),maxf(1,size.y))*SURFACE_RENDER_SCALE)