Skip to content

Instantly share code, notes, and snippets.

@levidavidmurray
Created June 6, 2024 19:40
Show Gist options
  • Save levidavidmurray/6df04f77b07e8d24cf55cabe0c562ad5 to your computer and use it in GitHub Desktop.
Save levidavidmurray/6df04f77b07e8d24cf55cabe0c562ad5 to your computer and use it in GitHub Desktop.
class_name Player
extends FPSController3D
signal died
signal strength_test_started(fitness: FitnessHandler, strength_test: StrengthTestInteractable)
signal key_input(event: InputEventKey)
@export var underwater_env: Environment
@export var debug = false
@export var rb_contact_force := 2.0
@export var scn_audio_listener: PackedScene
@export var scn_blood_decal: PackedScene
@export var damage_vignette_curve: Curve
@export var player_name: String = "[name]":
set(value):
player_name = value
if tps_rig:
tps_rig.nameplate.text = player_name
@onready var player_input: PlayerInput = $ClientSync
@onready var fps_camera_parent: Node3D = %FpsCamera
@onready var fps_camera: Camera3D = %FpsCamera/Camera
@onready var phantom_fps: PhantomCamera3D = %PhantomFPS
@onready var phantom_follow: PhantomCamera3D = %PhantomFollow
@onready var camera_shake: Shaker = %CameraShake
@onready var fps_rig: FPSRig = %FPSRig
@onready var tps_rig: TPSRig = %TPSRig
@onready var card_handler: CardHandler = $CardHandler
@onready var interact_handler: InteractHandler = $InteractHandler
@onready var fitness: FitnessHandler = $FitnessHandler
@onready var inventory: Inventory = %Inventory
@onready var hotbar: Hotbar = %Hotbar
@onready var voip_handler: VOIPHandler = $VOIPHandler
@onready var ground_ray = $GroundRay
@onready var fog_volume: FogVolume = $FogVolume
@onready var cave_particles: GPUParticles3D = $CaveParticles
@onready var health: HealthComponent = $HealthComponent
@onready var sfx_damage: AudioStreamPlayer3D = %SFX_Damage
@onready var sfx_blood: AudioStreamPlayer3D = $SFX_Blood
@onready var vignette_rect: ColorRect = %VignetteRect
var player_rig: PlayerRig
var _handcar_rot_last_frame = INF
var _can_move_last_frame = true
var _was_on_floor = false
var _vignette_orig_outer_radius: float
var _vignette_tween: Tween
var is_resting = false
var can_move = true
var can_look = true
var head_collision_y_offset: float
var p_cam_host: PhantomCameraHost
# TODO: Really need an FSM-based player
var ladder: Ladder
var ladder_enter_pos: Vector3
# TODO: Remove these after Voruk showcase demo
var blood_decals: Array[Decal]
var last_blood_decal_health: int = 100
var is_holding_voruk = false
var is_dancing = false
var hold_disable_tween: Tween
var player_id := -1:
get:
return int(str(name))
var is_dead: bool:
get:
return health.is_dead
func _enter_tree():
set_multiplayer_authority(player_id)
$HealthComponent.set_multiplayer_authority(1)
func _exit_tree():
PlayerManager.remove_player(self.player_id)
func _ready():
PlayerManager.add_player(self)
mouse_sensitivity = GameSettings.look_sensitivity
head_collision_y_offset = (head.position - collision.position).y
setup()
tps_rig.hide()
fps_rig.hide()
if is_multiplayer_authority():
_player_ready()
else:
_player_puppet_ready()
interact_handler.activate(camera, inventory, hotbar)
player_rig.activate(hotbar)
health.died.connect(_on_died)
health.damage_taken.connect(_on_damage_taken)
func _process(delta):
# if can_move and not _can_move_last_frame:
# player_rig.play_anim(G.PlayerAnimType.IDLE)
_update_spine_rotation()
if is_multiplayer_authority():
hotbar.check_input = can_look
head.mouse_sensitivity = GameSettings.look_sensitivity
head.invert_y_axis = GameSettings.invert_y_axis
head.position.y = collision.position.y + head_collision_y_offset
if health.is_alive:
camera.transform = phantom_fps.transform
fps_camera_parent.global_transform = camera.global_transform
if ladder and not fly_ability.is_actived():
fly_ability.set_active(true)
elif not ladder and fly_ability.is_actived():
fly_ability.set_active(false)
# TODO: Remove (or move?)
if Input.is_action_just_pressed("voip"):
voip_handler.toggle_mute()
var carry_weight = inventory.get_weight()
fitness.tick(delta, carry_weight, is_moving(), is_sprinting())
if is_resting and velocity.length() > 0.01 and GameManager.can_break_rest:
set_resting.rpc(false)
if is_dancing and velocity.length() > 0.01:
is_dancing = false
tps_rig.play_anim(G.PlayerAnimType.IDLE)
_can_move_last_frame = can_move
func _physics_process(delta):
if not can_move:
player_input.input_jump = false
player_input.input_action_primary = false
player_input.input_interact = false
player_input.input_toggle_flashlight = false
player_input.input_drop_item = false
velocity = Vector3()
return
if ladder:
if player_input.input_jump:
ladder = null
move(
delta,
player_input.input_dir,
player_input.input_jump,
player_input.input_crouch,
player_input.input_sprint and fitness.can_sprint(),
player_input.input_crouch,
player_input.input_jump
)
if ladder:
# constrain player to ladder
global_position.x = ladder_enter_pos.x
global_position.z = ladder_enter_pos.z
var dist_to_top = ladder.global_position.y - global_position.y
if dist_to_top < 1.75:
global_position = ladder.global_position
ladder = null
elif is_on_floor() and not _was_on_floor:
ladder = null
_process_collisions()
_handle_handcar_rotation()
if player_input.input_action_primary:
_handle_action_primary()
if player_input.input_interact:
interact_handler.interact()
if player_input.input_toggle_flashlight:
# TODO: Make flashlight an item?
player_rig.toggle_flashlight()
if player_input.input_drop_item:
# TODO: Pass path of node to parent item to
var result = _head_raycast()
var parent_path = NodePath("")
if result and result.collider:
parent_path = result.collider.get_path()
hotbar.drop_current_item(fps_rig.item_hand_pos.global_position, parent_path)
player_input.input_jump = false
player_input.input_action_primary = false
player_input.input_interact = false
player_input.input_toggle_flashlight = false
player_input.input_drop_item = false
tps_rig.velocity = velocity
tps_rig.is_crouching = is_crouching()
_was_on_floor = is_on_floor()
func _unhandled_key_input(event: InputEvent):
if not is_multiplayer_authority():
return
if event is InputEventKey and event.is_released():
event = event as InputEventKey
if event.keycode == KEY_1:
phantom_fps.global_position.y -= 0.25
# dance.rpc(G.PlayerAnimType.DANCE_CLASSIC)
if event.keycode == KEY_2:
phantom_fps.global_position.y += 0.25
dance.rpc(G.PlayerAnimType.DANCE_LUDDY)
if event.keycode == KEY_3:
var alive_players = PlayerManager.get_alive_players()
if alive_players.size() > 0:
var player = alive_players[0]
tps_rig.drag_bone_target = player.tps_rig.right_hand
else:
# TODO: Remove after showcase
key_input.emit(event)
@rpc
func dance(anim_type: G.PlayerAnimType):
is_dancing = true
tps_rig.play_anim(anim_type)
# TODO: Remove after showcase?
@rpc
func hold_voruk():
is_holding_voruk = not is_holding_voruk
if hold_disable_tween and hold_disable_tween.is_running():
hold_disable_tween.kill()
hold_disable_tween = create_tween()
if is_holding_voruk:
hold_disable_tween.tween_method(tps_rig._set_body_blend, 0.0, 1.0, 0.25)
tps_rig._upper_body_sm.travel("Hold_Voruk")
else:
hold_disable_tween = create_tween()
hold_disable_tween.tween_method(tps_rig._set_body_blend, 1.0, 0.0, 0.4)
tps_rig._upper_body_sm.travel("Idle")
func _input(event: InputEvent) -> void:
if not is_multiplayer_authority():
return
if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
return
if not can_look:
return
# Mouse look (only if the mouse is captured).
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
rotate_head(event.relative)
@rpc("call_local", "reliable")
func go_fetal():
if is_multiplayer_authority():
_drop_inventory()
can_move = false
can_look = false
tps_rig.anim_tree.active = true
tps_rig.skeleton.show_rest_only = false
tps_rig.play_anim(G.PlayerAnimType.FETAL)
fps_rig.play_anim(G.PlayerAnimType.FETAL)
@rpc("call_local")
func set_player_name(_name: String):
player_name = _name
@rpc("call_local", "reliable")
func set_resting(resting: bool):
is_resting = resting
can_look = not resting
if is_resting:
GameManager.player_rested()
@rpc("any_peer", "call_local")
func set_player_position(pos: Vector3):
global_position = pos
@rpc("any_peer", "call_local")
func set_player_rotation(rot: Vector3, is_global = false):
if is_global:
global_rotation = rot
else:
rotation = rot
head.actual_rotation.y = rotation.y
@rpc("call_local", "reliable")
func take_damage(damage: int):
health.take_damage(damage)
@rpc("call_local", "reliable")
func die():
take_damage(health.health)
@rpc("any_peer", "call_local", "reliable")
func respawn():
GameManager.stop_spectating()
global_position = GameManager.player_spawn_pos
health.reset()
tps_rig.set_ragdoll(false)
voip_handler.set_mute(false)
fps_rig.visible = is_multiplayer_authority()
tps_rig.visible = not is_multiplayer_authority()
tps_rig.nameplate.visible = not is_multiplayer_authority()
if not is_multiplayer_authority():
tps_rig._set_body_blend(0.0)
can_look = true
can_move = true
if p_cam_host:
p_cam_host.queue_free()
p_cam_host = null
func start_spectate() -> PhantomCamera3D:
reset_vignette()
fps_rig.hide()
tps_rig.show()
tps_rig.set_ragdoll(true)
tps_rig.nameplate.hide()
return phantom_follow.duplicate() as PhantomCamera3D
@rpc("call_local", "reliable")
func _on_died():
if is_multiplayer_authority():
_drop_inventory()
p_cam_host = PhantomCameraHost.new()
camera.add_child(p_cam_host)
else:
tps_rig.set_ragdoll(true)
tps_rig.nameplate.hide()
died.emit()
voip_handler.set_mute(true)
if fps_rig.flashlight.visible:
fps_rig.toggle_flashlight()
tps_rig.toggle_flashlight()
can_look = false
can_move = false
func shake_camera(stress: float):
camera_shake.shake(stress)
func is_moving() -> bool:
return Vector2(velocity.x, velocity.z).length() > 0.1 and is_on_floor()
func is_sprinting() -> bool:
return (
player_input.input_sprint
and Vector2(velocity.x, velocity.z).length() > _normal_speed
and is_on_floor()
)
func set_disabled(disabled: bool):
can_move = not disabled
can_look = not disabled
func vertical_look_percent() -> float:
return head.rotation_degrees.x / vertical_angle_limit
# Cave FX functions should be moved
func show_cave_fx():
cave_particles.emitting = true
cave_particles.show()
# fog_volume.show()
func hide_cave_fx():
cave_particles.emitting = false
cave_particles.hide()
fog_volume.hide()
func _player_ready():
GameManager.player = self
player_name = GameManager.steam_name
hotbar.slot_count = inventory.slot_count
fps_rig.show()
player_rig = fps_rig
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
emerged.connect(_on_controller_emerged.bind())
submerged.connect(_on_controller_submerged.bind())
set_player_name.rpc(player_name)
fog_volume.show()
voip_handler.init_capture()
interact_handler.strength_test_interacted.connect(_on_strength_test_interacted)
interact_handler.rest_interacted.connect(_on_rest_interacted)
_vignette_orig_outer_radius = vignette_rect.material.get("shader_parameter/outer_radius")
var audio_listener = scn_audio_listener.instantiate() as Node3D
camera.add_child(audio_listener)
var studio_listener = audio_listener.get_node("StudioListener3D") as StudioListener3D
studio_listener.rigidbody = self
phantom_fps.set_priority(1)
camera.make_current()
fps_camera.make_current()
func _player_puppet_ready():
player_rig = tps_rig
fps_rig.hide()
tps_rig.show()
camera.clear_current()
fps_camera.clear_current()
fog_volume.queue_free()
voip_handler.init_playback()
func _update_spine_rotation():
tps_rig.set_spine_blend_space(vertical_look_percent())
func _process_collisions():
for i in range(get_slide_collision_count()):
var col = get_slide_collision(i)
for k in range(col.get_collision_count()):
var body = col.get_collider(k)
if body == null:
continue
if body is RigidBody3D:
_collide_rigidbody(body, col, k)
elif body is Ladder:
_collide_ladder(body, col, k)
func _collide_ladder(body: Ladder, col: KinematicCollision3D, col_idx: int):
if ladder:
return
print("collide ladder, vlp: %s" % vertical_look_percent())
if is_on_floor() and vertical_look_percent() > 0.55:
ladder = body
ladder_enter_pos = global_position
func _collide_rigidbody(body: RigidBody3D, col: KinematicCollision3D, col_idx: int):
var point = col.get_position(col_idx) - body.global_position
body.apply_impulse(-col.get_normal(col_idx) * rb_contact_force, point)
func _handle_action_primary():
player_rig.primary_action(self)
# var item = hotbar.get_selected_item()
# if item is CardItemData and card_handler.can_throw():
# player_rig.play_anim(G.PlayerAnimType.CARD_THROW)
# card_handler.use_card(item)
# TODO: Commented out for testing
# inventory_handler.drop_from_inventory(hotbar.selection_index)
pass
func _handle_handcar_rotation():
var body = ground_ray.get_collider()
if body is Handcar:
var handcar = body as Handcar
var rot = handcar.rotation_degrees
if rot.y != _handcar_rot_last_frame and _handcar_rot_last_frame != INF:
var diff = rot.y - _handcar_rot_last_frame
rotate_y(deg_to_rad(diff))
head.actual_rotation.y = rotation.y
_handcar_rot_last_frame = rot.y
func _head_raycast(dir = Vector3.DOWN) -> Dictionary:
var space_state = get_world_3d().direct_space_state
var origin = head.global_position
var end = origin + dir * 100
var query = PhysicsRayQueryParameters3D.create(origin, end, 1 << 0)
return space_state.intersect_ray(query)
func _spawn_blood_decal():
# slightly randomize dir in x and z
var dir = Vector3.DOWN
dir.x += GameManager.rng.randf_range(-0.1, 0.1)
dir.z += GameManager.rng.randf_range(-0.1, 0.1)
var result = _head_raycast(dir)
if not result:
return
await G.wait(0.4)
sfx_blood.play()
var decal = scn_blood_decal.instantiate() as Decal
GameManager.world.add_child(decal)
decal.global_position = result.position
decal.global_transform = G.align_with_normal(decal.global_transform, result.normal)
decal.global_position = global_position
# random rotation
decal.rotation_degrees.y = GameManager.rng.randf_range(0, 360)
func _drop_inventory():
var result = _head_raycast()
var parent_path = NodePath("")
var positions = []
var parent_paths = []
positions.resize(inventory.slot_count)
parent_paths.resize(inventory.slot_count)
if result and result.collider:
parent_path = result.collider.get_path()
for i in range(inventory.slot_count):
positions[i] = fps_rig.item_hand_pos.global_position
parent_paths[i] = parent_path
inventory.drop_inventory.rpc(positions, parent_paths)
func reset_vignette():
if _vignette_tween:
_vignette_tween.kill()
var vignette_mat = vignette_rect.material as ShaderMaterial
vignette_mat.set("shader_parameter/outer_radius", _vignette_orig_outer_radius)
vignette_mat.set("shader_parameter/alpha", 0.0)
func _handle_damage_vignette():
if _vignette_tween:
_vignette_tween.kill()
_vignette_tween = create_tween().set_parallel()
var vignette_mat = vignette_rect.material as ShaderMaterial
var outer_radius = vignette_mat.get("shader_parameter/outer_radius")
_vignette_tween.tween_property(vignette_mat, "shader_parameter/alpha", 1.0, 0.25)
_vignette_tween.tween_property(
vignette_mat, "shader_parameter/outer_radius", outer_radius - 1.0, 0.25
)
_vignette_tween.chain()
_vignette_tween.tween_property(vignette_mat, "shader_parameter/alpha", 0.0, 0.5)
_vignette_tween.tween_property(
vignette_mat, "shader_parameter/outer_radius", _vignette_orig_outer_radius, 1.0
)
func _on_damage_taken(damage: int):
shake_camera(0.4)
if is_multiplayer_authority():
_handle_damage_vignette()
sfx_damage.play()
var blood_decal_health_interval = 20
# every 20% health drop, instantiate a blood decal
# example edge case: if health is at 84 and drops to 75, a decal should still be instantiate
if last_blood_decal_health - health.health >= blood_decal_health_interval:
_spawn_blood_decal()
last_blood_decal_health -= blood_decal_health_interval
func _on_strength_test_interacted(strength_test: StrengthTestInteractable):
can_move = false
can_look = false
strength_test_started.emit(fitness, strength_test)
func _on_rest_interacted():
set_resting.rpc(true)
func _on_controller_emerged():
if not is_multiplayer_authority():
return
camera.environment = null
func _on_controller_submerged():
if not is_multiplayer_authority():
return
camera.environment = underwater_env
func _on_hb_punch_body_entered(_body: Node3D):
if not is_multiplayer_authority():
return
if not $SFX_PunchImpact.playing:
$SFX_PunchImpact.play()
# TODO: Remember why top level player node is not multiplayer authority
# func _is_multiplayer_authority() -> bool:
# return player_id == multiplayer.get_unique_id() or debug
func is_fly_mode() -> bool:
return fly_ability.is_actived() or ladder
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment