Randoloot/scripts/ui/equipment_slot_view.gd
2026-10-17 16:20:14 +02:00

435 lines
18 KiB
GDScript

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
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.96)
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(64, 64)
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:
var binding := Label.new()
binding.mouse_filter = Control.MOUSE_FILTER_IGNORE
binding.text = (["LPM", "PPM", "ŚPM"][slot_index]) if slot_kind == "weapon" else (str(slot_index + 1) if slot_kind == "extension" else ShipInventory.MODULE_NAMES[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:
_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)
_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
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("101925")
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.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 = item.get_type_color().darkened(0.8)
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.definition.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