Elemente, Kombos, Fallen, KI mit Vorausschau, Kampagne, Hotseat und Netzwerk, Werkstatt, Erfolge, Tagesaufgaben, Tests und Exporte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
280 lines
8.3 KiB
GDScript
280 lines
8.3 KiB
GDScript
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}
|