mirror of
https://gitlab.com/open-fpsz/open-fpsz.git
synced 2026-01-19 19:44:46 +00:00
344 lines
12 KiB
GDScript
344 lines
12 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
|
|
|
|
signal killed(victim: Player, killer:int)
|
|
|
|
## Emitted when a source wants to damage this body
|
|
signal damage(source: Node, target: Node, amount: int)
|
|
|
|
## Emitted when the player respawns, see [member Player.respawn].
|
|
signal respawned(player: Player)
|
|
|
|
@export var iff:IFF
|
|
## This is the player [Health] component.
|
|
@export var health:Health
|
|
@export var flag_carry_component:FlagCarryComponent
|
|
@export var walkable_surface_sensor:ShapeCast3D
|
|
@export var hud:HUD
|
|
## The inventory component can store up to [member Inventory.slots] nodes.
|
|
@export var inventory:Inventory
|
|
@export var tp_mesh:Vanguard
|
|
@export var third_person:Node3D
|
|
@export var pivot : Node3D
|
|
@export var camera : Camera3D
|
|
|
|
@export_category("Parameters")
|
|
@export var ground_speed:float = 48 / 3.6 # m/s
|
|
@export var aerial_control_force:int = 320
|
|
@export var jump_height:float = 1.0
|
|
@export var max_floor_angle:float = 60
|
|
|
|
@export_group("Jetpack")
|
|
@export var energy_max:float = 100.
|
|
@export var energy: float = energy_max
|
|
@export var energy_charge_rate:float = 25 # energy per second
|
|
@export var energy_drain_rate:float = 30 # energy per second
|
|
@export var jetpack_force_factor:float = 2.
|
|
@export var jetpack_horizontal_force:float = 700
|
|
@export var jetpack_vertical_force:float = 900
|
|
@export_range(0., 1., .01) var stutter_treshold:float = .3
|
|
|
|
@export_group("State")
|
|
@export var input: PlayerInputController
|
|
|
|
@onready var animation_player:AnimationPlayer = $AnimationPlayer
|
|
@onready var collision_shape:CollisionShape3D = $CollisionShape3D
|
|
@onready var _jetpack_particles:Array = tp_mesh.get_node("JetpackFX").get_children()
|
|
|
|
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:bool = false
|
|
var _stutter:bool = false
|
|
var _stutter_treshold:int = int(energy_max * .3)
|
|
|
|
static var pawn_player:Player
|
|
|
|
@export var username:String = "Newblood":
|
|
set = set_username
|
|
|
|
@export var peer_id:int = 1:
|
|
set = set_peer_id
|
|
|
|
@export var team_id:int = -1
|
|
|
|
signal username_changed(new_username:int)
|
|
signal peer_id_changed(new_peer_id:int)
|
|
|
|
func set_username(new_username:String) -> void:
|
|
username = new_username
|
|
username_changed.emit(username)
|
|
|
|
func set_peer_id(new_peer_id:int) -> void:
|
|
#remove_from_group(str(peer_id))
|
|
peer_id = new_peer_id
|
|
#add_to_group(str(peer_id))
|
|
peer_id_changed.emit(peer_id)
|
|
|
|
# Maximum duration for pumping force when throwing a flag, in seconds.
|
|
@export var throw_duration_max := 1.2
|
|
var throw_timer := Timer.new()
|
|
|
|
func _ready() -> void:
|
|
input.set_multiplayer_authority(peer_id)
|
|
|
|
username_changed.connect(iff.set_username)
|
|
username_changed.emit(username) # trigger initial signal
|
|
|
|
health.updated.connect(hud.health_bar.set_value)
|
|
health.updated.connect(iff.health_bar.set_value)
|
|
health.killed.connect(_on_killed)
|
|
|
|
input.jump.connect(_jump)
|
|
input.throw.connect(_on_throw)
|
|
throw_timer.one_shot = true
|
|
add_child(throw_timer)
|
|
|
|
# bind inventory
|
|
inventory.selected.connect(func(node: Node) -> void:
|
|
if node is Weapon:
|
|
if not input.primary.is_connected(node._on_primary):
|
|
input.primary.connect(node._on_primary)
|
|
if not node.ammo_changed.is_connected(hud._on_ammo_changed):
|
|
node.ammo_changed.connect(hud._on_ammo_changed)
|
|
node.ammo_changed.emit(node.ammo)
|
|
)
|
|
|
|
inventory.unselected.connect(func(node: Node) -> void:
|
|
if node is Weapon:
|
|
if input.primary.is_connected(node._on_primary):
|
|
input.primary.disconnect(node._on_primary)
|
|
if node is AutomaticWeapon: node._on_primary(false) # release trigger
|
|
if node.ammo_changed.is_connected(hud._on_ammo_changed):
|
|
node.ammo_changed.disconnect(hud._on_ammo_changed)
|
|
hud.ammo_label.text = ""
|
|
)
|
|
|
|
inventory.select(0)
|
|
|
|
if Game.type is Singleplayer:
|
|
damage.connect(
|
|
func(_source: Node, target: Node, amount: int) -> void:
|
|
target.health.damage(amount, 0))
|
|
|
|
input.camera_rotation.x = rotation.x
|
|
input.camera_rotation.y = rotation.y
|
|
|
|
if _is_pawn():
|
|
pawn_player = self
|
|
camera.current = true
|
|
camera.fov = Settings.get_value("video", "fov")
|
|
iff.hide()
|
|
# hide hud on pawn when scoreboard is visible in multiplayer
|
|
if Game.type is Multiplayer:
|
|
Game.type.scoreboard.visibility_changed.connect(
|
|
func() -> void: hud.set_visible(!Game.type.scoreboard.visible))
|
|
# forward this peer env settings to current viewport world env
|
|
var world: World3D = get_viewport().find_world_3d()
|
|
if not world.environment:
|
|
world.environment = Game.environment
|
|
world.environment.sdfgi_enabled = Game.environment.sdfgi_enabled
|
|
world.environment.glow_enabled = Game.environment.glow_enabled
|
|
world.environment.ssao_enabled = Game.environment.ssao_enabled
|
|
world.environment.ssr_enabled = Game.environment.ssr_enabled
|
|
world.environment.ssr_max_steps = Game.environment.ssr_max_steps
|
|
world.environment.ssil_enabled = Game.environment.ssil_enabled
|
|
world.environment.volumetric_fog_enabled = Game.environment.volumetric_fog_enabled
|
|
else:
|
|
third_person.show()
|
|
%Inventory.hide()
|
|
hud.hide()
|
|
|
|
func _process(_delta:float) -> void:
|
|
if not _is_pawn():
|
|
if Game.type is Multiplayer and Game.type.mode == Multiplayer.Mode.FREE_FOR_ALL:
|
|
iff.fill = Color.RED
|
|
elif is_instance_valid(pawn_player) and team_id:
|
|
iff.fill = Color.GREEN if team_id == pawn_player.team_id else Color.RED
|
|
_update_third_person_animations()
|
|
|
|
if not is_alive():
|
|
return
|
|
|
|
# compute target rotation from input.camera_rotation
|
|
var target_euler := Vector3(input.camera_rotation.x, input.camera_rotation.y, 0.0)
|
|
# smoothly interpolate rotation of pivot node towards target rotation
|
|
pivot.global_transform.basis = pivot.global_transform.basis.slerp(Basis.from_euler(target_euler), .6)
|
|
|
|
if not _is_pawn():
|
|
# compute target rotation from input.camera_rotation
|
|
var tp_target_euler := Vector3(.0, input.camera_rotation.y + PI, 0.0)
|
|
# smoothly interpolate rotation of third person node towards target rotation
|
|
tp_mesh.global_transform.basis = tp_mesh.global_transform.basis.slerp(Basis.from_euler(tp_target_euler), .6)
|
|
else:
|
|
if hud.throw_progress.is_visible_in_tree():
|
|
var time_elapsed: float = throw_duration_max - throw_timer.time_left
|
|
hud.throw_progress.set_value(time_elapsed / throw_duration_max * 100)
|
|
|
|
func _physics_process(delta:float) -> void:
|
|
_update_jetpack_energy(delta)
|
|
|
|
func _is_pawn() -> bool:
|
|
if Game.type is Multiplayer:
|
|
if Game.type.is_peer_connected():
|
|
return multiplayer.get_unique_id() == peer_id
|
|
else:
|
|
queue_free()
|
|
return true
|
|
|
|
func _on_throw(pressed: bool) -> void:
|
|
var flag: Flag = flag_carry_component._flag
|
|
if pressed and flag and flag.state == flag.FlagState.TAKEN:
|
|
throw_timer.start(throw_duration_max)
|
|
hud.throw_progress.visible = true
|
|
else:
|
|
var time_left: float = throw_timer.time_left
|
|
throw_timer.stop()
|
|
var time_elapsed: float = throw_duration_max - time_left
|
|
var throw_force: float = \
|
|
time_elapsed / throw_duration_max * flag_carry_component.throw_force
|
|
flag_carry_component.throw(linear_velocity,
|
|
clamp(throw_force, 5., flag_carry_component.throw_force))
|
|
hud.throw_progress.visible = false
|
|
|
|
|
|
# @NOTE: this method works only because `tp_mesh` duplicates weapons meshes from the inventory
|
|
func _on_inventory_selection_changed(_selected:Node3D, index:int) -> void:
|
|
# hide any visible weapon
|
|
for child in tp_mesh.hand_attachment.get_children():
|
|
child.hide()
|
|
# get corresponding selected weapon mesh for third person
|
|
var tp_weapon:Node3D = tp_mesh.hand_attachment.get_child(index)
|
|
if tp_weapon:
|
|
tp_weapon.show()
|
|
|
|
func _jump() -> void:
|
|
if not is_alive():
|
|
return
|
|
_jumping = true
|
|
|
|
func is_on_floor() -> bool:
|
|
return walkable_surface_sensor.is_colliding()
|
|
|
|
func _handle_aerial_control(direction:Vector3) -> void:
|
|
if not input.jetting and not is_on_floor():
|
|
apply_force(direction * aerial_control_force)
|
|
|
|
func _handle_jetpack(direction:Vector3) -> void:
|
|
if input.jetting and energy > 0 and not _stutter:
|
|
var up_vector:Vector3 = Vector3.UP * jetpack_vertical_force * jetpack_force_factor
|
|
var side_vector:Vector3 = direction * jetpack_horizontal_force * jetpack_force_factor
|
|
apply_force(up_vector + side_vector)
|
|
for particle: GPUParticles3D in _jetpack_particles:
|
|
particle.emitting = true
|
|
|
|
func _handle_ski() -> void:
|
|
# set ski state
|
|
physics_material_override.friction = !input.skiing
|
|
# zero-damping ski
|
|
linear_damp_mode = DAMP_MODE_REPLACE if is_on_floor() and input.skiing else DAMP_MODE_COMBINE
|
|
|
|
func _update_jetpack_energy(delta:float) -> void:
|
|
if input.jetting:
|
|
if energy == 0:
|
|
_stutter = true
|
|
elif energy > _stutter_treshold:
|
|
_stutter = false
|
|
|
|
if not _stutter:
|
|
energy -= energy_drain_rate * delta
|
|
else:
|
|
energy += energy_charge_rate * delta
|
|
else:
|
|
energy += energy_charge_rate * delta
|
|
|
|
energy = clamp(energy, 0, energy_max)
|
|
hud.energy_bar.value = energy
|
|
|
|
func _integrate_forces(_state:PhysicsDirectBodyState3D) -> void:
|
|
# skip if player is dead
|
|
if not is_alive():
|
|
return
|
|
# compute direction in local space
|
|
var _direction:Vector3 = (transform.basis * Vector3(
|
|
input.direction.x, 0, input.direction.y)).normalized()
|
|
|
|
# adjust direction based on pivot rotation
|
|
_direction = _direction.rotated(Vector3.UP, pivot.rotation.y)
|
|
|
|
if is_on_floor():
|
|
if not _direction.is_zero_approx() and not input.skiing:
|
|
# retrieve collision normal
|
|
var normal:Vector3 = walkable_surface_sensor.get_collision_normal(0)
|
|
# calculate the angle between the ground normal and the up vector
|
|
var slope_angle:float = 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)
|
|
|
|
if _jumping:
|
|
var v:float = sqrt(2. * g * jump_height)
|
|
apply_central_impulse(Vector3(0., mass * v, 0.))
|
|
|
|
_jumping = false
|
|
|
|
_handle_aerial_control(_direction)
|
|
_handle_jetpack(_direction)
|
|
_handle_ski()
|
|
|
|
func _update_third_person_animations() -> void:
|
|
if not is_alive():
|
|
tp_mesh.set_ground_state(Vanguard.GroundState.GROUND_STATE_DEAD)
|
|
return
|
|
|
|
if is_on_floor():
|
|
tp_mesh.set_ground_state(Vanguard.GroundState.GROUND_STATE_GROUNDED)
|
|
else:
|
|
tp_mesh.set_ground_state(Vanguard.GroundState.GROUND_STATE_MID_AIR)
|
|
|
|
var local_velocity:Vector3 = (tp_mesh.global_basis.inverse() * linear_velocity)
|
|
const bias:float = 1.2 # Basically match feet speed with ground speed
|
|
|
|
tp_mesh.set_locomotion(Vector2(local_velocity.x, local_velocity.z), bias)
|
|
|
|
func is_alive() -> bool:
|
|
assert(health)
|
|
return health.state == health.HealthState.ALIVE
|
|
|
|
func has_flag() -> bool:
|
|
return flag_carry_component._flag != null
|
|
|
|
func _on_killed(by_peer_id:int) -> void:
|
|
flag_carry_component.drop(linear_velocity)
|
|
if _is_pawn():
|
|
animation_player.play("death")
|
|
killed.emit(self, by_peer_id)
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func respawn(location: Vector3) -> void:
|
|
animation_player.stop()
|
|
linear_velocity = Vector3.ZERO
|
|
health.heal()
|
|
global_position = location
|
|
for weapon: Weapon in inventory.get_children():
|
|
weapon.set_ammo.rpc(weapon.max_ammo)
|
|
respawned.emit(self)
|
|
|
|
func _exit_tree() -> void:
|
|
flag_carry_component.drop(linear_velocity)
|