From d0c79cafe9d4c698d54a0c05b9decd5a329e74e6 Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sat, 10 Oct 2026 23:07:57 +0200 Subject: [PATCH] =?UTF-8?q?=C5=81adowanei=20solarne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- documentation.txt | 2 +- scripts/game/combat_flash.gd | 23 ++++++++++ scripts/game/combat_flash.gd.uid | 1 + scripts/game/damage_popup.gd | 6 ++- scripts/game/enemy_ship.gd | 6 +++ scripts/game/flight_hud.gd | 34 +++++++++++++- scripts/game/game.gd | 6 +++ scripts/game/ship_controller.gd | 20 ++++++++- scripts/game/ship_energy_indicator.gd | 52 ++++++++++++++++++++++ scripts/game/ship_energy_indicator.gd.uid | 1 + scripts/game/solar_energy_effect.gd | 54 +++++++++++++++++++++++ scripts/game/solar_energy_effect.gd.uid | 1 + scripts/ui/combat_feedback.gd | 27 ++++++++++++ scripts/ui/combat_feedback.gd.uid | 1 + tests/captain/captain_system_test.gd | 1 + 15 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 scripts/game/combat_flash.gd create mode 100644 scripts/game/combat_flash.gd.uid create mode 100644 scripts/game/ship_energy_indicator.gd create mode 100644 scripts/game/ship_energy_indicator.gd.uid create mode 100644 scripts/game/solar_energy_effect.gd create mode 100644 scripts/game/solar_energy_effect.gd.uid create mode 100644 scripts/ui/combat_feedback.gd create mode 100644 scripts/ui/combat_feedback.gd.uid diff --git a/documentation.txt b/documentation.txt index 4dd775a..2956eb7 100644 --- a/documentation.txt +++ b/documentation.txt @@ -244,7 +244,7 @@ KOSZT ENERGII WYLOSOWANEJ BRONI ItemInstance.get_energy_cost() jest wspólne dla tooltipów i strzelania każdą bronią. Koszt = bazowy energy_cost * (średnie obrażenia / średnie obrażenia startowe)^0.5 - * (1 + liczba addonów * 0.05), zaokrąglony do 0.1 pkt. + * (1 + liczba addonów * 0.05), zaokrąglony do 0.1 pkt. Wyższy poziom zwiększa zakres obrażeń, a przez niego koszt. Czterokrotnie wyższe średnie obrażenia dają dwukrotny koszt przed addonami. Każdy addon dodaje 5% kosztu. Bonusy kapitana i innych zamontowanych przedmiotów nie zmieniają kosztu tego egzemplarza. diff --git a/scripts/game/combat_flash.gd b/scripts/game/combat_flash.gd new file mode 100644 index 0000000..749a328 --- /dev/null +++ b/scripts/game/combat_flash.gd @@ -0,0 +1,23 @@ +extends Node2D + +var flash_color := Color("77e8bc") +var _age := 0.0 +const DURATION := 0.45 + +func _ready() -> void: + z_index = 25 + +func _process(delta: float) -> void: + _age += delta + if _age >= DURATION: + queue_free() + else: + queue_redraw() + +func _draw() -> void: + var progress := _age / DURATION + var color := Color(flash_color, 1.0 - progress) + draw_arc(Vector2.ZERO, 24 + progress * 36, 0, TAU, 48, color, 3.0 * (1.0 - progress) + 1.0, true) + for index in 6: + var direction := Vector2.RIGHT.rotated(TAU * index / 6.0) + draw_line(direction * (20 + progress * 20), direction * (30 + progress * 42), color, 2.0, true) diff --git a/scripts/game/combat_flash.gd.uid b/scripts/game/combat_flash.gd.uid new file mode 100644 index 0000000..b1a68b3 --- /dev/null +++ b/scripts/game/combat_flash.gd.uid @@ -0,0 +1 @@ +uid://cex3lvclwdgjk diff --git a/scripts/game/damage_popup.gd b/scripts/game/damage_popup.gd index 947c518..6f627c4 100644 --- a/scripts/game/damage_popup.gd +++ b/scripts/game/damage_popup.gd @@ -2,6 +2,7 @@ extends Label @export var text_color := Color(1.0, 0.78, 0.28, 1.0) @export var is_critical := false +@export var is_evade := false func _ready() -> void: z_index = 30 @@ -9,7 +10,10 @@ func _ready() -> void: add_theme_color_override("font_shadow_color", Color(0.05, 0.08, 0.12, 0.95)) add_theme_constant_override("shadow_offset_x", 2) add_theme_constant_override("shadow_offset_y", 2) - add_theme_font_size_override("font_size", 25 if is_critical else 19) + add_theme_font_size_override("font_size", 28 if is_critical else (24 if is_evade else 19)) + if is_critical or is_evade: + scale = Vector2.ONE * 1.2 + create_tween().tween_property(self, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) var tween := create_tween() tween.set_parallel(true) tween.tween_property(self, "position:y", position.y - 48.0, 0.8).set_ease(Tween.EASE_OUT) diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index ed7e277..fef7550 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -123,6 +123,12 @@ func _spawn_destruction_effect() -> void: effect.call("configure", true, Color(1.0, 0.36, 0.18)) func _show_damage_popup(amount: float, critical: bool = false) -> void: + if critical: + var flash := Node2D.new() + flash.set_script(preload("res://scripts/game/combat_flash.gd")) + flash.set("flash_color", Color("ffbc5c")) + flash.position = position + get_parent().add_child(flash) var popup := DAMAGE_POPUP_SCENE.instantiate() as Label popup.text = "%s-%s DMG" % ["KRYT! " if critical else "", _format_damage(amount)] popup.set("is_critical", critical) diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index 8243675..dd928a6 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -9,8 +9,20 @@ extends CanvasLayer var _debug_expanded := true var _ability_buttons: Dictionary = {} var _ability_bar: HBoxContainer +var _ability_ready_style: StyleBoxFlat +var _ability_blocked_style: StyleBoxFlat func _ready() -> void: + var energy_indicator := Node2D.new() + energy_indicator.name = "EnergyIndicator" + energy_indicator.set_script(preload("res://scripts/game/ship_energy_indicator.gd")) + energy_indicator.set("player", player) + player.add_child(energy_indicator) + var feedback := Control.new() + feedback.name = "CombatFeedback" + feedback.set_script(preload("res://scripts/ui/combat_feedback.gd")) + feedback.set("player", player) + $HUDRoot.add_child(feedback) debug_info.add_theme_font_size_override("font_size", 9) # Establish anchor layout before applying offsets; absolute layout resets anchors. var equipment := $HUDRoot/ShipEquipmentPanel as Control @@ -81,7 +93,7 @@ OBRÓT: %.2f rad/s""" % [ roundi(float(player.get("hull"))), roundi(float(player.get("max_hull"))), roundi(float(player.get("shields"))), roundi(float(player.get("max_shields"))), roundi(float(player.get("energy"))), roundi(float(player.get("max_energy"))), - float(player.get("energy_regeneration")), + float(player.get("energy_regeneration")) + float(player.call("get_solar_regeneration")), float(player.get("shield_regeneration")), ship_velocity.length(), ship_velocity.x, ship_velocity.y, ship_position.x, ship_position.y, rad_to_deg(player.rotation), angular_speed, @@ -109,6 +121,7 @@ Szansa na unik: %.1f%% Redukcja obrażeń: %.1f%% Regeneracja energii: %.1f pkt/s (%+.1f) +Ładowanie solarne: +%.1f pkt/s Regeneracja tarcz: %.1f pkt/s (%+.1f) Maks. kadłub: %.1f (%+.1f pkt) Maks. tarcze: %.1f (%+.1f pkt) @@ -125,6 +138,7 @@ Zdobywane kredyty: %+.1f%%""" % [ float(player.call("get_damage_reduction")) * 100.0, float(player.get("energy_regeneration")), float(player.get("energy_regeneration")) - float(player.get("base_energy_regeneration")), + float(player.call("get_solar_regeneration")), float(player.get("shield_regeneration")), float(player.get("shield_regeneration")) - float(player.get("base_shield_regeneration")), float(player.get("max_hull")), float(player.get("max_hull")) - float(player.get("base_max_hull")), @@ -136,6 +150,8 @@ Zdobywane kredyty: %+.1f%%""" % [ ] func _create_ability_bar() -> void: + _ability_ready_style = _make_ability_style(true) + _ability_blocked_style = _make_ability_style(false) var panel := PanelContainer.new() panel.name = "ActiveAbilities" $HUDRoot.add_child(panel) @@ -168,6 +184,20 @@ func _update_ability_bar() -> void: any_equipped = true var reason: String = player.call("ability_unavailable_reason", index) button.disabled = not reason.is_empty() - button.text = "[%d] %s" % [index + 1, ability.display_name] + button.text = "[%d] %s\n%s" % [index + 1, ability.display_name, "GOTOWE" if reason.is_empty() else reason] + var style := _ability_ready_style if reason.is_empty() else _ability_blocked_style + button.add_theme_stylebox_override("normal", style) + button.add_theme_stylebox_override("disabled", style) + button.add_theme_stylebox_override("hover", style) + button.add_theme_color_override("font_color", Color("a5ffda")) + button.add_theme_color_override("font_disabled_color", Color("a3aebb")) button.tooltip_text = ability.description + ("\n" + reason if not reason.is_empty() else "\nGotowe") _ability_bar.get_parent().visible = any_equipped + +func _make_ability_style(ready: bool) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = Color("174237") if ready else Color("202731") + style.border_color = Color("77e8bc") if ready else Color("526071") + style.set_border_width_all(2 if ready else 1) + style.set_content_margin_all(6) + return style diff --git a/scripts/game/game.gd b/scripts/game/game.gd index 64fb656..f4d2534 100644 --- a/scripts/game/game.gd +++ b/scripts/game/game.gd @@ -71,6 +71,12 @@ func _spawn_celestial_bodies() -> void: star.set("radius", 140.0) star.set("is_star", true) bodies.add_child(star) + player.set("charging_star", star) + var solar_effect := Node2D.new() + solar_effect.name = "SolarEnergyEffect" + solar_effect.set_script(preload("res://scripts/game/solar_energy_effect.gd")) + solar_effect.set("player", player) + player.add_child(solar_effect) for index in system_data["planets"].size(): var data: Dictionary = system_data["planets"][index] var planet := Node2D.new() diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 7e8927d..561cd1a 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -6,6 +6,7 @@ const DAMAGE_POPUP_SCENE := preload("res://scenes/game/damage_popup.tscn") const COMBAT_ROLLS := preload("res://scripts/game/combat_rolls.gd") const CAPTAIN_BALANCE := preload("res://scripts/captain/captain_balance.gd") const SHIP_CATALOG := preload("res://scripts/game/ship_catalog.gd") +const COMBAT_FLASH := preload("res://scripts/game/combat_flash.gd") @export var max_speed := 76.0 @export var reverse_max_speed := 210.0 @@ -36,6 +37,10 @@ var experience_multiplier := 1.0 var credits_multiplier := 1.0 var critical_chance_bonus := 0.0 var critical_damage_bonus := 0.0 +@export var solar_charge_radius := 550.0 +@export var solar_charge_full_radius := 300.0 +@export var solar_charge_fraction_per_second := 0.25 +var charging_star: Node2D var angular_velocity := 0.0 var _sprite: Sprite2D @@ -114,8 +119,14 @@ func take_damage(amount: float) -> void: return var hit := COMBAT_ROLLS.incoming_hit(amount, get_evasion_chance(), get_damage_reduction()) if hit["evaded"]: + var flash := Node2D.new() + flash.set_script(COMBAT_FLASH) + flash.set("flash_color", Color("77e8bc")) + flash.position = position + get_parent().add_child(flash) var popup := DAMAGE_POPUP_SCENE.instantiate() as Label popup.text = "UNIK" + popup.set("is_evade", true) popup.set("text_color", Color("77e8bc")) popup.position = position + Vector2(-20, -38) get_parent().add_child(popup) @@ -191,7 +202,7 @@ func _physics_process(delta: float) -> void: if _is_destroyed or is_docked: return _contact_damage_timer = maxf(0.0, _contact_damage_timer - delta) - energy = minf(max_energy, energy + energy_regeneration * delta) + energy = minf(max_energy, energy + (energy_regeneration + get_solar_regeneration()) * delta) var was_boosting := is_boosting() if _boost_target > 0.0: _boost_speed = move_toward(_boost_speed, _boost_target, _boost_acceleration * delta) @@ -265,6 +276,13 @@ func _clamp_velocity_to_max_speed() -> void: func is_boosting() -> bool: return _boost_target > 0.0 or _boost_speed > 0.0 +func get_solar_regeneration() -> float: + if _is_destroyed or is_docked or not is_instance_valid(charging_star): + return 0.0 + var distance := global_position.distance_to(charging_star.global_position) + var strength := 1.0 - smoothstep(solar_charge_full_radius, solar_charge_radius, distance) + return max_energy * solar_charge_fraction_per_second * strength + func start_boost(speed_bonus: float, acceleration_time: float) -> void: _boost_target = max_speed * maxf(0.0, speed_bonus) _boost_acceleration = _boost_target / maxf(0.01, acceleration_time) diff --git a/scripts/game/ship_energy_indicator.gd b/scripts/game/ship_energy_indicator.gd new file mode 100644 index 0000000..c4744d1 --- /dev/null +++ b/scripts/game/ship_energy_indicator.gd @@ -0,0 +1,52 @@ +extends Node2D + +const FONT := preload("res://assets/fonts/font2.ttf") +var player: CharacterBody2D +var _display_ratio := 1.0 +var _background: StyleBoxFlat + +func _ready() -> void: + # World rendering interpolates this position together with the ship and camera. + top_level = true + process_physics_priority = 1 + global_position = player.global_position + reset_physics_interpolation() + z_index = 24 + _display_ratio = _energy_ratio() + _background = StyleBoxFlat.new() + _background.bg_color = Color("101925") + _background.border_color = Color("456277") + _background.set_border_width_all(1) + +func _physics_process(_delta: float) -> void: + if is_instance_valid(player): + var teleported := global_position.distance_to(player.global_position) > 600.0 + global_position = player.global_position + if teleported: + reset_physics_interpolation() + +func _process(delta: float) -> void: + if not is_instance_valid(player): + return + visible = not bool(player.get("_is_destroyed")) + var target := _energy_ratio() + _display_ratio = lerpf(_display_ratio, target, 1.0 - exp(-20.0 * delta)) + if absf(_display_ratio - target) < 0.001: + _display_ratio = target + queue_redraw() + +func _energy_ratio() -> float: + return clampf(float(player.get("energy")) / maxf(1.0, float(player.get("max_energy"))), 0.0, 1.0) + +func _draw() -> void: + if not is_instance_valid(player): + return + var color := Color("71e3ff") if _energy_ratio() >= 0.25 else Color("ffbd66") + var bar_position := Vector2(-44, 48) + draw_style_box(_background, Rect2(bar_position - Vector2(2, 2), Vector2(92, 10))) + draw_rect(Rect2(bar_position, Vector2(88 * _display_ratio, 6)), color) + var charging := float(player.call("get_solar_regeneration")) > 0.0 and _energy_ratio() < 1.0 + var caption := "ENERGIA %d%%" % roundi(_energy_ratio() * 100) + draw_string(FONT, bar_position + Vector2(0, 22), caption, HORIZONTAL_ALIGNMENT_CENTER, 88, 10, color) + if charging: + draw_string(FONT, bar_position + Vector2(-18, 36), "ŁADOWANIE SOLARNE", HORIZONTAL_ALIGNMENT_CENTER, 124, 9, Color("71e3ff")) diff --git a/scripts/game/ship_energy_indicator.gd.uid b/scripts/game/ship_energy_indicator.gd.uid new file mode 100644 index 0000000..efcb384 --- /dev/null +++ b/scripts/game/ship_energy_indicator.gd.uid @@ -0,0 +1 @@ +uid://bcs2adil8qg4e diff --git a/scripts/game/solar_energy_effect.gd b/scripts/game/solar_energy_effect.gd new file mode 100644 index 0000000..f37ac53 --- /dev/null +++ b/scripts/game/solar_energy_effect.gd @@ -0,0 +1,54 @@ +extends Node2D + +var player: CharacterBody2D +var _particles: Array[Dictionary] = [] +var _emission := 0.0 +var _phase := 0.0 + +func _ready() -> void: + top_level = true + process_physics_priority = 1 + global_position = player.global_position + reset_physics_interpolation() + z_index = 22 + +func _physics_process(_delta: float) -> void: + if is_instance_valid(player): + global_position = player.global_position + +func _process(delta: float) -> void: + if not is_instance_valid(player): + return + _phase += delta + var bonus := float(player.call("get_solar_regeneration")) + var charging := bonus > 0.0 and float(player.get("energy")) < float(player.get("max_energy")) + if charging: + _emission += delta * 30.0 + while _emission >= 1.0 and _particles.size() < 32: + _emission -= 1.0 + var star: Node2D = player.get("charging_star") + var direction := (global_position - star.global_position).normalized() + var source := star.global_position + direction * float(star.get("radius")) + source += direction.orthogonal() * randf_range(-35.0, 35.0) + _particles.append({"source": source, "age": 0.0, "duration": randf_range(0.45, 0.8)}) + else: + _emission = 0.0 + for index in range(_particles.size() - 1, -1, -1): + _particles[index]["age"] += delta + if _particles[index]["age"] >= _particles[index]["duration"]: + _particles.remove_at(index) + queue_redraw() + +func _draw() -> void: + if not is_instance_valid(player) or bool(player.get("_is_destroyed")): + return + for particle in _particles: + var progress: float = particle["age"] / particle["duration"] + var source: Vector2 = particle["source"] - global_position + var point := source.lerp(Vector2.ZERO, progress * progress) + var tail := source.lerp(Vector2.ZERO, maxf(0, progress - 0.06) ** 2) + var color := Color(0.35, 0.92, 1.0, sin(progress * PI)) + draw_line(tail, point, color, 2.0, true) + draw_circle(point, 2.5, color) + if not _particles.is_empty(): + draw_arc(Vector2.ZERO, 31.0 + sin(_phase * 5.0) * 2.0, 0, TAU, 48, Color(0.35, 0.92, 1.0, 0.45), 2.0, true) diff --git a/scripts/game/solar_energy_effect.gd.uid b/scripts/game/solar_energy_effect.gd.uid new file mode 100644 index 0000000..f50785e --- /dev/null +++ b/scripts/game/solar_energy_effect.gd.uid @@ -0,0 +1 @@ +uid://og6r3ft88u16 diff --git a/scripts/ui/combat_feedback.gd b/scripts/ui/combat_feedback.gd new file mode 100644 index 0000000..5ea490b --- /dev/null +++ b/scripts/ui/combat_feedback.gd @@ -0,0 +1,27 @@ +extends Control + +const FONT := preload("res://assets/fonts/font2.ttf") +const LOW_HULL_RATIO := 0.25 +var player: CharacterBody2D +var _elapsed := 0.0 + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + +func _process(delta: float) -> void: + _elapsed += delta + queue_redraw() + +func _draw() -> void: + if not is_instance_valid(player) or bool(player.get("_is_destroyed")): + return + var hull := float(player.get("hull")) + var max_hull := maxf(1.0, float(player.get("max_hull"))) + if hull / max_hull > LOW_HULL_RATIO or bool(player.get("is_docked")): + return + var pulse := 0.55 + 0.25 * (sin(_elapsed * TAU) + 1.0) * 0.5 + var warning_color := Color(1.0, 0.32, 0.26, pulse) + draw_rect(Rect2(Vector2(3, 3), size - Vector2(6, 6)), warning_color, false, 3) + var warning := "NISKI KADŁUB — %d%%" % roundi(hull / max_hull * 100) + draw_string(FONT, Vector2(0, 34), warning, HORIZONTAL_ALIGNMENT_CENTER, size.x, 18, warning_color) diff --git a/scripts/ui/combat_feedback.gd.uid b/scripts/ui/combat_feedback.gd.uid new file mode 100644 index 0000000..23fc5c8 --- /dev/null +++ b/scripts/ui/combat_feedback.gd.uid @@ -0,0 +1 @@ +uid://dxso36ob0ovm5 diff --git a/tests/captain/captain_system_test.gd b/tests/captain/captain_system_test.gd index 5840a2b..07f4d41 100644 --- a/tests/captain/captain_system_test.gd +++ b/tests/captain/captain_system_test.gd @@ -63,6 +63,7 @@ func _ready() -> void: assert(before == Vector2(float(player.get("hull")), float(player.get("shields")))) profile.skills["piloting"] = 0 profile.skills["mechanic"] = 50 + player.set("shields", 100.0) # Fixed fixture, independent of selected chassis capacity. player.call("take_damage", 100.0) assert(float(player.get("shields")) == 50 and float(player.get("hull")) == 100) player.set("shields", 0.0)