@tool extends Node3D class_name GridCharacterController @export var grid_manager_path: NodePath @export var camera_path: NodePath @export var start_cell: Vector2i = Vector2i.ZERO @export_range(0.1, 20.0, 0.1) var move_speed: float = 4.0 @export_range(0.0, 2.0, 0.01) var height_offset: float = 0.3 var _grid_manager: GridPlaneManager = null var _camera: Camera3D = null var _current_cell: Vector2i = Vector2i(-1, -1) var _target_position: Vector3 var _is_moving: bool = false func _ready() -> void: set_physics_process(true) set_process_unhandled_input(true) _resolve_references() if _grid_manager: _grid_manager.grid_layout_changed.connect(_on_grid_layout_changed) _move_to_cell(start_cell, true) func _physics_process(delta: float) -> void: if not _is_moving: return var direction: Vector3 = _target_position - global_transform.origin var distance: float = direction.length() if distance <= 0.01: global_position = _target_position _is_moving = false return var step: float = move_speed * delta var move_vector: Vector3 = direction.normalized() * min(step, distance) global_position += move_vector func _unhandled_input(event: InputEvent) -> void: if Engine.is_editor_hint(): return if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: _move_to_mouse_click() func _move_to_mouse_click() -> void: if not _grid_manager or not _camera: return var viewport: Viewport = get_viewport() if viewport == null: return var mouse_position: Vector2 = viewport.get_mouse_position() var target_cell: Vector2i = _pick_cell(mouse_position) if target_cell.x < 0 or target_cell.y < 0: return _move_to_cell(target_cell) func _move_to_cell(cell: Vector2i, instant: bool = false) -> void: if not _grid_manager or not _grid_manager.has_cell(cell): return _current_cell = cell var local_center: Vector3 = _grid_manager.cell_to_world(cell) var world_center: Vector3 = _grid_manager.to_global(local_center) _target_position = world_center + Vector3.UP * height_offset if instant: global_position = _target_position _is_moving = false else: _is_moving = true func _pick_cell(mouse_position: Vector2) -> Vector2i: var ray_origin: Vector3 = _camera.project_ray_origin(mouse_position) var ray_direction: Vector3 = _camera.project_ray_normal(mouse_position) var plane_normal: Vector3 = _grid_manager.global_transform.basis.y.normalized() var plane_point: Vector3 = _grid_manager.global_transform.origin var denominator: float = plane_normal.dot(ray_direction) if abs(denominator) < 0.0001: return Vector2i(-1, -1) var t: float = plane_normal.dot(plane_point - ray_origin) / denominator if t <= 0.0: return Vector2i(-1, -1) var world_point: Vector3 = ray_origin + ray_direction * t return _grid_manager.world_to_cell(world_point, true) func _on_grid_layout_changed(_cells: Dictionary) -> void: if not _grid_manager: return if not _grid_manager.has_cell(_current_cell): _current_cell = Vector2i( clamp(_current_cell.x, 0, _grid_manager.columns - 1), clamp(_current_cell.y, 0, _grid_manager.rows - 1) ) _move_to_cell(_current_cell, true) func _resolve_references() -> void: if grid_manager_path != NodePath(): _grid_manager = get_node_or_null(grid_manager_path) as GridPlaneManager else: _grid_manager = get_parent() as GridPlaneManager if camera_path != NodePath(): _camera = get_node_or_null(camera_path) as Camera3D