Dinger: Kartenspiel in Godot 4.7
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
This commit is contained in:
279
scripts/net.gd
Normal file
279
scripts/net.gd
Normal file
@@ -0,0 +1,279 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user