extends CharacterBody3D @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 dead = false: # used to workaround Godot crash when destroying player_nodes set(value): match value: true: #input_active = false self.hide() $Body.disabled = true #set_physics_process(false) false: #input_active = true self.show() $Body.disabled = false $SpawnSFX.play() $SpawnVFX.emitting = true #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) self.dead = is_dead # if is_multiplayer_authority(): # print("Rebroadcasting RPC call for set_dead ", dead) # rpc(&'set_dead', dead) 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): if dead: return $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]) func damage(hp: int): var target = main.player_list.players[self.get_multiplayer_authority()] target.health -= hp @rpc(any_peer, call_local, reliable) func die(killer_pid: int): 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 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()) main.destroy_player(self.get_multiplayer_authority()) set_dead(true) #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 #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() 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 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()