# This file is part of sunder. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ## This class defines the multiplayer game type. class_name Multiplayer extends Node signal connection_failed ## Enumeration that defines supported game modes. enum Mode { ## Free-for-all mode where players compete individually. FREE_FOR_ALL, ## Rabbit mode where players chase and protect a designated "rabbit" player. RABBIT, ## Capture the flag mode where teams compete to capture each other's flags. CAPTURE_THE_FLAG, ## Arena mode where players engage in team deathmatch battles. ARENA, ## Ball mode where teams aim to control the ball and score goals. BALL } func _enter_tree() -> void: # Sets our custom multiplayer as the main one in SceneTree. get_tree().set_multiplayer(MultiplayerMatchExtension.new()) multiplayer.hello() ## The [Match] manager. @export var match:Match ## The [Teams] manager. @export var teams:Teams var motion_service:MotionService ## The scoreboard to keep track of scores. @export var scoreboard:Scoreboard ### The multiplayer mode. #@export var mode:Mode = Mode.FREE_FOR_ALL ## The time it takes for a player to respawn when killed, secconds (s). @export var VICTIM_RESPAWN_TIME:float = 3.0 ## The total duration of a match, in secconds (s). @export var MATCH_DURATION := 1200 ## The total duration of the warmup phase of a match. @export var WARMUP_START_DURATION := 25 @export_group("Spawned Scenes") @export var _PLAYER:PackedScene @export var _FLAG:PackedScene ## The spawn root for [Player] nodes. @onready var players:Node = $Match/Players ## The spawn root for [Flag] nodes. @onready var objectives:Node = $Match/Objectives @onready var player_spawner:MultiplayerSpawner = $Match/PlayerSpawner @onready var map_spawner:MultiplayerSpawner = $Match/MapSpawner @onready var objectives_spawner:MultiplayerSpawner = $Match/ObjectiveSpawner func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED player_spawner.spawn_function = _on_player_spawn map_spawner.spawn_function = _on_map_spawn func _on_player_spawn(data:Dictionary) -> Player: var player:Player = _PLAYER.instantiate() # @NOTE: MultiplayerSpawner needs valid names so we need to set the node name # > Unable to auto-spawn node with reserved name: @RigidBody3D@101. # > Make sure to add your replicated scenes via 'add_child(node, true)' # > to produce valid names. player.peer_id = data.peer_id player.name = str(player.peer_id) player.username = data.username if "global_position" in data: player.set_deferred("global_position", data.global_position) return player func _on_map_spawn(data:Variant = null) -> Map: return Game.maps[data.index].instantiate() func _unhandled_input(event:InputEvent) -> void: if event.is_action_pressed("exit"): if is_peer_connected(): _leave_match.rpc_id(get_multiplayer_authority()) func is_peer_connected() -> bool: if not multiplayer.multiplayer_peer or multiplayer.multiplayer_peer is OfflineMultiplayerPeer: return false return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED ## This method starts a server. func start_server(port:int, _p_mode:int = 0, map_index:int = 0, _username:String = "Mercury") -> void: #mode = p_mode motion_service = MotionService.new() #multiplayer.peer_connected.connect(motion_service.register) #multiplayer.peer_disconnected.connect(_on_peer_disconnected) #multiplayer.peer_disconnected.connect(motion_service.unregister) Game.network.serve(port) Game.ping.synchronized.connect(match.scoreboard._on_ping_sync) Game.ping.start() map_spawner.spawn({ "index": map_index }) # start ping timer to ping connected clients and synchronize its state #match mode: #Mode.RABBIT: #var flag:Flag = _FLAG.instantiate() #objectives.add_child(flag) #var spawn:Node3D = map.get_spawn_position("objectives") #flag.global_position = spawn.global_position #flag.respawn_timer.timeout.connect(func() -> void: #if spawn: #flag.waypoint.text = "" #flag.global_position = spawn.global_position #flag.state = flag.FlagState.ON_STAND #) # #teams.team_added.connect(_on_team_added) #teams.team_erased.connect(_on_team_erased) #teams.add_teams(["rabbit", "chasers"]) #flag.grabbed.connect( #func(carry:FlagHolder) -> void: #carry.owner.hud.objective_label.set_visible(true) #switch_team(carry.owner.peer_id, "rabbit") #flag.respawn_timer.stop() #) #flag.dropped.connect( #func(carry:FlagHolder) -> void: #carry.owner.hud.objective_label.set_visible(false) #switch_team(carry.owner.peer_id, "chasers") #flag.respawn_timer.start() #) #scoreboard.add_child( #RabbitScoringComponent.new(scoreboard, flag)) #scoreboard.add_child( #DeathmatchScoringComponent.new(scoreboard, players)) # #players.child_entered_tree.connect(func(player:Player) -> void: #teams["chasers"].add(player.peer_id, player.username) #player.damage.connect(_rabbit_damage_handler) #) # #Mode.FREE_FOR_ALL: #scoreboard.add_panel() #scoreboard.add_child(DeathmatchScoringComponent.new(scoreboard, players)) #players.child_entered_tree.connect(func(player:Player) -> void: #var panel:ScorePanel = scoreboard.get_panel(0) #panel.add_entry(player.peer_id, player.username) #player.damage.connect(_ffa_damage_handler) #) # # when a new player is added into tree #players.child_entered_tree.connect(func(_player:Player) -> void: ## make sure we have enough players to start the match #if players.get_child_count() > 1 and timer.is_stopped(): #_start_match() #) #print("mode `%s` loaded" % Mode.keys()[mode]) #if DisplayServer.get_name() != "headless": #add_player(1, username) #func _rabbit_damage_handler(source: Node, target: Node, amount: float) -> void: #assert(target.find_children("*", "Health")) #if source == target or source.team_id != target.team_id: #target.health.damage.rpc(amount, source.peer_id) # #func _ffa_damage_handler(source: Node, target: Node, amount: float) -> void: #assert(target.find_children("*", "Health")) #target.health.damage.rpc(amount, source.peer_id) #func _on_peer_disconnected(peer_id:int) -> void: #print("peer `%d` disconnected" % peer_id) #if players.get_child_count() < 2: ## stop timer when there is not enough players #timer.stop() #var player: Player = players.get_child(0) #if player: #player.hud.timer_label.text = "Warmup" #func _start_match() -> void: #scoreboard.scoring = true #timer.start(WARMUP_START_DURATION) # wait few seconds #await timer.timeout # wait for the starting countdown to finish #for player: Player in players.get_children(): #if player.has_flag(): #var flag: Flag = player.flag_holder.flag #player.flag_holder.drop.rpc_id(get_multiplayer_authority()) #var spawn:Node3D = map.get_objective_spawn() #flag.global_position = spawn.global_position #scoreboard.reset_scores() #timer.start(MATCH_DURATION) # restart timer with match duration # #players.respawn() # respawn everyone #for player:Player in players: #player.respawn.rpc_id(get_multiplayer_authority(), Game.map.get_spawn_position("players")) # #if not timer.timeout.is_connected(_on_post_match): #timer.timeout.connect(_on_post_match) #func _on_post_match() -> void: ## disconnect handler for timer reuse #if timer.timeout.is_connected(_on_post_match): #timer.timeout.disconnect(_on_post_match) ## @TODO: display end of match stats with scoreboard data #scoreboard.reset_scores() #scoreboard.scoring = false #for player: Player in players.get_children(): #if player.has_flag(): #var flag: Flag = player.flag_holder.flag #player.flag_holder.drop.rpc_id(get_multiplayer_authority()) #var spawn:Node3D = map.get_objective_spawn() #flag.global_position = spawn.global_position ## restart match #_start_match() func join_server(host:String, port:int, username:String) -> void: multiplayer.connected_to_server.connect(_on_connected_to_server.bind(username)) multiplayer.connection_failed.connect(_on_connection_failed) multiplayer.server_disconnected.connect(_on_server_disconnected) Game.network.join(host, port) func _on_server_disconnected() -> void: Game.change_scene() # back to boot menu func add_player(peer_id:int, username:String) -> void: player_spawner.spawn({ "peer_id": peer_id, "username": username, # @NOTE: match would include map and teams to deduce where to spawn #"global_position": party.get_spawn(peer_id) }) func _on_connected_to_server(username:String) -> void: _join_match.rpc_id(get_multiplayer_authority(), username) func _on_connection_failed() -> void: connection_failed.emit() func _on_player_killed(victim: Player, _killer:int) -> void: await get_tree().create_timer(VICTIM_RESPAWN_TIME).timeout var spawn: Node3D = Game.map.get_player_spawn() victim.respawn.rpc_id(get_multiplayer_authority(), spawn.global_position) # This method notifies the server that a player wants to join the match. It # takes a single [param username] parameter and is invoked remotely by clients. @rpc("any_peer", "call_remote", "reliable") func _join_match(username:String) -> void: if multiplayer.is_server(): var peer_id:int = multiplayer.get_remote_sender_id() # make sure remote peer cannot add more than one player if not players.has_node(str(peer_id)): # spawn player on all connected peers add_player(peer_id, username) @rpc("any_peer", "call_local", "reliable") func _leave_match() -> void: if multiplayer.is_server(): var peer_id:int = multiplayer.get_remote_sender_id() var player:Player = players.get_node(str(peer_id)) if player: var team:Team = teams.get_peer_team(peer_id) if team: team.erase(peer_id) # @TODO: verify that a peer disconnecting out of # network problems drops the flag if player.has_flag(): player.flag_holder.drop() players.remove_child(player) player.queue_free() multiplayer.disconnect_peer(peer_id) func _exit_tree() -> void: if not is_multiplayer_authority(): _leave_match.rpc_id(get_multiplayer_authority())