This commit is contained in:
team3
2026-09-07 17:38:17 +02:00
parent 6478559252
commit 2a9ab4c7da
11 changed files with 521 additions and 125 deletions

View File

@@ -11,7 +11,7 @@ var hud: CanvasLayer
var rng := RandomNumberGenerator.new()
var seats: Array = [] ## [{peer, name, ai, count, alive}]
var local_seat := 0
var local_seat := 0 ## -1 auf dem Spielserver: der sitzt nicht mit
var host_values: Array = [] ## nur Host: seat -> Array[int]
var my_values: Array = [] ## Client-Sicht (-1 = vernebelt)
@@ -54,7 +54,9 @@ func setup(cfg: Dictionary) -> void:
seats.append({"peer": 0, "name": "Bot %d" % (i + 1), "ai": true,
"count": START_DICE, "alive": true})
# Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz.
var me := multiplayer.get_unique_id()
local_seat = -1
for i in seats.size():
if seats[i].peer == me:
local_seat = i
@@ -63,13 +65,15 @@ func setup(cfg: Dictionary) -> void:
add_child(table)
table.build(seats.size(), local_seat)
hud = preload("res://scripts/hud.gd").new()
add_child(hud)
hud.bid_pressed.connect(_on_bid_pressed)
hud.challenge_pressed.connect(_on_challenge_pressed)
hud.booster_chosen.connect(_on_booster_chosen)
hud.quit_pressed.connect(_on_quit)
hud.pause_changed.connect(_on_pause_changed)
# Ohne Platz am Tisch gibt es nichts anzuzeigen und nichts zu bedienen.
if local_seat >= 0:
hud = preload("res://scripts/hud.gd").new()
add_child(hud)
hud.bid_pressed.connect(_on_bid_pressed)
hud.challenge_pressed.connect(_on_challenge_pressed)
hud.booster_chosen.connect(_on_booster_chosen)
hud.quit_pressed.connect(_on_quit)
hud.pause_changed.connect(_on_pause_changed)
for i in seats.size():
table.set_dice_count(i, START_DICE)
@@ -735,7 +739,7 @@ func _apply_state(st: Dictionary) -> void:
## Der Nebel-Debuff muss auch den Würfel auf dem Tisch verdecken.
func _apply_fog_to_table() -> void:
if phase == "roll" or local_seat >= table.dice.size():
if phase == "roll" or local_seat < 0 or local_seat >= table.dice.size():
return
var dl: Array = table.dice_of(local_seat)
for i in mini(dl.size(), my_values.size()):

View File

@@ -1,16 +1,26 @@
extends Node
## Wechselt zwischen Menü und laufender Partie.
##
## Mit `-- server <port>` läuft dieselbe Datei als Raum auf dem Spielserver:
## kein Menü, kein Bild, nur die Spiellogik.
var menu: CanvasLayer
var game: Node3D
func _ready() -> void:
menu = preload("res://scripts/menu.gd").new()
add_child(menu)
Net.game_started.connect(_start_game)
Net.server_closed.connect(_back_to_menu)
var args := OS.get_cmdline_user_args()
if args.size() >= 2 and args[0] == "server":
if not Net.serve(int(args[1])):
get_tree().quit(1)
return
menu = preload("res://scripts/menu.gd").new()
add_child(menu)
## Das Spiel startet im Vollbild; F11 schaltet um.
func _unhandled_key_input(event: InputEvent) -> void:

View File

@@ -1,23 +1,25 @@
extends CanvasLayer
## Startmenü: Einzelspieler, Mehrspieler (Host/Client mit Lobby), Beenden.
## Startmenü: Einzelspieler, Mehrspieler (hosten oder per Code beitreten),
## Lobby und Beenden.
const BOT_NAMES_MAX := 7
var screens: Dictionary = {}
var status_labels: Dictionary = {}
var current := "main"
# Einzelspieler
var solo_bots: NumberRow
# Mehrspieler
var name_edit: LineEdit
var ip_edit: LineEdit
var multi_bots: NumberRow
var code_edit: LineEdit
# Lobby
var lobby_bots: NumberRow
var code_label: Label
var lobby_list: Label
var status: Label
var host_btn: Button
var join_btn: Button
var start_btn: Button
var leave_btn: Button
func _ready() -> void:
@@ -33,6 +35,8 @@ func _ready() -> void:
screens["main"] = _screen(root, _build_main())
screens["single"] = _screen(root, _build_single())
screens["multi"] = _screen(root, _build_multi())
screens["join"] = _screen(root, _build_join())
screens["lobby"] = _screen(root, _build_lobby())
Net.lobby_changed.connect(_refresh)
Net.connection_failed.connect(_on_failed)
@@ -51,12 +55,20 @@ func _screen(root: Control, content: Control) -> CenterContainer:
func _show(which: String) -> void:
current = which
for key: String in screens:
screens[key].visible = key == which
if which == "multi":
_sagen("")
if which == "lobby":
_refresh()
## Meldungen gehören auf den Bildschirm, auf dem man gerade steht.
func _sagen(text: String) -> void:
for key: String in status_labels:
status_labels[key].text = text if key == current else ""
func _box(width: int) -> VBoxContainer:
var v := VBoxContainer.new()
v.custom_minimum_size = Vector2(width, 0)
@@ -94,6 +106,13 @@ func _button(text: String, parent: Control, action: Callable) -> Button:
return b
## Eine Zeile für Meldungen, unten auf jedem Bildschirm.
func _status_row(box: VBoxContainer, key: String) -> void:
var l := _text("", 14, Color(0.95, 0.55, 0.5))
box.add_child(l)
status_labels[key] = l
## Eine Reihe anklickbarer Zahlen - bei so wenigen Werten schneller als eine
## Spinbox mit ihren winzigen Pfeilen.
class NumberRow extends HBoxContainer:
@@ -120,17 +139,14 @@ class NumberRow extends HBoxContainer:
for i in _buttons.size():
_buttons[i].button_pressed = _lo + i == n
## Werte oberhalb von `hi` sperren (im Mehrspieler sind die Plätze begrenzt).
func set_limit(hi: int) -> void:
## Obergrenze und Bedienbarkeit in einem Zug - getrennt würde der eine
## Aufruf den anderen wieder aufheben.
func anpassen(hi: int, on: bool) -> void:
for i in _buttons.size():
_buttons[i].disabled = _lo + i > hi
_buttons[i].disabled = not on or _lo + i > hi
if value > hi:
pick(maxi(_lo, hi))
func set_enabled(on: bool) -> void:
for b in _buttons:
b.disabled = not on
func _title(box: VBoxContainer) -> void:
box.add_child(_text("LIAR'S DICE 3D", 40))
@@ -162,6 +178,7 @@ func _build_single() -> Control:
return box
## Erster Schritt: nur der Name, dann hosten oder beitreten.
func _build_multi() -> Control:
var box := _box(440)
_title(box)
@@ -172,34 +189,68 @@ func _build_multi() -> Control:
name_edit.placeholder_text = "Dein Name"
box.add_child(_row("Name", name_edit))
ip_edit = LineEdit.new()
ip_edit.text = "127.0.0.1"
box.add_child(_row("Server-IP", ip_edit))
box.add_child(_text("Bots (nur Host)", 15))
multi_bots = NumberRow.new(0, BOT_NAMES_MAX, 1)
box.add_child(multi_bots)
var brow := HBoxContainer.new()
brow.add_theme_constant_override("separation", 8)
box.add_child(brow)
host_btn = _button("Spiel hosten", brow, _on_host)
join_btn = _button("Beitreten", brow, _on_join)
_button("Hosten", brow, _on_host)
_button("Beitreten", brow, _show.bind("join"))
_button("Zurück", box, _show.bind("main"))
_status_row(box, "multi")
return box
## Zweiter Schritt beim Beitreten: der Code vom Host.
func _build_join() -> Control:
var box := _box(440)
_title(box)
box.add_child(_text("Beitreten", 22, Color(1.0, 0.82, 0.32)))
box.add_child(_text("Den Code bekommst du vom Host.", 15,
Color(0.65, 0.67, 0.72)))
code_edit = LineEdit.new()
code_edit.placeholder_text = "z. B. K7QM"
code_edit.max_length = 6
code_edit.alignment = HORIZONTAL_ALIGNMENT_CENTER
code_edit.add_theme_font_size_override("font_size", 24)
code_edit.text_submitted.connect(func(_t: String) -> void: _on_join())
box.add_child(code_edit)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
box.add_child(row)
_button("Beitreten", row, _on_join)
_button("Zurück", row, _show.bind("multi"))
_status_row(box, "join")
return box
## Dritter Schritt: warten, bis alle da sind.
func _build_lobby() -> Control:
var box := _box(440)
_title(box)
box.add_child(_text("Lobby", 22, Color(1.0, 0.82, 0.32)))
code_label = _text("", 34, Color(1.0, 0.82, 0.32))
box.add_child(code_label)
box.add_child(_text("Diesen Code an die Mitspieler geben.", 14,
Color(0.65, 0.67, 0.72)))
box.add_child(HSeparator.new())
box.add_child(_text("Lobby", 18, Color(1.0, 0.82, 0.32)))
lobby_list = _text("-", 15, Color(0.85, 0.87, 0.92))
lobby_list = _text("-", 16, Color(0.85, 0.87, 0.92))
box.add_child(lobby_list)
box.add_child(HSeparator.new())
var arow := HBoxContainer.new()
arow.add_theme_constant_override("separation", 8)
box.add_child(arow)
start_btn = _button("Spiel starten", arow, _on_start)
leave_btn = _button("Trennen", arow, _on_leave)
box.add_child(_text("Bots", 15))
lobby_bots = NumberRow.new(0, BOT_NAMES_MAX, 1)
box.add_child(lobby_bots)
_button("Zurück", box, _on_back)
status = _text("", 14, Color(0.95, 0.55, 0.5))
box.add_child(status)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
box.add_child(row)
start_btn = _button("Spiel starten", row, _on_start)
_button("Verlassen", row, _on_leave)
_status_row(box, "lobby")
return box
@@ -216,66 +267,57 @@ func _spielername() -> String:
func _on_host() -> void:
status.text = ""
if Net.host_game(_spielername()):
status.text = "Server läuft auf Port %d. Warte auf Mitspieler." % Net.PORT
_refresh()
_sagen("Raum wird angelegt ...")
Net.create_room(_spielername())
func _on_join() -> void:
status.text = "Verbinde..."
Net.join_game(ip_edit.text.strip_edges(), _spielername())
_refresh()
_sagen("Verbinde ...")
Net.join_room(code_edit.text, _spielername())
func _on_leave() -> void:
Net.leave()
status.text = ""
func _on_back() -> void:
if Net.is_online():
Net.leave()
status.text = ""
_show("main")
_show("multi")
func _on_start() -> void:
var humans: int = Net.lobby.size()
var ai: int = mini(multi_bots.value, Net.MAX_HUMANS - humans)
var ai: int = mini(lobby_bots.value, Net.MAX_HUMANS - humans)
if humans + ai < 2:
status.text = "Mindestens 2 Spieler nötig (Mitspieler oder Bots)."
_sagen("Mindestens 2 Spieler nötig (Mitspieler oder Bots).")
return
Net.start_game(ai)
Net.request_start(ai)
func _on_failed(msg: String) -> void:
status.text = msg
_refresh()
_sagen(msg)
func _on_closed() -> void:
status.text = "Server hat die Verbindung beendet."
_refresh()
_show("multi")
_sagen("Der Raum wurde geschlossen.")
## Sobald der Server uns kennt, sind wir in der Lobby.
func _refresh() -> void:
if status == null:
if lobby_list == null:
return
var connected := Net.is_online()
host_btn.disabled = connected
join_btn.disabled = connected
leave_btn.disabled = not connected
start_btn.disabled = not (connected and Net.is_host)
if not connected or not Net.is_host:
multi_bots.set_enabled(not connected)
else:
multi_bots.set_enabled(true)
multi_bots.set_limit(Net.MAX_HUMANS - Net.lobby.size())
if Net.lobby.is_empty():
lobby_list.text = "- nicht verbunden -"
else:
var names: Array = []
for id: int in Net.lobby:
names.append("%s%s" % [Net.lobby[id], " (Host)" if id == 1 else ""])
lobby_list.text = "\n".join(names)
var drin := Net.is_online() and Net.lobby.has(multiplayer.get_unique_id())
if drin and current != "lobby":
_show("lobby")
elif not drin and current == "lobby":
_show("multi")
if current != "lobby":
return
code_label.text = Net.room_code
start_btn.disabled = not Net.is_owner
lobby_bots.anpassen(Net.MAX_HUMANS - Net.lobby.size(), Net.is_owner)
var names: Array = []
for id: int in Net.lobby:
names.append("%s%s" % [Net.lobby[id], " (Host)" if id == Net.owner_id else ""])
lobby_list.text = "\n".join(names)
if not Net.is_owner:
_sagen("Der Host startet die Partie.")

View File

@@ -1,19 +1,33 @@
extends Node
## Netzwerk-Verwaltung: Host/Client via ENet, Lobby und Spielstart.
## Netzwerk: Räume liegen auf dem Spielserver, Beitritt läuft über einen Code.
##
## Der Server ist eine eigene headless Instanz und sitzt nicht mit am Tisch.
## Er ist Peer 1 und damit die Autorität. Ein Spieler im Raum ist Eigentümer
## und darf die Partie starten.
signal lobby_changed
signal game_started(config: Dictionary)
signal connection_failed(reason: String)
signal server_closed
signal room_opened(code: String)
const PORT := 8910
const MAX_HUMANS := 8
const STANDARD_SERVER := "https://pirate-dice.marha.de"
## Ohne Spieler schliesst sich ein Raum von selbst, sonst stehen Leichen herum
const LEERLAUF := 120.0
var player_name := "Spieler"
## peer_id -> name (enthält auch den Host mit id 1)
## peer_id -> Name. Nur Menschen; der Server steht nicht drin.
var lobby: Dictionary = {}
var is_host := false
var owner_id := 0
var is_owner := false
var room_code := ""
var in_game := false
## true in der headless Instanz auf dem Server
var dedicated := false
var _leer_seit := 0.0
func _ready() -> void:
@@ -24,29 +38,77 @@ func _ready() -> void:
multiplayer.server_disconnected.connect(_on_server_disconnected)
func host_game(pname: String, port: int = PORT) -> bool:
player_name = pname
## Zum Entwickeln auf einen lokalen Matchmaker zeigen lassen:
## PIRATE_DICE_SERVER=http://127.0.0.1:8020 godot
func server_url() -> String:
var eigen := OS.get_environment("PIRATE_DICE_SERVER")
return eigen if eigen != "" else STANDARD_SERVER
# ---------------------------------------------------------------- Räume
## Neuen Raum anlegen lassen und gleich betreten.
func create_room(pname: String) -> void:
var raum := await _api(HTTPClient.METHOD_POST, "/api/raum")
if raum.is_empty():
connection_failed.emit("Spielserver nicht erreichbar.")
return
room_code = String(raum.get("code", ""))
room_opened.emit(room_code)
join_game(String(raum.get("host", "")), pname, int(raum.get("port", PORT)))
func join_room(code: String, pname: String) -> void:
code = code.strip_edges().to_upper()
if code == "":
connection_failed.emit("Kein Code eingegeben.")
return
var raum := await _api(HTTPClient.METHOD_GET, "/api/raum/" + code)
if raum.is_empty():
connection_failed.emit("Den Code %s gibt es nicht." % code)
return
room_code = code
join_game(String(raum.get("host", "")), pname, int(raum.get("port", PORT)))
func _api(method: int, pfad: String) -> Dictionary:
var req := HTTPRequest.new()
add_child(req)
req.timeout = 8.0
if req.request(server_url() + pfad, PackedStringArray(), method) != OK:
req.queue_free()
return {}
var antwort: Array = await req.request_completed
req.queue_free()
if int(antwort[1]) != 200:
return {}
var daten: Variant = JSON.parse_string(
(antwort[3] as PackedByteArray).get_string_from_utf8())
return daten if daten is Dictionary else {}
# ---------------------------------------------------------------- Verbindung
## Nur die headless Instanz auf dem Server ruft das auf.
func serve(port: int) -> bool:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_server(port, MAX_HUMANS - 1)
if err != OK:
connection_failed.emit("Server konnte nicht gestartet werden (Port %d belegt?)" % port)
if peer.create_server(port, MAX_HUMANS) != OK:
push_error("Port %d ist belegt." % port)
return false
multiplayer.multiplayer_peer = peer
is_host = true
lobby = {1: pname}
lobby_changed.emit()
dedicated = true
_leer_seit = _jetzt()
print("Raum offen auf Port %d." % port)
return true
func join_game(ip: String, pname: String, port: int = PORT) -> bool:
func join_game(host: String, pname: String, port: int = PORT) -> bool:
player_name = pname
var peer := ENetMultiplayerPeer.new()
var err := peer.create_client(ip, port)
if err != OK:
connection_failed.emit("Verbindung zu %s:%d fehlgeschlagen" % [ip, port])
if peer.create_client(host, port) != OK:
connection_failed.emit("Verbindung zu %s:%d fehlgeschlagen." % [host, port])
return false
multiplayer.multiplayer_peer = peer
is_host = false
return true
@@ -55,7 +117,9 @@ func leave() -> void:
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
lobby.clear()
is_host = false
owner_id = 0
is_owner = false
room_code = ""
in_game = false
lobby_changed.emit()
@@ -66,16 +130,36 @@ func is_online() -> bool:
and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED
## Ein leerer Raum kostet sonst dauerhaft Speicher auf dem Server.
func _process(_delta: float) -> void:
if not dedicated:
return
if lobby.is_empty():
if _jetzt() - _leer_seit > LEERLAUF:
print("Niemand mehr im Raum, wird geschlossen.")
get_tree().quit()
else:
_leer_seit = _jetzt()
func _jetzt() -> float:
return Time.get_ticks_msec() / 1000.0
func _on_peer_connected(id: int) -> void:
if is_host:
_push_lobby.rpc_id(id, lobby)
if multiplayer.is_server():
_push_lobby.rpc_id(id, lobby, owner_id)
func _on_peer_disconnected(id: int) -> void:
if is_host:
lobby.erase(id)
_push_lobby.rpc(lobby)
lobby_changed.emit()
if not multiplayer.is_server():
return
lobby.erase(id)
# Geht der Eigentümer, rückt der nächste nach - sonst startet niemand mehr
if id == owner_id:
owner_id = 0 if lobby.is_empty() else int(lobby.keys()[0])
_push_lobby.rpc(lobby, owner_id)
lobby_changed.emit()
func _on_connected() -> void:
@@ -84,50 +168,94 @@ func _on_connected() -> void:
func _on_connect_fail() -> void:
multiplayer.multiplayer_peer = null
connection_failed.emit("Server nicht erreichbar.")
connection_failed.emit("Raum nicht erreichbar.")
func _on_server_disconnected() -> void:
multiplayer.multiplayer_peer = null
lobby.clear()
owner_id = 0
is_owner = false
in_game = false
server_closed.emit()
@rpc("any_peer", "reliable")
func _register(pname: String) -> void:
if not is_host:
if not multiplayer.is_server():
return
var id := multiplayer.get_remote_sender_id()
if in_game:
_abweisen(id, "In dem Raum läuft schon eine Partie.")
return
if lobby.size() >= MAX_HUMANS:
_abweisen(id, "Der Raum ist voll.")
return
lobby[id] = pname.substr(0, 16)
_push_lobby.rpc(lobby)
if owner_id == 0:
owner_id = id
_push_lobby.rpc(lobby, owner_id)
lobby_changed.emit()
## Erst die Begründung schicken, dann trennen - andersherum kommt sie nie an.
func _abweisen(id: int, grund: String) -> void:
_abgewiesen.rpc_id(id, grund)
await get_tree().create_timer(0.5).timeout
var peer := multiplayer.multiplayer_peer
if peer is ENetMultiplayerPeer:
peer.disconnect_peer(id)
@rpc("authority", "reliable")
func _push_lobby(data: Dictionary) -> void:
func _abgewiesen(grund: String) -> void:
connection_failed.emit(grund)
leave()
@rpc("authority", "reliable")
func _push_lobby(data: Dictionary, besitzer: int) -> void:
lobby = data
owner_id = besitzer
is_owner = besitzer == multiplayer.get_unique_id()
lobby_changed.emit()
# ---------------------------------------------------------------- Start
## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots.
func start_singleplayer(pname: String, ai_count: int) -> void:
player_name = pname
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
is_host = true
lobby = {1: pname}
owner_id = 1
is_owner = true
lobby_changed.emit()
_begin({"peers": [1], "names": lobby.duplicate(), "ai": ai_count})
## Nur der Host ruft das auf.
## Der Eigentümer bittet den Server um den Start.
func request_start(ai_count: int) -> void:
_req_start.rpc_id(1, ai_count)
@rpc("any_peer", "reliable")
func _req_start(ai_count: int) -> void:
if not multiplayer.is_server():
return
if multiplayer.get_remote_sender_id() != owner_id:
return
start_game(ai_count)
func start_game(ai_count: int) -> void:
if not is_host:
if not multiplayer.is_server() or in_game:
return
var order: Array = lobby.keys()
order.sort()
var cfg := {"peers": order, "names": lobby.duplicate(), "ai": ai_count}
_begin.rpc(cfg)
if order.size() + ai_count < 2:
return
_begin.rpc({"peers": order, "names": lobby.duplicate(), "ai": ai_count})
@rpc("authority", "call_local", "reliable")