class_name Net extends Node ## LAN-Mehrspieler: der Host führt das Spiel, Mitspieler senden Aktionen und erhalten ## Zustandsbilder aus ihrer Sicht (fremde Hände bleiben verborgen). signal lobby_changed signal started(seat: int) signal snapshot_received(snap: Dictionary) signal action_applied signal ended(reason: String) const PORT := 24455 var peer: ENetMultiplayerPeer var is_host := false var connected := false var my_name := "Spieler" var my_deck: Array = [] # Karten-IDs var names := {} # peer_id -> Name (nur im Host vollständig, sonst Kopie) var decks := {} # peer_id -> Karten-IDs (Host) var seats := {} # peer_id -> Spielerindex (Host) 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 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")) func host(port: int = PORT) -> Error: peer = ENetMultiplayerPeer.new() var err := peer.create_server(port, Game.MAX_OPPONENTS) if err != OK: status = "Kann nicht hosten (Port %d belegt?)" % port return err multiplayer.multiplayer_peer = peer is_host = true connected = true names = {1: my_name} decks = {1: my_deck} status = "Warte auf Mitspieler …" lobby_changed.emit() return OK func join(ip: String, port: int = PORT) -> Error: peer = ENetMultiplayerPeer.new() var err := peer.create_client(ip, port) if err != OK: status = "Adresse ungültig" return err multiplayer.multiplayer_peer = peer is_host = false status = "Verbinde mit %s …" % ip return OK func leave() -> void: if multiplayer.multiplayer_peer != null: multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() peer = null is_host = false connected = false names = {} decks = {} seats = {} waiting = {} my_seat = -1 game = null status = "" func local_addresses() -> Array: return Array(IP.get_local_addresses()).filter(func(a): return "." in a and not a.begins_with("127.")) func _fail(reason: String) -> void: status = reason ended.emit(reason) leave() func _on_connected() -> void: connected = true status = "Verbunden" register.rpc_id(1, my_name, my_deck) func _on_peer_connected(_id: int) -> void: pass func _on_peer_disconnected(id: int) -> void: if not is_host: return var pname: String = names.get(id, "") names.erase(id) decks.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)]) seats.erase(id) action_applied.emit() lobby_update.rpc(names) lobby_changed.emit() ## Host, je Bild: wartende Spieler überspringen; nach Ablauf der Frist scheiden sie aus. func tick() -> void: if not is_host or game == null or game.winner != "": return var now := Time.get_unix_time_from_system() 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: waiting.erase(pname) game.say("%s kehrt nicht zurück und scheidet aus." % p.name) game.damage(p, 999) game.check_end() action_applied.emit() elif game.current() == p: p.mulligan_done = true game.say("%s ist nicht da, der Zug wird übersprungen." % p.name) game.end_turn() action_applied.emit() @rpc("any_peer", "reliable") func register(name: String, deck_ids: Array) -> void: if not is_host: return var id := multiplayer.get_remote_sender_id() if game != null: # Laufendes Spiel: nur Rückkehr auf einen freigehaltenen Platz if waiting.has(name): seats[id] = waiting[name].seat names[id] = name waiting.erase(name) game.say("%s ist zurück." % name) start_game.rpc_id(id, seats[id]) receive_snapshot.rpc_id(id, game.snapshot(seats[id])) 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_changed.emit() @rpc("authority", "reliable") func lobby_update(n: Dictionary) -> void: names = n lobby_changed.emit() ## Host: Spiel mit allen Verbundenen und n_ai KI-Gegnern starten. func start_battle(n_ai: int) -> void: var ids: Array = names.keys() ids.sort() var r := Game.make_rng() var players := [] seats = {} for id in ids: var deck_ids: Array = decks.get(id, []) var deck := [] for cid in deck_ids: if Cards.has_id(cid): deck.append(Cards.by_id(cid)) if Game.validate_deck(deck)[0] == false: var pool: Array = Cards.ELEMENTS.duplicate() Game.shuffle(pool, r) deck = Game.build_opponent_deck(r, [pool[0], pool[1]]) players.append(Game.Player.new(names[id], deck, true, Game.START_LIFE)) for i in n_ai: players.append(Game.random_opponent(r, i + 1)) game = Game.new(players[0], players.slice(1), r) game.start() for id in ids: seats[id] = game.players.find(players[ids.find(id)]) my_seat = seats[1] for id in ids: if id != 1: start_game.rpc_id(id, seats[id]) started.emit(my_seat) @rpc("authority", "reliable") func start_game(seat: int) -> void: my_seat = seat started.emit(seat) ## Host: Zustand an alle Mitspieler senden (jeder aus seiner Sicht). 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])) @rpc("authority", "reliable") func receive_snapshot(snap: Dictionary) -> void: snapshot_received.emit(snap) ## Mitspieler: Aktion an den Host schicken. func send_action(action: Dictionary) -> void: if is_host: _apply(1, action) else: request_action.rpc_id(1, action) @rpc("any_peer", "reliable") func request_action(action: Dictionary) -> void: if not is_host: return _apply(multiplayer.get_remote_sender_id(), action) func _apply(id: int, action: Dictionary) -> void: if game == null or not seats.has(id): return var p: Game.Player = game.players[seats[id]] if game.current() != p or game.winner != "": return match str(action.get("type", "")): "play": var idx := int(action.get("hand", -1)) if idx < 0 or idx >= p.hand.size(): return game.play_card(p, p.hand[idx], resolve_ref(action.get("target"))) "attack": var cr := game.find_creature(int(action.get("uid", -1))) var t = resolve_ref(action.get("target")) if cr != null and t != null: game.attack(cr, t) "mulligan": var cards := [] for i in action.get("hand", []): if int(i) >= 0 and int(i) < p.hand.size(): cards.append(p.hand[int(i)]) game.mulligan(p, cards) "end": game.end_turn() _: return action_applied.emit() func resolve_ref(ref): if ref == null or not (ref is Dictionary): return null if ref.get("kind") == "creature": return game.find_creature(int(ref.get("uid", -1))) var i := int(ref.get("index", -1)) if i >= 0 and i < game.players.size(): return game.players[i] return null static func ref_dict(target) -> Dictionary: if target is Game.Creature: return {"kind": "creature", "uid": target.uid} return {"kind": "player", "index": target.order}