From 0c9f812cfef69d7e8f73b11fff20139776345923 Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sun, 11 Oct 2026 17:42:46 +0200 Subject: [PATCH] Slot na rakiety --- resources/items/affix_database.tres | 6 +- resources/items/affixes/missile_cooldown.tres | 13 +++ resources/items/engine_extension.tres | 6 +- resources/items/missile_launcher.tres | 4 +- resources/items/tuned_thrusters.tres | 13 --- resources/items/unguided_missile.tres | 22 ++++ scenes/ui/ship_equipment_panel.tscn | 10 ++ scripts/game/flight_hud.gd | 2 +- scripts/game/weapon_controller.gd | 36 ++++-- scripts/game/weapon_projectile.gd | 3 +- scripts/items/item_affix_definition.gd | 2 +- scripts/items/item_definition.gd | 1 + scripts/items/item_generator.gd | 4 +- scripts/items/item_instance.gd | 4 +- scripts/items/ship_inventory.gd | 68 +++++++++-- scripts/ui/equipment_slot_view.gd | 23 +++- scripts/ui/planet_services_menu.gd | 15 +++ scripts/ui/ship_inventory_panel.gd | 14 ++- tests/abilities/missile_system_test.gd | 107 ++++++++++++++++++ tests/abilities/missile_system_test.gd.uid | 1 + tests/abilities/missile_system_test.tscn | 4 + tests/items/weapon_damage_test.gd | 13 ++- 22 files changed, 322 insertions(+), 49 deletions(-) create mode 100644 resources/items/affixes/missile_cooldown.tres delete mode 100644 resources/items/tuned_thrusters.tres create mode 100644 resources/items/unguided_missile.tres create mode 100644 tests/abilities/missile_system_test.gd create mode 100644 tests/abilities/missile_system_test.gd.uid create mode 100644 tests/abilities/missile_system_test.tscn diff --git a/resources/items/affix_database.tres b/resources/items/affix_database.tres index ec1c2c3..3c1ed73 100644 --- a/resources/items/affix_database.tres +++ b/resources/items/affix_database.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=13 format=3] +[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=14 format=3] [ext_resource type="Script" path="res://scripts/items/item_affix_database.gd" id="1_database"] [ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="2_type"] @@ -13,6 +13,8 @@ [ext_resource type="Resource" path="res://resources/items/affixes/critical_chance_percent.tres" id="11_affix"] [ext_resource type="Resource" path="res://resources/items/affixes/critical_damage_percent.tres" id="12_affix"] +[ext_resource type="Resource" path="res://resources/items/affixes/missile_cooldown.tres" id="13_affix"] + [resource] script = ExtResource("1_database") -affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix")]) +affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix")]) diff --git a/resources/items/affixes/missile_cooldown.tres b/resources/items/affixes/missile_cooldown.tres new file mode 100644 index 0000000..118e494 --- /dev/null +++ b/resources/items/affixes/missile_cooldown.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"] +[resource] +script = ExtResource("1_affix") +affix_id = "missile_cooldown" +display_name = "Skrócenie cooldownu rakiet" +stat_key = "missile_cooldown_reduction" +unit = "s" +category_mask = 63 +minimum_value_level_1 = 0.1 +maximum_value_level_1 = 0.15 +minimum_value_level_1000 = 1.0 +maximum_value_level_1000 = 2.0 diff --git a/resources/items/engine_extension.tres b/resources/items/engine_extension.tres index b96a475..c7735eb 100644 --- a/resources/items/engine_extension.tres +++ b/resources/items/engine_extension.tres @@ -5,8 +5,8 @@ [resource] script = ExtResource("1_definition") item_id = "engine_extension" -display_name = "Moduł silnika" -description = "Pasywny moduł zwiększający prędkość maksymalną." +display_name = "Ulepszony silnik" +description = "Pasywny moduł prędkości. Na poziomie 1 bonus wynosi 1–5 px/s i rośnie wraz z poziomem przedmiotu." primary_stat = "max_speed" -primary_value = 40.0 +primary_value = 3.0 stat_suffix = " px/s" diff --git a/resources/items/missile_launcher.tres b/resources/items/missile_launcher.tres index 7fbbc83..64a6004 100644 --- a/resources/items/missile_launcher.tres +++ b/resources/items/missile_launcher.tres @@ -5,7 +5,7 @@ [resource] script = ExtResource("1_definition") item_id = "missile_launcher" -display_name = "Wyrzutnia rakiet" +display_name = "Wyrzutnia rakiet naprowadzanych" category = 0 description = "Wystrzeliwuje naprowadzaną rakietę z eksplozją obszarową. Cooldown: 5 s." primary_stat = "damage" @@ -16,7 +16,7 @@ fire_interval = 5.0 weapon_cooldown = 5.0 projectile_speed = 400.0 weapon_range = 1000.0 -energy_cost = 25.0 +energy_cost = 0.0 damage_lower_roll_min = 45 damage_lower_roll_max = 58 damage_spread_roll_min = 5 diff --git a/resources/items/tuned_thrusters.tres b/resources/items/tuned_thrusters.tres deleted file mode 100644 index 28f2372..0000000 --- a/resources/items/tuned_thrusters.tres +++ /dev/null @@ -1,13 +0,0 @@ -[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3] - -[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1_definition"] - -[resource] -script = ExtResource("1_definition") -item_id = "tuned_thrusters" -display_name = "Dostrojone silniki" -category = 4 -description = "Ulepszone dysze zwiększające prędkość maksymalną statku o 20 px/s." -primary_stat = "max_speed" -primary_value = 20.0 -stat_suffix = " px/s" diff --git a/resources/items/unguided_missile.tres b/resources/items/unguided_missile.tres new file mode 100644 index 0000000..fded306 --- /dev/null +++ b/resources/items/unguided_missile.tres @@ -0,0 +1,22 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1_definition"] +[resource] +script = ExtResource("1_definition") +item_id = "unguided_missile" +display_name = "Wyrzutnia rakiet niekierowanych" +category = 0 +description = "Wystrzeliwuje rakietę prosto w kierunku celownika. Większe obrażenia i eksplozja obszarowa. Zużywa amunicję rakietową." +primary_stat = "damage" +primary_value = 110.0 +stat_suffix = " DMG" +weapon_kind = 3 +missile_homing = false +fire_interval = 5.0 +weapon_cooldown = 5.0 +projectile_speed = 480.0 +weapon_range = 1000.0 +energy_cost = 0.0 +damage_lower_roll_min = 90 +damage_lower_roll_max = 116 +damage_spread_roll_min = 10 +damage_spread_roll_max = 24 diff --git a/scenes/ui/ship_equipment_panel.tscn b/scenes/ui/ship_equipment_panel.tscn index ce7d425..ba58e17 100644 --- a/scenes/ui/ship_equipment_panel.tscn +++ b/scenes/ui/ship_equipment_panel.tscn @@ -77,6 +77,16 @@ slot_kind = "weapon" slot_index = 1 overlay_slot = true +[node name="Missile" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] +layout_mode = 0 +offset_left = 108.0 +offset_top = 260.0 +offset_right = 198.0 +offset_bottom = 304.0 +slot_kind = "weapon" +slot_index = 2 +overlay_slot = true + [node name="Module1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] layout_mode = 0 offset_left = 0.0 diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index 7605c01..d1e3006 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -74,7 +74,7 @@ func _process(_delta: float) -> void: 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 +LPM / PPM: broń 1 / broń 2 · ŚPM: rakieta 1–6: aktywne rozszerzenia UKŁAD: %d · PLANETY: %d diff --git a/scripts/game/weapon_controller.gd b/scripts/game/weapon_controller.gd index 4a26455..c9f4633 100644 --- a/scripts/game/weapon_controller.gd +++ b/scripts/game/weapon_controller.gd @@ -14,10 +14,14 @@ var _laser_lines: Array[Line2D] = [] var _fire_buttons_down := [false, false] var _fire_just_pressed := [false, false] var _drag_fire_blocked := false +var missile_cooldown_remaining := 0.0 +var missile_cooldown_duration := 5.0 +var _missile_just_pressed := false func _ready() -> void: _ensure_input_action(&"ship_fire_primary", MOUSE_BUTTON_LEFT) _ensure_input_action(&"ship_fire_secondary", MOUSE_BUTTON_RIGHT) + _ensure_input_action(&"ship_fire_missile", MOUSE_BUTTON_MIDDLE) for slot_index in 2: var beam := Line2D.new() beam.width = 4.0 @@ -41,6 +45,9 @@ func _unhandled_input(event: InputEvent) -> void: if ship.get_viewport().gui_is_dragging() or _drag_fire_blocked: return var slot_index := -1 + if event.button_index == MOUSE_BUTTON_MIDDLE: + _missile_just_pressed = event.pressed + return if event.button_index == MOUSE_BUTTON_LEFT: slot_index = 0 elif event.button_index == MOUSE_BUTTON_RIGHT: @@ -52,9 +59,11 @@ func _unhandled_input(event: InputEvent) -> void: _fire_just_pressed[slot_index] = true func _physics_process(delta: float) -> void: + missile_cooldown_remaining = maxf(0.0, missile_cooldown_remaining - delta) if ship.get_viewport().gui_is_dragging(): _drag_fire_blocked = true if _drag_fire_blocked: + _missile_just_pressed = false for index in 2: _fire_buttons_down[index] = false _fire_just_pressed[index] = false @@ -66,10 +75,14 @@ func _physics_process(delta: float) -> void: _drag_fire_blocked = false return if bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")): + _missile_just_pressed = false for line in _laser_lines: line.visible = false return var mouse_over_ui := ship.get_viewport().gui_get_hovered_control() != null + if not mouse_over_ui and (Input.is_action_just_pressed("ship_fire_missile") or _missile_just_pressed): + try_fire_missile() + _missile_just_pressed = false for slot_index in 2: _shot_timers[slot_index] = maxf(0.0, _shot_timers[slot_index] - delta) _laser_pulse_timers[slot_index] = maxf(0.0, _laser_pulse_timers[slot_index] - delta) @@ -99,13 +112,7 @@ func _physics_process(delta: float) -> void: _laser_lines[slot_index].visible = _laser_pulse_timers[slot_index] > 0.0 _fire_laser(slot_index, item) ItemDefinition.WeaponKind.MISSILE: - if ( - is_just_pressed - and _shot_timers[slot_index] <= 0.0 - and _consume_energy(item.get_energy_cost()) - ): - _fire_projectile(item) - _shot_timers[slot_index] = definition.weapon_cooldown + pass # Rockets fire exclusively from slot 3. _: if _shot_timers[slot_index] <= 0.0 and _consume_shot_resource(item): _fire_projectile(item) @@ -113,6 +120,20 @@ func _physics_process(delta: float) -> void: for slot_index in 2: _fire_just_pressed[slot_index] = false +func try_fire_missile() -> bool: + if missile_cooldown_remaining > 0.0 or bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")): + return false + var item := inventory.get_item("weapon", ShipInventory.MISSILE_SLOT) + if item == null or item.definition.weapon_kind != ItemDefinition.WeaponKind.MISSILE: + return false + if inventory.missile_ammunition <= 0: + return false + missile_cooldown_duration = inventory.get_missile_cooldown(item) + _fire_projectile(item) + missile_cooldown_remaining = missile_cooldown_duration + inventory.consume_missile() + return true + func _fire_projectile(item: ItemInstance) -> void: var definition := item.definition var hit: Dictionary = ship.call("roll_weapon_hit", item) @@ -131,6 +152,7 @@ func _fire_projectile(item: ItemInstance) -> void: bool(hit["critical"]) ) ship.get_parent().add_child(projectile) + projectile.set("missile_homing", definition.missile_homing) WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind) func _fire_laser(slot_index: int, item: ItemInstance) -> void: diff --git a/scripts/game/weapon_projectile.gd b/scripts/game/weapon_projectile.gd index 0ce365d..13f94e5 100644 --- a/scripts/game/weapon_projectile.gd +++ b/scripts/game/weapon_projectile.gd @@ -11,6 +11,7 @@ var is_critical := false var max_range := 800.0 var weapon_kind := ItemDefinition.WeaponKind.PLASMA var homing_target: Node2D +var missile_homing := true var homing_turn_acceleration := 2.3 var homing_max_turn_speed := 1.7 var missile_steering_acceleration := 520.0 @@ -58,7 +59,7 @@ func configure( func _physics_process(delta: float) -> void: var travel_velocity := direction * projectile_speed - if weapon_kind == ItemDefinition.WeaponKind.MISSILE: + if weapon_kind == ItemDefinition.WeaponKind.MISSILE and missile_homing: _flight_time += delta if not is_instance_valid(homing_target): _has_locked_target = false diff --git a/scripts/items/item_affix_definition.gd b/scripts/items/item_affix_definition.gd index a369a03..6dadc48 100644 --- a/scripts/items/item_affix_definition.gd +++ b/scripts/items/item_affix_definition.gd @@ -5,7 +5,7 @@ class_name ItemAffixDefinition @export var display_name: String = "" @export_multiline var description: String = "" @export var stat_key: String = "" -@export_enum("%", "pkt", "/s", "px/s") var unit: String = "%" +@export_enum("%", "pkt", "/s", "px/s", "s") var unit: String = "%" @export var enabled: bool = true @export_range(0.0, 1000.0, 0.1) var selection_weight: float = 1.0 diff --git a/scripts/items/item_definition.gd b/scripts/items/item_definition.gd index 008534a..e20f1e6 100644 --- a/scripts/items/item_definition.gd +++ b/scripts/items/item_definition.gd @@ -15,6 +15,7 @@ enum WeaponKind { PLASMA, MACHINE_GUN, LASER, MISSILE } @export var weapon_kind: WeaponKind = WeaponKind.PLASMA @export var fire_interval: float = 0.25 @export var weapon_cooldown: float = 0.0 +@export var missile_homing: bool = true @export var laser_pulse_duration: float = 0.08 @export var projectile_speed: float = 0.0 @export var weapon_range: float = 0.0 diff --git a/scripts/items/item_generator.gd b/scripts/items/item_generator.gd index bda1283..5c4deb7 100644 --- a/scripts/items/item_generator.gd +++ b/scripts/items/item_generator.gd @@ -5,11 +5,11 @@ const DEFINITIONS := [ preload("res://resources/items/beam_laser.tres"), preload("res://resources/items/machine_gun.tres"), preload("res://resources/items/missile_launcher.tres"), + preload("res://resources/items/unguided_missile.tres"), preload("res://resources/items/reinforced_shield.tres"), preload("res://resources/items/hull_plating.tres"), preload("res://resources/items/compact_generator.tres"), preload("res://resources/items/engine_extension.tres"), - preload("res://resources/items/tuned_thrusters.tres"), preload("res://resources/items/shield_recharger.tres"), preload("res://resources/items/afterburner.tres"), preload("res://resources/items/shield_charge.tres"), @@ -26,6 +26,8 @@ static func generate_for_definition(definition: ItemDefinition, level: int, enem item.item_level = clampi(level, 1, 1000) var scale_factor := 1.0 + 0.05 * (item.item_level - 1) item.primary_value_override = item.definition.primary_value * scale_factor * randf_range(0.8, 1.2) + if definition.primary_stat == "max_speed": + item.primary_value_override = randf_range(1.0, 5.0) * scale_factor if definition.category == ItemDefinition.Category.WEAPON: var lower := randi_range(mini(definition.damage_lower_roll_min, definition.damage_lower_roll_max), maxi(definition.damage_lower_roll_min, definition.damage_lower_roll_max)) var spread := randi_range(mini(definition.damage_spread_roll_min, definition.damage_spread_roll_max), maxi(definition.damage_spread_roll_min, definition.damage_spread_roll_max)) diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index 3f7905f..d0dc3a6 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -55,7 +55,7 @@ func roll_damage() -> float: func get_energy_cost() -> float: if definition == null or definition.category != ItemDefinition.Category.WEAPON: return 0.0 - if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + if definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.MISSILE]: return 0.0 # Compare this weapon with its starter damage. Captain/global gear buffs are free. var base_lower := roundf((definition.damage_lower_roll_min + definition.damage_lower_roll_max) * 0.5) @@ -115,6 +115,8 @@ func get_tooltip_text() -> String: lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.") if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: lines.append("Zasięg: %.0f px • Amunicja: 1 / strzał" % definition.weapon_range) + elif definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: + lines.append("ŚPM • Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii") else: lines.append("Zasięg: %.0f px • Energia: %.1f / strzał" % [definition.weapon_range, get_energy_cost()]) lines.append("Odstęp strzałów: %.2f s" % maxf(definition.fire_interval, definition.weapon_cooldown)) diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index bf1a8eb..14eba06 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -5,7 +5,12 @@ signal inventory_changed signal ammunition_changed const CARGO_SIZE := 20 -const WEAPON_SLOT_COUNT := 2 +const WEAPON_SLOT_COUNT := 3 +const MISSILE_SLOT := 2 +const STARTING_MISSILE_AMMUNITION := 2 +const MISSILE_CRAFT_PARTS_COST := 50.0 +const MISSILE_BUY_BASE_COST := 10 +const MISSILE_DEFINITIONS := [preload("res://resources/items/missile_launcher.tres"), preload("res://resources/items/unguided_missile.tres")] const MODULE_SLOT_COUNT := 6 const STARTING_AMMUNITION := 300 const AMMUNITION_CRAFT_AMOUNT := 100 @@ -14,6 +19,7 @@ const AMMUNITION_BUY_AMOUNT := 100 const AMMUNITION_BUY_BASE_COST := 20 var ammunition: int = STARTING_AMMUNITION +var missile_ammunition: int = STARTING_MISSILE_AMMUNITION var cargo: Array[ItemInstance] = [] var weapon_slots: Array[ItemInstance] = [] @@ -31,8 +37,10 @@ func _ready() -> void: else: cargo.assign(GameState.restart_inventory["cargo"]) weapon_slots.assign(GameState.restart_inventory["weapon"]) + weapon_slots.resize(WEAPON_SLOT_COUNT) module_slots.assign(GameState.restart_inventory["module"]) ammunition = int(GameState.restart_inventory.get("ammunition", STARTING_AMMUNITION)) + missile_ammunition = int(GameState.restart_inventory.get("missile_ammunition", STARTING_MISSILE_AMMUNITION)) GameState.restart_inventory.clear() _recalculate_ship_stats() inventory_changed.emit() @@ -51,6 +59,7 @@ func preserve_for_restart() -> void: "weapon": weapon_slots.duplicate(), "module": module_slots.duplicate(), "ammunition": ammunition, + "missile_ammunition": missile_ammunition, } func consume_ammunition() -> bool: @@ -122,18 +131,63 @@ func can_move_item(from_kind: String, from_index: int, to_kind: String, to_index var item := source[from_index] if item == null or item.definition == null: return false - if to_kind == "weapon" and item.definition.category != ItemDefinition.Category.WEAPON: - return false - if to_kind == "module" and item.definition.category == ItemDefinition.Category.WEAPON: + if not _accepts_item(to_kind, to_index, item): return false var displaced := destination[to_index] if displaced != null and displaced.definition != null: - if from_kind == "weapon" and displaced.definition.category != ItemDefinition.Category.WEAPON: - return false - if from_kind == "module" and displaced.definition.category == ItemDefinition.Category.WEAPON: + if not _accepts_item(from_kind, from_index, displaced): return false return true +func _accepts_item(kind: String, index: int, item: ItemInstance) -> bool: + if kind == "weapon": + return item.definition.category == ItemDefinition.Category.WEAPON and (item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE) == (index == MISSILE_SLOT) + return kind != "module" or item.definition.category != ItemDefinition.Category.WEAPON + +func get_missile_count() -> int: + return missile_ammunition + +func get_missile_cooldown(item: ItemInstance) -> float: + var reduction := item.get_stat_total("missile_cooldown_reduction") + for equipped in weapon_slots + module_slots: + if equipped != null and equipped != item: + reduction += equipped.get_stat_total("missile_cooldown_reduction") + return maxf(1.0, 5.0 - reduction) + +func consume_missile() -> bool: + if missile_ammunition <= 0: + return false + missile_ammunition -= 1 + ammunition_changed.emit() + return true + +func can_craft_missile() -> bool: + return GameState.captain.parts >= MISSILE_CRAFT_PARTS_COST + +func get_missile_buy_cost() -> int: + return MISSILE_BUY_BASE_COST * maxi(1, GameState.system_level) + +func can_buy_missile() -> bool: + return GameState.captain.credits >= get_missile_buy_cost() + +func craft_missile() -> bool: + if not can_craft_missile(): + return false + GameState.captain.parts -= MISSILE_CRAFT_PARTS_COST + missile_ammunition += 1 + GameState.captain.emit_changed() + ammunition_changed.emit() + return true + +func buy_missile() -> bool: + if not can_buy_missile(): + return false + GameState.captain.credits -= get_missile_buy_cost() + missile_ammunition += 1 + GameState.captain.emit_changed() + ammunition_changed.emit() + return true + func move_item(from_kind: String, from_index: int, to_kind: String, to_index: int) -> bool: if not can_move_item(from_kind, from_index, to_kind, to_index): return false diff --git a/scripts/ui/equipment_slot_view.gd b/scripts/ui/equipment_slot_view.gd index cac39ac..f945717 100644 --- a/scripts/ui/equipment_slot_view.gd +++ b/scripts/ui/equipment_slot_view.gd @@ -32,6 +32,7 @@ func _ready() -> void: _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.inventory_changed.connect(_refresh) + _inventory.ammunition_changed.connect(_refresh) match slot_kind: "weapon": custom_minimum_size = Vector2(90, 38) @@ -52,7 +53,7 @@ func _ready() -> void: slot_label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING var binding := Label.new() binding.mouse_filter = Control.MOUSE_FILTER_IGNORE - binding.text = ("LPM" if slot_index == 0 else "PPM") if slot_kind == "weapon" else str(slot_index + 1) + binding.text = (["LPM", "PPM", "ŚPM"][slot_index]) if slot_kind == "weapon" else str(slot_index + 1) binding.position = Vector2(2, -14) binding.add_theme_font_size_override("font_size", 9) binding.add_theme_color_override("font_color", Color("a0b2c4")) @@ -63,17 +64,33 @@ func _ready() -> void: marker.add_child(binding) _refresh() +func _process(_delta: float) -> void: + if slot_kind == "weapon" and slot_index == ShipInventory.MISSILE_SLOT: + queue_redraw() + +func _draw() -> void: + if slot_kind != "weapon" or slot_index != ShipInventory.MISSILE_SLOT or _inventory == null: + return + var weapons := _inventory.ship.get_node("WeaponController") + var remaining := float(weapons.get("missile_cooldown_remaining")) + var duration := float(weapons.get("missile_cooldown_duration")) + if remaining > 0.0: + var height := (size.y - 4.0) * clampf(remaining / duration, 0.0, 1.0) + draw_rect(Rect2(2.0, size.y - 2.0 - height, size.x - 4.0, height), Color(1.0, 0.5, 0.1, 0.45)) + func _refresh() -> void: if not is_node_ready() or not _inventory: return var item: ItemInstance = _inventory.get_item(slot_kind, slot_index) _apply_slot_style(item) - tooltip_text = item.get_tooltip_text() if item != null else "" + tooltip_text = item.get_tooltip_text() if item != null else ("WYRZUTNIA [ŚPM] — przeciągnij wyrzutnię z magazynu." if slot_kind == "weapon" and slot_index == ShipInventory.MISSILE_SLOT else "") + if item != null and slot_kind == "weapon" and slot_index == ShipInventory.MISSILE_SLOT: + tooltip_text += "\nCooldown po addonach: %.2f s\nZapas rakiet: %d" % [_inventory.get_missile_cooldown(item), _inventory.get_missile_count()] if _delete_button: _delete_button.visible = item != null if item == null or item.definition == null: slot_label.add_theme_color_override("font_color", Color("82909e")) - slot_label.text = "" + slot_label.text = "RAKIETY" if slot_kind == "weapon" and slot_index == ShipInventory.MISSILE_SLOT else "" return var definition := item.definition slot_label.add_theme_color_override("font_color", Color("f1f4f8")) diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd index 67aaf67..241e0e4 100644 --- a/scripts/ui/planet_services_menu.gd +++ b/scripts/ui/planet_services_menu.gd @@ -12,14 +12,27 @@ signal next_level_requested var _inventory: ShipInventory var planet: Node2D var _open_tween: Tween +var _missile_button: Button func _ready() -> void: next_button.pressed.connect(func() -> void: next_level_requested.emit()) buy_ammunition_button.pressed.connect(_buy_ammunition) reset_skills_button.pressed.connect(_reset_skills) + _missile_button = Button.new() + _missile_button.custom_minimum_size.y = 36 + _missile_button.add_theme_font_size_override("font_size", 12) + _missile_button.focus_mode = Control.FOCUS_NONE + _missile_button.tooltip_text = "Jedna sztuka amunicji rakietowej do dowolnej wyrzutni." + _missile_button.pressed.connect(func() -> void: + if visible and _inventory: + _inventory.buy_missile() + ) + $Center/Panel/Contents.add_child(_missile_button) + $Center/Panel/Contents.move_child(_missile_button, reset_skills_button.get_index()) _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.ammunition_changed.connect(_refresh_ammunition_service) + _inventory.inventory_changed.connect(_refresh_ammunition_service) GameState.captain.changed.connect(_refresh_ammunition_service) _refresh_ammunition_service() @@ -61,6 +74,8 @@ func _position_at_planet() -> void: position = Vector2(clampf(target.x, 8.0, maxf(8.0, screen_right - size.x)), clampf(target.y, 8.0, maxf(8.0, viewport_size.y - size.y - 80.0))) func _refresh_ammunition_service() -> void: + _missile_button.disabled = _inventory == null or not _inventory.can_buy_missile() + _missile_button.text = "KUP +1 RAKIETĘ — %d KREDYTÓW" % (_inventory.get_missile_buy_cost() if _inventory else ShipInventory.MISSILE_BUY_BASE_COST) reset_skills_button.disabled = not GameState.captain.can_reset_skills() reset_skills_button.text = "RESET SKILLI — %d KREDYTÓW" % GameState.captain.get_skill_reset_cost() reset_skills_button.tooltip_text = "Zwraca wszystkie %d wydanych punktów nauki. Koszt: 100 kredytów × poziom kapitana." % GameState.captain.get_spent_learning_points() diff --git a/scripts/ui/ship_inventory_panel.gd b/scripts/ui/ship_inventory_panel.gd index bc60fd0..53fbef0 100644 --- a/scripts/ui/ship_inventory_panel.gd +++ b/scripts/ui/ship_inventory_panel.gd @@ -12,12 +12,23 @@ const CARGO_SIZE := 20 var _inventory: ShipInventory var _drawer_open := false var _slide_tween: Tween +var _missile_button: Button var drawer_width := 780.0 func _ready() -> void: close_button.pressed.connect(close_drawer) craft_button.pressed.connect(_craft_ammunition) craft_button.text = "TWÓRZ +%d AMUNICJI (%.0f części)" % [ShipInventory.AMMUNITION_CRAFT_AMOUNT, ShipInventory.AMMUNITION_CRAFT_PARTS_COST] + var missile_row := HBoxContainer.new() + $Contents.add_child(missile_row) + $Contents.move_child(missile_row, $Contents/CargoGrid.get_index()) + _missile_button = Button.new() + _missile_button.text = "TWÓRZ +1 RAKIETĘ (%.0f części)" % ShipInventory.MISSILE_CRAFT_PARTS_COST + _missile_button.tooltip_text = "Amunicja do obu typów wyrzutni rakiet. Wyrzutnie zdobywasz jako loot." + _missile_button.add_theme_font_size_override("font_size", 11) + _missile_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _missile_button.pressed.connect(func() -> void: _inventory.craft_missile()) + missile_row.add_child(_missile_button) _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.inventory_changed.connect(_refresh_title) @@ -32,9 +43,10 @@ func _ready() -> void: func _refresh_title() -> void: craft_button.disabled = _inventory == null or not _inventory.can_craft_ammunition() + _missile_button.disabled = _inventory == null or not _inventory.can_craft_missile() if _inventory: title_label.text = "MAGAZYN (%d/%d) · Części: %.1f" % [_inventory.get_cargo_used(), CARGO_SIZE, GameState.captain.parts] - ammunition_label.text = "AMUNICJA: %d" % _inventory.ammunition + ammunition_label.text = "AMUNICJA: %d · RAKIETY: %d" % [_inventory.ammunition, _inventory.get_missile_count()] func _craft_ammunition() -> void: if _inventory: diff --git a/tests/abilities/missile_system_test.gd b/tests/abilities/missile_system_test.gd new file mode 100644 index 0000000..f834a26 --- /dev/null +++ b/tests/abilities/missile_system_test.gd @@ -0,0 +1,107 @@ +extends Node2D + +const PLAYER := preload("res://scenes/game/player_ship.tscn") +const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") + +func _ready() -> void: + GameState.start_new_run() + var ship := PLAYER.instantiate() as CharacterBody2D + add_child(ship) + ship.set_physics_process(false) + var weapons := ship.get_node("WeaponController") + weapons.set_physics_process(false) + var inventory := ship.get_node("ShipInventory") as ShipInventory + assert(inventory.weapon_slots.size() == 3) + assert(inventory.get_missile_count() == 2 and inventory.get_cargo_used() == 0) + var launcher := ItemInstance.new() + launcher.definition = ShipInventory.MISSILE_DEFINITIONS[0] + assert(inventory.add_item(launcher)) + assert(not inventory.move_item("cargo", 0, "weapon", 0)) + assert(not inventory.move_item("weapon", 0, "weapon", 2)) + assert(inventory.move_item("cargo", 0, "weapon", 2)) + assert(not inventory.move_item("weapon", 2, "weapon", 0)) + assert(not inventory.move_item("weapon", 2, "module", 0)) + var energy_before := float(ship.get("energy")) + var missile := inventory.weapon_slots[2] + assert(missile.get_energy_cost() == 0.0) + var addon := ItemStatModifier.new() + addon.stat_key = "missile_cooldown_reduction" + addon.value = 0.1 + missile.additional_modifiers.append(addon) + assert(is_equal_approx(inventory.get_missile_cooldown(missile), 4.9)) + var middle := InputEventMouseButton.new() + middle.button_index = MOUSE_BUTTON_MIDDLE + middle.pressed = true + assert(InputMap.action_has_event("ship_fire_missile", middle)) + weapons.call("_unhandled_input", middle) + weapons.call("_physics_process", 0.1) + assert(ship.get("energy") == energy_before and inventory.get_missile_count() == 1) + assert(inventory.weapon_slots[2] == missile and missile.additional_modifiers[0] == addon) + assert(is_equal_approx(weapons.get("missile_cooldown_remaining"), 4.9)) + assert(not weapons.call("try_fire_missile")) + weapons.call("_physics_process", 2.0) + assert(is_equal_approx(weapons.get("missile_cooldown_remaining"), 2.9)) + weapons.call("_physics_process", 3.0) + assert(weapons.call("try_fire_missile")) + assert(inventory.get_missile_count() == 0 and inventory.weapon_slots[2] == missile) + weapons.set("missile_cooldown_remaining", 0.0) + assert(not weapons.call("try_fire_missile")) + assert(inventory.weapon_slots[2] == missile and ship.get("energy") == energy_before) + GameState.captain.parts = 49.0 + assert(not inventory.craft_missile()) + GameState.captain.parts = 50.0 + assert(inventory.craft_missile() and GameState.captain.parts == 0.0) + var unguided := ItemInstance.new() + unguided.definition = ShipInventory.MISSILE_DEFINITIONS[1] + assert(inventory.add_item(unguided)) + assert(not unguided.definition.missile_homing) + assert(unguided.get_damage_range().x > missile.get_damage_range().y) + assert(inventory.move_item("cargo", 0, "weapon", 2)) + weapons.set("missile_cooldown_remaining", 0.0) + assert(weapons.call("try_fire_missile")) + var found_unguided := false + for child in get_children(): + if child.scene_file_path == "res://scenes/game/weapon_projectile.tscn" and not child.get("missile_homing"): + found_unguided = true + assert(found_unguided) + var target := Node2D.new() + target.position = Vector2(0, 100) + add_child(target) + var straight := PROJECTILE.instantiate() as Area2D + straight.call("configure", Vector2.ZERO, Vector2.RIGHT, ItemDefinition.WeaponKind.MISSILE, 480.0, 100.0, 1000.0, target) + straight.set("missile_homing", false) + straight.collision_mask = 0 + add_child(straight) + straight.set_physics_process(false) + straight.call("_physics_process", 0.5) + assert(straight.position.is_equal_approx(Vector2(240, 0))) + GameState.system_level = 3 + GameState.captain.credits = 29 + assert(not inventory.buy_missile()) + GameState.captain.credits = 30 + assert(inventory.buy_missile() and GameState.captain.credits == 0) + GameState.captain.parts = 100.0 + GameState.captain.credits = 100 + for index in inventory.cargo.size(): + if inventory.cargo[index] == null: + inventory.cargo[index] = missile + var ammo_before := inventory.missile_ammunition + assert(inventory.craft_missile() and GameState.captain.parts == 50.0) + assert(inventory.buy_missile() and GameState.captain.credits == 70) + assert(inventory.missile_ammunition == ammo_before + 2) + addon.value = 100.0 + assert(inventory.get_missile_cooldown(missile) == 1.0) + var affix := load("res://resources/items/affixes/missile_cooldown.tres") as ItemAffixDefinition + assert(affix.can_roll(ShipInventory.MISSILE_DEFINITIONS[0], 1)) + assert(affix.roll_value(1) >= 0.1 and affix.roll_value(1000) >= 1.0) + inventory.preserve_for_restart() + assert(GameState.restart_inventory["weapon"].size() == 3) + var restored := PLAYER.instantiate() as CharacterBody2D + add_child(restored) + restored.set_physics_process(false) + restored.get_node("WeaponController").set_physics_process(false) + var restored_inventory := restored.get_node("ShipInventory") as ShipInventory + assert(restored_inventory.missile_ammunition == inventory.missile_ammunition) + assert(restored_inventory.weapon_slots[2] == unguided) + print("MISSILE_SYSTEM_TEST: PASS (slots, permanent launchers, separate ammo, cooldown, no energy, variants, crafting, purchases, affix, restart)") + get_tree().quit() diff --git a/tests/abilities/missile_system_test.gd.uid b/tests/abilities/missile_system_test.gd.uid new file mode 100644 index 0000000..ba8ce71 --- /dev/null +++ b/tests/abilities/missile_system_test.gd.uid @@ -0,0 +1 @@ +uid://bui0803ewdmjn diff --git a/tests/abilities/missile_system_test.tscn b/tests/abilities/missile_system_test.tscn new file mode 100644 index 0000000..1653b55 --- /dev/null +++ b/tests/abilities/missile_system_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/abilities/missile_system_test.gd" id="1"] +[node name="MissileSystemTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/weapon_damage_test.gd b/tests/items/weapon_damage_test.gd index 9dc6d86..69c115b 100644 --- a/tests/items/weapon_damage_test.gd +++ b/tests/items/weapon_damage_test.gd @@ -32,7 +32,7 @@ func _ready() -> void: addon.stat_key = "damage_percent" addon.value = 10.0 stronger.additional_modifiers.append(addon) - if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + if definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.MISSILE]: assert(stronger.get_energy_cost() == 0.0) else: assert(stronger.get_energy_cost() > cost_before_addon) @@ -92,13 +92,14 @@ func _ready() -> void: assert(inventory.craft_ammunition()) assert(inventory.ammunition == 100 and GameState.captain.parts == 0.0) GameState.system_level = 3 - assert(inventory.get_ammunition_buy_cost() == 300) - GameState.captain.credits = 299 + var buy_cost := ShipInventory.AMMUNITION_BUY_BASE_COST * 3 + assert(inventory.get_ammunition_buy_cost() == buy_cost) + GameState.captain.credits = buy_cost - 1 assert(not inventory.buy_ammunition()) - assert(inventory.ammunition == 100 and GameState.captain.credits == 299) - GameState.captain.credits = 600 + assert(inventory.ammunition == 100 and GameState.captain.credits == buy_cost - 1) + GameState.captain.credits = buy_cost * 2 assert(inventory.buy_ammunition()) - assert(inventory.ammunition == 200 and GameState.captain.credits == 300) + assert(inventory.ammunition == 200 and GameState.captain.credits == buy_cost) assert(inventory.buy_ammunition()) assert(inventory.ammunition == 300 and GameState.captain.credits == 0) assert(not inventory.buy_ammunition())