extends CharacterBody3D ## How fast the player slides between tiles (units per second). @export var move_speed: float = 6.0 ## How fast the player rotates 90 degrees (degrees per second). @export var turn_speed: float = 360.0 # Movement state var _is_moving := false var _is_turning := false var _move_start := Vector3.ZERO var _move_target := Vector3.ZERO var _move_progress := 0.0 var _turn_start := 0.0 var _turn_target := 0.0 var _turn_progress := 0.0 func _ready() -> void: position = Grid.snap(position) # Snap rotation to nearest 90° so we stay grid-aligned rotation.y = roundf(rotation.y / (PI / 2.0)) * (PI / 2.0) func _physics_process(delta: float) -> void: if _is_moving: _process_move(delta) elif _is_turning: _process_turn(delta) else: _handle_input() func _handle_input() -> void: var forward := -transform.basis.z forward.y = 0.0 forward = forward.normalized() # Forward / back if Input.is_action_just_pressed("move_forward"): _try_move(forward) elif Input.is_action_just_pressed("move_back"): _try_move(-forward) # Turn left / right elif Input.is_action_just_pressed("turn_left"): _start_turn(-1) elif Input.is_action_just_pressed("turn_right"): _start_turn(1) # Interact with entities on this tile elif Input.is_action_just_pressed("interact"): _try_interact() func _try_move(direction: Vector3) -> void: direction = _snap_direction(direction) var target := position + direction * Grid.CELL_SIZE # Raycast to check for walls before moving var ray_start := position + Vector3(0, 0.5, 0) var ray_end := target + Vector3(0, 0.5, 0) var space := get_world_3d().direct_space_state var query := PhysicsRayQueryParameters3D.create( ray_start, ray_end, collision_mask ) query.exclude = [get_rid()] var result := space.intersect_ray(query) print("--- MOVE ATTEMPT ---") print(" from: ", position, " to: ", target) print(" direction: ", direction) print(" ray from: ", ray_start, " ray to: ", ray_end) print(" collision_mask: ", collision_mask) if result.is_empty(): print(" result: CLEAR - moving") _move_start = position _move_target = target _move_progress = 0.0 _is_moving = true else: print(" result: BLOCKED") print(" hit collider: ", result.collider.name if result.collider else "unknown") print(" hit position: ", result.position) print(" hit normal: ", result.normal) func _process_move(delta: float) -> void: var distance := _move_start.distance_to(_move_target) _move_progress += (move_speed * delta) / distance _move_progress = minf(_move_progress, 1.0) position = _move_start.lerp(_move_target, _move_progress) if _move_progress >= 1.0: position = _move_target _is_moving = false _check_step_on() func _start_turn(direction: int) -> void: _turn_start = rotation.y _turn_target = _turn_start - direction * (PI / 2.0) _turn_progress = 0.0 _is_turning = true func _process_turn(delta: float) -> void: _turn_progress += (deg_to_rad(turn_speed) * delta) / abs(_turn_target - _turn_start) _turn_progress = minf(_turn_progress, 1.0) rotation.y = lerpf(_turn_start, _turn_target, _turn_progress) if _turn_progress >= 1.0: rotation.y = _turn_target _is_turning = false func _try_interact() -> void: var my_cell := Grid.world_to_cell(position) # Same-cell interactables (statues, etc.) for node in get_tree().get_nodes_in_group("interactable"): var entity := node as Node3D if my_cell == Grid.world_to_cell(entity.global_position): entity.interact() return # Facing interactables (doors) — detected via their own Area3D for node in get_tree().get_nodes_in_group("facing_interactable"): if node.try_interact(self): return func _check_step_on() -> void: var my_cell := Grid.world_to_cell(position) for node in get_tree().get_nodes_in_group("steppable"): var entity := node as Node3D if my_cell == Grid.world_to_cell(entity.global_position): entity.stepped_on() func _snap_direction(dir: Vector3) -> Vector3: if absf(dir.x) >= absf(dir.z): return Vector3(signf(dir.x), 0.0, 0.0) else: return Vector3(0.0, 0.0, signf(dir.z))