class_name Player extends CharacterBody3D signal bounced() signal shot() signal charge_canceled() @export_group("Visuals") @export var goal_animation_time: float = 1.0 @export var charge_gradient: Gradient @export var power_line_material: StandardMaterial3D @export_group("Movement") @export var gravity: float @export var friction: float @export var friction_coef: float @export var friction_pow: float @export var power_scale: float @export var power_sensitivity: float @export var power_threshold: float @export var stop_threshold: float @export_group("Camera", "camera_") @export_range(0,90,1,"radians_as_degrees") var camera_low_angle: float @export_range(0,90,1,"radians_as_degrees") var camera_high_angle: float @export_range(0,90,0.5,"radians_as_degrees") var camera_yaw_sensitivity: float @export_range(0,90,0.5,"radians_as_degrees") var camera_pitch_sensitivity: float @export_group("Node References") @export var state_chart: StateChart @export var graphics: Node3D @export var power_indicator: Node3D @export var camera_arm: SpringArm3D @export var collision_shape: CollisionShape3D var power: float = 0.0: set(value): power = clampf(value, 0.0, 1.0) var charging_power: bool = false var prev_velocity: Vector3 = Vector3.ZERO var _entered_goal: Node3D = null #region Builtin Overrides func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func _physics_process(delta: float) -> void: prev_velocity = velocity move_and_slide() state_chart.set_expression_property(&"velocity", velocity) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed(&"charge_shot"): state_chart.send_event(&"charge_pressed") if event.is_action_released(&"charge_shot"): state_chart.send_event(&"charge_released") if event is InputEventMouseMotion: camera_arm.rotate_y(event.screen_relative.x * camera_yaw_sensitivity) if charging_power: power += event.screen_relative.y * power_sensitivity else: camera_arm.rotation.x -= event.screen_relative.y * camera_pitch_sensitivity camera_arm.rotation.x = clampf(camera_arm.rotation.x, -camera_high_angle, -camera_low_angle) #endregion #region Public Functions func enter_goal(goal: GoalPost) -> void: _entered_goal = goal state_chart.send_event(&"goal_entered") func attach_to_pole(pole: WatermanPole) -> void: _attached_pole = pole state_chart.send_event(&"pole_attached") #endregion #region Charging func _start_charge() -> void: charging_power = true power_indicator.visible = true power_indicator.scale.z = 0.0 power = 0.0 func _update_charge(_delta: float) -> void: power_indicator.scale.z = power var camera_z = get_viewport().get_camera_3d().global_transform.basis.z camera_z.y = 0.0 power_indicator.look_at(power_indicator.global_position + camera_z) power_line_material.albedo_color = charge_gradient.sample(power) func _end_charge() -> void: charging_power = false power_indicator.visible = false if power >= power_threshold: var camera_z = get_viewport().get_camera_3d().global_transform.basis.z camera_z.y = 0.0 velocity = -camera_z.normalized() * power * power_scale prev_velocity = velocity _bounce_on_walls(1.0/60.0) shot.emit() else: charge_canceled.emit() #endregion #region Moving func _apply_gravity(delta: float) -> void: velocity.y -= gravity * delta func _slow_to_stop(delta: float) -> void: if is_on_floor(): var new_velocity = velocity * Vector3(1.0, 0.0, 1.0) #new_velocity = lerp(new_velocity, Vector3.ZERO, friction_coef * delta) #new_velocity = lerp( #new_velocity, Vector3.ZERO, #power_scale * pow(friction_coef / new_velocity.length(), friction_pow * delta) #) new_velocity = new_velocity.move_toward(Vector3.ZERO, friction * delta) if new_velocity.length_squared() <= stop_threshold * stop_threshold: new_velocity = Vector3.ZERO velocity.x = new_velocity.x velocity.z = new_velocity.z func _bounce_on_walls(delta: float = 0.0) -> void: var h_vel = (prev_velocity * Vector3(1.0, 0.0, 1.0)) var col = move_and_collide(h_vel * delta, true) if col: if col.get_angle() > floor_max_angle: h_vel = h_vel.bounce(col.get_normal()) velocity.x = h_vel.x velocity.z = h_vel.z bounced.emit() #endregion #region Winning func _start_winning() -> void: velocity = Vector3.ZERO prev_velocity = velocity collision_shape.disabled = true var tween = create_tween() tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS) tween.tween_property(graphics, ^"scale", Vector3.ZERO, goal_animation_time) tween.set_parallel(true) tween.tween_property(graphics, ^"global_position", _entered_goal.global_position, goal_animation_time) #endregion #region Pole Spinning var _attached_pole: WatermanPole = null var _pole_angle: float = 0.0 var _pole_stored_speed: float = 0.0 func _start_pole_spin() -> void: _pole_stored_speed = flatten_vector(velocity).length() velocity = Vector3.ZERO var pole_xz = flatten_vector(_attached_pole.global_position) var self_xz = flatten_vector(global_position) _pole_angle = Vector3.FORWARD.angle_to(self_xz - pole_xz) func _process_pole_spin(delta: float) -> void: # rise global_position.y += _attached_pole.rise_speed * delta global_position.y = clampf( global_position.y, _attached_pole.global_position.y, _attached_pole.top.global_position.y ) # spin _pole_angle += _attached_pole.spin_speed * delta var pole_xz = flatten_vector(_attached_pole.global_position) var self_dir = Vector3.FORWARD.rotated(Vector3.UP, _pole_angle) var self_xz = pole_xz + self_dir * _attached_pole.offset global_position.x = self_xz.x global_position.z = self_xz.z func _end_pole_spin() -> void: var pole_xz = flatten_vector(_attached_pole.global_position) var impulse = Vector3.FORWARD.rotated(Vector3.UP, _pole_angle) * _pole_stored_speed velocity.x = impulse.x velocity.z = impulse.z #endregion #region Helpers func flatten_vector(vector: Vector3) -> Vector3: return Vector3(vector.x, 0.0, vector.z) #endregion