extends Node const BULLET_HOLE: PackedScene = preload("uid://u8aj6fs32ql6") enum TEAMS { DEFENCE, ATTACK, SPECTATE, UNASSIGNED } enum ROUND_STATES { NOT_SET = 0, BUY = 1, ROUND = 2, AFTER_ROUND = 3, AFTER_PLANT = 4, AFTER_SESSION = 5, } const WIN_MONEY: int = 3400 const LOSE_MONEY: int = 2000 const START_MONEY: int = 800 const ATTACK_LAYER: int = 0b10000 const DEFENCE_LAYER: int = 0b100000 signal session_started signal session_ended signal round_started signal round_state_changed(state: int) signal player_stopped_interacting(id: int) signal team_won(team: TEAMS) var player_nodes: Dictionary[int,Player] = {} var player_data: Dictionary[int,Dictionary] = {} var dynamic_objects_parent: Node3D var object_containers: Array[ObjectContainer] var plants: Array[PlantSite] var plant_deadzones: Dictionary[StringName, Area3D] var session_started_flag: bool = false var round_state: ROUND_STATES var current_round: int = 0 var attacker_score: int = 0 var defender_score: int = 0 var attackers_alive: int = 0 var defenders_alive: int = 0 var reference_round_time: float var bomb_timer: Timer var round_timer: Timer var buy_timer: Timer func _ready() -> void: if multiplayer.is_server() == false: return bomb_timer = Timer.new() bomb_timer.wait_time = 45.0 bomb_timer.one_shot = true bomb_timer.timeout.connect(end_round.bind(TEAMS.ATTACK)) round_timer = Timer.new() round_timer.wait_time = 150 round_timer.one_shot = true round_timer.timeout.connect(end_round.bind(TEAMS.DEFENCE)) buy_timer = Timer.new() buy_timer.wait_time = 1 buy_timer.one_shot = true buy_timer.timeout.connect(begin_main_stage) add_child(bomb_timer) add_child(round_timer) add_child(buy_timer) multiplayer.peer_connected.connect(send_session_data) multiplayer.peer_disconnected.connect(delete_player) func send_session_data(peer_id: int = -1): if multiplayer.is_server() == false or session_started_flag == false: return var data: PackedByteArray data.resize(5) data.encode_u8(0,current_round) data.encode_u8(1,attackers_alive) data.encode_u8(2,defenders_alive) data.encode_u8(3,round_state) data.encode_u8(4,session_started_flag) if peer_id == -1: recieve_session_data.rpc(data,player_data) else: recieve_session_data.rpc_id(peer_id,data,player_data) @rpc("authority","call_remote","unreliable") func recieve_session_data(data: PackedByteArray,players_data_sent: Dictionary[int,Dictionary]): if not is_server_request(): return current_round = data.decode_u8(0) attackers_alive = data.decode_u8(1) defenders_alive = data.decode_u8(2) round_state = data.decode_u8(3) as ROUND_STATES if session_started_flag != bool(data.decode_u8(4)): session_started_flag = bool(data.decode_u8(4)) if session_started_flag: session_started.emit() else: session_ended.emit() player_data = players_data_sent func _process(_delta: float) -> void: if multiplayer.is_server() == false or not session_started: return match round_state: ROUND_STATES.BUY: reference_round_time = buy_timer.time_left ROUND_STATES.ROUND: reference_round_time = round_timer.time_left ROUND_STATES.AFTER_PLANT: reference_round_time = bomb_timer.time_left _: reference_round_time = 0 update_clock.rpc(reference_round_time) send_session_data() @rpc("authority","call_remote","unreliable") func update_clock(time: float): reference_round_time = time @rpc("authority","call_remote","reliable") func start_session() -> void: if not is_server_request(): return if multiplayer.is_server(): start_session.rpc() current_round = 0 attacker_score = 0 defender_score = 0 session_started_flag = true session_started.emit() bomb_timer.wait_time = LobbySettings.bomb_time round_timer.wait_time = LobbySettings.round_time buy_timer.wait_time = LobbySettings.buy_time var all_players: PackedInt32Array = multiplayer.get_peers() all_players.append(multiplayer.get_unique_id()) for player in all_players: player_data[player] = { "money" : START_MONEY, "nickname" : Lobby.client_nicknames[player], "saved_slots" : {}, "kills" : 0, "deaths" : 0, } start_round() @rpc("authority","call_remote","reliable") func end_session() -> void: if not is_server_request(): return if multiplayer.is_server(): end_session.rpc() bomb_timer.stop() round_timer.stop() buy_timer.stop() object_containers.clear() session_ended.emit() session_started_flag = false plants = [] plant_deadzones = {} player_nodes.clear() player_data.clear() dynamic_objects_parent = null func quit_session() -> void: if multiplayer.is_server(): end_session() await get_tree().create_timer(0.1).timeout else: session_ended.emit() session_started_flag = false Lobby.leave() @rpc("authority","call_remote","reliable") func start_round() -> void: if not is_server_request(): return if multiplayer.is_server(): buy_timer.start() start_round.rpc() round_timer.stop() bomb_timer.stop() if not current_round == LobbySettings.half_rounds: for player_id in player_nodes.keys(): var player: Player = player_nodes[player_id] if player.dead: player_data[player_id].saved_slots = {} continue var weapon_system: WeaponSystem = player.get_node("%WeaponSystem") var saved_slots: Dictionary[StringName,StringName] = {} for slot in weapon_system.slots: if slot == "bomb" or slot == "knife" or weapon_system.slots[slot] == null: continue saved_slots[slot] = weapon_system.slots[slot].weapon_gid player_data[player_id].saved_slots = saved_slots for container in object_containers: container.despawn() attackers_alive = 0 defenders_alive = 0 current_round += 1 round_started.emit.call_deferred() round_state = ROUND_STATES.BUY round_state_changed.emit.call_deferred(round_state) @rpc("authority","call_remote","reliable") func end_round(win_team: int) -> void: if not is_server_request(): return if win_team == TEAMS.DEFENCE: defender_score += 1 for defender in Lobby.defence_team: player_data[defender].money += WIN_MONEY for attacker in Lobby.attack_team: player_data[attacker].money += LOSE_MONEY if defender_score >= LobbySettings.win_score: win_session(TEAMS.DEFENCE) elif win_team == TEAMS.ATTACK: attacker_score += 1 for defender in Lobby.defence_team: player_data[defender].money += LOSE_MONEY for attacker in Lobby.attack_team: player_data[attacker].money += WIN_MONEY if attacker_score >= LobbySettings.win_score: win_session(TEAMS.ATTACK) if multiplayer.is_server(): end_round.rpc(win_team) if defender_score >= LobbySettings.win_score or attacker_score >= LobbySettings.win_score: get_tree().create_timer(7.5).timeout.connect(end_session) else: get_tree().create_timer(5).timeout.connect(start_round) if current_round == LobbySettings.half_rounds: Lobby.swap_teams() var temp_rounds = attacker_score attacker_score = defender_score defender_score = temp_rounds for id in player_data: player_data[id].money = START_MONEY player_data[id].saved_slots = {} sync_score.rpc(attacker_score,defender_score) round_state = ROUND_STATES.AFTER_ROUND round_state_changed.emit(round_state) @rpc("authority","call_remote","reliable") func begin_main_stage() -> void: if not is_server_request(): return if multiplayer.is_server(): round_timer.start() begin_main_stage.rpc() round_state = ROUND_STATES.ROUND round_state_changed.emit(round_state) @rpc("authority","call_remote","reliable") func begin_bomb_stage() -> void: if not is_server_request(): return if multiplayer.is_server(): bomb_timer.start() round_timer.stop() begin_bomb_stage.rpc() round_state = ROUND_STATES.AFTER_PLANT round_state_changed.emit(round_state) func defuse_win() -> void: if not multiplayer.is_server(): return bomb_timer.stop() end_round(TEAMS.DEFENCE) @rpc("reliable") func win_session(team: int): team_won.emit(team) MouseConfiner.set_global_mode(Input.MOUSE_MODE_VISIBLE) if is_multiplayer_authority(): win_session.rpc(team) func add_dead(team: int): if multiplayer.is_server() == false: return if team == TEAMS.ATTACK: attackers_alive -= 1 if attackers_alive == 0 and round_state != ROUND_STATES.AFTER_ROUND and round_state != ROUND_STATES.AFTER_PLANT: end_round(TEAMS.DEFENCE) if team == TEAMS.DEFENCE: defenders_alive -= 1 if defenders_alive == 0 and round_state != ROUND_STATES.AFTER_ROUND: end_round(TEAMS.ATTACK) func is_server_request() -> bool: return multiplayer.is_server() or multiplayer.get_remote_sender_id() == 1 func shoot(id:int , limb_damage: int, torso_damage: int,head_damage: int, distance: float,damage_reduction_curve: Curve = null) -> void: if multiplayer.is_server() == false: return var player: Player = player_nodes[id] var player_camera: Camera3D = player.get_node("Camera3D") var space: PhysicsDirectSpaceState3D = player.get_world_3d().direct_space_state var endpoint: Vector3 = player_camera.global_position - player_camera.global_basis.z * distance var ray = PhysicsRayQueryParameters3D.create(player_camera.global_position,endpoint,1) ray.exclude = [player.get_rid()] ray.collide_with_areas = false match player.team: TEAMS.DEFENCE: ray.collision_mask |= ATTACK_LAYER TEAMS.ATTACK: ray.collision_mask |= DEFENCE_LAYER _: ray.collision_mask |= ATTACK_LAYER | DEFENCE_LAYER var collision = space.intersect_ray(ray) if collision != {}: if collision["collider"] is Player: var hit_player: Player = collision["collider"] var shape_object: CollisionShape3D = hit_player.shape_owner_get_owner(collision["shape"]) var reduction: float = 1 var damage: int = 0 match shape_object.get_groups()[0]: "Head": damage = head_damage "Limb": damage = limb_damage _: damage = torso_damage if damage_reduction_curve != null: var distance_to_hit = (player_camera.global_position - collision["position"]).length()/distance reduction = damage_reduction_curve.sample(distance_to_hit) hit_player.take_damage(int(float(damage) * reduction)) else: var bullet_hole: Decal = BULLET_HOLE.instantiate() dynamic_objects_parent.add_child(bullet_hole,true) var rotation_quat: Quaternion = Quaternion(Vector3.UP,collision["normal"]) bullet_hole.quaternion *= rotation_quat bullet_hole.global_position = collision["position"] func shoot_pellets(id:int,limb_pellet_damage: int, torso_pellet_damage: int,head_pellet_damage: int, distance: float, pellets: PackedVector2Array, damage_reduction_curve: Curve = null): if multiplayer.is_server() == false: return var amount: int = len(pellets) var player: Player = player_nodes[id] var player_camera: Camera3D = player.get_node("Camera3D") var space: PhysicsDirectSpaceState3D = player.get_world_3d().direct_space_state for i in range(amount): var endpoint: Vector3 = player_camera.project_position(pellets[i],distance) var ray = PhysicsRayQueryParameters3D.create(player_camera.global_position,endpoint,1) ray.exclude = [player.get_rid()] ray.collide_with_areas = false match player.team: TEAMS.DEFENCE: ray.collision_mask |= ATTACK_LAYER TEAMS.ATTACK: ray.collision_mask |= DEFENCE_LAYER _: ray.collision_mask |= ATTACK_LAYER | DEFENCE_LAYER var collision = space.intersect_ray(ray) if collision != {}: if collision["collider"] is Player: var hit_player: Player = collision["collider"] var shape_object: CollisionShape3D = hit_player.shape_owner_get_owner(collision["shape"]) var reduction: float = 1 var damage: int = 0 match shape_object.get_groups()[0]: "Head": damage = head_pellet_damage "Limb": damage = limb_pellet_damage _: damage = torso_pellet_damage if damage_reduction_curve != null: var distance_to_hit = (player_camera.global_position - collision["position"]).length()/distance reduction = damage_reduction_curve.sample(distance_to_hit) hit_player.take_damage(int(float(damage) * reduction)) else: var bullet_hole: Decal = BULLET_HOLE.instantiate() dynamic_objects_parent.add_child(bullet_hole,true) var rotation_quat: Quaternion = Quaternion(Vector3.UP,collision["normal"]) bullet_hole.quaternion *= rotation_quat bullet_hole.global_position = collision["position"] func interact(id: int) -> void: if multiplayer.is_server() == false: return var player: Player = player_nodes[id] var player_camera: Camera3D = player.get_node("Camera3D") var space: PhysicsDirectSpaceState3D = player.get_world_3d().direct_space_state var endpoint: Vector3 = player_camera.global_position - player_camera.global_basis.z * 3 var ray = PhysicsRayQueryParameters3D.create(player_camera.global_position,endpoint,1) ray.exclude = [player.get_rid()] ray.collide_with_areas = false ray.collision_mask |= 64 var collision = space.intersect_ray(ray) if collision != {} and collision["collider"] is Interactible: collision["collider"].interaction_start(id) func stop_interact(id: int) -> void: if multiplayer.is_server() == false: return player_stopped_interacting.emit(id) func is_on_site(id: int) -> bool: for plant in plants: if plant.is_player_on_site(id): return true return false func get_site(id: int) -> PlantSite: for plant in plants: if plant.is_player_on_site(id): return plant return null func kill_site(site: StringName) -> void: if multiplayer.is_server() == false: return for body in plant_deadzones[site].get_overlapping_bodies(): if body is Player: body.die() @rpc("authority","call_remote","unreliable") func sync_score(attack: int, defence: int): attacker_score = attack defender_score = defence if defender_score >= LobbySettings.win_score: win_session(TEAMS.DEFENCE) elif attacker_score >= LobbySettings.win_score: win_session(TEAMS.ATTACK) @rpc("authority","call_remote","reliable") func delete_player(id: int): if not is_server_request(): return if multiplayer.is_server(): delete_player.rpc_id(id) player_data.erase(id) # For clients only func get_player_data() -> Dictionary: var id: int = multiplayer.get_unique_id() if player_data.has(id): return player_data[id] else: return {}