403 lines
17 KiB
GDScript
403 lines
17 KiB
GDScript
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)
|