👽 interstellar delivery

This commit is contained in:
anyreso 2024-10-16 21:43:01 +00:00
parent 547c97bfba
commit 97c8292858
257 changed files with 7309 additions and 4637 deletions

View file

@ -0,0 +1,50 @@
# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
class_name Match extends Node
enum MatchState {
READY,
RUNNING,
PAUSED,
ENDED
}
signal entered_state(state: MatchState)
signal exiting_state(state: MatchState)
@export var state : MatchState = MatchState.READY:
set = set_state
func set_state(new_state: MatchState) -> void:
if state == new_state: return
exiting_state.emit(state)
state = new_state
entered_state.emit(state)
func _ready() -> void:
pass # Replace with function body.
func start() -> void:
state = MatchState.RUNNING
func end() -> void:
state = MatchState.ENDED
func pause() -> void:
#state = MatchState.PAUSED
push_warning("not implemented yet")
func is_ready() -> bool:
return state == MatchState.READY

View file

@ -0,0 +1,315 @@
# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU 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 connected_to_server
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
}
## The scoreboard to keep track of scores.
@export var scoreboard : Scoreboard
## The multiplayer mode.
@export var mode : Mode = Mode.FREE_FOR_ALL
## The current map node.
@export var map : Node: set = set_map
## The maximum number of clients.
@export var MAX_CLIENTS := 24
## 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
## This is the match timer.
var timer:Timer = null
## The [Teams] manager.
@onready var teams : Teams = $Teams
## The spawn root for [Player] nodes.
@onready var players : Node = $Players
## The spawn root for [Flag] nodes.
@onready var objectives : Node = $Objectives
## The spawn root for Map nodes.
@onready var map_root : Node = $Map
# The spawn root for the scoreboard node.
@onready var _scoreboard_spawn_root : Node = $Scoreboard
## The [Ping] manager.
@onready var ping : Ping = $Ping
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
func set_map(new_map: Node) -> void:
map = new_map
map_root.add_child(map)
MapsManager.current_map = map
## This method starts a server.
func start_server(port : int, map_scene : PackedScene, _mode: Mode = mode, username : String = "mercury") -> void:
# setup enet server peer
var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
peer.create_server(port, MAX_CLIENTS)
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.multiplayer_peer = peer
timer = Timer.new()
timer.one_shot = true
add_child(timer)
map = map_scene.instantiate()
mode = _mode
ping.synchronized.connect(scoreboard._on_ping_sync)
match mode:
Mode.RABBIT:
var flag : Flag = _FLAG.instantiate()
objectives.add_child(flag)
var flagstand : Marker3D = map.get_node("FlagStand")
if flagstand:
flag.global_position = flagstand.global_position
teams.team_added.connect(_on_team_added)
teams.team_erased.connect(_on_team_erased)
teams.add_teams(["rabbit", "chasers"])
flag.grabbed.connect(
func(carry:FlagCarryComponent) -> void:
carry.owner.hud.objective_label.set_visible(true)
switch_team(carry.owner.peer_id, "rabbit"))
flag.dropped.connect(
func(carry:FlagCarryComponent) -> void:
carry.owner.hud.objective_label.set_visible(false)
switch_team(carry.owner.peer_id, "chasers"))
_scoreboard_spawn_root.add_child(
RabbitScoringComponent.new(scoreboard, flag))
_scoreboard_spawn_root.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_spawn_root.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_connected(peer_id: int) -> void:
print("peer `%d` connected" % 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_carry_component._flag
player.flag_carry_component.drop(player.linear_velocity)
var flagstand : Marker3D = map.get_node("FlagStand")
if flagstand:
flag.global_position = flagstand.global_position
scoreboard.reset_scores()
timer.start(MATCH_DURATION) # restart timer with match duration
players.respawn() # respawn everyone
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_carry_component._flag
player.flag_carry_component.drop(player.linear_velocity)
var flagstand : Marker3D = map.get_node("FlagStand")
if flagstand:
flag.global_position = flagstand.global_position
# restart match
_start_match()
func join_server(host : String, port : int, username : String) -> void:
var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
peer.create_client(host, port)
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)
multiplayer.multiplayer_peer = peer
func _on_server_disconnected() -> void:
Game.exit_pressed.emit()
queue_free()
func add_player(_peer_id : int, username : String) -> void:
var player : Player = _PLAYER.instantiate()
# @NOTE: MultiplayerSpawner needs valid names so we can use either
# `add_child(node, true)` or `player.name = str(_peer_id)`.
# > 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.name = str(_peer_id)
player.peer_id = _peer_id
player.username = username
# @NOTE: player scene requires both params prior being added to the tree
players.add_child(player)
# @TODO: get spawn location randomly instead of using points
player.global_position = MapsManager.get_player_spawn().global_position
player.killed.connect(_on_player_killed)
## This method switch a [param peer_id] to the specified team along with its score.
func switch_team(peer_id: int, to_team_name: String) -> void:
var score : Vector3i = scoreboard.get_score(peer_id)
teams.switch_team(peer_id, to_team_name)
scoreboard.set_score(peer_id, score)
# This method is called when the [member Teams.team_addded] signal is emitted.
func _on_team_added(team_name: String) -> void:
var panel : ScorePanel = scoreboard.add_panel(team_name)
panel.title.show()
var team : Team = teams[team_name]
panel.title.set_text(team_name)
team.renamed.connect(panel.title.set_text)
team.player_added.connect(_on_team_player_added)
team.player_erased.connect(_on_team_player_erased)
# This method is called when the [member Teams.team_erased] signal is emitted.
func _on_team_erased(team_name: String) -> void:
var panel : ScorePanel = scoreboard.panels.get_node(team_name)
scoreboard.remove_panel(panel)
# This method is called when the [member Team.player_added] signal is emitted.
func _on_team_player_added(team_name: String, peer_id: int, username : String = "newblood") -> void:
var panel : ScorePanel = scoreboard.panels.get_node(team_name)
panel.add_entry(peer_id, username)
var player:Player = players.get_node(str(peer_id))
player.team_id = teams[team_name].get_instance_id()
# This method is called when the [member Teams.Team.player_erased] signal is emitted.
func _on_team_player_erased(team_name: String, peer_id: int) -> void:
var panel : ScorePanel = scoreboard.panels.get_node(team_name)
panel.remove_entry_by_peer_id(peer_id)
var player:Player = players.get_node(str(peer_id))
player.team_id = -1
func _on_connected_to_server(username : String) -> void:
connected_to_server.emit()
_join_match.rpc_id(1, 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 = MapsManager.get_player_spawn()
victim.respawn.rpc_id(1, 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():
# add player to server with unique id and username
add_player(multiplayer.get_remote_sender_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)
players.remove_child(player)
player.queue_free()
_cleanup_peer.rpc_id(peer_id)
@rpc("authority", "call_local", "reliable")
func _cleanup_peer() -> void:
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
queue_free()
func _exit_tree() -> void:
# @NOTE: The `is_multiplayer_authority` method in `_extree` push an error
# about the multiplayer instance not being the currently active one.it_
# see https://github.com/godotengine/godot/issues/77723
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()

View file

@ -0,0 +1,55 @@
[gd_scene load_steps=9 format=3 uid="uid://bvwxfgygm2xb8"]
[ext_resource type="Script" path="res://types/multiplayer/multiplayer.gd" id="1_r1kd6"]
[ext_resource type="Script" path="res://types/multiplayer/ping.gd" id="1_tafn1"]
[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://entities/player/player.tscn" id="2_og1vb"]
[ext_resource type="PackedScene" uid="uid://c88l3h0ph00c7" path="res://entities/flag/flag.tscn" id="3_h0rie"]
[ext_resource type="PackedScene" uid="uid://b8bosdd0o1lu7" path="res://interfaces/scoreboard/scoreboard.tscn" id="4_rhwwf"]
[ext_resource type="Script" path="res://types/multiplayer/teams.gd" id="5_op0if"]
[ext_resource type="Script" path="res://types/multiplayer/players.gd" id="7_ycqj1"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_g11jg"]
properties/0/path = NodePath(".:mode")
properties/0/spawn = true
properties/0/replication_mode = 2
[node name="Multiplayer" type="Node" node_paths=PackedStringArray("scoreboard")]
script = ExtResource("1_r1kd6")
scoreboard = NodePath("Scoreboard/Scoreboard")
_PLAYER = ExtResource("2_og1vb")
_FLAG = ExtResource("3_h0rie")
[node name="MultiplayerSync" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_g11jg")
[node name="Ping" type="Node" parent="."]
script = ExtResource("1_tafn1")
active = true
sync_interval = 0.5
[node name="Teams" type="Node" parent="."]
script = ExtResource("5_op0if")
[node name="Scoreboard" type="Node" parent="."]
[node name="Scoreboard" parent="Scoreboard" instance=ExtResource("4_rhwwf")]
[node name="Map" type="Node" parent="."]
[node name="MapSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://maps/desert/desert.tscn")
spawn_path = NodePath("../Map")
spawn_limit = 1
[node name="Players" type="Node" parent="."]
script = ExtResource("7_ycqj1")
[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://entities/player/player.tscn")
spawn_path = NodePath("../Players")
[node name="Objectives" type="Node" parent="."]
[node name="ObjectiveSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://entities/flag/flag.tscn")
spawn_path = NodePath("../Objectives")

173
types/multiplayer/ping.gd Normal file
View file

@ -0,0 +1,173 @@
# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## This defines a ping manager.
##
## When added to your scene and connect the [member synchronized] signal to your
## custom handler in order to receive ping updates on a regular basis (as
## defined by [param ping_interval] and [param sync_interval]) for each peer.
## [codeblock]
## func _ready() -> void:
## $Ping.synchronized.connect(_on_ping_sync)
##
## func _on_ping_sync(state: Dictionary) -> void:
## for peer_id in state:
## print("peer `%s` latency is %d ms" % [peer_id, state[peer_id]])
## [/codeblock]
class_name Ping extends Node
## This class defines a ping information to keep track of ping results
## and provide average ping.
class PingInfo:
const SIZE := 21
var samples := PackedInt32Array()
var pos := 0
func _init() -> void:
samples.resize(SIZE)
for i in range(SIZE):
samples[i] = 0
func get_average() -> int:
var value := 0
for sample in samples:
value += sample
@warning_ignore("integer_division")
return value / SIZE if value > 0 else 0
func set_next(value: int) -> void:
samples[pos] = value
pos = (pos + 1) % SIZE
## Emitted when the state is synchronized.
signal synchronized(state : Dictionary)
## Controls activation state of the ping feature.
@export var active: bool = false
## The time interval (in milliseconds) between ping requests sent by the server to measure client latency.
@export_range(0.,1.,.01) var ping_interval: float = .2 # ms
## The time interval (in milliseconds) between state synchronization updates sent to connected clients.
@export_range(0.,1.,.01) var sync_interval: float = 1. # ms
## The size of the ping history buffer used to store past ping times for each client.
@export var ping_history: int = 10:
set = set_ping_history
# internal variables
var _clients := {}
var _state := {}
var _last_ping := 0
var _pings := []
var _last_sync := 0
func _init() -> void:
clear_pings()
func _ready() -> void:
multiplayer.peer_connected.connect(_add_peer)
multiplayer.peer_disconnected.connect(_del_peer)
func _exit_tree() -> void:
multiplayer.peer_connected.disconnect(_add_peer)
multiplayer.peer_disconnected.disconnect(_del_peer)
# synchronize state only on server
func _process(_delta: float) -> void:
if not active \
or multiplayer.multiplayer_peer is OfflineMultiplayerPeer \
or multiplayer.multiplayer_peer.get_connection_status() \
!= MultiplayerPeer.CONNECTION_CONNECTED \
or not multiplayer.is_server() \
or not multiplayer.has_multiplayer_peer():
return
# sync state at regular intervals
var now := Time.get_ticks_msec()
if sync_interval * 1000 > 0 and now >= _last_sync + sync_interval * 1000:
synchronize()
_last_sync = now
# check and update ping intervals
if now >= _pings[_last_ping] + ping_interval * 1000:
_last_ping = (_last_ping + 1) % ping_history
_pings[_last_ping] = now
_ping.rpc(now)
## Clears the ping history array.
func clear_pings() -> void:
_last_ping = 0
_pings.resize(ping_history)
_pings.fill(0)
## Sets the size of the ping history buffer and clears existing ping data.
func set_ping_history(value: int) -> void:
if value < 1:
return
ping_history = value
clear_pings()
## Adds a new peer to the registry
func _add_peer(peer_id: int) -> void:
_clients[peer_id] = PingInfo.new()
## Deletes a peer from the registry
func _del_peer(peer_id: int) -> void:
_clients.erase(peer_id)
@rpc("unreliable")
func _ping(time: int) -> void:
_pong.rpc_id(1, time)
@rpc("any_peer")
func _pong(time: int) -> void:
if not multiplayer.is_server():
return
# get id of the peer sending the pong
var peer_id: int = multiplayer.get_remote_sender_id()
# check if peer exists in the registered clients dictionary
if not _clients.has(peer_id):
return
# init variables
var now := Time.get_ticks_msec()
var last := (_last_ping + 1) % ping_history
var found := ping_history * ping_interval * 1000
# search related ping in history
for i in range(ping_history):
# check if current ping matches received pong
if time == _pings[last]:
found = _pings[last]
break
# move to next ping in history
last = (last + 1) % ping_history
# compute round-trip time (RTT) and update ping info for this peer
_clients[peer_id].set_next((now - found) / 2)
## This function sends the current state of ping information to all connected peers
## and emits a signal indicating that the state has been synchronized.
func synchronize() -> void:
# check if server is active and has multiplayer peers connected
if not active or not multiplayer.has_multiplayer_peer() or not multiplayer.is_server():
return
# clear current state
_state.clear()
# iterate over registered clients
for peer_id:int in _clients:
# set client average ping state
_state[peer_id] = _clients[peer_id].get_average()
# send state to connected peers
_synchronize.rpc(_state)
# emit signal to indicate that synchronization has occured
synchronized.emit(_state)
@rpc("call_local", "unreliable")
func _synchronize(state: Dictionary) -> void:
_state = state
synchronized.emit(_state)

View file

@ -0,0 +1,21 @@
# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
extends Node
func respawn() -> void:
for player : Player in get_children():
var spawn : Node3D = MapsManager.get_player_spawn()
if spawn:
player.respawn.rpc_id(get_multiplayer_authority(), spawn.global_position)

85
types/multiplayer/team.gd Normal file
View file

@ -0,0 +1,85 @@
# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## This class defines a Team.
class_name Team extends RefCounted
## Emitted when the [Team] is renamed.
signal renamed(team_name: String)
## Emitted when a peer_id is added to the [Team].
signal player_added(team_name: String, peer_id: int, username: String)
## Emitted when a peer_id is erased from the [Team].
signal player_erased(team_name: String, peer_id: int)
var name : String:
set(new_name):
name = new_name
renamed.emit(name)
var players: Dictionary = {}
## Constructor.
func _init(team_name: String, team_players: Dictionary = {}) -> void:
name = team_name
for peer_id : int in team_players:
add(peer_id, team_players[peer_id])
# This is the iterator index cursor.
var _iter_cursor : int = 0
# This method is an iterator initializer.
func _iter_init(_arg : Variant) -> bool:
_iter_cursor = 0 # reset
print(_iter_cursor < players.size())
return _iter_cursor < players.size()
# This method checks if the iterator has a next value.
func _iter_next(_arg : Variant) -> bool:
_iter_cursor += 1
return _iter_cursor < players.size()
# This method gets the next iterator value.
func _iter_get(_arg : Variant) -> int:
return players.keys()[_iter_cursor]
## Add [param peer_id] to the [Team] if not already present.
func add(peer_id: int, username : String = "") -> void:
if peer_id not in players:
players[peer_id] = username
player_added.emit(name, peer_id, username)
## Erase [param peer_id] from the [Team].
func erase(peer_id: int) -> bool:
if players.erase(peer_id):
player_erased.emit(name, peer_id)
return true
return false
## This method pops out the related [param peer_id] string.
func pop(peer_id: int) -> String:
var value : String = players[peer_id]
erase(peer_id)
return value
## Returns the size of the team.
func size() -> int:
return players.size()
func serialize() -> Variant:
return [name, players]
func _to_string() -> String:
return "<Team(%s)#%s>" % [name, get_instance_id()]

144
types/multiplayer/teams.gd Normal file
View file

@ -0,0 +1,144 @@
# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## This class defines a Teams manager
class_name Teams extends Node
## Emitted when a [Team] is created.
signal team_added(team_name : String)
## Emitted when a [Team] is removed.
signal team_erased(team_name : String)
## Emitted when a [Player] is added to a [Team].
signal player_added(team_name: String, peer_id: int, username: String)
## Emitted when a [Player] is removed from a [Team].
signal player_erased(team_name: String, peer_id: int)
# The synced teams state.
@export var _state : Dictionary = {}:
get = get_state, set = set_state
func get_state() -> Dictionary:
for team : Team in _teams.values():
_state[team.name] = team.players
return _state
func set_state(new_state: Dictionary) -> void:
_state = new_state
# The internal dictionary of teams.
var _teams : Dictionary = {}
## Retrieves an [Array] of [int] by [param team_name].
## [codeblock]
## var teams := Teams.new()
## teams.add_team("Phoenix")
## print(teams["Phoenix"]) # this calls `_get`
## [/codeblock]
func _get(team_name : StringName) -> Variant:
return _teams.get(team_name)
## Sets a [Team] by [param name] with an optional [param default].
## [codeblock]
## var teams := Teams.new()
## teams["Phoenix"] = Teams.Team.new("Phoenix") # this calls `_set`
## teams["Phoenix"] = null # refcount for Team is now 0
## [/codeblock]
func _set(key : StringName, team : Variant) -> bool:
if team == null:
return erase(key)
if team is Team:
_teams[key] = team
team_added.emit(key)
return true
return false
## This method retrieves the array of managed [Team] names.
func keys() -> Array:
return _teams.keys()
## This method retrieves the array of managed [Team].
func values() -> Array:
return _teams.values()
# This is the iterator index cursor.
var _iter_cursor : int = 0
# This method is an iterator initializer.
func _iter_init(_arg : Variant) -> bool:
_iter_cursor = 0 # reset
return _iter_cursor < len(_teams)
# This method checks if the iterator has a next value.
func _iter_next(_arg : Variant) -> bool:
_iter_cursor += 1
return _iter_cursor < len(_teams)
# This method gets the next iterator value.
func _iter_get(_arg : Variant) -> Team:
return _teams.values()[_iter_cursor]
## This method adds a new [Team] into the manager.
func add_team(key: String) -> bool:
if not _teams.has(key):
var team : Team = Team.new(key)
team.player_added.connect(_on_team_player_added)
team.player_erased.connect(_on_team_player_erased)
return _set(key, team)
return false
## This method adds new [Team] in batch from [param team_names].
func add_teams(team_names: Array[String]) -> Node:
for team_name in team_names:
add_team(team_name)
return self
func get_peer_team(peer_id : int) -> Team:
for team : Team in _teams.values():
if peer_id in team.players:
return team
return null
func erase(team_name: String) -> bool:
var res: bool = _teams.erase(team_name)
if res:
team_erased.emit(team_name)
return res
func erase_player(peer_id: int) -> void:
for team : Team in _teams.values():
if team.erase(peer_id):
break
## The number of [Team].
func size() -> int:
return len(_teams)
## This method eases peer team switching by moving them to the specified [param team_name].
## It does not do anything if the peer is not already added to a [Team], see [method Team.add].
func switch_team(peer_id: int, team_name: String) -> void:
var lhs : Team = get_peer_team(peer_id)
var rhs : Team = _get(team_name)
if lhs and rhs:
rhs.add(peer_id, lhs.pop(peer_id))
# This method runs when a Team emits `player_added` signal
func _on_team_player_added(team_name: String, peer_id: int, username: String) -> void:
player_added.emit(team_name, peer_id, username) # emit manager signal
# This method runs when a Team emits `player_erased` signal
func _on_team_player_erased(team_name: String, peer_id: int) -> void:
player_erased.emit(team_name, peer_id) # emit manager signal

View file

@ -1,6 +0,0 @@
class_name ArrayPackedSceneResource extends Resource
@export var _packed_scenes : Array[PackedScene] = []
func get_items() -> Array[PackedScene]:
return _packed_scenes

View file

@ -1,7 +0,0 @@
[gd_resource type="Resource" script_class="ArrayPackedSceneResource" load_steps=2 format=3 uid="uid://cpnargstkucch"]
[ext_resource type="Script" path="res://types/resources/array_packed_scene.gd" id="1_r1ygd"]
[resource]
script = ExtResource("1_r1ygd")
_packed_scenes = Array[PackedScene]([])

View file

@ -0,0 +1,35 @@
# This file is part of open-fpsz.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
class_name Singleplayer extends Node
@onready var player : Player = $Player
@onready var flag : Flag = $Flag
@onready var map : Node = $Desert
func _ready() -> void:
player.health.exhausted.connect(_on_player_dead)
var flagstand : Marker3D = map.get_node("FlagStand")
if flagstand:
flag.global_position = flagstand.position
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("exit"):
queue_free()
func _on_player_dead(_player : Player) -> void:
var spawns : Node = map.get_node("PlayerSpawns")
var spawn : Marker3D = spawns.get_child(
randi_range(0, spawns.get_child_count() - 1))
player.respawn(spawn.global_position)

View file

@ -0,0 +1,27 @@
[gd_scene load_steps=7 format=3 uid="uid://boviiugcnfyrj"]
[ext_resource type="Script" path="res://types/singleplayer/demo.gd" id="1_kkjqs"]
[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://entities/player/player.tscn" id="2_6wbjq"]
[ext_resource type="PackedScene" uid="uid://c88l3h0ph00c7" path="res://entities/flag/flag.tscn" id="4_1j2pw"]
[ext_resource type="PackedScene" uid="uid://btlkog4b87p4x" path="res://maps/desert/desert.tscn" id="4_dogmv"]
[ext_resource type="PackedScene" uid="uid://dpnu1lvfncx6q" path="res://entities/dummy_target/dummy_target.tscn" id="5_euor2"]
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_c5jqv"]
resource_local_to_scene = true
bounce = 1.0
absorbent = true
[node name="Demo" type="Node"]
script = ExtResource("1_kkjqs")
[node name="Player" parent="." instance=ExtResource("2_6wbjq")]
transform = Transform3D(-0.000506087, 0, -1, 0, 1, 0, 1, 0, -0.000506087, 612.497, 211.292, 853.632)
physics_material_override = SubResource("PhysicsMaterial_c5jqv")
[node name="Desert" parent="." instance=ExtResource("4_dogmv")]
[node name="DummyTarget" parent="." instance=ExtResource("5_euor2")]
transform = Transform3D(-0.997996, 0, -0.0632782, 0, 1, 0, 0.0632782, 0, -0.997996, 901.962, 145.367, 882.65)
[node name="Flag" parent="." instance=ExtResource("4_1j2pw")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 895.356, 145.367, 888.261)