From 8e90447486f5ab6ca1ed823e58715aa71ab6666a Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sun, 11 Oct 2026 20:24:33 +0200 Subject: [PATCH] Zmiany w bosach i balans --- resources/items/beam_laser.tres | 2 +- scenes/ui/ship_selection.tscn | 21 +- scripts/game/enemy_health_bar.gd | 24 ++- scripts/game/enemy_ship.gd | 11 +- scripts/game/flight_hud.gd | 195 +++++++++++------- scripts/levels/level_balance.gd | 17 +- scripts/ui/main_menu.gd | 53 ++++- scripts/ui/planet_services_menu.gd | 3 +- scripts/ui/ship_selection.gd | 49 +++-- tests/captain/captain_system_test.gd | 2 +- tests/items/laser_energy_regeneration_test.gd | 52 +++++ .../laser_energy_regeneration_test.gd.uid | 1 + .../items/laser_energy_regeneration_test.tscn | 6 + .../machine_gun_shield_recovery_test.gd.uid | 1 + tests/levels/enemy_tiers_test.gd | 34 +++ tests/levels/enemy_tiers_test.gd.uid | 1 + tests/levels/enemy_tiers_test.tscn | 4 + tests/levels/system_lifecycle_test.gd | 4 +- tests/ships/captain_ship_drawer_test.gd | 60 ++++++ tests/ships/captain_ship_drawer_test.gd.uid | 1 + tests/ships/captain_ship_drawer_test.tscn | 4 + tests/ships/ship_selection_drawer_test.gd | 43 ++++ tests/ships/ship_selection_drawer_test.gd.uid | 1 + tests/ships/ship_selection_drawer_test.tscn | 4 + tests/ships/ship_selection_test.gd | 5 +- 25 files changed, 459 insertions(+), 139 deletions(-) create mode 100644 tests/items/laser_energy_regeneration_test.gd create mode 100644 tests/items/laser_energy_regeneration_test.gd.uid create mode 100644 tests/items/laser_energy_regeneration_test.tscn create mode 100644 tests/items/machine_gun_shield_recovery_test.gd.uid create mode 100644 tests/levels/enemy_tiers_test.gd create mode 100644 tests/levels/enemy_tiers_test.gd.uid create mode 100644 tests/levels/enemy_tiers_test.tscn create mode 100644 tests/ships/captain_ship_drawer_test.gd create mode 100644 tests/ships/captain_ship_drawer_test.gd.uid create mode 100644 tests/ships/captain_ship_drawer_test.tscn create mode 100644 tests/ships/ship_selection_drawer_test.gd create mode 100644 tests/ships/ship_selection_drawer_test.gd.uid create mode 100644 tests/ships/ship_selection_drawer_test.tscn diff --git a/resources/items/beam_laser.tres b/resources/items/beam_laser.tres index 28337f9..a2675e0 100644 --- a/resources/items/beam_laser.tres +++ b/resources/items/beam_laser.tres @@ -14,7 +14,7 @@ stat_suffix = " DMG" weapon_kind = 2 fire_interval = 0.2 weapon_range = 800.0 -energy_cost = 2.0 +energy_cost = 2.5 damage_lower_roll_min = 1 damage_lower_roll_max = 3 damage_spread_roll_min = 1 diff --git a/scenes/ui/ship_selection.tscn b/scenes/ui/ship_selection.tscn index f389d35..c1af85e 100644 --- a/scenes/ui/ship_selection.tscn +++ b/scenes/ui/ship_selection.tscn @@ -18,16 +18,16 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -color = Color(0.018, 0.035, 0.065, 1) +color = Color(0.018, 0.035, 0.065, 0.97) [node name="Layout" type="VBoxContainer" parent="."] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 48.0 +offset_left = 28.0 offset_top = 28.0 -offset_right = -48.0 +offset_right = -28.0 offset_bottom = -28.0 grow_horizontal = 2 grow_vertical = 2 @@ -42,7 +42,7 @@ text = "Wybierz statek" [node name="Instructions" type="Label" parent="Layout"] layout_mode = 2 theme_override_colors/font_color = Color(0.55, 0.7, 0.82, 1) -text = "Wybierz jednostkę, którą rozpoczniesz nową grę." +text = "Kliknij statek, aby od razu rozpocząć grę." [node name="ShipScroll" type="ScrollContainer" parent="Layout"] layout_mode = 2 @@ -55,22 +55,15 @@ size_flags_horizontal = 3 size_flags_vertical = 0 theme_override_constants/h_separation = 14 theme_override_constants/v_separation = 14 -columns = 4 +columns = 2 [node name="Footer" type="HBoxContainer" parent="Layout"] layout_mode = 2 theme_override_constants/separation = 20 -[node name="SelectedLabel" type="Label" parent="Layout/Footer"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -vertical_alignment = 1 -text = "Wybierz statek" - -[node name="ContinueButton" type="Button" parent="Layout/Footer"] +[node name="CloseButton" type="Button" parent="Layout/Footer"] unique_name_in_owner = true custom_minimum_size = Vector2(220, 52) layout_mode = 2 theme_override_font_sizes/font_size = 20 -text = "Rozpocznij grę" +text = "WRÓĆ" diff --git a/scripts/game/enemy_health_bar.gd b/scripts/game/enemy_health_bar.gd index ba26d1d..86dff48 100644 --- a/scripts/game/enemy_health_bar.gd +++ b/scripts/game/enemy_health_bar.gd @@ -1,30 +1,50 @@ extends Node2D var enemy: CharacterBody2D +var _tier_label: Label func _ready() -> void: enemy = get_parent() as CharacterBody2D top_level = true z_index = 30 light_mask = 4 + _tier_label = Label.new() + _tier_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + _tier_label.light_mask = 4 + _tier_label.position = Vector2(-50, -23) + _tier_label.size = Vector2(100, 16) + _tier_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _tier_label.add_theme_font_size_override("font_size", 11) + _tier_label.add_theme_color_override("font_outline_color", Color("08111d")) + _tier_label.add_theme_constant_override("outline_size", 3) + add_child(_tier_label) _update_position() + _update_tier_label() func _process(_delta: float) -> void: _update_position() + _update_tier_label() queue_redraw() func _update_position() -> void: global_position = enemy.global_position + Vector2(0, -enemy.get_visual_size() * 0.5 - 12.0) +func _tier_color() -> Color: + return Color("ff76d6") if enemy.loot_tier == 2 else (Color("ffc785") if enemy.loot_tier == 1 else Color("ef5967")) + +func _update_tier_label() -> void: + _tier_label.text = "BOSS" if enemy.loot_tier == 2 else ("ELITA" if enemy.loot_tier == 1 else "") + _tier_label.add_theme_color_override("font_color", _tier_color()) + func get_health_ratio() -> float: return clampf(enemy.hull / maxf(enemy.max_hull, 0.001), 0.0, 1.0) func _draw() -> void: if not is_instance_valid(enemy) or enemy.hull <= 0.0: return - var width := 72.0 if enemy.loot_tier == 2 else 44.0 + var width := 72.0 if enemy.loot_tier == 2 else (56.0 if enemy.loot_tier == 1 else 44.0) var background := Rect2(-width * 0.5, -3.0, width, 6.0) draw_rect(background.grow(1.0), Color("08111dee")) draw_rect(background, Color("39232bee")) - var color := Color("ffac46") if enemy.loot_tier == 2 else Color("ef5967") + var color := _tier_color() draw_rect(Rect2(background.position, Vector2(width * get_health_ratio(), 6.0)), color) diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 0f79fe5..324f5c4 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -34,13 +34,15 @@ func _ready() -> void: func apply_level_settings(level: int, tier: int) -> void: loot_tier = tier + $Sprite2D.modulate = get_tier_tint() var power := LEVEL_BALANCE.power_scale(level) + var combat_multiplier := 2.0 if tier == 2 else 1.25 var boss_multiplier := 2.0 if tier == 2 else 1.0 var hull_multiplier := 4.0 if tier == 2 else (2.0 if tier == 1 else 1.0) var damage_bonus := 4.0 if tier == 2 else (1.3 if tier == 1 else 1.0) - max_hull = 50.0 * power * hull_multiplier + max_hull = 50.0 * power * hull_multiplier * combat_multiplier hull = max_hull - weapon_damage = 8.0 * power * damage_bonus + weapon_damage = 8.0 * power * damage_bonus * combat_multiplier approach_speed = 62.0 * (1.0 + 0.0005 * (clampi(level, 1, 1000) - 1)) * boss_multiplier approach_acceleration = 85.0 * boss_multiplier turn_speed = 1.8 * boss_multiplier @@ -56,9 +58,12 @@ func configure(player: CharacterBody2D, ship_texture: Texture2D) -> void: var largest_dimension := maxf(ship_texture.get_width(), ship_texture.get_height()) var visual_size := get_visual_size() _sprite.scale = Vector2.ONE * (visual_size / largest_dimension) - _sprite.modulate = Color("ffc785") if loot_tier == 1 else Color.WHITE + _sprite.modulate = get_tier_tint() preload("res://scripts/game/ship_shadow.gd").attach_to(_sprite) +func get_tier_tint() -> Color: + return Color("ffb3df") if loot_tier == 2 else (Color("ffc785") if loot_tier == 1 else Color.WHITE) + func get_visual_size() -> float: return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0) diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index 9d2e5be..82567b5 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -12,6 +12,11 @@ var _ability_bar: HBoxContainer var _ability_ready_style: StyleBoxFlat var _ability_blocked_style: StyleBoxFlat var _inventory_clip: Control +var _sidebar: PanelContainer +var _sidebar_toggle: Button +var _sidebar_open := false +var _sidebar_tween: Tween +const SIDEBAR_WIDTH := 344.0 func _ready() -> void: var energy_indicator := Node2D.new() @@ -25,12 +30,7 @@ func _ready() -> void: 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 - equipment.set("layout_mode", 1) - equipment.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) - equipment.offset_left = -330.0 - equipment.offset_right = -10.0 + _create_sidebar() _inventory_clip = Control.new() _inventory_clip.name = "InventoryDrawer" _inventory_clip.clip_contents = true @@ -45,67 +45,101 @@ func _ready() -> void: debug_toggle.pressed.connect(_toggle_debug) inventory_button.pressed.connect(_toggle_inventory) _update_debug_toggle() - $HUDRoot/CaptainPanel.resized.connect(func() -> void: _layout_sidebar.call_deferred()) _layout_sidebar.call_deferred() +func _create_sidebar() -> void: + _sidebar = PanelContainer.new() + _sidebar.name = "CaptainShipDrawer" + var style := StyleBoxFlat.new() + style.bg_color = Color(0.018, 0.028, 0.043, 0.97) + style.border_color = Color("52677e") + style.set_border_width_all(1) + style.set_content_margin_all(8) + _sidebar.add_theme_stylebox_override("panel", style) + $HUDRoot.add_child(_sidebar) + var scroll := ScrollContainer.new() + scroll.name = "Scroll" + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + _sidebar.add_child(scroll) + var contents := VBoxContainer.new() + contents.name = "Contents" + contents.size_flags_horizontal = Control.SIZE_EXPAND_FILL + contents.add_theme_constant_override("separation", 8) + scroll.add_child(contents) + var captain := $HUDRoot/CaptainPanel as PanelContainer + var equipment := $HUDRoot/ShipEquipmentPanel as PanelContainer + for panel in [captain, equipment]: + panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT) + panel.reparent(contents, false) + panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) + if panel == captain: + contents.add_child(HSeparator.new()) + _sidebar_toggle = Button.new() + _sidebar_toggle.name = "CaptainShipToggle" + _sidebar_toggle.text = "KAPITAN / STATEK ◀" + _sidebar_toggle.add_theme_font_size_override("font_size", 12) + _sidebar_toggle.focus_mode = Control.FOCUS_NONE + $HUDRoot.add_child(_sidebar_toggle) + _sidebar_toggle.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) + _sidebar_toggle.offset_left = -210 + _sidebar_toggle.offset_right = -8 + _sidebar_toggle.offset_top = 8 + _sidebar_toggle.offset_bottom = 38 + _sidebar_toggle.pressed.connect(_toggle_sidebar) + _sidebar.hide() + +func _toggle_sidebar() -> void: + _sidebar_open = not _sidebar_open + if _sidebar_tween != null and _sidebar_tween.is_running(): + _sidebar_tween.kill() + _sidebar_toggle.text = "ZAMKNIJ ▶" if _sidebar_open else "KAPITAN / STATEK ◀" + if _sidebar_open: + _sidebar.show() + else: + close_inventory() + _sidebar_tween = create_tween() + _sidebar_tween.tween_property(_sidebar, "position:x", $HUDRoot.size.x - SIDEBAR_WIDTH - 8 if _sidebar_open else $HUDRoot.size.x, 0.25).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT if _sidebar_open else Tween.EASE_IN) + if not _sidebar_open: + _sidebar_tween.tween_callback(_sidebar.hide) + func _layout_sidebar() -> void: - var captain_panel := $HUDRoot/CaptainPanel as Control - var equipment_panel := $HUDRoot/ShipEquipmentPanel as Control - captain_panel.grow_vertical = Control.GROW_DIRECTION_END - equipment_panel.grow_vertical = Control.GROW_DIRECTION_END - equipment_panel.position.y = captain_panel.position.y + captain_panel.size.y + 6.0 - equipment_panel.size.y = equipment_panel.get_combined_minimum_size().y + if _sidebar_tween != null and _sidebar_tween.is_running(): + _sidebar_tween.kill() + _sidebar.size = Vector2(SIDEBAR_WIDTH, maxf(100.0, $HUDRoot.size.y - 52.0)) + _sidebar.position = Vector2($HUDRoot.size.x - SIDEBAR_WIDTH - 8 if _sidebar_open else $HUDRoot.size.x, 44) + _sidebar.visible = _sidebar_open _inventory_clip.size = inventory_panel.size - _inventory_clip.position = Vector2(equipment_panel.position.x - _inventory_clip.size.x - 8, equipment_panel.position.y) + _layout_inventory() + +func _layout_inventory() -> void: + if _inventory_clip == null: + return + var equipment := _sidebar.get_node("Scroll/Contents/ShipEquipmentPanel") as Control + _inventory_clip.position = Vector2(_sidebar.position.x - _inventory_clip.size.x - 8, minf(equipment.global_position.y, maxf(8.0, $HUDRoot.size.y - inventory_panel.size.y - 8))) func _process(_delta: float) -> void: + _layout_inventory() _update_ability_bar() if not _debug_expanded: return - - var ship_position := player.global_position - var ship_velocity := player.velocity - var angular_speed := float(player.get("angular_velocity")) var game := get_parent() - var enemy_count: int = game.call("get_remaining_enemies") - var goal: Node2D = game.get("protected_planet") - var to_planet := goal.global_position - player.global_position - debug_info.text = """STEROWANIE -W: ciąg S: hamulec / wsteczny -Tab: mini boost (+100%% prędkości, 20 energii/s) -Q / E: strafe A / D: obrót -LPM / PPM: broń 1 / broń 2 · ŚPM: rakieta -1–6: aktywne rozszerzenia - -UKŁAD: %d · PLANETY: %d + debug_info.text = """POZIOM UKŁADU: %d WROGOWIE: %d -CEL: %s -PLANETA RASY: %.0f px · %.0f° - -STATEK -KADŁUB: %d / %d -TARCZE: %d / %d -ENERGIA: %d / %d -REGEN E / T: %.1f / %.1f /s - -RUCH PRĘDKOŚĆ: %.1f px/s -WEKTOR: [%.1f, %.1f] px/s -POZYCJA: [%.1f, %.1f] -KIERUNEK: %.1f° -OBRÓT: %.2f rad/s""" % [ - GameState.system_level, int(game.get("system_data")["planets"].size()), enemy_count, - "Odwiedź chronioną rasę" if bool(game.get("system_cleared")) else "Oczyść układ", - to_planet.length(), wrapf(rad_to_deg(to_planet.angle()) + 90.0, 0.0, 360.0), - 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"))), + +AKTUALNA REGENERACJA +Energia: %.1f pkt/s +Tarcze: %.1f pkt/s + +%s""" % [ + GameState.system_level, + int(game.call("get_remaining_enemies")), + player.velocity.length(), 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, + _get_bonus_summary(), ] - debug_info.text += "\n\n" + _get_bonus_summary() func _toggle_debug() -> void: _debug_expanded = not _debug_expanded @@ -116,47 +150,48 @@ func _update_debug_toggle() -> void: debug_toggle.text = "DEBUG [−]" if _debug_expanded else "DEBUG [+]" func _toggle_inventory() -> void: + if not _sidebar_open: + _toggle_sidebar() inventory_panel.call("toggle_drawer") func close_inventory() -> void: inventory_panel.call("close_drawer", true) func _get_bonus_summary() -> String: - # Read final ship values so gear and captain multipliers match actual combat. - return """BONUSY ŁĄCZNE (MODUŁY + KAPITAN) -Obrażenia broni: %+.1f%% (×%.3f) -Szansa na krytyk: %.1f%% -Obrażenia krytyczne: ×%.2f (%+.1f%% bonusu) -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) + var cooldown_bonus := 0.0 + var inventory := player.get_node("ShipInventory") as ShipInventory + for item in inventory.weapon_slots + inventory.module_slots: + if item != null: + cooldown_bonus += item.get_stat_total("missile_cooldown_reduction") + return """BONUSY (ADDONY + KAPITAN) +Obrażenia broni: %+.1f%% +Szansa na krytyk: %+.1f%% +Obrażenia krytyczne: %+.1f%% +Szansa na unik: %+.1f%% +Redukcja obrażeń: %+.1f%% Odzysk tarcz z obrażeń: %.1f%% -Maks. kadłub: %.1f (%+.1f pkt) -Maks. tarcze: %.1f (%+.1f pkt) -Maks. energia: %.1f (%+.1f pkt) -Maks. prędkość: %.1f px/s (%+.1f%%) -Zdobywane XP: %+.1f%% -Zdobywane kredyty: %+.1f%%""" % [ +Regeneracja energii: %+.1f pkt/s +Regeneracja tarcz: %+.1f pkt/s +Kadłub: %+.1f pkt +Tarcze: %+.1f pkt +Energia: %+.1f pkt +Prędkość maks.: %+.1f%% +Skrócenie cooldownu rakiet: %.2f s +Doświadczenie: %+.1f%% +Kredyty: %+.1f%%""" % [ (float(player.call("get_weapon_damage_multiplier")) - 1.0) * 100.0, - float(player.call("get_weapon_damage_multiplier")), float(player.call("get_critical_chance")) * 100.0, - float(player.call("get_critical_multiplier")), float(player.get("critical_damage_bonus")) * 100.0, float(player.call("get_evasion_chance")) * 100.0, 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("shield_on_damage_percent")), - float(player.get("max_hull")), float(player.get("max_hull")) - float(player.get("base_max_hull")), - float(player.get("max_shields")), float(player.get("max_shields")) - float(player.get("base_max_shields")), - float(player.get("max_energy")), float(player.get("max_energy")) - float(player.get("base_max_energy")), - float(player.get("max_speed")), (float(player.get("max_speed")) / float(player.get("base_max_speed")) - 1.0) * 100.0, + float(player.get("energy_regeneration")) - float(player.get("base_energy_regeneration")), + float(player.get("shield_regeneration")) - float(player.get("base_shield_regeneration")), + float(player.get("max_hull")) - float(player.get("base_max_hull")), + float(player.get("max_shields")) - float(player.get("base_max_shields")), + float(player.get("max_energy")) - float(player.get("base_max_energy")), + (float(player.get("max_speed")) / float(player.get("base_max_speed")) - 1.0) * 100.0, + cooldown_bonus, (float(player.get("experience_multiplier")) - 1.0) * 100.0, (float(player.get("credits_multiplier")) - 1.0) * 100.0, ] diff --git a/scripts/levels/level_balance.gd b/scripts/levels/level_balance.gd index db5e4f6..8b7a8d0 100644 --- a/scripts/levels/level_balance.gd +++ b/scripts/levels/level_balance.gd @@ -2,8 +2,7 @@ extends RefCounted const MAX_LEVEL := 1000 const COUNT_POINTS := [Vector2(1, 5), Vector2(10, 10), Vector2(50, 18), Vector2(100, 23), Vector2(250, 29), Vector2(500, 34), Vector2(1000, 40)] -# x = level, y = elite fraction. Each level has exactly one boss. -const TIER_POINTS := [Vector2(10, 0), Vector2(50, 0.15), Vector2(100, 0.25), Vector2(500, 0.45), Vector2(1000, 0.55)] +const ELITES_PER_LEVEL := 2 static func enemy_count(level: int) -> int: var clamped_level := clampi(level, 1, MAX_LEVEL) @@ -16,19 +15,9 @@ static func enemy_count(level: int) -> int: static func enemy_tiers(level: int) -> Array[int]: var count := enemy_count(level) - var elite_fraction := 0.0 - for index in range(1, TIER_POINTS.size()): - var before: Vector2 = TIER_POINTS[index - 1] - var after: Vector2 = TIER_POINTS[index] - if level <= after.x: - var progress := clampf(inverse_lerp(before.x, after.x, level), 0.0, 1.0) - elite_fraction = lerpf(before.y, after.y, progress) - break - if level >= MAX_LEVEL: - elite_fraction = 0.55 - # One boss on every level, included in the total enemy count. + # One boss and two elites on every level, included in the total count. var bosses := 1 - var elites := mini(count - bosses, roundi(count * elite_fraction)) + var elites := ELITES_PER_LEVEL var tiers: Array[int] = [2] for elite in elites: tiers.append(1) diff --git a/scripts/ui/main_menu.gd b/scripts/ui/main_menu.gd index 0d872db..defd332 100644 --- a/scripts/ui/main_menu.gd +++ b/scripts/ui/main_menu.gd @@ -1,16 +1,65 @@ extends Control -const SHIP_SELECTION_SCENE := "res://scenes/ui/ship_selection.tscn" +const SHIP_SELECTION_SCENE := preload("res://scenes/ui/ship_selection.tscn") +const GAME_SCENE := "res://scenes/game/game.tscn" @onready var start_button: Button = %StartButton @onready var exit_button: Button = %ExitButton +var _ship_panel: Control +var _panel_open := false +var _launching := false +var _drawer_tween: Tween func _ready() -> void: start_button.pressed.connect(_start_game) exit_button.pressed.connect(_exit_game) + _ship_panel = SHIP_SELECTION_SCENE.instantiate() as Control + _ship_panel.name = "ShipSelectionPanel" + _ship_panel.hide() + add_child(_ship_panel) + _ship_panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT) + _ship_panel.connect("ship_selected", _launch_game) + _ship_panel.connect("close_requested", _close_ship_panel) + resized.connect(_layout_ship_panel) + _layout_ship_panel() func _start_game() -> void: - get_tree().change_scene_to_file(SHIP_SELECTION_SCENE) + if _panel_open or _launching: + return + if _drawer_tween != null and _drawer_tween.is_running(): + _drawer_tween.kill() + _panel_open = true + start_button.disabled = true + _ship_panel.position.x = size.x + _ship_panel.show() + _drawer_tween = create_tween() + _drawer_tween.tween_property(_ship_panel, "position:x", size.x - _ship_panel.size.x, 0.3).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT) + +func _layout_ship_panel() -> void: + if _ship_panel == null: + return + if _drawer_tween != null and _drawer_tween.is_running(): + _drawer_tween.kill() + _ship_panel.size = Vector2(minf(700.0, size.x * 0.55), size.y) + _ship_panel.position = Vector2(size.x - _ship_panel.size.x if _panel_open else size.x, 0) + +func _close_ship_panel() -> void: + if not _panel_open or _launching: + return + _panel_open = false + start_button.disabled = false + if _drawer_tween != null and _drawer_tween.is_running(): + _drawer_tween.kill() + _drawer_tween = create_tween() + _drawer_tween.tween_property(_ship_panel, "position:x", size.x, 0.2).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN) + _drawer_tween.tween_callback(_ship_panel.hide) + +func _launch_game() -> void: + if _launching: + return + _launching = true + GameState.start_new_run() + get_tree().change_scene_to_file(GAME_SCENE) func _exit_game() -> void: get_tree().quit() diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd index 241e0e4..7d13b47 100644 --- a/scripts/ui/planet_services_menu.gd +++ b/scripts/ui/planet_services_menu.gd @@ -67,7 +67,8 @@ func _position_at_planet() -> void: var planet_radius := float(planet.get("radius")) * canvas.x.length() size = panel.get_combined_minimum_size() var viewport_size := get_viewport_rect().size - var screen_right := viewport_size.x - 320.0 + var sidebar := get_parent().get_node_or_null("CaptainShipDrawer") as Control + var screen_right := viewport_size.x - (sidebar.size.x + 8.0 if sidebar != null and sidebar.visible else 8.0) var target := planet_screen + Vector2(planet_radius + 24.0, -size.y * 0.5) if target.x + size.x > screen_right: target.x = planet_screen.x - planet_radius - 24.0 - size.x diff --git a/scripts/ui/ship_selection.gd b/scripts/ui/ship_selection.gd index 38f39cd..cd898d4 100644 --- a/scripts/ui/ship_selection.gd +++ b/scripts/ui/ship_selection.gd @@ -1,17 +1,17 @@ extends Control -const GAME_SCENE := "res://scenes/game/game.tscn" +signal ship_selected +signal close_requested const CATALOG := preload("res://scripts/game/ship_catalog.gd") @onready var ship_grid: GridContainer = %ShipGrid -@onready var selected_label: Label = %SelectedLabel -@onready var continue_button: Button = %ContinueButton +@onready var close_button: Button = %CloseButton var button_group := ButtonGroup.new() func _ready() -> void: button_group.allow_unpress = false - continue_button.pressed.connect(_start_game) + close_button.pressed.connect(func() -> void: close_requested.emit()) _load_ships() func _load_ships() -> void: @@ -23,25 +23,35 @@ func _load_ships() -> void: continue var button := Button.new() button.name = "Ship%d" % int(chassis["number"]) - button.custom_minimum_size = Vector2(250, 260) + button.custom_minimum_size = Vector2(250, 240) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.toggle_mode = true button.button_group = button_group + var contents := VBoxContainer.new() + contents.mouse_filter = Control.MOUSE_FILTER_IGNORE + button.add_child(contents) + contents.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + contents.offset_left = 12 + contents.offset_right = -12 + contents.offset_top = 12 + contents.offset_bottom = -12 + contents.add_theme_constant_override("separation", 12) var preview := TextureRect.new() + preview.custom_minimum_size.y = 100 preview.texture = texture preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED preview.mouse_filter = Control.MOUSE_FILTER_IGNORE - button.add_child(preview) - preview.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) - preview.offset_left = -50.0 - preview.offset_right = 50.0 - preview.offset_top = 12.0 - preview.offset_bottom = 102.0 - button.text = "%s\n%s\n\n%s" % [chassis["name"], chassis["role"], CATALOG.stats_text(chassis)] + contents.add_child(preview) + var description := Label.new() + description.mouse_filter = Control.MOUSE_FILTER_IGNORE + description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + description.text = "%s\n%s\n\n%s" % [chassis["name"], chassis["role"], CATALOG.stats_text(chassis)] + description.add_theme_font_size_override("font_size", 13) + contents.add_child(description) button.add_theme_font_size_override("font_size", 13) button.tooltip_text = "%s — %s\nStatystyki bazowe przed bonusami modułów.\n%s" % [chassis["name"], chassis["role"], CATALOG.stats_text(chassis)] - button.pressed.connect(_select_ship.bind(chassis, button)) + button.pressed.connect(_choose_ship.bind(chassis, button)) ship_grid.add_child(button) if chassis["number"] == selected_ship["number"]: _select_ship(chassis, button) @@ -49,9 +59,12 @@ func _load_ships() -> void: func _select_ship(chassis: Dictionary, button: Button) -> void: GameState.selected_ship_path = CATALOG.texture_path(chassis) button.button_pressed = true - selected_label.text = "Wybrano: %s · %s" % [chassis["name"], chassis["role"]] - continue_button.disabled = false -func _start_game() -> void: - GameState.start_new_run() - get_tree().change_scene_to_file(GAME_SCENE) +func _choose_ship(chassis: Dictionary, button: Button) -> void: + _select_ship(chassis, button) + ship_selected.emit() + +func _unhandled_input(event: InputEvent) -> void: + if visible and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: + close_requested.emit() + get_viewport().set_input_as_handled() diff --git a/tests/captain/captain_system_test.gd b/tests/captain/captain_system_test.gd index b7a828c..783608d 100644 --- a/tests/captain/captain_system_test.gd +++ b/tests/captain/captain_system_test.gd @@ -54,7 +54,7 @@ func _ready() -> void: assert(profile.credits == 0) await _collect_credits(game.get_node("PlayerShip") as CharacterBody2D) assert(profile.credits == 180) - var panel := game.get_node("HUD/HUDRoot/CaptainPanel") + var panel := game.get_node("HUD/HUDRoot/CaptainShipDrawer/Scroll/Contents/CaptainPanel") panel.get_node("Contents/Skills/gunner/Train_gunner").emit_signal("pressed") assert(profile.get_skill_rank("gunner") == 1 and profile.learning_points == 0) panel.get_node("Contents/Skills/piloting/Train_piloting").emit_signal("pressed") diff --git a/tests/items/laser_energy_regeneration_test.gd b/tests/items/laser_energy_regeneration_test.gd new file mode 100644 index 0000000..400d636 --- /dev/null +++ b/tests/items/laser_energy_regeneration_test.gd @@ -0,0 +1,52 @@ +extends Node2D + +const GENERATOR := preload("res://scripts/items/item_generator.gd") +const REGEN := preload("res://resources/items/affixes/energy_regeneration.tres") + +class MeasuredLaser: + extends "res://scripts/game/weapon_controller.gd" + var shots := 0 + func _consume_energy(amount: float) -> bool: + var consumed := super._consume_energy(amount) + if consumed: + shots += 1 + return consumed + +func _ready() -> void: + seed(54321) + GameState.start_new_run() + var ship := preload("res://scenes/game/player_ship.tscn").instantiate() as CharacterBody2D + add_child(ship) + ship.get_node("WeaponController").set_physics_process(false) + var inventory := ship.get_node("ShipInventory") as ShipInventory + var laser := GENERATOR.generate_for_definition(preload("res://resources/items/beam_laser.tres"), 5) + assert(inventory.add_item(laser)) + assert(inventory.move_item("cargo", inventory.cargo.find(laser), "weapon", 0)) + # Five separate level-5 regeneration affixes, on five ordinary hull modules. + for index in 5: + var module := ItemInstance.new() + module.definition = preload("res://resources/items/hull_plating.tres") + module.item_level = 5 + var addon := ItemStatModifier.new() + addon.affix = REGEN + addon.stat_key = REGEN.stat_key + addon.value = REGEN.roll_value(5) + module.additional_modifiers.append(addon) + assert(inventory.add_item(module)) + assert(inventory.move_item("cargo", inventory.cargo.find(module), "module", index)) + print("REGEN_BONUS_%d: %.4f energy/s" % [index + 1, module.get_stat_total("energy_regeneration")]) + var controller := MeasuredLaser.new() + ship.add_child(controller) + await get_tree().physics_frame + ship.set("energy", 80.0) + assert(ship.call("get_solar_regeneration") == 0.0) + print("LASER_SETUP: level=5 DMG=%s addons=%d cost=%.2f regen=%.4f" % [laser.get_damage_range_text(), laser.get_addon_count(), laser.get_energy_cost(), ship.get("energy_regeneration")]) + Input.action_press("ship_fire_primary") + for frame in 600: + await get_tree().physics_frame + Input.action_release("ship_fire_primary") + var final_energy := float(ship.get("energy")) + assert(controller.shots > 0) + assert(final_energy < 80.0) + print("LASER_ENERGY_TEST: PASS duration=10s shots=%d initial=80 final=%.4f loss=%.4f" % [controller.shots, final_energy, 80.0 - final_energy]) + get_tree().quit() diff --git a/tests/items/laser_energy_regeneration_test.gd.uid b/tests/items/laser_energy_regeneration_test.gd.uid new file mode 100644 index 0000000..eb0061c --- /dev/null +++ b/tests/items/laser_energy_regeneration_test.gd.uid @@ -0,0 +1 @@ +uid://c4qoktbpmppg6 diff --git a/tests/items/laser_energy_regeneration_test.tscn b/tests/items/laser_energy_regeneration_test.tscn new file mode 100644 index 0000000..bb49da9 --- /dev/null +++ b/tests/items/laser_energy_regeneration_test.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/items/laser_energy_regeneration_test.gd" id="1"] + +[node name="LaserEnergyRegenerationTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/machine_gun_shield_recovery_test.gd.uid b/tests/items/machine_gun_shield_recovery_test.gd.uid new file mode 100644 index 0000000..d27e965 --- /dev/null +++ b/tests/items/machine_gun_shield_recovery_test.gd.uid @@ -0,0 +1 @@ +uid://b7motgp3dyqv5 diff --git a/tests/levels/enemy_tiers_test.gd b/tests/levels/enemy_tiers_test.gd new file mode 100644 index 0000000..ab98f1a --- /dev/null +++ b/tests/levels/enemy_tiers_test.gd @@ -0,0 +1,34 @@ +extends Node + +const BALANCE := preload("res://scripts/levels/level_balance.gd") +const GENERATOR := preload("res://scripts/levels/system_generator.gd") +const ENEMY := preload("res://scenes/game/enemy_ship.tscn") + +func _ready() -> void: + for level in range(1, 1001): + var tiers := BALANCE.enemy_tiers(level) + assert(tiers.count(1) == 2 and tiers.count(2) == 1) + assert(tiers.size() == BALANCE.enemy_count(level)) + assert(tiers.count(0) >= 2) + for level in [1, 10, 100, 1000]: + var data: Dictionary = GENERATOR.generate(level, 314159) + var elites := 0 + var bosses := 0 + for entry in data["enemies"]: + elites += 1 if entry["tier"] == 1 else 0 + bosses += 1 if entry["tier"] == 2 else 0 + assert(elites == 2 and bosses == 1) + for tier in 3: + var enemy := ENEMY.instantiate() as CharacterBody2D + add_child(enemy) + enemy.set_physics_process(false) + enemy.call("apply_level_settings", 1, tier) + var bar := enemy.get_node("HealthBar") + bar.call("_process", 0.0) + var label := bar.get_child(0) as Label + assert(label.text == ["", "ELITA", "BOSS"][tier]) + assert(label.light_mask == 4) + assert(enemy.get_node("Sprite2D").modulate == enemy.call("get_tier_tint")) + enemy.queue_free() + print("ENEMY_TIERS_TEST: PASS (1000 levels, generated encounters, exactly two elites/one boss, tint and labels)") + get_tree().quit() diff --git a/tests/levels/enemy_tiers_test.gd.uid b/tests/levels/enemy_tiers_test.gd.uid new file mode 100644 index 0000000..5cc1ee9 --- /dev/null +++ b/tests/levels/enemy_tiers_test.gd.uid @@ -0,0 +1 @@ +uid://df4ca8t4gt0f diff --git a/tests/levels/enemy_tiers_test.tscn b/tests/levels/enemy_tiers_test.tscn new file mode 100644 index 0000000..583bb92 --- /dev/null +++ b/tests/levels/enemy_tiers_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/levels/enemy_tiers_test.gd" id="1"] +[node name="EnemyTiersTest" type="Node"] +script = ExtResource("1") diff --git a/tests/levels/system_lifecycle_test.gd b/tests/levels/system_lifecycle_test.gd index a2c19a3..077b63e 100644 --- a/tests/levels/system_lifecycle_test.gd +++ b/tests/levels/system_lifecycle_test.gd @@ -127,7 +127,7 @@ func _ready() -> void: _assert_full_resources(player) var enemy := get_tree().get_nodes_in_group("enemies")[0] as CharacterBody2D enemy.call("apply_level_settings", 2, 2) - assert(is_equal_approx(float(enemy.get("max_hull")), 210.0)) + assert(is_equal_approx(float(enemy.get("max_hull")), 420.0)) enemy.call("take_damage", 100000.0) await get_tree().process_frame await get_tree().process_frame @@ -193,7 +193,7 @@ func _assert_boss_and_health(game: Node2D) -> void: normal = enemy assert(boss != null and normal != null) for key in ["max_hull", "weapon_damage"]: - assert(is_equal_approx(float(boss.get(key)), float(normal.get(key)) * 4.0)) + assert(is_equal_approx(float(boss.get(key)), float(normal.get(key)) * 6.4)) for key in ["approach_speed", "approach_acceleration", "turn_speed", "activation_range", "weapon_range"]: assert(is_equal_approx(float(boss.get(key)), float(normal.get(key)) * 2.0)) assert(is_equal_approx(boss.weapon_cooldown, normal.weapon_cooldown * 0.5)) diff --git a/tests/ships/captain_ship_drawer_test.gd b/tests/ships/captain_ship_drawer_test.gd new file mode 100644 index 0000000..c5d42a1 --- /dev/null +++ b/tests/ships/captain_ship_drawer_test.gd @@ -0,0 +1,60 @@ +extends Node + +const GAME := preload("res://scenes/game/game.tscn") + +func _ready() -> void: + get_tree().create_timer(10.0).timeout.connect(func() -> void: + push_error("CAPTAIN_SHIP_DRAWER_TEST: timed out") + get_tree().quit(1) + ) + GameState.start_new_run() + var game := GAME.instantiate() as Node2D + add_child(game) + await get_tree().process_frame + for enemy in get_tree().get_nodes_in_group("enemies"): + enemy.set_physics_process(false) + var ship := game.get_node("PlayerShip") as CharacterBody2D + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + var hud := game.get_node("HUD") + var root := hud.get_node("HUDRoot") as Control + var drawer := root.get_node("CaptainShipDrawer") as Control + var toggle := root.get_node("CaptainShipToggle") as Button + var captain := drawer.get_node("Scroll/Contents/CaptainPanel") + var equipment := drawer.get_node("Scroll/Contents/ShipEquipmentPanel") + assert(captain.get_parent() == equipment.get_parent()) + assert(not drawer.visible) + toggle.pressed.emit() + await get_tree().create_timer(0.3).timeout + assert(drawer.visible and is_equal_approx(drawer.position.x + drawer.size.x, root.size.x - 8.0)) + GameState.captain.learning_points = 1 + GameState.captain.emit_changed() + captain.get_node("Contents/Skills/gunner/Train_gunner").pressed.emit() + assert(GameState.captain.get_skill_rank("gunner") == 1) + equipment.get_node("Contents/InventoryButton").pressed.emit() + await get_tree().create_timer(0.3).timeout + var inventory_panel: Control = hud.get("inventory_panel") + assert(inventory_panel.visible and inventory_panel.get("_drawer_open")) + var inventory := ship.get_node("ShipInventory") as ShipInventory + var item := ItemInstance.new() + item.definition = preload("res://resources/items/engine_extension.tres") + assert(inventory.add_item(item)) + assert(inventory.move_item("cargo", inventory.cargo.find(item), "module", 0)) + assert(ship.get("max_speed") > ship.get("base_max_speed")) + toggle.pressed.emit() + await get_tree().create_timer(0.3).timeout + assert(not drawer.visible and not inventory_panel.visible) + toggle.pressed.emit() + await get_tree().create_timer(0.3).timeout + assert(drawer.visible and not inventory_panel.visible) + var original_size := root.size + root.size = Vector2(root.size.x, 480) + hud.call("_layout_sidebar") + await get_tree().process_frame + await get_tree().process_frame + var scroll := drawer.get_node("Scroll") as ScrollContainer + assert(scroll.get_v_scroll_bar().visible) + root.size = original_size + hud.call("_layout_sidebar") + print("CAPTAIN_SHIP_DRAWER_TEST: PASS (merged panel, slide, close/reopen, skills, inventory, equipment stats)") + get_tree().quit() diff --git a/tests/ships/captain_ship_drawer_test.gd.uid b/tests/ships/captain_ship_drawer_test.gd.uid new file mode 100644 index 0000000..dff4ec3 --- /dev/null +++ b/tests/ships/captain_ship_drawer_test.gd.uid @@ -0,0 +1 @@ +uid://deox4ywvpn5mi diff --git a/tests/ships/captain_ship_drawer_test.tscn b/tests/ships/captain_ship_drawer_test.tscn new file mode 100644 index 0000000..3dd29e9 --- /dev/null +++ b/tests/ships/captain_ship_drawer_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/ships/captain_ship_drawer_test.gd" id="1"] +[node name="CaptainShipDrawerTest" type="Node"] +script = ExtResource("1") diff --git a/tests/ships/ship_selection_drawer_test.gd b/tests/ships/ship_selection_drawer_test.gd new file mode 100644 index 0000000..3425817 --- /dev/null +++ b/tests/ships/ship_selection_drawer_test.gd @@ -0,0 +1,43 @@ +extends Node + +const MENU := preload("res://scenes/ui/main_menu.tscn") +const CATALOG := preload("res://scripts/game/ship_catalog.gd") + +func _ready() -> void: + get_tree().create_timer(10.0).timeout.connect(func() -> void: + push_error("SHIP_SELECTION_DRAWER_TEST: timed out") + get_tree().quit(1) + ) + var menu := MENU.instantiate() as Control + get_tree().root.add_child.call_deferred(menu) + await get_tree().process_frame + await get_tree().process_frame + get_tree().current_scene = menu + var panel := menu.get_node("ShipSelectionPanel") as Control + assert(not panel.visible) + var start := menu.get_node("MenuLayout/StartButton") as Button + start.pressed.emit() + assert(get_tree().current_scene == menu and panel.visible) + assert(is_equal_approx(panel.position.x, menu.size.x)) + await get_tree().create_timer(0.35).timeout + assert(is_equal_approx(panel.position.x + panel.size.x, menu.size.x)) + var grid := panel.get_node("Layout/ShipScroll/ShipGrid") as GridContainer + assert(grid.columns == 2 and grid.get_child_count() == 4) + assert(not panel.has_node("Layout/Footer/ContinueButton")) + panel.get_node("Layout/Footer/CloseButton").pressed.emit() + await get_tree().create_timer(0.25).timeout + assert(not panel.visible and not start.disabled) + start.pressed.emit() + await get_tree().create_timer(0.35).timeout + GameState.captain.credits = 999 + grid.get_node("Ship7").pressed.emit() + assert(GameState.selected_ship_path == CATALOG.texture_path(CATALOG.SHIPS[3])) + await get_tree().process_frame + await get_tree().process_frame + await get_tree().process_frame + var game := get_tree().current_scene + assert(game.scene_file_path == "res://scenes/game/game.tscn") + assert(GameState.captain.credits == 0 and GameState.system_level == 1) + assert(game.get_node("PlayerShip").get("max_energy") == 125.0) + print("SHIP_SELECTION_DRAWER_TEST: PASS (Start opens drawer in menu, animation, four ships, close/reopen, one-click launch, selected ship stats, fresh run)") + get_tree().quit() diff --git a/tests/ships/ship_selection_drawer_test.gd.uid b/tests/ships/ship_selection_drawer_test.gd.uid new file mode 100644 index 0000000..0a4e097 --- /dev/null +++ b/tests/ships/ship_selection_drawer_test.gd.uid @@ -0,0 +1 @@ +uid://dsx5ej1byds5h diff --git a/tests/ships/ship_selection_drawer_test.tscn b/tests/ships/ship_selection_drawer_test.tscn new file mode 100644 index 0000000..94499fa --- /dev/null +++ b/tests/ships/ship_selection_drawer_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/ships/ship_selection_drawer_test.gd" id="1"] +[node name="ShipSelectionDrawerTest" type="Node"] +script = ExtResource("1") diff --git a/tests/ships/ship_selection_test.gd b/tests/ships/ship_selection_test.gd index 74bd0d1..5ad1df9 100644 --- a/tests/ships/ship_selection_test.gd +++ b/tests/ships/ship_selection_test.gd @@ -23,7 +23,10 @@ func _ready() -> void: assert(ship.get("max_speed") == chassis["speed"] and ship.get("energy_regeneration") == 5.0) assert(ship.get_node("Sprite2D").texture.resource_path == CATALOG.texture_path(chassis)) var inventory: ShipInventory = ship.get_node("ShipInventory") - assert(inventory.move_item("cargo", 2, "module", 0)) + var shield := ItemInstance.new() + shield.definition = preload("res://resources/items/reinforced_shield.tres") + assert(inventory.add_item(shield)) + assert(inventory.move_item("cargo", inventory.cargo.find(shield), "module", 0)) var equipped := inventory.get_item("module", 0) assert(ship.get("max_shields") == chassis["shields"] + equipped.get_stat_total("max_shields")) inventory.preserve_for_restart()