2025-07-28 13:55:03 +00:00
|
|
|
extends Control
|
2025-07-29 12:08:08 +00:00
|
|
|
|
2025-07-28 13:55:03 +00:00
|
|
|
class_name SelectionManager
|
|
|
|
|
2025-07-29 12:34:48 +00:00
|
|
|
@export_category("Controls")
|
|
|
|
@export var hover:Control
|
2025-07-29 12:59:56 +00:00
|
|
|
@export var selecteddisplay:Control
|
2025-07-29 12:34:48 +00:00
|
|
|
@export_category("Settings")
|
|
|
|
@export var rect_style: StyleBoxFlat
|
|
|
|
|
2025-07-28 13:55:03 +00:00
|
|
|
var draw_selection := false
|
|
|
|
var drag_start: Vector2
|
|
|
|
var select_box: Rect2
|
|
|
|
var perform_selection := false # Flag to trigger selection in _physics_process
|
2025-07-29 12:34:48 +00:00
|
|
|
var mouse_position:Vector2 # Storing mous eposition to use it on differents processes
|
2025-07-28 13:55:03 +00:00
|
|
|
@onready var box_color: Color = ProjectSettings.get_setting("game/interface/selection_rectangle_color", Color("#00ff0066"))
|
|
|
|
@onready var box_color_outline: Color = ProjectSettings.get_setting("game/interface/selection_rectangle_color_outline", Color("#00ff00"))
|
|
|
|
@onready var optimized_rect: bool = ProjectSettings.get_setting("game/interface/optimized_rectangle", false)
|
2025-07-29 12:34:48 +00:00
|
|
|
|
2025-07-28 13:55:03 +00:00
|
|
|
@onready var cam = get_viewport().get_camera_3d()
|
2025-07-29 12:59:56 +00:00
|
|
|
|
2025-07-29 12:08:08 +00:00
|
|
|
@onready var debug_mode = get_tree().debug_collisions_hint #ProjectSettings.get_setting("debug/collisions/show_mouse_trace", false)
|
2025-07-28 13:55:03 +00:00
|
|
|
|
|
|
|
|
2025-07-29 12:59:56 +00:00
|
|
|
func _ready() -> void:
|
|
|
|
if !hover:
|
|
|
|
hover = %HoverLabel
|
|
|
|
|
|
|
|
|
2025-07-28 13:55:03 +00:00
|
|
|
func _unhandled_input(e: InputEvent) -> void:
|
|
|
|
if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT:
|
|
|
|
if e.pressed:
|
|
|
|
draw_selection = true
|
|
|
|
drag_start = e.position
|
|
|
|
else:
|
|
|
|
draw_selection = false
|
|
|
|
if drag_start.is_equal_approx(e.position):
|
|
|
|
# Single click: set a zero-sized selection box
|
|
|
|
select_box = Rect2(e.position, Vector2.ZERO)
|
|
|
|
print("one click")
|
|
|
|
else:
|
|
|
|
# Drag: calculate the final selection box
|
|
|
|
var x_min = min(drag_start.x, e.position.x)
|
|
|
|
var y_min = min(drag_start.y, e.position.y)
|
|
|
|
select_box = Rect2(x_min, y_min,
|
2025-07-29 12:08:08 +00:00
|
|
|
max(drag_start.x, e.position.x) - x_min,
|
|
|
|
max(drag_start.y, e.position.y) - y_min)
|
|
|
|
|
|
|
|
perform_selection = true
|
2025-07-28 13:55:03 +00:00
|
|
|
|
|
|
|
queue_redraw()
|
|
|
|
elif draw_selection and e is InputEventMouseMotion:
|
|
|
|
# Update selection box for drawing during drag
|
|
|
|
var x_min = min(drag_start.x, e.position.x)
|
|
|
|
var y_min = min(drag_start.y, e.position.y)
|
|
|
|
select_box = Rect2(x_min, y_min,
|
|
|
|
max(drag_start.x, e.position.x) - x_min,
|
|
|
|
max(drag_start.y, e.position.y) - y_min)
|
|
|
|
|
|
|
|
queue_redraw()
|
|
|
|
if !draw_selection and e is InputEventMouseMotion:
|
2025-07-29 12:34:48 +00:00
|
|
|
mouse_position = e.position
|
2025-07-28 13:55:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
|
|
if perform_selection:
|
|
|
|
update_selected_units()
|
|
|
|
perform_selection = false
|
2025-07-29 12:34:48 +00:00
|
|
|
else:
|
|
|
|
check_hover(mouse_position)
|
2025-07-28 13:55:03 +00:00
|
|
|
|
|
|
|
|
2025-07-29 12:34:48 +00:00
|
|
|
func check_hover(mouse_pos:Vector2):
|
2025-07-28 13:55:03 +00:00
|
|
|
# TODO: Hovering and display names
|
2025-07-29 12:34:48 +00:00
|
|
|
var raycast = check_raycast(mouse_pos)
|
|
|
|
if raycast.is_empty():
|
|
|
|
hover.text = ""
|
|
|
|
elif raycast.collider:
|
|
|
|
var hovered_entity = raycast.collider.get_parent()
|
|
|
|
hover.text = hovered_entity.name
|
|
|
|
|
2025-07-28 13:55:03 +00:00
|
|
|
var selected = get_selected()
|
2025-07-29 12:59:56 +00:00
|
|
|
selecteddisplay.text = ""
|
2025-07-28 13:55:03 +00:00
|
|
|
if selected.size() > 0:
|
2025-07-29 12:59:56 +00:00
|
|
|
for s in selected:
|
|
|
|
selecteddisplay.text += ","+s.name
|
2025-07-28 13:55:03 +00:00
|
|
|
else:
|
2025-07-29 12:34:48 +00:00
|
|
|
selecteddisplay.text = ""
|
2025-07-28 13:55:03 +00:00
|
|
|
|
2025-07-29 12:34:48 +00:00
|
|
|
selecteddisplay.queue_redraw()
|
2025-07-28 13:55:03 +00:00
|
|
|
|
|
|
|
|
2025-07-29 12:08:08 +00:00
|
|
|
## Returns an array of entities selected by the player, using multiplayer-unique group names
|
2025-07-28 13:55:03 +00:00
|
|
|
func get_selected() -> Array[Entity]:
|
2025-07-29 12:08:08 +00:00
|
|
|
var selected: Array[Node] = get_tree().get_nodes_in_group('selected-by-' + str(get_tree().get_multiplayer().get_unique_id()))
|
2025-07-28 13:55:03 +00:00
|
|
|
var selected_entity: Array[Entity]
|
|
|
|
selected_entity.assign(selected)
|
|
|
|
return selected_entity
|
|
|
|
|
|
|
|
|
|
|
|
## [param collision_mask] is 32768 by default, so the collision channel 16
|
|
|
|
func check_raycast(mouse_pos: Vector2, collision_mask: int = 32768) -> Dictionary:
|
|
|
|
var from = cam.project_ray_origin(mouse_pos)
|
2025-07-29 12:59:56 +00:00
|
|
|
var to = from + cam.project_ray_normal(mouse_pos) * 1000 #FIXME: Magic number
|
2025-07-28 13:55:03 +00:00
|
|
|
var query = PhysicsRayQueryParameters3D.create(from,to,collision_mask)
|
|
|
|
query.collide_with_areas = true
|
|
|
|
query.collide_with_bodies = false
|
|
|
|
var space_state = cam.get_world_3d().direct_space_state
|
2025-07-29 12:34:48 +00:00
|
|
|
if space_state:
|
|
|
|
return space_state.intersect_ray(query)
|
|
|
|
|
|
|
|
return {}
|
2025-07-28 13:55:03 +00:00
|
|
|
|
|
|
|
|
2025-07-29 12:08:08 +00:00
|
|
|
## Deselects all currently selected entities
|
2025-07-28 13:55:03 +00:00
|
|
|
func deselect_all() -> void:
|
|
|
|
var selected = get_selected()
|
2025-07-29 12:08:08 +00:00
|
|
|
for entity: Entity in selected:
|
2025-07-28 13:55:03 +00:00
|
|
|
entity.deselect()
|
|
|
|
|
|
|
|
|
2025-07-29 12:08:08 +00:00
|
|
|
## Updates selection state of entities based on single-click (raycast) or drag (box) selection [br]
|
|
|
|
## Iterate on every entities inside the [param selectable-entity] group
|
2025-07-28 13:55:03 +00:00
|
|
|
func update_selected_units() -> void:
|
2025-07-30 17:18:47 +00:00
|
|
|
|
2025-07-29 12:08:08 +00:00
|
|
|
## FIXME: Could be optimized if we store an array of visible entity by adding[br]
|
|
|
|
## an [Area3D] child of the camera OR changing the group based on if the 3d model is loaded
|
2025-07-30 17:18:47 +00:00
|
|
|
## Seems like storing references of the [method Object.get_instance_id]
|
2025-07-28 13:55:03 +00:00
|
|
|
var units_array = get_tree().get_nodes_in_group("selectable-entity")
|
|
|
|
var raycast = {}
|
|
|
|
# Only perform raycast if it's a single click (zero-sized select_box)
|
|
|
|
if select_box.size == Vector2.ZERO:
|
2025-07-29 12:08:08 +00:00
|
|
|
raycast = check_raycast(select_box.position)
|
|
|
|
if raycast.is_empty():
|
|
|
|
deselect_all()
|
|
|
|
return
|
|
|
|
if raycast.collider:
|
|
|
|
if debug_mode:
|
|
|
|
DebugTools.DrawCube(raycast.position,0.02,3.0,Color())
|
|
|
|
|
2025-07-28 13:55:03 +00:00
|
|
|
# select entity under mouse
|
|
|
|
# maybe can be extracted from hover instead?
|
|
|
|
var entity = raycast.collider.get_parent()
|
|
|
|
deselect_all()
|
|
|
|
entity.select()
|
|
|
|
return
|
|
|
|
for entity: Entity in units_array:
|
|
|
|
# TODO: Optimize by searching only in spawned chunks
|
|
|
|
# select entities within the selection/draggin box
|
|
|
|
if entity.is_in_selection(select_box, cam):
|
|
|
|
entity.select()
|
|
|
|
else:
|
|
|
|
entity.deselect()
|
|
|
|
|
|
|
|
|
2025-07-29 12:08:08 +00:00
|
|
|
## Draws the selection rectangle during drag or selection, using optimized or styled rendering
|
2025-07-28 13:55:03 +00:00
|
|
|
func _draw() -> void:
|
|
|
|
if not (draw_selection or perform_selection):return
|
|
|
|
if select_box and rect_style:
|
|
|
|
if optimized_rect:
|
|
|
|
draw_rect(select_box,box_color)
|
|
|
|
draw_rect(select_box,box_color_outline,false,2.0,true)
|
|
|
|
elif rect_style:
|
|
|
|
draw_style_box(rect_style,select_box)
|