liblast/Game/Assets/Characters/Player.gd

548 lines
18 KiB
GDScript

extends CharacterBody3D
var impact_player = preload("res://Assets/Effects/ImpactBlood.tscn")
var max_health = 100
var health = max_health:
set(value):
if not dead:
if main.player_list.players.has(self.get_multiplayer_authority()):
main.player_list.players[self.get_multiplayer_authority()].health = value
main.push_local_player_info()
get:
if not dead:
return main.player_list.players[self.get_multiplayer_authority()].health
else:
return 0
@export var mouse_sensitivity := 0.15
@onready var main = get_tree().root.get_node("Main")
@onready var gui = main.get_node("GUI")
@onready var settings = gui.settings
@onready var hud = main.get_node("HUD")
@onready var crosshair = hud.get_node("Crosshair")
@onready var vignette = hud.get_node("Vignette")
@onready var banner_busy = preload("res://Assets/Effects/Busy.png")
@onready var banner_chat = preload("res://Assets/Effects/Typing.png")
@onready var head = $Head
@onready var camera = $Head/Camera
#@onready var tween = $Head/Camera/Tween
@onready var ground_check = $GroundCheck
#@onready var climb_tween = $ClimbTween # undergoing redesign in Godot 4
#@onready var climb_check = $ClimbCheck
@onready var body = $Body
@onready var mesh = $Mesh
@onready var weapon = $Head/Camera/Hand/Weapon
var gibs_vfx = preload("res://Assets/Effects/Gibs.tscn")
var blood_decal = preload("res://Assets/Decals/Blood/BloodSplash.tscn")
# climb functions - temporarily disabled
#@onready var body_height = body.shape.height
#@onready var body_y = body.position.y
#@onready var mesh_height = mesh.mesh.mid_height
#@onready var mesh_y = mesh.position.y
#@onready var climb_check_y = climb_check.position.y
@onready var ground_check_y = ground_check.position.y
var input_active = false
var base_fov = 90
var view_zoom_target := 1.0
var view_zoom_direction = true
var view_zoom := view_zoom_target :
set(zoom):
view_zoom = zoom
camera.fov = base_fov / zoom
crosshair.modulate.a = clamp(1 - (zoom - 1), 0, 1)
vignette.modulate.a = (zoom - 1) / 3
var revenge_pid: int #store PID of the player who killed you recently
# climbing code - disabled for now
#var climb_height := 0.75
#var climb_time := 0.15
#var climb_state := 0.0 :
# set(factor):
# #print("climb_state is now ", factor)
# climb_state = factor
# body.shape.height = body_height - factor * climb_height
# body.position.y = body_y + factor * climb_height / 2
#
# mesh.mesh.mid_height = mesh_height - factor * climb_height
# mesh.position.y = mesh_y + factor * climb_height / 2
#
# ground_check.position.y = ground_check_y + factor * climb_height / 2
# climb_check.position.y = climb_check_y + factor * climb_height / 2
var direction := Vector3.ZERO
var accel := 0
var speed := 0
var medium = "ground"
var accel_type := {
"ground": 12,
"air": 1,
"water": 4
}
var speed_type := {
"ground": 10,
"air": 10,
"water": 5
}
var gravity := 28
var jump := 14 * 1
var jetpack_active = false
var jetpack_thrust = 48 # force applied against gravity
var jetpack_tank = 0.5 # maximum fuel (jetpack use time
var jetpack_fuel = jetpack_tank # current fuel (use time) left
var jetpack_recharge = 0.25 # how long to recharge to full
var jetpack_min = 1.0/8
var jetpack_was_active = false
var velocity := Vector3.ZERO
var gravity_vec := Vector3.ZERO
var previously_on_floor := false
var lagging_movement_velocity = Vector3.ZERO
var dead = false#: # used to workaround Godot crash when destroying player_nodes
# set(value):
# match value:
# true:
# #input_active = false
# self.hide()
# $Body.disabled = true
# hud.pain = 3
# crosshair.hide()
# #$Head/Camera.transform.origin.y
# #set_physics_process(false)
# false:
# #input_active = true
# self.show()
# $Body.disabled = false
# $SpawnSFX.play()
# $SpawnVFX.emitting = true
# hud.pain = 0
# crosshair.show()
# #$Head/Camera.transform.origin = Vector3(0,0,0)
# #set_physics_process(true)
# dead = value
#
var focus_banner_alpha = 0
var focus_banner_inc = 5
var focus_banner_dec = 1
var focus_banner_show = false
var last_viewed_banner = null
func view_banner(show:bool):
focus_banner_show = show
@rpc(authority, reliable) func focus_banner(show:bool, type:=0 ):
if show:
$FocusBanner.show()
else:
$FocusBanner.hide()
match type:
0: $FocusBanner.mesh.surface_get_material(0).set("albedo_texture", banner_busy)
1: $FocusBanner.mesh.surface_get_material(0).set("albedo_texture", banner_chat)
@rpc(authority, unreliable) func update_movement(player_transform, head_rotation, lin_velocity, jetpack):
global_transform = player_transform
head.set_rotation(head_rotation)
jetpack_active = jetpack
motion_velocity = lin_velocity
#@rpc(any_peer, call_local, reliable) func set_dead(is_dead: bool):
#print("Recieved RPC call for set_dead ", is_dead)
# if not is_dead:
# spawn()
# self.dead = is_dead
# if is_multiplayer_authority():
# print("Rebroadcasting RPC call for set_dead ", dead)
# rpc(&'set_dead', dead)
@rpc(any_peer, reliable) func damage_feedback(kill=false):
if is_multiplayer_authority():
var victim = get_tree().multiplayer.get_remote_sender_id()
if kill:
crosshair.kill()
var pid = get_multiplayer_authority()
main.player_list.players[pid].score += 1 # we get a point
main.rpc(&'player_list_update', main.player_list.get(pid).serialize(), pid) # tell everyone
main.check_game_win_condition()
# check for firstblood
if main.player_list.players[pid].score == 1:
var firstblood = true
for i in main.player_list.players.keys():
if i != pid and main.player_list.players[i].score > 0:
firstblood = false
if firstblood:
main.get_node("Announcer").speak(main.get_node("Announcer").firstblood) # design a proper API
# check for revenge (payback) - don't play if this is a duel, it'd be silly
if main.player_list.players.size() > 2 and victim == revenge_pid:
main.get_node("Announcer").speak(main.get_node("Announcer").payback)
revenge_pid = -1 # reset revenge
else:
crosshair.hit()
else:
print("damage feedback called on puppet, ignoring")
return
func _ready() -> void:
#Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
view_zoom_target = 1.0
var banner_material = $FocusBanner.mesh.surface_get_material(0).duplicate()
$FocusBanner.mesh.surface_set_material(0, banner_material)
focus_banner(false)
if is_multiplayer_authority(): # prevent puppets from attempting to steer the authority - this just causes RPC errors
input_active = true
$Head/Camera.current = true
else:
input_active = false
$Head/Camera.current = false
$Jetpack/JetpackSound.playing = true
$Jetpack/JetpackSound.stream_paused = true
$SpawnVFX.emitting = true
func aim(event) -> void:
var mouse_motion = event as InputEventMouseMotion
if mouse_motion:
var adjusted_mouse_sensitivity = 1.0
if "Sensitivity" in settings.keys():
adjusted_mouse_sensitivity = mouse_sensitivity * settings["Sensitivity"]
rotation.y -= deg2rad(mouse_motion.relative.x * adjusted_mouse_sensitivity / view_zoom)
var current_tilt: float = head.rotation.x
current_tilt -= deg2rad(mouse_motion.relative.y * adjusted_mouse_sensitivity / view_zoom)
head.rotation.x = clamp(current_tilt, deg2rad(-90), deg2rad(90))
func _input(event) -> void:
if dead:
return
if not input_active:
return
#assert(is_multiplayer_authority() == true, "input_active is true, even though the node is not multiplayer_authority")
if is_multiplayer_authority() == false:
print("Input is active, but we're not the authority. WTF?!")
input_active = false
return
if Input.is_action_just_pressed("view_zoom"):
# tween.remove_all()
# tween.interpolate_property(self, "view_zoom", view_zoom, 4.0, 0.5, Tween.TRANS_SINE, Tween.EASE_IN_OUT)
# tween.start()
view_zoom_direction = true
view_zoom_target = 4.0
if Input.is_action_just_released("view_zoom"):
# tween.remove_all()
# tween.interpolate_property(self, "view_zoom", view_zoom, 1.0, 0.25, Tween.TRANS_SINE, Tween.EASE_IN_OUT)
# tween.start()
view_zoom_direction = false
view_zoom_target = 1.0
aim(event)
var can_shoot = true if view_zoom <= 1.05 else false
if can_shoot and Input.is_action_just_pressed("trigger_primary"):
weapon.trigger(0, true)
elif Input.is_action_just_released("trigger_primary"):
weapon.trigger(0, false)
if can_shoot and Input.is_action_just_pressed("trigger_secondary"):
weapon.trigger(1, true)
elif Input.is_action_just_released("trigger_secondary"):
weapon.trigger(1, false)
func _process(delta):
$Jetpack/GPUParticles3D.emitting = jetpack_active
if jetpack_active:
$Jetpack/JetpackSound.stream_paused = false
$Jetpack/OmniLight3D.show()
$Jetpack/OmniLight3D.light_energy = randf_range(2, 5)
else:
$Jetpack/JetpackSound.stream_paused = true
$Jetpack/OmniLight3D.hide()
# show focus banner on demand
if focus_banner_show:
focus_banner_alpha = min(focus_banner_alpha + focus_banner_inc * delta, 1.0)
else:
focus_banner_alpha = max(focus_banner_alpha - focus_banner_dec * delta, 0.0)
$FocusBanner.mesh.surface_get_material(0).set("albedo_color", Color(1,1,1, focus_banner_alpha))
#print("PLayer: ", name, "; Focus banner alpha: ", focus_banner_alpha, "; Focus banner show: ", focus_banner_show)
if not input_active:
return
# demand seeing other player's banner:
#print("Last viewed banner is ", last_viewed_banner)
if $"Head/Camera/RayCast3D".is_colliding():
#print("Probe got ", $"Head/Camera/RayCast3D".get_collider().name)
if $"Head/Camera/RayCast3D".get_collider().has_method(&"view_banner"):
$"Head/Camera/RayCast3D".get_collider().view_banner(true)
last_viewed_banner = $"Head/Camera/RayCast3D".get_collider()
elif last_viewed_banner:
last_viewed_banner.view_banner(false)
#assert(is_multiplayer_authority() == true, "input_active is true, even though the node is not multiplayer_authority")
if is_multiplayer_authority() == false:
print("input_active is true, while we're not the authority. WTF?")
input_active = false
return
if view_zoom_direction and view_zoom < view_zoom_target:
view_zoom = min(view_zoom_target, view_zoom + delta * 4)
elif not view_zoom_direction and view_zoom > view_zoom_target:
view_zoom = max(view_zoom_target, view_zoom - delta * 4)
hud.get_node("Stats").get_node("JetpackBar").value = (jetpack_fuel / jetpack_tank) * 100
# weapon spread
weapon.spread = max(lerp(weapon.spread, weapon.spread_min, weapon.spread_lerp), weapon.spread_min)
@rpc(call_local, any_peer, reliable) func moan():
var anims = ["01", "02", "03", "04"]
$Pain.play(anims[randi() % 4])
@rpc(call_local, any_peer, reliable) func take_damage(attacker: int, hit_position: Vector3, hit_normal: Vector3, damage:int, source_position: Vector3, damage_type, push: float):
var attacker_node = main.get_node("Players").get_node(str(attacker))
if is_multiplayer_authority():
print("Taken damage: ", damage, " by: ", attacker, " from: ", source_position)
hud.damage(damage)
health -= damage # reduce health
if health <= 0: # are we dead?
print("Died")
rpc(&'die', attacker)
attacker_node.rpc(&'damage_feedback', true) # let the attacker know he's got a kill
else:
attacker_node.rpc(&'damage_feedback', false) # let the attackr know he's got a hit
main.update_hud()
if not dead:
rpc(&'moan') # all puppets must scream!
# spawn the bullet hit effect
var impact_vfx = impact_player.instantiate()
impact_vfx.global_transform = impact_vfx.global_transform.looking_at(hit_normal)
impact_vfx.global_transform.origin = hit_position
get_tree().root.add_child(impact_vfx)
@rpc(any_peer, call_local, reliable) func spawn(spawn_transform: Transform3D):
dead = false
self.global_transform = spawn_transform
health = max_health
$Head/Camera.position.y = 0
$Head/Camera.rotation.z = 0
jetpack_fuel = jetpack_tank
self.show()
$Body.disabled = false
$SpawnSFX.play()
$SpawnVFX.emitting = true
if is_multiplayer_authority(): # don't touch these on puppets
hud.pain = 0
crosshair.show()
@rpc(any_peer, call_local, reliable) func die(killer_pid: int):
if killer_pid == -1: # we're disconnecting from the game
#main.chat.chat_notification("Player [/i][b][color=" + main.player_list.players[self.get_multiplayer_authority()].color.to_html() + "]" + main.player_list.players[self.get_multiplayer_authority()].name + "[/color][/b][i] left the game.")
pass
else:
var gibs = gibs_vfx.instantiate()
get_tree().root.add_child(gibs)
gibs.global_transform = self.global_transform
var decal = blood_decal.instantiate()
get_tree().root.add_child(decal)
decal.global_transform = self.global_transform
if is_multiplayer_authority(): # don't touch these on puppets
hud.pain = 3
crosshair.hide()
#main.chat.rpc(&'chat_notification', "Player [/i][b][color=" + main.player_list.players[self.get_multiplayer_authority()].color.to_html() + "]" + main.player_list.players[self.get_multiplayer_authority()].name + "[/color][/b][i] was killed by " + main.player_list.players[killer_pid].name )
main.chat.chat_notification("Player [/i][b][color=" + main.player_list.players[self.get_multiplayer_authority()].color.to_html() + "]" + main.player_list.players[self.get_multiplayer_authority()].name + "[/color][/b][i] was killed by " + main.player_list.players[killer_pid].name )
revenge_pid = killer_pid
$Head/Camera.position.y = -1 # lower the head to the ground, let the player see their gibs
$Head/Camera.rotation.x = 0 # reset the tilt so the camera looks forward
$Head/Camera.rotation.z = -20
jetpack_active = false
view_zoom_target = 1.0
view_zoom = 1
dead = true
main.destroy_player(self.get_multiplayer_authority())
self.hide()
$Body.disabled = true
#if get_tree().get_rpc_sender_id() != get_multiplayer_authority():
# print ("Death requested by a non-master. Ignoring")
# return
#main.rpc(&'destroy_player', self.get_multiplayer_authority())
#queue_free()
func update_player(info) -> void:
update_color(info.color)
func update_color(color) -> void: #change player's wolrldmodel color
var player_material = mesh.mesh.surface_get_material(0).duplicate()
player_material.albedo_color = color
mesh.set_surface_override_material(0, player_material)
func _physics_process(delta):
if dead: # workaround for Godot player destruction crash
motion_velocity = Vector3.ZERO
return
if not is_multiplayer_authority():
move_and_slide()
return
direction = Vector3.ZERO
if is_on_floor() and ground_check.is_colliding() and not jetpack_active:
#snap = -get_floor_normal()
medium = "ground"
gravity_vec = Vector3.ZERO
else:
#snap = Vector3.DOWN
medium = "air"
gravity_vec += Vector3.DOWN * gravity * delta
if input_active:
if Input.is_action_just_pressed("move_jump") and is_on_floor():
#snap = Vector3.ZERO
gravity_vec = Vector3.UP * jump
$JumpSFX.rpc(&"play")
if Input.is_action_pressed("move_forward"):
direction -= transform.basis.z
if Input.is_action_pressed("move_backward"):
direction += transform.basis.z
if Input.is_action_pressed("move_left"):
direction -= transform.basis.x
if Input.is_action_pressed("move_right"):
direction += transform.basis.x
jetpack_was_active = jetpack_active
if jetpack_was_active:
jetpack_active = Input.is_action_pressed("move_special") if jetpack_fuel > 0 else false
else:
jetpack_active = Input.is_action_just_pressed("move_special") if jetpack_fuel > jetpack_min else false
if jetpack_active:
gravity_vec[1] += jetpack_thrust * delta
jetpack_fuel -= delta
elif jetpack_fuel < jetpack_tank:
jetpack_fuel = min(jetpack_tank, jetpack_fuel + jetpack_recharge * delta)
#print("Jetpack fuel: ", jetpack_fuel, " active: ", jetpack_active, " delta: ", delta, " gravity vec: ", gravity_vec)
if direction.length() > 0: # normalized() will return a null
direction = direction.normalized()
# weapon bob
if direction.length() > 0:
if is_on_floor():
$Head/Camera/Hand/Weapon.transform.origin.y = lerp($Head/Camera/Hand/Weapon.transform.origin.y, sin(main.uptime * 10) / 15, 4 * delta)
else:
$Head/Camera/Hand/Weapon.transform.origin.y *= 1 - delta * 8
$Head/Camera/Hand/Weapon.transform.origin.y += sin(main.uptime * 2) / 1000 * delta
$Head/Camera/Hand/Weapon.transform.origin.y -= motion_velocity.y * delta / 60
if Input.is_action_just_pressed("move_jump") and is_on_floor():
# var tween = create_tween()
$Head/Camera/Hand/Weapon.transform.origin.y -= 0.025
#$Head/Camera/Hand/Weapon.transform.origin.y -= 0.05
if is_on_floor() and not previously_on_floor:
$Head/Camera/Hand/Weapon.transform.origin.y -= 1
# if
# $Head/Camera/Hand/Weapon.transform.origin.y -= 0.25
speed = speed_type[medium]
accel = accel_type[medium]
velocity = velocity.lerp(direction * speed, accel * delta)
motion_velocity = velocity + gravity_vec
move_and_slide()
if not is_on_floor() and not ground_check.is_colliding(): # while in mid-air collisions affect momentum
velocity.x = motion_velocity.x
velocity.z = motion_velocity.z
gravity_vec.y = motion_velocity.y
previously_on_floor = is_on_floor()
#lagging_movement_velocity.lerp(motion_velocity, delta)
rpc(&'update_movement', global_transform, head.get_rotation(), motion_velocity, jetpack_active)
# (stair) climbing
# ↓ disabled - Tween is undergoing redesign in Godot 4
# if get_slide_count() > 1 and climb_check.is_colliding() and false:
# #print("climb started at climb state: ", climb_state)
# var test_y = climb_height * (1 - climb_state)
# #print("test_y: ", test_y)
# var climb_test_start = global_transform.translated(Vector3(0, test_y, 0))
# var climb_test_step = Vector3(0,0,-0.1).rotated(Vector3.UP, rotation.y)
# if not test_move(climb_test_start, climb_test_step): # no collision
# var step = climb_check.get_collision_point().y
# var start = global_transform.origin.y
## print("step: ", step, " start: ", start)
# climb_state = clamp((step - start) / climb_height, 0, 1)
# global_transform.origin.y += climb_height * climb_state
# #print("climb state to start: ", climb_state)
## print("Climb height: ", step - start, " Climb state: ", climb_state)
# climb_tween.remove_all()
# climb_tween.interpolate_property(self, "climb_state", climb_state, 0.0, climb_time, Tween.TRANS_CUBIC, Tween.EASE_IN)
# climb_tween.start()