51 lines
1.5 KiB
GDScript
51 lines
1.5 KiB
GDScript
extends Node3D
|
|
|
|
## NS-16: walkable ground ray pick. Emits world target for MoveCommand; camera wired from main.
|
|
## (No `class_name` so headless/CI can load the project before `.godot` import exists.)
|
|
|
|
signal target_chosen(world: Vector3)
|
|
|
|
## Fallback when `Viewport.get_camera_3d()` is null (e.g. some editor setups). Set from `main.gd`
|
|
## in `_ready`.
|
|
var fallback_camera: Camera3D
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if event is InputEventMouseButton:
|
|
var mb := event as InputEventMouseButton
|
|
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
|
|
_try_pick(get_viewport().get_mouse_position())
|
|
|
|
|
|
func _try_pick(screen_pos: Vector2) -> void:
|
|
var cam: Camera3D = get_viewport().get_camera_3d()
|
|
if cam == null:
|
|
cam = fallback_camera
|
|
if cam == null:
|
|
return
|
|
|
|
var origin: Vector3 = cam.project_ray_origin(screen_pos)
|
|
var ray_dir: Vector3 = cam.project_ray_normal(screen_pos)
|
|
var to: Vector3 = origin + ray_dir * 2000.0
|
|
var query := PhysicsRayQueryParameters3D.create(origin, to)
|
|
query.collision_mask = 1
|
|
var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
|
|
var hit: Dictionary = space.intersect_ray(query)
|
|
if hit.is_empty():
|
|
return
|
|
var collider: Variant = hit.get("collider")
|
|
if not _collider_is_walkable(collider):
|
|
return
|
|
var hit_pos: Variant = hit.get("position")
|
|
if hit_pos is Vector3:
|
|
target_chosen.emit(hit_pos as Vector3)
|
|
|
|
|
|
func _collider_is_walkable(collider: Variant) -> bool:
|
|
var n: Node = collider as Node
|
|
while n:
|
|
if n.is_in_group("walkable"):
|
|
return true
|
|
n = n.get_parent()
|
|
return false
|