summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnson Bridges <bridges.anson@gmail.com>2025-08-22 12:46:04 -0700
committerAnson Bridges <bridges.anson@gmail.com>2025-08-22 12:46:04 -0700
commit64f37f4209d80bfad976dd4a139c98002caef15f (patch)
tree94c91953986e960573ae0092f8ad7120c1c266f0
parent255fbf19cc9499ef384d41f68515da5e49e8a3ce (diff)
the harmonious transition from menu to gameHEADmaster
-rw-r--r--export_presets.cfg2
-rw-r--r--network/GameCoordinatorClient.gd209
-rw-r--r--network/GameCoordinatorTester.gd2
-rw-r--r--network/WSClient.tscn6
-rw-r--r--network/websocket_client_basic.gd5
-rw-r--r--objects/Airplane.tscn (renamed from objects/Plane.tscn)2
-rw-r--r--objects/HexSpace.tscn4
-rw-r--r--objects/PlaneControlBoard.tscn96
-rw-r--r--objects/PlayerListEntry.tscn42
-rw-r--r--pages/GameTable.tscn412
-rw-r--r--pages/MainMenu.tscn53
-rw-r--r--pages/MainScene.tscn7
-rw-r--r--pages/ServerBrowser.tscn1
-rw-r--r--project.godot20
-rw-r--r--resources/external/game_coordinator.py65
-rw-r--r--resources/unused_code.gd16
-rw-r--r--scripts/Airplane.gd (renamed from scripts/Plane.gd)3
-rw-r--r--scripts/Board.gd30
-rw-r--r--scripts/GameTable.gd161
-rw-r--r--scripts/Globals.gd3
-rw-r--r--scripts/HexSpace.gd5
-rw-r--r--scripts/MainMenu.gd27
-rw-r--r--scripts/MainScene.gd71
-rw-r--r--scripts/PlaneControlBoard.gd6
-rw-r--r--scripts/ServerBrowser.gd6
25 files changed, 1105 insertions, 149 deletions
diff --git a/export_presets.cfg b/export_presets.cfg
index c34ad69..64cd65b 100644
--- a/export_presets.cfg
+++ b/export_presets.cfg
@@ -5,7 +5,7 @@ platform="HTML5"
runnable=true
custom_features=""
export_filter="all_resources"
-include_filter=""
+include_filter="*.txt"
exclude_filter=""
export_path=""
script_export_mode=1
diff --git a/network/GameCoordinatorClient.gd b/network/GameCoordinatorClient.gd
index 3d87b7b..eac86e8 100644
--- a/network/GameCoordinatorClient.gd
+++ b/network/GameCoordinatorClient.gd
@@ -1,32 +1,80 @@
+class_name GCClient
extends Node
# CONNECTION DATA
-enum { NONE, SERVER_BROWSER, LOBBY, VOICE }
+
+enum { NONE, SERVER_BROWSER, LOBBY_WAITING, LOBBY, VOICE }
+var connection_type : int = NONE
+
var socket_client: WebSocketClient = null
var socket: WebSocketPeer = null
-var state: int = 0 # -1 = CONNECTION_FAILED, 0 = CONNECTION_DISCONNECTED, 1 = CONNECTION_CONNECTING, 2 = CONNECTION_CONNECTED, 3 = CONNECTION_DISCONNECTING
+
+enum { CONNECTION_FAILED, CONNECTION_DISCONNECTED, CONNECTION_CONNECTING, CONNECTION_CONNECTED, CONNECTION_DISCONNECTING }
+var state: int = CONNECTION_DISCONNECTED
+
var message_queue : Array = [] # messages to be sent upon connection
-var connection_type : int = NONE
+
+# END CONNECTION DATA
# GC DATA
+
+# connected from menus
+signal gc_connection_failed()
+signal lobby_list_acquired(lobbies) # connected before calling get_lobby_list to retrieve result
+signal join_request_success()
+signal join_failure(reason)
+signal join_success(args)
+signal host_failure(reason)
+signal host_success()
+
+# connected from in game
+signal disconnected_from_lobby(reason)
+signal join_request_received(player_id, args)
+signal connection_closed_dirty() # game should be quit for unexpected reasons
+
var is_host : bool
-var host_id : String
-var lobby_id : String
-var player_id : String
-var rejoin_key : String
+var host_id : String = ""
+var lobby_id : String = ""
+var player_id : String = ""
+var rejoin_key : String = ""
var players : Array = []
+var max_players : int = -1
+var lobby_name : String = ""
+var is_private : bool = false
+var lobby_password : String = ""
+
+const GC_MSG_TYPES : Array = ["join_request_ok", "join_ok", "join_fail",
+ "rejoin_ok", "rejoin_fail", "host_ok", "host_fail",
+ "disconnected", "join_request", "lobby_closing"]
+
+
+# END GC DATA
func _ready():
socket_client = WebSocketClient.new()
socket_client.connect("connection_established", self, "on_connection_success")
- socket_client.connect("connection_closed", self, "on_connection_close_success")
+ socket_client.connect("connection_closed", self, "on_connection_close")
socket_client.connect("connection_error", self, "on_connection_error")
#socket_client.connect("data_received", self, "receive")
+func gc_client_reset():
+ self.player_id = ""
+ self.lobby_id = ""
+ self.host_id = ""
+ self.rejoin_key = ""
+ self.players.clear()
+ self.is_host = false
+ self.connection_type = NONE
+ self.max_players = -1
+ self.lobby_name = ""
+ self.is_private = false
+
func connect_to_gc(c_type: int):
- if !(c_type in [SERVER_BROWSER, LOBBY, VOICE]):
+ if !(c_type in [SERVER_BROWSER, LOBBY_WAITING, VOICE]):
print("Invalid connection type!")
return
+
+ gc_client_reset()
connection_type = c_type
var url : String = Globals.GC_URL
print("Connecting to game coordinator: %s..." % url)
@@ -35,67 +83,172 @@ func connect_to_gc(c_type: int):
if error != OK:
return error
- state = 1 # CONNECTING
+ state = CONNECTION_CONNECTING
return OK
-func sock_close(code = 1000, reason = ""):
+func close_socket(code = 1000, reason = ""):
+ if state == CONNECTION_DISCONNECTED or state == CONNECTION_FAILED: return
print("Closing websocket...")
socket_client.disconnect_from_host(code, reason)
- state = 3 # DISCONNECTING
+ state = CONNECTION_DISCONNECTING # DISCONNECTING
func on_connection_success(protocol):
print("WebSocket connection success with protocol %s." % protocol)
socket = socket_client.get_peer(1)
socket.set_write_mode(WebSocketPeer.WRITE_MODE_TEXT) # defaults to text mode
- state = 2 # CONNECTED
+ state = CONNECTION_CONNECTED # CONNECTED
while len(message_queue) > 0:
var msg = message_queue.pop_at(0)
send(msg)
-func on_connection_close_success(clean):
+func on_connection_close(clean):
print("WebSocket closed successfully.")
socket = null
connection_type = NONE
if clean:
- state = 0 # DISCONNECTED
+ state = CONNECTION_DISCONNECTED # DISCONNECTED
else:
- state = -1 # DISCONNECT DIRTY
+ emit_signal("connection_closed_dirty")
+ state = CONNECTION_FAILED # DISCONNECT DIRTY
func on_connection_error(): # connection failed
print("WebSocket connection failed!")
socket = null
- state = -1 # DISCONNECT DIRTY
+ state = CONNECTION_FAILED # DISCONNECT DIRTY
connection_type = NONE
+ emit_signal("gc_connection_failed")
func send(message, as_bytes=false) -> int:
- if state != 2:
+ if state != CONNECTION_CONNECTED:
message_queue.push_back(message)
return -1
return socket.put_packet(message)
-
func send_json(message) -> int:
- if state != 2:
+ if state != CONNECTION_CONNECTED:
message_queue.push_back(JSON.print(message).to_utf8())
return -1
var message_json = JSON.print(message).to_utf8()
return socket.put_packet(message_json)
-func receive(string_to_json=false):
- if state != 2: return null
+func receive():
+ if state != CONNECTION_CONNECTED: return null
if socket.get_available_packet_count() < 1: return null
var packet : PoolByteArray = socket.get_packet()
if socket.was_string_packet():
var message = packet.get_string_from_utf8()
- if string_to_json:
- var json = JSON.parse(message)
- if json.error:
- return null
- message = json.result
+ var json = JSON.parse(message)
+ if json.error:
+ return null
+ message = json.result
+ process_gc_message(message)
return message
- return bytes2var(packet)
+ return null # bytes2var(packet)
+
+# process GC state specific message/content of messages
+# returns false in the case of game-breaking invalid message receipt
+func process_gc_message(message):
+ # always update player information whenever an update is received from the GC
+ if "current_players" in message:
+ for player in players:
+ if player["is_host"] and self.host_id != player["player_id"]:
+ self.host_id = player["player_id"]
+ self.is_host = self.host_id == self.player_id
+
+ if "type" in message and (message["type"] in GC_MSG_TYPES): # validate GC message format
+ if message["type"] == "join_request_ok":
+ if !("player_id" in message and "rejoin_key" in message):
+ close_socket() # GC did not provide necessary info
+ self.player_id = message["player_id"]
+ self.rejoin_key = message["rejoin_key"]
+ self.lobby_name = message["lobby_name"]
+ self.max_players = message["max_players"]
+ self.is_private = message["private"]
+ emit_signal("join_request_success")
+ elif message["type"] == "join_ok":
+ self.connection_type = LOBBY
+ var join_args = {}
+ if "args" in message:
+ join_args = message["args"]
+
+ emit_signal("join_success", join_args)
+ elif message["type"] == "join_fail":
+ var reason = "Unknown failure." if not ("message" in message) else message["message"]
+ gc_client_reset()
+ emit_signal( "join_failure", reason )
+
+ elif message["type"] == "disconnected":
+ state = CONNECTION_DISCONNECTING # socket will be closed soon
+ close_socket()
+ gc_client_reset()
+ emit_signal( "disconnected_from_lobby", message["reason"] )
+
+ elif message["type"] == "host_ok":
+ if !(("player_id" in message) and ("rejoin_key" in message) and ("lobby_id" in message)):
+ close_socket() # GC did not provide necessary info
+ self.lobby_id = message["lobby_id"]
+ self.player_id = message["player_id"]
+ self.rejoin_key = message["rejoin_key"]
+ self.is_host = true
+ self.connection_type = LOBBY
+ emit_signal("host_success")
+ elif message["type"] == "host_fail":
+ var reason = "Unknown failure." if not ("message" in message) else message["message"]
+ close_socket()
+ gc_client_reset()
+ emit_signal("host_failure", reason)
+
+ elif message["type"] == "join_request":
+ if !("player_id" in message and "args" in message):
+ print("Invalid join request received.")
+ return
+ emit_signal("join_request_received", message["player_id"], message["args"])
+
+ elif message["type"] == "lobby_list":
+ emit_signal("lobby_list_acquired", message["lobby_list"])
+ gc_client_reset()
+ close_socket()
+
+ elif message["type"] == "lobby_closing":
+ emit_signal("disconnected_from_lobby", "Lobby closing.")
+ gc_client_reset()
+ close_socket()
+
+ return
+
+func get_lobby_list():
+ gc_client_reset()
+ var message = {"type" : "list_open_lobbies"}
+ if state != CONNECTION_CONNECTED:
+ connect_to_gc(SERVER_BROWSER)
+ send_json( message )
+ else:
+ send_json( message )
+func host_lobby(lobby_name : String, username : String, max_players : int, private : bool, password : String = ""):
+ close_socket()
+ gc_client_reset()
+
+ var request = {"type" : "host_lobby", "lobby_name" : lobby_name, "game_type" : Globals.GAME_TYPE, "lobby_id" : lobby_id, "username" : username, "max_players" : max_players, "private" : private, "password" : password}
+ connect_to_gc(LOBBY_WAITING)
+ self.lobby_name = lobby_name
+ self.max_players = max_players
+ self.is_private = private
+ self.lobby_password = password
+ send_json(request)
+
+func join_lobby(lobby_id : String, password : String, username : String, args = null):
+ close_socket()
+ gc_client_reset()
+
+ var request = {"type" : "join_lobby", "game_type" : Globals.GAME_TYPE, "lobby_id" : lobby_id, "password" : password, "username" : username}
+ if args: request["args"] = args
+ connect_to_gc(LOBBY_WAITING)
+ self.lobby_id = lobby_id
+ self.is_host = false
+ self.lobby_password = password
+ send_json(request)
func _process(_delta):
socket_client.poll()
diff --git a/network/GameCoordinatorTester.gd b/network/GameCoordinatorTester.gd
index 12f92ab..bae0e82 100644
--- a/network/GameCoordinatorTester.gd
+++ b/network/GameCoordinatorTester.gd
@@ -3,7 +3,7 @@ extends Control
onready var ws_client_template : PackedScene = preload("res://network/WSClient.tscn")
-var game_coordinator_url : String = "ws://192.168.7.112:8181"
+var game_coordinator_url : String = "ws://acetyl.net:8181"
var gc_client_menus : Dictionary = {}
var latest_gccid : int = 0
diff --git a/network/WSClient.tscn b/network/WSClient.tscn
new file mode 100644
index 0000000..bfb9383
--- /dev/null
+++ b/network/WSClient.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://network/websocket_client_basic.gd" type="Script" id=1]
+
+[node name="WSClient" type="Node"]
+script = ExtResource( 1 )
diff --git a/network/websocket_client_basic.gd b/network/websocket_client_basic.gd
index 353dd81..846e721 100644
--- a/network/websocket_client_basic.gd
+++ b/network/websocket_client_basic.gd
@@ -39,12 +39,13 @@ func on_connection_success(protocol):
send(msg)
func on_connection_close_success(clean):
- print("WebSocket closed successfully.")
socket = null
if clean:
state = 0 # DISCONNECTED
+ print("WebSocket closed successfully.")
else:
state = -1 # DISCONNECT DIRTY
+ print("WebSocket closed unsuccessfully!")
func on_connection_error(): # connection failed
print("WebSocket connection failed!")
@@ -59,7 +60,9 @@ func send(message, as_bytes=false) -> int:
func send_json(message) -> int:
+ print("sending")
if state != 2:
+ print("adding to queue")
message_queue.push_back(JSON.print(message).to_utf8())
return -1
var message_json = JSON.print(message).to_utf8()
diff --git a/objects/Plane.tscn b/objects/Airplane.tscn
index 21ce972..975fd6f 100644
--- a/objects/Plane.tscn
+++ b/objects/Airplane.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=15 format=2]
-[ext_resource path="res://scripts/Plane.gd" type="Script" id=1]
+[ext_resource path="res://scripts/Airplane.gd" type="Script" id=1]
[ext_resource path="res://resources/fonts/Cochineal-Bold.otf" type="DynamicFontData" id=2]
[ext_resource path="res://textures/plane_action_indicator_board.png" type="Texture" id=3]
[ext_resource path="res://textures/gradient_transparent.png" type="Texture" id=4]
diff --git a/objects/HexSpace.tscn b/objects/HexSpace.tscn
index 2fa1d71..f703422 100644
--- a/objects/HexSpace.tscn
+++ b/objects/HexSpace.tscn
@@ -147,9 +147,9 @@ transform = Transform( 0.866025, 0, -0.5, 0, 1, 0, 0.5, 0, 0.866025, 0.0160618,
mesh = SubResource( 17 )
[node name="AirportName" type="Label3D" parent="Airport"]
-transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.971102, 0 )
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -0.252818, 0.302783, -0.208905 )
visible = false
-pixel_size = 0.001
+pixel_size = 0.0007
billboard = 1
fixed_size = true
text = "AIRPORT NAME"
diff --git a/objects/PlaneControlBoard.tscn b/objects/PlaneControlBoard.tscn
index 4c58d5d..7f2364a 100644
--- a/objects/PlaneControlBoard.tscn
+++ b/objects/PlaneControlBoard.tscn
@@ -67,7 +67,7 @@ margin_right = -2.0
margin_bottom = -6.0
[node name="Label" type="Label" parent="DestinationHBOX"]
-margin_top = 24.0
+margin_top = 23.0
margin_right = 111.0
margin_bottom = 40.0
theme = ExtResource( 1 )
@@ -75,11 +75,21 @@ custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "DESTINATION: "
[node name="DestinationIcon" type="TextureRect" parent="DestinationHBOX"]
+visible = false
margin_left = 115.0
margin_right = 179.0
margin_bottom = 64.0
texture = ExtResource( 5 )
+[node name="DestinationText" type="Label" parent="DestinationHBOX"]
+visible = false
+margin_left = 115.0
+margin_top = 23.0
+margin_right = 115.0
+margin_bottom = 40.0
+theme = ExtResource( 1 )
+custom_colors/font_color = Color( 0, 0, 0, 1 )
+
[node name="AltitudeHBox" type="HBoxContainer" parent="."]
anchor_left = 0.05
anchor_top = 0.3
@@ -87,30 +97,30 @@ anchor_right = 0.941
anchor_bottom = 0.3
[node name="Label" type="Label" parent="AltitudeHBox"]
-margin_top = 24.0
-margin_right = 109.0
+margin_top = 23.0
+margin_right = 106.0
margin_bottom = 40.0
theme = ExtResource( 1 )
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "ALTITUDE: "
[node name="Altitude1" type="TextureRect" parent="AltitudeHBox"]
-margin_left = 113.0
-margin_right = 177.0
+margin_left = 110.0
+margin_right = 174.0
margin_bottom = 64.0
texture = ExtResource( 5 )
[node name="Altitude2" type="TextureRect" parent="AltitudeHBox"]
modulate = Color( 0.3, 0.3, 0.3, 1 )
-margin_left = 181.0
-margin_right = 245.0
+margin_left = 178.0
+margin_right = 242.0
margin_bottom = 64.0
texture = ExtResource( 3 )
[node name="Altitude3" type="TextureRect" parent="AltitudeHBox"]
modulate = Color( 0.3, 0.3, 0.3, 1 )
-margin_left = 249.0
-margin_right = 313.0
+margin_left = 246.0
+margin_right = 310.0
margin_bottom = 64.0
size_flags_horizontal = 0
size_flags_vertical = 0
@@ -124,160 +134,160 @@ anchor_bottom = 0.9
[node name="ActionSquare1" type="VBoxContainer" parent="ActionHBox"]
margin_right = 50.0
-margin_bottom = 74.0
+margin_bottom = 77.0
script = SubResource( 3 )
[node name="MoveFirstButton" type="Button" parent="ActionHBox/ActionSquare1"]
margin_right = 50.0
-margin_bottom = 22.0
+margin_bottom = 23.0
theme = ExtResource( 1 )
text = ">>"
[node name="ActionOption" type="OptionButton" parent="ActionHBox/ActionSquare1"]
-margin_top = 26.0
+margin_top = 27.0
margin_right = 50.0
-margin_bottom = 48.0
+margin_bottom = 50.0
rect_min_size = Vector2( 50, 0 )
theme = ExtResource( 1 )
items = [ "", null, false, 0, null, "←", null, false, 2, null, "→", null, false, 3, null, "↑", null, false, 4, null, "↓", null, false, 5, null ]
selected = 0
[node name="MoveLastButton" type="Button" parent="ActionHBox/ActionSquare1"]
-margin_top = 52.0
+margin_top = 54.0
margin_right = 50.0
-margin_bottom = 74.0
+margin_bottom = 77.0
theme = ExtResource( 1 )
[node name="Spacer1" type="Control" parent="ActionHBox"]
margin_left = 54.0
margin_right = 73.0
-margin_bottom = 74.0
+margin_bottom = 77.0
size_flags_horizontal = 3
[node name="ActionSquare2" type="VBoxContainer" parent="ActionHBox"]
margin_left = 77.0
margin_right = 127.0
-margin_bottom = 74.0
+margin_bottom = 77.0
script = SubResource( 3 )
[node name="MoveFirstButton" type="Button" parent="ActionHBox/ActionSquare2"]
margin_right = 50.0
-margin_bottom = 22.0
+margin_bottom = 23.0
theme = ExtResource( 1 )
text = ">>"
[node name="ActionOption" type="OptionButton" parent="ActionHBox/ActionSquare2"]
-margin_top = 26.0
+margin_top = 27.0
margin_right = 50.0
-margin_bottom = 48.0
+margin_bottom = 50.0
rect_min_size = Vector2( 50, 0 )
theme = ExtResource( 1 )
items = [ "", null, false, 0, null, "←", null, false, 2, null, "→", null, false, 3, null, "↑", null, false, 4, null, "↓", null, false, 5, null ]
selected = 0
[node name="MoveLastButton" type="Button" parent="ActionHBox/ActionSquare2"]
-margin_top = 52.0
+margin_top = 54.0
margin_right = 50.0
-margin_bottom = 74.0
+margin_bottom = 77.0
theme = ExtResource( 1 )
[node name="Spacer2" type="Control" parent="ActionHBox"]
margin_left = 131.0
margin_right = 151.0
-margin_bottom = 74.0
+margin_bottom = 77.0
size_flags_horizontal = 3
[node name="ActionSquare3" type="VBoxContainer" parent="ActionHBox"]
margin_left = 155.0
margin_right = 205.0
-margin_bottom = 74.0
+margin_bottom = 77.0
script = SubResource( 3 )
[node name="MoveFirstButton" type="Button" parent="ActionHBox/ActionSquare3"]
margin_right = 50.0
-margin_bottom = 22.0
+margin_bottom = 23.0
theme = ExtResource( 1 )
text = ">>"
[node name="ActionOption" type="OptionButton" parent="ActionHBox/ActionSquare3"]
-margin_top = 26.0
+margin_top = 27.0
margin_right = 50.0
-margin_bottom = 48.0
+margin_bottom = 50.0
rect_min_size = Vector2( 50, 0 )
theme = ExtResource( 1 )
items = [ "", null, false, 0, null, "←", null, false, 2, null, "→", null, false, 3, null, "↑", null, false, 4, null, "↓", null, false, 5, null ]
selected = 0
[node name="MoveLastButton" type="Button" parent="ActionHBox/ActionSquare3"]
-margin_top = 52.0
+margin_top = 54.0
margin_right = 50.0
-margin_bottom = 74.0
+margin_bottom = 77.0
theme = ExtResource( 1 )
[node name="Spacer3" type="Control" parent="ActionHBox"]
margin_left = 209.0
margin_right = 228.0
-margin_bottom = 74.0
+margin_bottom = 77.0
size_flags_horizontal = 3
[node name="ActionSquare4" type="VBoxContainer" parent="ActionHBox"]
margin_left = 232.0
margin_right = 282.0
-margin_bottom = 74.0
+margin_bottom = 77.0
script = SubResource( 3 )
[node name="MoveFirstButton" type="Button" parent="ActionHBox/ActionSquare4"]
margin_right = 50.0
-margin_bottom = 22.0
+margin_bottom = 23.0
theme = ExtResource( 1 )
text = ">>"
[node name="ActionOption" type="OptionButton" parent="ActionHBox/ActionSquare4"]
-margin_top = 26.0
+margin_top = 27.0
margin_right = 50.0
-margin_bottom = 48.0
+margin_bottom = 50.0
rect_min_size = Vector2( 50, 0 )
theme = ExtResource( 1 )
items = [ "", null, false, 0, null, "←", null, false, 2, null, "→", null, false, 3, null, "↑", null, false, 4, null, "↓", null, false, 5, null ]
selected = 0
[node name="MoveLastButton" type="Button" parent="ActionHBox/ActionSquare4"]
-margin_top = 52.0
+margin_top = 54.0
margin_right = 50.0
-margin_bottom = 74.0
+margin_bottom = 77.0
theme = ExtResource( 1 )
[node name="Spacer4" type="Control" parent="ActionHBox"]
margin_left = 286.0
margin_right = 306.0
-margin_bottom = 74.0
+margin_bottom = 77.0
size_flags_horizontal = 3
[node name="ActionSquare5" type="VBoxContainer" parent="ActionHBox"]
margin_left = 310.0
margin_right = 360.0
-margin_bottom = 74.0
+margin_bottom = 77.0
script = SubResource( 3 )
[node name="MoveFirstButton" type="Button" parent="ActionHBox/ActionSquare5"]
margin_right = 50.0
-margin_bottom = 22.0
+margin_bottom = 23.0
theme = ExtResource( 1 )
text = ">>"
[node name="ActionOption" type="OptionButton" parent="ActionHBox/ActionSquare5"]
-margin_top = 26.0
+margin_top = 27.0
margin_right = 50.0
-margin_bottom = 48.0
+margin_bottom = 50.0
rect_min_size = Vector2( 50, 0 )
theme = ExtResource( 1 )
items = [ "", null, false, 0, null, "←", null, false, 2, null, "→", null, false, 3, null, "↑", null, false, 4, null, "↓", null, false, 5, null ]
selected = 0
[node name="MoveLastButton" type="Button" parent="ActionHBox/ActionSquare5"]
-margin_top = 52.0
+margin_top = 54.0
margin_right = 50.0
-margin_bottom = 74.0
+margin_bottom = 77.0
theme = ExtResource( 1 )
[connection signal="pressed" from="ActionHBox/ActionSquare1/MoveFirstButton" to="ActionHBox/ActionSquare1" method="set_move_first" binds= [ true ]]
diff --git a/objects/PlayerListEntry.tscn b/objects/PlayerListEntry.tscn
new file mode 100644
index 0000000..2a280b1
--- /dev/null
+++ b/objects/PlayerListEntry.tscn
@@ -0,0 +1,42 @@
+[gd_scene load_steps=6 format=2]
+
+[ext_resource path="res://resources/fonts/Cochineal-Roman.otf" type="DynamicFontData" id=1]
+[ext_resource path="res://resources/fonts/Cochineal-Bold.otf" type="DynamicFontData" id=2]
+
+[sub_resource type="DynamicFont" id=1]
+size = 26
+font_data = ExtResource( 2 )
+
+[sub_resource type="DynamicFont" id=2]
+size = 26
+font_data = ExtResource( 1 )
+
+[sub_resource type="DynamicFont" id=3]
+size = 22
+font_data = ExtResource( 1 )
+
+[node name="PlayerListEntry" type="HBoxContainer"]
+
+[node name="ColorRect" type="ColorRect" parent="."]
+margin_right = 32.0
+margin_bottom = 32.0
+rect_min_size = Vector2( 32, 32 )
+
+[node name="RichTextLabel" type="RichTextLabel" parent="."]
+margin_left = 36.0
+margin_right = 236.0
+margin_bottom = 32.0
+rect_min_size = Vector2( 200, 30 )
+custom_fonts/bold_font = SubResource( 1 )
+custom_fonts/normal_font = SubResource( 2 )
+bbcode_enabled = true
+bbcode_text = "Waiting..."
+text = "Waiting..."
+
+[node name="KickButton" type="Button" parent="."]
+margin_left = 240.0
+margin_right = 292.0
+margin_bottom = 32.0
+custom_fonts/font = SubResource( 3 )
+disabled = true
+text = "Kick"
diff --git a/pages/GameTable.tscn b/pages/GameTable.tscn
index efd6540..dec2312 100644
--- a/pages/GameTable.tscn
+++ b/pages/GameTable.tscn
@@ -1,8 +1,9 @@
-[gd_scene load_steps=8 format=2]
+[gd_scene load_steps=9 format=2]
[ext_resource path="res://scripts/GameTable.gd" type="Script" id=1]
[ext_resource path="res://textures/wood_board_knotty.png" type="Texture" id=2]
[ext_resource path="res://scripts/Board.gd" type="Script" id=3]
+[ext_resource path="res://resources/MenuOptions.theme" type="Theme" id=4]
[sub_resource type="Environment" id=1]
background_mode = 1
@@ -76,7 +77,6 @@ transform = Transform( 1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 15.3
light_color = Color( 1, 0.941176, 0.733333, 1 )
light_energy = 1.512
light_specular = 0.0
-shadow_enabled = true
spot_range = 45.4139
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
@@ -94,3 +94,411 @@ transform = Transform( 1, 0, 0, 0, 0.965926, 0.258819, 0, -0.258819, 0.965926, 0
[node name="Camera" type="Camera" parent="CameraHingeYaw/CameraHingePitch"]
transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 7 )
script = SubResource( 4 )
+
+[node name="PregameUI" type="VBoxContainer" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 2
+
+[node name="Header" type="HBoxContainer" parent="PregameUI"]
+margin_right = 1600.0
+margin_bottom = 39.0
+mouse_filter = 2
+size_flags_horizontal = 3
+
+[node name="GameMenuButton" type="Button" parent="PregameUI/Header"]
+margin_right = 30.0
+margin_bottom = 39.0
+theme = ExtResource( 4 )
+text = "="
+
+[node name="TitleText" type="Label" parent="PregameUI/Header"]
+margin_left = 34.0
+margin_top = 3.0
+margin_right = 387.0
+margin_bottom = 36.0
+mouse_filter = 0
+mouse_default_cursor_shape = 2
+theme = ExtResource( 4 )
+text = "$LOBBY_NAME ($JCODE)"
+
+[node name="PregameControls" type="HBoxContainer" parent="PregameUI"]
+margin_top = 43.0
+margin_right = 1600.0
+margin_bottom = 443.0
+mouse_filter = 2
+
+[node name="PlayerTab" type="VBoxContainer" parent="PregameUI/PregameControls"]
+margin_right = 186.0
+margin_bottom = 400.0
+
+[node name="PlayersLabel" type="Label" parent="PregameUI/PregameControls/PlayerTab"]
+margin_right = 186.0
+margin_bottom = 14.0
+text = "Players ($CONNECTED/$MAX)"
+
+[node name="HSeparator" type="HSeparator" parent="PregameUI/PregameControls/PlayerTab"]
+margin_top = 18.0
+margin_right = 186.0
+margin_bottom = 22.0
+
+[node name="PlayerList" type="VBoxContainer" parent="PregameUI/PregameControls/PlayerTab"]
+margin_top = 26.0
+margin_right = 186.0
+margin_bottom = 26.0
+
+[node name="StartButton" type="Button" parent="PregameUI/PregameControls/PlayerTab"]
+margin_top = 30.0
+margin_right = 186.0
+margin_bottom = 69.0
+theme = ExtResource( 4 )
+text = "Start "
+
+[node name="ControlsTabContainer" type="TabContainer" parent="PregameUI/PregameControls"]
+margin_left = 190.0
+margin_right = 460.0
+margin_bottom = 400.0
+rect_min_size = Vector2( 250, 400 )
+
+[node name="Board Params" type="GridContainer" parent="PregameUI/PregameControls/ControlsTabContainer"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 4.0
+margin_top = 32.0
+margin_right = -4.0
+margin_bottom = -4.0
+columns = 2
+
+[node name="BoardSideLenLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_top = 5.0
+margin_right = 112.0
+margin_bottom = 19.0
+text = "Board Side Lenth:"
+align = 2
+
+[node name="board_side_length" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_left = 116.0
+margin_right = 262.0
+margin_bottom = 24.0
+min_value = 4.0
+max_value = 10.0
+value = 6.0
+
+[node name="AirportStyleLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_top = 31.0
+margin_right = 112.0
+margin_bottom = 45.0
+text = "Airport Style:"
+align = 2
+
+[node name="airport_style" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_left = 116.0
+margin_top = 28.0
+margin_right = 262.0
+margin_bottom = 48.0
+text = "Colors & Numbers"
+items = [ "Colors & Numbers", null, false, 0, null, "Names", null, false, 1, null ]
+selected = 0
+
+[node name="NumAirportsLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_top = 57.0
+margin_right = 112.0
+margin_bottom = 71.0
+text = "# Airports:"
+align = 2
+
+[node name="num_airports" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_left = 116.0
+margin_top = 52.0
+margin_right = 262.0
+margin_bottom = 76.0
+min_value = 4.0
+max_value = 32.0
+value = 18.0
+
+[node name="NumHillsLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_top = 85.0
+margin_right = 112.0
+margin_bottom = 99.0
+text = "# Hills:"
+align = 2
+
+[node name="num_hills" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_left = 116.0
+margin_top = 80.0
+margin_right = 262.0
+margin_bottom = 104.0
+max_value = 15.0
+value = 8.0
+
+[node name="NumMtnsLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_top = 113.0
+margin_right = 112.0
+margin_bottom = 127.0
+text = "# Mountains:"
+align = 2
+
+[node name="num_mountains" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_left = 116.0
+margin_top = 108.0
+margin_right = 262.0
+margin_bottom = 132.0
+max_value = 15.0
+value = 4.0
+
+[node name="RunwayCountLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_top = 139.0
+margin_right = 112.0
+margin_bottom = 153.0
+text = "Runway Count:"
+align = 2
+
+[node name="runways_per_airport" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_left = 116.0
+margin_top = 136.0
+margin_right = 262.0
+margin_bottom = 156.0
+text = "Random"
+items = [ "Random", null, false, 0, null, "1", null, false, 1, null, "2", null, false, 2, null, "3", null, false, 3, null ]
+selected = 0
+
+[node name="GenerateBoardButton" type="Button" parent="PregameUI/PregameControls/ControlsTabContainer/Board Params"]
+margin_top = 160.0
+margin_right = 112.0
+margin_bottom = 180.0
+text = "Generate Board"
+
+[node name="Game Rules" type="GridContainer" parent="PregameUI/PregameControls/ControlsTabContainer"]
+visible = false
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 4.0
+margin_top = 32.0
+margin_right = -4.0
+margin_bottom = -4.0
+columns = 2
+
+[node name="FlyOverLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 3.0
+margin_right = 166.0
+margin_bottom = 17.0
+text = "Fly Over Airports:"
+align = 2
+
+[node name="fly_over_airports" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_right = 355.0
+margin_bottom = 20.0
+text = "No"
+items = [ "No", null, false, 0, null, "Altitude 2 Only", null, false, 1, null, "All Altitudes", null, false, 2, null ]
+selected = 0
+
+[node name="MustLandLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 27.0
+margin_right = 166.0
+margin_bottom = 41.0
+text = "Must Land Final Action:"
+align = 2
+
+[node name="must_land_on_final_action" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 24.0
+margin_right = 355.0
+margin_bottom = 44.0
+text = "No"
+items = [ "No", null, false, 0, null, "Yes", null, false, 1, null ]
+selected = 0
+
+[node name="TakeoffActionLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 51.0
+margin_right = 166.0
+margin_bottom = 65.0
+text = "Takeoff Action:"
+align = 2
+
+[node name="takeoff_action" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 48.0
+margin_right = 355.0
+margin_bottom = 68.0
+text = "3"
+items = [ "1", null, false, 0, null, "2", null, false, 1, null, "3", null, false, 2, null, "4", null, false, 3, null, "5", null, false, 4, null ]
+selected = 2
+
+[node name="MoveForwardLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 75.0
+margin_right = 166.0
+margin_bottom = 89.0
+text = "Move Forward:"
+align = 2
+
+[node name="move_forward_order" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 72.0
+margin_right = 355.0
+margin_bottom = 92.0
+text = "Either"
+items = [ "Either", null, false, 0, null, "After Action", null, false, 1, null, "Before Action", null, false, 2, null ]
+selected = 0
+
+[node name="GamemodeLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 99.0
+margin_right = 166.0
+margin_bottom = 113.0
+text = "Gamemode:"
+align = 2
+
+[node name="gamemode" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 96.0
+margin_right = 355.0
+margin_bottom = 116.0
+text = "Cooperative"
+items = [ "Cooperative", null, false, 0, null, "Versus", null, false, 1, null ]
+selected = 0
+
+[node name="PlaneAssignmentLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 123.0
+margin_right = 166.0
+margin_bottom = 137.0
+text = "Plane Assignment:"
+align = 2
+
+[node name="plane_assignment" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 120.0
+margin_right = 355.0
+margin_bottom = 140.0
+text = "Random"
+items = [ "Random", null, false, 0, null, "Draft", null, false, 1, null ]
+selected = 0
+
+[node name="AltitudeChangeLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 147.0
+margin_right = 166.0
+margin_bottom = 161.0
+text = "Altitude Change:"
+align = 2
+
+[node name="altitude_and_turn" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 144.0
+margin_right = 355.0
+margin_bottom = 164.0
+text = "Turn OR Altitude Change"
+items = [ "Turn OR Altitude Change", null, false, 0, null, "Turn AND Altitude Change", null, false, 1, null ]
+selected = 0
+
+[node name="TurnsPerEventLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 173.0
+margin_right = 166.0
+margin_bottom = 187.0
+text = "Turns Per Event:"
+align = 2
+
+[node name="event_frequency" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 168.0
+margin_right = 355.0
+margin_bottom = 192.0
+min_value = 1.0
+max_value = 4.0
+value = 1.0
+
+[node name="WeatherEnabledLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 199.0
+margin_right = 166.0
+margin_bottom = 213.0
+text = "Weather:"
+align = 2
+
+[node name="weather_enabled" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 196.0
+margin_right = 355.0
+margin_bottom = 216.0
+text = "Enabled"
+items = [ "Disabled", null, false, 0, null, "Enabled", null, false, 1, null ]
+selected = 1
+
+[node name="OtherEventsLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 223.0
+margin_right = 166.0
+margin_bottom = 237.0
+text = "Other Events:"
+align = 2
+
+[node name="misc_enabled" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 220.0
+margin_right = 355.0
+margin_bottom = 240.0
+text = "Enabled"
+items = [ "Disabled", null, false, 0, null, "Enabled", null, false, 1, null ]
+selected = 1
+
+[node name="NewPlanesLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 249.0
+margin_right = 166.0
+margin_bottom = 263.0
+text = "New Planes Per Turn:"
+align = 2
+
+[node name="new_planes_per_turn" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 244.0
+margin_right = 355.0
+margin_bottom = 268.0
+min_value = 1.0
+max_value = 7.0
+value = 4.0
+
+[node name="NewPlaneRampupLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 275.0
+margin_right = 166.0
+margin_bottom = 289.0
+text = "New Plane Ramp Up:"
+align = 2
+
+[node name="ramp_up_enabled" type="OptionButton" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 272.0
+margin_right = 355.0
+margin_bottom = 292.0
+text = "Disabled"
+items = [ "Disabled", null, false, 0, null, "Enabled", null, false, 1, null ]
+selected = 0
+
+[node name="StartingPlanesLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 301.0
+margin_right = 166.0
+margin_bottom = 315.0
+text = "Starting Planes Per Player:"
+align = 2
+
+[node name="starting_planes_per_player" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 296.0
+margin_right = 355.0
+margin_bottom = 320.0
+min_value = 1.0
+max_value = 10.0
+value = 3.0
+
+[node name="RoundTimerLabel" type="Label" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_top = 329.0
+margin_right = 166.0
+margin_bottom = 343.0
+text = "Round Timer:"
+align = 2
+
+[node name="round_timer" type="SpinBox" parent="PregameUI/PregameControls/ControlsTabContainer/Game Rules"]
+margin_left = 170.0
+margin_top = 324.0
+margin_right = 355.0
+margin_bottom = 348.0
+min_value = 15.0
+max_value = 300.0
+value = 15.0
+
+[connection signal="gui_input" from="PregameUI/Header/TitleText" to="." method="_on_TitleText_gui_input"]
diff --git a/pages/MainMenu.tscn b/pages/MainMenu.tscn
index d216836..5d1f21c 100644
--- a/pages/MainMenu.tscn
+++ b/pages/MainMenu.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=17 format=2]
+[gd_scene load_steps=15 format=2]
[ext_resource path="res://scripts/MainMenu.gd" type="Script" id=1]
[ext_resource path="res://resources/fonts/Cochineal-Bold.otf" type="DynamicFontData" id=2]
@@ -25,15 +25,6 @@ font_data = ExtResource( 2 )
default_font = SubResource( 1 )
Button/fonts/font = SubResource( 30 )
-[sub_resource type="Gradient" id=15]
-offsets = PoolRealArray( 0 )
-colors = PoolColorArray( 0, 0, 0, 1 )
-
-[sub_resource type="GradientTexture2D" id=22]
-gradient = SubResource( 15 )
-width = 32
-height = 32
-
[sub_resource type="Gradient" id=23]
offsets = PoolRealArray( 0 )
colors = PoolColorArray( 0, 0, 0, 1 )
@@ -145,7 +136,7 @@ margin_right = 202.0
margin_bottom = 87.0
rect_min_size = Vector2( 60, 40 )
size_flags_horizontal = 0
-icon = SubResource( 22 )
+icon = SubResource( 21 )
expand_icon = true
items = [ "", SubResource( 21 ), false, 0, null, "", SubResource( 21 ), false, 1, null, "", SubResource( 21 ), false, 2, null, "", SubResource( 21 ), false, 3, null, "", SubResource( 21 ), false, 4, null, "", SubResource( 21 ), false, 5, null, "", SubResource( 21 ), false, 6, null, "", SubResource( 21 ), false, 7, null, "", SubResource( 21 ), false, 8, null, "", SubResource( 21 ), false, 9, null ]
selected = 0
@@ -297,3 +288,43 @@ theme = ExtResource( 4 )
max_length = 39
placeholder_text = "Game Coordinator URL"
placeholder_alpha = 0.389
+
+[node name="NotifPopup" type="PopupPanel" parent="."]
+anchor_left = 0.4
+anchor_top = 0.35
+anchor_right = 0.6
+anchor_bottom = 0.65
+margin_right = 8.0
+margin_bottom = 8.0
+popup_exclusive = true
+
+[node name="PopupControl" type="Control" parent="NotifPopup"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 4.0
+margin_top = 4.0
+margin_right = -4.0
+margin_bottom = -4.0
+
+[node name="TitleLabel" type="Label" parent="NotifPopup/PopupControl"]
+anchor_left = 0.1
+anchor_right = 0.9
+anchor_bottom = 0.15
+theme = ExtResource( 4 )
+
+[node name="MessageLabel" type="Label" parent="NotifPopup/PopupControl"]
+anchor_left = 0.05
+anchor_top = 0.25
+anchor_right = 0.95
+anchor_bottom = 0.75
+
+[node name="ClosePopupButton" type="Button" parent="NotifPopup/PopupControl"]
+anchor_left = 0.3
+anchor_top = 0.8
+anchor_right = 0.3
+anchor_bottom = 0.8
+rect_min_size = Vector2( 120, 50 )
+theme = ExtResource( 4 )
+text = "Close"
+
+[connection signal="pressed" from="NotifPopup/PopupControl/ClosePopupButton" to="NotifPopup" method="set_visible" binds= [ false ]]
diff --git a/pages/MainScene.tscn b/pages/MainScene.tscn
index da9bdcd..ce04e54 100644
--- a/pages/MainScene.tscn
+++ b/pages/MainScene.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=4 format=2]
-[ext_resource path="res://network/websocket_client_basic.gd" type="Script" id=1]
+[ext_resource path="res://network/GCClient.tscn" type="PackedScene" id=1]
[ext_resource path="res://pages/MainMenu.tscn" type="PackedScene" id=2]
[ext_resource path="res://scripts/MainScene.gd" type="Script" id=3]
@@ -9,7 +9,6 @@ anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 3 )
-[node name="GameCoordinator" type="Node" parent="."]
-script = ExtResource( 1 )
-
[node name="MainMenu" parent="." instance=ExtResource( 2 )]
+
+[node name="GCClient" parent="." instance=ExtResource( 1 )]
diff --git a/pages/ServerBrowser.tscn b/pages/ServerBrowser.tscn
index c5d2ef4..5c1c4bf 100644
--- a/pages/ServerBrowser.tscn
+++ b/pages/ServerBrowser.tscn
@@ -96,6 +96,7 @@ margin_left = 383.0
margin_top = 129.0
margin_right = 633.0
margin_bottom = 399.0
+popup_exclusive = true
[node name="Control" type="Control" parent="HostPopup"]
margin_left = 4.0
diff --git a/project.godot b/project.godot
index e90fb8a..d5bbade 100644
--- a/project.godot
+++ b/project.godot
@@ -8,6 +8,22 @@
config_version=4
+_global_script_classes=[ {
+"base": "Area",
+"class": "Airplane",
+"language": "GDScript",
+"path": "res://scripts/Airplane.gd"
+}, {
+"base": "Node",
+"class": "GCClient",
+"language": "GDScript",
+"path": "res://network/GameCoordinatorClient.gd"
+} ]
+_global_script_class_icons={
+"Airplane": "",
+"GCClient": ""
+}
+
[application]
config/name="ATC"
@@ -15,6 +31,10 @@ run/main_scene="res://pages/MainScene.tscn"
boot_splash/show_image=false
config/icon="res://icon.png"
+[audio]
+
+driver="Dummy"
+
[autoload]
Globals="*res://scripts/Globals.gd"
diff --git a/resources/external/game_coordinator.py b/resources/external/game_coordinator.py
index 0f04e18..8daae7f 100644
--- a/resources/external/game_coordinator.py
+++ b/resources/external/game_coordinator.py
@@ -104,6 +104,7 @@ async def remove_player(lobby, player_id):
del LOBBIES[lobby["lobby_id"]]
elif was_host: # host must be transferred
+ print("changing host")
new_host_id = None
for player_id in lobby["players"]:
@@ -128,7 +129,7 @@ async def disconnect_player(lobby, player_id, reason='disconnect'):
dc_player = lobby["players"][player_id]
dc_player["connection_status"] = 0
if "dead" not in dc_player:
- response = {"type" : "disconnected", "message" : f"Disconnected. Reason: {reason}"}
+ response = {"type" : "disconnected", "message" : f"Disconnected. Reason: {reason}", "reason" : reason}
try:
await dc_player["socket"].send(json.dumps(response))
except:
@@ -147,6 +148,9 @@ async def disconnect_player(lobby, player_id, reason='disconnect'):
async def connection_lost(lobby, player_id):
if player_id in lobby["players"]:
lobby["players"][player_id]["connection_status"] = 0
+ current_players = await get_player_info(lobby)
+ announcement = {"type" : "announcement_player_connerr", "player_id" : player_id, "current_players" : current_players }
+ await broadcast_active(lobby, announcement)
await asyncio.sleep( DISCONNECT_GRACE_PERIOD if not lobby["paused"] else PAUSED_GRACE_PERIOD )
if player_id in lobby["players"] and lobby["players"][player_id]["connection_status"] == 0:
lobby["players"][player_id]["dead"] = True
@@ -188,6 +192,8 @@ async def lobby_loop(websocket, lobby, player):
lobby_id = lobby["lobby_id"]
async for message in websocket:
event = json.loads(message)
+ lobby = LOBBIES[lobby_id]
+ player = lobby["players"][player_id]
# handle messages sent from connected players yet unconfirmed by host
if "waiting" in player:
@@ -197,7 +203,10 @@ async def lobby_loop(websocket, lobby, player):
if player["waiting_retries"] > MAX_JOIN_REQUESTS:
await end_lobby(lobby_id) # does this need to be awaited?
return True
- await send_join_request_to_host(lobby, player_id)
+ try:
+ await send_join_request_to_host(lobby, player_id)
+ except Exception as e:
+ print(e)
continue # deny any other requests
@@ -218,33 +227,38 @@ async def lobby_loop(websocket, lobby, player):
continue
if not player["is_host"]:
- response = {"type" : "gc_error", "message" : f"GC ERROR: Only host has access to command {event['command']}."}
+ response = {"type" : "gc_error", "message" : f"Only host has access to command {event['command']}."}
await websocket.send(json.dumps(response))
continue # following commands only available to game host
if event["command"] == "accept_player":
if ("player_id" not in event) or (event["player_id"] not in lobby["players"]):
- response = {"type" : "gc_error", "message" : "GC ERROR: No player ID given, or given player ID is not in the lobby."}
+ response = {"type" : "gc_error", "message" : "No player ID given, or given player ID is not in the lobby."}
await websocket.send(json.dumps(response))
continue
-
+
accepted_id = event["player_id"]
if "waiting" not in lobby["players"][accepted_id]:
- response = {"type" : "gc_error", "message" : "GC ERROR: Player already accepted."}
+
+ response = {"type" : "gc_error", "message" : "Player already accepted."}
await websocket.send(json.dumps(response))
continue
+
accepted_player = lobby["players"][accepted_id]
del accepted_player["waiting"]
del accepted_player["waiting_args"]
del accepted_player["waiting_since"]
del accepted_player["waiting_retries"]
- notification = { "type" : "join_ok" }
-
+
+ current_players = await get_player_info(lobby)
+ notification = { "type" : "join_ok", "current_players" : current_players}
+ if "args" in event: notification["args"] = event["args"]
+
await accepted_player["socket"].send(json.dumps(notification)) # notify waiting player of acceptance
elif event["command"] == "deny_player":
if ("player_id" not in event) or (event["player_id"] not in lobby["players"]):
- response = {"type" : "gc_error", "message" : "GC ERROR: No player ID given, or given player ID is not in the lobby."}
+ response = {"type" : "gc_error", "message" : "No player ID given, or given player ID is not in the lobby."}
await websocket.send(json.dumps(response))
continue
denied_id = event["player_id"]
@@ -253,7 +267,7 @@ async def lobby_loop(websocket, lobby, player):
await disconnect_player(lobby, denied_id, "denied"+reason_text)
elif event["command"] == "kick_player":
if ("player_id" not in event) or (event["player_id"] not in lobby["players"]):
- response = {"type" : "gc_error", "message" : "GC ERROR: No player ID given, or given player ID is not in the lobby."}
+ response = {"type" : "gc_error", "message" : "No player ID given, or given player ID is not in the lobby."}
await websocket.send(json.dumps(response))
continue
kicked_id = event["player_id"]
@@ -286,12 +300,13 @@ async def lobby_loop(websocket, lobby, player):
response = {"type" : "request", "destination" : player_id, "request_fields" : event["request_fields"] }
await game["players"][ event["source"] ]["socket"].send( json.dumps(response) )
elif event["type"] == "request_response":
- response = {"type" : "request_response", "data" : event["requested_data"] }
+ response = {"type" : "request_response", "source" : player_id ,"requested_data" : event["requested_data"] }
await game["players"][ event["destination"] ]["socket"].send( json.dumps(response) )
elif event["type"] == "broadcast":
- message = {"type" : "broadcast", "payload" : event["payload"]}
- for player in game["players"]:
- if player["connection_status"] == 1 and ( (player != player_id) or ("to_self" in event) ): await player["socket"].send(json.dumps(message))
+ message = {"type" : "broadcast", "command" : event["command"], "payload" : event["payload"]}
+ for _player_id in lobby["players"]:
+ player = lobby["players"][_player_id]
+ if player["connection_status"] == 1 and ( (_player_id != player_id) or ("to_self" in event) ): await player["socket"].send(json.dumps(message))
if lobby_id not in LOBBIES: return True # lobby closed
if player_id not in LOBBIES[lobby_id]["players"] or ("dead" in LOBBIES[lobby_id]["players"][player_id]): return True # player removed
@@ -301,39 +316,39 @@ async def lobby_loop(websocket, lobby, player):
async def join_lobby(websocket, event):
for required_field in ["lobby_id", "game_type", "username"]:
if required_field not in event:
- response = {"type" : "gc_error", "message" : f"GC ERROR: Missing required field: '{required_field}'."}
+ response = {"type" : "join_fail", "message" : f"Missing required field: '{required_field}'."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
lobby_id = event["lobby_id"]
if lobby_id not in LOBBIES:
- response = {"type" : "gc_error", "message" : f"GC ERROR: Lobby '{lobby_id}' does not exist."}
+ response = {"type" : "join_fail", "message" : f"Lobby '{lobby_id}' does not exist."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
lobby = LOBBIES[lobby_id]
if str(event["game_type"]) != lobby["game_type"]:
- response = {"type" : "gc_error", "message" : f"GC ERROR: Mismatched game types. Expected {LOBBIES[lobby_id]['game_type']}, received {event['game_type']}."}
+ response = {"type" : "join_fail", "message" : f"Mismatched game types. Expected {LOBBIES[lobby_id]['game_type']}, received {event['game_type']}."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
if len(lobby["players"]) >= lobby["max_players"]:
- response = {"type" : "gc_error", "message" : "GC ERROR: Lobby full."}
+ response = {"type" : "join_fail", "message" : "Lobby full."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
if lobby["locked"] and ("rejoin_key" not in event):
- response = {"type" : "gc_error", "message" : "GC ERROR: Lobby is locked, no new connections are being accepted."}
+ response = {"type" : "join_fail", "message" : "Lobby is locked, no new connections are being accepted."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
if lobby["private"] and (event["password"] != lobby["password"]) and ("rejoin_key" not in event):
- response = {"type" : "gc_error", "message" : "GC ERROR: Incorrect password for private lobby."}
+ response = {"type" : "join_fail", "message" : "Incorrect password for private lobby."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
@@ -365,7 +380,7 @@ async def join_lobby(websocket, event):
return # end of socket
# Could not rejoin
- response = {"type" : "gc_error", "message" : "GC ERROR: Could not rejoin with given key."}
+ response = {"type" : "rejoin_fail", "message" : "Could not rejoin with given key."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # end of socket
@@ -379,7 +394,7 @@ async def join_lobby(websocket, event):
player_info = await get_player_info(lobby)
notification = {"type" : "announcement_player_joining", "current_players" : player_info}
await broadcast_active(lobby, notification)
- response = { "type" : "join_request_ok", "player_id" : new_player["player_id"], "rejoin_key" : new_player["rejoin_key"] }
+ response = { "type" : "join_request_ok", "player_id" : new_player["player_id"], "rejoin_key" : new_player["rejoin_key"], "current_players" : player_info, "max_players" : lobby["max_players"], "lobby_name" : lobby["lobby_name"], "private" : lobby["private"]}
await websocket.send(json.dumps(response))
@@ -398,14 +413,14 @@ async def join_lobby(websocket, event):
async def host_lobby(websocket, event):
for reqd_i, required_field in enumerate(HOST_LOBBY_REQD_FIELDS):
if required_field not in event:
- response = {"type" : "gc_error", "message" : f"GC ERROR: Missing required field: '{required_field}'."}
+ response = {"type" : "host_fail", "message" : f"Missing required field: '{required_field}'."}
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
try:
- test_argument_type = HOST_LOBBY_REQD_FIELD_TYPES[reqd_i](event[required_field])
+ HOST_LOBBY_REQD_FIELD_TYPES[reqd_i](event[required_field])
except:
- response = { "type" : "gc_error", "message" : f"GC_ERROR: Invalid type for field '{required_field}'." }
+ response = { "type" : "host_fail", "message" : f"Invalid type for field '{required_field}'." }
await websocket.send(json.dumps(response))
await asyncio.sleep(SOCKET_GRACE_PERIOD)
return # close connection
diff --git a/resources/unused_code.gd b/resources/unused_code.gd
index 43570b5..9f8a9e8 100644
--- a/resources/unused_code.gd
+++ b/resources/unused_code.gd
@@ -15,3 +15,19 @@ func get_random_airport_name(exceptions=[]):
while airport_names[name_index] in exceptions:
name_index = randi() % len(airport_names)
return airport_names[name_index]
+
+func on_player_join_request():
+ var taken_colors : Array = []
+ for player_id in PLAYER_INFO.keys():
+ taken_colors.push_back( PLAYER_INFO[player_id]["color_id"] )
+ var available_colors : Array = []
+ for color in range(len(Globals.colors)):
+ if !(color in taken_colors): available_colors.push_back(color)
+ var response_args = { "color_id" : available_colors.pick_random() } # pick random color by default
+
+ if ("color_pref_1" in args) and (args["color_pref_1"] in available_colors):
+ response_args["color_id"] = args["color_pref_1"]
+ elif ("color_pref_2" in args) and (args["color_pref_2"] in available_colors):
+ response_args["color_id"] = args["color_pref_2"]
+
+ response["args"] = response_args
diff --git a/scripts/Plane.gd b/scripts/Airplane.gd
index 6889e31..b13ed33 100644
--- a/scripts/Plane.gd
+++ b/scripts/Airplane.gd
@@ -1,3 +1,4 @@
+class_name Airplane
extends Area
var starting_altitude: int = 0 # initial altitude for the turn, determines number of actions
@@ -16,6 +17,8 @@ var destination_col: Color # for display purposes only
var destination_name: String # for display purposes
var destination_id: int # determines above ^
+var owner_id : String
+
var rotation_tween: Tween = null
var actions: Array = []
diff --git a/scripts/Board.gd b/scripts/Board.gd
index f18ca74..04f2d90 100644
--- a/scripts/Board.gd
+++ b/scripts/Board.gd
@@ -17,6 +17,8 @@ var airports = {} # id : HexSpace of cell_type airport
var side_len: int
+const BOARD_GEN_ATTEMPTS : int = 5
+
# Y R B G
var airport_colors = [ Color(1, 1, 0), Color(1, 0, 0), Color(0.3, 0.3, 1), Color(0, 0.8, 0) ]
@@ -31,7 +33,7 @@ enum { PLAIN, HILLS, MOUNTAINS, AIRPORT }
const adjacent_offsets = [ [0,1] , [-1, 0], [-1, -1], [0, -1], [1, 0], [1, 1] ]
# indices of the offsets that are valid cells to approach from
-const approaches_i: Array= [ [0, 1, 2, 3, 4, 5], [0,1,3,4], [0,3] ]
+const approaches_i: Array= [ [], [0,3], [0,1,3,4], [0, 1, 2, 3, 4, 5] ]
func _ready():
@@ -87,8 +89,8 @@ func display_board():
var new_cell = hex_space.instance()
new_cell.call_deferred("set", "global_position", Vector3(x, randf()/15, z))
- new_cell.set_up(row[c])
add_child(new_cell)
+ new_cell.set_up(row[c])
row_display[c] = new_cell
board_display.push_back(row_display)
@@ -126,6 +128,7 @@ func populate_board(num_mountains : int, num_hills : int, num_airports : int, ru
airport_display = [ randi() % 9 + 1, randi() % 4 ] # number, color
while airport_display in used_airports:
airport_display = [ randi() % 9 + 1, randi() % 4 ]
+ used_airports.push_back(airport_display)
# find valid spot
var spot_okay:bool = false
var rot:int
@@ -189,25 +192,36 @@ func populate_board(num_mountains : int, num_hills : int, num_airports : int, ru
if not spot_okay:
return false # could not form valid map
- var args = {"cell_type" : AIRPORT , "pos" : [spot_r, spot_c], "orientation" : rot, "airport_id" : airport_id, "runways" : runways, 'valid_approach_offsets' : valid_approaches, "use_names" : use_names}
+ var args = {"cell_type" : AIRPORT , "pos" : [spot_r, spot_c], "orientation" : rot, "airport_id" : airport_id, "runway_count" : runways, 'valid_approach_offsets' : valid_approaches, "use_names" : use_names}
if use_names:
args["airport_name"] = airport_display
else:
- args["airport_color"] = airport_colors[airport_display[COLOR]]
+ args["airport_color"] = airport_colors[airport_display[COLOR]].to_html()
args["airport_number"] = airport_display[NUMBER]
board[spot_r][spot_c] = args
available_board_coords.pop_at(spot_i)
airport_id += 1
return true
-func get_json():
- return board
+func get_board():
+ return self.board
+
+func set_board(board_array):
+ reset_board()
+ self.board = board_array
+ display_board()
+
func generate_board(hex_side_len: int, num_mountains : int, num_hills : int, num_airports : int, runway_count : int, use_names : bool = false) -> bool:
create_board_base(hex_side_len)
- if not populate_board(num_mountains, num_hills, num_airports, runway_count, use_names):
+ var attempts : int = 1
+ while attempts < BOARD_GEN_ATTEMPTS and (not populate_board(num_mountains, num_hills, num_airports, runway_count, use_names)):
+ attempts += 1
+ reset_board()
+
+ if attempts >= BOARD_GEN_ATTEMPTS:
reset_board()
- print("Invalid board creation parameters")
+ display_board()
return false
display_board()
return true
diff --git a/scripts/GameTable.gd b/scripts/GameTable.gd
index b0bedf2..f21d027 100644
--- a/scripts/GameTable.gd
+++ b/scripts/GameTable.gd
@@ -2,7 +2,7 @@ extends Spatial
# MULTIPLAYER DATA
-var gc_client # to be assigned by MainScene upon game creation/joining
+var GC : GCClient # Game Coordinator connection/client, to be assigned by MainScene upon game creation/joining
@@ -33,7 +33,7 @@ var RULES = {
"misc_enabled" : 1,
"new_planes_per_turn" : 4, # 1 - 8
"ramp_up_enabled" : 0, # whether to ramp up to max planes per turn
- "starting_planes_per_player": 4,
+ "starting_planes_per_player": 3,
"round_timer" : 60, # seconds
}
@@ -43,26 +43,163 @@ var PLANES = []
var desired_player_count: int
var is_board_generated: bool = false # determine whether host can begin the game
+var PLAYER_INFO = {} #
+
# END GAME DATA
# directions: E, NE, NW, W, SW, SE
const adjacent_offsets = [ [0,1] , [-1, 0], [-1, -1], [0, -1], [1, 0], [1, 1] ]
+func _ready():
+ pass
+func _process(_delta):
+ var msg = GC.receive()
+ if msg: handle_gc_message(msg)
-func _ready():
+func set_up(gc_client):
+ GC = gc_client
+ GC.connect("join_request_received", self, "on_join_request_received")
+ GC.connect("disconnected_from_lobby", self, "on_disconnect")
+ GC.connect("connection_closed_dirty", self, "on_connection_closed_dirty")
+
+ var titletext = $PregameUI/Header/TitleText.text.replace("$JCODE", GC.lobby_id)
+ titletext = titletext.replace("$LOBBY_NAME", GC.lobby_name)
+ if OS.get_name() == "HTML5": # running on web
+ titletext += " (click to copy)"
+ $PregameUI/Header/TitleText.text = titletext
+
+ # set up pregame controls
+ # TODO : function where editability/enabled is changed on game host change
+ var i = 0
+ for control_group in ["Board Params", "Game Rules"]:
+ var path_root : String = "PregameUI/PregameControls/ControlsTabContainer/"+control_group+"/"
+ var dict = BOARD_GEN_PARAMS if i == 0 else RULES
+ for key in dict.keys():
+ var setting_node = get_node(path_root + key)
+ if setting_node is OptionButton:
+ setting_node.connect("item_selected", self, "on_setting_update", [control_group, key])
+ if not GC.is_host: setting_node.disabled = true
+ elif setting_node is SpinBox:
+ setting_node.connect("value_changed", self, "on_setting_update", [control_group, key])
+ if not GC.is_host: setting_node.editable = false
+ i += 1
+
+ $PregameUI/PregameControls/PlayerTab/StartButton.disabled = not GC.is_host
+ var gen_btn : Button = get_node("PregameUI/PregameControls/ControlsTabContainer/Board Params/GenerateBoardButton")
+ gen_btn.disabled = not GC.is_host
+ gen_btn.connect("pressed", self, "generate_board_ui")
+
+func set_up_args(args):
+ if !(GC.player_id in PLAYER_INFO): PLAYER_INFO[GC.player_id] = {}
+ PLAYER_INFO[GC.player_id]["color_id"] = args["color_id"]
+
+func on_join_request_received(player_id, args):
+ if !GC.is_host: return # just in case
+ var response = { "type" : "lobby_control", "command": "accept_player", "player_id" : player_id }
+
+ GC.send_json(response)
+
+# new_value filled by UI element signal, control_group and setting_name from custom binds
+func on_setting_update(new_value, control_group, setting_name):
+ if !GC.is_host: return # prevent non-hosts from broadcasting updates when setting the value because of https://github.com/godotengine/godot/issues/70821
+ var dict = BOARD_GEN_PARAMS if control_group == "Board Params" else RULES
+ dict[setting_name] = new_value
+
+ var command = "set_board_gen_params" if control_group == "Board Params" else "set_game_rules"
+ var message = {"type" : "broadcast", "command" : command, "payload" : { setting_name : new_value } }
+ GC.send_json(message)
+
+# fields being dictionary of form { field : value }
+func set_setting_remote(control_group, fields):
+ var dict = BOARD_GEN_PARAMS if control_group == "Board Params" else RULES
+ var path_root : String = "PregameUI/PregameControls/ControlsTabContainer/"+control_group+"/"
+ for field in fields:
+ var new_value = fields[field]
+ dict[field] = new_value
+ var setting_node = get_node(path_root + field)
+ if setting_node is SpinBox:
+ setting_node.set_value(new_value)
+ elif setting_node is OptionButton:
+ setting_node.select(new_value) # does not emit signal
+
+func generate_board_ui():
+ is_board_generated = BOARD.generate_board(BOARD_GEN_PARAMS["board_side_length"],
+ BOARD_GEN_PARAMS["num_mountains"],
+ BOARD_GEN_PARAMS["num_hills"],
+ BOARD_GEN_PARAMS["num_airports"],
+ BOARD_GEN_PARAMS["runways_per_airport"],
+ BOARD_GEN_PARAMS["airport_style"]
+ )
+ var message = {"type" : "broadcast", "command" : "set_board", "payload" : BOARD.board }
+ GC.send_json(message)
+
+func on_disconnect(reason):
pass
-func set_up(ws_client, is_host: bool, lobby_id: String, player_id: String, rejoin_key: String):
- self.ws_client = ws_client
- self.is_host = is_host
- self.lobby_id = lobby_id
- self.player_id = player_id
- self.rejoin_key = rejoin_key
+func on_connection_closed_dirty():
+ get_parent()
+
+# messages have been pre-processed by game coordinator client to ensure
+# valid JSON with "type" field
+func handle_gc_message(message):
+ if "current_players" in message: update_player_info() # add new players to PLAYER_INFO, remove disconnected players
+ if message["type"] == "request":
+ #var data = get_data()
+ #var response = { "type" : "request_response", "requested_data" : data, "destination" : event["source"] }
+ #GC.send_json
+ pass
+ elif message["type"] == "request_response":
+ #set_data()
+ pass
+ elif message["type"] == "broadcast" and "command" in message:
+ var cmd : String = message["command"]
+ if cmd == "set_game_rules":
+ set_setting_remote("Game Rules", message["payload"])
+ elif cmd == "set_board_gen_params":
+ set_setting_remote("Board Params", message["payload"])
+ elif cmd == "set_board":
+ BOARD.set_board(message["payload"])
+
+# info being a dictionary of format { player_id : {field : value} }
+func update_player_info(info = null):
+ # ADD NEW PLAYERS
+ var connected_player_ids : Array = []
+ for player in GC.players:
+ connected_player_ids.push_back(player["player_id"])
+ if !(player["player_id"] in PLAYER_INFO):
+ PLAYER_INFO[player["player_id"]] = {"username" : player["username"]}
+
+ # EDIT PLAYER ATTRIBUTES
+ if info is Dictionary:
+ for player_id in info.keys():
+ if !(info[player_id] is Dictionary) or !(player_id in PLAYER_INFO): return
+ for field in info[player_id].keys():
+ PLAYER_INFO[player_id][field] = info[player_id][field]
+
+ # DELETE DISCONNECTED PLAYERS
+ var player_ids_info = PLAYER_INFO.keys()
+ for player_id in player_ids_info:
+ if !(player_id in connected_player_ids) and (player_id in PLAYER_INFO):
+ PLAYER_INFO.erase(player_id)
+ continue
# ask host for complete game state. returned fields will depend on game state
func request_complete_game_state():
- if is_host: return
- var request = { "type" : "request", "source" : host_id, "requested_data" : "ALL" }
-
+ if GC.is_host: return
+ var request = { "type" : "request", "source" : GC.host_id, "requested_data" : "ALL" }
+ GC.send_json(request)
+
+
+func _on_TitleText_gui_input(event):
+ if (event is InputEventMouseButton && event.pressed && event.button_index == 1):
+ if OS.get_name() == "HTML5": # running on web
+ var direct_join_URL : String = JavaScript.eval("window.location.href")
+ direct_join_URL += "?lobby_id="+GC.lobby_id
+ if Globals.GC_URL != Globals.DEFAULT_GC_URL:
+ direct_join_URL += "&gc_url="+Globals.GC_URL
+ if GC.is_private:
+ direct_join_URL += "&pw="+GC.lobby_password
+ OS.set_clipboard(direct_join_URL)
+
diff --git a/scripts/Globals.gd b/scripts/Globals.gd
index 3603dbd..286b579 100644
--- a/scripts/Globals.gd
+++ b/scripts/Globals.gd
@@ -9,6 +9,8 @@ var airport_names : Array = []
const DEFAULT_GC_URL : String = "ws://192.168.7.112:8181"
var GC_URL : String = "ws://192.168.7.112:8181"
+const GAME_TYPE : String = "ATC"
+
func _ready():
load_airport_names()
set_process(false)
@@ -17,7 +19,6 @@ func update_gc_url(new_url : String):
GC_URL = new_url
-
func load_airport_names():
var f = File.new()
f.open(airport_names_file, File.READ)
diff --git a/scripts/HexSpace.gd b/scripts/HexSpace.gd
index bcf64f1..614039a 100644
--- a/scripts/HexSpace.gd
+++ b/scripts/HexSpace.gd
@@ -55,7 +55,7 @@ func set_up(settings):
valid_arrival_bearings.clear()
- if settings["orientation"]: # bearing according to E, NE, etc.
+ if "orientation" in settings: # bearing according to E, NE, etc.
orientation = settings["orientation"]
self.global_rotation.y = orientation * deg2rad(60)
@@ -68,10 +68,11 @@ func set_up(settings):
if settings["use_names"]:
airport_name = settings["airport_name"]
+ $Airport/AirportName.text = airport_name
$Airport/AirportName.visible = true
else:
airport_number = settings["airport_number"]
- airport_color = settings["airport_color"]
+ airport_color = Color(settings["airport_color"])
$Airport/AirportIcon.visible = true
$Airport/AirportIcon.texture = load("res://textures/airport_indicator_%d.png" % airport_number)
$Airport/AirportIcon.modulate = airport_color
diff --git a/scripts/MainMenu.gd b/scripts/MainMenu.gd
index fca6658..3411054 100644
--- a/scripts/MainMenu.gd
+++ b/scripts/MainMenu.gd
@@ -1,7 +1,7 @@
extends Control
-signal game_host_request(args)
-signal game_join_request(args)
+signal game_host_request(game_name, username, max_players, private, password)
+signal game_join_request(lobby_id, password, username, args)
var lobby_name_changed : bool = false # automatically update lobby name to be based on player's name
@@ -14,8 +14,19 @@ func _ready():
$SettingsMenu/GameCoordinatorURL.connect("text_changed", Globals, "update_gc_url")
$PlayerInfo/Username.connect("text_changed", self, "automatically_update_lobby_name")
$HostMenu/GameName.connect("text_changed", self, "set_lobby_name_changed")
+
+ $HostMenu/HostButton.connect("pressed", self, "on_host_button_pressed")
+ $JoinMenu/JoinButton.connect("pressed", self, "on_join_button_pressed")
#main_menu() # in case things are incorrectly visible from editing
+func open_popup(title, message, close_disabled = false):
+ $NotifPopup.popup_centered()
+ $NotifPopup/PopupControl/TitleLabel.text = title
+ $NotifPopup/PopupControl/MessageLabel.text = message
+ $NotifPopup/PopupControl/ClosePopupButton.disabled = close_disabled
+
+func close_popup():
+ $NotifPopup.visible = false
func set_lobby_name_changed(_disregard_new_text):
lobby_name_changed = true
@@ -54,6 +65,8 @@ func set_back_button_visible(visible : bool):
$BackButton.visible = visible
$BackButton.disabled = false # reset in case left disabled by other function
+## MENU CALLBACKS
+
# go to join game menu
func join_menu_button_pressed(lobby_id_from_url : String = "", password_from_url : String = ""):
set_menu_buttons_visible(false)
@@ -82,3 +95,13 @@ func settings_menu_button_pressed():
# return to main menu
func back_button():
main_menu()
+
+## GAME CALLBACKS
+func on_join_button_pressed():
+ open_popup("Connecting...", "Connecting to game coordinator...", true)
+ var args = { "color_pref_1" : $PlayerInfo/PlayerColor.selected, "color_pref_2" : $PlayerInfo/AltPlayerColor.selected }
+ emit_signal("game_join_request", $JoinMenu/LobbyID.text, $JoinMenu/Password.text, $PlayerInfo/Username.text, args)
+
+func on_host_button_pressed():
+ open_popup("Connecting...", "Connecting to game coordinator...", true)
+ emit_signal("game_host_request", $HostMenu/GameName.text, $PlayerInfo/Username.text, $HostMenu/PlayerCount.get_selected_id(), $HostMenu/PrivateToggle.pressed, $HostMenu/Password.text)
diff --git a/scripts/MainScene.gd b/scripts/MainScene.gd
index fc15463..c15ed1c 100644
--- a/scripts/MainScene.gd
+++ b/scripts/MainScene.gd
@@ -1,13 +1,78 @@
extends Control
+onready var GC = $GCClient
+onready var MM = $MainMenu
+
+enum { MENU, INGAME }
+var game_state : int = MENU
+
+onready var game_scene_base : PackedScene = preload("res://pages/GameTable.tscn")
+var game_scene = null
func _ready():
if OS.get_name() == "HTML5": # running on web
var lobby_id = JavaScript.eval("new URLSearchParams(document.location.search).get('lobby_id')")
var gc_url = JavaScript.eval("new URLSearchParams(document.location.search).get('gc_url')")
var password = JavaScript.eval("new URLSearchParams(document.location.search).get('pw')")
- if lobby_id:
- var pw : String = password if password else ""
- $MainMenu.join_menu_button_pressed(lobby_id, pw)
if gc_url:
Globals.update_gc_url(gc_url)
+ if lobby_id:
+ var pw : String = password if password else ""
+ MM.join_menu_button_pressed(lobby_id, pw)
+
+ GC.connect("join_request_success", self, "on_join_request_success")
+ GC.connect("host_success", self, "on_host_success")
+ GC.connect("host_failure", self, "on_host_failure")
+ GC.connect("join_success", self, "on_join_success")
+ GC.connect("join_failure", self, "on_join_failure")
+ GC.connect("gc_connection_failed", self, "on_gc_connection_failure")
+
+ MM.connect("game_host_request", GC, "host_lobby")
+ MM.connect("game_join_request", GC, "join_lobby")
+
+func _process(_delta):
+ if GC.state > 1 and (game_state == MENU):
+ GC.receive()
+
+func to_main_menu():
+ if game_state == MENU: return
+ if game_scene:
+ remove_child(game_scene)
+ game_scene.queue_free()
+ GC.reset_gc_client()
+ GC.close_socket()
+ MM.visible = true
+ game_state = MENU
+
+func to_game(args = null):
+ if game_state == INGAME: return
+ MM.close_popup()
+ MM.visible = false
+ game_scene = game_scene_base.instance()
+ game_scene.set_up(GC)
+ if args:
+ game_scene.set_up_args(args)
+ add_child(game_scene)
+ game_state = INGAME
+
+# GC FUNCTIONS
+
+func on_gc_connection_failure():
+ MM.open_popup("Error", "Could not connect to game coordinator.")
+
+func on_join_request_success():
+ MM.close_popup()
+ MM.open_popup("Connected", "Waiting for acknowledgment from host...")
+
+func on_host_success():
+ to_game()
+
+func on_host_failure(reason):
+ MM.open_popup("Error", "Could not host game: %s" % reason)
+
+func on_join_success(join_args):
+ to_game(join_args)
+
+func on_join_failure(reason):
+ MM.open_popup("Error", "Could not join game: %s" % reason)
+
diff --git a/scripts/PlaneControlBoard.gd b/scripts/PlaneControlBoard.gd
index cfe94b5..8c9c87e 100644
--- a/scripts/PlaneControlBoard.gd
+++ b/scripts/PlaneControlBoard.gd
@@ -28,5 +28,7 @@ func set_destination(number, color):
$DestinationHBOX/DestinationIcon.texture = airport_indicators[number - 1]
$DestinationHBOX/DestinationIcon.modulate = color
-func update_display_from_plane(plane):
- pass
+func on_plane_selected(plane : Airplane, destination):
+
+
+ self.show()
diff --git a/scripts/ServerBrowser.gd b/scripts/ServerBrowser.gd
index 410eec2..41f478a 100644
--- a/scripts/ServerBrowser.gd
+++ b/scripts/ServerBrowser.gd
@@ -18,6 +18,11 @@ func _ready():
$RefreshButton.connect("pressed", self, "refresh_game_list")
$HostPopup/Control/PrivateToggle.connect("toggled", self, "toggle_password_vis")
$Username.connect("text_changed", $HostPopup/Control/GameName, "set_text")
+ $HostPopup/Control/PlayerCount.connect("item_selected", self, "test_select_signal")
+
+
+func test_select_signal(index):
+ print("selected %d" % index)
func join_game():
$HostPopup.visible = false
@@ -53,6 +58,7 @@ func add_games_to_list(games):
game_ids.append( game["id"] )
func toggle_password_vis(pressed):
+ $HostPopup/Control/PlayerCount.select(3)
$HostPopup/Control/Password.visible = pressed