Skip to content

Instantly share code, notes, and snippets.

@levidavidmurray
Created June 6, 2024 19:40
Show Gist options
  • Select an option

  • Save levidavidmurray/6df04f77b07e8d24cf55cabe0c562ad5 to your computer and use it in GitHub Desktop.

Select an option

Save levidavidmurray/6df04f77b07e8d24cf55cabe0c562ad5 to your computer and use it in GitHub Desktop.

Revisions

  1. levidavidmurray created this gist Jun 6, 2024.
    608 changes: 608 additions & 0 deletions player.gd
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,608 @@
    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