extends CanvasLayer ## 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 var zurueck_btn: Button ## "Zurück in Raum XY" nach einem Abbruch var solo_stufe: OptionButton var lobby_stufe: OptionButton # Kampagne var kamp_kopf: Label var karte: Control # Mehrspieler var name_edit: LineEdit var code_edit: LineEdit # Lobby var lobby_bots: NumberRow var code_label: Label var lobby_list: Label var start_btn: Button func _ready() -> void: var root := Control.new() root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(root) var bg := ColorRect.new() bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) bg.color = Color(0.05, 0.06, 0.09) root.add_child(bg) 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()) screens["regeln"] = _screen(root, _build_regeln()) screens["kampagne"] = _screen(root, _build_kampagne()) Net.lobby_changed.connect(_refresh) Net.connection_failed.connect(_on_failed) Net.server_closed.connect(_on_closed) if Kampagne.zur_karte: Kampagne.zur_karte = false _show("kampagne") else: _show("main") # ------------------------------------------------------------ Aufbau func _screen(root: Control, content: Control) -> CenterContainer: var cc := CenterContainer.new() cc.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) cc.add_child(content) root.add_child(cc) return cc func _show(which: String) -> void: current = which for key: String in screens: screens[key].visible = key == which _sagen("") if which == "kampagne": _karte_fuellen() if which == "main" and zurueck_btn != null: zurueck_btn.visible = not Net.letzter_raum.is_empty() if zurueck_btn.visible: zurueck_btn.text = "Zurück in Raum %s" % Net.letzter_raum.code 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) v.add_theme_constant_override("separation", 10) return v func _text(t: String, size: int, col := Color.WHITE) -> Label: var l := Label.new() l.text = t l.add_theme_font_size_override("font_size", size) l.add_theme_color_override("font_color", col) l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER return l ## Gezeichnete Karte im Knete-Look: helle See, sandige Inseln unter jedem ## Hafen, eine gestrichelte Route von leicht nach schwer. class Seekarte extends Control: func _draw() -> void: var g := size draw_rect(Rect2(Vector2.ZERO, g), Color(0.56, 0.74, 0.88)) var sortiert: Array = Kampagne.kapitaene.duplicate() sortiert.sort_custom(func(a, b): return float(a.elo) < float(b.elo)) var letzte := Vector2(-1, -1) for k in sortiert: var p: Vector2 = Vector2(k.pos) * g if letzte.x >= 0.0: var n := 14 for i in n: if i % 2 == 0: draw_line(letzte.lerp(p, float(i) / n), letzte.lerp(p, float(i + 1) / n), Color(1, 1, 1, 0.45), 2.0) letzte = p for k in Kampagne.kapitaene: var p: Vector2 = Vector2(k.pos) * g var da: bool = int(k.weg) == 0 draw_circle(p + Vector2(0, 6), 62.0, Color(0.0, 0.0, 0.0, 0.10)) draw_circle(p, 62.0, Color(0.92, 0.84, 0.68) if da else Color(0.75, 0.78, 0.82)) draw_circle(p + Vector2(-10, -30), 16.0, Color(0.35, 0.72, 0.44) if da else Color(0.6, 0.65, 0.66)) draw_circle(p + Vector2(14, -34), 11.0, Color(0.35, 0.72, 0.44) if da else Color(0.6, 0.65, 0.66)) ## Die Seekarte: Kapitäne als Häfen. Wer auf See ist, steht ausgegraut da ## mit der Zahl der Partien bis zur Rückkehr. func _build_kampagne() -> Control: var box := _box(940) box.add_child(_text("Seekarte", 26, Color(1.0, 0.82, 0.32))) kamp_kopf = _text("", 15, Color(0.85, 0.87, 0.92)) box.add_child(kamp_kopf) karte = Seekarte.new() karte.custom_minimum_size = Vector2(920, 440) box.add_child(karte) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) box.add_child(row) _button("Neu anfangen", row, func() -> void: Kampagne.zuruecksetzen() Kampagne.speichern() _karte_fuellen()) _button("Zurück", row, _show.bind("main")) return box func _karte_fuellen() -> void: kamp_kopf.text = "Du: %s, Wertung %d · %d Partien, %d Siege" % [ Kampagne.mein_rang(), int(round(Kampagne.elo)), Kampagne.partien, Kampagne.siege] for c in karte.get_children(): if c is Button: karte.remove_child(c) c.queue_free() karte.queue_redraw() var groesse := karte.custom_minimum_size for k in Kampagne.kapitaene: var b := Button.new() var weg := int(k.weg) if weg > 0: b.text = "%s\nauf See · zurück in %d" % [k.name, weg] b.disabled = true else: b.text = "%s\n%s · %d" % [k.name, ["leicht", "normal", "schwer"][int(k.stufe)], int(round(k.elo))] b.pressed.connect(_on_kapitaen.bind(String(k.name))) b.custom_minimum_size = Vector2(170, 52) var p: Vector2 = k.pos b.position = Vector2(p.x * groesse.x - 85, p.y * groesse.y + 4) karte.add_child(b) func _on_kapitaen(name: String) -> void: Kampagne.starten(name) func _on_wieder_rein() -> void: _show("join") _sagen("Verbinde mit Raum %s …" % Net.letzter_raum.get("code", "")) Net.wieder_rein() ## Leicht, normal, schwer - stellt Bluff-Quote, Genau-Schwelle und Vorsicht ## der Bots ein. func _stufen_wahl() -> OptionButton: var o := OptionButton.new() for name in ["leicht", "normal", "schwer"]: o.add_item(name) o.selected = 1 o.add_theme_font_size_override("font_size", 16) return o func _row(caption: String, ctrl: Control) -> HBoxContainer: var h := HBoxContainer.new() var l := Label.new() l.text = caption l.custom_minimum_size = Vector2(150, 0) h.add_child(l) ctrl.size_flags_horizontal = Control.SIZE_EXPAND_FILL h.add_child(ctrl) return h func _button(text: String, parent: Control, action: Callable) -> Button: var b := Button.new() b.text = text b.custom_minimum_size = Vector2(0, 42) b.size_flags_horizontal = Control.SIZE_EXPAND_FILL b.pressed.connect(action) parent.add_child(b) 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: var value := 0 var _buttons: Array[Button] = [] var _lo := 0 func _init(lo: int, hi: int, start: int) -> void: _lo = lo alignment = BoxContainer.ALIGNMENT_CENTER add_theme_constant_override("separation", 6) for n in range(lo, hi + 1): var b := Button.new() b.text = str(n) b.toggle_mode = true b.custom_minimum_size = Vector2(40, 38) b.pressed.connect(pick.bind(n)) add_child(b) _buttons.append(b) pick(start) func pick(n: int) -> void: value = n for i in _buttons.size(): _buttons[i].button_pressed = _lo + i == n ## 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 = not on or _lo + i > hi if value > hi: pick(maxi(_lo, hi)) func _title(box: VBoxContainer) -> void: box.add_child(_text("LIAR'S DICE 3D", 40)) box.add_child(HSeparator.new()) func _build_main() -> Control: var box := _box(360) _title(box) # Nach einem Verbindungsabbruch: derselbe Name, derselbe Code, ein Klick. zurueck_btn = _button("", box, _on_wieder_rein) zurueck_btn.visible = false _button("Kampagne", box, _show.bind("kampagne")) _button("Einzelspieler", box, _show.bind("single")) _button("Mehrspieler", box, _show.bind("multi")) _button("Regeln", box, _show.bind("regeln")) _button("Beenden", box, func() -> void: get_tree().quit()) return box ## Die Regeln auf einer Seite. Wer die Münzen nicht kennt, wüsste sonst im ## Spiel nur, was der Tooltip verrät. func _build_regeln() -> Control: var box := _box(680) box.add_child(_text("Regeln", 26, Color(1.0, 0.82, 0.32))) var absaetze := [ ["Würfeln", "Jeder hat fünf Würfel unter dem Becher. Nur die eigenen sieht man."], ["Ansagen", "Reihum sagt man, wie viele Würfel einer Augenzahl auf dem ganzen Tisch liegen - zum Beispiel „vier Dreien\". Wer dran ist, muss höher gehen: mehr Würfel, oder gleich viele mit höherer Augenzahl. Oder er zweifelt."], ["Anfechten", "Stimmt die Ansage nicht, verliert der Ansager einen Würfel. Stimmt sie, verliert der Zweifler einen. Mehr als angesagt zählt als richtig."], ["Genau", "Wer glaubt, die Ansage trifft exakt, ruft „Genau\". Trifft es, verliert der Ansager einen Würfel; hat der Rufer weniger Würfel als der Ansager, bekommt er einen dazu. Daneben verliert der Rufer einen."], ["Münzen", "Jeder startet mit einer Münze; wer einen Würfel verliert, bekommt eine. Geworfen wird nur am eigenen Zug, vor der Ansage. Wappen verstärkt die Wirkung, Zahl lässt es bei der Grundwirkung - die kommt immer."], ["Topf", "Geworfene Münzen liegen in der Mitte. Wer das Aufdecken gewinnt, nimmt sie. Mehr als vier passen nicht in den Vorrat; der Rest bleibt liegen. Im Topf liegen höchstens sechs - die älteste fällt vom Tisch."], ["Ende", "Wer keine Würfel mehr hat, schaut zu. Der Letzte gewinnt."], ] for b in BoosterDB.ALL: absaetze.append([String(b.name), "%s Wappen: %s" % [b.desc, b.stark]]) for a: Array in absaetze: var h := HBoxContainer.new() h.add_theme_constant_override("separation", 12) var k := _text(String(a[0]), 15, Color(1.0, 0.82, 0.32)) k.custom_minimum_size = Vector2(110, 0) k.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT k.vertical_alignment = VERTICAL_ALIGNMENT_TOP h.add_child(k) var t := _text(String(a[1]), 15, Color(0.85, 0.87, 0.92)) t.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT t.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART t.size_flags_horizontal = Control.SIZE_EXPAND_FILL h.add_child(t) box.add_child(h) _button("Zurück", box, _show.bind("main")) # Mit den Münzen passt die Seite nicht mehr auf 720 Zeilen: scrollen. var sc := ScrollContainer.new() sc.custom_minimum_size = Vector2(720, 560) sc.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED box.size_flags_horizontal = Control.SIZE_EXPAND_FILL sc.add_child(box) return sc func _build_single() -> Control: var box := _box(400) _title(box) box.add_child(_text("Einzelspieler", 22, Color(1.0, 0.82, 0.32))) box.add_child(_text("Bots", 15)) solo_bots = NumberRow.new(1, BOT_NAMES_MAX, 3) box.add_child(solo_bots) solo_stufe = _stufen_wahl() box.add_child(_row("Bots spielen", solo_stufe)) box.add_child(HSeparator.new()) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) box.add_child(row) _button("Starten", row, _on_solo_start) _button("Zurück", row, _show.bind("main")) return box ## Erster Schritt: nur der Name, dann hosten oder beitreten. func _build_multi() -> Control: var box := _box(440) _title(box) box.add_child(_text("Mehrspieler", 22, Color(1.0, 0.82, 0.32))) name_edit = LineEdit.new() name_edit.text = "Spieler" name_edit.placeholder_text = "Dein Name" box.add_child(_row("Name", name_edit)) var brow := HBoxContainer.new() brow.add_theme_constant_override("separation", 8) box.add_child(brow) _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()) lobby_list = _text("-", 16, Color(0.85, 0.87, 0.92)) box.add_child(lobby_list) box.add_child(HSeparator.new()) box.add_child(_text("Bots", 15)) lobby_bots = NumberRow.new(0, BOT_NAMES_MAX, 1) box.add_child(lobby_bots) lobby_stufe = _stufen_wahl() box.add_child(_row("Bots spielen", lobby_stufe)) 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 # ------------------------------------------------------------ Aktionen func _on_solo_start() -> void: Kampagne.kampagnenpartie = false Net.start_singleplayer("Spieler", solo_bots.value, solo_stufe.selected) ## Ein leeres Namensfeld ergäbe einen namenlosen Platz am Tisch. func _spielername() -> String: var n := name_edit.text.strip_edges() return n if n != "" else "Spieler" func _on_host() -> void: _sagen("Raum wird angelegt ...") Net.create_room(_spielername()) func _on_join() -> void: _sagen("Verbinde ...") Net.join_room(code_edit.text, _spielername()) func _on_leave() -> void: Net.leave() _show("multi") func _on_start() -> void: var humans: int = Net.lobby.size() var ai: int = mini(lobby_bots.value, Net.MAX_HUMANS - humans) if humans + ai < 2: _sagen("Mindestens 2 Spieler nötig (Mitspieler oder Bots).") return Net.request_start(ai, lobby_stufe.selected) func _on_failed(msg: String) -> void: _sagen(msg) func _on_closed() -> void: _show("multi") _sagen("Der Raum wurde geschlossen.") ## Sobald der Server uns kennt, sind wir in der Lobby. func _refresh() -> void: if lobby_list == null: return 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) lobby_stufe.disabled = not 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.")