extends CharacterBody3D const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd") const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd") const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd") const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd") const PLAYER_INPUT_SOURCE_SCRIPT := preload("res://src/gameplay/input/player_input_source.gd") const PLAYER_INPUT_ACTIONS_SCRIPT := preload("res://src/gameplay/input/player_input_actions.gd") const LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT := preload("res://src/gameplay/movement/local_player_movement_controller.gd") const PLAYER_MOVEMENT_CAPABILITIES_SCRIPT := preload("res://src/gameplay/movement/player_movement_capabilities.gd") const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrain_query.gd") @export var extracted_dir: String = "res://data/extracted" @export var map_name: String = "Azeroth" @export var spawn_tile_x: int = 42 @export var spawn_tile_y: int = 28 @export var spawn_at_tile_center: bool = true @export var run_speed: float = 7.0 @export var backward_speed: float = 4.5 @export var strafe_speed: float = 4.5 @export var sprint_multiplier: float = 6.0 @export var flight_vertical_speed: float = 7.0 @export var acceleration: float = 28.0 ## TrinityCore 3.3.5 player MOVE_TURN_RATE default, in radians per second. @export var keyboard_turn_speed_radians_per_second: float = 3.141594 ## Local composition profile. RenderSandbox preserves debug sprint/free flight; ## Blizzlike335 rejects them but does not yet claim complete movement parity. @export_enum("RenderSandbox", "Blizzlike335") var movement_profile_id: String = "RenderSandbox" @export var ground_offset: float = 0.05 @export var ground_snap_speed: float = 24.0 @export var camera_pivot_path: NodePath = NodePath("CameraPivot") @export var visual_path: NodePath = NodePath("Visual") @export var animation_presenter_path: NodePath = NodePath("Visual/AnimationPresenter") var _camera_rig: ThirdPersonCameraRig var _appearance_presenter: CharacterAppearancePresenter var _animation_presenter: CharacterAnimationPresenter var _player_input_source: PlayerInputSource var _local_movement_controller: LocalPlayerMovementController var _terrain_query: TerrainQuery ## Replaces the ground-height backend without changing player movement or presentation. ## Passing null is a composition error and leaves the current query unchanged. func set_terrain_query(terrain_query: TerrainQuery) -> void: if terrain_query == null: push_error("ThirdPersonWowController: terrain query cannot be null") return _terrain_query = terrain_query func _ready() -> void: _player_input_source = PLAYER_INPUT_SOURCE_SCRIPT.new(StringName(movement_profile_id)) if _terrain_query == null: _terrain_query = ADT_TERRAIN_QUERY_SCRIPT.new(extracted_dir, map_name) _local_movement_controller = LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT.new( run_speed, backward_speed, strafe_speed, flight_vertical_speed, acceleration, sprint_multiplier, PLAYER_MOVEMENT_CAPABILITIES_SCRIPT.for_profile_id(StringName(movement_profile_id)), keyboard_turn_speed_radians_per_second ) _camera_rig = get_node_or_null(camera_pivot_path) as ThirdPersonCameraRig _appearance_presenter = get_node_or_null(visual_path) as CharacterAppearancePresenter _animation_presenter = get_node_or_null(animation_presenter_path) as CharacterAnimationPresenter if _camera_rig == null: push_error("ThirdPersonWowController: ThirdPersonCameraRig not found at %s" % camera_pivot_path) else: _camera_rig.initialize_for_character(self) if _appearance_presenter == null: push_error("ThirdPersonWowController: CharacterAppearancePresenter not found at %s" % visual_path) else: _appearance_presenter.character_appearance_ready.connect(_on_character_appearance_ready) _appearance_presenter.load_character_appearance(extracted_dir) if _animation_presenter == null: push_error("ThirdPersonWowController: CharacterAnimationPresenter not found at %s" % animation_presenter_path) if spawn_at_tile_center: var spawn_tile = ADT_TILE_COORDINATE_SCRIPT.new(spawn_tile_x, spawn_tile_y) var half_tile_size := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS * 0.5 var spawn_local = ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, global_position.y, half_tile_size) var spawn_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(spawn_tile, spawn_local) global_position = Vector3(spawn_position.x_units, spawn_position.y_units, spawn_position.z_units) var spawn_ground_sample := _sample_ground_height(global_position) if spawn_ground_sample.is_available: global_position.y = spawn_ground_sample.height_units + ground_offset func _unhandled_input(event: InputEvent) -> void: if _camera_rig != null: _camera_rig.handle_camera_input(event) if event.is_action_pressed(PLAYER_INPUT_ACTIONS_SCRIPT.DEBUG_TOGGLE_FLIGHT) and not event.is_echo(): _local_movement_controller.toggle_sandbox_flight() func _physics_process(delta: float) -> void: var move_intent := _player_input_source.sample_move_intent() var yaw_delta_radians := _local_movement_controller.calculate_yaw_delta_radians( move_intent, delta ) if not is_zero_approx(yaw_delta_radians): rotation.y += yaw_delta_radians if _camera_rig != null: _camera_rig.synchronize_yaw_from_character() var godot_world_movement_basis := global_basis if _local_movement_controller.is_flight_enabled and _camera_rig != null: godot_world_movement_basis = _camera_rig.godot_world_flight_movement_basis() global_position += _local_movement_controller.advance(move_intent, godot_world_movement_basis, delta) if not _local_movement_controller.is_flight_enabled: var ground_sample := _sample_ground_height(global_position) if ground_sample.is_available: var target_y := ground_sample.height_units + ground_offset global_position.y = lerpf(global_position.y, target_y, clampf(ground_snap_speed * delta, 0.0, 1.0)) var movement_velocity := _local_movement_controller.godot_world_velocity_units_per_second var horizontal_motion := Vector2(movement_velocity.x, movement_velocity.z) if _appearance_presenter != null and horizontal_motion.length_squared() > 0.01: _appearance_presenter.global_rotation.y = atan2(-movement_velocity.x, -movement_velocity.z) if _animation_presenter != null: _animation_presenter.present_locomotion(horizontal_motion.length_squared() > 0.04) func _on_character_appearance_ready(character_root: Node3D) -> void: if _animation_presenter != null: _animation_presenter.bind_character_root(character_root) func _sample_ground_height(godot_world_position: Vector3) -> TerrainGroundSample: return _terrain_query.sample_ground_height( GODOT_WORLD_POSITION_SCRIPT.new( godot_world_position.x, godot_world_position.y, godot_world_position.z ) )