Randoloot/scripts/ui/equipment_slot_view.gd
2026-10-25 22:38:13 +01:00

495 lines
21 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
var current := _inventory.get_item(slot_kind,slot_index)
if current != null and not current.definition.orb_id.is_empty():
_delete_button.hide()
return
_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)
var counter := get_node_or_null("StackOverlay/Count") as Label
if counter == null:
var overlay := Control.new()
overlay.name = "StackOverlay"
overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(overlay)
counter = Label.new()
counter.name = "Count"
counter.mouse_filter = Control.MOUSE_FILTER_IGNORE
counter.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT)
counter.offset_left = -42
counter.offset_top = -21
counter.offset_right = -4
counter.offset_bottom = -2
counter.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
counter.add_theme_font_size_override("font_size",13)
counter.add_theme_color_override("font_color",Color("ffe3a5"))
counter.add_theme_color_override("font_shadow_color",Color.BLACK)
counter.add_theme_constant_override("shadow_offset_x",1)
counter.add_theme_constant_override("shadow_offset_y",1)
overlay.add_child(counter)
counter.text = "×%d" % item.stack_count if item != null and not item.definition.orb_id.is_empty() else ""
_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
if _inventory != null and _inventory.selected_orb != null:
if item == _inventory.selected_orb or preload("res://scripts/items/special_orbs.gd").accepts(item,_inventory.selected_orb.definition.orb_id):
style.border_color = Color("ffe3a5") if item == _inventory.selected_orb else Color("73efb5")
style.set_border_width_all(3)
add_theme_stylebox_override("panel", style)
func _gui_input(event: InputEvent) -> void:
if _inventory == null or not event is InputEventMouseButton or not event.pressed or event.button_index != MOUSE_BUTTON_LEFT: return
var item := _inventory.get_item(slot_kind,slot_index)
if event.double_click and slot_kind == "cargo" and item != null and not item.definition.orb_id.is_empty():
_inventory.select_orb(slot_index)
accept_event()
elif _inventory.selected_orb != null:
_inventory.use_selected_orb(item)
accept_event()
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