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()