Kampf fortsetzen, Gamepad im Deckbau, Deck-Codes, Kapitel 3, Elementbooster, englische Karten, Host-Wechsel und Zuschauer
- KI-Bewertung mit Bedrohung und Führenden-Bremse (62:38 gegen die alte im Duell, gleichauf zu viert) - Balance: Sand +1/+1 statt Schild, Erde+Luft und Erde geschwächt, Wasser angehoben, Messlauf mit N=200 möglich - Einzelspieler-Kämpfe werden laufend gespeichert (voller Spielstand, viewer -1) und im Menü fortgesetzt - Karten, Deckzeilen und Kartenrücken fokussierbar: Gamepad in Deckbau, Werkstatt und Booster - Kartennamen (name_en) und Kartentexte übersetzbar, Übersetzungstest deckt cards.gd mit ab - Deck-Code DINGER1:id*anzahl mit Zwischenablage; acht neue Karten; Kampagne Kapitel 3; Elementbooster 120 Münzen - Netzwerk: Nachfolge-Host mit vollem Spielstand, Neuverbindung der anderen, Zuschauer (Sicht -2); Bot kann hosten - Große Schrift: Kartennamen, Kartentext, Knopfleisten, Gegnervorschau, Dialogtext - Anleitung 1 garantiert ein 1-Energie-Wesen; Tests: DINGER_ONLY für einzelne Oberflächentests - Oberflächentest auch auf Xephyr mit llvmpipe (wie Xvfb in der CI) grün Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
171
scripts/net.gd
171
scripts/net.gd
@@ -8,6 +8,8 @@ signal started(seat: int)
|
||||
signal snapshot_received(snap: Dictionary)
|
||||
signal action_applied
|
||||
signal ended(reason: String)
|
||||
signal migrated # nach einem Host-Ausfall: dieser Rechner führt das Spiel jetzt
|
||||
signal status_changed
|
||||
|
||||
const PORT := 24455
|
||||
|
||||
@@ -23,15 +25,25 @@ var my_seat := -1
|
||||
var game: Game # Host
|
||||
var status := ""
|
||||
var waiting := {} # Name -> {"seat": int, "since": float}: Platz nach Verbindungsabbruch freihalten
|
||||
const REJOIN_SECONDS := 60.0
|
||||
var rejoin_seconds := 60.0
|
||||
var spectator := false # eigene Rolle: nur zuschauen
|
||||
var spectators := {} # Host: peer_id -> Name
|
||||
var successor_ip := "" # Mitspieler: Adresse des Nachfolge-Hosts
|
||||
var i_am_successor := false
|
||||
var backup := {} # Nachfolger: voller Spielstand nach jeder Aktion
|
||||
var backup_names := {} # Nachfolger: Platz -> Name aller Menschen
|
||||
var last_winner := "" # Mitspieler: Sieger laut letztem Zustandsbild
|
||||
var reconnecting := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(_on_connected)
|
||||
multiplayer.connection_failed.connect(func(): _fail("Verbindung fehlgeschlagen"))
|
||||
multiplayer.server_disconnected.connect(func(): _fail("Host hat das Spiel beendet"))
|
||||
multiplayer.connection_failed.connect(func():
|
||||
if not reconnecting:
|
||||
_fail("Verbindung fehlgeschlagen"))
|
||||
multiplayer.server_disconnected.connect(_on_server_lost)
|
||||
|
||||
|
||||
func host(port: int = PORT) -> Error:
|
||||
@@ -45,6 +57,7 @@ func host(port: int = PORT) -> Error:
|
||||
connected = true
|
||||
names = {1: my_name}
|
||||
decks = {1: my_deck}
|
||||
spectators = {}
|
||||
status = "Warte auf Mitspieler …"
|
||||
lobby_changed.emit()
|
||||
return OK
|
||||
@@ -76,6 +89,13 @@ func leave() -> void:
|
||||
my_seat = -1
|
||||
game = null
|
||||
status = ""
|
||||
spectator = false
|
||||
spectators = {}
|
||||
successor_ip = ""
|
||||
i_am_successor = false
|
||||
backup = {}
|
||||
backup_names = {}
|
||||
last_winner = ""
|
||||
|
||||
|
||||
func local_addresses() -> Array:
|
||||
@@ -91,7 +111,114 @@ func _fail(reason: String) -> void:
|
||||
func _on_connected() -> void:
|
||||
connected = true
|
||||
status = "Verbunden"
|
||||
register.rpc_id(1, my_name, my_deck)
|
||||
register.rpc_id(1, my_name, my_deck, spectator)
|
||||
|
||||
|
||||
# --- Host-Wechsel ---------------------------------------------------------------
|
||||
# Der Host schickt dem Mitspieler mit der kleinsten Kennung nach jeder Aktion den vollen
|
||||
# Spielstand und allen die Adresse dieses Nachfolgers. Fällt der Host aus, eröffnet der
|
||||
# Nachfolger das Spiel neu, die anderen verbinden sich mit ihm und kehren auf ihre Plätze zurück.
|
||||
|
||||
func _successor_id() -> int:
|
||||
var ids: Array = seats.keys().filter(func(id): return id != 1)
|
||||
ids.sort()
|
||||
return ids[0] if not ids.is_empty() else -1
|
||||
|
||||
|
||||
func _update_successor() -> void:
|
||||
if not is_host or game == null:
|
||||
return
|
||||
var succ := _successor_id()
|
||||
var ip := ""
|
||||
if succ > 0 and peer != null and peer.get_peer(succ) != null:
|
||||
ip = peer.get_peer(succ).get_remote_address()
|
||||
for id in seats:
|
||||
if id != 1:
|
||||
set_successor.rpc_id(id, ip, id == succ)
|
||||
|
||||
|
||||
## Platz -> Name aller Menschen (auch der gerade fehlenden), für die Rückkehr nach dem Wechsel.
|
||||
func _seat_names() -> Dictionary:
|
||||
var out := {}
|
||||
for id in seats:
|
||||
out[seats[id]] = names.get(id, "")
|
||||
for pname in waiting:
|
||||
out[waiting[pname].seat] = pname
|
||||
return out
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func set_successor(ip: String, me: bool) -> void:
|
||||
successor_ip = ip
|
||||
i_am_successor = me
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func receive_backup(snap: Dictionary, seat_names: Dictionary) -> void:
|
||||
backup = snap
|
||||
backup_names = seat_names
|
||||
|
||||
|
||||
func _on_server_lost() -> void:
|
||||
if is_host:
|
||||
return
|
||||
if last_winner != "":
|
||||
_fail("Host hat das Spiel beendet")
|
||||
elif i_am_successor and not backup.is_empty():
|
||||
_become_host()
|
||||
elif successor_ip != "":
|
||||
status = "Host weg, verbinde mit dem Nachfolger …"
|
||||
status_changed.emit()
|
||||
_reconnect(successor_ip)
|
||||
else:
|
||||
_fail("Host hat das Spiel beendet")
|
||||
|
||||
|
||||
func _become_host() -> void:
|
||||
var snap := backup
|
||||
var seat_names := backup_names
|
||||
var seat := my_seat
|
||||
if multiplayer.multiplayer_peer != null:
|
||||
multiplayer.multiplayer_peer.close()
|
||||
var err := FAILED
|
||||
for attempt in 5:
|
||||
err = host()
|
||||
if err == OK:
|
||||
break
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
if err != OK:
|
||||
_fail("Host weg, Übernahme fehlgeschlagen")
|
||||
return
|
||||
game = Game.from_snapshot(snap)
|
||||
seats = {1: seat}
|
||||
my_seat = seat
|
||||
waiting = {}
|
||||
for s in seat_names:
|
||||
if int(s) != seat:
|
||||
waiting[seat_names[s]] = {"seat": int(s), "since": Time.get_unix_time_from_system()}
|
||||
game.say("%s übernimmt als Host." % my_name)
|
||||
status = "Du bist jetzt Host, die anderen verbinden neu …"
|
||||
successor_ip = ""
|
||||
i_am_successor = false
|
||||
backup = {}
|
||||
migrated.emit()
|
||||
|
||||
|
||||
func _reconnect(ip: String) -> void:
|
||||
reconnecting = true
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
for attempt in 5:
|
||||
if join(ip) == OK:
|
||||
var t := 0.0
|
||||
while not connected and t < 4.0 and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING:
|
||||
await get_tree().process_frame
|
||||
t += get_process_delta_time()
|
||||
if connected:
|
||||
reconnecting = false
|
||||
return
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
reconnecting = false
|
||||
_fail("Nachfolge-Host nicht erreichbar")
|
||||
|
||||
|
||||
func _on_peer_connected(_id: int) -> void:
|
||||
@@ -104,14 +231,16 @@ func _on_peer_disconnected(id: int) -> void:
|
||||
var pname: String = names.get(id, "")
|
||||
names.erase(id)
|
||||
decks.erase(id)
|
||||
spectators.erase(id)
|
||||
if game != null and seats.has(id):
|
||||
var p: Game.Player = game.players[seats[id]]
|
||||
if game.winner == "":
|
||||
waiting[pname] = {"seat": seats[id], "since": Time.get_unix_time_from_system()}
|
||||
game.say("%s hat die Verbindung verloren, der Platz bleibt %d s frei." % [p.name, int(REJOIN_SECONDS)])
|
||||
game.say("%s hat die Verbindung verloren, der Platz bleibt %d s frei." % [p.name, int(rejoin_seconds)])
|
||||
seats.erase(id)
|
||||
_update_successor()
|
||||
action_applied.emit()
|
||||
lobby_update.rpc(names)
|
||||
lobby_update.rpc(names, spectators)
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
@@ -123,7 +252,7 @@ func tick() -> void:
|
||||
for pname in waiting.keys():
|
||||
var w: Dictionary = waiting[pname]
|
||||
var p: Game.Player = game.players[w.seat]
|
||||
if now - float(w.since) > REJOIN_SECONDS:
|
||||
if now - float(w.since) > rejoin_seconds:
|
||||
waiting.erase(pname)
|
||||
game.say("%s kehrt nicht zurück und scheidet aus." % p.name)
|
||||
game.damage(p, 999)
|
||||
@@ -137,10 +266,18 @@ func tick() -> void:
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func register(name: String, deck_ids: Array) -> void:
|
||||
func register(name: String, deck_ids: Array, as_spectator: bool = false) -> void:
|
||||
if not is_host:
|
||||
return
|
||||
var id := multiplayer.get_remote_sender_id()
|
||||
if as_spectator:
|
||||
spectators[id] = name
|
||||
lobby_update.rpc(names, spectators)
|
||||
lobby_changed.emit()
|
||||
if game != null:
|
||||
start_game.rpc_id(id, -1)
|
||||
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
||||
return
|
||||
if game != null:
|
||||
# Laufendes Spiel: nur Rückkehr auf einen freigehaltenen Platz
|
||||
if waiting.has(name):
|
||||
@@ -150,19 +287,21 @@ func register(name: String, deck_ids: Array) -> void:
|
||||
game.say("%s ist zurück." % name)
|
||||
start_game.rpc_id(id, seats[id])
|
||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
||||
_update_successor()
|
||||
action_applied.emit()
|
||||
else:
|
||||
status = "%s wollte beitreten, das Spiel läuft schon" % name
|
||||
return
|
||||
names[id] = name
|
||||
decks[id] = deck_ids
|
||||
lobby_update.rpc(names)
|
||||
lobby_update.rpc(names, spectators)
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func lobby_update(n: Dictionary) -> void:
|
||||
func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void:
|
||||
names = n
|
||||
spectators = sp
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
@@ -194,6 +333,9 @@ func start_battle(n_ai: int) -> void:
|
||||
for id in ids:
|
||||
if id != 1:
|
||||
start_game.rpc_id(id, seats[id])
|
||||
for id in spectators:
|
||||
start_game.rpc_id(id, -1)
|
||||
_update_successor()
|
||||
started.emit(my_seat)
|
||||
|
||||
|
||||
@@ -203,17 +345,24 @@ func start_game(seat: int) -> void:
|
||||
started.emit(seat)
|
||||
|
||||
|
||||
## Host: Zustand an alle Mitspieler senden (jeder aus seiner Sicht).
|
||||
## Host: Zustand an alle senden (Mitspieler aus ihrer Sicht, Zuschauer ohne Verborgenes,
|
||||
## der Nachfolger zusätzlich den vollen Spielstand).
|
||||
func broadcast() -> void:
|
||||
if not is_host or game == null:
|
||||
return
|
||||
for id in seats:
|
||||
if id != 1:
|
||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
||||
for id in spectators:
|
||||
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
||||
var succ := _successor_id()
|
||||
if succ > 0:
|
||||
receive_backup.rpc_id(succ, game.snapshot(-1), _seat_names())
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func receive_snapshot(snap: Dictionary) -> void:
|
||||
last_winner = str(snap.get("winner", ""))
|
||||
snapshot_received.emit(snap)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user