extends PanelContainer @export var slot_kind: String = "cargo" @export var slot_index: int = 0 @export var overlay_slot: bool = false @onready var slot_label: Label = $SlotLabel var _inventory: ShipInventory var _icon: TextureRect var _delete_button: Button var _drop_highlight := false func _ready() -> void: if slot_kind == "cargo": var overlay := Control.new() overlay.name = "ActionOverlay" overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(overlay) _delete_button = Button.new() _delete_button.name = "SalvageButton" _delete_button.icon = preload("res://assets/icons/screwdriver.png") _delete_button.expand_icon = true _delete_button.add_theme_constant_override("icon_max_width", 18) _delete_button.z_index = 5 _delete_button.tooltip_text = "Rozłóż przedmiot: +10 części" _delete_button.focus_mode = Control.FOCUS_NONE var tool_style := StyleBoxFlat.new() tool_style.bg_color = Color(0.04, 0.07, 0.1, 0.72) tool_style.border_color = Color("b68c53") tool_style.set_border_width_all(1) tool_style.set_corner_radius_all(3) tool_style.set_content_margin_all(2) _delete_button.add_theme_stylebox_override("normal", tool_style) _delete_button.add_theme_stylebox_override("hover", tool_style) _delete_button.add_theme_stylebox_override("pressed", tool_style) overlay.add_child(_delete_button) _delete_button.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) _delete_button.offset_left = -24 _delete_button.offset_right = -2 _delete_button.offset_top = 2 _delete_button.offset_bottom = 24 _delete_button.custom_minimum_size = Vector2(22, 22) _delete_button.hide() _delete_button.pressed.connect(_delete_item) _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.inventory_changed.connect(_refresh) _inventory.ammunition_changed.connect(_refresh) custom_minimum_size = Vector2(72, 72) if slot_kind == "cargo" else Vector2(56, 56) slot_label.custom_minimum_size = Vector2.ZERO slot_label.add_theme_font_size_override("font_size", 9) _icon = TextureRect.new() _icon.name = "ItemIcon" _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_icon) if overlay_slot and slot_kind == "weapon": var binding := Label.new() binding.mouse_filter = Control.MOUSE_FILTER_IGNORE binding.text = ["LPM", "PPM", "ŚPM"][slot_index] binding.position = Vector2(2, -24) binding.add_theme_font_size_override("font_size", 9) binding.add_theme_color_override("font_color", Color("a0b2c4")) # Use a plain overlay to keep the binding outside the slot's content layout. var marker := Control.new() marker.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(marker) marker.add_child(binding) _refresh() func _process(_delta: float) -> void: var valid_drop := get_viewport().gui_is_dragging() and _can_drop_data(Vector2.ZERO, get_viewport().gui_get_drag_data()) if valid_drop != _drop_highlight: _drop_highlight = valid_drop _apply_slot_style(_inventory.get_item(slot_kind, slot_index) if _inventory != null else null) _update_salvage_visibility() if slot_kind == "weapon" and _has_missile(): queue_redraw() func _update_salvage_visibility() -> void: if _delete_button == null or _inventory == null: return var hovered_control := get_viewport().gui_get_hovered_control() var hovered := hovered_control == self or (hovered_control != null and is_ancestor_of(hovered_control)) _delete_button.visible = is_visible_in_tree() and hovered and _inventory.get_item(slot_kind, slot_index) != null and not get_viewport().gui_is_dragging() func _draw() -> void: if slot_kind != "weapon" or not _has_missile() 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) if not _inventory.is_slot_unlocked(slot_kind,slot_index): _icon.texture = null slot_label.visible = true slot_label.text = "BLOKADA" slot_label.add_theme_color_override("font_color",Color("9a795b")) tooltip_text = "Slot zablokowany. Odblokowuj kolejne miejsca w hangarze fregaty.\nNastępny slot: " + _inventory.get_slot_unlock_cost_text(slot_kind) _update_salvage_visibility() return _icon.texture = item.definition.icon if item != null and item.definition != null else _empty_icon() _icon.modulate = Color(1, 1, 1, 1.0 if item != null else 0.16) slot_label.visible = _icon.texture == null tooltip_text = item.get_tooltip_text() if item != null else _empty_slot_text() if item != null and slot_kind == "weapon" and _has_missile(): tooltip_text += "\nCooldown po addonach: %.2f s\nZapas rakiet: %d" % [_inventory.get_missile_cooldown(item), _inventory.get_missile_count()] _update_salvage_visibility() if item == null or item.definition == null: slot_label.add_theme_color_override("font_color", Color("82909e")) slot_label.text = "—" return var definition := item.definition slot_label.add_theme_color_override("font_color", Color("f1f4f8")) slot_label.text = definition.display_name func _apply_slot_style(item: ItemInstance) -> void: var style := StyleBoxFlat.new() style.bg_color = Color("101925") style.border_color = Color("354455") style.set_border_width_all(1) style.set_content_margin_all(5) style.set_corner_radius_all(4) if slot_kind == "cargo": style.content_margin_right = 5 if item != null and item.definition != null: style.border_color = item.get_type_color() style.bg_color = item.get_background_color() style.set_border_width_all(2) if overlay_slot: style.bg_color.a = 0.86 if item != null else 0.48 if _drop_highlight: style.border_color = Color("75ffd0") style.set_border_width_all(3) style.bg_color = Color(0.08, 0.3, 0.25, 0.85) style.shadow_color = Color(0.2, 1.0, 0.72, 0.3) style.shadow_size = 5 add_theme_stylebox_override("panel", style) func _delete_item() -> void: if _inventory: _inventory.delete_cargo_item(slot_index) func _make_custom_tooltip(for_text: String) -> Object: var panel := PanelContainer.new() panel.set_script(preload("res://scripts/ui/item_tooltip_frame.gd")) var background := StyleBoxFlat.new() background.bg_color = Color("101925b8") background.border_color = Color("52677e") background.set_border_width_all(1) background.border_width_top = 2 background.set_content_margin_all(12) background.set_corner_radius_all(3) background.shadow_size = 0 panel.add_theme_stylebox_override("panel", background) var label := RichTextLabel.new() label.fit_content = true label.scroll_active = false label.mouse_filter = Control.MOUSE_FILTER_IGNORE label.custom_minimum_size.x = 290 label.size.x = 290 label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.add_theme_font_size_override("normal_font_size", 11) label.add_theme_constant_override("line_separation", -2) label.add_theme_color_override("default_color", Color("c4d2df")) var item := _inventory.get_item(slot_kind, slot_index) if item: # Dark neutral body with an accent tied to the equipment category. background.bg_color = Color("0c1520").lerp(item.get_background_color().darkened(0.8), 0.25) background.bg_color.a = 0.72 background.border_color = item.get_type_color() panel.set("accent", item.get_type_color()) var columns := HBoxContainer.new() columns.add_theme_constant_override("separation", 12) panel.add_child(columns) var details := VBoxContainer.new() details.custom_minimum_size.x = 290 details.size_flags_vertical = Control.SIZE_SHRINK_BEGIN details.add_theme_constant_override("separation", 8) columns.add_child(details) var lines := for_text.split("\n") if item != null: var header_frame := PanelContainer.new() var header_style := StyleBoxFlat.new() header_style.bg_color = Color(item.get_type_color().darkened(0.8), 0.45) header_style.border_color = item.get_type_color().darkened(0.35) header_style.border_width_left = 3 header_style.set_content_margin_all(8) header_frame.add_theme_stylebox_override("panel", header_style) details.add_child(header_frame) var header := HBoxContainer.new() header.add_theme_constant_override("separation", 10) header_frame.add_child(header) if item.definition.icon != null: var icon := TextureRect.new() icon.texture = item.definition.icon icon.custom_minimum_size = Vector2(54, 54) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED header.add_child(icon) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL heading.add_theme_constant_override("separation", 2) header.add_child(heading) var title := _comparison_label(item.definition.display_name, 0, Color("f1f4f8")) title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.add_theme_font_size_override("font_size", 13) heading.add_child(title) heading.add_child(_comparison_label("%s · POZ. %d" % [item.get_rarity_name(), item.item_level], 0, item.get_type_color().lightened(0.25))) heading.add_child(_comparison_label("%s · ADDONY: %d/5" % [_category_name(item.definition.category), item.get_addon_count()], 0, Color("8ba2b7"))) lines = lines.slice(2) while not lines.is_empty() and lines[0].is_empty(): lines.remove_at(0) details.add_child(label) for line in lines: if line == "CECHA GŁÓWNA" or line.contains("ADDONY (aktywne"): label.push_font_size(10) label.push_color(Color("91dbea")) label.add_text("PARAMETRY\n" if line == "CECHA GŁÓWNA" else "ADDONY · PO ZAMONTOWANIU\n") label.pop() label.pop() continue var quality := 0 var is_addon := false if item: for modifier in item.additional_modifiers: if modifier != null and line == modifier.get_display_text(): is_addon = true quality = modifier.get_roll_quality(item.item_level) break var color := Color("ffd166") if quality == 2 else (Color("79e6a0") if quality == 1 else Color("c4d2df")) if line.begins_with("Obrażenia:"): label.push_font_size(13) color = Color("ffd166") if line.begins_with("Typ obrażeń:"): color = [Color("efb568"), Color("91dbea"), Color("a9ef83")][item.get_damage_type()] if item != null else Color("e6f2fc") if line.begins_with("Każdy strzał"): color = Color("8298ad") label.push_color(color) label.add_text(("★ " if quality == 2 else ("• " if is_addon else "")) + line + "\n") label.pop() if line.begins_with("Obrażenia:"): label.pop() if slot_kind == "cargo" and item != null: _add_comparison_column(columns, item) # Wrapped text can become shorter after the first layout pass. panel.minimum_size_changed.connect(func() -> void: panel.reset_size.call_deferred()) panel.reset_size.call_deferred() return panel func _add_comparison_column(columns: HBoxContainer, item: ItemInstance) -> void: if item.definition.category == ItemDefinition.Category.WEAPON: _add_weapon_comparison(columns, item) return var module_index := ShipInventory.MODULE_CATEGORIES.find(item.definition.category) if module_index < 0: return columns.add_child(VSeparator.new()) var comparison := RichTextLabel.new() comparison.name = "EquipmentComparison" comparison.fit_content = true comparison.scroll_active = false comparison.mouse_filter = Control.MOUSE_FILTER_IGNORE comparison.custom_minimum_size.x = 270 comparison.size.x = 270 comparison.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART comparison.add_theme_font_size_override("normal_font_size", 11) comparison.add_theme_constant_override("line_separation", -2) comparison.add_theme_color_override("default_color", Color("c4d2df")) comparison.size_flags_vertical = Control.SIZE_SHRINK_BEGIN columns.add_child(comparison) var equipped := _inventory.get_item("module", module_index) comparison.push_color(Color("91dbea")) comparison.add_text("ZMIANY PO ZAŁOŻENIU\n") comparison.pop() comparison.add_text("Slot: %s\n" % ShipInventory.MODULE_NAMES[module_index]) comparison.add_text("Obecnie: %s\n\n" % ("%s • Poz. %d" % [equipped.definition.display_name, equipped.item_level] if equipped != null else "pusty")) comparison.add_text("Zmiana statystyk statku:\n") var rows := _inventory.get_item_comparison(item, equipped) if rows.is_empty(): comparison.add_text("Bez zmian w bonusach.\n") for row in rows: comparison.push_color(Color("79e6a0") if row["delta"] > 0.0 else Color("ff8989")) var name: String = row["name"] comparison.add_text("%s: %+.2f %s\n" % [name, row["delta"], row["unit"]]) comparison.pop() comparison.push_color(Color("8298ad")) comparison.push_font_size(10) comparison.add_text("\nUwzględnia utratę bonusów zdejmowanego przedmiotu.") comparison.pop() comparison.pop() func _comparison_label(text_value: String, width: float, color: Color = Color("f1f4f8")) -> Label: var label := Label.new() label.text = text_value label.custom_minimum_size.x = width label.add_theme_font_size_override("font_size", 10) label.add_theme_color_override("font_color", color) label.mouse_filter = Control.MOUSE_FILTER_IGNORE return label func _comparison_delta(value: float) -> Label: var label := _comparison_label(("%+.2f" % value).replace(".", ",") if not is_zero_approx(value) else "—", 64, Color("79e6a0") if value > 0.0 else (Color("ff8989") if value < 0.0 else Color("82909e"))) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT return label func _add_weapon_comparison(columns: HBoxContainer, item: ItemInstance) -> void: columns.add_child(VSeparator.new()) var section := VBoxContainer.new() section.name = "WeaponComparison" section.add_theme_constant_override("separation", 6) columns.add_child(section) section.add_child(_comparison_label("ZMIANY PO WYMIANIE BRONI", 0, Color("91dbea"))) var grid := GridContainer.new() grid.columns = 4 grid.add_theme_constant_override("h_separation", 8) grid.add_theme_constant_override("v_separation", 4) section.add_child(grid) grid.add_child(_comparison_label("", 185)) var comparisons: Array[Dictionary] = [] for index in [0, 2, 1]: grid.add_child(_comparison_label("%d · %s" % [index + 1, ["LPM", "PPM", "ŚPM"][index]], 64, Color("91dbea"))) comparisons.append(_inventory.get_weapon_comparison(item, index)) grid.add_child(_comparison_label("DPS broni", 185, Color("ffd166"))) for comparison in comparisons: grid.add_child(_comparison_delta(comparison["dps_delta"])) var keys := {} for comparison in comparisons: for row in comparison["stats"]: keys[row["key"]] = row for key in keys: var row: Dictionary = keys[key] var compact_name: String = {"missile_cooldown_reduction": "Skróc. cooldownu rakiet", "critical_damage_percent": "Obraż. krytyczne", "energy_regeneration": "Regener. energii", "shield_regeneration": "Regener. tarcz", "shield_on_damage_percent": "Odzysk tarcz z obr."}.get(key, row["name"]) var caption: Label = _comparison_label("%s (%s)" % [compact_name, row["unit"]], 185) caption.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART caption.custom_minimum_size.x = 185 caption.size_flags_horizontal = Control.SIZE_FILL grid.add_child(caption) for comparison in comparisons: var delta := 0.0 for change in comparison["stats"]: if change["key"] == key: delta = float(change["delta"]) break grid.add_child(_comparison_delta(delta)) var note := _comparison_label("Średni DPS z krytykami i cooldownem.\nBez ograniczeń energii i amunicji.\nAddony uwzględniają zdjęcie obecnej broni.", 0, Color("a0b2c4")) note.add_theme_font_size_override("font_size", 10) section.add_child(note) func _get_drag_data(_at_position: Vector2) -> Variant: if not _inventory: return null var item: ItemInstance = _inventory.get_item(slot_kind, slot_index) if item == null: return null # Native preview lives in the viewport's base canvas, below HUD CanvasLayers. # Keep its drag lifecycle, but draw the centered icon in a dedicated top layer. var overlay := get_tree().get_first_node_in_group("item_drag_overlay") if overlay == null: overlay = CanvasLayer.new() overlay.name = "ItemDragOverlay" overlay.set_script(preload("res://scripts/ui/item_drag_overlay.gd")) var host := get_tree().current_scene if host == null: host = get_viewport() host.add_child(overlay) overlay.call("begin_drag", item) var native_preview := Control.new() native_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE set_drag_preview(native_preview) return {"from_kind": slot_kind, "from_index": slot_index} func _can_drop_data(_at_position: Vector2, data: Variant) -> bool: return ( _inventory != null and data is Dictionary and _inventory.can_move_item( str(data.get("from_kind", "")), int(data.get("from_index", -1)), slot_kind, slot_index ) ) func _drop_data(_at_position: Vector2, data: Variant) -> void: _inventory.move_item( str(data.get("from_kind", "")), int(data.get("from_index", -1)), slot_kind, slot_index ) func _has_missile() -> bool: if _inventory == null or slot_kind != "weapon": return false var item := _inventory.get_item(slot_kind, slot_index) return item != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE func _empty_slot_text() -> String: if slot_kind == "weapon": return "BROŃ [%s]" % ["LPM", "PPM", "ŚPM"][slot_index] if slot_kind == "module": return ShipInventory.MODULE_NAMES[slot_index].to_upper() if slot_kind == "extension": return "ROZSZ. [%d]" % (slot_index + 1) return "PUSTY" func _category_name(category: int) -> String: match category: ItemDefinition.Category.WEAPON: return "BROŃ" ItemDefinition.Category.SHIELD: return "TARCZA" ItemDefinition.Category.ARMOR: return "PANCERZ" ItemDefinition.Category.REACTOR: return "REAKTOR" ItemDefinition.Category.ENGINE: return "SILNIK" ItemDefinition.Category.COMPUTER: return "KOMPUTER" ItemDefinition.Category.ACTIVE_EXTENSION: return "ROZSZERZENIE" ItemDefinition.Category.CONSUMABLE: return "ZUŻYWALNY" return "PRZEDMIOT" func _stat_text(item: ItemInstance) -> String: var definition := item.definition if definition.category == ItemDefinition.Category.CONSUMABLE: return "[R] +10% energii" if definition.category == ItemDefinition.Category.WEAPON: return "%s · Poz. %d" % [item.get_damage_range_text(), item.item_level] if definition.primary_stat.is_empty(): return "Poz. %d" % item.item_level var total_value := item.get_stat_total(definition.primary_stat) var sign_text := "+" if total_value >= 0.0 else "" return "%s%.0f%s · Poz. %d" % [sign_text, total_value, definition.stat_suffix, item.item_level] func _empty_icon() -> Texture2D: if slot_kind != "module": return null var names := ["shield", "armor", "engine", "reactor", "computer"] return load("res://assets/icons/%s.png" % names[slot_index]) as Texture2D