Files
cards/scripts/ui/deck_screen.gd
Team3 8944fdd017
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled
update
2026-09-19 13:37:12 +02:00

423 lines
16 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name DeckScreen
extends Control
## Deckbau mit fünf Slots, Suche, Sortierung und fehlenden Karten.
const SORTS := ["Kosten", "Name", "Element", "Seltenheit"]
var app
var slot := 0
var deck: Array = []
var filter := ""
var search := ""
var sort_mode := 0
var show_missing := false
var msg_label: Label
var grid: CardGrid
var deck_list: Control
var deck_label: Label
var elems_label: Label
var valid_label: Label
var active_label: Label
var curve: Control
var preview: CardView
var search_box: LineEdit
var sort_box: OptionButton
var missing_box: CheckButton
var slot_buttons: Array = []
var b_activate: Button
var b_autofill: Button
var name_box: LineEdit
var confirm: Control
var code_dialog: Control
var code_box: LineEdit
class DeckRow extends Control:
signal clicked(card)
signal hover(card, entered: bool)
var card: Cards.Card
var count: int
var lit := false
func _init(c: Cards.Card, n: int, sz: Vector2) -> void:
card = c
count = n
size = sz
custom_minimum_size = sz
mouse_filter = Control.MOUSE_FILTER_STOP
focus_mode = Control.FOCUS_ALL
var light := func(on: bool):
lit = on
queue_redraw()
hover.emit(card, on)
mouse_entered.connect(light.bind(true))
mouse_exited.connect(light.bind(false))
focus_entered.connect(light.bind(true))
focus_exited.connect(light.bind(false))
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
accept_event()
clicked.emit(card)
elif has_focus() and event.is_action_pressed("ui_accept"):
accept_event()
clicked.emit(card)
func _draw() -> void:
if lit:
draw_style_box(Style.box(Style.PANEL_LIGHT, 4), Rect2(Vector2.ZERO, size))
for k in card.elements.size():
draw_circle(Vector2(10 + k * 12, size.y / 2), 7, Cards.ELEMENT_COLORS[card.elements[k]])
Style.text(self, "%dx %s" % [count, card.title()], 15, Style.TEXT, Vector2(40, size.y / 2), "midleft")
Style.text(self, str(card.cost), 15, Style.GOLD, Vector2(size.x - 10, size.y / 2), "midright", true)
class EnergyCurve extends Control:
var cards: Array = []
func _draw() -> void:
Style.text(self, "Energiekurve", 12, Style.MUTED, Vector2(0, 0))
var counts := {}
for c in cards:
var k: int = mini(c.cost, 8)
counts[k] = counts.get(k, 0) + 1
for i in range(1, 9):
var n: int = counts.get(i, 0)
var x := (i - 1) * 48
draw_rect(Rect2(x, 104 - n * 5, 36, n * 5), Style.BLUE)
if n > 0:
Style.text(self, str(n), 11, Style.TEXT, Vector2(x + 18, 102 - n * 5), "midbottom")
Style.text(self, str(i) + ("+" if i == 8 else ""), 12, Style.MUTED, Vector2(x + 18, 108), "midtop")
func _ready() -> void:
slot = app.profile.active_deck
deck = app.profile.decks[slot].cards.duplicate()
Style.label(self, "Deck bauen", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
Style.label(self, "Klick: Karte ins Deck. Klick in der Liste: Karte entfernen. Mausrad: scrollen.",
14, Style.MUTED, Rect2(300, 18, 700, 24))
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
for i in Cards.ELEMENTS.size():
var e: String = Cards.ELEMENTS[i]
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
fb.add_theme_font_size_override("font_size", 14)
search_box = LineEdit.new()
search_box.position = Vector2(424, 55)
search_box.size = Vector2(136, 34)
search_box.placeholder_text = "Suche"
search_box.text_changed.connect(func(t: String):
search = t
refresh())
add_child(search_box)
sort_box = OptionButton.new()
sort_box.position = Vector2(570, 55)
sort_box.size = Vector2(130, 34)
for s in SORTS:
sort_box.add_item(s)
sort_box.item_selected.connect(func(i: int):
sort_mode = i
refresh())
add_child(sort_box)
missing_box = CheckButton.new()
missing_box.position = Vector2(710, 58)
missing_box.text = "Fehlende"
missing_box.toggled.connect(func(on: bool):
show_missing = on
refresh())
add_child(missing_box)
grid = CardGrid.new(Rect2(20, 100, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 690))
grid.card_clicked.connect(add)
grid.card_hover.connect(show_preview)
add_child(grid)
# Rechte Seite: Slots, Deckliste, Kurve
Style.panel(self, Rect2(845, 50, 420, 680))
for i in Profile.DECK_SLOTS:
slot_buttons.append(Style.button(self, str(i + 1), Rect2(860 + i * 46, 58, 40, 32), select_slot.bind(i)))
b_activate = Style.button(self, "Aktiv setzen", Rect2(1100, 58, 150, 32), activate)
name_box = LineEdit.new()
name_box.position = Vector2(860, 98)
name_box.size = Vector2(200, 30)
name_box.max_length = 20
name_box.text_submitted.connect(func(t: String): rename(t))
name_box.focus_exited.connect(func(): rename(name_box.text))
add_child(name_box)
deck_label = Style.label(self, "", 22, Style.TEXT, Rect2(1070, 98, 120, 30), "left", true)
active_label = Style.label(self, "", 14, Style.GREEN, Rect2(1180, 104, 75, 24), "right")
b_autofill = Style.button(self, "Auto-Ausfüllen", Rect2(1100, 154, 150, 26), autofill)
b_autofill.add_theme_font_size_override("font_size", 13)
elems_label = Style.label(self, "", 16, Style.MUTED, Rect2(860, 130, 390, 24))
valid_label = Style.label(self, "", 14, Style.GREEN, Rect2(860, 156, 235, 22))
deck_list = Control.new()
deck_list.position = Vector2(860, 182)
deck_list.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(deck_list)
curve = EnergyCurve.new()
curve.position = Vector2(860, 604)
curve.size = Vector2(400, 125)
curve.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(curve)
msg_label = Style.label(self, "", 16, Style.GREEN, Rect2(860, 736, 400, 24))
Style.button(self, "Speichern", Rect2(860, 740, 140, 44), save_deck, Color("285a3c"))
Style.button(self, "Leeren", Rect2(1008, 740, 70, 44), clear_deck)
Style.button(self, "Code", Rect2(1086, 740, 84, 44), open_code)
Style.button(self, "Zurück", Rect2(1178, 740, 82, 44), leave)
# Deck-Code zum Teilen: kopieren, einfügen, übernehmen
code_dialog = Style.overlay(self)
Style.panel(code_dialog, Rect2(240, 270, 800, 240), Style.PANEL, 10, Style.GOLD, 2)
Style.label(code_dialog, "Deck-Code", 26, Style.TEXT, Rect2(240, 290, 800, 40), "center", true)
Style.label(code_dialog, "Zum Teilen kopieren oder einen Code einfügen. Nicht vorhandene Karten werden weggelassen.", 15, Style.MUTED, Rect2(240, 334, 800, 24), "center")
code_box = LineEdit.new()
code_box.position = Vector2(270, 368)
code_box.size = Vector2(740, 36)
code_dialog.add_child(code_box)
Style.button(code_dialog, "In Zwischenablage", Rect2(270, 430, 180, 44), func(): DisplayServer.clipboard_set(code_box.text))
Style.button(code_dialog, "Aus Zwischenablage", Rect2(460, 430, 180, 44), func(): code_box.text = DisplayServer.clipboard_get())
Style.button(code_dialog, "Übernehmen", Rect2(650, 430, 170, 44), func(): apply_code(code_box.text), Color("285a3c"))
Style.button(code_dialog, "Schließen", Rect2(830, 430, 180, 44), func(): code_dialog.visible = false)
confirm = Style.confirm(self, "Ungespeicherte Änderungen", "Das Deck wurde geändert, aber nicht gespeichert.",
"Verwerfen", func(): app.show_screen(MenuScreen.new()), "Weiter bearbeiten", Style.GOLD)
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview.visible = false
add_child(preview)
refresh()
func dirty() -> bool:
return deck != app.profile.decks[slot].cards
func leave() -> void:
if dirty():
confirm.visible = true
else:
app.show_screen(MenuScreen.new())
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel") and not search_box.has_focus() and not name_box.has_focus() and not code_box.has_focus():
if confirm.visible:
confirm.visible = false
elif code_dialog.visible:
code_dialog.visible = false
else:
leave()
func _process(_delta: float) -> void:
if preview.visible:
var pos := get_local_mouse_position() + Vector2(16, 16)
pos.x = clampf(pos.x, 0, 1280 - preview.size.x)
pos.y = clampf(pos.y, 0, 800 - preview.size.y)
preview.position = pos
func set_filter(e: String) -> void:
filter = e
refresh()
func select_slot(i: int) -> void:
slot = i
deck = app.profile.decks[slot].cards.duplicate()
say("", true)
refresh()
func rename(t: String) -> void:
if t.strip_edges() == app.profile.deck_name(slot):
return
var err: String = app.profile.rename_deck(slot, t)
if err == "":
app.save()
say("Umbenannt in %s" % app.profile.deck_name(slot), true)
else:
say(err, false)
refresh()
func autofill() -> void:
app.profile.decks[slot].cards = deck.duplicate()
var err: String = app.profile.autofill_deck(slot)
deck = app.profile.decks[slot].cards.duplicate()
if err == "":
app.save()
say("Aufgefüllt und gespeichert", true)
else:
say(err, false)
refresh()
func activate() -> void:
var ok: Array = Game.validate_deck(deck_cards(), app.profile.collection)
if not ok[0]:
say("Erst ein spielbares Deck speichern", false)
return
app.profile.decks[slot].cards = deck.duplicate()
app.profile.active_deck = slot
app.save()
say("%s ist jetzt das aktive Deck" % app.profile.deck_name(slot), true)
refresh()
## Karten der Sammlung (oder alle, wenn Fehlende gezeigt werden), gefiltert und sortiert.
func cards() -> Array:
var out: Array = Cards.all().duplicate() if show_missing else app.profile.owned_cards()
if filter != "":
out = out.filter(func(c): return filter in c.elements)
if search != "":
var q := search.to_lower()
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
match sort_mode:
0:
out.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
1:
out.sort_custom(func(a, b): return a.name < b.name)
3:
out.sort_custom(func(a, b):
var ra := Cards.RARITIES.find(a.rarity)
var rb := Cards.RARITIES.find(b.rarity)
return ra > rb or (ra == rb and a.cost < b.cost))
return out
func say(m: String, ok: bool) -> void:
msg_label.text = m
msg_label.add_theme_color_override("font_color", Style.GREEN if ok else Style.RED)
func add(card: Cards.Card) -> void:
var owned: int = app.profile.collection.get(card.id, 0)
var have := deck.count(card.id)
if owned == 0:
say("%s fehlt in deiner Sammlung" % card.title(), false)
elif deck.size() >= Game.DECK_SIZE:
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
elif have >= mini(owned, Game.MAX_COPIES):
say("Nicht mehr Kopien möglich (max. %d)" % mini(owned, Game.MAX_COPIES), false)
else:
var elems := Game.deck_elements(deck_cards())
for e in card.elements:
if not (e in elems):
elems.append(e)
if elems.size() > Game.MAX_DECK_ELEMENTS:
say("Höchstens %d Elemente im Deck" % Game.MAX_DECK_ELEMENTS, false)
else:
deck.append(card.id)
app.sfx.play("click")
say("", true)
refresh()
func remove(card: Cards.Card) -> void:
deck.erase(card.id)
say("", true)
refresh()
func clear_deck() -> void:
deck = []
refresh()
func save_deck() -> void:
var ok: Array = Game.validate_deck(deck_cards(), app.profile.collection)
if ok[0]:
app.profile.decks[slot].cards = deck.duplicate()
app.save()
say("%s gespeichert" % app.profile.deck_name(slot), true)
else:
say(ok[1], false)
refresh()
func open_code() -> void:
code_box.text = Profile.deck_code(deck)
code_dialog.visible = true
## Übernimmt einen Deck-Code; Karten, die in der Sammlung fehlen, werden gemeldet und weggelassen.
func apply_code(code: String) -> void:
var ids: Array = Profile.parse_deck_code(code)
if ids.is_empty():
say("Ungültiger Code", false)
return
var p: Profile = app.profile
var missing := {}
deck = []
for cid in ids:
var owned: int = p.collection.get(cid, 0)
if deck.count(cid) < owned and deck.size() < Game.DECK_SIZE:
deck.append(cid)
else:
missing[cid] = true
code_dialog.visible = false
if missing.is_empty():
say("Code übernommen", true)
else:
say(tr("Code übernommen, es fehlen: ") + ", ".join(missing.keys().map(func(cid): return Cards.by_id(cid).title())), false)
refresh()
func deck_cards() -> Array:
var out := []
for cid in deck:
out.append(Cards.by_id(cid))
return out
func show_preview(card: Cards.Card, entered: bool) -> void:
if entered:
preview.card = card
preview.visible = true
preview.queue_redraw()
elif preview.card == card:
preview.visible = false
func refresh() -> void:
var p: Profile = app.profile
grid.fill(cards(),
func(c):
var owned: int = p.collection.get(c.id, 0)
return "fehlt" if owned == 0 else "%d/%d im Deck" % [deck.count(c.id), owned],
func(c):
var owned: int = p.collection.get(c.id, 0)
return owned == 0 or deck.count(c.id) >= mini(owned, Game.MAX_COPIES))
for i in slot_buttons.size():
var b: Button = slot_buttons[i]
var col: Color = Style.GOLD if i == slot else (Style.GREEN if p.deck_valid(i)[0] else Style.MUTED)
b.add_theme_stylebox_override("normal", Style.box(Style.PANEL_LIGHT if i != slot else Style.darker(Style.GOLD, 0.45), 6, col, 2))
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Style.PANEL_LIGHT), 6, col, 2))
active_label.text = "aktiv" if slot == p.active_deck else ""
# Deckliste
Style.clear(deck_list)
var counts := {}
for cid in deck:
counts[cid] = counts.get(cid, 0) + 1
var listed: Array = counts.keys().map(func(cid): return Cards.by_id(cid))
listed.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
for i in listed.size():
var c: Cards.Card = listed[i]
var row := DeckRow.new(c, counts[c.id], Vector2(400, 20))
row.position = Vector2(0, i * 21)
row.clicked.connect(remove)
row.hover.connect(show_preview)
deck_list.add_child(row)
var dc := deck_cards()
if not name_box.has_focus():
name_box.text = p.deck_name(slot)
deck_label.text = "%d/%d" % [deck.size(), Game.DECK_SIZE]
deck_label.tooltip_text = tr("Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten")
deck_label.add_theme_color_override("font_color", Style.GREEN if deck.size() == Game.DECK_SIZE else Style.TEXT)
var elems := Game.deck_elements(dc)
elems.sort_custom(func(a, b): return Cards.ELEMENTS.find(a) < Cards.ELEMENTS.find(b))
elems_label.text = tr("Elemente: ") + (", ".join(elems.map(func(e): return tr(e))) if not elems.is_empty() else "–")
if not dc.is_empty():
elems_label.text += " · " + tr("Stärke %.1f") % Game.deck_rating(dc)
var ok: Array = Game.validate_deck(dc, p.collection)
valid_label.text = "spielbar" if ok[0] else ok[1]
valid_label.add_theme_color_override("font_color", Style.GREEN if ok[0] else Style.ORANGE)
curve.cards = dc
curve.queue_redraw()