Add bullet creation and viewer dialogs with 2D/3D support

- Implement BulletCreatorDialog for configuring bullet parameters.
- Introduce BulletViewer to display bullets in a grid format.
- Add filtering options for 2D and 3D bullets in the viewer.
- Enhance WeaponCreatorDock to include bullet creation functionality.
This commit is contained in:
MaddoScientisto 2026-02-08 18:10:19 +01:00
commit ac96dabf2e
8 changed files with 1045 additions and 76 deletions

View file

@ -0,0 +1,394 @@
@tool
extends Window
# Popup window for configuring bullet parameters before creation
signal bullet_data_confirmed(bullet_data: Dictionary)
# UI fields
var bullet_name_field: LineEdit
var bullet_sprite_picker: EditorResourcePicker
var bullet_sprite_preview: TextureRect
var bullet_scene_picker: EditorResourcePicker
var bullet_size_field: SpinBox
var bullet_speed_field: SpinBox
var bullet_damage_field: SpinBox
var max_damage_field: SpinBox
var knockback_field: SpinBox
var life_time_field: SpinBox
var graze_value_field: SpinBox
var destruction_particles_picker: EditorResourcePicker
# Buttons
var create_button: Button
var cancel_button: Button
# Mode and prefill data
var is_3d_mode: bool = true
var prefill_data: Dictionary = {}
var _ui_built: bool = false
var editor_interface: EditorInterface
func setup(editor_iface: EditorInterface, is_3d: bool = true, prefill: Dictionary = {}) -> void:
editor_interface = editor_iface
is_3d_mode = is_3d
prefill_data = prefill
var mode_text = "3D" if is_3d_mode else "2D"
var action_text = "Duplicate" if not prefill_data.is_empty() else "Create New"
title = action_text + " Bullet (" + mode_text + ")"
if _ui_built:
if prefill_data.is_empty():
_set_default_values()
else:
_apply_prefill_data()
func _ready() -> void:
var mode_text = "3D" if is_3d_mode else "2D"
var action_text = "Duplicate" if not prefill_data.is_empty() else "Create New"
title = action_text + " Bullet (" + mode_text + ")"
size = Vector2i(750, 750)
transient = false
exclusive = false
unresizable = false
close_requested.connect(_on_cancel_pressed)
position = (DisplayServer.screen_get_size() - size) / 2
_build_ui()
_ui_built = true
if prefill_data.is_empty():
_set_default_values()
else:
_apply_prefill_data()
func _build_ui() -> void:
var margin = MarginContainer.new()
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 12)
margin.add_theme_constant_override("margin_top", 12)
margin.add_theme_constant_override("margin_right", 12)
margin.add_theme_constant_override("margin_bottom", 12)
add_child(margin)
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 8)
margin.add_child(vbox)
var scroll = ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
vbox.add_child(scroll)
var main_vbox = VBoxContainer.new()
main_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.add_theme_constant_override("separation", 12)
scroll.add_child(main_vbox)
_build_basic_section(main_vbox)
_build_stats_section(main_vbox)
vbox.add_child(HSeparator.new())
var button_hbox = HBoxContainer.new()
button_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
button_hbox.add_theme_constant_override("separation", 8)
vbox.add_child(button_hbox)
cancel_button = Button.new()
cancel_button.text = "Cancel"
cancel_button.custom_minimum_size = Vector2(100, 0)
cancel_button.pressed.connect(_on_cancel_pressed)
button_hbox.add_child(cancel_button)
create_button = Button.new()
create_button.text = "Create Bullet"
create_button.custom_minimum_size = Vector2(150, 0)
create_button.pressed.connect(_on_create_pressed)
button_hbox.add_child(create_button)
func _build_basic_section(parent: Control) -> void:
var section = VBoxContainer.new()
section.add_theme_constant_override("separation", 4)
parent.add_child(section)
var header = Label.new()
header.text = "Basic Information"
header.add_theme_font_size_override("font_size", 16)
section.add_child(header)
bullet_name_field = _add_line_edit_field(section, "Bullet Name:", "e.g., ice_bullet")
section.add_child(HSeparator.new())
# Sprite Selection
var sprite_header = Label.new()
sprite_header.text = "Bullet Sprite"
sprite_header.add_theme_font_size_override("font_size", 14)
section.add_child(sprite_header)
var sprite_container = HBoxContainer.new()
sprite_container.add_theme_constant_override("separation", 8)
section.add_child(sprite_container)
var sprite_vbox = VBoxContainer.new()
sprite_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
sprite_vbox.add_theme_constant_override("separation", 4)
sprite_container.add_child(sprite_vbox)
var sprite_picker_hbox = HBoxContainer.new()
sprite_picker_hbox.add_theme_constant_override("separation", 4)
sprite_vbox.add_child(sprite_picker_hbox)
var sprite_label = Label.new()
sprite_label.text = "Sprite:"
sprite_label.custom_minimum_size = Vector2(120, 0)
sprite_picker_hbox.add_child(sprite_label)
bullet_sprite_picker = EditorResourcePicker.new()
bullet_sprite_picker.base_type = "Texture2D"
bullet_sprite_picker.editable = true
bullet_sprite_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bullet_sprite_picker.resource_changed.connect(_on_sprite_changed)
bullet_sprite_picker.resource_selected.connect(_on_resource_picker_opening)
sprite_picker_hbox.add_child(bullet_sprite_picker)
var preview_container = PanelContainer.new()
preview_container.custom_minimum_size = Vector2(64, 64)
sprite_container.add_child(preview_container)
bullet_sprite_preview = TextureRect.new()
bullet_sprite_preview.custom_minimum_size = Vector2(64, 64)
bullet_sprite_preview.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
bullet_sprite_preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
preview_container.add_child(bullet_sprite_preview)
section.add_child(HSeparator.new())
# Bullet Scene
var scene_header = Label.new()
scene_header.text = "Bullet Scene"
scene_header.add_theme_font_size_override("font_size", 14)
section.add_child(scene_header)
var scene_hbox = HBoxContainer.new()
scene_hbox.add_theme_constant_override("separation", 4)
section.add_child(scene_hbox)
var scene_label = Label.new()
scene_label.text = "Scene:"
scene_label.custom_minimum_size = Vector2(120, 0)
scene_hbox.add_child(scene_label)
bullet_scene_picker = EditorResourcePicker.new()
bullet_scene_picker.base_type = "PackedScene"
bullet_scene_picker.editable = true
bullet_scene_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bullet_scene_picker.resource_selected.connect(_on_resource_picker_opening)
scene_hbox.add_child(bullet_scene_picker)
section.add_child(HSeparator.new())
# Destruction Particles
var particles_header = Label.new()
particles_header.text = "Destruction Particles (Optional)"
particles_header.add_theme_font_size_override("font_size", 14)
section.add_child(particles_header)
var particles_hbox = HBoxContainer.new()
particles_hbox.add_theme_constant_override("separation", 4)
section.add_child(particles_hbox)
var particles_label = Label.new()
particles_label.text = "Particles Scene:"
particles_label.custom_minimum_size = Vector2(120, 0)
particles_hbox.add_child(particles_label)
destruction_particles_picker = EditorResourcePicker.new()
destruction_particles_picker.base_type = "PackedScene"
destruction_particles_picker.editable = true
destruction_particles_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
destruction_particles_picker.resource_selected.connect(_on_resource_picker_opening)
particles_hbox.add_child(destruction_particles_picker)
func _build_stats_section(parent: Control) -> void:
var section = VBoxContainer.new()
section.add_theme_constant_override("separation", 4)
parent.add_child(section)
var header = Label.new()
header.text = "Bullet Statistics"
header.add_theme_font_size_override("font_size", 16)
section.add_child(header)
var grid = GridContainer.new()
grid.columns = 2
grid.add_theme_constant_override("h_separation", 8)
grid.add_theme_constant_override("v_separation", 4)
section.add_child(grid)
bullet_size_field = _add_spinbox_field(grid, "Bullet Size:", 0, 100, 0.1, 1.0)
bullet_speed_field = _add_spinbox_field(grid, "Bullet Speed:", 0, 10000, 1, 100)
bullet_damage_field = _add_spinbox_field(grid, "Bullet Damage:", 0, 10000, 0.1, 1.0)
max_damage_field = _add_spinbox_field(grid, "Max Damage:", 0, 10000, 0.1, 1.0)
knockback_field = _add_spinbox_field(grid, "Knockback:", 0, 1000, 0.1, 1.0)
life_time_field = _add_spinbox_field(grid, "Life Time (s):", 0, 100, 0.1, 10.0)
graze_value_field = _add_spinbox_field(grid, "Graze Value:", 0, 100, 0.1, 0.2)
func _add_line_edit_field(parent: Control, label_text: String, placeholder: String) -> LineEdit:
var hbox = HBoxContainer.new()
parent.add_child(hbox)
var label = Label.new()
label.text = label_text
label.custom_minimum_size = Vector2(120, 0)
hbox.add_child(label)
var line_edit = LineEdit.new()
line_edit.placeholder_text = placeholder
line_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(line_edit)
return line_edit
func _add_spinbox_field(parent: Control, label_text: String, min_val: float, max_val: float, step_val: float, default_val: float) -> SpinBox:
var label = Label.new()
label.text = label_text
parent.add_child(label)
var spinbox = SpinBox.new()
spinbox.min_value = min_val
spinbox.max_value = max_val
spinbox.step = step_val
spinbox.value = default_val
spinbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
parent.add_child(spinbox)
return spinbox
func _set_default_values() -> void:
bullet_name_field.text = "new_bullet"
# Set default bullet scene based on mode
if is_3d_mode:
var default_scene = load("res://Scenes/Weapons/base_generic_bullet_3D.tscn")
bullet_scene_picker.edited_resource = default_scene
bullet_size_field.value = 1.0
bullet_speed_field.value = 100.0
bullet_damage_field.value = 1.0
max_damage_field.value = 1.0
knockback_field.value = 1.0
life_time_field.value = 10.0
graze_value_field.value = 0.2
func _apply_prefill_data() -> void:
if prefill_data.has("bullet_sprite") and prefill_data["bullet_sprite"] != null:
bullet_sprite_picker.edited_resource = prefill_data["bullet_sprite"]
bullet_sprite_preview.texture = prefill_data["bullet_sprite"]
if prefill_data.has("bullet_scene") and prefill_data["bullet_scene"] != null:
bullet_scene_picker.edited_resource = prefill_data["bullet_scene"]
if prefill_data.has("bullet_size"):
bullet_size_field.value = prefill_data["bullet_size"]
if prefill_data.has("bullet_speed"):
bullet_speed_field.value = prefill_data["bullet_speed"]
if prefill_data.has("bullet_damage"):
bullet_damage_field.value = prefill_data["bullet_damage"]
if prefill_data.has("max_damage"):
max_damage_field.value = prefill_data["max_damage"]
if prefill_data.has("knockback"):
knockback_field.value = prefill_data["knockback"]
if prefill_data.has("life_time"):
life_time_field.value = prefill_data["life_time"]
if prefill_data.has("graze_value"):
graze_value_field.value = prefill_data["graze_value"]
if prefill_data.has("destruction_particles_scene") and prefill_data["destruction_particles_scene"] != null:
destruction_particles_picker.edited_resource = prefill_data["destruction_particles_scene"]
func _on_sprite_changed(resource: Resource) -> void:
if resource is Texture2D:
bullet_sprite_preview.texture = resource
# Restore window focus after resource picker closes
call_deferred("_restore_window_focus")
func _on_resource_picker_opening(_resource: Resource, _inspect: bool) -> void:
# Called when resource picker button is clicked - store current state
pass
func _restore_window_focus() -> void:
# Bring window back to front after file picker closes
if visible:
move_to_foreground()
func _on_create_pressed() -> void:
if not _validate_inputs():
return
var dimension_suffix = "_3D" if is_3d_mode else ""
var bullet_name = bullet_name_field.text.strip_edges()
# Ensure name ends with dimension suffix if 3D
if is_3d_mode and not bullet_name.ends_with("_3D"):
bullet_name += "_3D"
var bullet_data = {
"bullet_name": bullet_name,
"is_3d": is_3d_mode,
"bullet_sprite": bullet_sprite_picker.edited_resource,
"bullet_scene": bullet_scene_picker.edited_resource,
"bullet_size": bullet_size_field.value,
"bullet_speed": bullet_speed_field.value,
"bullet_damage": bullet_damage_field.value,
"max_damage": max_damage_field.value,
"knockback": knockback_field.value,
"life_time": life_time_field.value,
"graze_value": graze_value_field.value,
"destruction_particles_scene": destruction_particles_picker.edited_resource
}
bullet_data_confirmed.emit(bullet_data)
hide()
queue_free()
func _on_cancel_pressed() -> void:
hide()
queue_free()
func _validate_inputs() -> bool:
if bullet_name_field.text.strip_edges().is_empty():
_show_error("Bullet name cannot be empty")
return false
var bullet_name = bullet_name_field.text.strip_edges()
if is_3d_mode and not bullet_name.ends_with("_3D"):
bullet_name += "_3D"
var bullets_dir = "res://Resources/Bullets/"
if is_3d_mode:
bullets_dir += "3D/"
var bullet_path = bullets_dir + bullet_name + ".tres"
if ResourceLoader.exists(bullet_path):
_show_error("Bullet resource already exists at:\n" + bullet_path + "\n\nPlease choose a different name.")
return false
if not bullet_scene_picker.edited_resource:
_show_error("Bullet scene is required")
return false
return true
func _show_error(message: String) -> void:
var dialog = AcceptDialog.new()
dialog.dialog_text = message
dialog.title = "Error"
add_child(dialog)
dialog.popup_centered()

View file

@ -0,0 +1 @@
uid://dx6paglgfjh1c

View file

@ -0,0 +1,428 @@
@tool
extends PanelContainer
# Displays all bullets from the Resources/Bullets folders in a grid format
# Shows sprite and name, with tooltip showing resource path
signal bullet_selected(bullet_resource_path: String)
signal duplicate_bullet_requested(bullet_data: Dictionary)
signal bullet_deleted(bullet_name: String, bullet_path: String)
signal bullet_duplication_started(bullet_name: String, is_3d: bool)
var _editor_interface: EditorInterface
var _grid_container: HFlowContainer
var _show_2d_checkbox: CheckBox
var _show_3d_checkbox: CheckBox
var _dock: PanelContainer
const SETTING_SHOW_2D = "weapon_creator/bullet_filter_show_2d"
const SETTING_SHOW_3D = "weapon_creator/bullet_filter_show_3d"
func setup(editor_interface: EditorInterface, dock: PanelContainer = null) -> void:
_editor_interface = editor_interface
_dock = dock
if _show_2d_checkbox:
_show_2d_checkbox.button_pressed = _load_filter_setting(SETTING_SHOW_2D, true)
if _show_3d_checkbox:
_show_3d_checkbox.button_pressed = _load_filter_setting(SETTING_SHOW_3D, true)
refresh_bullets()
func _ready() -> void:
_build_ui()
refresh_bullets()
func _build_ui() -> void:
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 8)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_right", 8)
margin.add_theme_constant_override("margin_bottom", 8)
add_child(margin)
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 8)
margin.add_child(vbox)
# Header with title, filters, and buttons
var header_hbox = HBoxContainer.new()
vbox.add_child(header_hbox)
var create_2d_button = Button.new()
create_2d_button.text = "Create (2D)"
create_2d_button.pressed.connect(_on_create_bullet_pressed.bind(false))
header_hbox.add_child(create_2d_button)
var create_3d_button = Button.new()
create_3d_button.text = "Create (3D)"
create_3d_button.pressed.connect(_on_create_bullet_pressed.bind(true))
header_hbox.add_child(create_3d_button)
# Spacer
var spacer = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_hbox.add_child(spacer)
_show_2d_checkbox = CheckBox.new()
_show_2d_checkbox.text = "2D"
_show_2d_checkbox.button_pressed = true
_show_2d_checkbox.toggled.connect(_on_filter_changed)
header_hbox.add_child(_show_2d_checkbox)
_show_3d_checkbox = CheckBox.new()
_show_3d_checkbox.text = "3D"
_show_3d_checkbox.button_pressed = true
_show_3d_checkbox.toggled.connect(_on_filter_changed)
header_hbox.add_child(_show_3d_checkbox)
var refresh_button = Button.new()
refresh_button.text = "Refresh"
refresh_button.pressed.connect(refresh_bullets)
header_hbox.add_child(refresh_button)
vbox.add_child(HSeparator.new())
# Scroll container for grid
var scroll = ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
vbox.add_child(scroll)
_grid_container = HFlowContainer.new()
_grid_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_grid_container.add_theme_constant_override("h_separation", 8)
_grid_container.add_theme_constant_override("v_separation", 8)
scroll.add_child(_grid_container)
func refresh_bullets() -> void:
if not _grid_container:
return
# Clear existing items
for child in _grid_container.get_children():
child.queue_free()
var show_2d = _show_2d_checkbox == null or _show_2d_checkbox.button_pressed
var show_3d = _show_3d_checkbox == null or _show_3d_checkbox.button_pressed
var bullet_count = 0
# Load 2D bullets
if show_2d:
bullet_count += _load_bullets_from_directory("res://Resources/Bullets/", false)
# Load 3D bullets
if show_3d:
bullet_count += _load_bullets_from_directory("res://Resources/Bullets/3D/", true)
if bullet_count == 0:
_add_error_label("No bullets found")
func _load_bullets_from_directory(dir_path: String, is_3d: bool) -> int:
var dir = DirAccess.open(dir_path)
if dir == null:
return 0
var count = 0
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if not dir.current_is_dir() and file_name.ends_with(".tres"):
var bullet_path = dir_path + file_name
var bullet_resource = load(bullet_path)
if bullet_resource and bullet_resource.get_script():
var script_path = bullet_resource.get_script().resource_path
if script_path and "BulletResource.cs" in script_path:
_create_bullet_tile(bullet_resource, is_3d)
count += 1
file_name = dir.get_next()
dir.list_dir_end()
return count
func _create_bullet_tile(bullet_resource: Resource, is_3d: bool) -> void:
var panel = PanelContainer.new()
panel.custom_minimum_size = Vector2(100, 145)
panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
panel.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
panel.mouse_filter = Control.MOUSE_FILTER_STOP
panel.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
var sprite: Texture2D = bullet_resource.get("BulletSprite")
# Container for sprite with badge overlay
var sprite_container = Control.new()
sprite_container.custom_minimum_size = Vector2(64, 64)
sprite_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
vbox.add_child(sprite_container)
var texture_rect = TextureRect.new()
texture_rect.custom_minimum_size = Vector2(64, 64)
texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
sprite_container.add_child(texture_rect)
if sprite:
texture_rect.texture = sprite
else:
var placeholder_label = Label.new()
placeholder_label.text = "No Icon"
placeholder_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
placeholder_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
placeholder_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
placeholder_label.add_theme_font_size_override("font_size", 10)
placeholder_label.set_anchors_preset(Control.PRESET_FULL_RECT)
sprite_container.add_child(placeholder_label)
# Add 2D/3D badge
var badge = Label.new()
badge.text = "3D" if is_3d else "2D"
badge.add_theme_font_size_override("font_size", 14)
if is_3d:
badge.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2))
else:
badge.add_theme_color_override("font_color", Color(0.3, 0.5, 1.0))
badge.add_theme_color_override("font_outline_color", Color(0.15, 0.15, 0.15))
badge.add_theme_constant_override("outline_size", 3)
badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
badge.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.position = Vector2(38, 44)
badge.size = Vector2(24, 20)
sprite_container.add_child(badge)
var name_label = Label.new()
var bullet_name: String = bullet_resource.resource_path.get_file().get_basename()
name_label.text = bullet_name if bullet_name else "Unknown"
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
name_label.custom_minimum_size = Vector2(92, 48)
name_label.clip_text = false
name_label.max_lines_visible = 3
name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
name_label.add_theme_font_size_override("font_size", 14)
name_label.add_theme_constant_override("line_spacing", -2)
vbox.add_child(name_label)
panel.add_child(vbox)
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
vbox.add_theme_constant_override("margin_left", 4)
vbox.add_theme_constant_override("margin_top", 4)
vbox.add_theme_constant_override("margin_right", 4)
vbox.add_theme_constant_override("margin_bottom", 4)
var bullet_path = bullet_resource.resource_path
if bullet_path:
panel.tooltip_text = bullet_path
else:
panel.tooltip_text = "Built-in resource (no file path)"
panel.gui_input.connect(_on_bullet_gui_input.bind(panel, bullet_resource))
_grid_container.add_child(panel)
func _on_bullet_gui_input(event: InputEvent, panel: PanelContainer, bullet_resource: Resource) -> void:
if event is InputEventMouseButton:
var mouse_event = event as InputEventMouseButton
if mouse_event.pressed:
if mouse_event.button_index == MOUSE_BUTTON_LEFT:
_on_bullet_clicked(bullet_resource)
elif mouse_event.button_index == MOUSE_BUTTON_RIGHT:
_show_context_menu(panel, bullet_resource)
func _on_bullet_clicked(bullet_resource: Resource) -> void:
if not _editor_interface:
push_error("Editor interface not available")
return
if bullet_resource:
_editor_interface.edit_resource(bullet_resource)
bullet_selected.emit(bullet_resource.resource_path)
func _show_context_menu(panel: PanelContainer, bullet_resource: Resource) -> void:
var popup = PopupMenu.new()
popup.add_item("Open Bullet", 0)
popup.add_separator()
# Determine if this is a 2D or 3D bullet
var is_3d = "3D" in bullet_resource.resource_path
popup.add_item("Duplicate Bullet (2D)", 1)
popup.add_item("Duplicate Bullet (3D)", 2)
popup.add_separator()
popup.add_item("Copy Resource Path", 3)
popup.add_separator()
popup.add_item("Delete", 4)
popup.id_pressed.connect(_on_context_menu_id_pressed.bind(popup, bullet_resource))
get_tree().root.add_child(popup)
var mouse_pos = DisplayServer.mouse_get_position()
popup.position = mouse_pos
popup.popup()
popup.popup_hide.connect(func(): popup.queue_free())
func _on_context_menu_id_pressed(id: int, popup: PopupMenu, bullet_resource: Resource) -> void:
match id:
0:
_open_bullet(bullet_resource)
1:
_duplicate_bullet(bullet_resource, false)
2:
_duplicate_bullet(bullet_resource, true)
3:
_copy_bullet_resource_path(bullet_resource)
4:
_confirm_delete(bullet_resource)
func _open_bullet(bullet_resource: Resource) -> void:
if not _editor_interface:
push_error("Editor interface not available")
return
if bullet_resource:
_editor_interface.edit_resource(bullet_resource)
func _duplicate_bullet(bullet_resource: Resource, is_3d: bool) -> void:
if not _editor_interface:
push_error("Editor interface not available")
return
var bullet_name = bullet_resource.resource_path.get_file().get_basename()
var dimension = "3D" if is_3d else "2D"
if _dock:
_dock.call("add_log", "=== Duplicating Bullet (" + dimension + ") ===", Color.CYAN)
_dock.call("add_log", "Source: " + bullet_name, Color.CYAN)
bullet_duplication_started.emit(bullet_name, is_3d)
# Extract bullet data
var prefill_data = {}
prefill_data["bullet_sprite"] = bullet_resource.get("BulletSprite")
prefill_data["bullet_size"] = bullet_resource.get("BulletSize")
prefill_data["bullet_speed"] = bullet_resource.get("BulletSpeed")
prefill_data["bullet_damage"] = bullet_resource.get("BulletDamage")
prefill_data["max_damage"] = bullet_resource.get("MaxDamage")
prefill_data["knockback"] = bullet_resource.get("Knockback")
prefill_data["life_time"] = bullet_resource.get("LifeTime")
prefill_data["graze_value"] = bullet_resource.get("GrazeValue")
prefill_data["bullet_scene"] = bullet_resource.get("BulletScene")
prefill_data["destruction_particles_scene"] = bullet_resource.get("DestructionParticlesScene")
_open_bullet_dialog(is_3d, prefill_data)
func _on_duplicate_bullet_confirmed(bullet_data: Dictionary) -> void:
duplicate_bullet_requested.emit(bullet_data)
get_tree().create_timer(0.5).timeout.connect(refresh_bullets)
func _copy_bullet_resource_path(bullet_resource: Resource) -> void:
if bullet_resource and bullet_resource.resource_path:
DisplayServer.clipboard_set(bullet_resource.resource_path)
print("Copied bullet resource path: ", bullet_resource.resource_path)
func _confirm_delete(bullet_resource: Resource) -> void:
var bullet_name: String = bullet_resource.resource_path.get_file().get_basename()
var bullet_path = bullet_resource.resource_path
var dialog_text = "Are you REALLY sure you want to delete this bullet?\n\n"
dialog_text += "Bullet: " + bullet_name + "\n\n"
dialog_text += "Path: " + bullet_path + "\n\n"
dialog_text += "This action cannot be undone!"
var confirm_dialog = ConfirmationDialog.new()
confirm_dialog.dialog_text = dialog_text
confirm_dialog.title = "Confirm Deletion"
confirm_dialog.ok_button_text = "Delete"
confirm_dialog.confirmed.connect(_perform_delete.bind(bullet_resource, confirm_dialog))
confirm_dialog.close_requested.connect(func(): confirm_dialog.queue_free())
confirm_dialog.canceled.connect(func(): confirm_dialog.queue_free())
get_tree().root.add_child(confirm_dialog)
confirm_dialog.popup_centered()
func _perform_delete(bullet_resource: Resource, dialog: ConfirmationDialog) -> void:
var bullet_path = bullet_resource.resource_path
var bullet_name = bullet_resource.resource_path.get_file().get_basename()
if not bullet_path:
push_error("Cannot delete built-in resources")
dialog.queue_free()
return
var deleted_success = false
if DirAccess.remove_absolute(bullet_path) == OK:
print("Deleted bullet resource: ", bullet_path)
deleted_success = true
else:
push_error("Failed to delete bullet resource: ", bullet_path)
if _editor_interface:
_editor_interface.get_resource_filesystem().scan()
dialog.queue_free()
if deleted_success:
bullet_deleted.emit(bullet_name, bullet_path)
if _dock:
_dock.call("add_log", "=== Bullet Deleted ===", Color.ORANGE)
_dock.call("add_log", "Bullet: " + bullet_name, Color.ORANGE)
_dock.call("add_log", "✓ Deleted BulletResource: " + bullet_path, Color.GREEN)
refresh_bullets()
func _on_create_bullet_pressed(is_3d: bool) -> void:
_open_bullet_dialog(is_3d, {})
func _open_bullet_dialog(is_3d: bool, prefill_data: Dictionary = {}) -> void:
var dialog_script = load("res://addons/weapon_creator/BulletCreatorDialog.gd")
var dialog = Window.new()
dialog.set_script(dialog_script)
get_tree().root.add_child(dialog)
dialog.call_deferred("setup", _editor_interface, is_3d, prefill_data)
dialog.call_deferred("connect", "bullet_data_confirmed", _on_duplicate_bullet_confirmed)
dialog.call_deferred("popup_centered")
func _add_error_label(error_message: String) -> void:
var label = Label.new()
label.text = error_message
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.modulate = Color.ORANGE_RED
_grid_container.add_child(label)
func _on_filter_changed(_toggled: bool) -> void:
_save_filter_settings()
refresh_bullets()
func _load_filter_setting(key: String, default_value: bool) -> bool:
if not _editor_interface:
return default_value
var editor_settings = _editor_interface.get_editor_settings()
if editor_settings and editor_settings.has_setting(key):
return editor_settings.get_setting(key)
return default_value
func _save_filter_settings() -> void:
if not _editor_interface:
return
var editor_settings = _editor_interface.get_editor_settings()
if editor_settings:
editor_settings.set_setting(SETTING_SHOW_2D, _show_2d_checkbox.button_pressed)
editor_settings.set_setting(SETTING_SHOW_3D, _show_3d_checkbox.button_pressed)

View file

@ -0,0 +1 @@
uid://b56a4ljlod8rm

View file

@ -72,8 +72,9 @@ func _ready() -> void:
var action_text = "Duplicate" if not prefill_data.is_empty() else "Create New"
title = action_text + " Weapon (" + mode_text + ")"
size = Vector2i(750, 950)
transient = true
exclusive = true
transient = false
exclusive = false
unresizable = false
# Connect close request (X button) to cancel action
close_requested.connect(_on_cancel_pressed)
@ -243,6 +244,7 @@ func _build_sprite_selector(parent: Control) -> void:
sprite_picker.editable = true
sprite_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
sprite_picker.resource_changed.connect(_on_sprite_resource_changed)
sprite_picker.resource_selected.connect(_on_resource_picker_opening)
picker_hbox.add_child(sprite_picker)
# Setup editor integration if interface is available
@ -261,7 +263,6 @@ func _build_sprite_selector(parent: Control) -> void:
preview_container.add_child(sprite_preview)
func _on_sprite_resource_changed(resource: Resource) -> void:
# Update sprite resource and preview when changed via EditorResourcePicker
if resource is Texture2D:
sprite_resource = resource
sprite_preview.texture = resource
@ -272,6 +273,18 @@ func _on_sprite_resource_changed(resource: Resource) -> void:
print("Sprite resource cleared")
else:
push_warning("Selected resource is not a Texture2D")
# Restore window focus after resource picker closes
call_deferred("_restore_window_focus")
func _on_resource_picker_opening(_resource: Resource, _inspect: bool) -> void:
# Called when resource picker button is clicked
pass
func _restore_window_focus() -> void:
# Bring window back to front after file picker closes
if visible:
move_to_foreground()
func _setup_sprite_picker() -> void:
# Ensure sprite picker is properly integrated with editor

View file

@ -2,15 +2,17 @@
extends PanelContainer
# UI dock for the Weapon Creator plugin
# Provides a button to open the creation dialog and displays creation logs
# Provides tabs for weapons and bullets, with creation dialogs and logs
signal create_weapon_requested(weapon_data: Dictionary)
signal create_bullet_requested(bullet_data: Dictionary)
# UI elements
var open_dialog_button: Button
var clear_button: Button
var log_output: RichTextLabel
var weapon_viewer: PanelContainer
var bullet_viewer: PanelContainer
var tab_container: TabContainer
var _is_creating: bool = false
var _editor_interface: EditorInterface
@ -35,40 +37,7 @@ func _build_ui() -> void:
vbox.add_theme_constant_override("separation", 8)
margin.add_child(vbox)
# Title
var title = Label.new()
title.text = "3D Weapon Creator"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(title)
vbox.add_child(HSeparator.new())
# Buttons
var button_hbox = HBoxContainer.new()
button_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
button_hbox.add_theme_constant_override("separation", 8)
vbox.add_child(button_hbox)
open_dialog_button = Button.new()
open_dialog_button.text = "Create Weapon (2D)"
open_dialog_button.custom_minimum_size = Vector2(150, 0)
open_dialog_button.pressed.connect(_on_open_dialog_pressed.bind(false))
button_hbox.add_child(open_dialog_button)
var open_dialog_button_3d = Button.new()
open_dialog_button_3d.text = "Create Weapon (3D)"
open_dialog_button_3d.custom_minimum_size = Vector2(150, 0)
open_dialog_button_3d.pressed.connect(_on_open_dialog_pressed.bind(true))
button_hbox.add_child(open_dialog_button_3d)
clear_button = Button.new()
clear_button.text = "Clear Log"
clear_button.pressed.connect(_on_clear_button_pressed)
button_hbox.add_child(clear_button)
vbox.add_child(HSeparator.new())
# Horizontal split container for weapon viewer and log
# Horizontal split container for tabs and log
var hsplit = HSplitContainer.new()
hsplit.size_flags_vertical = Control.SIZE_EXPAND_FILL
vbox.add_child(hsplit)
@ -79,23 +48,50 @@ func _build_ui() -> void:
log_vbox.add_theme_constant_override("separation", 4)
hsplit.add_child(log_vbox)
# Weapon Viewer (right side)
# Tab Container for Weapons and Bullets (right side)
tab_container = TabContainer.new()
tab_container.custom_minimum_size = Vector2(300, 0)
tab_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hsplit.add_child(tab_container)
# Weapon Viewer Tab
var weapon_viewer_script = load("res://addons/weapon_creator/WeaponViewer.gd")
weapon_viewer = PanelContainer.new()
weapon_viewer.set_script(weapon_viewer_script)
weapon_viewer.custom_minimum_size = Vector2(300, 0)
weapon_viewer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# Connect duplicate weapon signal
weapon_viewer.name = "Weapons"
weapon_viewer.duplicate_weapon_requested.connect(_on_weapon_data_confirmed)
hsplit.add_child(weapon_viewer)
weapon_viewer.weapon_deleted.connect(_on_weapon_deleted)
weapon_viewer.weapon_duplication_started.connect(_on_weapon_duplication_started)
tab_container.add_child(weapon_viewer)
# Bullet Viewer Tab
var bullet_viewer_script = load("res://addons/weapon_creator/BulletViewer.gd")
bullet_viewer = PanelContainer.new()
bullet_viewer.set_script(bullet_viewer_script)
bullet_viewer.name = "Bullets"
bullet_viewer.duplicate_bullet_requested.connect(_on_bullet_data_confirmed)
bullet_viewer.bullet_deleted.connect(_on_bullet_deleted)
bullet_viewer.bullet_duplication_started.connect(_on_bullet_duplication_started)
tab_container.add_child(bullet_viewer)
# Setup after adding to scene tree
if _editor_interface:
weapon_viewer.setup(_editor_interface)
weapon_viewer.setup(_editor_interface, self)
bullet_viewer.setup(_editor_interface, self)
# Log header with label and clear button
var log_header_hbox = HBoxContainer.new()
log_vbox.add_child(log_header_hbox)
var log_label = Label.new()
log_label.text = "Output Log:"
log_vbox.add_child(log_label)
log_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
log_header_hbox.add_child(log_label)
clear_button = Button.new()
clear_button.text = "Clear Log"
clear_button.pressed.connect(_on_clear_button_pressed)
log_header_hbox.add_child(clear_button)
var log_scroll = ScrollContainer.new()
log_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
@ -110,49 +106,48 @@ func _build_ui() -> void:
log_output.size_flags_vertical = Control.SIZE_EXPAND_FILL
log_scroll.add_child(log_output)
func _on_open_dialog_pressed(is_3d: bool) -> void:
# Open the weapon creation dialog window
var dialog_script = load("res://addons/weapon_creator/WeaponCreatorDialog.gd")
var dialog = Window.new()
dialog.set_script(dialog_script)
# Add to scene tree first
get_tree().root.add_child(dialog)
# Setup editor interface and mode using call_deferred to ensure script is ready
if _editor_interface:
dialog.call_deferred("setup", _editor_interface, is_3d)
# Connect to the confirmation signal using call_deferred
dialog.call_deferred("connect", "weapon_data_confirmed", _on_weapon_data_confirmed)
# Show the dialog
dialog.call_deferred("popup_centered")
func _on_weapon_data_confirmed(weapon_data: Dictionary) -> void:
# Receive weapon data from dialog and forward to plugin
if _is_creating:
return
_is_creating = true
open_dialog_button.disabled = true
log_output.clear()
# Emit signal to plugin
create_weapon_requested.emit(weapon_data)
func _on_clear_button_pressed() -> void:
log_output.clear()
func _on_weapon_deleted(weapon_name: String, weapon_path: String, item_path: String) -> void:
pass
func _on_weapon_duplication_started(weapon_name: String, is_3d: bool) -> void:
pass
func _on_bullet_data_confirmed(bullet_data: Dictionary) -> void:
if _is_creating:
return
_is_creating = true
create_bullet_requested.emit(bullet_data)
func _on_bullet_deleted(bullet_name: String, bullet_path: String) -> void:
# Logging is handled directly in BulletViewer
pass
func _on_bullet_duplication_started(bullet_name: String, is_3d: bool) -> void:
# Logging is handled directly in BulletViewer
pass
func add_log(message: String, color: Color = Color.WHITE) -> void:
# Add colored message to log output
log_output.append_text("[color=#" + color.to_html(false) + "]" + message + "[/color]\n")
func set_creation_complete() -> void:
# Re-enable the create button after creation is complete
_is_creating = false
open_dialog_button.disabled = false
# Refresh weapon viewer to show newly created weapon
if weapon_viewer:
weapon_viewer.call("refresh_weapons")
if bullet_viewer:
bullet_viewer.call("refresh_bullets")

View file

@ -18,6 +18,8 @@ func _enter_tree() -> void:
# Connect the create button signal from the dock
if dock_instance.has_signal("create_weapon_requested"):
dock_instance.create_weapon_requested.connect(_on_create_weapon_requested)
if dock_instance.has_signal("create_bullet_requested"):
dock_instance.create_bullet_requested.connect(_on_create_bullet_requested)
# Add the dock to the editor (bottom dock area)
add_control_to_bottom_panel(dock_instance, "Weapon Creator")
@ -249,3 +251,77 @@ func _add_to_items_database(database_path: String, item_resource_path: String) -
return false
return true
func _on_create_bullet_requested(bullet_data: Dictionary) -> void:
var bullet_name: String = bullet_data.get("bullet_name", "new_bullet")
var is_3d: bool = bullet_data.get("is_3d", true)
var bullets_dir = "res://Resources/Bullets/"
if is_3d:
bullets_dir += "3D/"
var bullet_path = bullets_dir + bullet_name + ".tres"
dock_instance.call("add_log", "=== Starting Bullet Creation ===")
dock_instance.call("add_log", "Bullet Name: " + bullet_name)
dock_instance.call("add_log", "Mode: " + ("3D" if is_3d else "2D"))
if _create_bullet_resource(bullet_path, bullet_data):
dock_instance.call("add_log", "✓ Created BulletResource at: " + bullet_path, Color.GREEN)
else:
dock_instance.call("add_log", "✗ Failed to create BulletResource", Color.RED)
dock_instance.call("set_creation_complete")
return
dock_instance.call("add_log", "=== Bullet Creation Complete ===", Color.CYAN)
dock_instance.call("add_log", "Next steps:")
dock_instance.call("add_log", "1. Test the bullet in-game")
dock_instance.call("add_log", "2. Adjust properties as needed")
dock_instance.call("set_creation_complete")
get_editor_interface().get_resource_filesystem().scan()
func _create_bullet_resource(path: String, bullet_data: Dictionary) -> bool:
if ResourceLoader.exists(path):
dock_instance.call("add_log", "Warning: BulletResource already exists at " + path, Color.YELLOW)
return false
var bullet_script = load("res://Scripts/Resources/BulletResource.cs")
if bullet_script == null:
dock_instance.call("add_log", "Error: Could not load BulletResource.cs script", Color.RED)
return false
var bullet_resource = Resource.new()
bullet_resource.set_script(bullet_script)
# Set properties from bullet_data
var bullet_sprite = bullet_data.get("bullet_sprite", null)
if bullet_sprite:
bullet_resource.set("BulletSprite", bullet_sprite)
var bullet_scene = bullet_data.get("bullet_scene", null)
if bullet_scene:
bullet_resource.set("BulletScene", bullet_scene)
bullet_resource.set("BulletSize", bullet_data.get("bullet_size", 1.0))
bullet_resource.set("BulletSpeed", bullet_data.get("bullet_speed", 100.0))
bullet_resource.set("BulletDamage", bullet_data.get("bullet_damage", 1.0))
bullet_resource.set("MaxDamage", bullet_data.get("max_damage", 1.0))
bullet_resource.set("Knockback", bullet_data.get("knockback", 1.0))
bullet_resource.set("LifeTime", bullet_data.get("life_time", 10.0))
bullet_resource.set("GrazeValue", bullet_data.get("graze_value", 0.2))
var destruction_particles = bullet_data.get("destruction_particles_scene", null)
if destruction_particles:
bullet_resource.set("DestructionParticlesScene", destruction_particles)
# Set default Direction
bullet_resource.set("Direction", Vector2.RIGHT)
var err = ResourceSaver.save(bullet_resource, path)
if err != OK:
dock_instance.call("add_log", "Error saving BulletResource: " + str(err), Color.RED)
return false
return true

View file

@ -7,18 +7,22 @@ extends PanelContainer
signal weapon_selected(weapon_resource_path: String)
signal duplicate_weapon_requested(weapon_data: Dictionary)
signal weapon_deleted(weapon_name: String, weapon_path: String, item_path: String)
signal weapon_duplication_started(weapon_name: String, is_3d: bool)
var _editor_interface: EditorInterface
var _grid_container: HFlowContainer
var _items_database_path := "res://Resources/ItemsDatabase.tres"
var _show_2d_checkbox: CheckBox
var _show_3d_checkbox: CheckBox
var _dock: PanelContainer # Reference to the dock for logging
const SETTING_SHOW_2D = "weapon_creator/filter_show_2d"
const SETTING_SHOW_3D = "weapon_creator/filter_show_3d"
func setup(editor_interface: EditorInterface) -> void:
func setup(editor_interface: EditorInterface, dock: PanelContainer = null) -> void:
_editor_interface = editor_interface
_dock = dock
# Load saved filter settings after editor interface is available
if _show_2d_checkbox:
_show_2d_checkbox.button_pressed = _load_filter_setting(SETTING_SHOW_2D, true)
@ -49,10 +53,20 @@ func _build_ui() -> void:
var header_hbox = HBoxContainer.new()
vbox.add_child(header_hbox)
var title = Label.new()
title.text = "Weapons"
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_hbox.add_child(title)
var create_2d_button = Button.new()
create_2d_button.text = "Create (2D)"
create_2d_button.pressed.connect(_on_create_weapon_pressed.bind(false))
header_hbox.add_child(create_2d_button)
var create_3d_button = Button.new()
create_3d_button.text = "Create (3D)"
create_3d_button.pressed.connect(_on_create_weapon_pressed.bind(true))
header_hbox.add_child(create_3d_button)
# Spacer
var spacer = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_hbox.add_child(spacer)
_show_2d_checkbox = CheckBox.new()
_show_2d_checkbox.text = "2D"
@ -318,6 +332,20 @@ func _duplicate_weapon(loot_item: Resource, weapon_data: Resource, is_3d: bool)
push_error("Editor interface not available")
return
var weapon_name = loot_item.get("ItemName") if loot_item else "Unknown"
var dimension = "3D" if is_3d else "2D"
# Log to dock
if _dock:
_dock.call("add_log", "=== Duplicating Weapon (" + dimension + ") ===", Color.CYAN)
_dock.call("add_log", "Source: " + weapon_name, Color.CYAN)
_dock.call("add_log", "Opening creation dialog with prefilled data...", Color.CYAN)
# Also log to Godot console
print("Duplicating weapon (", dimension, "): ", weapon_name)
weapon_duplication_started.emit(weapon_name, is_3d)
# Extract all weapon data into a prefill dictionary
var prefill_data = {}
@ -413,6 +441,7 @@ func _confirm_delete(loot_item: Resource, weapon_data: Resource) -> void:
func _perform_delete(loot_item: Resource, weapon_data: Resource, dialog: ConfirmationDialog) -> void:
var weapon_path = weapon_data.resource_path
var loot_item_path = loot_item.resource_path
var weapon_name = loot_item.get("ItemName") if loot_item else "Unknown"
if not weapon_path or not loot_item_path:
push_error("Cannot delete built-in resources")
@ -440,13 +469,18 @@ func _perform_delete(loot_item: Resource, weapon_data: Resource, dialog: Confirm
var file_system = _editor_interface.get_resource_filesystem()
var weapon_deleted_success = false
var item_deleted_success = false
if DirAccess.remove_absolute(weapon_path) == OK:
print("Deleted weapon resource: ", weapon_path)
weapon_deleted_success = true
else:
push_error("Failed to delete weapon resource: ", weapon_path)
if DirAccess.remove_absolute(loot_item_path) == OK:
print("Deleted loot item resource: ", loot_item_path)
item_deleted_success = true
else:
push_error("Failed to delete loot item resource: ", loot_item_path)
@ -455,6 +489,18 @@ func _perform_delete(loot_item: Resource, weapon_data: Resource, dialog: Confirm
dialog.queue_free()
# Emit deletion signal and log if both files were deleted successfully
if weapon_deleted_success and item_deleted_success:
weapon_deleted.emit(weapon_name, weapon_path, loot_item_path)
# Log to dock
if _dock:
_dock.call("add_log", "=== Weapon Deleted ===", Color.ORANGE)
_dock.call("add_log", "Weapon: " + weapon_name, Color.ORANGE)
_dock.call("add_log", "✓ Deleted WeaponResource: " + weapon_path, Color.GREEN)
_dock.call("add_log", "✓ Deleted LootItem: " + loot_item_path, Color.GREEN)
_dock.call("add_log", "✓ Removed from ItemsDatabase", Color.GREEN)
refresh_weapons()
func _add_error_label(error_message: String) -> void:
@ -464,3 +510,18 @@ func _add_error_label(error_message: String) -> void:
label.modulate = Color.ORANGE_RED
_grid_container.add_child(label)
func _on_create_weapon_pressed(is_3d: bool) -> void:
if not _editor_interface:
push_error("Editor interface not available")
return
var dialog_script = load("res://addons/weapon_creator/WeaponCreatorDialog.gd")
var dialog = Window.new()
dialog.set_script(dialog_script)
get_tree().root.add_child(dialog)
dialog.call_deferred("setup", _editor_interface, is_3d)
dialog.call_deferred("connect", "weapon_data_confirmed", _on_duplicate_weapon_confirmed)
dialog.call_deferred("popup_centered")