Created
June 6, 2024 19:40
-
-
Save levidavidmurray/6df04f77b07e8d24cf55cabe0c562ad5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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