Files
pirate-dice/scripts/menu.gd
2026-09-16 17:55:28 +02:00

487 lines
15 KiB
GDScript

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.")