8776a6b362
Work-Package: M02-GMP-INPUT-PROFILE-335-001 Agent: sindo-main-codex Tests: player input/movement/capability/terrain/camera/presentation, coordinate/streaming, docs/coordination and renderer dry-run passed Fidelity: build-12340 binding hashes and selected defaults pinned; TrinityCore turn rate and WoWee mouse routing are compatibility references with explicit limits
176 lines
5.8 KiB
GDScript
176 lines
5.8 KiB
GDScript
class_name ThirdPersonCameraRig
|
|
extends Node3D
|
|
|
|
## Third-person orbit/zoom state and Camera3D presentation adapter.
|
|
## The rig owns camera input state and local transforms. Character movement,
|
|
## terrain, rendering and collision-policy decisions remain external boundaries.
|
|
|
|
const NO_CAMERA_COLLISION_POLICY_SCRIPT := preload("res://src/scenes/player/no_camera_collision_policy.gd")
|
|
|
|
## Relative Camera3D child used by this rig.
|
|
@export var camera_path: NodePath = NodePath("Camera3D")
|
|
## Mouse radians applied per input pixel.
|
|
@export var mouse_sensitivity_radians_per_pixel: float = 0.003
|
|
## Minimum orbit pitch in radians.
|
|
@export var minimum_pitch_radians: float = deg_to_rad(-65.0)
|
|
## Maximum orbit pitch in radians.
|
|
@export var maximum_pitch_radians: float = deg_to_rad(35.0)
|
|
## Pivot height above the character origin in Godot units.
|
|
@export var pivot_height_units: float = 1.7
|
|
## Initial requested camera distance in Godot units.
|
|
@export var initial_distance_units: float = 8.0
|
|
## Minimum user-requested zoom distance in Godot units.
|
|
@export var minimum_distance_units: float = 2.0
|
|
## Maximum user-requested zoom distance in Godot units.
|
|
@export var maximum_distance_units: float = 18.0
|
|
## Distance change per zoom action in Godot units.
|
|
@export var zoom_step_units: float = 1.0
|
|
## Camera far plane retained from the renderer sandbox baseline.
|
|
@export var camera_far_plane_units: float = 50000.0
|
|
|
|
var yaw_radians: float:
|
|
get:
|
|
return _yaw_radians
|
|
|
|
var pitch_radians: float:
|
|
get:
|
|
return _pitch_radians
|
|
|
|
var requested_distance_units: float:
|
|
get:
|
|
return _requested_distance_units
|
|
|
|
var resolved_distance_units: float:
|
|
get:
|
|
return _resolved_distance_units
|
|
|
|
var is_mouse_captured: bool:
|
|
get:
|
|
return _is_mouse_captured
|
|
|
|
var _character_body: Node3D
|
|
var _camera: Camera3D
|
|
var _collision_policy: CameraCollisionPolicy
|
|
var _yaw_radians := 0.0
|
|
var _pitch_radians := deg_to_rad(-18.0)
|
|
var _requested_distance_units := 8.0
|
|
var _resolved_distance_units := 8.0
|
|
var _is_mouse_captured := false
|
|
|
|
|
|
func _ready() -> void:
|
|
_camera = get_node_or_null(camera_path) as Camera3D
|
|
if _camera == null:
|
|
push_error("ThirdPersonCameraRig: Camera3D not found at %s" % camera_path)
|
|
if _collision_policy == null:
|
|
_collision_policy = NO_CAMERA_COLLISION_POLICY_SCRIPT.new()
|
|
_pitch_radians = clampf(_pitch_radians, minimum_pitch_radians, maximum_pitch_radians)
|
|
_requested_distance_units = clampf(
|
|
initial_distance_units,
|
|
minimum_distance_units,
|
|
maximum_distance_units
|
|
)
|
|
if _camera != null:
|
|
_camera.current = true
|
|
_camera.far = camera_far_plane_units
|
|
refresh_camera_transform()
|
|
|
|
|
|
func _physics_process(_delta_seconds: float) -> void:
|
|
refresh_camera_transform()
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
if _is_mouse_captured:
|
|
_is_mouse_captured = false
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
|
|
|
|
## Attaches the character transform rotated by orbit input and captures its initial yaw.
|
|
func initialize_for_character(character_body: Node3D) -> void:
|
|
_character_body = character_body
|
|
if _character_body != null:
|
|
_yaw_radians = _character_body.rotation.y
|
|
refresh_camera_transform()
|
|
|
|
|
|
## Synchronizes orbit yaw after keyboard turning changes the character transform.
|
|
func synchronize_yaw_from_character() -> void:
|
|
if _character_body != null:
|
|
_yaw_radians = _character_body.rotation.y
|
|
|
|
|
|
## Replaces camera-distance collision behavior and immediately refreshes the camera.
|
|
## Passing null is rejected and preserves the current policy.
|
|
func set_collision_policy(collision_policy: CameraCollisionPolicy) -> void:
|
|
if collision_policy == null:
|
|
push_error("ThirdPersonCameraRig: collision policy cannot be null")
|
|
return
|
|
_collision_policy = collision_policy
|
|
refresh_camera_transform()
|
|
|
|
|
|
## Applies remappable camera actions and orbit mouse motion.
|
|
## Returns true when the event changed or consumed camera state.
|
|
func handle_camera_input(event: InputEvent) -> bool:
|
|
if event.is_action_pressed(PlayerInputActions.CAMERA_ROTATE):
|
|
_is_mouse_captured = true
|
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
return true
|
|
if event.is_action_released(PlayerInputActions.CAMERA_ROTATE):
|
|
_is_mouse_captured = false
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
return true
|
|
if event.is_action_pressed(PlayerInputActions.CAMERA_ZOOM_IN):
|
|
_requested_distance_units = clampf(
|
|
_requested_distance_units - zoom_step_units,
|
|
minimum_distance_units,
|
|
maximum_distance_units
|
|
)
|
|
refresh_camera_transform()
|
|
return true
|
|
if event.is_action_pressed(PlayerInputActions.CAMERA_ZOOM_OUT):
|
|
_requested_distance_units = clampf(
|
|
_requested_distance_units + zoom_step_units,
|
|
minimum_distance_units,
|
|
maximum_distance_units
|
|
)
|
|
refresh_camera_transform()
|
|
return true
|
|
if event.is_action_pressed(PlayerInputActions.RELEASE_CURSOR):
|
|
_is_mouse_captured = false
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
return true
|
|
if _is_mouse_captured and event is InputEventMouseMotion:
|
|
_yaw_radians -= event.relative.x * mouse_sensitivity_radians_per_pixel
|
|
_pitch_radians = clampf(
|
|
_pitch_radians - event.relative.y * mouse_sensitivity_radians_per_pixel,
|
|
minimum_pitch_radians,
|
|
maximum_pitch_radians
|
|
)
|
|
if _character_body != null:
|
|
_character_body.rotation.y = _yaw_radians
|
|
refresh_camera_transform()
|
|
return true
|
|
return false
|
|
|
|
|
|
## Returns the pivot's Godot-world basis for camera-relative sandbox flight.
|
|
func godot_world_flight_movement_basis() -> Basis:
|
|
return global_basis
|
|
|
|
|
|
## Applies pivot and Camera3D local transforms using the active collision policy.
|
|
func refresh_camera_transform() -> void:
|
|
position = Vector3(0.0, pivot_height_units, 0.0)
|
|
rotation.x = _pitch_radians
|
|
if _camera == null:
|
|
return
|
|
var policy_distance := _collision_policy.resolve_camera_distance(
|
|
self,
|
|
_requested_distance_units
|
|
) if _collision_policy != null else _requested_distance_units
|
|
_resolved_distance_units = clampf(policy_distance, 0.0, _requested_distance_units)
|
|
_camera.position = Vector3(0.0, 0.0, _resolved_distance_units)
|
|
_camera.rotation = Vector3.ZERO
|