forked from unfa/liblast
548 lines
18 KiB
GDScript
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()
|