class_name PlayerController extends Node const SPEED = 192.0 @export var dl_map: CombatMap signal combat_requested(attacker: Unit, defender: Unit) signal mouse_grid_changed(coords: Vector2i) signal camera_drag(delta: Vector2) var input_disabled := false var _selected_unit: Unit = null var _target_pos: Vector2 var _goal_pos: Vector2 var _moving := false var _left_pending := false var _drag_start := Vector2.ZERO var _dragging := false var _current_grid_coords := Vector2i(-99999, -99999) const DRAG_THRESHOLD := 8.0 func _ready() -> void: for unit: Unit in get_tree().get_nodes_in_group("units"): unit.unit_died.connect(_on_unit_died) get_tree().node_added.connect(_on_node_added) func _on_node_added(node: Node) -> void: if node is Unit and node.is_in_group("units"): if not node.unit_died.is_connected(_on_unit_died): node.unit_died.connect(_on_unit_died) func _on_unit_died(unit: Unit) -> void: if _selected_unit == unit: _selected_unit = null _moving = false func _process(_delta: float) -> void: if input_disabled: return var mouse_pos := get_viewport().get_canvas_transform().affine_inverse() * get_viewport().get_mouse_position() var coords := dl_map.world_to_coords(mouse_pos) if coords != _current_grid_coords: _current_grid_coords = coords mouse_grid_changed.emit(coords) func _unhandled_input(event: InputEvent) -> void: if input_disabled: return if event is InputEventMouseButton: match event.button_index: MOUSE_BUTTON_LEFT: if event.pressed: _left_pending = true _drag_start = event.position else: if _dragging: _dragging = false _left_pending = false Input.set_default_cursor_shape(Input.CURSOR_ARROW) get_viewport().set_input_as_handled() else: _left_pending = false _handle_left_click(event.position) MOUSE_BUTTON_MIDDLE: if event.pressed: _dragging = true _drag_start = event.position Input.set_default_cursor_shape(Input.CURSOR_DRAG) else: _dragging = false Input.set_default_cursor_shape(Input.CURSOR_ARROW) get_viewport().set_input_as_handled() elif event is InputEventMouseMotion: if _left_pending and not _dragging: if event.position.distance_to(_drag_start) >= DRAG_THRESHOLD: _dragging = true _left_pending = false Input.set_default_cursor_shape(Input.CURSOR_DRAG) if _dragging: var delta: Vector2 = _drag_start - event.position _drag_start = event.position camera_drag.emit(delta) get_viewport().set_input_as_handled() func _physics_process(delta: float) -> void: if not _selected_unit or not _selected_unit.is_alive(): return if _moving: var remaining := _target_pos - _selected_unit.position var step := SPEED * delta if remaining.length() <= step: _selected_unit.position = _target_pos _moving = false else: _selected_unit.position += remaining.normalized() * step return if _selected_unit.position != _goal_pos: var diff := _goal_pos - _selected_unit.position var dir: Vector2 if absf(diff.x) >= absf(diff.y): dir = Vector2(signf(diff.x), 0) else: dir = Vector2(0, signf(diff.y)) var next_pos := _selected_unit.position + dir * dl_map.TILE_SIZE var grid_coords := dl_map.world_to_coords(next_pos) var current_coords := dl_map.world_to_coords(_selected_unit.position) if not dl_map.is_tile_passable(current_coords, grid_coords): _goal_pos = _selected_unit.position return _target_pos = next_pos _moving = true func _handle_left_click(screen_pos: Vector2) -> void: var world_pos: Vector2 = get_viewport().get_canvas_transform().affine_inverse() * screen_pos var clicked_unit := _get_unit_at(world_pos) if clicked_unit: if _selected_unit and clicked_unit != _selected_unit and _selected_unit.is_alive() and clicked_unit.is_alive(): combat_requested.emit(_selected_unit, clicked_unit) else: _select_unit(clicked_unit) get_viewport().set_input_as_handled() elif _selected_unit: var snapped_pos := dl_map.snap_to_grid(world_pos) var grid_coords := dl_map.world_to_coords(world_pos) if not dl_map.is_tile_valid(grid_coords): return _goal_pos = snapped_pos get_viewport().set_input_as_handled() func _select_unit(unit: Unit) -> void: if _selected_unit: _selected_unit.set_selected(false) _selected_unit = unit _selected_unit.set_selected(true) _goal_pos = _selected_unit.position _target_pos = _selected_unit.position _moving = false func _get_unit_at(world_pos: Vector2) -> Unit: var snapped := dl_map.snap_to_grid(world_pos) for unit: Unit in get_tree().get_nodes_in_group("units"): if not unit.is_alive(): continue var unit_snapped := dl_map.snap_to_grid(unit.global_position) if unit_snapped == snapped: return unit return null