open-fpsz/entities/player/player.gd

230 lines
7.2 KiB
GDScript

# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
class_name Player extends RigidBody3D
enum PlayerState { PLAYER_ALIVE, PLAYER_DEAD }
@export_category("Parameters")
@export var ground_speed : float = 48 / 3.6 # m/s
@export var aerial_control_force : int = 400
@export var jump_height : float = 2.0
@export var max_floor_angle : float = 60
@export_group("Jetpack")
@export var energy: float = 100.0
@export var energy_charge_rate : float = 20 # energy per second
@export var energy_drain_rate : float = 25 # energy per second
@export var energy_max : float = 100.
@export var jetpack_force_factor : float = 2.
@export var jetpack_horizontal_force : float = 600
@export var jetpack_vertical_force : float = 800
@export_group("State")
@export var player_state = PlayerState.PLAYER_ALIVE
@export var player_id : int = 1:
set(id):
player_id = id
$PlayerInput.set_multiplayer_authority(id)
@export var nickname : String
@onready var input : PlayerInput = $PlayerInput
@onready var camera = $SpringArm3D/Camera3D
@onready var hud = $HUD
@onready var iff = $ThirdPerson/IFF
@onready var shape_cast : ShapeCast3D = $ShapeCast3D
@onready var weapon = $SpringArm3D/Inventory/SpaceGun
@onready var animation_player : AnimationPlayer = $AnimationPlayer
@onready var health_component = $HealthComponent
@onready var flag_carry_component : FlagCarryComponent = $FlagCarryComponent
@onready var spring_arm_height = $SpringArm3D.position.y
@onready var _original_weapon_transform : Transform3D = weapon.transform
@onready var flag_carry_attachment = $SpringArm3D/FlagCarryAttachment
signal died(player)
signal energy_changed(energy)
var g : float = ProjectSettings.get_setting("physics/3d/default_gravity") # in m/s²
var gravity : Vector3 = g * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
var _jumping = false
func _ready():
energy_changed.connect(hud._on_energy_changed)
if _is_first_person():
health_component.health_changed.connect(hud._on_health_changed)
camera.current = true
remove_child($ThirdPerson)
else:
health_component.health_changed.connect(iff._on_health_changed)
remove_child(hud)
weapon.hide()
health_component.health_changed.emit(health_component.health)
health_component.health_zeroed.connect(die)
input.fired_primary.connect(_fire_primary)
input.jumped.connect(_jump)
input.throwed_flag.connect(_throw_flag)
func _is_first_person():
return player_id == multiplayer.get_unique_id()
func _fire_primary():
if _is_player_dead():
return
if not weapon.can_fire():
return
var current_weapon_transform = weapon.transform
weapon.transform = _original_weapon_transform
weapon.fire_primary()
weapon.transform = current_weapon_transform
if _is_first_person():
animation_player.stop()
animation_player.play("shoot")
func _jump():
if _is_player_dead():
return
_jumping = true
func _throw_flag():
flag_carry_component.throw(linear_velocity)
func is_on_floor() -> bool:
if shape_cast.is_colliding():
for i in shape_cast.get_collision_count():
var collider = shape_cast.get_collider(i)
if collider is Terrain3D:
return true
return false
func _is_skiing() -> bool:
return input.skiing
func _handle_aerial_control(direction):
if not input.jetting and not is_on_floor():
apply_force(direction * aerial_control_force)
func _handle_jetpack(delta, direction):
if input.jetting:
if energy > 0:
var up_vector = Vector3.UP * jetpack_vertical_force * jetpack_force_factor
var side_vector = direction * jetpack_horizontal_force * jetpack_force_factor
apply_force(up_vector + side_vector)
energy -= energy_drain_rate * delta
else:
energy += energy_charge_rate * delta
energy = clamp(energy, 0, energy_max)
energy_changed.emit(energy)
func _process(_delta):
if _is_player_dead():
iff.hide()
return
else:
iff.show()
if not _is_first_person():
$ThirdPerson/PlayerMesh.global_transform.basis = Basis.from_euler(Vector3(0.0, input.camera_rotation.x + PI, 0.0))
else:
if animation_player.current_animation == "shoot":
pass
else:
animation_player.play("idle")
%SpringArm3D.global_transform.basis = Basis.from_euler(Vector3(input.camera_rotation.y, input.camera_rotation.x, 0.0))
func _physics_process(delta):
# retrieve user's direction vector
var _input_dir = input.direction
# compute direction in local space
var _direction = (transform.basis * Vector3(_input_dir.x, 0, _input_dir.y)).normalized()
_update_third_person_animations()
if _is_player_dead():
return
# adjust direction based on spring arm rotation
_direction = _direction.rotated(Vector3.UP, $SpringArm3D.rotation.y)
_handle_aerial_control(_direction)
_handle_jetpack(delta, _direction)
# handle ski
if _is_skiing():
physics_material_override.friction = 0
else:
physics_material_override.friction = 1
if is_on_floor():
if not _direction.is_zero_approx() and not _is_skiing():
# retrieve collision normal
var normal = shape_cast.get_collision_normal(0)
# calculate the angle between the ground normal and the up vector
var slope_angle = rad_to_deg(acos(normal.dot(Vector3.UP)))
# check if the slope angle exceeds the maximum slope angle
if slope_angle <= max_floor_angle:
# adjust direction based on the floor normal to align with the slope
_direction = _direction.slide(normal)
linear_velocity = lerp(linear_velocity, _direction * ground_speed, .1)
func _integrate_forces(_state):
if is_on_floor() and _jumping:
var v = sqrt(2 * g * jump_height)
apply_central_impulse(Vector3(0, mass * v, 0))
_jumping = false
func _update_third_person_animations():
if _is_first_person():
return
var tp_player : Vanguard = $ThirdPerson/PlayerMesh
if _is_player_dead():
tp_player.set_ground_state(Vanguard.GroundState.GROUND_STATE_DEAD)
return
if is_on_floor():
tp_player.set_ground_state(Vanguard.GroundState.GROUND_STATE_GROUNDED)
else:
tp_player.set_ground_state(Vanguard.GroundState.GROUND_STATE_MID_AIR)
var local_velocity = (tp_player.global_basis.inverse() * linear_velocity)
const bias : float = 1.2 # Basically match feet speed with ground speed
tp_player.set_locomotion(Vector2(local_velocity.x, local_velocity.z), bias)
func _is_player_dead():
return player_state == PlayerState.PLAYER_DEAD
func die():
player_state = PlayerState.PLAYER_DEAD
if _is_first_person():
animation_player.stop()
animation_player.play("death")
var tween = create_tween()
tween.tween_interval(4)
tween.tween_callback(func():
died.emit(self)
if _is_first_person():
animation_player.stop()
)
flag_carry_component.drop()
@rpc("call_local")
func set_nickname(value):
nickname = value
func respawn(location):
linear_velocity = Vector3()
health_component.heal_full()
position = location
player_state = PlayerState.PLAYER_ALIVE