Add private multiplayer room codes
This commit is contained in:
1 parent
7362ca0f02
commit
19b1a67773
8 files changed
+341
-24
No files matched your search
+4
-4
@@ -1,4 +1,4 @@
|
||||
# Void Bastion Online 0.19
|
||||
# Void Bastion Online 0.20
|
||||
|
||||
## Fertiger Stand
|
||||
|
||||
@@ -22,9 +22,9 @@ Mögliche Spielarten sind `mission`, `endless`, `coop_ai` und `coop_online`.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
Die Godot-Anbindung öffnet nach der Anmeldung eine WSS/WS-Echtzeitverbindung. Die schnelle Spielersuche vermittelt exakt zwei Spieler mit derselben Suchart in eine serverautoritative Koop-Sitzung. Der Matchserver verwaltet Teilnehmer, gemeinsame Sitzungsdaten und die Weiterleitung zulässiger Spielnachrichten.
|
||||
Die Godot-Anbindung öffnet nach der Anmeldung eine WSS/WS-Echtzeitverbindung. Die schnelle Spielersuche vermittelt exakt zwei passende Spieler. Alternativ erstellt ein Spieler einen privaten Raum mit einem sechsstelligen Code, dem der zweite Spieler gezielt beitritt.
|
||||
|
||||
Der lokale KI-Koop bleibt unabhängig davon spielbar. Die vollständige Synchronisation von Türmen, Gegnern und gemeinsamem Reaktor zwischen zwei sichtbaren Spielclients ist der nächste Gameplay-Schritt; Konto, Transport, Vermittlung und Server-Match sind bereits funktionsfähig.
|
||||
Der autoritative Matchserver verwaltet Spielerplätze, getrennte Energie, Türme, Gegner, Wellen und die gemeinsamen Reaktorleben. Beide Clients erhalten denselben geprüften Spielzustand. Der lokale KI-Koop bleibt unabhängig davon spielbar.
|
||||
|
||||
## Adressen
|
||||
|
||||
@@ -41,4 +41,4 @@ Die öffentliche Freischaltung ist abgeschlossen: Der autoritative DNS-Eintrag `
|
||||
- echter Onlinetest im Heimnetz: `godot --headless --path . --script res://tests/online_integration_test.gd`
|
||||
- Produktionstest: `VOID_BASTION_TEST_HOST=api.void-bastion.gergames.de VOID_BASTION_TEST_SCHEME=https VOID_BASTION_TEST_PORT=443 godot --headless --path . --script res://tests/online_integration_test.gd`
|
||||
|
||||
Der Onlinetest prüft zwei Gastanmeldungen, RPC, Ergebnisannahme, Ranglistenabruf, zwei WebSocket-Verbindungen, Matchmaking und den Beitritt beider Clients zur selben autoritativen Partie.
|
||||
Der Onlinetest prüft zwei Gastanmeldungen, RPC, Ergebnisannahme, Ranglistenabruf, zwei WebSocket-Verbindungen, schnelles Matchmaking, autoritative Spielzustände sowie Erstellen und Beitreten eines privaten Raums per Code.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Void Bastion – spielbarer Prototyp 0.19
|
||||
# Void Bastion – spielbarer Prototyp 0.20
|
||||
|
||||
Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuelle Stand ist für Windows, Android und später iOS ausgelegt.
|
||||
|
||||
@@ -39,10 +39,10 @@ Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuell
|
||||
- dauerhaftes lokales Gastprofil mit stabiler Spieler-ID als Grundlage für Apple-, Google- und Windows-Anmeldung
|
||||
- eigene Profilansicht mit lokalen Missions- und Endlosbestwerten pro Karte
|
||||
- wartende, lokal gespeicherte Highscore-Einträge für die spätere sichere Serverübertragung
|
||||
- eigener Multiplayerbereich mit schnellem Spiel, Freundeseinladung und privatem Raum als vorbereitete Sucharten
|
||||
- eigener Multiplayerbereich mit schneller Spielersuche sowie privaten Koop-Räumen mit sechsstelligen Raumcodes
|
||||
- Vorschau einer symmetrisch gespiegelten Zwei-Spieler-Arena mit gemeinsamem Reaktor
|
||||
- getrennte Ressourcenmodelle für beide Koop-Spieler sowie gemeinsam geteilte Reaktorleben
|
||||
- sichtbarer, abbrechbarer Vorbereitungszustand für die spätere servergestützte Spielersuche
|
||||
- sichtbarer und jederzeit abbrechbarer Wartezustand für schnelle und private Online-Partien
|
||||
- bereits spielbarer lokaler Koop-Modus mit einem KI-Partner auf der gespiegelten Kartenhälfte
|
||||
- nahezu verdoppelte Koop-Weltbreite: Beide Spieler erhalten eine vollständige Kartenhälfte in ursprünglicher Größe
|
||||
- Spiegelachse direkt durch den vorhandenen Kartenreaktor, sodass Hintergrundreaktor, gemeinsamer Zielpunkt und Laufwege wieder zusammenpassen
|
||||
@@ -102,7 +102,7 @@ Die öffentliche Spiel-API ist unter `api.void-bastion.gergames.de` erreichbar.
|
||||
|
||||
## Steuerung
|
||||
|
||||
> **Online-Stand 0.19:** Der offizielle Nakama-Godot-Client, automatische Gastanmeldung, 36 autoritative Ranglisten, geprüfte Ergebnisübermittlung, Echtzeit-Sockets und das serverautoritative Zwei-Spieler-Matchmaking sind eingebaut und gegen Unraid getestet. Die öffentliche API ist über `https://api.void-bastion.gergames.de` mit einem gültigen Let’s-Encrypt-Zertifikat freigeschaltet. Details stehen in `ONLINE_SETUP.md`.
|
||||
> **Online-Stand 0.20:** Der offizielle Nakama-Godot-Client, automatische Gastanmeldung, 36 autoritative Ranglisten, geprüfte Ergebnisübermittlung, Echtzeit-Sockets, serverautoritativer Zwei-Spieler-Koop und private Räume per Raumcode sind eingebaut. Die öffentliche API ist über `https://api.void-bastion.gergames.de` mit einem gültigen Let’s-Encrypt-Zertifikat freigeschaltet. Details stehen in `ONLINE_SETUP.md`.
|
||||
|
||||
- Turm unten auswählen und auf eine freie Plattformfläche klicken oder tippen.
|
||||
- Am PC folgt der Reichweitenkreis vor dem Bau dem Mauszeiger. Auf Mobilgeräten markiert der erste Tipp den Bauplatz samt Reichweite; der zweite Tipp auf dasselbe Feld bestätigt den Bau.
|
||||
@@ -117,6 +117,7 @@ Die öffentliche Spiel-API ist unter `api.void-bastion.gergames.de` erreichbar.
|
||||
- `P`, Escape oder die Pause-Schaltfläche pausiert das Spiel.
|
||||
- Nach Welle 15 kann die erfolgreiche Mission beendet oder freiwillig im Endlosmodus fortgesetzt werden.
|
||||
- Unter **Multiplayer → Mit KI-Partner** startet die gespiegelte Koop-Arena sofort. Du baust auf der vollständigen linken Karte, die KI auf deren vollständiger Spiegelung; beide Seiten verteidigen denselben Reaktor. Mit **KI >** und **< ICH** wird ohne Verkleinerung zwischen beiden Seiten gewechselt.
|
||||
- Unter **Multiplayer → Raum erstellen** wird ein privater Koop-Raum angelegt und ein sechsstelliger Code angezeigt. Der Mitspieler wählt **Raum beitreten** und gibt diesen Code ein.
|
||||
|
||||
## Nächste Produktionsschritte
|
||||
|
||||
@@ -125,8 +126,8 @@ Die öffentliche Spiel-API ist unter `api.void-bastion.gergames.de` erreichbar.
|
||||
3. Kampagnenfortschritt und zusätzliche Bossvarianten
|
||||
4. Freischaltmenü für neue Türme und Karten mit den bereits gespeicherten Kernkristallen
|
||||
5. Plattformkonten mit einer eigenen plattformübergreifenden Spieler-ID verbinden
|
||||
6. Autoritativen Koop-Spielserver, Räume, Freundeseinladungen und Spielersuche anbinden
|
||||
7. Die gespiegelte Koop-Arena spielbar machen und anschließend über echte Geräte synchron testen
|
||||
6. Private Raumcodes und die autoritative Koop-Synchronisierung auf echten Android-, iOS- und Windows-Geräten testen
|
||||
7. Freundesliste, Einladungen und Wiederverbindung nach kurzzeitigem Netzverlust ergänzen
|
||||
8. Store-Käufe erst nach stabiler Offline- und Koop-Kernversion
|
||||
|
||||
## Godot Agent Tools MCP
|
||||
|
||||
@@ -137,7 +137,10 @@ var friend_match_button := Rect2(475, 665, 300, 64)
|
||||
var private_match_button := Rect2(825, 665, 300, 64)
|
||||
var ai_match_button := Rect2(1175, 665, 300, 64)
|
||||
var cancel_search_button := Rect2(625, 650, 350, 64)
|
||||
var room_code_input_rect := Rect2(510, 340, 580, 90)
|
||||
var join_room_button := Rect2(625, 525, 350, 64)
|
||||
var coop_side_button := Rect2(920, 18, 140, 54)
|
||||
var room_code_input := ""
|
||||
|
||||
@onready var ads_manager = $AdsManager
|
||||
@onready var account_manager = $AccountManager
|
||||
@@ -366,6 +369,20 @@ func _update_effects(delta: float) -> void:
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if game_mode == "room_code_entry" and event is InputEventKey and event.pressed and not event.echo:
|
||||
if event.keycode == KEY_ESCAPE:
|
||||
_hide_virtual_keyboard()
|
||||
game_mode = "multiplayer_menu"
|
||||
elif event.keycode == KEY_BACKSPACE:
|
||||
room_code_input = room_code_input.left(maxi(0, room_code_input.length() - 1))
|
||||
elif event.keycode == KEY_ENTER:
|
||||
_join_entered_room()
|
||||
elif event.unicode > 0 and room_code_input.length() < 6:
|
||||
var character := char(event.unicode).to_upper()
|
||||
if "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".contains(character):
|
||||
room_code_input += character
|
||||
queue_redraw()
|
||||
return
|
||||
if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_P, KEY_ESCAPE] and game_mode in ["playing", "paused"]:
|
||||
_toggle_pause()
|
||||
return
|
||||
@@ -409,13 +426,23 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
|
||||
elif quick_match_button.has_point(position):
|
||||
_open_matchmaking("quick")
|
||||
elif friend_match_button.has_point(position):
|
||||
_open_matchmaking("friend")
|
||||
_open_room_code_entry()
|
||||
elif private_match_button.has_point(position):
|
||||
_open_matchmaking("private")
|
||||
_open_private_room()
|
||||
elif ai_match_button.has_point(position):
|
||||
_start_coop_ai_game()
|
||||
queue_redraw()
|
||||
return
|
||||
if game_mode == "room_code_entry":
|
||||
if multiplayer_back_button.has_point(position):
|
||||
_hide_virtual_keyboard()
|
||||
game_mode = "multiplayer_menu"
|
||||
elif room_code_input_rect.has_point(position):
|
||||
_show_room_code_keyboard()
|
||||
elif join_room_button.has_point(position):
|
||||
_join_entered_room()
|
||||
queue_redraw()
|
||||
return
|
||||
if game_mode == "matchmaking":
|
||||
if cancel_search_button.has_point(position) or multiplayer_back_button.has_point(position):
|
||||
multiplayer_manager.cancel_search()
|
||||
@@ -607,6 +634,41 @@ func _open_matchmaking(mode: String) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _open_private_room() -> void:
|
||||
multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30)
|
||||
multiplayer_manager.create_private_room(selected_difficulty)
|
||||
game_mode = "matchmaking"
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _open_room_code_entry() -> void:
|
||||
room_code_input = ""
|
||||
game_mode = "room_code_entry"
|
||||
_show_room_code_keyboard()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _join_entered_room() -> void:
|
||||
if room_code_input.length() != 6:
|
||||
_show_status("DER RAUMCODE MUSS SECHS ZEICHEN HABEN")
|
||||
return
|
||||
_hide_virtual_keyboard()
|
||||
multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30)
|
||||
multiplayer_manager.join_private_room(room_code_input)
|
||||
game_mode = "matchmaking"
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _show_room_code_keyboard() -> void:
|
||||
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
|
||||
DisplayServer.virtual_keyboard_show(room_code_input, room_code_input_rect, DisplayServer.KEYBOARD_TYPE_DEFAULT, 6, room_code_input.length(), room_code_input.length())
|
||||
|
||||
|
||||
func _hide_virtual_keyboard() -> void:
|
||||
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
|
||||
DisplayServer.virtual_keyboard_hide()
|
||||
|
||||
|
||||
func _start_game() -> void:
|
||||
coop_online_mode = false
|
||||
credits = int(_difficulty().start_credits)
|
||||
@@ -1111,7 +1173,7 @@ func _load_progress() -> void:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if game_mode in ["menu", "profile", "multiplayer_menu", "matchmaking"]:
|
||||
if game_mode in ["menu", "profile", "multiplayer_menu", "room_code_entry", "matchmaking"]:
|
||||
draw_texture_rect(MAP_TEXTURES[selected_map], Rect2(Vector2.ZERO, SCREEN), false, Color("f3f6fb"))
|
||||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.01, 0.025, 0.055, 0.06))
|
||||
if game_mode == "menu":
|
||||
@@ -1120,6 +1182,8 @@ func _draw() -> void:
|
||||
_draw_profile()
|
||||
elif game_mode == "multiplayer_menu":
|
||||
_draw_multiplayer_menu()
|
||||
elif game_mode == "room_code_entry":
|
||||
_draw_room_code_entry()
|
||||
else:
|
||||
_draw_matchmaking()
|
||||
return
|
||||
@@ -1426,14 +1490,30 @@ func _draw_multiplayer_menu() -> void:
|
||||
draw_string(font, Vector2(945, 225), "MITSPIELER • ENERGIE %d" % multiplayer_manager.player_credits[1], HORIZONTAL_ALIGNMENT_RIGHT, 440, 17, Color("ffcf7a"))
|
||||
draw_string(font, Vector2(570, 555), "REAKTORLEBEN %02d • GETRENNTE RESSOURCEN" % multiplayer_manager.shared_reactor_lives, HORIZONTAL_ALIGNMENT_CENTER, 460, 16, Color.WHITE)
|
||||
_draw_button(quick_match_button, "SCHNELLES SPIEL", false, Color("185f68"))
|
||||
_draw_button(friend_match_button, "MIT FREUND SPIELEN", false, Color("254f75"))
|
||||
_draw_button(private_match_button, "PRIVATES SPIEL", false, Color("493f67"))
|
||||
_draw_button(friend_match_button, "RAUM BEITRETEN", false, Color("254f75"))
|
||||
_draw_button(private_match_button, "RAUM ERSTELLEN", false, Color("493f67"))
|
||||
_draw_button(ai_match_button, "MIT KI-PARTNER", false, Color("6b4b28"))
|
||||
var service_text := "GERGAMES-SERVER VERBUNDEN • ONLINE-KOOP IST SPIELBEREIT" if online_manager.is_online() else "VERBINDUNG ZUM GERGAMES-SERVER WIRD AUTOMATISCH AUFGEBAUT"
|
||||
var service_color := Color("83ffc8") if online_manager.is_online() else Color("ffcf7a")
|
||||
draw_string(font, Vector2(0, 770), service_text, HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, service_color)
|
||||
|
||||
|
||||
func _draw_room_code_entry() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.88))
|
||||
_draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a"))
|
||||
draw_string(font, Vector2(0, 205), "PRIVATEN RAUM BEITRETEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff"))
|
||||
draw_string(font, Vector2(0, 260), "GIB DEN SECHSSTELLIGEN CODE DEINES MITPILOTEN EIN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("66dff3"))
|
||||
draw_rect(room_code_input_rect, Color(0.01, 0.035, 0.065, 0.98), true)
|
||||
draw_rect(room_code_input_rect, Color("65eaff") if room_code_input.length() == 6 else Color("39788e"), false, 3.0)
|
||||
var shown_code := room_code_input
|
||||
if shown_code.is_empty():
|
||||
shown_code = "------"
|
||||
draw_string(font, Vector2(room_code_input_rect.position.x, room_code_input_rect.position.y + 61), " ".join(shown_code.split("")), HORIZONTAL_ALIGNMENT_CENTER, room_code_input_rect.size.x, 38, Color("8ffaff") if not room_code_input.is_empty() else Color("688293"))
|
||||
draw_string(font, Vector2(0, 470), "BUCHSTABEN UND ZAHLEN • GROSS-/KLEINSCHREIBUNG IST EGAL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("9fdce9"))
|
||||
_draw_button(join_room_button, "RAUM BEITRETEN", room_code_input.length() == 6, Color("185f68") if room_code_input.length() == 6 else Color("26394a"))
|
||||
|
||||
|
||||
func _draw_coop_lane_preview(mirrored: bool) -> void:
|
||||
var points := PackedVector2Array([Vector2(205, 300), Vector2(505, 300), Vector2(505, 385), Vector2(720, 385)])
|
||||
if mirrored:
|
||||
@@ -1453,13 +1533,17 @@ func _draw_matchmaking() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.88))
|
||||
_draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a"))
|
||||
draw_string(font, Vector2(0, 210), "MITSPIELERSUCHE", HORIZONTAL_ALIGNMENT_CENTER, 1600, 46, Color("c7f5ff"))
|
||||
var private_mode: bool = multiplayer_manager.matchmaking_mode in ["private_host", "private_join"]
|
||||
draw_string(font, Vector2(0, 210), "PRIVATER KOOP-RAUM" if private_mode else "MITSPIELERSUCHE", HORIZONTAL_ALIGNMENT_CENTER, 1600, 46, Color("c7f5ff"))
|
||||
var pulse := 56.0 + sin(elapsed * 3.0) * 8.0
|
||||
draw_arc(Vector2(800, 390), pulse, -PI * 0.2, PI * 1.45, 52, Color("65eaff"), 5.0)
|
||||
draw_circle(Vector2(800, 390), 22.0, Color("8ffaff"))
|
||||
var mode_labels := {"quick": "SCHNELLES SPIEL", "friend": "FREUND EINLADEN", "private": "PRIVATEN RAUM ERSTELLEN"}
|
||||
draw_string(font, Vector2(0, 500), mode_labels.get(multiplayer_manager.matchmaking_mode, "KOOP-SPIEL"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color.WHITE)
|
||||
_draw_button(cancel_search_button, "SUCHE ABBRECHEN", false, Color("5c3342"))
|
||||
var mode_labels := {"quick": "SCHNELLES SPIEL", "private_host": "RAUM ERSTELLEN", "private_join": "RAUM BEITRETEN"}
|
||||
var mode_text: String = mode_labels.get(multiplayer_manager.matchmaking_mode, "KOOP-SPIEL")
|
||||
if not multiplayer_manager.private_room_code.is_empty():
|
||||
mode_text = "RAUMCODE %s" % " ".join(multiplayer_manager.private_room_code.split(""))
|
||||
draw_string(font, Vector2(0, 500), mode_text, HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color.WHITE)
|
||||
_draw_button(cancel_search_button, "ZURÜCK" if multiplayer_manager.matchmaking_state == "room_not_found" else "SUCHE ABBRECHEN", false, Color("5c3342"))
|
||||
var online_text := "ONLINE • SUCHE NACH EINEM MITPILOTEN"
|
||||
var online_color := Color("83ffc8")
|
||||
if multiplayer_manager.matchmaking_state == "waiting_for_service":
|
||||
@@ -1471,6 +1555,18 @@ func _draw_matchmaking() -> void:
|
||||
elif multiplayer_manager.matchmaking_state == "matched":
|
||||
online_text = "MITPILOT GEFUNDEN • KOOP-SITZUNG BEREIT"
|
||||
online_color = Color("8ffaff")
|
||||
elif multiplayer_manager.matchmaking_state == "creating_room":
|
||||
online_text = "PRIVATER RAUM WIRD ERSTELLT ."
|
||||
online_color = Color("ffcf7a")
|
||||
elif multiplayer_manager.matchmaking_state == "hosting":
|
||||
online_text = "TEILE DEN RAUMCODE MIT DEINEM MITPILOTEN • WARTE AUF BEITRITT"
|
||||
online_color = Color("83ffc8")
|
||||
elif multiplayer_manager.matchmaking_state == "joining_room":
|
||||
online_text = "PRIVATER RAUM WIRD GESUCHT ."
|
||||
online_color = Color("ffcf7a")
|
||||
elif multiplayer_manager.matchmaking_state == "room_not_found":
|
||||
online_text = "RAUM NICHT GEFUNDEN ODER BEREITS VOLL"
|
||||
online_color = Color("ff8e8e")
|
||||
draw_rect(Rect2(180, 520, 1240, 44), Color(0.005, 0.015, 0.035, 0.98), true)
|
||||
draw_string(font, Vector2(0, 548), online_text, HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, online_color)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const COOP_REACTOR_LIVES := 30
|
||||
var matchmaking_state := "idle"
|
||||
var matchmaking_mode := ""
|
||||
var matchmaking_difficulty := 1
|
||||
var private_room_code := ""
|
||||
var online_match = null
|
||||
var online_mode := false
|
||||
var player_slot := -1
|
||||
@@ -26,6 +27,7 @@ func reset_coop_session(start_credits := COOP_START_CREDITS, reactor_lives := CO
|
||||
online_mode = false
|
||||
player_slot = -1
|
||||
latest_snapshot = {}
|
||||
private_room_code = ""
|
||||
|
||||
|
||||
func spend_player_credits(player_index: int, amount: int) -> bool:
|
||||
@@ -76,11 +78,37 @@ func begin_search(mode := "quick", difficulty := 1) -> void:
|
||||
call_deferred("_begin_online_search", mode)
|
||||
|
||||
|
||||
func create_private_room(difficulty := 1) -> void:
|
||||
matchmaking_mode = "private_host"
|
||||
matchmaking_difficulty = clampi(difficulty, 0, 2)
|
||||
private_room_code = ""
|
||||
online_match = null
|
||||
online_mode = false
|
||||
player_slot = -1
|
||||
latest_snapshot = {}
|
||||
matchmaking_state = "waiting_for_service"
|
||||
state_changed.emit(matchmaking_state)
|
||||
call_deferred("_create_private_room_online")
|
||||
|
||||
|
||||
func join_private_room(room_code: String) -> void:
|
||||
matchmaking_mode = "private_join"
|
||||
private_room_code = room_code.to_upper().strip_edges()
|
||||
online_match = null
|
||||
online_mode = false
|
||||
player_slot = -1
|
||||
latest_snapshot = {}
|
||||
matchmaking_state = "waiting_for_service"
|
||||
state_changed.emit(matchmaking_state)
|
||||
call_deferred("_join_private_room_online")
|
||||
|
||||
|
||||
func cancel_search() -> void:
|
||||
var online_manager = get_parent().get_node_or_null("OnlineManager")
|
||||
if online_manager:
|
||||
online_manager.cancel_matchmaking()
|
||||
matchmaking_mode = ""
|
||||
private_room_code = ""
|
||||
matchmaking_state = "idle"
|
||||
state_changed.emit(matchmaking_state)
|
||||
|
||||
@@ -102,10 +130,43 @@ func _begin_online_search(mode: String) -> void:
|
||||
state_changed.emit(matchmaking_state)
|
||||
|
||||
|
||||
func _ensure_online_manager():
|
||||
var online_manager = get_parent().get_node_or_null("OnlineManager")
|
||||
if not online_manager:
|
||||
matchmaking_state = "offline"
|
||||
state_changed.emit(matchmaking_state)
|
||||
return null
|
||||
if not online_manager.is_authenticated():
|
||||
await online_manager.connect_and_authenticate()
|
||||
return online_manager
|
||||
|
||||
|
||||
func _create_private_room_online() -> void:
|
||||
var online_manager = await _ensure_online_manager()
|
||||
if not online_manager or matchmaking_mode != "private_host":
|
||||
return
|
||||
var created: bool = await online_manager.create_private_room(matchmaking_difficulty)
|
||||
private_room_code = online_manager.private_room_code if created else ""
|
||||
matchmaking_state = "hosting" if created else online_manager.connection_state
|
||||
state_changed.emit(matchmaking_state)
|
||||
|
||||
|
||||
func _join_private_room_online() -> void:
|
||||
var online_manager = await _ensure_online_manager()
|
||||
if not online_manager or matchmaking_mode != "private_join":
|
||||
return
|
||||
var joined: bool = await online_manager.join_private_room(private_room_code)
|
||||
matchmaking_state = "matched" if joined else "room_not_found"
|
||||
state_changed.emit(matchmaking_state)
|
||||
|
||||
|
||||
func _on_online_matchmaking_state_changed(value: String) -> void:
|
||||
if matchmaking_state == "idle":
|
||||
return
|
||||
matchmaking_state = value
|
||||
var online_manager = get_parent().get_node_or_null("OnlineManager")
|
||||
if online_manager and not online_manager.private_room_code.is_empty():
|
||||
private_room_code = online_manager.private_room_code
|
||||
state_changed.emit(matchmaking_state)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ signal matchmaking_state_changed(state: String)
|
||||
signal match_found(match_data)
|
||||
signal match_snapshot_received(snapshot: Dictionary)
|
||||
signal match_event_received(event: Dictionary)
|
||||
signal private_room_created(room_code: String)
|
||||
|
||||
const SERVER_KEY := "voidbastion-public-v1"
|
||||
const PRODUCTION_HOST := "api.void-bastion.gergames.de"
|
||||
@@ -30,6 +31,7 @@ var last_error := ""
|
||||
var current_host := ""
|
||||
var current_match = null
|
||||
var matchmaker_ticket := ""
|
||||
var private_room_code := ""
|
||||
var account_manager: AccountManager
|
||||
var auto_connect_enabled := true
|
||||
|
||||
@@ -102,6 +104,7 @@ func disconnect_from_server() -> void:
|
||||
session = null
|
||||
current_match = null
|
||||
matchmaker_ticket = ""
|
||||
private_room_code = ""
|
||||
_set_connection_state("offline")
|
||||
|
||||
|
||||
@@ -178,9 +181,62 @@ func cancel_matchmaking() -> void:
|
||||
if socket and not matchmaker_ticket.is_empty():
|
||||
await socket.remove_matchmaker_async(matchmaker_ticket)
|
||||
matchmaker_ticket = ""
|
||||
if current_match != null and not private_room_code.is_empty():
|
||||
await socket.leave_match_async(current_match.match_id)
|
||||
current_match = null
|
||||
private_room_code = ""
|
||||
matchmaking_state_changed.emit("idle")
|
||||
|
||||
|
||||
func create_private_room(difficulty := 1) -> bool:
|
||||
if not is_online() or socket == null:
|
||||
matchmaking_state_changed.emit("offline")
|
||||
return false
|
||||
matchmaking_state_changed.emit("creating_room")
|
||||
var result = await client.rpc_async(session, "create_private_room", JSON.stringify({"difficulty": clampi(difficulty, 0, 2)}))
|
||||
var data := _parse_rpc_result(result)
|
||||
var match_id := String(data.get("match_id", ""))
|
||||
private_room_code = String(data.get("room_code", ""))
|
||||
if match_id.is_empty() or private_room_code.length() != 6:
|
||||
private_room_code = ""
|
||||
matchmaking_state_changed.emit("error")
|
||||
return false
|
||||
current_match = await socket.join_match_async(match_id)
|
||||
if current_match == null or current_match.is_exception():
|
||||
current_match = null
|
||||
private_room_code = ""
|
||||
matchmaking_state_changed.emit("error")
|
||||
return false
|
||||
private_room_created.emit(private_room_code)
|
||||
matchmaking_state_changed.emit("hosting")
|
||||
await send_match_command(OP_READY, {})
|
||||
return true
|
||||
|
||||
|
||||
func join_private_room(room_code: String) -> bool:
|
||||
if not is_online() or socket == null:
|
||||
matchmaking_state_changed.emit("offline")
|
||||
return false
|
||||
private_room_code = room_code.to_upper().strip_edges()
|
||||
matchmaking_state_changed.emit("joining_room")
|
||||
var result = await client.rpc_async(session, "join_private_room", JSON.stringify({"room_code": private_room_code}))
|
||||
var data := _parse_rpc_result(result)
|
||||
var match_id := String(data.get("match_id", ""))
|
||||
if not bool(data.get("found", false)) or match_id.is_empty():
|
||||
private_room_code = ""
|
||||
matchmaking_state_changed.emit("room_not_found")
|
||||
return false
|
||||
current_match = await socket.join_match_async(match_id)
|
||||
if current_match == null or current_match.is_exception():
|
||||
current_match = null
|
||||
private_room_code = ""
|
||||
matchmaking_state_changed.emit("room_not_found")
|
||||
return false
|
||||
matchmaking_state_changed.emit("matched")
|
||||
await send_match_command(OP_READY, {})
|
||||
return true
|
||||
|
||||
|
||||
func _on_matchmaker_matched(matched) -> void:
|
||||
matchmaker_ticket = ""
|
||||
current_match = await socket.join_matched_async(matched)
|
||||
@@ -219,6 +275,7 @@ func leave_current_match() -> void:
|
||||
if socket and current_match != null:
|
||||
await socket.leave_match_async(current_match.match_id)
|
||||
current_match = null
|
||||
private_room_code = ""
|
||||
|
||||
|
||||
func _on_match_state(data) -> void:
|
||||
|
||||
@@ -3,6 +3,8 @@ var MAP_COUNT = 3;
|
||||
var DIFFICULTY_COUNT = 3;
|
||||
var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"];
|
||||
var MATCH_MODULE = "void_bastion_coop";
|
||||
var ROOM_COLLECTION = "private_rooms";
|
||||
var SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
|
||||
var OP_SNAPSHOT = 1, OP_EVENT = 2, OP_BUILD = 10, OP_UPGRADE = 11, OP_SELL = 12, OP_START_WAVE = 13, OP_READY = 14;
|
||||
var TICK_RATE = 10, SNAPSHOT_TICKS = 2, GRID = 80, WORLD_WIDTH = 2960, MIRROR_X = 1480;
|
||||
var REACTOR_X = 1480, REACTOR_Y = 440, REACTOR_RADIUS = 112, PLAYFIELD_TOP = 92, PLAYFIELD_BOTTOM = 758;
|
||||
@@ -46,6 +48,47 @@ function rpcSubmitRun(ctx, logger, nk, payload) {
|
||||
return JSON.stringify({accepted:true,leaderboard_id:id,rank:record.rank,score:Number(record.score),wave:wave});
|
||||
}
|
||||
|
||||
function normalizeRoomCode(value) {
|
||||
return String(value || "").toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 6);
|
||||
}
|
||||
function generateRoomCode() {
|
||||
var alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789", code = "";
|
||||
for (var i = 0; i < 6; i++) code += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
||||
return code;
|
||||
}
|
||||
function privateRoomRecord(nk, code) {
|
||||
var records = nk.storageRead([{collection:ROOM_COLLECTION,key:code,userId:SYSTEM_USER_ID}]);
|
||||
return records.length ? records[0] : null;
|
||||
}
|
||||
function deletePrivateRoomRecord(nk, code) {
|
||||
if (code) nk.storageDelete([{collection:ROOM_COLLECTION,key:code,userId:SYSTEM_USER_ID}]);
|
||||
}
|
||||
function rpcCreatePrivateRoom(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("authentication required");
|
||||
var request;
|
||||
try { request = JSON.parse(payload || "{}"); } catch (error) { request = {}; }
|
||||
var difficulty = Math.max(0, Math.min(2, Math.floor(Number(request.difficulty || 1))));
|
||||
var code = "";
|
||||
for (var attempt = 0; attempt < 24; attempt++) {
|
||||
var candidate = generateRoomCode();
|
||||
if (!privateRoomRecord(nk, candidate)) { code = candidate; break; }
|
||||
}
|
||||
if (!code) throw new Error("room code allocation failed");
|
||||
var matchId = nk.matchCreate(MATCH_MODULE, {difficulty:difficulty,mode:"private",roomCode:code,hostUserId:ctx.userId});
|
||||
nk.storageWrite([{collection:ROOM_COLLECTION,key:code,userId:SYSTEM_USER_ID,value:{match_id:matchId,host_user_id:ctx.userId,created_at:Date.now()},permissionRead:0,permissionWrite:0}]);
|
||||
return JSON.stringify({created:true,room_code:code,match_id:matchId});
|
||||
}
|
||||
function rpcJoinPrivateRoom(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("authentication required");
|
||||
var request;
|
||||
try { request = JSON.parse(payload || "{}"); } catch (error) { request = {}; }
|
||||
var code = normalizeRoomCode(request.room_code);
|
||||
if (code.length !== 6) return JSON.stringify({found:false,error:"invalid_room_code"});
|
||||
var record = privateRoomRecord(nk, code);
|
||||
if (!record || !record.value || !record.value.match_id) return JSON.stringify({found:false,error:"room_not_found"});
|
||||
return JSON.stringify({found:true,room_code:code,match_id:String(record.value.match_id)});
|
||||
}
|
||||
|
||||
function matchmakerMatched(ctx, logger, nk, matches) {
|
||||
var difficulty = 1;
|
||||
if (matches.length && matches[0].properties && matches[0].properties.difficulty !== undefined) difficulty = Math.max(0,Math.min(2,Math.floor(Number(matches[0].properties.difficulty))));
|
||||
@@ -54,9 +97,10 @@ function matchmakerMatched(ctx, logger, nk, matches) {
|
||||
function initialState(params) {
|
||||
var difficulty = Math.max(0,Math.min(2,Math.floor(Number(params.difficulty||1))));
|
||||
var credits = DIFFICULTIES[difficulty].startCredits;
|
||||
return {presences:{},slots:{},started:false,gameOver:false,difficulty:difficulty,credits:[credits,credits],reactorLives:30,wave:0,waveActive:false,spawnRemaining:0,spawnTimer:0,nextSpawnRoute:0,nextEnemyId:1,nextTowerId:1,towers:[],enemies:[],lastSnapshotTick:0};
|
||||
return {presences:{},slots:{},started:false,gameOver:false,difficulty:difficulty,mode:String(params.mode||"quick"),roomCode:normalizeRoomCode(params.roomCode),hostUserId:String(params.hostUserId||""),emptyTicks:0,credits:[credits,credits],reactorLives:30,wave:0,waveActive:false,spawnRemaining:0,spawnTimer:0,nextSpawnRoute:0,nextEnemyId:1,nextTowerId:1,towers:[],enemies:[],lastSnapshotTick:0};
|
||||
}
|
||||
function matchInit(ctx,logger,nk,params) { return {state:initialState(params||{}),tickRate:TICK_RATE,label:JSON.stringify({game:"void_bastion",mode:"coop",open:true})}; }
|
||||
function matchLabel(state) { return state.mode === "private" ? state.roomCode : JSON.stringify({game:"void_bastion",mode:state.mode,open:!state.started&&!state.gameOver}); }
|
||||
function matchInit(ctx,logger,nk,params) { var state=initialState(params||{}); return {state:state,tickRate:TICK_RATE,label:matchLabel(state)}; }
|
||||
function matchJoinAttempt(ctx,logger,nk,dispatcher,tick,state,presence,metadata) { return {state:state,accept:Object.keys(state.presences).length<2}; }
|
||||
function firstFreeSlot(state) {
|
||||
var used={},ids=Object.keys(state.slots);
|
||||
@@ -71,13 +115,15 @@ function broadcastEvent(dispatcher,type,data) { var payload=data||{}; payload.ty
|
||||
function matchJoin(ctx,logger,nk,dispatcher,tick,state,presences) {
|
||||
for (var i=0;i<presences.length;i++) { var p=presences[i]; state.presences[p.userId]=p; if (state.slots[p.userId]===undefined) state.slots[p.userId]=firstFreeSlot(state); }
|
||||
state.started=Object.keys(state.presences).length===2;
|
||||
state.emptyTicks=0;
|
||||
dispatcher.matchLabelUpdate(matchLabel(state));
|
||||
broadcastEvent(dispatcher,"players",{count:Object.keys(state.presences).length,started:state.started,slots:state.slots});
|
||||
broadcastSnapshot(dispatcher,state,tick);
|
||||
return {state:state};
|
||||
}
|
||||
function matchLeave(ctx,logger,nk,dispatcher,tick,state,presences) {
|
||||
for (var i=0;i<presences.length;i++) delete state.presences[presences[i].userId];
|
||||
state.started=false; broadcastEvent(dispatcher,"partner_left",{}); broadcastSnapshot(dispatcher,state,tick); return {state:state};
|
||||
state.started=false; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"partner_left",{}); broadcastSnapshot(dispatcher,state,tick); return {state:state};
|
||||
}
|
||||
function parseMessage(nk,message) { try { return JSON.parse(nk.binaryToString(message.data)||"{}"); } catch (error) { return null; } }
|
||||
function towerDamage(t) { return TOWERS[t.type].damage*(1+(t.level-1)*0.52); }
|
||||
@@ -174,18 +220,24 @@ function handleCommand(nk,dispatcher,state,message) {
|
||||
if (!changed) broadcastEvent(dispatcher,"command_rejected",{op_code:message.opCode,player:slot});
|
||||
}
|
||||
function matchLoop(ctx,logger,nk,dispatcher,tick,state,messages) {
|
||||
if (!Object.keys(state.presences).length) {
|
||||
state.emptyTicks += 1;
|
||||
if (state.emptyTicks >= TICK_RATE * 60) { deletePrivateRoomRecord(nk,state.roomCode); return null; }
|
||||
} else state.emptyTicks = 0;
|
||||
for (var i=0;i<messages.length;i++) handleCommand(nk,dispatcher,state,messages[i]);
|
||||
if (state.started&&!state.gameOver) { var delta=1/TICK_RATE; updateWave(state,delta,dispatcher); updateEnemies(state,delta); updateTowers(state,delta); }
|
||||
if (tick-state.lastSnapshotTick>=SNAPSHOT_TICKS||messages.length) broadcastSnapshot(dispatcher,state,tick);
|
||||
return {state:state};
|
||||
}
|
||||
function matchTerminate(ctx,logger,nk,dispatcher,tick,state,graceSeconds) { broadcastEvent(dispatcher,"server_shutdown",{grace:graceSeconds}); return {state:state}; }
|
||||
function matchTerminate(ctx,logger,nk,dispatcher,tick,state,graceSeconds) { deletePrivateRoomRecord(nk,state.roomCode); broadcastEvent(dispatcher,"server_shutdown",{grace:graceSeconds}); return {state:state}; }
|
||||
function matchSignal(ctx,logger,nk,dispatcher,tick,state,data) { return {state:state,data:JSON.stringify(snapshot(state,tick))}; }
|
||||
|
||||
function InitModule(ctx,logger,nk,initializer) {
|
||||
for (var mode=0;mode<VALID_MODES.length;mode++) for (var map=0;map<MAP_COUNT;map++) for (var difficulty=0;difficulty<DIFFICULTY_COUNT;difficulty++) nk.leaderboardCreate(leaderboardId(map,difficulty,VALID_MODES[mode]),true,"desc","best",null,{game:"void_bastion"});
|
||||
initializer.registerRpc("server_info",rpcServerInfo);
|
||||
initializer.registerRpc("submit_run",rpcSubmitRun);
|
||||
initializer.registerRpc("create_private_room",rpcCreatePrivateRoom);
|
||||
initializer.registerRpc("join_private_room",rpcJoinPrivateRoom);
|
||||
initializer.registerMatchmakerMatched(matchmakerMatched);
|
||||
initializer.registerMatch(MATCH_MODULE,{matchInit:matchInit,matchJoinAttempt:matchJoinAttempt,matchJoin:matchJoin,matchLeave:matchLeave,matchLoop:matchLoop,matchTerminate:matchTerminate,matchSignal:matchSignal});
|
||||
logger.info("Void Bastion online runtime %s loaded",GAME_VERSION);
|
||||
|
||||
@@ -12,6 +12,11 @@ const sent = [];
|
||||
const dispatcher = {
|
||||
broadcastMessage(opCode, data) {
|
||||
sent.push({ opCode, data: JSON.parse(data) });
|
||||
},
|
||||
matchLabelUpdate(label) {
|
||||
let data = label;
|
||||
try { data = JSON.parse(label); } catch (error) {}
|
||||
sent.push({ opCode: "label", data });
|
||||
}
|
||||
};
|
||||
const nk = { binaryToString: data => Buffer.from(data).toString("utf8") };
|
||||
@@ -25,6 +30,24 @@ state = context.matchJoin({}, logger, nk, dispatcher, 2, state, [presenceB]).sta
|
||||
assert.strictEqual(state.started, true);
|
||||
assert.strictEqual(state.slots["pilot-a"], 0);
|
||||
assert.strictEqual(state.slots["pilot-b"], 1);
|
||||
assert.ok(sent.some(entry => entry.opCode === "label" && entry.data.open === false));
|
||||
assert.strictEqual(context.normalizeRoomCode(" ab-c12! "), "ABC12");
|
||||
|
||||
const privateMatch = context.matchInit({}, logger, nk, { difficulty: 2, mode: "private", roomCode: "AB23CD", hostUserId: "pilot-a" });
|
||||
assert.strictEqual(privateMatch.label, "AB23CD");
|
||||
assert.strictEqual(privateMatch.state.difficulty, 2);
|
||||
|
||||
const roomStorage = new Map();
|
||||
const nkRooms = {
|
||||
storageRead(ops) { return ops.map(op => roomStorage.get(op.key)).filter(Boolean); },
|
||||
storageWrite(ops) { for (const op of ops) roomStorage.set(op.key, { value: op.value }); },
|
||||
storageDelete(ops) { for (const op of ops) roomStorage.delete(op.key); },
|
||||
matchCreate() { return "private-match-id"; }
|
||||
};
|
||||
const createdRoom = JSON.parse(context.rpcCreatePrivateRoom({ userId: "pilot-a" }, logger, nkRooms, JSON.stringify({ difficulty: 2 })));
|
||||
assert.strictEqual(createdRoom.room_code.length, 6);
|
||||
const joinedRoom = JSON.parse(context.rpcJoinPrivateRoom({ userId: "pilot-b" }, logger, nkRooms, JSON.stringify({ room_code: createdRoom.room_code.toLowerCase() })));
|
||||
assert.strictEqual(joinedRoom.match_id, "private-match-id");
|
||||
|
||||
function message(sender, opCode, payload) {
|
||||
return { sender, opCode, data: Buffer.from(JSON.stringify(payload)) };
|
||||
|
||||
@@ -34,7 +34,7 @@ func _run() -> void:
|
||||
assert(info_data is Dictionary and info_data.get("status") == "online", "Der Server muss seinen Onlinestatus melden.")
|
||||
assert(info_data.get("version") == "0.20", "Der Server muss die aktuelle Runtime 0.20 geladen haben: %s" % info_data)
|
||||
|
||||
var run_payload := JSON.stringify({"map": 1, "difficulty": 1, "mode": "mission", "wave": 4, "score": 4700, "client_version": "0.19-integration"})
|
||||
var run_payload := JSON.stringify({"map": 1, "difficulty": 1, "mode": "mission", "wave": 4, "score": 4700, "client_version": "0.20-integration"})
|
||||
var submitted = await client_a.rpc_async(session_a, "submit_run", run_payload)
|
||||
assert(not submitted.is_exception(), "Ein plausibles Ergebnis muss serverseitig angenommen werden.")
|
||||
var records = await client_a.list_leaderboard_records_async(session_a, "mission_map1_difficulty1", null, null, 10)
|
||||
@@ -83,6 +83,33 @@ func _run() -> void:
|
||||
assert((snapshot_a.get("towers", []) as Array).size() == 2, "Beide autorisierten Bauaufträge müssen serverseitig sichtbar sein.")
|
||||
assert(JSON.stringify(snapshot_a.get("towers", [])) == JSON.stringify(snapshot_b.get("towers", [])), "Beide Clients müssen denselben Turmzustand erhalten.")
|
||||
assert(int(snapshot_a.get("reactor_lives", 0)) == int(snapshot_b.get("reactor_lives", -1)), "Beide Clients müssen dieselben gemeinsamen Reaktorleben sehen.")
|
||||
assert(not (await socket_a.leave_match_async(match_a.match_id)).is_exception(), "Spieler A muss die schnelle Partie verlassen können.")
|
||||
assert(not (await socket_b.leave_match_async(match_b.match_id)).is_exception(), "Spieler B muss die schnelle Partie verlassen können.")
|
||||
await create_timer(0.2).timeout
|
||||
snapshot_a = {}
|
||||
snapshot_b = {}
|
||||
var created_room = await client_a.rpc_async(session_a, "create_private_room", JSON.stringify({"difficulty": 2}))
|
||||
assert(not created_room.is_exception(), "Spieler A muss einen privaten Raum erstellen können.")
|
||||
var created_data = JSON.parse_string(created_room.payload)
|
||||
assert(created_data is Dictionary, "Der private Raum muss gültige Daten liefern.")
|
||||
var room_code := String(created_data.get("room_code", ""))
|
||||
var private_match_id := String(created_data.get("match_id", ""))
|
||||
assert(room_code.length() == 6 and not private_match_id.is_empty(), "Der Server muss einen sechsstelligen Raumcode und eine Partie liefern.")
|
||||
var private_match_a = await socket_a.join_match_async(private_match_id)
|
||||
assert(not private_match_a.is_exception(), "Der Gastgeber muss seinem privaten Raum beitreten können.")
|
||||
var found_room = await client_b.rpc_async(session_b, "join_private_room", JSON.stringify({"room_code": room_code.to_lower()}))
|
||||
assert(not found_room.is_exception(), "Der Mitspieler muss den Raumcode unabhängig von Groß-/Kleinschreibung finden können.")
|
||||
var found_data = JSON.parse_string(found_room.payload)
|
||||
assert(found_data is Dictionary and String(found_data.get("match_id", "")) == private_match_id, "Der Raumcode muss genau zur erstellten Partie führen: %s" % found_data)
|
||||
var private_match_b = await socket_b.join_match_async(private_match_id)
|
||||
assert(not private_match_b.is_exception(), "Der Mitspieler muss dem privaten Raum beitreten können.")
|
||||
for attempt in 100:
|
||||
if bool(snapshot_a.get("started", false)) and bool(snapshot_b.get("started", false)):
|
||||
break
|
||||
await create_timer(0.05).timeout
|
||||
assert(bool(snapshot_a.get("started", false)) and bool(snapshot_b.get("started", false)), "Der private Raum muss mit zwei Spielern die gemeinsame Partie starten.")
|
||||
assert(int(snapshot_a.get("difficulty", -1)) == 2 and int(snapshot_b.get("difficulty", -1)) == 2, "Die private Partie muss den Schwierigkeitsgrad des Gastgebers übernehmen.")
|
||||
assert(private_match_a.match_id == private_match_b.match_id, "Beide Spieler müssen über den Raumcode dieselbe private Partie erhalten.")
|
||||
socket_a.close()
|
||||
socket_b.close()
|
||||
print("VOID_BASTION_ONLINE_INTEGRATION_OK")
|
||||
|
||||
Reference in new issue
Block a user