mirror of
https://codeberg.org/sunder/sunder.git
synced 2026-03-07 02:40:23 +00:00
291 lines
10 KiB
GDScript
291 lines
10 KiB
GDScript
# 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 <https://www.gnu.org/licenses/>.
|
|
## 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())
|